next-intlayer 9.4.3 → 9.5.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/README.md +0 -2
- package/dist/cjs/extractorLoader.cjs +148 -0
- package/dist/cjs/extractorLoader.cjs.map +1 -0
- package/dist/cjs/proxy/intlayerProxy.cjs +1 -1
- package/dist/cjs/server/prepareSwcOptimization.cjs +12 -20
- package/dist/cjs/server/prepareSwcOptimization.cjs.map +1 -1
- package/dist/cjs/server/startContentWatcher.cjs +91 -0
- package/dist/cjs/server/startContentWatcher.cjs.map +1 -0
- package/dist/cjs/server/withIntlayer.cjs +98 -13
- package/dist/cjs/server/withIntlayer.cjs.map +1 -1
- package/dist/esm/extractorLoader.mjs +146 -0
- package/dist/esm/extractorLoader.mjs.map +1 -0
- package/dist/esm/proxy/intlayerProxy.mjs +1 -1
- package/dist/esm/server/prepareSwcOptimization.mjs +12 -20
- package/dist/esm/server/prepareSwcOptimization.mjs.map +1 -1
- package/dist/esm/server/startContentWatcher.mjs +90 -0
- package/dist/esm/server/startContentWatcher.mjs.map +1 -0
- package/dist/esm/server/withIntlayer.mjs +98 -13
- package/dist/esm/server/withIntlayer.mjs.map +1 -1
- package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-field-rename.d.ts +1 -0
- package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-field-rename.d.ts.map +1 -1
- package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-optimize.d.ts +1 -1
- package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-purge.d.ts +1 -0
- package/dist/types/@intlayer/babel/dist/types/babel-plugin-intlayer-usage-analyzer.d.ts +3 -2
- package/dist/types/@intlayer/babel/dist/types/index.d.ts +2 -0
- package/dist/types/@intlayer/babel/dist/types/pruneContext.d.ts +1 -0
- package/dist/types/@intlayer/babel/dist/types/staticAstReaders.d.ts +2 -0
- package/dist/types/@intlayer/babel/dist/types/transformers.d.ts +1 -0
- package/dist/types/extractorLoader.d.ts +63 -0
- package/dist/types/extractorLoader.d.ts.map +1 -0
- package/dist/types/server/prepareSwcOptimization.d.ts +2 -2
- package/dist/types/server/prepareSwcOptimization.d.ts.map +1 -1
- package/dist/types/server/startContentWatcher.d.ts +5 -0
- package/dist/types/server/startContentWatcher.d.ts.map +1 -0
- package/dist/types/server/withIntlayer.d.ts +8 -2
- package/dist/types/server/withIntlayer.d.ts.map +1 -1
- package/package.json +17 -9
package/README.md
CHANGED
|
@@ -69,8 +69,6 @@ With **per-locale content files**, **TypeScript autocompletion**, **tree-shakabl
|
|
|
69
69
|
| <img src="https://github.com/aymericzip/intlayer/blob/main/docs/assets/interoperability.png?raw=true" alt="Feature" width="700"> | **Interoperability**<br><br>Allow interoperability with react-i18next, next-i18next, next-intl, react-intl, vue-i18n. <br><br> - [Intlayer and react-intl](https://intlayer.org/blog/intlayer-with-react-intl) <br> - [Intlayer and next-intl](https://intlayer.org/blog/intlayer-with-next-intl) <br> - [Intlayer and next-i18next](https://intlayer.org/blog/intlayer-with-next-i18next) <br> - [Intlayer and vue-i18n](https://intlayer.org/blog/intlayer-with-vue-i18n) <br> - [Intlayer compat adapters](https://intlayer.org/doc/compatibility) |
|
|
70
70
|
| <img src="https://github.com/aymericzip/intlayer/blob/main/docs/assets/benchmark.png?raw=true" alt="Feature" width="700"> | **Performances & Benchmark**<br><br>Uses advanced tree-shaking and dynamic loading to boost performances and keep the solution as light as possible. <br><br> - [Performances & Benchmark](https://intlayer.org/doc/benchmark) |
|
|
71
71
|
|
|
72
|
-
---
|
|
73
|
-
|
|
74
72
|
## 📦 Installation
|
|
75
73
|
|
|
76
74
|
Start your journey with Intlayer today and experience a smoother, more powerful approach to internationalization.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
|
|
2
|
+
let node_path = require("node:path");
|
|
3
|
+
let _intlayer_config_colors = require("@intlayer/config/colors");
|
|
4
|
+
_intlayer_config_colors = require_runtime.__toESM(_intlayer_config_colors);
|
|
5
|
+
let _intlayer_config_logger = require("@intlayer/config/logger");
|
|
6
|
+
let _intlayer_config_node = require("@intlayer/config/node");
|
|
7
|
+
let _intlayer_config_utils = require("@intlayer/config/utils");
|
|
8
|
+
let _intlayer_engine_utils = require("@intlayer/engine/utils");
|
|
9
|
+
|
|
10
|
+
//#region src/extractorLoader.ts
|
|
11
|
+
const REQUIRED_BABEL_EXPORTS = [
|
|
12
|
+
"detectPackageName",
|
|
13
|
+
"extractContent",
|
|
14
|
+
"getExtractPluginOptions",
|
|
15
|
+
"writeContentHelper"
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Serialises the dictionary writes of every process taking part in the build.
|
|
19
|
+
* `writeContentHelper` rebuilds the merged `.intlayer` artifacts, which is a
|
|
20
|
+
* read-modify-write over every dictionary on disk — two extractions running it
|
|
21
|
+
* at the same time would each merge a half-written state.
|
|
22
|
+
*/
|
|
23
|
+
const WRITE_LOCK_FILE_NAME = "intlayer-compiler-write.lock";
|
|
24
|
+
/** Set once per process: the loader runs in a long-lived bundler worker. */
|
|
25
|
+
let cachedConfiguration;
|
|
26
|
+
let cachedExtractOptions;
|
|
27
|
+
let cachedFilesListSet;
|
|
28
|
+
let cachedBabelModule;
|
|
29
|
+
/** Intlayer package detected per component directory. */
|
|
30
|
+
const packageNameByDirectory = /* @__PURE__ */ new Map();
|
|
31
|
+
/**
|
|
32
|
+
* Content already written for a given dictionary key, as a stable hash.
|
|
33
|
+
*
|
|
34
|
+
* Next.js compiles the same file once per compilation (client, server, edge),
|
|
35
|
+
* and Turbopack re-runs the loader whenever the file changes. Re-writing a
|
|
36
|
+
* dictionary whose content did not move costs a full `.intlayer` rebuild for
|
|
37
|
+
* nothing.
|
|
38
|
+
*/
|
|
39
|
+
const writtenContentHashByDictionaryKey = /* @__PURE__ */ new Map();
|
|
40
|
+
/** Serialises the writes issued by this process, before the file lock. */
|
|
41
|
+
let pendingWrite = Promise.resolve();
|
|
42
|
+
/**
|
|
43
|
+
* Lazily loads `@intlayer/babel`, or `null` when it is not installed or too old
|
|
44
|
+
* to expose the extraction API.
|
|
45
|
+
*/
|
|
46
|
+
const loadIntlayerBabel = (configuration) => {
|
|
47
|
+
if (cachedBabelModule !== void 0) return cachedBabelModule;
|
|
48
|
+
try {
|
|
49
|
+
const intlayerBabel = (configuration.build?.require ?? (0, _intlayer_config_utils.getProjectRequire)())("@intlayer/babel");
|
|
50
|
+
cachedBabelModule = REQUIRED_BABEL_EXPORTS.every((exportName) => typeof intlayerBabel?.[exportName] === "function") ? intlayerBabel : null;
|
|
51
|
+
} catch {
|
|
52
|
+
cachedBabelModule = null;
|
|
53
|
+
}
|
|
54
|
+
return cachedBabelModule;
|
|
55
|
+
};
|
|
56
|
+
const getOptions = (context) => {
|
|
57
|
+
if (typeof context.getOptions === "function") return context.getOptions();
|
|
58
|
+
if (typeof context.query === "object" && context.query) return context.query;
|
|
59
|
+
return {};
|
|
60
|
+
};
|
|
61
|
+
const hashContent = (content) => JSON.stringify(Object.keys(content).sort().map((key) => [key, content[key]]));
|
|
62
|
+
/**
|
|
63
|
+
* Writes one extracted dictionary, skipping the write when its content is
|
|
64
|
+
* already on disk and holding the cross-process lock while `.intlayer` is
|
|
65
|
+
* rebuilt.
|
|
66
|
+
*/
|
|
67
|
+
const writeDictionary = async (intlayerBabel, configuration, dictionaryKey, content, filePath) => {
|
|
68
|
+
const contentHash = hashContent(content);
|
|
69
|
+
if (writtenContentHashByDictionaryKey.get(dictionaryKey) === contentHash) return;
|
|
70
|
+
const lockFilePath = (0, node_path.join)(configuration.system.baseDir, ".intlayer", "cache", WRITE_LOCK_FILE_NAME);
|
|
71
|
+
await (0, _intlayer_engine_utils.withFileLock)(lockFilePath, () => intlayerBabel.writeContentHelper(content, dictionaryKey, filePath, configuration));
|
|
72
|
+
writtenContentHashByDictionaryKey.set(dictionaryKey, contentHash);
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Extracts the translatable content of one file and returns the rewritten
|
|
76
|
+
* source, or the original source when there is nothing to extract.
|
|
77
|
+
*/
|
|
78
|
+
const extractFile = async (filePath, source, mode) => {
|
|
79
|
+
cachedConfiguration ??= (0, _intlayer_config_node.getConfiguration)();
|
|
80
|
+
const configuration = cachedConfiguration;
|
|
81
|
+
const intlayerBabel = loadIntlayerBabel(configuration);
|
|
82
|
+
if (!intlayerBabel) return source;
|
|
83
|
+
cachedExtractOptions ??= intlayerBabel.getExtractPluginOptions(configuration, mode);
|
|
84
|
+
if (!cachedExtractOptions.enabled) return source;
|
|
85
|
+
cachedFilesListSet ??= new Set((cachedExtractOptions.filesList ?? []).map(_intlayer_config_utils.normalizePath));
|
|
86
|
+
if (!cachedFilesListSet.has((0, _intlayer_config_utils.normalizePath)(filePath))) return source;
|
|
87
|
+
const appLogger = (0, _intlayer_config_logger.getAppLogger)(configuration);
|
|
88
|
+
const componentDirectory = (0, node_path.dirname)(filePath);
|
|
89
|
+
let packageName = packageNameByDirectory.get(componentDirectory);
|
|
90
|
+
if (!packageName) {
|
|
91
|
+
packageName = intlayerBabel.detectPackageName(componentDirectory);
|
|
92
|
+
packageNameByDirectory.set(componentDirectory, packageName);
|
|
93
|
+
}
|
|
94
|
+
const result = await intlayerBabel.extractContent(filePath, packageName, {
|
|
95
|
+
configuration,
|
|
96
|
+
code: source,
|
|
97
|
+
declarationOnly: !configuration.compiler.saveComponents,
|
|
98
|
+
onExtract: ({ key, content }) => {
|
|
99
|
+
pendingWrite = pendingWrite.then(() => writeDictionary(intlayerBabel, configuration, key, content, filePath)).catch((error) => {
|
|
100
|
+
appLogger([`${(0, _intlayer_config_logger.colorize)("Compiler:", _intlayer_config_colors.GREY_DARK)} Failed to write the dictionary extracted from ${(0, _intlayer_config_logger.colorizePath)((0, node_path.relative)(configuration.system.baseDir, filePath))}:`, error], { level: "error" });
|
|
101
|
+
});
|
|
102
|
+
return pendingWrite;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
if (!result?.transformedCode) return source;
|
|
106
|
+
appLogger(`${(0, _intlayer_config_logger.colorize)("Compiler:", _intlayer_config_colors.GREY_DARK)} Extracted content from ${(0, _intlayer_config_logger.colorizePath)((0, node_path.relative)(configuration.system.baseDir, filePath))}`, { level: "debug" });
|
|
107
|
+
return result.transformedCode;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Webpack-compatible loader running the Intlayer compiler extraction pass.
|
|
111
|
+
*
|
|
112
|
+
* It replaces the `babel.config.js` setup on Next.js: Turbopack never reads a
|
|
113
|
+
* Babel config, and on webpack a Babel config would opt the whole project out
|
|
114
|
+
* of SWC. Registered automatically by `withIntlayer` on both bundlers.
|
|
115
|
+
*
|
|
116
|
+
* @example next.config.ts — registered by hand
|
|
117
|
+
* ```ts
|
|
118
|
+
* export default {
|
|
119
|
+
* turbopack: {
|
|
120
|
+
* rules: {
|
|
121
|
+
* // No `as`: it renames the module, so naming the extension the rule
|
|
122
|
+
* // already matches would ask for `Component.tsx.tsx`.
|
|
123
|
+
* '*.tsx': {
|
|
124
|
+
* loaders: [
|
|
125
|
+
* {
|
|
126
|
+
* loader: require.resolve('next-intlayer/extractor-loader'),
|
|
127
|
+
* options: { mode: 'dev' },
|
|
128
|
+
* },
|
|
129
|
+
* ],
|
|
130
|
+
* },
|
|
131
|
+
* },
|
|
132
|
+
* },
|
|
133
|
+
* };
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
const intlayerExtractorLoader = function(source) {
|
|
137
|
+
const callback = this.async();
|
|
138
|
+
const { mode } = getOptions(this);
|
|
139
|
+
const filePath = this.resourcePath;
|
|
140
|
+
extractFile(filePath, source, mode ?? "build").then((code) => callback(null, code)).catch((error) => {
|
|
141
|
+
(0, _intlayer_config_logger.getAppLogger)(cachedConfiguration)([`${(0, _intlayer_config_logger.colorize)("Compiler:", _intlayer_config_colors.GREY_DARK)} Failed to extract content from ${(0, _intlayer_config_logger.colorizePath)(filePath)}:`, error], { level: "error" });
|
|
142
|
+
callback(null, source);
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
//#endregion
|
|
147
|
+
module.exports = intlayerExtractorLoader;
|
|
148
|
+
//# sourceMappingURL=extractorLoader.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractorLoader.cjs","names":["getProjectRequire","join","withFileLock","getConfiguration","normalizePath","getAppLogger","dirname","colorize","ANSIColors","colorizePath","relative"],"sources":["../../src/extractorLoader.ts"],"sourcesContent":["import { dirname, join, relative } from 'node:path';\nimport type {\n detectPackageName as DetectPackageName,\n extractContent as ExtractContent,\n ExtractPluginOptions,\n getExtractPluginOptions as GetExtractPluginOptions,\n PackageName,\n writeContentHelper as WriteContentHelper,\n} from '@intlayer/babel';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, colorizePath, getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getProjectRequire, normalizePath } from '@intlayer/config/utils';\nimport { withFileLock } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n\n/**\n * Options the loader accepts. Turbopack serialises loader options to JSON, so\n * every field has to stay a plain JSON value — the Intlayer configuration\n * itself (which carries functions, e.g. `compiler.output`) is therefore loaded\n * by the loader rather than handed to it.\n */\nexport type IntlayerExtractorLoaderOptions = {\n /**\n * Whether the host bundler is serving (`dev`) or building (`build`).\n *\n * Passed explicitly because a Turbopack loader runs in a worker process whose\n * environment does not necessarily carry `INTLAYER_IS_DEV_COMMAND`.\n */\n mode?: 'dev' | 'build';\n};\n\n/**\n * Structural view of the loader context, limited to what this loader uses.\n * Typed locally rather than imported from `webpack` because the same file is\n * also run by Turbopack's webpack-loader compatibility layer, and `webpack` is\n * only an optional peer dependency.\n */\ntype IntlayerLoaderContext = {\n /** Absolute path of the file being compiled. */\n resourcePath: string;\n /** Switches the loader to asynchronous mode. */\n async: () => (error: Error | null, content?: string) => void;\n /** webpack 5 / Turbopack accessor for the loader options. */\n getOptions?: () => IntlayerExtractorLoaderOptions;\n /** Legacy options holder, used when `getOptions` is unavailable. */\n query?: IntlayerExtractorLoaderOptions | string;\n};\n\n/**\n * Subset of `@intlayer/babel` the loader needs. The package is an optional\n * dependency, so it is required lazily and never imported at module scope.\n */\ntype IntlayerBabelModule = {\n detectPackageName: typeof DetectPackageName;\n extractContent: typeof ExtractContent;\n getExtractPluginOptions: typeof GetExtractPluginOptions;\n writeContentHelper: typeof WriteContentHelper;\n};\n\nconst REQUIRED_BABEL_EXPORTS = [\n 'detectPackageName',\n 'extractContent',\n 'getExtractPluginOptions',\n 'writeContentHelper',\n] as const satisfies readonly (keyof IntlayerBabelModule)[];\n\n/**\n * Serialises the dictionary writes of every process taking part in the build.\n * `writeContentHelper` rebuilds the merged `.intlayer` artifacts, which is a\n * read-modify-write over every dictionary on disk — two extractions running it\n * at the same time would each merge a half-written state.\n */\nconst WRITE_LOCK_FILE_NAME = 'intlayer-compiler-write.lock';\n\n/** Set once per process: the loader runs in a long-lived bundler worker. */\nlet cachedConfiguration: IntlayerConfig | undefined;\nlet cachedExtractOptions: ExtractPluginOptions | undefined;\nlet cachedFilesListSet: Set<string> | undefined;\nlet cachedBabelModule: IntlayerBabelModule | null | undefined;\n\n/** Intlayer package detected per component directory. */\nconst packageNameByDirectory = new Map<string, PackageName>();\n\n/**\n * Content already written for a given dictionary key, as a stable hash.\n *\n * Next.js compiles the same file once per compilation (client, server, edge),\n * and Turbopack re-runs the loader whenever the file changes. Re-writing a\n * dictionary whose content did not move costs a full `.intlayer` rebuild for\n * nothing.\n */\nconst writtenContentHashByDictionaryKey = new Map<string, string>();\n\n/** Serialises the writes issued by this process, before the file lock. */\nlet pendingWrite: Promise<void> = Promise.resolve();\n\n/**\n * Lazily loads `@intlayer/babel`, or `null` when it is not installed or too old\n * to expose the extraction API.\n */\nconst loadIntlayerBabel = (\n configuration: IntlayerConfig\n): IntlayerBabelModule | null => {\n if (cachedBabelModule !== undefined) return cachedBabelModule;\n\n try {\n const requireFunction = configuration.build?.require ?? getProjectRequire();\n\n const intlayerBabel = requireFunction(\n '@intlayer/babel'\n ) as Partial<IntlayerBabelModule>;\n\n const hasEveryRequiredExport = REQUIRED_BABEL_EXPORTS.every(\n (exportName) => typeof intlayerBabel?.[exportName] === 'function'\n );\n\n cachedBabelModule = hasEveryRequiredExport\n ? (intlayerBabel as IntlayerBabelModule)\n : null;\n } catch {\n cachedBabelModule = null;\n }\n\n return cachedBabelModule;\n};\n\nconst getOptions = (\n context: IntlayerLoaderContext\n): IntlayerExtractorLoaderOptions => {\n if (typeof context.getOptions === 'function') return context.getOptions();\n if (typeof context.query === 'object' && context.query) return context.query;\n\n return {};\n};\n\nconst hashContent = (content: Record<string, string>): string =>\n JSON.stringify(\n Object.keys(content)\n .sort()\n .map((key) => [key, content[key]])\n );\n\n/**\n * Writes one extracted dictionary, skipping the write when its content is\n * already on disk and holding the cross-process lock while `.intlayer` is\n * rebuilt.\n */\nconst writeDictionary = async (\n intlayerBabel: IntlayerBabelModule,\n configuration: IntlayerConfig,\n dictionaryKey: string,\n content: Record<string, string>,\n filePath: string\n): Promise<void> => {\n const contentHash = hashContent(content);\n\n if (writtenContentHashByDictionaryKey.get(dictionaryKey) === contentHash) {\n return;\n }\n\n const lockFilePath = join(\n configuration.system.baseDir,\n '.intlayer',\n 'cache',\n WRITE_LOCK_FILE_NAME\n );\n\n await withFileLock(lockFilePath, () =>\n intlayerBabel.writeContentHelper(\n content,\n dictionaryKey,\n filePath,\n configuration\n )\n );\n\n writtenContentHashByDictionaryKey.set(dictionaryKey, contentHash);\n};\n\n/**\n * Extracts the translatable content of one file and returns the rewritten\n * source, or the original source when there is nothing to extract.\n */\nconst extractFile = async (\n filePath: string,\n source: string,\n mode: 'dev' | 'build'\n): Promise<string> => {\n cachedConfiguration ??= getConfiguration();\n const configuration = cachedConfiguration;\n\n const intlayerBabel = loadIntlayerBabel(configuration);\n\n // Without `@intlayer/babel` there is no extraction to run; `withIntlayer`\n // only registers this loader when the package resolves, so reaching this\n // branch means it was removed after the config was read.\n if (!intlayerBabel) return source;\n\n cachedExtractOptions ??= intlayerBabel.getExtractPluginOptions(\n configuration,\n mode\n );\n\n if (!cachedExtractOptions.enabled) return source;\n\n cachedFilesListSet ??= new Set(\n (cachedExtractOptions.filesList ?? []).map(normalizePath)\n );\n\n if (!cachedFilesListSet.has(normalizePath(filePath))) return source;\n\n const appLogger = getAppLogger(configuration);\n const componentDirectory = dirname(filePath);\n\n let packageName = packageNameByDirectory.get(componentDirectory);\n\n if (!packageName) {\n packageName = intlayerBabel.detectPackageName(componentDirectory);\n packageNameByDirectory.set(componentDirectory, packageName);\n }\n\n const result = await intlayerBabel.extractContent(filePath, packageName, {\n configuration,\n code: source,\n // `extractContent` rewrites the component on disk unless told otherwise,\n // so the config flag has to be forwarded — the transform is meant to stay\n // in the bundle output until the user opts into `saveComponents`.\n declarationOnly: !configuration.compiler.saveComponents,\n // Supplying `onExtract` turns off the internal dictionary write, so the\n // writes stay serialised behind the lock below.\n onExtract: ({ key, content }) => {\n pendingWrite = pendingWrite\n .then(() =>\n writeDictionary(intlayerBabel, configuration, key, content, filePath)\n )\n .catch((error) => {\n appLogger(\n [\n `${colorize('Compiler:', ANSIColors.GREY_DARK)} Failed to write the dictionary extracted from ${colorizePath(\n relative(configuration.system.baseDir, filePath)\n )}:`,\n error,\n ],\n { level: 'error' }\n );\n });\n\n return pendingWrite;\n },\n });\n\n if (!result?.transformedCode) return source;\n\n appLogger(\n `${colorize('Compiler:', ANSIColors.GREY_DARK)} Extracted content from ${colorizePath(\n relative(configuration.system.baseDir, filePath)\n )}`,\n { level: 'debug' }\n );\n\n return result.transformedCode;\n};\n\n/**\n * Webpack-compatible loader running the Intlayer compiler extraction pass.\n *\n * It replaces the `babel.config.js` setup on Next.js: Turbopack never reads a\n * Babel config, and on webpack a Babel config would opt the whole project out\n * of SWC. Registered automatically by `withIntlayer` on both bundlers.\n *\n * @example next.config.ts — registered by hand\n * ```ts\n * export default {\n * turbopack: {\n * rules: {\n * // No `as`: it renames the module, so naming the extension the rule\n * // already matches would ask for `Component.tsx.tsx`.\n * '*.tsx': {\n * loaders: [\n * {\n * loader: require.resolve('next-intlayer/extractor-loader'),\n * options: { mode: 'dev' },\n * },\n * ],\n * },\n * },\n * },\n * };\n * ```\n */\nconst intlayerExtractorLoader = function (\n this: IntlayerLoaderContext,\n source: string\n): void {\n const callback = this.async();\n const { mode } = getOptions(this);\n const filePath = this.resourcePath;\n\n extractFile(filePath, source, mode ?? 'build')\n .then((code) => callback(null, code))\n .catch((error) => {\n // An extraction failure must not take the build down: the file simply\n // keeps its own hardcoded strings.\n const appLogger = getAppLogger(cachedConfiguration);\n\n appLogger(\n [\n `${colorize('Compiler:', ANSIColors.GREY_DARK)} Failed to extract content from ${colorizePath(filePath)}:`,\n error,\n ],\n { level: 'error' }\n );\n\n callback(null, source);\n });\n};\n\nexport default intlayerExtractorLoader;\n"],"mappings":"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"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_proxy_localeDetector = require('./localeDetector.cjs');
|
|
3
|
+
let _intlayer_config_logger = require("@intlayer/config/logger");
|
|
3
4
|
let _intlayer_core_localization = require("@intlayer/core/localization");
|
|
4
5
|
let _intlayer_core_utils = require("@intlayer/core/utils");
|
|
5
6
|
let _intlayer_config_built = require("@intlayer/config/built");
|
|
6
7
|
let _intlayer_config_defaultValues = require("@intlayer/config/defaultValues");
|
|
7
|
-
let _intlayer_config_logger = require("@intlayer/config/logger");
|
|
8
8
|
let next_server = require("next/server");
|
|
9
9
|
|
|
10
10
|
//#region src/proxy/intlayerProxy.ts
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
|
|
3
|
-
let _intlayer_config_logger = require("@intlayer/config/logger");
|
|
4
3
|
let node_path = require("node:path");
|
|
5
4
|
let _intlayer_config_colors = require("@intlayer/config/colors");
|
|
6
5
|
_intlayer_config_colors = require_runtime.__toESM(_intlayer_config_colors);
|
|
6
|
+
let _intlayer_config_logger = require("@intlayer/config/logger");
|
|
7
7
|
let _intlayer_config_utils = require("@intlayer/config/utils");
|
|
8
8
|
let _intlayer_engine_utils = require("@intlayer/engine/utils");
|
|
9
9
|
let node_fs = require("node:fs");
|
|
@@ -48,25 +48,16 @@ const resolveSwcLogLevel = (intlayerConfig) => {
|
|
|
48
48
|
/**
|
|
49
49
|
* Whether the purge / minify pipeline should run for this build.
|
|
50
50
|
*
|
|
51
|
-
* Compat-adapter callers disable it: their descriptors reach
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
51
|
+
* Compat-adapter callers do not disable it: their descriptors reach the usage
|
|
52
|
+
* analyser intact (`compatCallers`), which records the dictionaries they read
|
|
53
|
+
* and marks those as unrenamable — so purging stays field-accurate and the
|
|
54
|
+
* minify step leaves them alone.
|
|
55
55
|
*/
|
|
56
|
-
const getIsPurgePipelineEnabled = (intlayerConfig,
|
|
56
|
+
const getIsPurgePipelineEnabled = (intlayerConfig, isSwcPluginUsable) => {
|
|
57
57
|
const { purge, minify, optimize } = intlayerConfig.build;
|
|
58
58
|
if (optimize === false) return false;
|
|
59
59
|
if (!purge && !minify) return false;
|
|
60
60
|
if (!isSwcPluginUsable) return false;
|
|
61
|
-
if (swcExtraCallers && swcExtraCallers.length > 0) {
|
|
62
|
-
(0, _intlayer_config_logger.getAppLogger)(intlayerConfig)([
|
|
63
|
-
"Dictionary purge and minification are",
|
|
64
|
-
(0, _intlayer_config_logger.colorize)("disabled", _intlayer_config_colors.GREY_DARK),
|
|
65
|
-
"because compat-adapter callers are configured — their call sites are",
|
|
66
|
-
"not visible to the usage analyser."
|
|
67
|
-
], { level: "warn" });
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
61
|
return true;
|
|
71
62
|
};
|
|
72
63
|
/**
|
|
@@ -105,7 +96,7 @@ const getIsPurgePipelineAvailable = (intlayerConfig) => loadIntlayerBabel(intlay
|
|
|
105
96
|
* Runs the purge / minify pipeline and writes the resulting field-rename
|
|
106
97
|
* tables to the cache file so every process of the build reads the same ones.
|
|
107
98
|
*/
|
|
108
|
-
const runPurgePipelineOnce = async (intlayerConfig, configOptions, dictionaries, fieldRenameMapFilePath) => {
|
|
99
|
+
const runPurgePipelineOnce = async (intlayerConfig, configOptions, dictionaries, compatCallers, fieldRenameMapFilePath) => {
|
|
109
100
|
const appLogger = (0, _intlayer_config_logger.getAppLogger)(intlayerConfig);
|
|
110
101
|
const intlayerBabel = loadIntlayerBabel(intlayerConfig);
|
|
111
102
|
if (!intlayerBabel) {
|
|
@@ -124,7 +115,8 @@ const runPurgePipelineOnce = async (intlayerConfig, configOptions, dictionaries,
|
|
|
124
115
|
try {
|
|
125
116
|
fieldRenameMap = serializeFieldRenameMap(runIntlayerPurgePipeline(getPurgePluginOptions({
|
|
126
117
|
configOptions,
|
|
127
|
-
dictionaries
|
|
118
|
+
dictionaries,
|
|
119
|
+
overrides: compatCallers?.length ? { compatCallers } : void 0
|
|
128
120
|
})));
|
|
129
121
|
} catch (pipelineError) {
|
|
130
122
|
appLogger([
|
|
@@ -173,13 +165,13 @@ const removeFieldRenameMapFile = (fieldRenameMapFilePath) => {
|
|
|
173
165
|
* pipeline is disabled, unavailable, or renamed nothing.
|
|
174
166
|
*/
|
|
175
167
|
const prepareSwcOptimization = async (intlayerConfig, params) => {
|
|
176
|
-
const { configOptions, dictionaries,
|
|
168
|
+
const { configOptions, dictionaries, compatCallers, isSwcPluginUsable } = params;
|
|
177
169
|
const fieldRenameMapFilePath = getCacheFilePath(intlayerConfig, FIELD_RENAME_MAP_FILE_NAME);
|
|
178
|
-
if (!getIsPurgePipelineEnabled(intlayerConfig,
|
|
170
|
+
if (!getIsPurgePipelineEnabled(intlayerConfig, isSwcPluginUsable)) {
|
|
179
171
|
removeFieldRenameMapFile(fieldRenameMapFilePath);
|
|
180
172
|
return {};
|
|
181
173
|
}
|
|
182
|
-
await (0, _intlayer_engine_utils.runOnce)(getCacheFilePath(intlayerConfig, PURGE_SENTINEL_FILE_NAME), () => runPurgePipelineOnce(intlayerConfig, configOptions, dictionaries, fieldRenameMapFilePath), { cacheTimeoutMs: PURGE_CACHE_TIMEOUT_MS });
|
|
174
|
+
await (0, _intlayer_engine_utils.runOnce)(getCacheFilePath(intlayerConfig, PURGE_SENTINEL_FILE_NAME), () => runPurgePipelineOnce(intlayerConfig, configOptions, dictionaries, compatCallers, fieldRenameMapFilePath), { cacheTimeoutMs: PURGE_CACHE_TIMEOUT_MS });
|
|
183
175
|
return readFieldRenameMapFile(fieldRenameMapFilePath);
|
|
184
176
|
};
|
|
185
177
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prepareSwcOptimization.cjs","names":["join","colorize","ANSIColors","getProjectRequire","getAppLogger","mkdir","dirname","writeFile","readFileSync","runOnce"],"sources":["../../../src/server/prepareSwcOptimization.ts"],"sourcesContent":["import { readFileSync, rmSync } from 'node:fs';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport type {\n getPurgePluginOptions as GetPurgePluginOptions,\n runIntlayerPurgePipeline as RunIntlayerPurgePipeline,\n SerializedFieldRenameMap,\n serializeFieldRenameMap as SerializeFieldRenameMap,\n} from '@intlayer/babel';\nimport type { SwcExtraCallerConfig } from '@intlayer/config/callers';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport type { GetConfigurationOptions } from '@intlayer/config/node';\nimport { getProjectRequire } from '@intlayer/config/utils';\nimport { runOnce } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\n\n/**\n * Field-rename tables keyed by dictionary key, forwarded to the\n * `@intlayer/swc` plugin's `fieldRenameMap` option.\n */\nexport type FieldRenameMapByDictionaryKey = Record<\n string,\n SerializedFieldRenameMap\n>;\n\n/** Verbosity forwarded to the `@intlayer/swc` plugin's `logLevel` option. */\nexport type SwcLogLevel = 'off' | 'info' | 'debug';\n\n/**\n * Subset of `@intlayer/babel` this module needs. The package is only an\n * optional runtime dependency, so it is required lazily and never imported at\n * module scope.\n */\ntype IntlayerBabelModule = {\n getPurgePluginOptions: typeof GetPurgePluginOptions;\n runIntlayerPurgePipeline: typeof RunIntlayerPurgePipeline;\n serializeFieldRenameMap: typeof SerializeFieldRenameMap;\n};\n\n/**\n * Opt-in to the SWC plugin's per-file tracing.\n *\n * The plugin transforms one file at a time with no cross-file state, so all it\n * can report is a line per file — hundreds of them on a real project, and with\n * no counterpart in the Vite build output. The purge and minify summaries that\n * `log.mode` does control are emitted by the pipeline itself, in Node; this\n * variable is purely a debugging aid on top.\n */\nconst SWC_LOG_LEVEL_ENV_VAR = 'INTLAYER_SWC_LOG_LEVEL';\n\n/** Sentinel coordinating the pipeline across the processes of one build. */\nconst PURGE_SENTINEL_FILE_NAME = 'intlayer-swc-purge.lock';\n\n/** Where the serialised field-rename tables are cached between processes. */\nconst FIELD_RENAME_MAP_FILE_NAME = 'swc-field-rename-map.json';\n\n/**\n * Next.js evaluates `next.config.*` in the main build process and again in each\n * Turbopack/webpack worker. The dictionary rewrite must happen exactly once —\n * a second run would derive the short aliases from the already-renamed JSON —\n * so the sentinel window has to cover a whole build startup.\n */\nconst PURGE_CACHE_TIMEOUT_MS = 30 * 1000;\n\n/** Absolute path of a file in the intlayer cache directory. */\nconst getCacheFilePath = (\n intlayerConfig: IntlayerConfig,\n fileName: string\n): string =>\n join(intlayerConfig.system.baseDir, '.intlayer', 'cache', fileName);\n\n/**\n * Resolves the verbosity the `@intlayer/swc` plugin should report at.\n *\n * Off unless {@link SWC_LOG_LEVEL_ENV_VAR} asks for tracing: `log.mode` governs\n * the purge and minify reporting, which the pipeline emits in Node so the\n * Next.js build reads the same as the Vite one.\n */\nexport const resolveSwcLogLevel = (\n intlayerConfig: IntlayerConfig\n): SwcLogLevel => {\n if (intlayerConfig.log.mode === 'disabled') return 'off';\n\n const requestedLogLevel = process.env[SWC_LOG_LEVEL_ENV_VAR];\n\n return requestedLogLevel === 'info' || requestedLogLevel === 'debug'\n ? requestedLogLevel\n : 'off';\n};\n\n/**\n * Whether the purge / minify pipeline should run for this build.\n *\n * Compat-adapter callers disable it: their descriptors reach `withIntlayer`\n * only in the SWC plugin's lossy wire format, so the usage analyser would not\n * recognise `useTranslation(...)` & co. and would purge fields those call sites\n * still read.\n */\nconst getIsPurgePipelineEnabled = (\n intlayerConfig: IntlayerConfig,\n swcExtraCallers: SwcExtraCallerConfig[] | undefined,\n isSwcPluginUsable: boolean\n): boolean => {\n const { purge, minify, optimize } = intlayerConfig.build;\n\n if (optimize === false) return false;\n if (!purge && !minify) return false;\n\n // The pipeline rewrites the compiled dictionaries in place — dropping unused\n // fields and renaming the rest to short aliases. Only the SWC plugin rewrites\n // the matching source accesses, so running this without it would ship\n // dictionaries whose keys no longer match the code reading them.\n if (!isSwcPluginUsable) return false;\n\n // `editor.enabled` is deliberately not checked here: it only stands the\n // field-rename step down — inside the pipeline, and with its own explanation\n // — the same way the Vite build does. Purging still runs, and the rename map\n // the SWC plugin receives comes back empty, so source and JSON stay in sync.\n\n if (swcExtraCallers && swcExtraCallers.length > 0) {\n getAppLogger(intlayerConfig)(\n [\n 'Dictionary purge and minification are',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because compat-adapter callers are configured — their call sites are',\n 'not visible to the usage analyser.',\n ],\n { level: 'warn' }\n );\n return false;\n }\n\n return true;\n};\n\n/**\n * Exports this module needs from `@intlayer/babel`. Checked one by one so a\n * version predating the purge pipeline is rejected like a missing package,\n * instead of failing later with `runIntlayerPurgePipeline is not a function`.\n */\nconst REQUIRED_BABEL_EXPORTS = [\n 'getPurgePluginOptions',\n 'runIntlayerPurgePipeline',\n 'serializeFieldRenameMap',\n] as const satisfies readonly (keyof IntlayerBabelModule)[];\n\n/**\n * Lazily loads `@intlayer/babel`, or `null` when it is not installed or too old\n * to expose the purge pipeline.\n */\nconst loadIntlayerBabel = (\n intlayerConfig: IntlayerConfig\n): IntlayerBabelModule | null => {\n try {\n const requireFunction =\n intlayerConfig.build?.require ?? getProjectRequire();\n\n const intlayerBabel = requireFunction(\n '@intlayer/babel'\n ) as Partial<IntlayerBabelModule>;\n\n const hasEveryRequiredExport = REQUIRED_BABEL_EXPORTS.every(\n (exportName) => typeof intlayerBabel?.[exportName] === 'function'\n );\n\n if (!hasEveryRequiredExport) return null;\n\n return intlayerBabel as IntlayerBabelModule;\n } catch {\n return null;\n }\n};\n\n/**\n * Whether the purge / minify pipeline can run at all, i.e. whether a usable\n * `@intlayer/babel` is resolvable from the project.\n *\n * Used by `withIntlayer` to keep the build report honest: announcing\n * `Dictionary minification enabled` while the pipeline cannot load would describe a\n * pass that never happens.\n */\nexport const getIsPurgePipelineAvailable = (\n intlayerConfig: IntlayerConfig\n): boolean => loadIntlayerBabel(intlayerConfig) !== null;\n\n/**\n * Runs the purge / minify pipeline and writes the resulting field-rename\n * tables to the cache file so every process of the build reads the same ones.\n */\nconst runPurgePipelineOnce = async (\n intlayerConfig: IntlayerConfig,\n configOptions: GetConfigurationOptions | undefined,\n dictionaries: Dictionary[] | undefined,\n fieldRenameMapFilePath: string\n): Promise<void> => {\n const appLogger = getAppLogger(intlayerConfig);\n\n const intlayerBabel = loadIntlayerBabel(intlayerConfig);\n\n if (!intlayerBabel) {\n // Not verbose: the build asked for purge / minify and is silently getting\n // neither, which is exactly the kind of thing a default build must report.\n appLogger(\n [\n 'Dictionary purge and minification are',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('@intlayer/babel', ANSIColors.GREY_LIGHT),\n 'is missing or too old to expose the purge pipeline — install it in',\n 'this project to enable them.',\n ],\n { level: 'warn' }\n );\n return;\n }\n\n const {\n getPurgePluginOptions,\n runIntlayerPurgePipeline,\n serializeFieldRenameMap,\n } = intlayerBabel;\n\n // `runOnce` discards whatever this callback throws, so a pipeline failure\n // would otherwise leave the build with no dictionaries rewritten and no\n // explanation of why.\n let fieldRenameMap: FieldRenameMapByDictionaryKey;\n\n try {\n // The pipeline reports what it purged and minified through the intlayer\n // logger, matching the Vite build output.\n const pruneContext = runIntlayerPurgePipeline(\n getPurgePluginOptions({ configOptions, dictionaries })\n );\n\n fieldRenameMap = serializeFieldRenameMap(pruneContext);\n } catch (pipelineError) {\n appLogger(\n [\n 'Dictionary purge and minification',\n colorize('failed', ANSIColors.RED),\n '— the compiled dictionaries are left untouched.',\n pipelineError instanceof Error\n ? `(${pipelineError.message})`\n : String(pipelineError),\n ],\n { level: 'error' }\n );\n return;\n }\n\n await mkdir(dirname(fieldRenameMapFilePath), { recursive: true });\n await writeFile(\n fieldRenameMapFilePath,\n JSON.stringify(fieldRenameMap),\n 'utf-8'\n );\n};\n\n/** Reads the cached field-rename tables, or `{}` when none were written. */\nconst readFieldRenameMapFile = (\n fieldRenameMapFilePath: string\n): FieldRenameMapByDictionaryKey => {\n try {\n return JSON.parse(\n readFileSync(fieldRenameMapFilePath, 'utf-8')\n ) as FieldRenameMapByDictionaryKey;\n } catch {\n return {};\n }\n};\n\n/** Removes the cached field-rename tables, ignoring a missing file. */\nconst removeFieldRenameMapFile = (fieldRenameMapFilePath: string): void => {\n try {\n rmSync(fieldRenameMapFilePath, { force: true });\n } catch {\n // Nothing to clean up.\n }\n};\n\ntype PrepareSwcOptimizationParams = {\n /** Options forwarded to the intlayer configuration loader. */\n configOptions?: GetConfigurationOptions;\n /** Pre-loaded dictionaries, to avoid a second `getDictionaries` call. */\n dictionaries?: Dictionary[];\n /** Compat-adapter callers declared by the consuming plugin, if any. */\n swcExtraCallers?: SwcExtraCallerConfig[];\n /**\n * Whether the `@intlayer/swc` plugin is installed *and* the resolved Next.js\n * version can load it. False leaves the compiled dictionaries untouched.\n */\n isSwcPluginUsable: boolean;\n};\n\n/**\n * Prepares the parts of the build optimisation the `@intlayer/swc` plugin\n * cannot do itself, and returns the field-rename tables it needs.\n *\n * Removing unused content fields (`build.purge`) and assigning short field\n * aliases (`build.minify`) require reading every component source file and\n * rewriting the compiled dictionary JSON — file I/O and cross-file state that a\n * per-file Wasm transform has no access to. That analysis therefore runs here,\n * in Node, through `@intlayer/babel`; the plugin only receives the resulting\n * tables and rewrites the matching source accesses\n * (`content.title` → `content.a`).\n *\n * The work is coordinated by a sentinel file: Next.js loads `next.config.*` in\n * several processes per build, and running the minify pass twice would derive\n * the aliases from the already-renamed dictionaries. Processes that lose the\n * race read the tables the winner cached.\n *\n * @returns The field-rename tables, keyed by dictionary key. Empty when the\n * pipeline is disabled, unavailable, or renamed nothing.\n */\nexport const prepareSwcOptimization = async (\n intlayerConfig: IntlayerConfig,\n params: PrepareSwcOptimizationParams\n): Promise<FieldRenameMapByDictionaryKey> => {\n const { configOptions, dictionaries, swcExtraCallers, isSwcPluginUsable } =\n params;\n\n const fieldRenameMapFilePath = getCacheFilePath(\n intlayerConfig,\n FIELD_RENAME_MAP_FILE_NAME\n );\n\n // A stale file from an earlier build would rename source accesses that the\n // dictionaries no longer match.\n if (\n !getIsPurgePipelineEnabled(\n intlayerConfig,\n swcExtraCallers,\n isSwcPluginUsable\n )\n ) {\n removeFieldRenameMapFile(fieldRenameMapFilePath);\n return {};\n }\n\n await runOnce(\n getCacheFilePath(intlayerConfig, PURGE_SENTINEL_FILE_NAME),\n () =>\n runPurgePipelineOnce(\n intlayerConfig,\n configOptions,\n dictionaries,\n fieldRenameMapFilePath\n ),\n { cacheTimeoutMs: PURGE_CACHE_TIMEOUT_MS }\n );\n\n return readFieldRenameMapFile(fieldRenameMapFilePath);\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"prepareSwcOptimization.cjs","names":["join","getProjectRequire","getAppLogger","colorize","ANSIColors","mkdir","dirname","writeFile","readFileSync","runOnce"],"sources":["../../../src/server/prepareSwcOptimization.ts"],"sourcesContent":["import { readFileSync, rmSync } from 'node:fs';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport type {\n getPurgePluginOptions as GetPurgePluginOptions,\n runIntlayerPurgePipeline as RunIntlayerPurgePipeline,\n SerializedFieldRenameMap,\n serializeFieldRenameMap as SerializeFieldRenameMap,\n} from '@intlayer/babel';\nimport type { CallerDescriptor } from '@intlayer/config/callers';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport type { GetConfigurationOptions } from '@intlayer/config/node';\nimport { getProjectRequire } from '@intlayer/config/utils';\nimport { runOnce } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\n\n/**\n * Field-rename tables keyed by dictionary key, forwarded to the\n * `@intlayer/swc` plugin's `fieldRenameMap` option.\n */\nexport type FieldRenameMapByDictionaryKey = Record<\n string,\n SerializedFieldRenameMap\n>;\n\n/** Verbosity forwarded to the `@intlayer/swc` plugin's `logLevel` option. */\nexport type SwcLogLevel = 'off' | 'info' | 'debug';\n\n/**\n * Subset of `@intlayer/babel` this module needs. The package is only an\n * optional runtime dependency, so it is required lazily and never imported at\n * module scope.\n */\ntype IntlayerBabelModule = {\n getPurgePluginOptions: typeof GetPurgePluginOptions;\n runIntlayerPurgePipeline: typeof RunIntlayerPurgePipeline;\n serializeFieldRenameMap: typeof SerializeFieldRenameMap;\n};\n\n/**\n * Opt-in to the SWC plugin's per-file tracing.\n *\n * The plugin transforms one file at a time with no cross-file state, so all it\n * can report is a line per file — hundreds of them on a real project, and with\n * no counterpart in the Vite build output. The purge and minify summaries that\n * `log.mode` does control are emitted by the pipeline itself, in Node; this\n * variable is purely a debugging aid on top.\n */\nconst SWC_LOG_LEVEL_ENV_VAR = 'INTLAYER_SWC_LOG_LEVEL';\n\n/** Sentinel coordinating the pipeline across the processes of one build. */\nconst PURGE_SENTINEL_FILE_NAME = 'intlayer-swc-purge.lock';\n\n/** Where the serialised field-rename tables are cached between processes. */\nconst FIELD_RENAME_MAP_FILE_NAME = 'swc-field-rename-map.json';\n\n/**\n * Next.js evaluates `next.config.*` in the main build process and again in each\n * Turbopack/webpack worker. The dictionary rewrite must happen exactly once —\n * a second run would derive the short aliases from the already-renamed JSON —\n * so the sentinel window has to cover a whole build startup.\n */\nconst PURGE_CACHE_TIMEOUT_MS = 30 * 1000;\n\n/** Absolute path of a file in the intlayer cache directory. */\nconst getCacheFilePath = (\n intlayerConfig: IntlayerConfig,\n fileName: string\n): string =>\n join(intlayerConfig.system.baseDir, '.intlayer', 'cache', fileName);\n\n/**\n * Resolves the verbosity the `@intlayer/swc` plugin should report at.\n *\n * Off unless {@link SWC_LOG_LEVEL_ENV_VAR} asks for tracing: `log.mode` governs\n * the purge and minify reporting, which the pipeline emits in Node so the\n * Next.js build reads the same as the Vite one.\n */\nexport const resolveSwcLogLevel = (\n intlayerConfig: IntlayerConfig\n): SwcLogLevel => {\n if (intlayerConfig.log.mode === 'disabled') return 'off';\n\n const requestedLogLevel = process.env[SWC_LOG_LEVEL_ENV_VAR];\n\n return requestedLogLevel === 'info' || requestedLogLevel === 'debug'\n ? requestedLogLevel\n : 'off';\n};\n\n/**\n * Whether the purge / minify pipeline should run for this build.\n *\n * Compat-adapter callers do not disable it: their descriptors reach the usage\n * analyser intact (`compatCallers`), which records the dictionaries they read\n * and marks those as unrenamable — so purging stays field-accurate and the\n * minify step leaves them alone.\n */\nconst getIsPurgePipelineEnabled = (\n intlayerConfig: IntlayerConfig,\n isSwcPluginUsable: boolean\n): boolean => {\n const { purge, minify, optimize } = intlayerConfig.build;\n\n if (optimize === false) return false;\n if (!purge && !minify) return false;\n\n // The pipeline rewrites the compiled dictionaries in place — dropping unused\n // fields and renaming the rest to short aliases. Only the SWC plugin rewrites\n // the matching source accesses, so running this without it would ship\n // dictionaries whose keys no longer match the code reading them.\n if (!isSwcPluginUsable) return false;\n\n // `editor.enabled` is deliberately not checked here: it only stands the\n // field-rename step down — inside the pipeline, and with its own explanation\n // — the same way the Vite build does. Purging still runs, and the rename map\n // the SWC plugin receives comes back empty, so source and JSON stay in sync.\n\n return true;\n};\n\n/**\n * Exports this module needs from `@intlayer/babel`. Checked one by one so a\n * version predating the purge pipeline is rejected like a missing package,\n * instead of failing later with `runIntlayerPurgePipeline is not a function`.\n */\nconst REQUIRED_BABEL_EXPORTS = [\n 'getPurgePluginOptions',\n 'runIntlayerPurgePipeline',\n 'serializeFieldRenameMap',\n] as const satisfies readonly (keyof IntlayerBabelModule)[];\n\n/**\n * Lazily loads `@intlayer/babel`, or `null` when it is not installed or too old\n * to expose the purge pipeline.\n */\nconst loadIntlayerBabel = (\n intlayerConfig: IntlayerConfig\n): IntlayerBabelModule | null => {\n try {\n const requireFunction =\n intlayerConfig.build?.require ?? getProjectRequire();\n\n const intlayerBabel = requireFunction(\n '@intlayer/babel'\n ) as Partial<IntlayerBabelModule>;\n\n const hasEveryRequiredExport = REQUIRED_BABEL_EXPORTS.every(\n (exportName) => typeof intlayerBabel?.[exportName] === 'function'\n );\n\n if (!hasEveryRequiredExport) return null;\n\n return intlayerBabel as IntlayerBabelModule;\n } catch {\n return null;\n }\n};\n\n/**\n * Whether the purge / minify pipeline can run at all, i.e. whether a usable\n * `@intlayer/babel` is resolvable from the project.\n *\n * Used by `withIntlayer` to keep the build report honest: announcing\n * `Dictionary minification enabled` while the pipeline cannot load would describe a\n * pass that never happens.\n */\nexport const getIsPurgePipelineAvailable = (\n intlayerConfig: IntlayerConfig\n): boolean => loadIntlayerBabel(intlayerConfig) !== null;\n\n/**\n * Runs the purge / minify pipeline and writes the resulting field-rename\n * tables to the cache file so every process of the build reads the same ones.\n */\nconst runPurgePipelineOnce = async (\n intlayerConfig: IntlayerConfig,\n configOptions: GetConfigurationOptions | undefined,\n dictionaries: Dictionary[] | undefined,\n compatCallers: CallerDescriptor[] | undefined,\n fieldRenameMapFilePath: string\n): Promise<void> => {\n const appLogger = getAppLogger(intlayerConfig);\n\n const intlayerBabel = loadIntlayerBabel(intlayerConfig);\n\n if (!intlayerBabel) {\n // Not verbose: the build asked for purge / minify and is silently getting\n // neither, which is exactly the kind of thing a default build must report.\n appLogger(\n [\n 'Dictionary purge and minification are',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('@intlayer/babel', ANSIColors.GREY_LIGHT),\n 'is missing or too old to expose the purge pipeline — install it in',\n 'this project to enable them.',\n ],\n { level: 'warn' }\n );\n return;\n }\n\n const {\n getPurgePluginOptions,\n runIntlayerPurgePipeline,\n serializeFieldRenameMap,\n } = intlayerBabel;\n\n // `runOnce` discards whatever this callback throws, so a pipeline failure\n // would otherwise leave the build with no dictionaries rewritten and no\n // explanation of why.\n let fieldRenameMap: FieldRenameMapByDictionaryKey;\n\n try {\n // The pipeline reports what it purged and minified through the intlayer\n // logger, matching the Vite build output.\n const pruneContext = runIntlayerPurgePipeline(\n getPurgePluginOptions({\n configOptions,\n dictionaries,\n // Without them the analyser would not recognise `useTranslation(...)`\n // & co. and would purge the fields those call sites read.\n overrides: compatCallers?.length ? { compatCallers } : undefined,\n })\n );\n\n fieldRenameMap = serializeFieldRenameMap(pruneContext);\n } catch (pipelineError) {\n appLogger(\n [\n 'Dictionary purge and minification',\n colorize('failed', ANSIColors.RED),\n '— the compiled dictionaries are left untouched.',\n pipelineError instanceof Error\n ? `(${pipelineError.message})`\n : String(pipelineError),\n ],\n { level: 'error' }\n );\n return;\n }\n\n await mkdir(dirname(fieldRenameMapFilePath), { recursive: true });\n await writeFile(\n fieldRenameMapFilePath,\n JSON.stringify(fieldRenameMap),\n 'utf-8'\n );\n};\n\n/** Reads the cached field-rename tables, or `{}` when none were written. */\nconst readFieldRenameMapFile = (\n fieldRenameMapFilePath: string\n): FieldRenameMapByDictionaryKey => {\n try {\n return JSON.parse(\n readFileSync(fieldRenameMapFilePath, 'utf-8')\n ) as FieldRenameMapByDictionaryKey;\n } catch {\n return {};\n }\n};\n\n/** Removes the cached field-rename tables, ignoring a missing file. */\nconst removeFieldRenameMapFile = (fieldRenameMapFilePath: string): void => {\n try {\n rmSync(fieldRenameMapFilePath, { force: true });\n } catch {\n // Nothing to clean up.\n }\n};\n\ntype PrepareSwcOptimizationParams = {\n /** Options forwarded to the intlayer configuration loader. */\n configOptions?: GetConfigurationOptions;\n /** Pre-loaded dictionaries, to avoid a second `getDictionaries` call. */\n dictionaries?: Dictionary[];\n /** Compat-adapter callers declared by the consuming plugin, if any. */\n compatCallers?: CallerDescriptor[];\n /**\n * Whether the `@intlayer/swc` plugin is installed *and* the resolved Next.js\n * version can load it. False leaves the compiled dictionaries untouched.\n */\n isSwcPluginUsable: boolean;\n};\n\n/**\n * Prepares the parts of the build optimisation the `@intlayer/swc` plugin\n * cannot do itself, and returns the field-rename tables it needs.\n *\n * Removing unused content fields (`build.purge`) and assigning short field\n * aliases (`build.minify`) require reading every component source file and\n * rewriting the compiled dictionary JSON — file I/O and cross-file state that a\n * per-file Wasm transform has no access to. That analysis therefore runs here,\n * in Node, through `@intlayer/babel`; the plugin only receives the resulting\n * tables and rewrites the matching source accesses\n * (`content.title` → `content.a`).\n *\n * The work is coordinated by a sentinel file: Next.js loads `next.config.*` in\n * several processes per build, and running the minify pass twice would derive\n * the aliases from the already-renamed dictionaries. Processes that lose the\n * race read the tables the winner cached.\n *\n * @returns The field-rename tables, keyed by dictionary key. Empty when the\n * pipeline is disabled, unavailable, or renamed nothing.\n */\nexport const prepareSwcOptimization = async (\n intlayerConfig: IntlayerConfig,\n params: PrepareSwcOptimizationParams\n): Promise<FieldRenameMapByDictionaryKey> => {\n const { configOptions, dictionaries, compatCallers, isSwcPluginUsable } =\n params;\n\n const fieldRenameMapFilePath = getCacheFilePath(\n intlayerConfig,\n FIELD_RENAME_MAP_FILE_NAME\n );\n\n // A stale file from an earlier build would rename source accesses that the\n // dictionaries no longer match.\n if (!getIsPurgePipelineEnabled(intlayerConfig, isSwcPluginUsable)) {\n removeFieldRenameMapFile(fieldRenameMapFilePath);\n return {};\n }\n\n await runOnce(\n getCacheFilePath(intlayerConfig, PURGE_SENTINEL_FILE_NAME),\n () =>\n runPurgePipelineOnce(\n intlayerConfig,\n configOptions,\n dictionaries,\n compatCallers,\n fieldRenameMapFilePath\n ),\n { cacheTimeoutMs: PURGE_CACHE_TIMEOUT_MS }\n );\n\n return readFieldRenameMapFile(fieldRenameMapFilePath);\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let _intlayer_config_logger = require("@intlayer/config/logger");
|
|
3
|
+
let _intlayer_engine_utils = require("@intlayer/engine/utils");
|
|
4
|
+
|
|
5
|
+
//#region src/server/startContentWatcher.ts
|
|
6
|
+
/**
|
|
7
|
+
* Release from which `withIntlayer` watches the content declarations on
|
|
8
|
+
* Turbopack, making `intlayer watch --with` redundant on Next.js.
|
|
9
|
+
*/
|
|
10
|
+
const SELF_WATCHING_RELEASE = "9.5.0";
|
|
11
|
+
/**
|
|
12
|
+
* How often a process that lost the ownership race checks whether the owner is
|
|
13
|
+
* gone. Next.js's short-lived config processes release the lock when they exit,
|
|
14
|
+
* so the long-lived dev server has to be able to pick it up afterwards.
|
|
15
|
+
*/
|
|
16
|
+
const TAKEOVER_POLL_INTERVAL_MS = 5e3;
|
|
17
|
+
/** Set once the watcher is running in this process. */
|
|
18
|
+
let isWatching = false;
|
|
19
|
+
/**
|
|
20
|
+
* Set while an ownership attempt is in flight.
|
|
21
|
+
*
|
|
22
|
+
* Taking the lock is asynchronous, and `next.config.*` is evaluated several
|
|
23
|
+
* times per command — without this, those evaluations would pile up concurrent
|
|
24
|
+
* attempts, each awaiting the same lock.
|
|
25
|
+
*/
|
|
26
|
+
let isClaimInFlight = false;
|
|
27
|
+
/** Reports a redundant `intlayer watch` running alongside this dev server. */
|
|
28
|
+
const reportRedundantCliWatcher = (configuration, cliWatcherLabel) => (0, _intlayer_engine_utils.reportRedundantContentWatcher)(configuration, {
|
|
29
|
+
cliLabel: cliWatcherLabel,
|
|
30
|
+
bundlerLabel: "next-intlayer",
|
|
31
|
+
since: SELF_WATCHING_RELEASE
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Starts the content declaration watcher for a Turbopack dev server.
|
|
35
|
+
*
|
|
36
|
+
* On webpack this is done by `IntlayerPlugin`, which Turbopack cannot run — so
|
|
37
|
+
* before {@link SELF_WATCHING_RELEASE} a Turbopack app had to be started
|
|
38
|
+
* through `intlayer watch --with next dev` for `.content` edits to rebuild
|
|
39
|
+
* `.intlayer`.
|
|
40
|
+
*
|
|
41
|
+
* Stands down — with an explanation — when that CLI watcher is running anyway,
|
|
42
|
+
* since two watchers would rebuild the same dictionaries at the same time.
|
|
43
|
+
* Otherwise starting is idempotent: the first process to take the lock watches,
|
|
44
|
+
* and the others retry in the background so a short-lived config process
|
|
45
|
+
* handing the lock back does not leave the session unwatched.
|
|
46
|
+
*
|
|
47
|
+
* @param configuration - The resolved Intlayer configuration.
|
|
48
|
+
*/
|
|
49
|
+
const claimAndWatch = async (configuration) => {
|
|
50
|
+
const appLogger = (0, _intlayer_config_logger.getAppLogger)(configuration);
|
|
51
|
+
if (!await (0, _intlayer_engine_utils.acquireContentWatcherLock)(configuration, {
|
|
52
|
+
source: "bundler",
|
|
53
|
+
label: "next-intlayer",
|
|
54
|
+
since: SELF_WATCHING_RELEASE
|
|
55
|
+
})) {
|
|
56
|
+
const owner = await (0, _intlayer_engine_utils.getContentWatcherOwner)(configuration);
|
|
57
|
+
if (owner?.source === "cli") {
|
|
58
|
+
reportRedundantCliWatcher(configuration, owner.label);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
startContentWatcher(configuration);
|
|
63
|
+
}, TAKEOVER_POLL_INTERVAL_MS).unref?.();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
isWatching = true;
|
|
67
|
+
try {
|
|
68
|
+
const { watch } = await import("@intlayer/engine/watcher");
|
|
69
|
+
await watch({ configuration });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
isWatching = false;
|
|
72
|
+
appLogger(["Failed to watch Intlayer content declarations:", error], { level: "error" });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const startContentWatcher = (configuration) => {
|
|
76
|
+
if (isWatching || isClaimInFlight) return;
|
|
77
|
+
if (!configuration.content.watch) return;
|
|
78
|
+
const cliWatcherLabel = (0, _intlayer_engine_utils.getCliContentWatcherLabel)();
|
|
79
|
+
if (cliWatcherLabel) {
|
|
80
|
+
reportRedundantCliWatcher(configuration, cliWatcherLabel);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
isClaimInFlight = true;
|
|
84
|
+
claimAndWatch(configuration).finally(() => {
|
|
85
|
+
isClaimInFlight = false;
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
exports.startContentWatcher = startContentWatcher;
|
|
91
|
+
//# sourceMappingURL=startContentWatcher.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startContentWatcher.cjs","names":["reportRedundantContentWatcher","getAppLogger","acquireContentWatcherLock","getContentWatcherOwner","getCliContentWatcherLabel"],"sources":["../../../src/server/startContentWatcher.ts"],"sourcesContent":["import { getAppLogger } from '@intlayer/config/logger';\nimport {\n acquireContentWatcherLock,\n getCliContentWatcherLabel,\n getContentWatcherOwner,\n reportRedundantContentWatcher,\n} from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n\n/**\n * Release from which `withIntlayer` watches the content declarations on\n * Turbopack, making `intlayer watch --with` redundant on Next.js.\n */\nconst SELF_WATCHING_RELEASE = '9.5.0';\n\n/**\n * How often a process that lost the ownership race checks whether the owner is\n * gone. Next.js's short-lived config processes release the lock when they exit,\n * so the long-lived dev server has to be able to pick it up afterwards.\n */\nconst TAKEOVER_POLL_INTERVAL_MS = 5000;\n\n/** Set once the watcher is running in this process. */\nlet isWatching = false;\n\n/**\n * Set while an ownership attempt is in flight.\n *\n * Taking the lock is asynchronous, and `next.config.*` is evaluated several\n * times per command — without this, those evaluations would pile up concurrent\n * attempts, each awaiting the same lock.\n */\nlet isClaimInFlight = false;\n\n/** Reports a redundant `intlayer watch` running alongside this dev server. */\nconst reportRedundantCliWatcher = (\n configuration: IntlayerConfig,\n cliWatcherLabel: string\n): void =>\n reportRedundantContentWatcher(configuration, {\n cliLabel: cliWatcherLabel,\n bundlerLabel: 'next-intlayer',\n since: SELF_WATCHING_RELEASE,\n });\n\n/**\n * Starts the content declaration watcher for a Turbopack dev server.\n *\n * On webpack this is done by `IntlayerPlugin`, which Turbopack cannot run — so\n * before {@link SELF_WATCHING_RELEASE} a Turbopack app had to be started\n * through `intlayer watch --with next dev` for `.content` edits to rebuild\n * `.intlayer`.\n *\n * Stands down — with an explanation — when that CLI watcher is running anyway,\n * since two watchers would rebuild the same dictionaries at the same time.\n * Otherwise starting is idempotent: the first process to take the lock watches,\n * and the others retry in the background so a short-lived config process\n * handing the lock back does not leave the session unwatched.\n *\n * @param configuration - The resolved Intlayer configuration.\n */\nconst claimAndWatch = async (configuration: IntlayerConfig): Promise<void> => {\n const appLogger = getAppLogger(configuration);\n\n const hasAcquiredLock = await acquireContentWatcherLock(configuration, {\n source: 'bundler',\n label: 'next-intlayer',\n since: SELF_WATCHING_RELEASE,\n });\n\n if (!hasAcquiredLock) {\n // A CLI watcher started from another terminal is just as redundant as one\n // wrapping this command, and worth the same explanation.\n const owner = await getContentWatcherOwner(configuration);\n\n if (owner?.source === 'cli') {\n reportRedundantCliWatcher(configuration, owner.label);\n return;\n }\n\n const retryTimer = setTimeout(() => {\n startContentWatcher(configuration);\n }, TAKEOVER_POLL_INTERVAL_MS);\n\n // Unreferenced so a process whose only remaining work is this retry can\n // still exit.\n retryTimer.unref?.();\n\n return;\n }\n\n isWatching = true;\n\n try {\n // Imported lazily: `@intlayer/engine/watcher` pulls in `@parcel/watcher`, a\n // native module that has no business being loaded during a production\n // build. `withIntlayer` already awaited `prepareIntlayer`, so the watcher\n // only has to pick up the changes that follow.\n const { watch } = await import('@intlayer/engine/watcher');\n\n await watch({ configuration });\n } catch (error) {\n isWatching = false;\n\n appLogger(['Failed to watch Intlayer content declarations:', error], {\n level: 'error',\n });\n }\n};\n\nexport const startContentWatcher = (configuration: IntlayerConfig): void => {\n if (isWatching || isClaimInFlight) return;\n if (!configuration.content.watch) return;\n\n // Spawned by `intlayer watch --with`: the CLI watcher covers the project, and\n // this is known from the environment alone — no need to touch the lock.\n const cliWatcherLabel = getCliContentWatcherLabel();\n\n if (cliWatcherLabel) {\n reportRedundantCliWatcher(configuration, cliWatcherLabel);\n return;\n }\n\n isClaimInFlight = true;\n\n // Deliberately not awaited: `withIntlayerSync` is synchronous, and the dev\n // server must not wait on the watcher to start serving.\n claimAndWatch(configuration).finally(() => {\n isClaimInFlight = false;\n });\n};\n"],"mappings":";;;;;;;;;AAaA,MAAM,wBAAwB;;;;;;AAO9B,MAAM,4BAA4B;;AAGlC,IAAI,aAAa;;;;;;;;AASjB,IAAI,kBAAkB;;AAGtB,MAAM,6BACJ,eACA,wBAEAA,sDAA8B,eAAe;CAC3C,UAAU;CACV,cAAc;CACd,OAAO;AACT,CAAC;;;;;;;;;;;;;;;;;AAkBH,MAAM,gBAAgB,OAAO,kBAAiD;CAC5E,MAAM,gBAAYC,sCAAa,aAAa;CAQ5C,IAAI,CAAC,UANyBC,kDAA0B,eAAe;EACrE,QAAQ;EACR,OAAO;EACP,OAAO;CACT,CAAC,GAEqB;EAGpB,MAAM,QAAQ,UAAMC,+CAAuB,aAAa;EAExD,IAAI,OAAO,WAAW,OAAO;GAC3B,0BAA0B,eAAe,MAAM,KAAK;GACpD;EACF;EAQA,AANmB,iBAAiB;GAClC,oBAAoB,aAAa;EACnC,GAAG,yBAIM,CAAC,CAAC,QAAQ;EAEnB;CACF;CAEA,aAAa;CAEb,IAAI;EAKF,MAAM,EAAE,UAAU,MAAM,OAAO;EAE/B,MAAM,MAAM,EAAE,cAAc,CAAC;CAC/B,SAAS,OAAO;EACd,aAAa;EAEb,UAAU,CAAC,kDAAkD,KAAK,GAAG,EACnE,OAAO,QACT,CAAC;CACH;AACF;AAEA,MAAa,uBAAuB,kBAAwC;CAC1E,IAAI,cAAc,iBAAiB;CACnC,IAAI,CAAC,cAAc,QAAQ,OAAO;CAIlC,MAAM,sBAAkBC,kDAA0B;CAElD,IAAI,iBAAiB;EACnB,0BAA0B,eAAe,eAAe;EACxD;CACF;CAEA,kBAAkB;CAIlB,cAAc,aAAa,CAAC,CAAC,cAAc;EACzC,kBAAkB;CACpB,CAAC;AACH"}
|