Skip to content

Commit c1354a1

Browse files
committed
Stability improvements
1 parent 2be05e3 commit c1354a1

12 files changed

+2583
-110
lines changed

.eslintrc

-13
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,5 @@
1010
"import/no-dynamic-require": "warn",
1111
"global-require": "warn",
1212
"no-continue": "off"
13-
},
14-
"settings": {
15-
"import/resolver": {
16-
"node": {
17-
"extensions": [
18-
".js"
19-
],
20-
"moduleDirectory": [
21-
"node_modules",
22-
"."
23-
]
24-
}
25-
}
2613
}
2714
}

.nvmrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
stable
1+
10.10

README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Your Webpack config
2323
Babel plugin traverses all ObjectExpression nodes in order to find messages object.
2424
When a matching object is found, it extracts useful data (such as IDs) and assigns it to file's context, which is read and processed by Webpack plugin later on.
2525
Once bundle is about to be emitted, we slice the languages to separate files and make some optimization.
26-
Each language file can be accessed un `messages/{langCode}.json`.
26+
Each language file can be accessed at `messages/{langCode}.json`.
2727

2828
## Features
2929

@@ -38,6 +38,17 @@ By default, react-intl-optimizer does not remove unused message pairs, as it's s
3838
* add some documentation
3939
* tests
4040

41+
42+
## Caveats
43+
44+
* No dynamic ID resolution
45+
* No multiple threads support
46+
47+
48+
## Kudos
49+
50+
51+
4152
## LICENSE
4253

4354
[MIT](https://github.com/P0lip/react-intl-optimizer/blob/master/LICENSE)

package.json

+10-4
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,30 @@
2828
"scripts": {
2929
"build": "NODE_ENV=production rollup -c",
3030
"lint": "eslint --cache --cache-location .cache/ src/",
31+
"webpack": "webpack-cli",
3132
"test": "exit 0",
32-
"prepublish": "yarn lint && yarn test && yarn build"
33+
"prepublish": "yarn lint && yarn test && yarn build",
34+
"postpublish": "rm babel.js && rm.webpack.js"
35+
},
36+
"dependencies": {
37+
"uniqid": "5.0.3"
3338
},
34-
"dependencies": {},
3539
"devDependencies": {
3640
"@babel/core": "7.1.2",
3741
"@babel/plugin-external-helpers": "7.0.0",
38-
"@babel/plugin-proposal-object-rest-spread": "7.0.0",
3942
"@babel/plugin-syntax-dynamic-import": "7.0.0",
4043
"@babel/plugin-transform-modules-commonjs": "7.1.0",
4144
"@babel/preset-env": "7.1.0",
4245
"babel-core": "^7.0.0-0",
4346
"babel-eslint": "10.0.1",
47+
"babel-loader": "8.0.4",
4448
"babel-plugin-dynamic-import-node": "2.1.0",
4549
"eslint": "^5.6.1",
4650
"eslint-config-airbnb-base": "^13.1.0",
4751
"eslint-plugin-import": "^2.11.0",
4852
"rollup": "^0.66.4",
49-
"rollup-plugin-babel": "4.0.3"
53+
"rollup-plugin-babel": "4.0.3",
54+
"webpack": "4.21.0",
55+
"webpack-cli": "3.1.2"
5056
}
5157
}

src/babel/index.js

+23-12
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
1-
import { isValidMessagesShape, getMessages } from './utils';
1+
import {
2+
isValidMessagesShape,
3+
messagesObjectVisitor,
4+
fileMessages,
5+
} from './utils';
6+
import { METADATA_NAME } from '../consts';
27

3-
const MESSAGES = Symbol('ReactIntlMessages');
4-
5-
// eslint-disable-next-line no-unused-vars
6-
export default function ({ types: t }) {
8+
export default function () {
79
return {
810
post(file) {
9-
file.metadata['react-intl'] = file[MESSAGES];
11+
const messages = fileMessages.get(file);
12+
13+
if (messages !== undefined) {
14+
file.metadata[METADATA_NAME] = messages;
15+
}
1016
},
1117

1218
visitor: {
13-
ObjectExpression(path, state) {
19+
CallExpression(path, { file }) {
20+
const callee = path.get('callee');
21+
22+
if (callee.referencesImport('react-intl', 'defineMessages')) {
23+
path.traverse(messagesObjectVisitor, { file });
24+
path.skip();
25+
}
26+
},
27+
28+
ObjectExpression(path, { file }) {
1429
if (isValidMessagesShape(path)) {
15-
if (state.file[MESSAGES] !== undefined) {
16-
state.file[MESSAGES].push(...getMessages(path));
17-
} else {
18-
state.file[MESSAGES] = getMessages(path);
19-
}
30+
path.traverse(messagesObjectVisitor, { file });
2031
}
2132
},
2233
},

src/babel/utils.js

+30-15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
import { getActualId } from '../mangler/utils';
2+
import { SUBSCRIBER_NAME } from '../consts';
3+
4+
export const fileMessages = new WeakMap();
5+
6+
const addMessage = (file, messageId) => {
7+
if (fileMessages.has(file)) {
8+
fileMessages.get(file).push(messageId);
9+
} else {
10+
fileMessages.set(file, [messageId]);
11+
}
12+
};
13+
114
export const isValidMessagesShape = (node) => {
215
if (!node.isObjectExpression()) {
316
return false;
@@ -17,11 +30,11 @@ export const isValidMessagesShape = (node) => {
1730
continue;
1831
}
1932

20-
if (prop.get('key').node.name === 'defaultMessage') {
33+
if (prop.get('key').isIdentifier({ name: 'defaultMessage' })) {
2134
continue;
2235
}
2336

24-
if (prop.get('key').node.name === 'id' && prop.get('value').isStringLiteral() && typeof prop.get('value').node.value === 'string') {
37+
if (prop.get('key').isIdentifier({ name: 'id' }) && prop.get('value').isStringLiteral()) {
2538
continue;
2639
}
2740

@@ -34,18 +47,20 @@ export const isValidMessagesShape = (node) => {
3447
return true;
3548
};
3649

37-
export function getMessages(node, messages = []) {
38-
for (const prop of node.get('properties')) {
39-
if (prop.isObjectExpression()) {
40-
getMessages(prop, messages);
41-
} else if (prop.isObjectProperty()) {
42-
if (prop.isObjectExpression()) {
43-
getMessages(prop.get('value'), messages);
44-
} else if (prop.get('key').node.name === 'id') {
45-
messages.push(String(prop.get('value').node.value));
50+
export const messagesObjectVisitor = {
51+
ObjectProperty(path) {
52+
const { mangleMap = null } = global[SUBSCRIBER_NAME] || {};
53+
54+
if (path.get('key').isIdentifier({ name: 'id' })) {
55+
const valueNode = path.get('value').node;
56+
const { value } = valueNode;
57+
const actualId = getActualId(mangleMap, value);
58+
59+
if (actualId !== value) {
60+
valueNode.value = actualId;
4661
}
47-
}
48-
}
4962

50-
return messages;
51-
}
63+
addMessage(this.file, actualId);
64+
}
65+
},
66+
};

src/consts.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const SUBSCRIBER_NAME = 'metadataReactIntlOptimizer';
2+
export const METADATA_NAME = 'react-intl-optimizer';

src/mangler/mangler.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import uniqid from 'uniqid';
2+
3+
export class MangleMap extends Map {
4+
constructor(whitelist) {
5+
super();
6+
7+
for (const id of whitelist) {
8+
this.set(id, id);
9+
}
10+
}
11+
12+
get(key) {
13+
let id = super.get(key);
14+
if (id !== undefined) {
15+
return id;
16+
}
17+
18+
id = uniqid.process();
19+
this.set(key, id.length >= key ? key : id);
20+
21+
return id;
22+
}
23+
}

src/mangler/utils.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
export function getActualId(mangleMap, id) {
3+
if (mangleMap === null) return id;
4+
5+
return mangleMap.get(id);
6+
}

src/webpack/index.js

+63-27
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,79 @@
1-
import { filterMessagesObj } from './utils';
1+
import { createReplacer, mangleMessagesObj } from './utils';
2+
import { SUBSCRIBER_NAME, METADATA_NAME } from '../consts';
3+
import { MangleMap } from '../mangler/mangler';
24

3-
class ReactIntlPlugin {
4-
constructor({ messages, keepUnused = true }) {
5+
class ReactIntlOptimizer {
6+
constructor({
7+
messages,
8+
optimization = {},
9+
output = langKey => `messages/${langKey}.json`,
10+
}) {
511
this.messages = messages;
6-
this.keepUnused = keepUnused;
12+
this.optimization = optimization;
13+
this.output = output;
714
}
815

916
static get metadataContextFunctionName() {
10-
return 'metadataReactIntlPlugin';
17+
return SUBSCRIBER_NAME;
1118
}
1219

1320
apply(compiler) {
14-
const allMessagesIDs = new Set();
15-
16-
compiler.hooks.compilation.tap('ReactIntlPlugin', (compilation) => {
17-
compilation.hooks.normalModuleLoader.tap('ReactIntlPlugin', (context) => {
18-
context[ReactIntlPlugin.metadataContextFunctionName] = (metadata) => {
19-
if (!this.keepUnused && metadata['react-intl']) {
20-
for (const id of metadata['react-intl']) {
21-
allMessagesIDs.add(id);
21+
const {
22+
mangle,
23+
unsafeMangling,
24+
removeUnused = false,
25+
whitelist = [],
26+
} = this.optimization;
27+
28+
const allMessagesIDs = removeUnused
29+
? new Set()
30+
: null;
31+
32+
const mangleMap = mangle
33+
? new MangleMap(whitelist)
34+
: null;
35+
36+
compiler.hooks.beforeRun.tapAsync(ReactIntlOptimizer.name, (compilation, callback) => {
37+
// todo: let's try to avoid such exposure...
38+
// prepare mangle map aot and just pass it as an option to babel-loader
39+
global[ReactIntlOptimizer.metadataContextFunctionName] = {
40+
mangleMap,
41+
};
42+
43+
callback();
44+
});
45+
46+
if (removeUnused) {
47+
compiler.hooks.compilation.tap(ReactIntlOptimizer.name, (compilation) => {
48+
compilation.hooks.normalModuleLoader.tap(ReactIntlOptimizer.name, (context) => {
49+
context[ReactIntlOptimizer.metadataContextFunctionName] = (metadata) => {
50+
if (metadata[METADATA_NAME] !== undefined) {
51+
for (const id of metadata[METADATA_NAME]) {
52+
allMessagesIDs.add(id);
53+
}
2254
}
23-
}
24-
};
55+
};
56+
});
2557
});
26-
});
58+
}
59+
60+
compiler.hooks.emit.tapAsync(ReactIntlOptimizer.name, (compilation, callback) => {
61+
const replacer = createReplacer(
62+
whitelist,
63+
allMessagesIDs,
64+
);
65+
66+
for (const [langKey, allMessages] of Object.entries(this.messages)) {
67+
let messages = allMessages;
2768

28-
compiler.hooks.emit.tapAsync('ReactIntlPlugin', (compilation, callback) => {
29-
allMessagesIDs.delete('');
69+
if (mangle) {
70+
messages = mangleMessagesObj(messages, mangleMap);
71+
}
3072

31-
for (const [langKey, messages] of Object.entries(this.messages)) {
32-
const jsonString = JSON.stringify(
33-
this.keepUnused
34-
? messages
35-
: filterMessagesObj({ ...messages }, allMessagesIDs),
36-
);
73+
const jsonString = JSON.stringify(messages, replacer);
3774

38-
const filename = `messages/${langKey}.json`;
75+
const filename = this.output(langKey);
3976

40-
compilation.fileDependencies.add(filename);
4177
compilation.assets[filename] = {
4278
source: () => jsonString,
4379
size: () => jsonString.length,
@@ -49,4 +85,4 @@ class ReactIntlPlugin {
4985
}
5086
}
5187

52-
export default ReactIntlPlugin;
88+
export default ReactIntlOptimizer;

src/webpack/utils.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1-
export function filterMessagesObj(messages, foundIDs) {
2-
for (const key of Object.keys(messages)) {
3-
if (!foundIDs.has(key)) {
1+
export function createReplacer(whitelist, foundIds) {
2+
return (key, value) => {
3+
if (key === '' || foundIds === null || foundIds.has(key) || whitelist.includes(key)) {
4+
return value;
5+
}
6+
7+
return undefined;
8+
};
9+
}
10+
11+
export function mangleMessagesObj(messages, mangleMap) {
12+
for (const [key, value] of Object.entries(messages)) {
13+
if (mangleMap.has(key)) {
414
delete messages[key];
15+
messages[mangleMap.get(key)] = value;
516
}
617
}
718

0 commit comments

Comments
 (0)