@elementor/extract-i18n-wordpress-expressions-webpack-plugin 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/index.d.mts +12 -22
- package/dist/index.d.ts +12 -22
- package/dist/index.js +88 -107
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +87 -108
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
- package/src/__tests__/__snapshots__/index.test.ts.snap +17 -10
- package/src/__tests__/index.test.ts +75 -27
- package/src/index.ts +1 -201
- package/src/plugin.ts +78 -0
- package/src/types.ts +14 -0
- package/src/utils.ts +70 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.3.1](https://github.com/elementor/elementor-packages/compare/@elementor/extract-i18n-wordpress-expressions-webpack-plugin@0.3.0...@elementor/extract-i18n-wordpress-expressions-webpack-plugin@0.3.1) (2023-11-07)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @elementor/extract-i18n-wordpress-expressions-webpack-plugin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [0.3.0](https://github.com/elementor/elementor-packages/compare/@elementor/extract-i18n-wordpress-expressions-webpack-plugin@0.2.2...@elementor/extract-i18n-wordpress-expressions-webpack-plugin@0.3.0) (2023-09-12)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **i18n-webpack:** changed the way it extracts expressions ([#117](https://github.com/elementor/elementor-packages/issues/117)) ([5be687d](https://github.com/elementor/elementor-packages/commit/5be687d57ca9d0c335b9803aea12cdacc14f8202))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
## 0.2.2 (2023-08-31)
|
|
7
26
|
|
|
8
27
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,30 +1,20 @@
|
|
|
1
|
-
import { Compiler, Compilation
|
|
1
|
+
import { Chunk, Compiler, Compilation } from 'webpack';
|
|
2
2
|
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
modules?: Module[];
|
|
3
|
+
type EntrySettings = {
|
|
4
|
+
id: string;
|
|
5
|
+
chunk: Chunk;
|
|
6
|
+
path: string;
|
|
7
|
+
pattern: string;
|
|
10
8
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
value: string;
|
|
9
|
+
|
|
10
|
+
type Options = {
|
|
11
|
+
pattern: (entryPath: string, entryId: string) => string;
|
|
15
12
|
};
|
|
16
|
-
type TranslationCallExpressions = Map<string, TranslationCallExpression[]>;
|
|
17
13
|
declare class ExtractI18nWordpressExpressionsWebpackPlugin {
|
|
14
|
+
options: Options;
|
|
15
|
+
constructor(options: Options);
|
|
18
16
|
apply(compiler: Compiler): void;
|
|
19
|
-
|
|
20
|
-
createTranslationsFiles(compilation: Compilation, translationCallExpressions: TranslationCallExpressions): Promise<void[]>;
|
|
21
|
-
getSubModulesToCheck(module: Module): Module[];
|
|
22
|
-
getFileFromChunk(chunk: Chunk): string | undefined;
|
|
23
|
-
shouldCheckModule(module: Module): boolean;
|
|
24
|
-
findMainModuleOfEntry(module: Module, compilation: Compilation): string | null;
|
|
25
|
-
extractExpressionsFromSubmodule(subModule: Module): TranslationCallExpression[];
|
|
26
|
-
generateTranslationFilename(basePath: string, filename: string): string;
|
|
27
|
-
generateTranslationFileContent(expressions: TranslationCallExpression[]): string;
|
|
17
|
+
getEntries(compilation: Compilation): EntrySettings[];
|
|
28
18
|
}
|
|
29
19
|
|
|
30
20
|
export { ExtractI18nWordpressExpressionsWebpackPlugin };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,30 +1,20 @@
|
|
|
1
|
-
import { Compiler, Compilation
|
|
1
|
+
import { Chunk, Compiler, Compilation } from 'webpack';
|
|
2
2
|
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
modules?: Module[];
|
|
3
|
+
type EntrySettings = {
|
|
4
|
+
id: string;
|
|
5
|
+
chunk: Chunk;
|
|
6
|
+
path: string;
|
|
7
|
+
pattern: string;
|
|
10
8
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
value: string;
|
|
9
|
+
|
|
10
|
+
type Options = {
|
|
11
|
+
pattern: (entryPath: string, entryId: string) => string;
|
|
15
12
|
};
|
|
16
|
-
type TranslationCallExpressions = Map<string, TranslationCallExpression[]>;
|
|
17
13
|
declare class ExtractI18nWordpressExpressionsWebpackPlugin {
|
|
14
|
+
options: Options;
|
|
15
|
+
constructor(options: Options);
|
|
18
16
|
apply(compiler: Compiler): void;
|
|
19
|
-
|
|
20
|
-
createTranslationsFiles(compilation: Compilation, translationCallExpressions: TranslationCallExpressions): Promise<void[]>;
|
|
21
|
-
getSubModulesToCheck(module: Module): Module[];
|
|
22
|
-
getFileFromChunk(chunk: Chunk): string | undefined;
|
|
23
|
-
shouldCheckModule(module: Module): boolean;
|
|
24
|
-
findMainModuleOfEntry(module: Module, compilation: Compilation): string | null;
|
|
25
|
-
extractExpressionsFromSubmodule(subModule: Module): TranslationCallExpression[];
|
|
26
|
-
generateTranslationFilename(basePath: string, filename: string): string;
|
|
27
|
-
generateTranslationFileContent(expressions: TranslationCallExpression[]): string;
|
|
17
|
+
getEntries(compilation: Compilation): EntrySettings[];
|
|
28
18
|
}
|
|
29
19
|
|
|
30
20
|
export { ExtractI18nWordpressExpressionsWebpackPlugin };
|
package/dist/index.js
CHANGED
|
@@ -33,129 +33,110 @@ __export(src_exports, {
|
|
|
33
33
|
ExtractI18nWordpressExpressionsWebpackPlugin: () => ExtractI18nWordpressExpressionsWebpackPlugin
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(src_exports);
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
// src/plugin.ts
|
|
37
38
|
var path = __toESM(require("path"));
|
|
38
|
-
var
|
|
39
|
-
|
|
39
|
+
var fs2 = __toESM(require("fs"));
|
|
40
|
+
|
|
41
|
+
// src/utils.ts
|
|
42
|
+
var import_glob = require("glob");
|
|
43
|
+
var fs = __toESM(require("fs"));
|
|
44
|
+
var COMMENTS_REGEXPS = [
|
|
40
45
|
// Matches translators comment block: `/* translators: %s */`.
|
|
41
46
|
/\/\*[\t ]*translators:.*\*\//gm,
|
|
42
|
-
|
|
47
|
+
// Matches translators inline comment: `// translators: %s`.
|
|
43
48
|
/(\/\/)[\t ]*translators:[^\r\n]*/gm
|
|
44
|
-
]
|
|
45
|
-
var TRANSLATIONS_REGEXPS =
|
|
49
|
+
];
|
|
50
|
+
var TRANSLATIONS_REGEXPS = [
|
|
46
51
|
// Matches translation functions: `__('Hello', 'elementor')`, `_n('Me', 'Us', 2, 'elementor-pro')`.
|
|
47
|
-
/\b_(?:_|n|nx|x)\(.*?,\s*(?<c>['"`])[\w-]+\k<c>\)/
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
/\b_(?:_|n|nx|x)\(.*?,\s*(?<c>['"`])[\w-]+\k<c>\s*?\)/sg
|
|
53
|
+
];
|
|
54
|
+
function createStringsFilePath(path2, suffix = ".strings.js") {
|
|
55
|
+
return path2.replace(/(\.min)?\.js$/i, suffix);
|
|
56
|
+
}
|
|
57
|
+
function getFilesPaths(pattern) {
|
|
58
|
+
return (0, import_glob.glob)(pattern, {
|
|
59
|
+
ignore: {
|
|
60
|
+
ignored: (p) => !/\.(js|ts|jsx|tsx)$/.test(p.name),
|
|
61
|
+
childrenIgnored: (p) => p.isNamed("__tests__") || p.isNamed("__mocks__")
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Fix for Windows paths escaping.
|
|
65
|
+
* Note: This means we don't support paths with special character (like `*`,`?`, etc.)
|
|
66
|
+
* and only allow patterns that are constructed using `path.join()` or `path.resolve()`.
|
|
67
|
+
*
|
|
68
|
+
* @see https://github.com/isaacs/node-glob#options
|
|
69
|
+
* @see https://github.com/isaacs/node-glob#windows
|
|
70
|
+
* @see https://github.com/isaacs/node-glob/issues/212#issuecomment-1449062925
|
|
71
|
+
*/
|
|
72
|
+
windowsPathsNoEscape: true
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function getFilesContents(paths) {
|
|
76
|
+
return Promise.all(paths.map((filePath) => fs.promises.readFile(filePath, "utf-8")));
|
|
77
|
+
}
|
|
78
|
+
function generateStringsFileContent(contents) {
|
|
79
|
+
return contents.map((content) => extractExpressions(content)).flat().map((expr) => `${expr.value}${expr.type === "comment" ? "" : ";"}`).join("\n");
|
|
80
|
+
}
|
|
81
|
+
function extractExpressions(content) {
|
|
82
|
+
const expressions = [];
|
|
83
|
+
[...TRANSLATIONS_REGEXPS, ...COMMENTS_REGEXPS].forEach((regexp) => {
|
|
84
|
+
[...content.matchAll(regexp)].forEach((res) => {
|
|
85
|
+
expressions.push({
|
|
86
|
+
type: COMMENTS_REGEXPS.includes(regexp) ? "comment" : "call-expression",
|
|
87
|
+
index: res.index || 0,
|
|
88
|
+
value: res[0]
|
|
55
89
|
});
|
|
56
90
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
});
|
|
92
|
+
return expressions.sort((a, b) => a.index - b.index);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/plugin.ts
|
|
96
|
+
var ExtractI18nWordpressExpressionsWebpackPlugin = class {
|
|
97
|
+
options;
|
|
98
|
+
constructor(options) {
|
|
99
|
+
this.options = options;
|
|
60
100
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
this.extractExpressionsFromSubmodule(subModule).forEach((expression) => {
|
|
75
|
-
moduleExpressionsMap.get(mainEntryFile).push(expression);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
});
|
|
101
|
+
apply(compiler) {
|
|
102
|
+
compiler.hooks.afterEmit.tapPromise(this.constructor.name, async (compilation) => {
|
|
103
|
+
const entries = this.getEntries(compilation);
|
|
104
|
+
await Promise.all(
|
|
105
|
+
entries.map(async (entry) => {
|
|
106
|
+
const fileContents = await getFilesContents(
|
|
107
|
+
await getFilesPaths(entry.pattern)
|
|
108
|
+
);
|
|
109
|
+
const entryContent = generateStringsFileContent(fileContents);
|
|
110
|
+
await fs2.promises.writeFile(entry.path, entryContent);
|
|
111
|
+
})
|
|
112
|
+
);
|
|
79
113
|
});
|
|
80
|
-
return moduleExpressionsMap;
|
|
81
114
|
}
|
|
82
|
-
|
|
83
|
-
|
|
115
|
+
getEntries(compilation) {
|
|
116
|
+
return [...compilation.entrypoints].map(([id, entrypoint]) => {
|
|
84
117
|
const chunk = entrypoint.chunks.find(({ name }) => name === id);
|
|
85
118
|
if (!chunk) {
|
|
86
|
-
return
|
|
119
|
+
return null;
|
|
87
120
|
}
|
|
88
|
-
const chunkJSFile =
|
|
121
|
+
const chunkJSFile = [...chunk.files].find((f) => /\.(js|ts)$/i.test(f));
|
|
89
122
|
if (!chunkJSFile) {
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
const { entry } = compilation.options;
|
|
93
|
-
if (!entry || typeof entry !== "object" || !(id in entry)) {
|
|
94
|
-
throw new Error(`Entry must be an object. e.g: {app: './path/to/app.js'}`);
|
|
123
|
+
return null;
|
|
95
124
|
}
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
98
|
-
|
|
125
|
+
const { path: basePath } = compilation.options.output;
|
|
126
|
+
if (!basePath) {
|
|
127
|
+
return null;
|
|
99
128
|
}
|
|
100
|
-
const {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return fs.promises.writeFile(assetFilename, assetFileContent);
|
|
112
|
-
});
|
|
113
|
-
return await Promise.all(promises2);
|
|
114
|
-
}
|
|
115
|
-
getSubModulesToCheck(module2) {
|
|
116
|
-
return [...module2.modules || [], module2].filter((subModule) => this.shouldCheckModule(subModule));
|
|
117
|
-
}
|
|
118
|
-
getFileFromChunk(chunk) {
|
|
119
|
-
return [...chunk.files].find((f) => /\.js$/i.test(f));
|
|
120
|
-
}
|
|
121
|
-
shouldCheckModule(module2) {
|
|
122
|
-
return MODULE_FILTERS.every((filter) => filter.test(module2.userRequest || ""));
|
|
123
|
-
}
|
|
124
|
-
findMainModuleOfEntry(module2, compilation) {
|
|
125
|
-
const issuer = compilation.moduleGraph.getIssuer(module2);
|
|
126
|
-
if (issuer) {
|
|
127
|
-
return this.findMainModuleOfEntry(issuer, compilation);
|
|
128
|
-
}
|
|
129
|
-
return module2.rawRequest || null;
|
|
130
|
-
}
|
|
131
|
-
extractExpressionsFromSubmodule(subModule) {
|
|
132
|
-
const source = subModule?._source?._valueAsString;
|
|
133
|
-
if (!source) {
|
|
134
|
-
return [];
|
|
135
|
-
}
|
|
136
|
-
const translationCallExpressions = [];
|
|
137
|
-
[
|
|
138
|
-
...TRANSLATIONS_REGEXPS,
|
|
139
|
-
...COMMENTS_REGEXPS
|
|
140
|
-
].forEach((regexp) => {
|
|
141
|
-
[...source.matchAll(regexp)].forEach((res) => {
|
|
142
|
-
translationCallExpressions.push({
|
|
143
|
-
type: COMMENTS_REGEXPS.includes(regexp) ? "comment" : "call-expression",
|
|
144
|
-
index: res.index || 0,
|
|
145
|
-
value: res[0]
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
return translationCallExpressions;
|
|
150
|
-
}
|
|
151
|
-
generateTranslationFilename(basePath, filename) {
|
|
152
|
-
return path.join(
|
|
153
|
-
basePath,
|
|
154
|
-
filename.replace(/(\.min)?\.js$/i, ".strings.js")
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
generateTranslationFileContent(expressions) {
|
|
158
|
-
return expressions.sort((a, b) => a.index - b.index).map((expr) => `${expr.value}${expr.type === "comment" ? "" : ";"}`).join("\n");
|
|
129
|
+
const filePath = createStringsFilePath(compilation.getPath("[file]", { filename: chunkJSFile }));
|
|
130
|
+
return {
|
|
131
|
+
id,
|
|
132
|
+
chunk,
|
|
133
|
+
path: path.join(basePath, filePath),
|
|
134
|
+
pattern: this.options.pattern(
|
|
135
|
+
path.resolve(process.cwd(), entrypoint.origins[0].request),
|
|
136
|
+
id
|
|
137
|
+
)
|
|
138
|
+
};
|
|
139
|
+
}).filter(Boolean);
|
|
159
140
|
}
|
|
160
141
|
};
|
|
161
142
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { Compilation, Compiler, Chunk, Module as WebpackModule } from 'webpack';\n\ntype Module = WebpackModule & {\n\tuserRequest?: string;\n\trawRequest?: string;\n\t_source?: {\n\t\t_valueAsString?: string;\n\t},\n\tmodules?: Module[];\n}\n\ntype TranslationCallExpression = {\n\ttype: 'comment' | 'call-expression';\n\tindex: number;\n\tvalue: string;\n};\n\ntype TranslationCallExpressions = Map<string, TranslationCallExpression[]>;\n\nconst MODULE_FILTERS = Object.freeze( [ /(([^!?\\s]+?)(?:\\.js|\\.jsx|\\.ts|\\.tsx))$/, /^((?!node_modules).)*$/ ] );\n\nconst COMMENTS_REGEXPS = Object.freeze( [\n\t// Matches translators comment block: `/* translators: %s */`.\n\t/\\/\\*[\\t ]*translators:.*\\*\\//gm,\n\t/// Matches translators inline comment: `// translators: %s`.\n\t/(\\/\\/)[\\t ]*translators:[^\\r\\n]*/gm,\n] );\n\nconst TRANSLATIONS_REGEXPS = Object.freeze( [\n\t// Matches translation functions: `__('Hello', 'elementor')`, `_n('Me', 'Us', 2, 'elementor-pro')`.\n\t/\\b_(?:_|n|nx|x)\\(.*?,\\s*(?<c>['\"`])[\\w-]+\\k<c>\\)/gm,\n] );\n\nexport class ExtractI18nWordpressExpressionsWebpackPlugin {\n\tapply( compiler: Compiler ) {\n\t\t// Learn more about Webpack plugin system: https://webpack.js.org/api/plugins/\n\n\t\tlet translationCallExpressions: TranslationCallExpressions = new Map();\n\n\t\t// Learn more about Webpack compilation process and hooks: https://webpack.js.org/api/compilation-hooks/\n\t\tcompiler.hooks.thisCompilation.tap( this.constructor.name, ( compilation ) => {\n\t\t\t// We tap into the time that Webpack has finished processing all the other assets\n\t\t\t// learn more: https://webpack.js.org/api/compilation-hooks/#processassets.\n\t\t\tcompilation.hooks.processAssets.tap( { name: this.constructor.name }, () => {\n\t\t\t\ttranslationCallExpressions = this.getModuleExpressionsMap( compilation );\n\t\t\t} );\n\t\t} );\n\n\t\tcompiler.hooks.afterEmit.tapPromise( this.constructor.name, async ( compilation ) => {\n\t\t\t// Create all the translations files based on the call expressions.\n\t\t\tawait this.createTranslationsFiles( compilation, translationCallExpressions );\n\t\t} );\n\t}\n\n\tgetModuleExpressionsMap( compilation: Compilation ) {\n\t\tconst moduleExpressionsMap = new Map();\n\n\t\t[ ...compilation.chunks ].forEach( ( chunk ) => {\n\t\t\tconst chunkJSFile = this.getFileFromChunk( chunk );\n\n\t\t\tif ( ! chunkJSFile ) {\n\t\t\t\t// There's no JS file in this chunk, no work for us. Typically a `style.css` from cache group.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcompilation.chunkGraph.getChunkModules( chunk ).forEach( ( module ) => {\n\t\t\t\tthis.getSubModulesToCheck( module ).forEach( ( subModule ) => {\n\t\t\t\t\tconst mainEntryFile = this.findMainModuleOfEntry( subModule, compilation );\n\n\t\t\t\t\tif ( ! moduleExpressionsMap.has( mainEntryFile ) ) {\n\t\t\t\t\t\tmoduleExpressionsMap.set( mainEntryFile, [] );\n\t\t\t\t\t}\n\n\t\t\t\t\t// Running over the submodules and find all the translation call expressions and their translators comment\n\t\t\t\t\t// (e.g `/* translators: %s: name*/ __('Hello %s', 'elementor')`),\n\t\t\t\t\t// extract them and add them to a Map, where the key is the main entry file, and the value is an array of all the\n\t\t\t\t\t// translation call expressions.\n\t\t\t\t\tthis.extractExpressionsFromSubmodule( subModule ).forEach( ( expression ) => {\n\t\t\t\t\t\tmoduleExpressionsMap.get( mainEntryFile ).push( expression );\n\t\t\t\t\t} );\n\t\t\t\t} );\n\t\t\t} );\n\t\t} );\n\n\t\treturn moduleExpressionsMap;\n\t}\n\n\tasync createTranslationsFiles( compilation: Compilation, translationCallExpressions: TranslationCallExpressions ) {\n\t\tconst promises = [ ...compilation.entrypoints ].map( ( [ id, entrypoint ] ) => {\n\t\t\tconst chunk = entrypoint.chunks.find( ( { name } ) => name === id );\n\n\t\t\tif ( ! chunk ) {\n\t\t\t\treturn Promise.resolve();\n\t\t\t}\n\n\t\t\tconst chunkJSFile = this.getFileFromChunk( chunk );\n\n\t\t\tif ( ! chunkJSFile ) {\n\t\t\t\treturn Promise.resolve();\n\t\t\t}\n\n\t\t\tconst { entry } = compilation.options;\n\n\t\t\tif ( ! entry || typeof entry !== 'object' || ! ( id in entry ) ) {\n\t\t\t\tthrow new Error( `Entry must be an object. e.g: {app: './path/to/app.js'}` );\n\t\t\t}\n\n\t\t\tconst mainFilePath = entry[ id ].import?.[ 0 ];\n\n\t\t\tif ( ! mainFilePath ) {\n\t\t\t\tthrow new Error( 'Entry is invalid' );\n\t\t\t}\n\n\t\t\tconst { path: outputPath } = compilation.options.output;\n\n\t\t\tif ( ! outputPath ) {\n\t\t\t\tthrow new Error( 'Output path is invalid' );\n\t\t\t}\n\n\t\t\tconst assetFilename = this.generateTranslationFilename(\n\t\t\t\toutputPath,\n\t\t\t\tcompilation.getPath( '[file]', { filename: chunkJSFile } )\n\t\t\t);\n\n\t\t\tconst assetFileContent = this.generateTranslationFileContent(\n\t\t\t\ttranslationCallExpressions.get( mainFilePath ) || []\n\t\t\t);\n\n\t\t\treturn fs.promises.writeFile( assetFilename, assetFileContent );\n\t\t} );\n\n\t\treturn await Promise.all( promises );\n\t}\n\n\tgetSubModulesToCheck( module: Module ) {\n\t\treturn [ ...( module.modules || [] ), module ]\n\t\t\t.filter( ( subModule ) => this.shouldCheckModule( subModule ) );\n\t}\n\n\tgetFileFromChunk( chunk: Chunk ) {\n\t\treturn [ ...chunk.files ].find( ( f ) => /\\.js$/i.test( f ) );\n\t}\n\n\tshouldCheckModule( module: Module ) {\n\t\treturn MODULE_FILTERS.every( ( filter ) => filter.test( module.userRequest || '' ) );\n\t}\n\n\tfindMainModuleOfEntry( module: Module, compilation: Compilation ) : string | null {\n\t\tconst issuer = compilation.moduleGraph.getIssuer( module );\n\n\t\tif ( issuer ) {\n\t\t\treturn this.findMainModuleOfEntry( issuer, compilation );\n\t\t}\n\n\t\treturn module.rawRequest || null;\n\t}\n\n\textractExpressionsFromSubmodule( subModule: Module ) {\n\t\tconst source = subModule?._source?._valueAsString;\n\n\t\tif ( ! source ) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst translationCallExpressions: TranslationCallExpression[] = [];\n\n\t\t[\n\t\t\t...TRANSLATIONS_REGEXPS,\n\t\t\t...COMMENTS_REGEXPS,\n\t\t].forEach( ( regexp ) => {\n\t\t\t[ ...source.matchAll( regexp ) ].forEach( ( res ) => {\n\t\t\t\ttranslationCallExpressions.push( {\n\t\t\t\t\ttype: COMMENTS_REGEXPS.includes( regexp ) ? 'comment' : 'call-expression',\n\t\t\t\t\tindex: res.index || 0,\n\t\t\t\t\tvalue: res[ 0 ],\n\t\t\t\t} );\n\t\t\t} );\n\t\t} );\n\n\t\treturn translationCallExpressions;\n\t}\n\n\tgenerateTranslationFilename( basePath: string, filename: string ) {\n\t\treturn path.join(\n\t\t\tbasePath,\n\t\t\tfilename.replace( /(\\.min)?\\.js$/i, '.strings.js' )\n\t\t);\n\t}\n\n\tgenerateTranslationFileContent( expressions: TranslationCallExpression[] ) {\n\t\treturn expressions\n\t\t\t// Sort by the index it was found in the file based on the regexp (and not by the order it was added to the array).\n\t\t\t.sort( ( a, b ) => a.index - b.index )\n\t\t\t// Add a semicolon when needed.\n\t\t\t.map( ( expr ) => `${ expr.value }${ expr.type === 'comment' ? '' : ';' }` )\n\t\t\t// Join all the expressions to a single string with line-breaks between them.\n\t\t\t.join( '\\n' );\n\t}\n}\n"],"mappings":"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","names":["module","promises"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/plugin.ts","../src/utils.ts"],"sourcesContent":["export { default as ExtractI18nWordpressExpressionsWebpackPlugin } from './plugin';\n","import * as path from 'path';\nimport * as fs from 'fs';\nimport { Compilation, Compiler } from 'webpack';\nimport { EntrySettings } from './types';\nimport {\n\tcreateStringsFilePath,\n\tgenerateStringsFileContent,\n\tgetFilesContents,\n\tgetFilesPaths,\n} from './utils';\n\ntype Options = {\n\tpattern: ( entryPath: string, entryId: string ) => string;\n}\n\nexport default class ExtractI18nWordpressExpressionsWebpackPlugin {\n\toptions: Options;\n\n\tconstructor( options: Options ) {\n\t\tthis.options = options;\n\t}\n\n\tapply( compiler: Compiler ) {\n\t\tcompiler.hooks.afterEmit.tapPromise( this.constructor.name, async ( compilation ) => {\n\t\t\tconst entries = this.getEntries( compilation );\n\n\t\t\tawait Promise.all(\n\t\t\t\tentries.map( async ( entry ) => {\n\t\t\t\t\tconst fileContents = await getFilesContents(\n\t\t\t\t\t\tawait getFilesPaths( entry.pattern )\n\t\t\t\t\t);\n\n\t\t\t\t\tconst entryContent = generateStringsFileContent( fileContents );\n\n\t\t\t\t\t// Writing manually instead of using `chunk.files.add()` in order to avoid passing\n\t\t\t\t\t// the file through the loaders (transpilers, minifiers, etc.).\n\t\t\t\t\tawait fs.promises.writeFile( entry.path, entryContent );\n\t\t\t\t} )\n\t\t\t);\n\t\t} );\n\t}\n\n\tgetEntries( compilation: Compilation ) {\n\t\treturn [ ...compilation.entrypoints ]\n\t\t\t.map( ( [ id, entrypoint ] ) => {\n\t\t\t\tconst chunk = entrypoint.chunks.find( ( { name } ) => name === id );\n\n\t\t\t\tif ( ! chunk ) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst chunkJSFile = [ ...chunk.files ].find( ( f ) => /\\.(js|ts)$/i.test( f ) );\n\n\t\t\t\tif ( ! chunkJSFile ) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst { path: basePath } = compilation.options.output;\n\n\t\t\t\tif ( ! basePath ) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst filePath = createStringsFilePath( compilation.getPath( '[file]', { filename: chunkJSFile } ) );\n\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tchunk,\n\t\t\t\t\tpath: path.join( basePath, filePath ),\n\t\t\t\t\tpattern: this.options.pattern(\n\t\t\t\t\t\tpath.resolve( process.cwd(), entrypoint.origins[ 0 ].request ),\n\t\t\t\t\t\tid\n\t\t\t\t\t),\n\t\t\t\t};\n\t\t\t} )\n\t\t\t.filter( Boolean ) as EntrySettings[];\n\t}\n}\n","import { TranslationCallExpression } from './types';\nimport { glob } from 'glob';\nimport * as fs from 'fs';\n\nconst COMMENTS_REGEXPS = [\n\t// Matches translators comment block: `/* translators: %s */`.\n\t/\\/\\*[\\t ]*translators:.*\\*\\//gm,\n\t// Matches translators inline comment: `// translators: %s`.\n\t/(\\/\\/)[\\t ]*translators:[^\\r\\n]*/gm,\n] as const;\n\nconst TRANSLATIONS_REGEXPS = [\n\t// Matches translation functions: `__('Hello', 'elementor')`, `_n('Me', 'Us', 2, 'elementor-pro')`.\n\t/\\b_(?:_|n|nx|x)\\(.*?,\\s*(?<c>['\"`])[\\w-]+\\k<c>\\s*?\\)/sg,\n] as const;\n\nexport function createStringsFilePath( path: string, suffix = '.strings.js' ) {\n\treturn path.replace( /(\\.min)?\\.js$/i, suffix );\n}\n\nexport function getFilesPaths( pattern: string ) {\n\treturn glob( pattern, {\n\t\tignore: {\n\t\t\tignored: ( p ) => ! ( /\\.(js|ts|jsx|tsx)$/.test( p.name ) ),\n\t\t\tchildrenIgnored: ( p ) => p.isNamed( '__tests__' ) || p.isNamed( '__mocks__' ),\n\t\t},\n\n\t\t/**\n\t\t * Fix for Windows paths escaping.\n\t\t * Note: This means we don't support paths with special character (like `*`,`?`, etc.)\n\t\t * and only allow patterns that are constructed using `path.join()` or `path.resolve()`.\n\t\t *\n\t\t * @see https://github.com/isaacs/node-glob#options\n\t\t * @see https://github.com/isaacs/node-glob#windows\n\t\t * @see https://github.com/isaacs/node-glob/issues/212#issuecomment-1449062925\n\t\t */\n\t\twindowsPathsNoEscape: true,\n\t} );\n}\n\nexport function getFilesContents( paths: string[] ) {\n\treturn Promise.all( paths.map( ( filePath ) => fs.promises.readFile( filePath, 'utf-8' ) ) );\n}\n\nexport function generateStringsFileContent( contents: string[] ) {\n\treturn contents\n\t\t.map( ( content, ) => extractExpressions( content ) )\n\t\t.flat()\n\t\t// Add a semicolon when needed.\n\t\t.map( ( expr ) => `${ expr.value }${ expr.type === 'comment' ? '' : ';' }` )\n\t\t// Join all the expressions to a single string with line-breaks between them.\n\t\t.join( '\\n' );\n}\n\nfunction extractExpressions( content: string ): TranslationCallExpression[] {\n\tconst expressions: TranslationCallExpression[] = [];\n\n\t[ ...TRANSLATIONS_REGEXPS, ...COMMENTS_REGEXPS ].forEach( ( regexp ) => {\n\t\t[ ...content.matchAll( regexp ) ].forEach( ( res ) => {\n\t\t\texpressions.push( {\n\t\t\t\ttype: COMMENTS_REGEXPS.includes( regexp ) ? 'comment' : 'call-expression',\n\t\t\t\tindex: res.index || 0,\n\t\t\t\tvalue: res[ 0 ],\n\t\t\t} );\n\t\t} );\n\t} );\n\n\t// Sort by the index it was found in the file based on the regexp (and not by the order it was added to the array).\n\treturn expressions.sort( ( a, b ) => a.index - b.index );\n}\n"],"mappings":"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","names":["fs","path"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,127 +1,106 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import * as fs from "fs";
|
|
1
|
+
// src/plugin.ts
|
|
3
2
|
import * as path from "path";
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import * as fs2 from "fs";
|
|
4
|
+
|
|
5
|
+
// src/utils.ts
|
|
6
|
+
import { glob } from "glob";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
var COMMENTS_REGEXPS = [
|
|
6
9
|
// Matches translators comment block: `/* translators: %s */`.
|
|
7
10
|
/\/\*[\t ]*translators:.*\*\//gm,
|
|
8
|
-
|
|
11
|
+
// Matches translators inline comment: `// translators: %s`.
|
|
9
12
|
/(\/\/)[\t ]*translators:[^\r\n]*/gm
|
|
10
|
-
]
|
|
11
|
-
var TRANSLATIONS_REGEXPS =
|
|
13
|
+
];
|
|
14
|
+
var TRANSLATIONS_REGEXPS = [
|
|
12
15
|
// Matches translation functions: `__('Hello', 'elementor')`, `_n('Me', 'Us', 2, 'elementor-pro')`.
|
|
13
|
-
/\b_(?:_|n|nx|x)\(.*?,\s*(?<c>['"`])[\w-]+\k<c>\)/
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
/\b_(?:_|n|nx|x)\(.*?,\s*(?<c>['"`])[\w-]+\k<c>\s*?\)/sg
|
|
17
|
+
];
|
|
18
|
+
function createStringsFilePath(path2, suffix = ".strings.js") {
|
|
19
|
+
return path2.replace(/(\.min)?\.js$/i, suffix);
|
|
20
|
+
}
|
|
21
|
+
function getFilesPaths(pattern) {
|
|
22
|
+
return glob(pattern, {
|
|
23
|
+
ignore: {
|
|
24
|
+
ignored: (p) => !/\.(js|ts|jsx|tsx)$/.test(p.name),
|
|
25
|
+
childrenIgnored: (p) => p.isNamed("__tests__") || p.isNamed("__mocks__")
|
|
26
|
+
},
|
|
27
|
+
/**
|
|
28
|
+
* Fix for Windows paths escaping.
|
|
29
|
+
* Note: This means we don't support paths with special character (like `*`,`?`, etc.)
|
|
30
|
+
* and only allow patterns that are constructed using `path.join()` or `path.resolve()`.
|
|
31
|
+
*
|
|
32
|
+
* @see https://github.com/isaacs/node-glob#options
|
|
33
|
+
* @see https://github.com/isaacs/node-glob#windows
|
|
34
|
+
* @see https://github.com/isaacs/node-glob/issues/212#issuecomment-1449062925
|
|
35
|
+
*/
|
|
36
|
+
windowsPathsNoEscape: true
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function getFilesContents(paths) {
|
|
40
|
+
return Promise.all(paths.map((filePath) => fs.promises.readFile(filePath, "utf-8")));
|
|
41
|
+
}
|
|
42
|
+
function generateStringsFileContent(contents) {
|
|
43
|
+
return contents.map((content) => extractExpressions(content)).flat().map((expr) => `${expr.value}${expr.type === "comment" ? "" : ";"}`).join("\n");
|
|
44
|
+
}
|
|
45
|
+
function extractExpressions(content) {
|
|
46
|
+
const expressions = [];
|
|
47
|
+
[...TRANSLATIONS_REGEXPS, ...COMMENTS_REGEXPS].forEach((regexp) => {
|
|
48
|
+
[...content.matchAll(regexp)].forEach((res) => {
|
|
49
|
+
expressions.push({
|
|
50
|
+
type: COMMENTS_REGEXPS.includes(regexp) ? "comment" : "call-expression",
|
|
51
|
+
index: res.index || 0,
|
|
52
|
+
value: res[0]
|
|
21
53
|
});
|
|
22
54
|
});
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
55
|
+
});
|
|
56
|
+
return expressions.sort((a, b) => a.index - b.index);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/plugin.ts
|
|
60
|
+
var ExtractI18nWordpressExpressionsWebpackPlugin = class {
|
|
61
|
+
options;
|
|
62
|
+
constructor(options) {
|
|
63
|
+
this.options = options;
|
|
26
64
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
this.extractExpressionsFromSubmodule(subModule).forEach((expression) => {
|
|
41
|
-
moduleExpressionsMap.get(mainEntryFile).push(expression);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
});
|
|
65
|
+
apply(compiler) {
|
|
66
|
+
compiler.hooks.afterEmit.tapPromise(this.constructor.name, async (compilation) => {
|
|
67
|
+
const entries = this.getEntries(compilation);
|
|
68
|
+
await Promise.all(
|
|
69
|
+
entries.map(async (entry) => {
|
|
70
|
+
const fileContents = await getFilesContents(
|
|
71
|
+
await getFilesPaths(entry.pattern)
|
|
72
|
+
);
|
|
73
|
+
const entryContent = generateStringsFileContent(fileContents);
|
|
74
|
+
await fs2.promises.writeFile(entry.path, entryContent);
|
|
75
|
+
})
|
|
76
|
+
);
|
|
45
77
|
});
|
|
46
|
-
return moduleExpressionsMap;
|
|
47
78
|
}
|
|
48
|
-
|
|
49
|
-
|
|
79
|
+
getEntries(compilation) {
|
|
80
|
+
return [...compilation.entrypoints].map(([id, entrypoint]) => {
|
|
50
81
|
const chunk = entrypoint.chunks.find(({ name }) => name === id);
|
|
51
82
|
if (!chunk) {
|
|
52
|
-
return
|
|
83
|
+
return null;
|
|
53
84
|
}
|
|
54
|
-
const chunkJSFile =
|
|
85
|
+
const chunkJSFile = [...chunk.files].find((f) => /\.(js|ts)$/i.test(f));
|
|
55
86
|
if (!chunkJSFile) {
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
const { entry } = compilation.options;
|
|
59
|
-
if (!entry || typeof entry !== "object" || !(id in entry)) {
|
|
60
|
-
throw new Error(`Entry must be an object. e.g: {app: './path/to/app.js'}`);
|
|
87
|
+
return null;
|
|
61
88
|
}
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
64
|
-
|
|
89
|
+
const { path: basePath } = compilation.options.output;
|
|
90
|
+
if (!basePath) {
|
|
91
|
+
return null;
|
|
65
92
|
}
|
|
66
|
-
const {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return fs.promises.writeFile(assetFilename, assetFileContent);
|
|
78
|
-
});
|
|
79
|
-
return await Promise.all(promises2);
|
|
80
|
-
}
|
|
81
|
-
getSubModulesToCheck(module) {
|
|
82
|
-
return [...module.modules || [], module].filter((subModule) => this.shouldCheckModule(subModule));
|
|
83
|
-
}
|
|
84
|
-
getFileFromChunk(chunk) {
|
|
85
|
-
return [...chunk.files].find((f) => /\.js$/i.test(f));
|
|
86
|
-
}
|
|
87
|
-
shouldCheckModule(module) {
|
|
88
|
-
return MODULE_FILTERS.every((filter) => filter.test(module.userRequest || ""));
|
|
89
|
-
}
|
|
90
|
-
findMainModuleOfEntry(module, compilation) {
|
|
91
|
-
const issuer = compilation.moduleGraph.getIssuer(module);
|
|
92
|
-
if (issuer) {
|
|
93
|
-
return this.findMainModuleOfEntry(issuer, compilation);
|
|
94
|
-
}
|
|
95
|
-
return module.rawRequest || null;
|
|
96
|
-
}
|
|
97
|
-
extractExpressionsFromSubmodule(subModule) {
|
|
98
|
-
const source = subModule?._source?._valueAsString;
|
|
99
|
-
if (!source) {
|
|
100
|
-
return [];
|
|
101
|
-
}
|
|
102
|
-
const translationCallExpressions = [];
|
|
103
|
-
[
|
|
104
|
-
...TRANSLATIONS_REGEXPS,
|
|
105
|
-
...COMMENTS_REGEXPS
|
|
106
|
-
].forEach((regexp) => {
|
|
107
|
-
[...source.matchAll(regexp)].forEach((res) => {
|
|
108
|
-
translationCallExpressions.push({
|
|
109
|
-
type: COMMENTS_REGEXPS.includes(regexp) ? "comment" : "call-expression",
|
|
110
|
-
index: res.index || 0,
|
|
111
|
-
value: res[0]
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
return translationCallExpressions;
|
|
116
|
-
}
|
|
117
|
-
generateTranslationFilename(basePath, filename) {
|
|
118
|
-
return path.join(
|
|
119
|
-
basePath,
|
|
120
|
-
filename.replace(/(\.min)?\.js$/i, ".strings.js")
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
generateTranslationFileContent(expressions) {
|
|
124
|
-
return expressions.sort((a, b) => a.index - b.index).map((expr) => `${expr.value}${expr.type === "comment" ? "" : ";"}`).join("\n");
|
|
93
|
+
const filePath = createStringsFilePath(compilation.getPath("[file]", { filename: chunkJSFile }));
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
chunk,
|
|
97
|
+
path: path.join(basePath, filePath),
|
|
98
|
+
pattern: this.options.pattern(
|
|
99
|
+
path.resolve(process.cwd(), entrypoint.origins[0].request),
|
|
100
|
+
id
|
|
101
|
+
)
|
|
102
|
+
};
|
|
103
|
+
}).filter(Boolean);
|
|
125
104
|
}
|
|
126
105
|
};
|
|
127
106
|
export {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { Compilation, Compiler, Chunk, Module as WebpackModule } from 'webpack';\n\ntype Module = WebpackModule & {\n\tuserRequest?: string;\n\trawRequest?: string;\n\t_source?: {\n\t\t_valueAsString?: string;\n\t},\n\tmodules?: Module[];\n}\n\ntype TranslationCallExpression = {\n\ttype: 'comment' | 'call-expression';\n\tindex: number;\n\tvalue: string;\n};\n\ntype TranslationCallExpressions = Map<string, TranslationCallExpression[]>;\n\nconst MODULE_FILTERS = Object.freeze( [ /(([^!?\\s]+?)(?:\\.js|\\.jsx|\\.ts|\\.tsx))$/, /^((?!node_modules).)*$/ ] );\n\nconst COMMENTS_REGEXPS = Object.freeze( [\n\t// Matches translators comment block: `/* translators: %s */`.\n\t/\\/\\*[\\t ]*translators:.*\\*\\//gm,\n\t/// Matches translators inline comment: `// translators: %s`.\n\t/(\\/\\/)[\\t ]*translators:[^\\r\\n]*/gm,\n] );\n\nconst TRANSLATIONS_REGEXPS = Object.freeze( [\n\t// Matches translation functions: `__('Hello', 'elementor')`, `_n('Me', 'Us', 2, 'elementor-pro')`.\n\t/\\b_(?:_|n|nx|x)\\(.*?,\\s*(?<c>['\"`])[\\w-]+\\k<c>\\)/gm,\n] );\n\nexport class ExtractI18nWordpressExpressionsWebpackPlugin {\n\tapply( compiler: Compiler ) {\n\t\t// Learn more about Webpack plugin system: https://webpack.js.org/api/plugins/\n\n\t\tlet translationCallExpressions: TranslationCallExpressions = new Map();\n\n\t\t// Learn more about Webpack compilation process and hooks: https://webpack.js.org/api/compilation-hooks/\n\t\tcompiler.hooks.thisCompilation.tap( this.constructor.name, ( compilation ) => {\n\t\t\t// We tap into the time that Webpack has finished processing all the other assets\n\t\t\t// learn more: https://webpack.js.org/api/compilation-hooks/#processassets.\n\t\t\tcompilation.hooks.processAssets.tap( { name: this.constructor.name }, () => {\n\t\t\t\ttranslationCallExpressions = this.getModuleExpressionsMap( compilation );\n\t\t\t} );\n\t\t} );\n\n\t\tcompiler.hooks.afterEmit.tapPromise( this.constructor.name, async ( compilation ) => {\n\t\t\t// Create all the translations files based on the call expressions.\n\t\t\tawait this.createTranslationsFiles( compilation, translationCallExpressions );\n\t\t} );\n\t}\n\n\tgetModuleExpressionsMap( compilation: Compilation ) {\n\t\tconst moduleExpressionsMap = new Map();\n\n\t\t[ ...compilation.chunks ].forEach( ( chunk ) => {\n\t\t\tconst chunkJSFile = this.getFileFromChunk( chunk );\n\n\t\t\tif ( ! chunkJSFile ) {\n\t\t\t\t// There's no JS file in this chunk, no work for us. Typically a `style.css` from cache group.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcompilation.chunkGraph.getChunkModules( chunk ).forEach( ( module ) => {\n\t\t\t\tthis.getSubModulesToCheck( module ).forEach( ( subModule ) => {\n\t\t\t\t\tconst mainEntryFile = this.findMainModuleOfEntry( subModule, compilation );\n\n\t\t\t\t\tif ( ! moduleExpressionsMap.has( mainEntryFile ) ) {\n\t\t\t\t\t\tmoduleExpressionsMap.set( mainEntryFile, [] );\n\t\t\t\t\t}\n\n\t\t\t\t\t// Running over the submodules and find all the translation call expressions and their translators comment\n\t\t\t\t\t// (e.g `/* translators: %s: name*/ __('Hello %s', 'elementor')`),\n\t\t\t\t\t// extract them and add them to a Map, where the key is the main entry file, and the value is an array of all the\n\t\t\t\t\t// translation call expressions.\n\t\t\t\t\tthis.extractExpressionsFromSubmodule( subModule ).forEach( ( expression ) => {\n\t\t\t\t\t\tmoduleExpressionsMap.get( mainEntryFile ).push( expression );\n\t\t\t\t\t} );\n\t\t\t\t} );\n\t\t\t} );\n\t\t} );\n\n\t\treturn moduleExpressionsMap;\n\t}\n\n\tasync createTranslationsFiles( compilation: Compilation, translationCallExpressions: TranslationCallExpressions ) {\n\t\tconst promises = [ ...compilation.entrypoints ].map( ( [ id, entrypoint ] ) => {\n\t\t\tconst chunk = entrypoint.chunks.find( ( { name } ) => name === id );\n\n\t\t\tif ( ! chunk ) {\n\t\t\t\treturn Promise.resolve();\n\t\t\t}\n\n\t\t\tconst chunkJSFile = this.getFileFromChunk( chunk );\n\n\t\t\tif ( ! chunkJSFile ) {\n\t\t\t\treturn Promise.resolve();\n\t\t\t}\n\n\t\t\tconst { entry } = compilation.options;\n\n\t\t\tif ( ! entry || typeof entry !== 'object' || ! ( id in entry ) ) {\n\t\t\t\tthrow new Error( `Entry must be an object. e.g: {app: './path/to/app.js'}` );\n\t\t\t}\n\n\t\t\tconst mainFilePath = entry[ id ].import?.[ 0 ];\n\n\t\t\tif ( ! mainFilePath ) {\n\t\t\t\tthrow new Error( 'Entry is invalid' );\n\t\t\t}\n\n\t\t\tconst { path: outputPath } = compilation.options.output;\n\n\t\t\tif ( ! outputPath ) {\n\t\t\t\tthrow new Error( 'Output path is invalid' );\n\t\t\t}\n\n\t\t\tconst assetFilename = this.generateTranslationFilename(\n\t\t\t\toutputPath,\n\t\t\t\tcompilation.getPath( '[file]', { filename: chunkJSFile } )\n\t\t\t);\n\n\t\t\tconst assetFileContent = this.generateTranslationFileContent(\n\t\t\t\ttranslationCallExpressions.get( mainFilePath ) || []\n\t\t\t);\n\n\t\t\treturn fs.promises.writeFile( assetFilename, assetFileContent );\n\t\t} );\n\n\t\treturn await Promise.all( promises );\n\t}\n\n\tgetSubModulesToCheck( module: Module ) {\n\t\treturn [ ...( module.modules || [] ), module ]\n\t\t\t.filter( ( subModule ) => this.shouldCheckModule( subModule ) );\n\t}\n\n\tgetFileFromChunk( chunk: Chunk ) {\n\t\treturn [ ...chunk.files ].find( ( f ) => /\\.js$/i.test( f ) );\n\t}\n\n\tshouldCheckModule( module: Module ) {\n\t\treturn MODULE_FILTERS.every( ( filter ) => filter.test( module.userRequest || '' ) );\n\t}\n\n\tfindMainModuleOfEntry( module: Module, compilation: Compilation ) : string | null {\n\t\tconst issuer = compilation.moduleGraph.getIssuer( module );\n\n\t\tif ( issuer ) {\n\t\t\treturn this.findMainModuleOfEntry( issuer, compilation );\n\t\t}\n\n\t\treturn module.rawRequest || null;\n\t}\n\n\textractExpressionsFromSubmodule( subModule: Module ) {\n\t\tconst source = subModule?._source?._valueAsString;\n\n\t\tif ( ! source ) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst translationCallExpressions: TranslationCallExpression[] = [];\n\n\t\t[\n\t\t\t...TRANSLATIONS_REGEXPS,\n\t\t\t...COMMENTS_REGEXPS,\n\t\t].forEach( ( regexp ) => {\n\t\t\t[ ...source.matchAll( regexp ) ].forEach( ( res ) => {\n\t\t\t\ttranslationCallExpressions.push( {\n\t\t\t\t\ttype: COMMENTS_REGEXPS.includes( regexp ) ? 'comment' : 'call-expression',\n\t\t\t\t\tindex: res.index || 0,\n\t\t\t\t\tvalue: res[ 0 ],\n\t\t\t\t} );\n\t\t\t} );\n\t\t} );\n\n\t\treturn translationCallExpressions;\n\t}\n\n\tgenerateTranslationFilename( basePath: string, filename: string ) {\n\t\treturn path.join(\n\t\t\tbasePath,\n\t\t\tfilename.replace( /(\\.min)?\\.js$/i, '.strings.js' )\n\t\t);\n\t}\n\n\tgenerateTranslationFileContent( expressions: TranslationCallExpression[] ) {\n\t\treturn expressions\n\t\t\t// Sort by the index it was found in the file based on the regexp (and not by the order it was added to the array).\n\t\t\t.sort( ( a, b ) => a.index - b.index )\n\t\t\t// Add a semicolon when needed.\n\t\t\t.map( ( expr ) => `${ expr.value }${ expr.type === 'comment' ? '' : ';' }` )\n\t\t\t// Join all the expressions to a single string with line-breaks between them.\n\t\t\t.join( '\\n' );\n\t}\n}\n"],"mappings":"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","names":["promises"]}
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts","../src/utils.ts"],"sourcesContent":["import * as path from 'path';\nimport * as fs from 'fs';\nimport { Compilation, Compiler } from 'webpack';\nimport { EntrySettings } from './types';\nimport {\n\tcreateStringsFilePath,\n\tgenerateStringsFileContent,\n\tgetFilesContents,\n\tgetFilesPaths,\n} from './utils';\n\ntype Options = {\n\tpattern: ( entryPath: string, entryId: string ) => string;\n}\n\nexport default class ExtractI18nWordpressExpressionsWebpackPlugin {\n\toptions: Options;\n\n\tconstructor( options: Options ) {\n\t\tthis.options = options;\n\t}\n\n\tapply( compiler: Compiler ) {\n\t\tcompiler.hooks.afterEmit.tapPromise( this.constructor.name, async ( compilation ) => {\n\t\t\tconst entries = this.getEntries( compilation );\n\n\t\t\tawait Promise.all(\n\t\t\t\tentries.map( async ( entry ) => {\n\t\t\t\t\tconst fileContents = await getFilesContents(\n\t\t\t\t\t\tawait getFilesPaths( entry.pattern )\n\t\t\t\t\t);\n\n\t\t\t\t\tconst entryContent = generateStringsFileContent( fileContents );\n\n\t\t\t\t\t// Writing manually instead of using `chunk.files.add()` in order to avoid passing\n\t\t\t\t\t// the file through the loaders (transpilers, minifiers, etc.).\n\t\t\t\t\tawait fs.promises.writeFile( entry.path, entryContent );\n\t\t\t\t} )\n\t\t\t);\n\t\t} );\n\t}\n\n\tgetEntries( compilation: Compilation ) {\n\t\treturn [ ...compilation.entrypoints ]\n\t\t\t.map( ( [ id, entrypoint ] ) => {\n\t\t\t\tconst chunk = entrypoint.chunks.find( ( { name } ) => name === id );\n\n\t\t\t\tif ( ! chunk ) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst chunkJSFile = [ ...chunk.files ].find( ( f ) => /\\.(js|ts)$/i.test( f ) );\n\n\t\t\t\tif ( ! chunkJSFile ) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst { path: basePath } = compilation.options.output;\n\n\t\t\t\tif ( ! basePath ) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst filePath = createStringsFilePath( compilation.getPath( '[file]', { filename: chunkJSFile } ) );\n\n\t\t\t\treturn {\n\t\t\t\t\tid,\n\t\t\t\t\tchunk,\n\t\t\t\t\tpath: path.join( basePath, filePath ),\n\t\t\t\t\tpattern: this.options.pattern(\n\t\t\t\t\t\tpath.resolve( process.cwd(), entrypoint.origins[ 0 ].request ),\n\t\t\t\t\t\tid\n\t\t\t\t\t),\n\t\t\t\t};\n\t\t\t} )\n\t\t\t.filter( Boolean ) as EntrySettings[];\n\t}\n}\n","import { TranslationCallExpression } from './types';\nimport { glob } from 'glob';\nimport * as fs from 'fs';\n\nconst COMMENTS_REGEXPS = [\n\t// Matches translators comment block: `/* translators: %s */`.\n\t/\\/\\*[\\t ]*translators:.*\\*\\//gm,\n\t// Matches translators inline comment: `// translators: %s`.\n\t/(\\/\\/)[\\t ]*translators:[^\\r\\n]*/gm,\n] as const;\n\nconst TRANSLATIONS_REGEXPS = [\n\t// Matches translation functions: `__('Hello', 'elementor')`, `_n('Me', 'Us', 2, 'elementor-pro')`.\n\t/\\b_(?:_|n|nx|x)\\(.*?,\\s*(?<c>['\"`])[\\w-]+\\k<c>\\s*?\\)/sg,\n] as const;\n\nexport function createStringsFilePath( path: string, suffix = '.strings.js' ) {\n\treturn path.replace( /(\\.min)?\\.js$/i, suffix );\n}\n\nexport function getFilesPaths( pattern: string ) {\n\treturn glob( pattern, {\n\t\tignore: {\n\t\t\tignored: ( p ) => ! ( /\\.(js|ts|jsx|tsx)$/.test( p.name ) ),\n\t\t\tchildrenIgnored: ( p ) => p.isNamed( '__tests__' ) || p.isNamed( '__mocks__' ),\n\t\t},\n\n\t\t/**\n\t\t * Fix for Windows paths escaping.\n\t\t * Note: This means we don't support paths with special character (like `*`,`?`, etc.)\n\t\t * and only allow patterns that are constructed using `path.join()` or `path.resolve()`.\n\t\t *\n\t\t * @see https://github.com/isaacs/node-glob#options\n\t\t * @see https://github.com/isaacs/node-glob#windows\n\t\t * @see https://github.com/isaacs/node-glob/issues/212#issuecomment-1449062925\n\t\t */\n\t\twindowsPathsNoEscape: true,\n\t} );\n}\n\nexport function getFilesContents( paths: string[] ) {\n\treturn Promise.all( paths.map( ( filePath ) => fs.promises.readFile( filePath, 'utf-8' ) ) );\n}\n\nexport function generateStringsFileContent( contents: string[] ) {\n\treturn contents\n\t\t.map( ( content, ) => extractExpressions( content ) )\n\t\t.flat()\n\t\t// Add a semicolon when needed.\n\t\t.map( ( expr ) => `${ expr.value }${ expr.type === 'comment' ? '' : ';' }` )\n\t\t// Join all the expressions to a single string with line-breaks between them.\n\t\t.join( '\\n' );\n}\n\nfunction extractExpressions( content: string ): TranslationCallExpression[] {\n\tconst expressions: TranslationCallExpression[] = [];\n\n\t[ ...TRANSLATIONS_REGEXPS, ...COMMENTS_REGEXPS ].forEach( ( regexp ) => {\n\t\t[ ...content.matchAll( regexp ) ].forEach( ( res ) => {\n\t\t\texpressions.push( {\n\t\t\t\ttype: COMMENTS_REGEXPS.includes( regexp ) ? 'comment' : 'call-expression',\n\t\t\t\tindex: res.index || 0,\n\t\t\t\tvalue: res[ 0 ],\n\t\t\t} );\n\t\t} );\n\t} );\n\n\t// Sort by the index it was found in the file based on the regexp (and not by the order it was added to the array).\n\treturn expressions.sort( ( a, b ) => a.index - b.index );\n}\n"],"mappings":"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","names":["fs","path"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/extract-i18n-wordpress-expressions-webpack-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "Elementor Team",
|
|
6
6
|
"homepage": "https://elementor.com/",
|
|
@@ -34,5 +34,8 @@
|
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"webpack": "5.x"
|
|
36
36
|
},
|
|
37
|
-
"
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"glob": "^10.3.10"
|
|
39
|
+
},
|
|
40
|
+
"gitHead": "020375ae889f6e0e237c34421cef7445b704b273"
|
|
38
41
|
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
3
|
exports[`@elementor/extract-i18n-wordpress-expressions-webpack-plugin should extract translations from scripts 1`] = `
|
|
4
|
-
"__(
|
|
5
|
-
|
|
4
|
+
"__(
|
|
5
|
+
'Some long text with multiple lines in the function call',
|
|
6
|
+
'elementor'
|
|
7
|
+
);
|
|
8
|
+
__( "Unique domain", "some-plugin-slug" );
|
|
9
|
+
// translators: %1$s - special placeholder.
|
|
10
|
+
__('special placeholder %1$s','elementor' );
|
|
6
11
|
// translators: %s - regular comment.
|
|
7
|
-
__('regular comment %s','elementor');
|
|
12
|
+
__( 'regular comment %s','elementor');
|
|
8
13
|
/* translators: %s - comment block. */
|
|
9
|
-
__('comment block %s','elementor');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
__('comment block %s', 'elementor');
|
|
15
|
+
__( 'inside console log', 'elementor' );
|
|
16
|
+
_n( 'basic with', 'plural', 2, 'elementor' );
|
|
17
|
+
_nx( 'another with' , 'plural' , 2 , 'elementor' );
|
|
18
|
+
_x( 'context', 'elementor' );
|
|
19
|
+
__('hook','elementor');
|
|
20
|
+
__( "Component", 'elementor' );"
|
|
16
21
|
`;
|
|
22
|
+
|
|
23
|
+
exports[`@elementor/extract-i18n-wordpress-expressions-webpack-plugin should extract translations from scripts 2`] = `"__( 'from app 2', 'elementor' );"`;
|
|
@@ -12,34 +12,78 @@ jest.mock( 'fs', () => jest.requireActual( 'memfs' ) );
|
|
|
12
12
|
|
|
13
13
|
describe( '@elementor/extract-i18n-wordpress-expressions-webpack-plugin', () => {
|
|
14
14
|
beforeEach( () => {
|
|
15
|
-
|
|
15
|
+
// Entry file.
|
|
16
|
+
fs.mkdirSync( path.resolve( '/app/dist' ), { recursive: true } );
|
|
17
|
+
fs.writeFileSync( path.resolve( '/app/dist/index.js' ), '' );
|
|
18
|
+
|
|
19
|
+
fs.mkdirSync( path.resolve( '/app2/dist' ), { recursive: true } );
|
|
20
|
+
fs.writeFileSync( path.resolve( '/app2/dist/index.js' ), '' );
|
|
21
|
+
|
|
22
|
+
// Should be in output.
|
|
23
|
+
fs.mkdirSync( path.resolve( '/app/src' ), { recursive: true } );
|
|
24
|
+
fs.mkdirSync( path.resolve( '/app/src/components' ), { recursive: true } );
|
|
25
|
+
fs.mkdirSync( path.resolve( '/app/src/hooks' ), { recursive: true } );
|
|
26
|
+
|
|
27
|
+
fs.writeFileSync( path.resolve( '/app/src/components/component.js' ), `
|
|
16
28
|
export default function Component() {
|
|
17
|
-
|
|
29
|
+
return __( "Component", 'elementor' );
|
|
30
|
+
}
|
|
31
|
+
` );
|
|
18
32
|
|
|
19
|
-
|
|
33
|
+
fs.writeFileSync( path.resolve( '/app/src/hooks/hook.ts' ), `
|
|
34
|
+
export default function useHook() {
|
|
35
|
+
return __('hook','elementor');
|
|
36
|
+
}
|
|
37
|
+
` );
|
|
20
38
|
|
|
21
|
-
|
|
39
|
+
fs.writeFileSync( path.resolve( '/app/src/index.js' ), `
|
|
40
|
+
export default function Index() {
|
|
41
|
+
__(
|
|
42
|
+
'Some long text with multiple lines in the function call',
|
|
43
|
+
'elementor'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
__( "Unique domain", "some-plugin-slug" );
|
|
47
|
+
|
|
48
|
+
// translators: %1$s - special placeholder.
|
|
49
|
+
const withSpecialPlaceHolder = __('special placeholder %1$s','elementor' );
|
|
22
50
|
|
|
23
51
|
// translators: %s - regular comment.
|
|
24
|
-
const withComment = __('regular comment %s','elementor');
|
|
52
|
+
const withComment = __( 'regular comment %s','elementor');
|
|
25
53
|
|
|
26
54
|
/* translators: %s - comment block. */
|
|
27
|
-
const withCommentBlock = __('comment block %s','elementor');
|
|
28
|
-
|
|
29
|
-
// translators: %1$s - special placeholder.
|
|
30
|
-
const withSpecialPlaceHolder = __('special placeholder %1$s','elementor');
|
|
55
|
+
const withCommentBlock = __('comment block %s', 'elementor');
|
|
31
56
|
|
|
32
|
-
console.log(__('inside console log', 'elementor'))
|
|
57
|
+
console.log(__( 'inside console log', 'elementor' ))
|
|
33
58
|
|
|
34
59
|
return [
|
|
35
|
-
_n('basic with','plural',2,'elementor'),
|
|
36
|
-
_nx('another with','plural',2,'elementor'),
|
|
37
|
-
_x('context','elementor'),
|
|
38
|
-
invalid__('invalid','elementor'),
|
|
39
|
-
]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
_n( 'basic with', 'plural', 2, 'elementor' ),
|
|
61
|
+
_nx( 'another with' , 'plural' , 2 , 'elementor' ),
|
|
62
|
+
_x( 'context', 'elementor' ),
|
|
63
|
+
invalid__( 'invalid', 'elementor' ),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
` );
|
|
67
|
+
|
|
68
|
+
fs.mkdirSync( path.resolve( '/app2/src' ), { recursive: true } );
|
|
69
|
+
fs.writeFileSync( path.resolve( '/app2/src/index.js' ), `
|
|
70
|
+
export const test = __( 'from app 2', 'elementor' );
|
|
71
|
+
` );
|
|
72
|
+
|
|
73
|
+
// Should not be in output.
|
|
74
|
+
const ignoredContent = `
|
|
75
|
+
export default function ShouldIgnoreComponent() {
|
|
76
|
+
return __('should ignore','elementor');
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
fs.mkdirSync( path.resolve( '/app/fake-src' ), { recursive: true } );
|
|
81
|
+
fs.writeFileSync( path.resolve( '/app/fake-src/index.js' ), ignoredContent );
|
|
82
|
+
|
|
83
|
+
fs.mkdirSync( path.resolve( '/app/src/__tests__' ), { recursive: true } );
|
|
84
|
+
fs.writeFileSync( path.resolve( '/app/src/__tests__/mock.test.js' ), ignoredContent );
|
|
85
|
+
|
|
86
|
+
fs.writeFileSync( path.resolve( '/app/src/not-a-js-file.txt' ), ignoredContent );
|
|
43
87
|
} );
|
|
44
88
|
|
|
45
89
|
afterEach( () => {
|
|
@@ -51,20 +95,20 @@ describe( '@elementor/extract-i18n-wordpress-expressions-webpack-plugin', () =>
|
|
|
51
95
|
const compiler = webpack( {
|
|
52
96
|
mode: 'development',
|
|
53
97
|
entry: {
|
|
54
|
-
app: path.resolve( '/app.js' ),
|
|
98
|
+
app: path.resolve( '/app/dist/index.js' ),
|
|
99
|
+
app2: path.resolve( '/app2/dist/index.js' ),
|
|
55
100
|
},
|
|
56
101
|
output: {
|
|
57
|
-
filename: '[name].js',
|
|
58
|
-
path: path.resolve( '/
|
|
102
|
+
filename: '[name]/[name].js',
|
|
103
|
+
path: path.resolve( '/output' ),
|
|
59
104
|
},
|
|
60
105
|
plugins: [
|
|
61
|
-
new ExtractI18nWordpressExpressionsWebpackPlugin(
|
|
106
|
+
new ExtractI18nWordpressExpressionsWebpackPlugin( {
|
|
107
|
+
pattern: ( entryPath ) => path.resolve( entryPath, '../../src/**/*.{ts,tsx,js,jsx}' ),
|
|
108
|
+
} ),
|
|
62
109
|
],
|
|
63
110
|
} );
|
|
64
111
|
|
|
65
|
-
// Expect.
|
|
66
|
-
expect.assertions( 4 );
|
|
67
|
-
|
|
68
112
|
// Act.
|
|
69
113
|
compiler.run( ( err, stats ) => {
|
|
70
114
|
// Assert.
|
|
@@ -72,9 +116,13 @@ describe( '@elementor/extract-i18n-wordpress-expressions-webpack-plugin', () =>
|
|
|
72
116
|
expect( stats?.hasErrors() ).toBe( false );
|
|
73
117
|
expect( stats?.hasWarnings() ).toBe( false );
|
|
74
118
|
|
|
75
|
-
|
|
119
|
+
expect(
|
|
120
|
+
fs.readFileSync( path.resolve( `/output/app/app.strings.js` ), { encoding: 'utf8' } )
|
|
121
|
+
).toMatchSnapshot();
|
|
76
122
|
|
|
77
|
-
expect(
|
|
123
|
+
expect(
|
|
124
|
+
fs.readFileSync( path.resolve( `/output/app2/app2.strings.js` ), { encoding: 'utf8' } )
|
|
125
|
+
).toMatchSnapshot();
|
|
78
126
|
|
|
79
127
|
done();
|
|
80
128
|
} );
|
package/src/index.ts
CHANGED
|
@@ -1,201 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { Compilation, Compiler, Chunk, Module as WebpackModule } from 'webpack';
|
|
4
|
-
|
|
5
|
-
type Module = WebpackModule & {
|
|
6
|
-
userRequest?: string;
|
|
7
|
-
rawRequest?: string;
|
|
8
|
-
_source?: {
|
|
9
|
-
_valueAsString?: string;
|
|
10
|
-
},
|
|
11
|
-
modules?: Module[];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type TranslationCallExpression = {
|
|
15
|
-
type: 'comment' | 'call-expression';
|
|
16
|
-
index: number;
|
|
17
|
-
value: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type TranslationCallExpressions = Map<string, TranslationCallExpression[]>;
|
|
21
|
-
|
|
22
|
-
const MODULE_FILTERS = Object.freeze( [ /(([^!?\s]+?)(?:\.js|\.jsx|\.ts|\.tsx))$/, /^((?!node_modules).)*$/ ] );
|
|
23
|
-
|
|
24
|
-
const COMMENTS_REGEXPS = Object.freeze( [
|
|
25
|
-
// Matches translators comment block: `/* translators: %s */`.
|
|
26
|
-
/\/\*[\t ]*translators:.*\*\//gm,
|
|
27
|
-
/// Matches translators inline comment: `// translators: %s`.
|
|
28
|
-
/(\/\/)[\t ]*translators:[^\r\n]*/gm,
|
|
29
|
-
] );
|
|
30
|
-
|
|
31
|
-
const TRANSLATIONS_REGEXPS = Object.freeze( [
|
|
32
|
-
// Matches translation functions: `__('Hello', 'elementor')`, `_n('Me', 'Us', 2, 'elementor-pro')`.
|
|
33
|
-
/\b_(?:_|n|nx|x)\(.*?,\s*(?<c>['"`])[\w-]+\k<c>\)/gm,
|
|
34
|
-
] );
|
|
35
|
-
|
|
36
|
-
export class ExtractI18nWordpressExpressionsWebpackPlugin {
|
|
37
|
-
apply( compiler: Compiler ) {
|
|
38
|
-
// Learn more about Webpack plugin system: https://webpack.js.org/api/plugins/
|
|
39
|
-
|
|
40
|
-
let translationCallExpressions: TranslationCallExpressions = new Map();
|
|
41
|
-
|
|
42
|
-
// Learn more about Webpack compilation process and hooks: https://webpack.js.org/api/compilation-hooks/
|
|
43
|
-
compiler.hooks.thisCompilation.tap( this.constructor.name, ( compilation ) => {
|
|
44
|
-
// We tap into the time that Webpack has finished processing all the other assets
|
|
45
|
-
// learn more: https://webpack.js.org/api/compilation-hooks/#processassets.
|
|
46
|
-
compilation.hooks.processAssets.tap( { name: this.constructor.name }, () => {
|
|
47
|
-
translationCallExpressions = this.getModuleExpressionsMap( compilation );
|
|
48
|
-
} );
|
|
49
|
-
} );
|
|
50
|
-
|
|
51
|
-
compiler.hooks.afterEmit.tapPromise( this.constructor.name, async ( compilation ) => {
|
|
52
|
-
// Create all the translations files based on the call expressions.
|
|
53
|
-
await this.createTranslationsFiles( compilation, translationCallExpressions );
|
|
54
|
-
} );
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
getModuleExpressionsMap( compilation: Compilation ) {
|
|
58
|
-
const moduleExpressionsMap = new Map();
|
|
59
|
-
|
|
60
|
-
[ ...compilation.chunks ].forEach( ( chunk ) => {
|
|
61
|
-
const chunkJSFile = this.getFileFromChunk( chunk );
|
|
62
|
-
|
|
63
|
-
if ( ! chunkJSFile ) {
|
|
64
|
-
// There's no JS file in this chunk, no work for us. Typically a `style.css` from cache group.
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
compilation.chunkGraph.getChunkModules( chunk ).forEach( ( module ) => {
|
|
69
|
-
this.getSubModulesToCheck( module ).forEach( ( subModule ) => {
|
|
70
|
-
const mainEntryFile = this.findMainModuleOfEntry( subModule, compilation );
|
|
71
|
-
|
|
72
|
-
if ( ! moduleExpressionsMap.has( mainEntryFile ) ) {
|
|
73
|
-
moduleExpressionsMap.set( mainEntryFile, [] );
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Running over the submodules and find all the translation call expressions and their translators comment
|
|
77
|
-
// (e.g `/* translators: %s: name*/ __('Hello %s', 'elementor')`),
|
|
78
|
-
// extract them and add them to a Map, where the key is the main entry file, and the value is an array of all the
|
|
79
|
-
// translation call expressions.
|
|
80
|
-
this.extractExpressionsFromSubmodule( subModule ).forEach( ( expression ) => {
|
|
81
|
-
moduleExpressionsMap.get( mainEntryFile ).push( expression );
|
|
82
|
-
} );
|
|
83
|
-
} );
|
|
84
|
-
} );
|
|
85
|
-
} );
|
|
86
|
-
|
|
87
|
-
return moduleExpressionsMap;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async createTranslationsFiles( compilation: Compilation, translationCallExpressions: TranslationCallExpressions ) {
|
|
91
|
-
const promises = [ ...compilation.entrypoints ].map( ( [ id, entrypoint ] ) => {
|
|
92
|
-
const chunk = entrypoint.chunks.find( ( { name } ) => name === id );
|
|
93
|
-
|
|
94
|
-
if ( ! chunk ) {
|
|
95
|
-
return Promise.resolve();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const chunkJSFile = this.getFileFromChunk( chunk );
|
|
99
|
-
|
|
100
|
-
if ( ! chunkJSFile ) {
|
|
101
|
-
return Promise.resolve();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const { entry } = compilation.options;
|
|
105
|
-
|
|
106
|
-
if ( ! entry || typeof entry !== 'object' || ! ( id in entry ) ) {
|
|
107
|
-
throw new Error( `Entry must be an object. e.g: {app: './path/to/app.js'}` );
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const mainFilePath = entry[ id ].import?.[ 0 ];
|
|
111
|
-
|
|
112
|
-
if ( ! mainFilePath ) {
|
|
113
|
-
throw new Error( 'Entry is invalid' );
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const { path: outputPath } = compilation.options.output;
|
|
117
|
-
|
|
118
|
-
if ( ! outputPath ) {
|
|
119
|
-
throw new Error( 'Output path is invalid' );
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const assetFilename = this.generateTranslationFilename(
|
|
123
|
-
outputPath,
|
|
124
|
-
compilation.getPath( '[file]', { filename: chunkJSFile } )
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
const assetFileContent = this.generateTranslationFileContent(
|
|
128
|
-
translationCallExpressions.get( mainFilePath ) || []
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
return fs.promises.writeFile( assetFilename, assetFileContent );
|
|
132
|
-
} );
|
|
133
|
-
|
|
134
|
-
return await Promise.all( promises );
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
getSubModulesToCheck( module: Module ) {
|
|
138
|
-
return [ ...( module.modules || [] ), module ]
|
|
139
|
-
.filter( ( subModule ) => this.shouldCheckModule( subModule ) );
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
getFileFromChunk( chunk: Chunk ) {
|
|
143
|
-
return [ ...chunk.files ].find( ( f ) => /\.js$/i.test( f ) );
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
shouldCheckModule( module: Module ) {
|
|
147
|
-
return MODULE_FILTERS.every( ( filter ) => filter.test( module.userRequest || '' ) );
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
findMainModuleOfEntry( module: Module, compilation: Compilation ) : string | null {
|
|
151
|
-
const issuer = compilation.moduleGraph.getIssuer( module );
|
|
152
|
-
|
|
153
|
-
if ( issuer ) {
|
|
154
|
-
return this.findMainModuleOfEntry( issuer, compilation );
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return module.rawRequest || null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
extractExpressionsFromSubmodule( subModule: Module ) {
|
|
161
|
-
const source = subModule?._source?._valueAsString;
|
|
162
|
-
|
|
163
|
-
if ( ! source ) {
|
|
164
|
-
return [];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const translationCallExpressions: TranslationCallExpression[] = [];
|
|
168
|
-
|
|
169
|
-
[
|
|
170
|
-
...TRANSLATIONS_REGEXPS,
|
|
171
|
-
...COMMENTS_REGEXPS,
|
|
172
|
-
].forEach( ( regexp ) => {
|
|
173
|
-
[ ...source.matchAll( regexp ) ].forEach( ( res ) => {
|
|
174
|
-
translationCallExpressions.push( {
|
|
175
|
-
type: COMMENTS_REGEXPS.includes( regexp ) ? 'comment' : 'call-expression',
|
|
176
|
-
index: res.index || 0,
|
|
177
|
-
value: res[ 0 ],
|
|
178
|
-
} );
|
|
179
|
-
} );
|
|
180
|
-
} );
|
|
181
|
-
|
|
182
|
-
return translationCallExpressions;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
generateTranslationFilename( basePath: string, filename: string ) {
|
|
186
|
-
return path.join(
|
|
187
|
-
basePath,
|
|
188
|
-
filename.replace( /(\.min)?\.js$/i, '.strings.js' )
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
generateTranslationFileContent( expressions: TranslationCallExpression[] ) {
|
|
193
|
-
return expressions
|
|
194
|
-
// Sort by the index it was found in the file based on the regexp (and not by the order it was added to the array).
|
|
195
|
-
.sort( ( a, b ) => a.index - b.index )
|
|
196
|
-
// Add a semicolon when needed.
|
|
197
|
-
.map( ( expr ) => `${ expr.value }${ expr.type === 'comment' ? '' : ';' }` )
|
|
198
|
-
// Join all the expressions to a single string with line-breaks between them.
|
|
199
|
-
.join( '\n' );
|
|
200
|
-
}
|
|
201
|
-
}
|
|
1
|
+
export { default as ExtractI18nWordpressExpressionsWebpackPlugin } from './plugin';
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { Compilation, Compiler } from 'webpack';
|
|
4
|
+
import { EntrySettings } from './types';
|
|
5
|
+
import {
|
|
6
|
+
createStringsFilePath,
|
|
7
|
+
generateStringsFileContent,
|
|
8
|
+
getFilesContents,
|
|
9
|
+
getFilesPaths,
|
|
10
|
+
} from './utils';
|
|
11
|
+
|
|
12
|
+
type Options = {
|
|
13
|
+
pattern: ( entryPath: string, entryId: string ) => string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default class ExtractI18nWordpressExpressionsWebpackPlugin {
|
|
17
|
+
options: Options;
|
|
18
|
+
|
|
19
|
+
constructor( options: Options ) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
apply( compiler: Compiler ) {
|
|
24
|
+
compiler.hooks.afterEmit.tapPromise( this.constructor.name, async ( compilation ) => {
|
|
25
|
+
const entries = this.getEntries( compilation );
|
|
26
|
+
|
|
27
|
+
await Promise.all(
|
|
28
|
+
entries.map( async ( entry ) => {
|
|
29
|
+
const fileContents = await getFilesContents(
|
|
30
|
+
await getFilesPaths( entry.pattern )
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const entryContent = generateStringsFileContent( fileContents );
|
|
34
|
+
|
|
35
|
+
// Writing manually instead of using `chunk.files.add()` in order to avoid passing
|
|
36
|
+
// the file through the loaders (transpilers, minifiers, etc.).
|
|
37
|
+
await fs.promises.writeFile( entry.path, entryContent );
|
|
38
|
+
} )
|
|
39
|
+
);
|
|
40
|
+
} );
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getEntries( compilation: Compilation ) {
|
|
44
|
+
return [ ...compilation.entrypoints ]
|
|
45
|
+
.map( ( [ id, entrypoint ] ) => {
|
|
46
|
+
const chunk = entrypoint.chunks.find( ( { name } ) => name === id );
|
|
47
|
+
|
|
48
|
+
if ( ! chunk ) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const chunkJSFile = [ ...chunk.files ].find( ( f ) => /\.(js|ts)$/i.test( f ) );
|
|
53
|
+
|
|
54
|
+
if ( ! chunkJSFile ) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { path: basePath } = compilation.options.output;
|
|
59
|
+
|
|
60
|
+
if ( ! basePath ) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const filePath = createStringsFilePath( compilation.getPath( '[file]', { filename: chunkJSFile } ) );
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
id,
|
|
68
|
+
chunk,
|
|
69
|
+
path: path.join( basePath, filePath ),
|
|
70
|
+
pattern: this.options.pattern(
|
|
71
|
+
path.resolve( process.cwd(), entrypoint.origins[ 0 ].request ),
|
|
72
|
+
id
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
} )
|
|
76
|
+
.filter( Boolean ) as EntrySettings[];
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Chunk } from 'webpack';
|
|
2
|
+
|
|
3
|
+
export type EntrySettings = {
|
|
4
|
+
id: string,
|
|
5
|
+
chunk: Chunk,
|
|
6
|
+
path: string,
|
|
7
|
+
pattern: string,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type TranslationCallExpression = {
|
|
11
|
+
type: 'comment' | 'call-expression',
|
|
12
|
+
index: number,
|
|
13
|
+
value: string,
|
|
14
|
+
};
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { TranslationCallExpression } from './types';
|
|
2
|
+
import { glob } from 'glob';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
|
|
5
|
+
const COMMENTS_REGEXPS = [
|
|
6
|
+
// Matches translators comment block: `/* translators: %s */`.
|
|
7
|
+
/\/\*[\t ]*translators:.*\*\//gm,
|
|
8
|
+
// Matches translators inline comment: `// translators: %s`.
|
|
9
|
+
/(\/\/)[\t ]*translators:[^\r\n]*/gm,
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
const TRANSLATIONS_REGEXPS = [
|
|
13
|
+
// Matches translation functions: `__('Hello', 'elementor')`, `_n('Me', 'Us', 2, 'elementor-pro')`.
|
|
14
|
+
/\b_(?:_|n|nx|x)\(.*?,\s*(?<c>['"`])[\w-]+\k<c>\s*?\)/sg,
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export function createStringsFilePath( path: string, suffix = '.strings.js' ) {
|
|
18
|
+
return path.replace( /(\.min)?\.js$/i, suffix );
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getFilesPaths( pattern: string ) {
|
|
22
|
+
return glob( pattern, {
|
|
23
|
+
ignore: {
|
|
24
|
+
ignored: ( p ) => ! ( /\.(js|ts|jsx|tsx)$/.test( p.name ) ),
|
|
25
|
+
childrenIgnored: ( p ) => p.isNamed( '__tests__' ) || p.isNamed( '__mocks__' ),
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fix for Windows paths escaping.
|
|
30
|
+
* Note: This means we don't support paths with special character (like `*`,`?`, etc.)
|
|
31
|
+
* and only allow patterns that are constructed using `path.join()` or `path.resolve()`.
|
|
32
|
+
*
|
|
33
|
+
* @see https://github.com/isaacs/node-glob#options
|
|
34
|
+
* @see https://github.com/isaacs/node-glob#windows
|
|
35
|
+
* @see https://github.com/isaacs/node-glob/issues/212#issuecomment-1449062925
|
|
36
|
+
*/
|
|
37
|
+
windowsPathsNoEscape: true,
|
|
38
|
+
} );
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getFilesContents( paths: string[] ) {
|
|
42
|
+
return Promise.all( paths.map( ( filePath ) => fs.promises.readFile( filePath, 'utf-8' ) ) );
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function generateStringsFileContent( contents: string[] ) {
|
|
46
|
+
return contents
|
|
47
|
+
.map( ( content, ) => extractExpressions( content ) )
|
|
48
|
+
.flat()
|
|
49
|
+
// Add a semicolon when needed.
|
|
50
|
+
.map( ( expr ) => `${ expr.value }${ expr.type === 'comment' ? '' : ';' }` )
|
|
51
|
+
// Join all the expressions to a single string with line-breaks between them.
|
|
52
|
+
.join( '\n' );
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractExpressions( content: string ): TranslationCallExpression[] {
|
|
56
|
+
const expressions: TranslationCallExpression[] = [];
|
|
57
|
+
|
|
58
|
+
[ ...TRANSLATIONS_REGEXPS, ...COMMENTS_REGEXPS ].forEach( ( regexp ) => {
|
|
59
|
+
[ ...content.matchAll( regexp ) ].forEach( ( res ) => {
|
|
60
|
+
expressions.push( {
|
|
61
|
+
type: COMMENTS_REGEXPS.includes( regexp ) ? 'comment' : 'call-expression',
|
|
62
|
+
index: res.index || 0,
|
|
63
|
+
value: res[ 0 ],
|
|
64
|
+
} );
|
|
65
|
+
} );
|
|
66
|
+
} );
|
|
67
|
+
|
|
68
|
+
// Sort by the index it was found in the file based on the regexp (and not by the order it was added to the array).
|
|
69
|
+
return expressions.sort( ( a, b ) => a.index - b.index );
|
|
70
|
+
}
|