@intlayer/babel 8.6.10 → 8.7.0-canary.0
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/dist/cjs/babel-plugin-intlayer-extract.cjs +1 -1
- package/dist/cjs/babel-plugin-intlayer-field-rename.cjs +172 -0
- package/dist/cjs/babel-plugin-intlayer-field-rename.cjs.map +1 -0
- package/dist/cjs/babel-plugin-intlayer-optimize.cjs +1 -1
- package/dist/cjs/babel-plugin-intlayer-usage-analyzer.cjs +171 -0
- package/dist/cjs/babel-plugin-intlayer-usage-analyzer.cjs.map +1 -0
- package/dist/cjs/extractContent/contentWriter.cjs +1 -1
- package/dist/cjs/extractContent/extractContent.cjs +1 -1
- package/dist/cjs/extractContent/utils/extractDictionaryInfo.cjs +1 -1
- package/dist/cjs/extractContent/utils/resolveDictionaryKey.cjs +1 -1
- package/dist/cjs/extractScriptBlocks.cjs +80 -0
- package/dist/cjs/extractScriptBlocks.cjs.map +1 -0
- package/dist/cjs/getOptimizePluginOptions.cjs +1 -1
- package/dist/cjs/index.cjs +19 -0
- package/dist/cjs/transformers.cjs +150 -0
- package/dist/cjs/transformers.cjs.map +1 -0
- package/dist/esm/babel-plugin-intlayer-extract.mjs +1 -1
- package/dist/esm/babel-plugin-intlayer-field-rename.mjs +169 -0
- package/dist/esm/babel-plugin-intlayer-field-rename.mjs.map +1 -0
- package/dist/esm/babel-plugin-intlayer-optimize.mjs +1 -1
- package/dist/esm/babel-plugin-intlayer-usage-analyzer.mjs +167 -0
- package/dist/esm/babel-plugin-intlayer-usage-analyzer.mjs.map +1 -0
- package/dist/esm/extractContent/contentWriter.mjs +1 -1
- package/dist/esm/extractContent/extractContent.mjs +1 -1
- package/dist/esm/extractContent/utils/extractDictionaryInfo.mjs +1 -1
- package/dist/esm/extractContent/utils/resolveDictionaryKey.mjs +1 -1
- package/dist/esm/extractScriptBlocks.mjs +77 -0
- package/dist/esm/extractScriptBlocks.mjs.map +1 -0
- package/dist/esm/getOptimizePluginOptions.mjs +1 -1
- package/dist/esm/index.mjs +5 -1
- package/dist/esm/transformers.mjs +142 -0
- package/dist/esm/transformers.mjs.map +1 -0
- package/dist/types/babel-plugin-intlayer-field-rename.d.ts +57 -0
- package/dist/types/babel-plugin-intlayer-field-rename.d.ts.map +1 -0
- package/dist/types/babel-plugin-intlayer-usage-analyzer.d.ts +119 -0
- package/dist/types/babel-plugin-intlayer-usage-analyzer.d.ts.map +1 -0
- package/dist/types/extractScriptBlocks.d.ts +45 -0
- package/dist/types/extractScriptBlocks.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/types/transformers.d.ts +64 -0
- package/dist/types/transformers.d.ts.map +1 -0
- package/package.json +10 -10
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_babel_plugin_intlayer_usage_analyzer = require('./babel-plugin-intlayer-usage-analyzer.cjs');
|
|
3
|
+
const require_extractScriptBlocks = require('./extractScriptBlocks.cjs');
|
|
2
4
|
const require_extractContent_utils_extractDictionaryKey = require('./extractContent/utils/extractDictionaryKey.cjs');
|
|
3
5
|
const require_extractContent_utils_extractDictionaryInfo = require('./extractContent/utils/extractDictionaryInfo.cjs');
|
|
4
6
|
const require_extractContent_contentWriter = require('./extractContent/contentWriter.cjs');
|
|
@@ -9,26 +11,43 @@ const require_extractContent_utils_generateKey = require('./extractContent/utils
|
|
|
9
11
|
const require_extractContent_utils_getComponentName = require('./extractContent/utils/getComponentName.cjs');
|
|
10
12
|
const require_extractContent_extractContent = require('./extractContent/extractContent.cjs');
|
|
11
13
|
const require_babel_plugin_intlayer_extract = require('./babel-plugin-intlayer-extract.cjs');
|
|
14
|
+
const require_babel_plugin_intlayer_field_rename = require('./babel-plugin-intlayer-field-rename.cjs');
|
|
12
15
|
const require_getOptimizePluginOptions = require('./getOptimizePluginOptions.cjs');
|
|
13
16
|
const require_babel_plugin_intlayer_optimize = require('./babel-plugin-intlayer-optimize.cjs');
|
|
17
|
+
const require_transformers = require('./transformers.cjs');
|
|
14
18
|
|
|
15
19
|
exports.ATTRIBUTES_TO_EXTRACT = require_extractContent_utils_constants.ATTRIBUTES_TO_EXTRACT;
|
|
20
|
+
exports.BABEL_PARSER_OPTIONS = require_transformers.BABEL_PARSER_OPTIONS;
|
|
21
|
+
exports.INTLAYER_CALLER_NAMES = require_babel_plugin_intlayer_usage_analyzer.INTLAYER_CALLER_NAMES;
|
|
22
|
+
exports.INTLAYER_USAGE_REGEX = require_transformers.INTLAYER_USAGE_REGEX;
|
|
16
23
|
exports.SERVER_CAPABLE_PACKAGES = require_extractContent_utils_constants.SERVER_CAPABLE_PACKAGES;
|
|
24
|
+
exports.SOURCE_FILE_REGEX = require_transformers.SOURCE_FILE_REGEX;
|
|
25
|
+
exports.analyzeFieldUsageInFile = require_transformers.analyzeFieldUsageInFile;
|
|
26
|
+
exports.buildNestedRenameMapFromContent = require_babel_plugin_intlayer_field_rename.buildNestedRenameMapFromContent;
|
|
27
|
+
exports.createPruneContext = require_babel_plugin_intlayer_usage_analyzer.createPruneContext;
|
|
17
28
|
exports.detectPackageName = require_extractContent_utils_detectPackageName.detectPackageName;
|
|
18
29
|
exports.extractContent = require_extractContent_extractContent.extractContent;
|
|
19
30
|
exports.extractContentSync = require_extractContent_extractContent.extractContentSync;
|
|
20
31
|
exports.extractDictionaryInfo = require_extractContent_utils_extractDictionaryInfo.extractDictionaryInfo;
|
|
21
32
|
exports.extractDictionaryKey = require_extractContent_utils_extractDictionaryKey.extractDictionaryKey;
|
|
22
33
|
exports.extractDictionaryKeyFromPath = require_extractContent_utils_extractDictionaryKey.extractDictionaryKeyFromPath;
|
|
34
|
+
exports.extractScriptBlocks = require_extractScriptBlocks.extractScriptBlocks;
|
|
23
35
|
exports.generateKey = require_extractContent_utils_generateKey.generateKey;
|
|
36
|
+
exports.generateShortFieldName = require_babel_plugin_intlayer_field_rename.generateShortFieldName;
|
|
24
37
|
exports.getComponentName = require_extractContent_utils_getComponentName.getComponentName;
|
|
25
38
|
exports.getExtractPluginOptions = require_getExtractPluginOptions.getExtractPluginOptions;
|
|
26
39
|
exports.getOptimizePluginOptions = require_getOptimizePluginOptions.getOptimizePluginOptions;
|
|
27
40
|
exports.getOutput = require_extractContent_utils_extractDictionaryInfo.getOutput;
|
|
41
|
+
exports.injectScriptBlocks = require_extractScriptBlocks.injectScriptBlocks;
|
|
28
42
|
exports.intlayerExtractBabelPlugin = require_babel_plugin_intlayer_extract.intlayerExtractBabelPlugin;
|
|
29
43
|
exports.intlayerOptimizeBabelPlugin = require_babel_plugin_intlayer_optimize.intlayerOptimizeBabelPlugin;
|
|
44
|
+
exports.makeFieldRenameBabelPlugin = require_babel_plugin_intlayer_field_rename.makeFieldRenameBabelPlugin;
|
|
45
|
+
exports.makeUsageAnalyzerBabelPlugin = require_babel_plugin_intlayer_usage_analyzer.makeUsageAnalyzerBabelPlugin;
|
|
30
46
|
exports.mergeWithExistingMultilingualDictionary = require_extractContent_contentWriter.mergeWithExistingMultilingualDictionary;
|
|
31
47
|
exports.mergeWithExistingPerLocaleDictionary = require_extractContent_contentWriter.mergeWithExistingPerLocaleDictionary;
|
|
48
|
+
exports.optimizeSourceFile = require_transformers.optimizeSourceFile;
|
|
32
49
|
exports.packageList = require_extractContent_utils_constants.packageList;
|
|
50
|
+
exports.renameFieldsInCode = require_transformers.renameFieldsInCode;
|
|
51
|
+
exports.renameFieldsInSourceFile = require_transformers.renameFieldsInSourceFile;
|
|
33
52
|
exports.resolveContentFilePaths = require_extractContent_utils_extractDictionaryInfo.resolveContentFilePaths;
|
|
34
53
|
exports.writeContentHelper = require_extractContent_contentWriter.writeContentHelper;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
|
|
3
|
+
const require_babel_plugin_intlayer_usage_analyzer = require('./babel-plugin-intlayer-usage-analyzer.cjs');
|
|
4
|
+
const require_extractScriptBlocks = require('./extractScriptBlocks.cjs');
|
|
5
|
+
const require_babel_plugin_intlayer_field_rename = require('./babel-plugin-intlayer-field-rename.cjs');
|
|
6
|
+
const require_babel_plugin_intlayer_optimize = require('./babel-plugin-intlayer-optimize.cjs');
|
|
7
|
+
let _babel_core = require("@babel/core");
|
|
8
|
+
|
|
9
|
+
//#region src/transformers.ts
|
|
10
|
+
/**
|
|
11
|
+
* Babel parser options covering the superset of syntaxes used across all
|
|
12
|
+
* supported frameworks (React / Vue / Svelte / Angular / …).
|
|
13
|
+
*/
|
|
14
|
+
const BABEL_PARSER_OPTIONS = {
|
|
15
|
+
sourceType: "module",
|
|
16
|
+
allowImportExportEverywhere: true,
|
|
17
|
+
plugins: [
|
|
18
|
+
"typescript",
|
|
19
|
+
"jsx",
|
|
20
|
+
"decorators-legacy",
|
|
21
|
+
"classProperties",
|
|
22
|
+
"objectRestSpread",
|
|
23
|
+
"asyncGenerators",
|
|
24
|
+
"functionBind",
|
|
25
|
+
"exportDefaultFrom",
|
|
26
|
+
"exportNamespaceFrom",
|
|
27
|
+
"dynamicImport",
|
|
28
|
+
"nullishCoalescingOperator",
|
|
29
|
+
"optionalChaining"
|
|
30
|
+
]
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Fast pre-check: matches files that could contain intlayer calls.
|
|
34
|
+
* Avoids running Babel on files with no relevant identifiers.
|
|
35
|
+
*/
|
|
36
|
+
const INTLAYER_USAGE_REGEX = /\b(use|get)Intlayer\b/;
|
|
37
|
+
/**
|
|
38
|
+
* Matches source files that are valid targets for usage analysis and Babel
|
|
39
|
+
* transformation. Excludes sourcemap files, declaration files, and other
|
|
40
|
+
* non-source extensions.
|
|
41
|
+
*/
|
|
42
|
+
const SOURCE_FILE_REGEX = /\.(tsx?|[mc]?jsx?|vue|svelte)$/;
|
|
43
|
+
/**
|
|
44
|
+
* Runs the usage-analysis Babel plugin on a single JS/TS code string.
|
|
45
|
+
*
|
|
46
|
+
* This is analysis-only: the transformed code output is discarded.
|
|
47
|
+
* Throws if Babel cannot parse the content.
|
|
48
|
+
*/
|
|
49
|
+
const analyzeScriptContent = async (scriptContent, sourceFilePath, pruneContext) => {
|
|
50
|
+
await (0, _babel_core.transformAsync)(scriptContent, {
|
|
51
|
+
filename: sourceFilePath,
|
|
52
|
+
plugins: [require_babel_plugin_intlayer_usage_analyzer.makeUsageAnalyzerBabelPlugin(pruneContext)],
|
|
53
|
+
parserOpts: BABEL_PARSER_OPTIONS,
|
|
54
|
+
ast: false,
|
|
55
|
+
code: false
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Runs the usage-analysis Babel plugin on a source file, accumulating
|
|
60
|
+
* field-usage data into `pruneContext`.
|
|
61
|
+
*
|
|
62
|
+
* For Vue / Svelte SFC files, script blocks are extracted before analysis so
|
|
63
|
+
* Babel does not attempt to parse the full SFC syntax (templates, styles, …).
|
|
64
|
+
* For plain JS/TS files, the whole file content is analysed directly.
|
|
65
|
+
*
|
|
66
|
+
* This is analysis-only: the transformed code output is discarded.
|
|
67
|
+
* Throws if Babel cannot parse the file (caller should handle and flag
|
|
68
|
+
* `pruneContext.hasUnparsableSourceFiles`).
|
|
69
|
+
*/
|
|
70
|
+
const analyzeFieldUsageInFile = async (sourceFilePath, code, pruneContext) => {
|
|
71
|
+
const scriptBlocks = require_extractScriptBlocks.extractScriptBlocks(sourceFilePath, code);
|
|
72
|
+
for (const block of scriptBlocks) {
|
|
73
|
+
if (!INTLAYER_USAGE_REGEX.test(block.content)) continue;
|
|
74
|
+
await analyzeScriptContent(block.content, sourceFilePath, pruneContext);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Applies field-renaming to a single JS/TS code string (not an SFC).
|
|
79
|
+
*
|
|
80
|
+
* Returns the renamed code string, or `null` if nothing changed or if
|
|
81
|
+
* Babel failed to parse the input (caller should fall back to original code).
|
|
82
|
+
*/
|
|
83
|
+
const renameFieldsInCode = async (code, sourceFilePath, pruneContext) => {
|
|
84
|
+
try {
|
|
85
|
+
return (await (0, _babel_core.transformAsync)(code, {
|
|
86
|
+
filename: sourceFilePath,
|
|
87
|
+
plugins: [require_babel_plugin_intlayer_field_rename.makeFieldRenameBabelPlugin(pruneContext)],
|
|
88
|
+
parserOpts: BABEL_PARSER_OPTIONS,
|
|
89
|
+
ast: false
|
|
90
|
+
}))?.code ?? null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Applies field-renaming to a source file, correctly handling both plain
|
|
97
|
+
* JS/TS files and SFC files (Vue / Svelte) by operating on each script block
|
|
98
|
+
* individually and injecting the results back into the original source.
|
|
99
|
+
*
|
|
100
|
+
* Returns the renamed code string, or `null` if nothing changed.
|
|
101
|
+
*/
|
|
102
|
+
const renameFieldsInSourceFile = async (sourceFilePath, code, pruneContext) => {
|
|
103
|
+
if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return null;
|
|
104
|
+
if (!INTLAYER_USAGE_REGEX.test(code)) return null;
|
|
105
|
+
const scriptBlocks = require_extractScriptBlocks.extractScriptBlocks(sourceFilePath, code);
|
|
106
|
+
if (scriptBlocks.length > 0 && (scriptBlocks[0].contentStartOffset > 0 || scriptBlocks.length > 1)) {
|
|
107
|
+
const modifications = [];
|
|
108
|
+
for (const block of scriptBlocks) {
|
|
109
|
+
if (!INTLAYER_USAGE_REGEX.test(block.content)) continue;
|
|
110
|
+
const renamedCode = await renameFieldsInCode(block.content, sourceFilePath, pruneContext);
|
|
111
|
+
if (renamedCode && renamedCode !== block.content) modifications.push({
|
|
112
|
+
block,
|
|
113
|
+
modifiedContent: renamedCode
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (modifications.length === 0) return null;
|
|
117
|
+
return require_extractScriptBlocks.injectScriptBlocks(code, modifications);
|
|
118
|
+
}
|
|
119
|
+
return renameFieldsInCode(code, sourceFilePath, pruneContext);
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Runs the intlayer optimize Babel plugin on a source file, transforming
|
|
123
|
+
* `useIntlayer('key')` / `getIntlayer('key')` calls into `useDictionary(_hash)`
|
|
124
|
+
* / `getDictionary(_hash)` and injecting the corresponding dictionary imports.
|
|
125
|
+
*
|
|
126
|
+
* Returns `{ code, map }` on success, or `null` if the transformation produced
|
|
127
|
+
* no output.
|
|
128
|
+
*/
|
|
129
|
+
const optimizeSourceFile = async (code, sourceFilePath, options) => {
|
|
130
|
+
const result = await (0, _babel_core.transformAsync)(code, {
|
|
131
|
+
filename: sourceFilePath,
|
|
132
|
+
plugins: [[require_babel_plugin_intlayer_optimize.intlayerOptimizeBabelPlugin, options]],
|
|
133
|
+
parserOpts: BABEL_PARSER_OPTIONS
|
|
134
|
+
});
|
|
135
|
+
if (!result?.code) return null;
|
|
136
|
+
return {
|
|
137
|
+
code: result.code,
|
|
138
|
+
map: result.map
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
exports.BABEL_PARSER_OPTIONS = BABEL_PARSER_OPTIONS;
|
|
144
|
+
exports.INTLAYER_USAGE_REGEX = INTLAYER_USAGE_REGEX;
|
|
145
|
+
exports.SOURCE_FILE_REGEX = SOURCE_FILE_REGEX;
|
|
146
|
+
exports.analyzeFieldUsageInFile = analyzeFieldUsageInFile;
|
|
147
|
+
exports.optimizeSourceFile = optimizeSourceFile;
|
|
148
|
+
exports.renameFieldsInCode = renameFieldsInCode;
|
|
149
|
+
exports.renameFieldsInSourceFile = renameFieldsInSourceFile;
|
|
150
|
+
//# sourceMappingURL=transformers.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformers.cjs","names":["makeUsageAnalyzerBabelPlugin","extractScriptBlocks","makeFieldRenameBabelPlugin","injectScriptBlocks","intlayerOptimizeBabelPlugin"],"sources":["../../src/transformers.ts"],"sourcesContent":["import { type TransformOptions, transformAsync } from '@babel/core';\nimport { makeFieldRenameBabelPlugin } from './babel-plugin-intlayer-field-rename';\nimport {\n intlayerOptimizeBabelPlugin,\n type OptimizePluginOptions,\n} from './babel-plugin-intlayer-optimize';\nimport {\n makeUsageAnalyzerBabelPlugin,\n type PruneContext,\n} from './babel-plugin-intlayer-usage-analyzer';\nimport { extractScriptBlocks, injectScriptBlocks } from './extractScriptBlocks';\n\n// ── Shared Babel parser configuration ─────────────────────────────────────────\n\n/**\n * Babel parser options covering the superset of syntaxes used across all\n * supported frameworks (React / Vue / Svelte / Angular / …).\n */\nexport const BABEL_PARSER_OPTIONS: NonNullable<TransformOptions['parserOpts']> =\n {\n sourceType: 'module',\n allowImportExportEverywhere: true,\n plugins: [\n 'typescript',\n 'jsx',\n 'decorators-legacy',\n 'classProperties',\n 'objectRestSpread',\n 'asyncGenerators',\n 'functionBind',\n 'exportDefaultFrom',\n 'exportNamespaceFrom',\n 'dynamicImport',\n 'nullishCoalescingOperator',\n 'optionalChaining',\n ],\n };\n\n/**\n * Fast pre-check: matches files that could contain intlayer calls.\n * Avoids running Babel on files with no relevant identifiers.\n */\nexport const INTLAYER_USAGE_REGEX = /\\b(use|get)Intlayer\\b/;\n\n/**\n * Matches source files that are valid targets for usage analysis and Babel\n * transformation. Excludes sourcemap files, declaration files, and other\n * non-source extensions.\n */\nexport const SOURCE_FILE_REGEX = /\\.(tsx?|[mc]?jsx?|vue|svelte)$/;\n\n// ── High-level transformer functions ──────────────────────────────────────────\n\n/**\n * Runs the usage-analysis Babel plugin on a single JS/TS code string.\n *\n * This is analysis-only: the transformed code output is discarded.\n * Throws if Babel cannot parse the content.\n */\nconst analyzeScriptContent = async (\n scriptContent: string,\n sourceFilePath: string,\n pruneContext: PruneContext\n): Promise<void> => {\n await transformAsync(scriptContent, {\n filename: sourceFilePath,\n plugins: [makeUsageAnalyzerBabelPlugin(pruneContext)],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n code: false, // analysis only – no output needed\n });\n};\n\n/**\n * Runs the usage-analysis Babel plugin on a source file, accumulating\n * field-usage data into `pruneContext`.\n *\n * For Vue / Svelte SFC files, script blocks are extracted before analysis so\n * Babel does not attempt to parse the full SFC syntax (templates, styles, …).\n * For plain JS/TS files, the whole file content is analysed directly.\n *\n * This is analysis-only: the transformed code output is discarded.\n * Throws if Babel cannot parse the file (caller should handle and flag\n * `pruneContext.hasUnparsableSourceFiles`).\n */\nexport const analyzeFieldUsageInFile = async (\n sourceFilePath: string,\n code: string,\n pruneContext: PruneContext\n): Promise<void> => {\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n\n // For SFC files (Vue / Svelte): scriptBlocks[0].contentStartOffset > 0\n // means we extracted actual <script> tags from the file.\n // For plain JS/TS: extractScriptBlocks returns the whole file as a single\n // block with offset 0, so we fall through to the same path.\n for (const block of scriptBlocks) {\n if (!INTLAYER_USAGE_REGEX.test(block.content)) continue;\n await analyzeScriptContent(block.content, sourceFilePath, pruneContext);\n }\n};\n\n/**\n * Applies field-renaming to a single JS/TS code string (not an SFC).\n *\n * Returns the renamed code string, or `null` if nothing changed or if\n * Babel failed to parse the input (caller should fall back to original code).\n */\nexport const renameFieldsInCode = async (\n code: string,\n sourceFilePath: string,\n pruneContext: PruneContext\n): Promise<string | null> => {\n try {\n const result = await transformAsync(code, {\n filename: sourceFilePath,\n plugins: [makeFieldRenameBabelPlugin(pruneContext)],\n parserOpts: BABEL_PARSER_OPTIONS,\n ast: false,\n });\n return result?.code ?? null;\n } catch {\n return null; // parse failure – caller falls back to original code\n }\n};\n\n/**\n * Applies field-renaming to a source file, correctly handling both plain\n * JS/TS files and SFC files (Vue / Svelte) by operating on each script block\n * individually and injecting the results back into the original source.\n *\n * Returns the renamed code string, or `null` if nothing changed.\n */\nexport const renameFieldsInSourceFile = async (\n sourceFilePath: string,\n code: string,\n pruneContext: PruneContext\n): Promise<string | null> => {\n if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return null;\n if (!INTLAYER_USAGE_REGEX.test(code)) return null;\n\n const scriptBlocks = extractScriptBlocks(sourceFilePath, code);\n\n const isSFC =\n scriptBlocks.length > 0 &&\n (scriptBlocks[0].contentStartOffset > 0 || scriptBlocks.length > 1);\n\n if (isSFC) {\n // Raw SFC: rename each script block individually and inject back.\n const modifications: Array<{\n block: (typeof scriptBlocks)[number];\n modifiedContent: string;\n }> = [];\n\n for (const block of scriptBlocks) {\n if (!INTLAYER_USAGE_REGEX.test(block.content)) continue;\n\n const renamedCode = await renameFieldsInCode(\n block.content,\n sourceFilePath,\n pruneContext\n );\n if (renamedCode && renamedCode !== block.content) {\n modifications.push({ block, modifiedContent: renamedCode });\n }\n }\n\n if (modifications.length === 0) return null;\n return injectScriptBlocks(code, modifications);\n }\n\n // Plain JS/TS or compiled SFC (no block delimiters) – rename the whole file.\n return renameFieldsInCode(code, sourceFilePath, pruneContext);\n};\n\n/**\n * Runs the intlayer optimize Babel plugin on a source file, transforming\n * `useIntlayer('key')` / `getIntlayer('key')` calls into `useDictionary(_hash)`\n * / `getDictionary(_hash)` and injecting the corresponding dictionary imports.\n *\n * Returns `{ code, map }` on success, or `null` if the transformation produced\n * no output.\n */\nexport const optimizeSourceFile = async (\n code: string,\n sourceFilePath: string,\n options: OptimizePluginOptions\n): Promise<{\n code: string;\n map: string | object | null | undefined;\n} | null> => {\n const result = await transformAsync(code, {\n filename: sourceFilePath,\n plugins: [[intlayerOptimizeBabelPlugin, options]],\n parserOpts: BABEL_PARSER_OPTIONS,\n });\n\n if (!result?.code) return null;\n\n return { code: result.code, map: result.map };\n};\n"],"mappings":";;;;;;;;;;;;;AAkBA,MAAa,uBACX;CACE,YAAY;CACZ,6BAA6B;CAC7B,SAAS;EACP;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD;CACF;;;;;AAMH,MAAa,uBAAuB;;;;;;AAOpC,MAAa,oBAAoB;;;;;;;AAUjC,MAAM,uBAAuB,OAC3B,eACA,gBACA,iBACkB;AAClB,uCAAqB,eAAe;EAClC,UAAU;EACV,SAAS,CAACA,0EAA6B,aAAa,CAAC;EACrD,YAAY;EACZ,KAAK;EACL,MAAM;EACP,CAAC;;;;;;;;;;;;;;AAeJ,MAAa,0BAA0B,OACrC,gBACA,MACA,iBACkB;CAClB,MAAM,eAAeC,gDAAoB,gBAAgB,KAAK;AAM9D,MAAK,MAAM,SAAS,cAAc;AAChC,MAAI,CAAC,qBAAqB,KAAK,MAAM,QAAQ,CAAE;AAC/C,QAAM,qBAAqB,MAAM,SAAS,gBAAgB,aAAa;;;;;;;;;AAU3E,MAAa,qBAAqB,OAChC,MACA,gBACA,iBAC2B;AAC3B,KAAI;AAOF,UANe,sCAAqB,MAAM;GACxC,UAAU;GACV,SAAS,CAACC,sEAA2B,aAAa,CAAC;GACnD,YAAY;GACZ,KAAK;GACN,CAAC,GACa,QAAQ;SACjB;AACN,SAAO;;;;;;;;;;AAWX,MAAa,2BAA2B,OACtC,gBACA,MACA,iBAC2B;AAC3B,KAAI,aAAa,8BAA8B,SAAS,EAAG,QAAO;AAClE,KAAI,CAAC,qBAAqB,KAAK,KAAK,CAAE,QAAO;CAE7C,MAAM,eAAeD,gDAAoB,gBAAgB,KAAK;AAM9D,KAHE,aAAa,SAAS,MACrB,aAAa,GAAG,qBAAqB,KAAK,aAAa,SAAS,IAExD;EAET,MAAM,gBAGD,EAAE;AAEP,OAAK,MAAM,SAAS,cAAc;AAChC,OAAI,CAAC,qBAAqB,KAAK,MAAM,QAAQ,CAAE;GAE/C,MAAM,cAAc,MAAM,mBACxB,MAAM,SACN,gBACA,aACD;AACD,OAAI,eAAe,gBAAgB,MAAM,QACvC,eAAc,KAAK;IAAE;IAAO,iBAAiB;IAAa,CAAC;;AAI/D,MAAI,cAAc,WAAW,EAAG,QAAO;AACvC,SAAOE,+CAAmB,MAAM,cAAc;;AAIhD,QAAO,mBAAmB,MAAM,gBAAgB,aAAa;;;;;;;;;;AAW/D,MAAa,qBAAqB,OAChC,MACA,gBACA,YAIW;CACX,MAAM,SAAS,sCAAqB,MAAM;EACxC,UAAU;EACV,SAAS,CAAC,CAACC,oEAA6B,QAAQ,CAAC;EACjD,YAAY;EACb,CAAC;AAEF,KAAI,CAAC,QAAQ,KAAM,QAAO;AAE1B,QAAO;EAAE,MAAM,OAAO;EAAM,KAAK,OAAO;EAAK"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { detectPackageName } from "./extractContent/utils/detectPackageName.mjs";
|
|
2
2
|
import { extractContentSync } from "./extractContent/extractContent.mjs";
|
|
3
|
+
import { relative } from "node:path";
|
|
3
4
|
import * as ANSIColors from "@intlayer/config/colors";
|
|
4
5
|
import { colorize, colorizePath, getAppLogger } from "@intlayer/config/logger";
|
|
5
|
-
import { relative } from "node:path";
|
|
6
6
|
import { parse } from "@babel/parser";
|
|
7
7
|
|
|
8
8
|
//#region src/babel-plugin-intlayer-extract.ts
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { INTLAYER_CALLER_NAMES } from "./babel-plugin-intlayer-usage-analyzer.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/babel-plugin-intlayer-field-rename.ts
|
|
4
|
+
/**
|
|
5
|
+
* Intlayer internal property names that must never be used as short-name
|
|
6
|
+
* targets. These appear inside content-node values (e.g. `{ nodeType:
|
|
7
|
+
* "translation", … }`) and are read by the intlayer runtime.
|
|
8
|
+
*/
|
|
9
|
+
const RESERVED_CONTENT_FIELD_NAMES = new Set(["nodeType"]);
|
|
10
|
+
/**
|
|
11
|
+
* Converts a zero-based index to a short alphabetic identifier.
|
|
12
|
+
* 0 → 'a', 1 → 'b', …, 25 → 'z', 26 → 'aa', 27 → 'ab', …
|
|
13
|
+
*/
|
|
14
|
+
const generateShortFieldName = (index) => {
|
|
15
|
+
const ALPHABET = "abcdefghijklmnopqrstuvwxyz";
|
|
16
|
+
const remainder = index % 26;
|
|
17
|
+
const quotient = Math.floor(index / 26);
|
|
18
|
+
return quotient === 0 ? ALPHABET[remainder] : generateShortFieldName(quotient - 1) + ALPHABET[remainder];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Recursively builds a `NestedRenameMap` from a compiled dictionary content
|
|
22
|
+
* value by traversing the intlayer node structure.
|
|
23
|
+
*
|
|
24
|
+
* Rules:
|
|
25
|
+
* - If the value has `nodeType: 'translation'`, user-defined fields live
|
|
26
|
+
* inside `translation[locale]`. Recurse into the first locale's value.
|
|
27
|
+
* - All other intlayer runtime nodes (enumeration, condition, gender, …) are
|
|
28
|
+
* treated as leaves — their internal keys must never be renamed.
|
|
29
|
+
* - Plain objects are user-defined records: rename their keys (filtered by
|
|
30
|
+
* `usedFieldFilter` at the top level) and recurse into each value.
|
|
31
|
+
* - Primitives and arrays produce an empty map (no further renaming).
|
|
32
|
+
*
|
|
33
|
+
* @param contentValue - The dictionary content value to analyse.
|
|
34
|
+
* @param usedFieldFilter - When provided, only keys in this set are included
|
|
35
|
+
* at the current level (matches the usage-analysis
|
|
36
|
+
* results so we don't rename purged fields).
|
|
37
|
+
*/
|
|
38
|
+
const buildNestedRenameMapFromContent = (contentValue, usedFieldFilter) => {
|
|
39
|
+
if (!contentValue || typeof contentValue !== "object" || Array.isArray(contentValue)) return /* @__PURE__ */ new Map();
|
|
40
|
+
const record = contentValue;
|
|
41
|
+
if (typeof record.nodeType === "string") {
|
|
42
|
+
if (record.translation && typeof record.translation === "object" && !Array.isArray(record.translation)) {
|
|
43
|
+
const firstLocaleValue = Object.values(record.translation)[0];
|
|
44
|
+
return buildNestedRenameMapFromContent(firstLocaleValue, usedFieldFilter);
|
|
45
|
+
}
|
|
46
|
+
return /* @__PURE__ */ new Map();
|
|
47
|
+
}
|
|
48
|
+
const allKeys = Object.keys(record).filter((key) => !RESERVED_CONTENT_FIELD_NAMES.has(key));
|
|
49
|
+
const sortedKeys = [...usedFieldFilter ? allKeys.filter((key) => usedFieldFilter.has(key)) : allKeys].sort();
|
|
50
|
+
const renameMap = /* @__PURE__ */ new Map();
|
|
51
|
+
for (let i = 0; i < sortedKeys.length; i++) {
|
|
52
|
+
const key = sortedKeys[i];
|
|
53
|
+
const children = buildNestedRenameMapFromContent(record[key]);
|
|
54
|
+
renameMap.set(key, {
|
|
55
|
+
shortName: generateShortFieldName(i),
|
|
56
|
+
children
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return renameMap;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Walks a MemberExpression chain starting from `startPath`, renaming each
|
|
63
|
+
* property found in `currentRenameMap` at the corresponding nesting level.
|
|
64
|
+
*/
|
|
65
|
+
const walkRenameChain = (babelTypes, startPath, currentRenameMap) => {
|
|
66
|
+
let refPath = startPath;
|
|
67
|
+
let renameMap = currentRenameMap;
|
|
68
|
+
while (renameMap.size > 0) {
|
|
69
|
+
const parentPath = refPath.parentPath;
|
|
70
|
+
if (!parentPath) break;
|
|
71
|
+
const parentNode = parentPath.node;
|
|
72
|
+
if (!babelTypes.isMemberExpression(parentNode) && !babelTypes.isOptionalMemberExpression(parentNode) || parentNode.object !== refPath.node) break;
|
|
73
|
+
const memberNode = parentNode;
|
|
74
|
+
let fieldName;
|
|
75
|
+
if (!memberNode.computed && babelTypes.isIdentifier(memberNode.property)) fieldName = memberNode.property.name;
|
|
76
|
+
else if (memberNode.computed && babelTypes.isStringLiteral(memberNode.property)) fieldName = memberNode.property.value;
|
|
77
|
+
else break;
|
|
78
|
+
const renameEntry = renameMap.get(fieldName);
|
|
79
|
+
if (!renameEntry) break;
|
|
80
|
+
if (!memberNode.computed && babelTypes.isIdentifier(memberNode.property)) memberNode.property.name = renameEntry.shortName;
|
|
81
|
+
else if (memberNode.computed && babelTypes.isStringLiteral(memberNode.property)) memberNode.property.value = renameEntry.shortName;
|
|
82
|
+
refPath = parentPath;
|
|
83
|
+
renameMap = renameEntry.children;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Creates a Babel plugin that rewrites dictionary content field accesses in
|
|
88
|
+
* source files to their short aliases defined in
|
|
89
|
+
* `pruneContext.dictionaryKeyToFieldRenameMap`.
|
|
90
|
+
*
|
|
91
|
+
* Handled patterns (mirrors the usage analyser):
|
|
92
|
+
*
|
|
93
|
+
* const { fieldA, fieldB } = useIntlayer('key')
|
|
94
|
+
* → const { shortA: fieldA, shortB: fieldB } = useIntlayer('key')
|
|
95
|
+
*
|
|
96
|
+
* useIntlayer('key').fieldA
|
|
97
|
+
* → useIntlayer('key').shortA
|
|
98
|
+
*
|
|
99
|
+
* const result = useIntlayer('key'); result.fieldA
|
|
100
|
+
* → const result = useIntlayer('key'); result.shortA
|
|
101
|
+
*
|
|
102
|
+
* This plugin must run in a separate `transformAsync` pass **before**
|
|
103
|
+
* `intlayerOptimizeBabelPlugin`, because the latter replaces `useIntlayer`
|
|
104
|
+
* with `useDictionary`, erasing the dictionary-key information needed here.
|
|
105
|
+
*/
|
|
106
|
+
const makeFieldRenameBabelPlugin = (pruneContext) => ({ types: babelTypes }) => ({
|
|
107
|
+
name: "intlayer-field-rename",
|
|
108
|
+
visitor: { Program: { exit: (programPath) => {
|
|
109
|
+
if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return;
|
|
110
|
+
const intlayerCallerLocalNameMap = /* @__PURE__ */ new Map();
|
|
111
|
+
programPath.traverse({ ImportDeclaration: (importDeclarationPath) => {
|
|
112
|
+
for (const importSpecifier of importDeclarationPath.node.specifiers) {
|
|
113
|
+
if (!babelTypes.isImportSpecifier(importSpecifier)) continue;
|
|
114
|
+
const importedName = babelTypes.isIdentifier(importSpecifier.imported) ? importSpecifier.imported.name : importSpecifier.imported.value;
|
|
115
|
+
if (INTLAYER_CALLER_NAMES.includes(importedName)) intlayerCallerLocalNameMap.set(importSpecifier.local.name, importedName);
|
|
116
|
+
}
|
|
117
|
+
} });
|
|
118
|
+
if (intlayerCallerLocalNameMap.size === 0) return;
|
|
119
|
+
programPath.traverse({ CallExpression: (callExpressionPath) => {
|
|
120
|
+
const calleeNode = callExpressionPath.node.callee;
|
|
121
|
+
let localCallerName;
|
|
122
|
+
if (babelTypes.isIdentifier(calleeNode)) localCallerName = calleeNode.name;
|
|
123
|
+
else if (babelTypes.isMemberExpression(calleeNode) && babelTypes.isIdentifier(calleeNode.property)) localCallerName = calleeNode.property.name;
|
|
124
|
+
if (!localCallerName || !intlayerCallerLocalNameMap.has(localCallerName)) return;
|
|
125
|
+
const callArguments = callExpressionPath.node.arguments;
|
|
126
|
+
if (callArguments.length === 0) return;
|
|
127
|
+
const firstArgument = callArguments[0];
|
|
128
|
+
let dictionaryKey;
|
|
129
|
+
if (babelTypes.isStringLiteral(firstArgument)) dictionaryKey = firstArgument.value;
|
|
130
|
+
else if (babelTypes.isTemplateLiteral(firstArgument) && firstArgument.expressions.length === 0 && firstArgument.quasis.length === 1) dictionaryKey = firstArgument.quasis[0].value.cooked ?? firstArgument.quasis[0].value.raw;
|
|
131
|
+
if (!dictionaryKey) return;
|
|
132
|
+
const fieldRenameMap = pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);
|
|
133
|
+
if (!fieldRenameMap || fieldRenameMap.size === 0) return;
|
|
134
|
+
const parentNode = callExpressionPath.parent;
|
|
135
|
+
if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isObjectPattern(parentNode.id)) {
|
|
136
|
+
for (const property of parentNode.id.properties) {
|
|
137
|
+
if (!babelTypes.isObjectProperty(property)) continue;
|
|
138
|
+
const keyName = babelTypes.isIdentifier(property.key) ? property.key.name : babelTypes.isStringLiteral(property.key) ? property.key.value : null;
|
|
139
|
+
if (!keyName) continue;
|
|
140
|
+
const renameEntry = fieldRenameMap.get(keyName);
|
|
141
|
+
if (!renameEntry) continue;
|
|
142
|
+
if (property.shorthand) {
|
|
143
|
+
property.shorthand = false;
|
|
144
|
+
property.key = babelTypes.identifier(renameEntry.shortName);
|
|
145
|
+
} else property.key = babelTypes.identifier(renameEntry.shortName);
|
|
146
|
+
if (renameEntry.children.size > 0 && babelTypes.isIdentifier(property.value)) {
|
|
147
|
+
const localVarBinding = callExpressionPath.scope.getBinding(property.value.name);
|
|
148
|
+
if (localVarBinding) for (const refPath of localVarBinding.referencePaths) walkRenameChain(babelTypes, refPath, renameEntry.children);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if ((babelTypes.isMemberExpression(parentNode) || babelTypes.isOptionalMemberExpression(parentNode)) && parentNode.object === callExpressionPath.node) {
|
|
154
|
+
walkRenameChain(babelTypes, callExpressionPath, fieldRenameMap);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isIdentifier(parentNode.id)) {
|
|
158
|
+
const variableName = parentNode.id.name;
|
|
159
|
+
const variableBinding = callExpressionPath.scope.getBinding(variableName);
|
|
160
|
+
if (!variableBinding) return;
|
|
161
|
+
for (const variableReferencePath of variableBinding.referencePaths) walkRenameChain(babelTypes, variableReferencePath, fieldRenameMap);
|
|
162
|
+
}
|
|
163
|
+
} });
|
|
164
|
+
} } }
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
//#endregion
|
|
168
|
+
export { buildNestedRenameMapFromContent, generateShortFieldName, makeFieldRenameBabelPlugin };
|
|
169
|
+
//# sourceMappingURL=babel-plugin-intlayer-field-rename.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"babel-plugin-intlayer-field-rename.mjs","names":[],"sources":["../../src/babel-plugin-intlayer-field-rename.ts"],"sourcesContent":["import type { NodePath, PluginObj } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport {\n INTLAYER_CALLER_NAMES,\n type IntlayerCallerName,\n type NestedRenameMap,\n type PruneContext,\n} from './babel-plugin-intlayer-usage-analyzer';\n\n// ── Field-name helpers ────────────────────────────────────────────────────────\n\n/**\n * Intlayer internal property names that must never be used as short-name\n * targets. These appear inside content-node values (e.g. `{ nodeType:\n * \"translation\", … }`) and are read by the intlayer runtime.\n */\nconst RESERVED_CONTENT_FIELD_NAMES = new Set(['nodeType']);\n\n/**\n * Converts a zero-based index to a short alphabetic identifier.\n * 0 → 'a', 1 → 'b', …, 25 → 'z', 26 → 'aa', 27 → 'ab', …\n */\nexport const generateShortFieldName = (index: number): string => {\n const ALPHABET = 'abcdefghijklmnopqrstuvwxyz';\n const remainder = index % ALPHABET.length;\n const quotient = Math.floor(index / ALPHABET.length);\n return quotient === 0\n ? ALPHABET[remainder]\n : generateShortFieldName(quotient - 1) + ALPHABET[remainder];\n};\n\n/**\n * Recursively builds a `NestedRenameMap` from a compiled dictionary content\n * value by traversing the intlayer node structure.\n *\n * Rules:\n * - If the value has `nodeType: 'translation'`, user-defined fields live\n * inside `translation[locale]`. Recurse into the first locale's value.\n * - All other intlayer runtime nodes (enumeration, condition, gender, …) are\n * treated as leaves — their internal keys must never be renamed.\n * - Plain objects are user-defined records: rename their keys (filtered by\n * `usedFieldFilter` at the top level) and recurse into each value.\n * - Primitives and arrays produce an empty map (no further renaming).\n *\n * @param contentValue - The dictionary content value to analyse.\n * @param usedFieldFilter - When provided, only keys in this set are included\n * at the current level (matches the usage-analysis\n * results so we don't rename purged fields).\n */\nexport const buildNestedRenameMapFromContent = (\n contentValue: unknown,\n usedFieldFilter?: Set<string>\n): NestedRenameMap => {\n if (\n !contentValue ||\n typeof contentValue !== 'object' ||\n Array.isArray(contentValue)\n ) {\n return new Map();\n }\n\n const record = contentValue as Record<string, unknown>;\n\n // Any object with `nodeType: string` is an intlayer runtime node.\n if (typeof record.nodeType === 'string') {\n // Translation node: user-defined fields live inside translation[locale].\n if (\n record.translation &&\n typeof record.translation === 'object' &&\n !Array.isArray(record.translation)\n ) {\n const firstLocaleValue = Object.values(\n record.translation as Record<string, unknown>\n )[0];\n return buildNestedRenameMapFromContent(firstLocaleValue, usedFieldFilter);\n }\n // All other intlayer nodes have runtime-managed internal structure — return\n // an empty map so they are treated as leaves.\n return new Map();\n }\n\n // User-defined record: collect non-reserved keys\n const allKeys = Object.keys(record).filter(\n (key) => !RESERVED_CONTENT_FIELD_NAMES.has(key)\n );\n\n const keysToRename = usedFieldFilter\n ? allKeys.filter((key) => usedFieldFilter.has(key))\n : allKeys;\n\n const sortedKeys = [...keysToRename].sort();\n const renameMap: NestedRenameMap = new Map();\n\n for (let i = 0; i < sortedKeys.length; i++) {\n const key = sortedKeys[i];\n const children = buildNestedRenameMapFromContent(record[key]); // no filter for nested\n renameMap.set(key, { shortName: generateShortFieldName(i), children });\n }\n\n return renameMap;\n};\n\n// ── Field-rename Babel plugin ─────────────────────────────────────────────────\n\n/**\n * Walks a MemberExpression chain starting from `startPath`, renaming each\n * property found in `currentRenameMap` at the corresponding nesting level.\n */\nconst walkRenameChain = (\n babelTypes: typeof BabelTypes,\n startPath: NodePath<BabelTypes.Node>,\n currentRenameMap: NestedRenameMap\n): void => {\n let refPath: NodePath<BabelTypes.Node> = startPath;\n let renameMap = currentRenameMap;\n\n while (renameMap.size > 0) {\n const parentPath = refPath.parentPath;\n if (!parentPath) break;\n\n const parentNode = parentPath.node;\n\n if (\n (!babelTypes.isMemberExpression(parentNode) &&\n !babelTypes.isOptionalMemberExpression(parentNode)) ||\n (parentNode as BabelTypes.MemberExpression).object !== refPath.node\n ) {\n break;\n }\n\n const memberNode = parentNode as BabelTypes.MemberExpression;\n let fieldName: string | undefined;\n\n if (!memberNode.computed && babelTypes.isIdentifier(memberNode.property)) {\n fieldName = memberNode.property.name;\n } else if (\n memberNode.computed &&\n babelTypes.isStringLiteral(memberNode.property)\n ) {\n fieldName = memberNode.property.value;\n } else {\n break; // dynamic key – stop\n }\n\n const renameEntry = renameMap.get(fieldName);\n if (!renameEntry) break; // not in map – stop\n\n // Apply the rename\n if (!memberNode.computed && babelTypes.isIdentifier(memberNode.property)) {\n memberNode.property.name = renameEntry.shortName;\n } else if (\n memberNode.computed &&\n babelTypes.isStringLiteral(memberNode.property)\n ) {\n memberNode.property.value = renameEntry.shortName;\n }\n\n refPath = parentPath;\n renameMap = renameEntry.children;\n }\n};\n\n/**\n * Creates a Babel plugin that rewrites dictionary content field accesses in\n * source files to their short aliases defined in\n * `pruneContext.dictionaryKeyToFieldRenameMap`.\n *\n * Handled patterns (mirrors the usage analyser):\n *\n * const { fieldA, fieldB } = useIntlayer('key')\n * → const { shortA: fieldA, shortB: fieldB } = useIntlayer('key')\n *\n * useIntlayer('key').fieldA\n * → useIntlayer('key').shortA\n *\n * const result = useIntlayer('key'); result.fieldA\n * → const result = useIntlayer('key'); result.shortA\n *\n * This plugin must run in a separate `transformAsync` pass **before**\n * `intlayerOptimizeBabelPlugin`, because the latter replaces `useIntlayer`\n * with `useDictionary`, erasing the dictionary-key information needed here.\n */\nexport const makeFieldRenameBabelPlugin =\n (pruneContext: PruneContext) =>\n ({ types: babelTypes }: { types: typeof BabelTypes }): PluginObj => ({\n name: 'intlayer-field-rename',\n visitor: {\n Program: {\n exit: (programPath) => {\n if (pruneContext.dictionaryKeyToFieldRenameMap.size === 0) return;\n\n // Collect local aliases for useIntlayer / getIntlayer\n const intlayerCallerLocalNameMap = new Map<string, string>();\n\n programPath.traverse({\n ImportDeclaration: (importDeclarationPath) => {\n for (const importSpecifier of importDeclarationPath.node\n .specifiers) {\n if (!babelTypes.isImportSpecifier(importSpecifier)) continue;\n\n const importedName = babelTypes.isIdentifier(\n importSpecifier.imported\n )\n ? importSpecifier.imported.name\n : (importSpecifier.imported as BabelTypes.StringLiteral)\n .value;\n\n if (\n INTLAYER_CALLER_NAMES.includes(\n importedName as IntlayerCallerName\n )\n ) {\n intlayerCallerLocalNameMap.set(\n importSpecifier.local.name,\n importedName\n );\n }\n }\n },\n });\n\n if (intlayerCallerLocalNameMap.size === 0) return;\n\n // Visit all useIntlayer / getIntlayer call-sites and rename field accesses\n programPath.traverse({\n CallExpression: (callExpressionPath) => {\n const calleeNode = callExpressionPath.node.callee;\n let localCallerName: string | undefined;\n\n if (babelTypes.isIdentifier(calleeNode)) {\n localCallerName = calleeNode.name;\n } else if (\n babelTypes.isMemberExpression(calleeNode) &&\n babelTypes.isIdentifier(calleeNode.property)\n ) {\n localCallerName = calleeNode.property.name;\n }\n\n if (\n !localCallerName ||\n !intlayerCallerLocalNameMap.has(localCallerName)\n )\n return;\n\n const callArguments = callExpressionPath.node.arguments;\n if (callArguments.length === 0) return;\n\n const firstArgument = callArguments[0];\n let dictionaryKey: string | undefined;\n\n if (babelTypes.isStringLiteral(firstArgument)) {\n dictionaryKey = firstArgument.value;\n } else if (\n babelTypes.isTemplateLiteral(firstArgument) &&\n firstArgument.expressions.length === 0 &&\n firstArgument.quasis.length === 1\n ) {\n dictionaryKey =\n firstArgument.quasis[0].value.cooked ??\n firstArgument.quasis[0].value.raw;\n }\n\n if (!dictionaryKey) return;\n\n const fieldRenameMap =\n pruneContext.dictionaryKeyToFieldRenameMap.get(dictionaryKey);\n if (!fieldRenameMap || fieldRenameMap.size === 0) return;\n\n const parentNode = callExpressionPath.parent;\n\n // ── Case 1: const { fieldA, fieldB } = useIntlayer('key') ────────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n for (const property of parentNode.id.properties) {\n if (!babelTypes.isObjectProperty(property)) continue;\n\n const keyName = babelTypes.isIdentifier(property.key)\n ? property.key.name\n : babelTypes.isStringLiteral(property.key)\n ? property.key.value\n : null;\n if (!keyName) continue;\n\n const renameEntry = fieldRenameMap.get(keyName);\n if (!renameEntry) continue;\n\n // { fieldA } → { shortA: fieldA }\n // { fieldA: localVar } → { shortA: localVar }\n if (property.shorthand) {\n property.shorthand = false;\n property.key = babelTypes.identifier(renameEntry.shortName);\n } else {\n property.key = babelTypes.identifier(renameEntry.shortName);\n }\n\n // Walk nested member accesses on the local variable\n if (\n renameEntry.children.size > 0 &&\n babelTypes.isIdentifier(property.value)\n ) {\n const localVarBinding = callExpressionPath.scope.getBinding(\n (property.value as BabelTypes.Identifier).name\n );\n if (localVarBinding) {\n for (const refPath of localVarBinding.referencePaths) {\n walkRenameChain(\n babelTypes,\n refPath,\n renameEntry.children\n );\n }\n }\n }\n }\n return;\n }\n\n // ── Case 2: useIntlayer('key').fieldA.nested ─────────────────────\n if (\n (babelTypes.isMemberExpression(parentNode) ||\n babelTypes.isOptionalMemberExpression(parentNode)) &&\n (parentNode as BabelTypes.MemberExpression).object ===\n callExpressionPath.node\n ) {\n walkRenameChain(babelTypes, callExpressionPath, fieldRenameMap);\n return;\n }\n\n // ── Case 3: const result = useIntlayer('key'); result.fieldA ─────\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n const variableName = parentNode.id.name;\n const variableBinding =\n callExpressionPath.scope.getBinding(variableName);\n if (!variableBinding) return;\n\n for (const variableReferencePath of variableBinding.referencePaths) {\n walkRenameChain(\n babelTypes,\n variableReferencePath,\n fieldRenameMap\n );\n }\n }\n },\n });\n },\n },\n },\n });\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getPathHash } from "@intlayer/chokidar/utils";
|
|
2
1
|
import { dirname, join, relative } from "node:path";
|
|
2
|
+
import { getPathHash } from "@intlayer/chokidar/utils";
|
|
3
3
|
import { normalizePath } from "@intlayer/config/utils";
|
|
4
4
|
|
|
5
5
|
//#region src/babel-plugin-intlayer-optimize.ts
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//#region src/babel-plugin-intlayer-usage-analyzer.ts
|
|
2
|
+
const createPruneContext = () => ({
|
|
3
|
+
dictionaryKeyToFieldUsageMap: /* @__PURE__ */ new Map(),
|
|
4
|
+
dictionariesWithEdgeCases: /* @__PURE__ */ new Set(),
|
|
5
|
+
hasUnparsableSourceFiles: false,
|
|
6
|
+
dictionaryKeysWithUntrackedBindings: /* @__PURE__ */ new Map(),
|
|
7
|
+
dictionaryKeyToFieldRenameMap: /* @__PURE__ */ new Map(),
|
|
8
|
+
dictionaryKeysWithOpaqueTopLevelFields: /* @__PURE__ */ new Map(),
|
|
9
|
+
dictionariesSkippingFieldRename: /* @__PURE__ */ new Set(),
|
|
10
|
+
pendingFrameworkAnalysis: /* @__PURE__ */ new Map()
|
|
11
|
+
});
|
|
12
|
+
/** Canonical intlayer caller names that trigger usage analysis. */
|
|
13
|
+
const INTLAYER_CALLER_NAMES = ["useIntlayer", "getIntlayer"];
|
|
14
|
+
/**
|
|
15
|
+
* Records the usage of a specific dictionary key's fields into `pruneContext`.
|
|
16
|
+
* Merges with any previously recorded usage for the same key.
|
|
17
|
+
*/
|
|
18
|
+
const recordFieldUsage = (pruneContext, dictionaryKey, fieldUsage) => {
|
|
19
|
+
const existingUsage = pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);
|
|
20
|
+
if (existingUsage === "all") return;
|
|
21
|
+
if (fieldUsage === "all") {
|
|
22
|
+
pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, "all");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const mergedFieldSet = existingUsage instanceof Set ? new Set([...existingUsage, ...fieldUsage]) : new Set(fieldUsage);
|
|
26
|
+
pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, mergedFieldSet);
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Analyses how the result of a single `useIntlayer('key')` / `getIntlayer('key')`
|
|
30
|
+
* call expression is consumed, then records the field usage into `pruneContext`.
|
|
31
|
+
*
|
|
32
|
+
* Recognised patterns:
|
|
33
|
+
* const { fieldA, fieldB } = useIntlayer('key') → records {fieldA, fieldB}
|
|
34
|
+
* useIntlayer('key').fieldA → records {fieldA}
|
|
35
|
+
* useIntlayer('key')['fieldA'] → records {fieldA}
|
|
36
|
+
* const { ...rest } = useIntlayer('key') → records 'all' (spread)
|
|
37
|
+
* const result = useIntlayer('key') → records 'all' (untracked binding)
|
|
38
|
+
*/
|
|
39
|
+
const analyzeCallExpressionUsage = (babelTypes, pruneContext, callExpressionPath, dictionaryKey, currentSourceFilePath, isSfcFile) => {
|
|
40
|
+
const parentNode = callExpressionPath.parent;
|
|
41
|
+
/** Mark the dictionary key as having an untracked binding in this file. */
|
|
42
|
+
const markUntrackedBinding = () => {
|
|
43
|
+
const existingPaths = pruneContext.dictionaryKeysWithUntrackedBindings.get(dictionaryKey) ?? [];
|
|
44
|
+
if (!existingPaths.includes(currentSourceFilePath)) pruneContext.dictionaryKeysWithUntrackedBindings.set(dictionaryKey, [...existingPaths, currentSourceFilePath]);
|
|
45
|
+
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
46
|
+
};
|
|
47
|
+
/** Record that a field value is consumed opaquely (not further destructured). */
|
|
48
|
+
const markOpaqueField = (fieldName, line) => {
|
|
49
|
+
const fieldToLocations = pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(dictionaryKey) ?? /* @__PURE__ */ new Map();
|
|
50
|
+
const location = line !== void 0 ? `${currentSourceFilePath}:${line}` : currentSourceFilePath;
|
|
51
|
+
const locations = fieldToLocations.get(fieldName) ?? [];
|
|
52
|
+
if (!locations.includes(location)) locations.push(location);
|
|
53
|
+
fieldToLocations.set(fieldName, locations);
|
|
54
|
+
pruneContext.dictionaryKeysWithOpaqueTopLevelFields.set(dictionaryKey, fieldToLocations);
|
|
55
|
+
};
|
|
56
|
+
/** Register a plain variable binding in an SFC file for a second-pass analysis. */
|
|
57
|
+
const deferFrameworkAnalysis = (variableName) => {
|
|
58
|
+
const existing = pruneContext.pendingFrameworkAnalysis.get(currentSourceFilePath) ?? [];
|
|
59
|
+
if (!existing.some((e) => e.variableName === variableName && e.dictionaryKey === dictionaryKey)) existing.push({
|
|
60
|
+
variableName,
|
|
61
|
+
dictionaryKey
|
|
62
|
+
});
|
|
63
|
+
pruneContext.pendingFrameworkAnalysis.set(currentSourceFilePath, existing);
|
|
64
|
+
};
|
|
65
|
+
if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isObjectPattern(parentNode.id)) {
|
|
66
|
+
if (parentNode.id.properties.some((prop) => babelTypes.isRestElement(prop))) {
|
|
67
|
+
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const accessedFieldNames = /* @__PURE__ */ new Set();
|
|
71
|
+
for (const property of parentNode.id.properties) if (babelTypes.isObjectProperty(property) && babelTypes.isIdentifier(property.key)) accessedFieldNames.add(property.key.name);
|
|
72
|
+
else if (babelTypes.isObjectProperty(property) && babelTypes.isStringLiteral(property.key)) accessedFieldNames.add(property.key.value);
|
|
73
|
+
recordFieldUsage(pruneContext, dictionaryKey, accessedFieldNames);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if ((babelTypes.isMemberExpression(parentNode) || babelTypes.isOptionalMemberExpression(parentNode)) && parentNode.object === callExpressionPath.node) {
|
|
77
|
+
if (!parentNode.computed && babelTypes.isIdentifier(parentNode.property)) recordFieldUsage(pruneContext, dictionaryKey, new Set([parentNode.property.name]));
|
|
78
|
+
else if (parentNode.computed && babelTypes.isStringLiteral(parentNode.property)) recordFieldUsage(pruneContext, dictionaryKey, new Set([parentNode.property.value]));
|
|
79
|
+
else markUntrackedBinding();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isIdentifier(parentNode.id)) {
|
|
83
|
+
const variableName = parentNode.id.name;
|
|
84
|
+
const variableBinding = callExpressionPath.scope.getBinding(variableName);
|
|
85
|
+
if (!variableBinding) {
|
|
86
|
+
markUntrackedBinding();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const accessedTopLevelFieldNames = /* @__PURE__ */ new Set();
|
|
90
|
+
let hasUntrackedReferenceAccess = false;
|
|
91
|
+
for (const variableReferencePath of variableBinding.referencePaths) {
|
|
92
|
+
const referenceParentNode = variableReferencePath.parent;
|
|
93
|
+
if ((babelTypes.isMemberExpression(referenceParentNode) || babelTypes.isOptionalMemberExpression(referenceParentNode)) && referenceParentNode.object === variableReferencePath.node) {
|
|
94
|
+
const memberExpressionNode = referenceParentNode;
|
|
95
|
+
if (!memberExpressionNode.computed && babelTypes.isIdentifier(memberExpressionNode.property)) {
|
|
96
|
+
const fieldName = memberExpressionNode.property.name;
|
|
97
|
+
accessedTopLevelFieldNames.add(fieldName);
|
|
98
|
+
const memberExprPath = variableReferencePath.parentPath;
|
|
99
|
+
const grandParentNode = memberExprPath?.parent;
|
|
100
|
+
if (!((babelTypes.isMemberExpression(grandParentNode) || babelTypes.isOptionalMemberExpression(grandParentNode)) && grandParentNode.object === memberExprPath?.node)) markOpaqueField(fieldName, memberExprPath?.node.loc?.start.line);
|
|
101
|
+
} else if (memberExpressionNode.computed && babelTypes.isStringLiteral(memberExpressionNode.property)) {
|
|
102
|
+
const fieldName = memberExpressionNode.property.value;
|
|
103
|
+
accessedTopLevelFieldNames.add(fieldName);
|
|
104
|
+
const memberExprPath = variableReferencePath.parentPath;
|
|
105
|
+
const grandParentNode = memberExprPath?.parent;
|
|
106
|
+
if (!((babelTypes.isMemberExpression(grandParentNode) || babelTypes.isOptionalMemberExpression(grandParentNode)) && grandParentNode.object === memberExprPath?.node)) markOpaqueField(fieldName, memberExprPath?.node.loc?.start.line);
|
|
107
|
+
} else {
|
|
108
|
+
hasUntrackedReferenceAccess = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
} else if (babelTypes.isArrayExpression(referenceParentNode)) {} else {
|
|
112
|
+
hasUntrackedReferenceAccess = true;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (hasUntrackedReferenceAccess) markUntrackedBinding();
|
|
117
|
+
else if (isSfcFile) deferFrameworkAnalysis(variableName);
|
|
118
|
+
else if (variableBinding.referencePaths.length === 0) markUntrackedBinding();
|
|
119
|
+
else recordFieldUsage(pruneContext, dictionaryKey, accessedTopLevelFieldNames);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (babelTypes.isExpressionStatement(parentNode)) return;
|
|
123
|
+
markUntrackedBinding();
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Creates a Babel plugin that traverses source files and records which
|
|
127
|
+
* top-level dictionary fields each `useIntlayer` / `getIntlayer` call-site
|
|
128
|
+
* accesses. Results are accumulated into `pruneContext`.
|
|
129
|
+
*
|
|
130
|
+
* This plugin is analysis-only: it does not transform the code (`code: false`
|
|
131
|
+
* should be passed to `transformAsync` when using it).
|
|
132
|
+
*/
|
|
133
|
+
const makeUsageAnalyzerBabelPlugin = (pruneContext) => ({ types: babelTypes }) => ({
|
|
134
|
+
name: "intlayer-usage-analyzer",
|
|
135
|
+
visitor: { Program: { exit: (programPath, state) => {
|
|
136
|
+
const currentSourceFilePath = state.file.opts.filename ?? "unknown file";
|
|
137
|
+
const isSfcFile = currentSourceFilePath.endsWith(".vue") || currentSourceFilePath.endsWith(".svelte");
|
|
138
|
+
const intlayerCallerLocalNameMap = /* @__PURE__ */ new Map();
|
|
139
|
+
programPath.traverse({ ImportDeclaration: (importDeclarationPath) => {
|
|
140
|
+
for (const importSpecifier of importDeclarationPath.node.specifiers) {
|
|
141
|
+
if (!babelTypes.isImportSpecifier(importSpecifier)) continue;
|
|
142
|
+
const importedName = babelTypes.isIdentifier(importSpecifier.imported) ? importSpecifier.imported.name : importSpecifier.imported.value;
|
|
143
|
+
if (INTLAYER_CALLER_NAMES.includes(importedName)) intlayerCallerLocalNameMap.set(importSpecifier.local.name, importedName);
|
|
144
|
+
}
|
|
145
|
+
} });
|
|
146
|
+
if (intlayerCallerLocalNameMap.size === 0) return;
|
|
147
|
+
programPath.traverse({ CallExpression: (callExpressionPath) => {
|
|
148
|
+
const calleeNode = callExpressionPath.node.callee;
|
|
149
|
+
let localCallerName;
|
|
150
|
+
if (babelTypes.isIdentifier(calleeNode)) localCallerName = calleeNode.name;
|
|
151
|
+
else if (babelTypes.isMemberExpression(calleeNode) && babelTypes.isIdentifier(calleeNode.property)) localCallerName = calleeNode.property.name;
|
|
152
|
+
if (!localCallerName || !intlayerCallerLocalNameMap.has(localCallerName)) return;
|
|
153
|
+
const callArguments = callExpressionPath.node.arguments;
|
|
154
|
+
if (callArguments.length === 0) return;
|
|
155
|
+
const firstArgument = callArguments[0];
|
|
156
|
+
let dictionaryKey;
|
|
157
|
+
if (babelTypes.isStringLiteral(firstArgument)) dictionaryKey = firstArgument.value;
|
|
158
|
+
else if (babelTypes.isTemplateLiteral(firstArgument) && firstArgument.expressions.length === 0 && firstArgument.quasis.length === 1) dictionaryKey = firstArgument.quasis[0].value.cooked ?? firstArgument.quasis[0].value.raw;
|
|
159
|
+
if (!dictionaryKey) return;
|
|
160
|
+
analyzeCallExpressionUsage(babelTypes, pruneContext, callExpressionPath, dictionaryKey, currentSourceFilePath, isSfcFile);
|
|
161
|
+
} });
|
|
162
|
+
} } }
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
export { INTLAYER_CALLER_NAMES, createPruneContext, makeUsageAnalyzerBabelPlugin };
|
|
167
|
+
//# sourceMappingURL=babel-plugin-intlayer-usage-analyzer.mjs.map
|