@intlayer/engine 9.0.0-canary.13 → 9.0.0-canary.14

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.
Files changed (66) hide show
  1. package/dist/cjs/createType/createModuleAugmentation.cjs +1 -1
  2. package/dist/cjs/createType/createModuleAugmentation.cjs.map +1 -1
  3. package/dist/cjs/createType/createType.cjs +2 -1
  4. package/dist/cjs/createType/createType.cjs.map +1 -1
  5. package/dist/cjs/loadDictionaries/loadContentDeclaration.cjs +2 -2
  6. package/dist/cjs/loadDictionaries/loadContentDeclaration.cjs.map +1 -1
  7. package/dist/cjs/prepareIntlayer.cjs +11 -7
  8. package/dist/cjs/prepareIntlayer.cjs.map +1 -1
  9. package/dist/cjs/syncPluginKit/createFileAdapter.cjs +119 -0
  10. package/dist/cjs/syncPluginKit/createFileAdapter.cjs.map +1 -0
  11. package/dist/cjs/syncPluginKit/createSyncPlugin.cjs +154 -0
  12. package/dist/cjs/syncPluginKit/createSyncPlugin.cjs.map +1 -0
  13. package/dist/cjs/syncPluginKit/extractKeyAndLocaleFromPath.cjs +37 -0
  14. package/dist/cjs/syncPluginKit/extractKeyAndLocaleFromPath.cjs.map +1 -0
  15. package/dist/cjs/syncPluginKit/filePathPatternHelpers.cjs +20 -0
  16. package/dist/cjs/syncPluginKit/filePathPatternHelpers.cjs.map +1 -0
  17. package/dist/cjs/syncPluginKit/index.cjs +11 -0
  18. package/dist/cjs/syncPluginKit/types.cjs +0 -0
  19. package/dist/cjs/utils/runParallel/index.cjs +2 -2
  20. package/dist/cjs/utils/runParallel/index.cjs.map +1 -1
  21. package/dist/cjs/watcher.cjs +2 -3
  22. package/dist/cjs/watcher.cjs.map +1 -1
  23. package/dist/cjs/writeFileIfChanged.cjs +41 -17
  24. package/dist/cjs/writeFileIfChanged.cjs.map +1 -1
  25. package/dist/esm/createType/createModuleAugmentation.mjs +1 -1
  26. package/dist/esm/createType/createModuleAugmentation.mjs.map +1 -1
  27. package/dist/esm/createType/createType.mjs +3 -2
  28. package/dist/esm/createType/createType.mjs.map +1 -1
  29. package/dist/esm/loadDictionaries/loadContentDeclaration.mjs +2 -2
  30. package/dist/esm/loadDictionaries/loadContentDeclaration.mjs.map +1 -1
  31. package/dist/esm/prepareIntlayer.mjs +11 -7
  32. package/dist/esm/prepareIntlayer.mjs.map +1 -1
  33. package/dist/esm/syncPluginKit/createFileAdapter.mjs +115 -0
  34. package/dist/esm/syncPluginKit/createFileAdapter.mjs.map +1 -0
  35. package/dist/esm/syncPluginKit/createSyncPlugin.mjs +152 -0
  36. package/dist/esm/syncPluginKit/createSyncPlugin.mjs.map +1 -0
  37. package/dist/esm/syncPluginKit/extractKeyAndLocaleFromPath.mjs +35 -0
  38. package/dist/esm/syncPluginKit/extractKeyAndLocaleFromPath.mjs.map +1 -0
  39. package/dist/esm/syncPluginKit/filePathPatternHelpers.mjs +18 -0
  40. package/dist/esm/syncPluginKit/filePathPatternHelpers.mjs.map +1 -0
  41. package/dist/esm/syncPluginKit/index.mjs +6 -0
  42. package/dist/esm/syncPluginKit/types.mjs +0 -0
  43. package/dist/esm/utils/runParallel/index.mjs +2 -2
  44. package/dist/esm/utils/runParallel/index.mjs.map +1 -1
  45. package/dist/esm/watcher.mjs +2 -3
  46. package/dist/esm/watcher.mjs.map +1 -1
  47. package/dist/esm/writeFileIfChanged.mjs +44 -20
  48. package/dist/esm/writeFileIfChanged.mjs.map +1 -1
  49. package/dist/types/createType/createType.d.ts.map +1 -1
  50. package/dist/types/syncPluginKit/createFileAdapter.d.ts +51 -0
  51. package/dist/types/syncPluginKit/createFileAdapter.d.ts.map +1 -0
  52. package/dist/types/syncPluginKit/createSyncPlugin.d.ts +18 -0
  53. package/dist/types/syncPluginKit/createSyncPlugin.d.ts.map +1 -0
  54. package/dist/types/syncPluginKit/extractKeyAndLocaleFromPath.d.ts +20 -0
  55. package/dist/types/syncPluginKit/extractKeyAndLocaleFromPath.d.ts.map +1 -0
  56. package/dist/types/syncPluginKit/filePathPatternHelpers.d.ts +16 -0
  57. package/dist/types/syncPluginKit/filePathPatternHelpers.d.ts.map +1 -0
  58. package/dist/types/syncPluginKit/index.d.ts +6 -0
  59. package/dist/types/syncPluginKit/types.d.ts +149 -0
  60. package/dist/types/syncPluginKit/types.d.ts.map +1 -0
  61. package/dist/types/utils/runParallel/index.d.ts +6 -1
  62. package/dist/types/utils/runParallel/index.d.ts.map +1 -1
  63. package/dist/types/watcher.d.ts.map +1 -1
  64. package/dist/types/writeFileIfChanged.d.ts +30 -6
  65. package/dist/types/writeFileIfChanged.d.ts.map +1 -1
  66. package/package.json +24 -8
@@ -1,7 +1,7 @@
1
- import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
1
+ import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
2
  import { basename, join } from "node:path";
3
- import { createHash, randomBytes } from "node:crypto";
4
- import { createReadStream, rmSync } from "node:fs";
3
+ import { randomBytes } from "node:crypto";
4
+ import { rmSync } from "node:fs";
5
5
 
6
6
  //#region src/writeFileIfChanged.ts
7
7
  const activeTempFiles = /* @__PURE__ */ new Set();
@@ -10,28 +10,52 @@ process.on("exit", () => {
10
10
  rmSync(file, { force: true });
11
11
  } catch {}
12
12
  });
13
- const getFileHash = (path) => {
14
- return new Promise((resolve) => {
15
- const hash = createHash("sha256");
16
- const stream = createReadStream(path);
17
- stream.on("data", (chunk) => hash.update(chunk));
18
- stream.on("end", () => resolve(hash.digest("hex")));
19
- stream.on("error", () => resolve(null));
20
- });
21
- };
22
- const writeFileIfChanged = async (path, data, { encoding = "utf8", tempDir } = {}) => {
23
- if (createHash("sha256").update(data, encoding).digest("hex") === await getFileHash(path)) return false;
13
+ /**
14
+ * Permission bits a freshly created file receives on this system. Detected once
15
+ * from the first temp file we actually write (`process.umask()` with no
16
+ * argument is deprecated and thread-unsafe) and cached, so the atomic path can
17
+ * skip restoring the mode whenever the source file already uses the default
18
+ * mode — which is the case for essentially every generated file.
19
+ */
20
+ let defaultFileMode;
21
+ /**
22
+ * Write `data` to `path` only when it differs from the file already on disk,
23
+ * preserving the existing file's permission mode.
24
+ *
25
+ * The whole file is read back and byte-compared because the files written here
26
+ * (dictionaries, entry points, types) are small, so reading beats streaming a
27
+ * hash. When the content is unchanged the write is skipped entirely, which both
28
+ * avoids inode churn and prevents downstream watchers from rebuilding.
29
+ *
30
+ * @returns `true` when the file was written, `false` when it was already up to date.
31
+ */
32
+ const writeFileIfChanged = async (path, data, { encoding = "utf8", tempDir, atomic = true } = {}) => {
33
+ const newData = Buffer.from(data, encoding);
34
+ let existingData = null;
35
+ try {
36
+ existingData = await readFile(path);
37
+ } catch {}
38
+ if (existingData?.equals(newData)) return false;
39
+ if (!atomic) {
40
+ await writeFile(path, newData);
41
+ return true;
42
+ }
43
+ let modeToRestore;
44
+ if (existingData !== null) try {
45
+ modeToRestore = (await stat(path)).mode & 511;
46
+ } catch {}
24
47
  if (tempDir) await mkdir(tempDir, { recursive: true });
25
48
  const tempFileName = `${basename(path)}.${Date.now()}-${randomBytes(4).toString("hex")}.tmp`;
26
49
  const tempPath = tempDir ? join(tempDir, tempFileName) : `${path}.${tempFileName}`;
27
50
  activeTempFiles.add(tempPath);
28
51
  try {
29
- let mode;
30
- try {
31
- mode = (await stat(path)).mode;
32
- } catch {}
33
- await writeFile(tempPath, data, { encoding });
34
- if (mode !== void 0) await chmod(tempPath, mode);
52
+ await writeFile(tempPath, newData);
53
+ if (modeToRestore !== void 0) {
54
+ if (defaultFileMode === void 0) try {
55
+ defaultFileMode = (await stat(tempPath)).mode & 511;
56
+ } catch {}
57
+ if (modeToRestore !== defaultFileMode) await chmod(tempPath, modeToRestore);
58
+ }
35
59
  await rename(tempPath, path);
36
60
  } catch (error) {
37
61
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"writeFileIfChanged.mjs","names":[],"sources":["../../src/writeFileIfChanged.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport { createReadStream, rmSync } from 'node:fs';\nimport { chmod, mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\n\nconst activeTempFiles = new Set<string>();\n\n// Synchronous cleanup on process exit\nprocess.on('exit', () => {\n for (const file of activeTempFiles) {\n try {\n rmSync(file, { force: true });\n } catch {}\n }\n});\n\n// Helper to hash existing file via stream\nconst getFileHash = (path: string): Promise<string | null> => {\n return new Promise((resolve) => {\n const hash = createHash('sha256');\n const stream = createReadStream(path);\n stream.on('data', (chunk) => hash.update(chunk));\n stream.on('end', () => resolve(hash.digest('hex')));\n stream.on('error', () => resolve(null));\n });\n};\n\nexport const writeFileIfChanged = async (\n path: string,\n data: string,\n {\n encoding = 'utf8',\n tempDir,\n }: { encoding?: BufferEncoding; tempDir?: string } = {}\n): Promise<boolean> => {\n const newDataHash = createHash('sha256').update(data, encoding).digest('hex');\n const existingHash = await getFileHash(path);\n\n if (newDataHash === existingHash) {\n return false;\n }\n\n if (tempDir) {\n await mkdir(tempDir, { recursive: true });\n }\n\n const tempFileName = `${basename(path)}.${Date.now()}-${randomBytes(4).toString('hex')}.tmp`;\n const tempPath = tempDir\n ? join(tempDir, tempFileName)\n : `${path}.${tempFileName}`;\n activeTempFiles.add(tempPath);\n\n try {\n let mode: number | undefined;\n try {\n mode = (await stat(path)).mode;\n } catch {}\n\n await writeFile(tempPath, data, { encoding });\n\n if (mode !== undefined) {\n await chmod(tempPath, mode);\n }\n\n await rename(tempPath, path);\n } catch (error) {\n try {\n await rm(tempPath, { force: true });\n } catch {}\n throw error;\n } finally {\n activeTempFiles.delete(tempPath);\n }\n\n return true;\n};\n"],"mappings":";;;;;;AAKA,MAAM,kCAAkB,IAAI,KAAa;AAGzC,QAAQ,GAAG,cAAc;AACvB,MAAK,MAAM,QAAQ,gBACjB,KAAI;AACF,SAAO,MAAM,EAAE,OAAO,MAAM,CAAC;SACvB;EAEV;AAGF,MAAM,eAAe,SAAyC;AAC5D,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,OAAO,WAAW,SAAS;EACjC,MAAM,SAAS,iBAAiB,KAAK;AACrC,SAAO,GAAG,SAAS,UAAU,KAAK,OAAO,MAAM,CAAC;AAChD,SAAO,GAAG,aAAa,QAAQ,KAAK,OAAO,MAAM,CAAC,CAAC;AACnD,SAAO,GAAG,eAAe,QAAQ,KAAK,CAAC;GACvC;;AAGJ,MAAa,qBAAqB,OAChC,MACA,MACA,EACE,WAAW,QACX,YACmD,EAAE,KAClC;AAIrB,KAHoB,WAAW,SAAS,CAAC,OAAO,MAAM,SAAS,CAAC,OAAO,MAGxD,KAAK,MAFO,YAAY,KAAK,CAG1C,QAAO;AAGT,KAAI,QACF,OAAM,MAAM,SAAS,EAAE,WAAW,MAAM,CAAC;CAG3C,MAAM,eAAe,GAAG,SAAS,KAAK,CAAC,GAAG,KAAK,KAAK,CAAC,GAAG,YAAY,EAAE,CAAC,SAAS,MAAM,CAAC;CACvF,MAAM,WAAW,UACb,KAAK,SAAS,aAAa,GAC3B,GAAG,KAAK,GAAG;AACf,iBAAgB,IAAI,SAAS;AAE7B,KAAI;EACF,IAAI;AACJ,MAAI;AACF,WAAQ,MAAM,KAAK,KAAK,EAAE;UACpB;AAER,QAAM,UAAU,UAAU,MAAM,EAAE,UAAU,CAAC;AAE7C,MAAI,SAAS,OACX,OAAM,MAAM,UAAU,KAAK;AAG7B,QAAM,OAAO,UAAU,KAAK;UACrB,OAAO;AACd,MAAI;AACF,SAAM,GAAG,UAAU,EAAE,OAAO,MAAM,CAAC;UAC7B;AACR,QAAM;WACE;AACR,kBAAgB,OAAO,SAAS;;AAGlC,QAAO"}
1
+ {"version":3,"file":"writeFileIfChanged.mjs","names":[],"sources":["../../src/writeFileIfChanged.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { rmSync } from 'node:fs';\nimport {\n chmod,\n mkdir,\n readFile,\n rename,\n rm,\n stat,\n writeFile,\n} from 'node:fs/promises';\nimport { basename, join } from 'node:path';\n\nconst activeTempFiles = new Set<string>();\n\n// Synchronous cleanup on process exit\nprocess.on('exit', () => {\n for (const file of activeTempFiles) {\n try {\n rmSync(file, { force: true });\n } catch {}\n }\n});\n\n/**\n * Permission bits a freshly created file receives on this system. Detected once\n * from the first temp file we actually write (`process.umask()` with no\n * argument is deprecated and thread-unsafe) and cached, so the atomic path can\n * skip restoring the mode whenever the source file already uses the default\n * mode — which is the case for essentially every generated file.\n */\nlet defaultFileMode: number | undefined;\n\n/**\n * Options for {@link writeFileIfChanged}.\n */\nexport type WriteFileIfChangedOptions = {\n /** Encoding used to turn `data` into bytes. Defaults to `'utf8'`. */\n encoding?: BufferEncoding;\n /** Directory to hold the temporary file used for the atomic swap. */\n tempDir?: string;\n /**\n * Write atomically via a temp file + `rename` so readers never observe a\n * half-written file. This creates a fresh inode on every write, which roughly\n * doubles the cost of a changed write compared to overwriting in place. Set to\n * `false` for callers whose consumers tolerate torn reads to write directly.\n * Defaults to `true`.\n */\n atomic?: boolean;\n};\n\n/**\n * Write `data` to `path` only when it differs from the file already on disk,\n * preserving the existing file's permission mode.\n *\n * The whole file is read back and byte-compared because the files written here\n * (dictionaries, entry points, types) are small, so reading beats streaming a\n * hash. When the content is unchanged the write is skipped entirely, which both\n * avoids inode churn and prevents downstream watchers from rebuilding.\n *\n * @returns `true` when the file was written, `false` when it was already up to date.\n */\nexport const writeFileIfChanged = async (\n path: string,\n data: string,\n { encoding = 'utf8', tempDir, atomic = true }: WriteFileIfChangedOptions = {}\n): Promise<boolean> => {\n const newData = Buffer.from(data, encoding);\n\n // Read the current content first: the common case is an unchanged file, and\n // this single read is the whole fast path (no stat/chmod needed to bail out).\n let existingData: Buffer | null = null;\n try {\n existingData = await readFile(path);\n } catch {}\n\n if (existingData?.equals(newData)) {\n return false;\n }\n\n // Fast path: overwrite in place. Truncating an existing file keeps its inode,\n // so its permission mode is preserved automatically (no chmod needed). This\n // skips the temp-file inode churn at the cost of exposing readers to a\n // partially written file.\n if (!atomic) {\n await writeFile(path, newData);\n return true;\n }\n\n // The atomic swap replaces the inode, so the existing mode is not preserved\n // and must be reapplied. Only a file that already exists has a mode to keep.\n let modeToRestore: number | undefined;\n if (existingData !== null) {\n try {\n modeToRestore = (await stat(path)).mode & 0o777;\n } catch {}\n }\n\n if (tempDir) {\n await mkdir(tempDir, { recursive: true });\n }\n\n const tempFileName = `${basename(path)}.${Date.now()}-${randomBytes(4).toString('hex')}.tmp`;\n const tempPath = tempDir\n ? join(tempDir, tempFileName)\n : `${path}.${tempFileName}`;\n activeTempFiles.add(tempPath);\n\n try {\n await writeFile(tempPath, newData);\n\n if (modeToRestore !== undefined) {\n // Learn the ambient default mode once from a real temp file, then only\n // chmod when the source file used a non-default mode.\n if (defaultFileMode === undefined) {\n try {\n defaultFileMode = (await stat(tempPath)).mode & 0o777;\n } catch {}\n }\n if (modeToRestore !== defaultFileMode) {\n await chmod(tempPath, modeToRestore);\n }\n }\n\n await rename(tempPath, path);\n } catch (error) {\n try {\n await rm(tempPath, { force: true });\n } catch {}\n throw error;\n } finally {\n activeTempFiles.delete(tempPath);\n }\n\n return true;\n};\n"],"mappings":";;;;;;AAaA,MAAM,kCAAkB,IAAI,KAAa;AAGzC,QAAQ,GAAG,cAAc;AACvB,MAAK,MAAM,QAAQ,gBACjB,KAAI;AACF,SAAO,MAAM,EAAE,OAAO,MAAM,CAAC;SACvB;EAEV;;;;;;;;AASF,IAAI;;;;;;;;;;;;AA+BJ,MAAa,qBAAqB,OAChC,MACA,MACA,EAAE,WAAW,QAAQ,SAAS,SAAS,SAAoC,EAAE,KACxD;CACrB,MAAM,UAAU,OAAO,KAAK,MAAM,SAAS;CAI3C,IAAI,eAA8B;AAClC,KAAI;AACF,iBAAe,MAAM,SAAS,KAAK;SAC7B;AAER,KAAI,cAAc,OAAO,QAAQ,CAC/B,QAAO;AAOT,KAAI,CAAC,QAAQ;AACX,QAAM,UAAU,MAAM,QAAQ;AAC9B,SAAO;;CAKT,IAAI;AACJ,KAAI,iBAAiB,KACnB,KAAI;AACF,mBAAiB,MAAM,KAAK,KAAK,EAAE,OAAO;SACpC;AAGV,KAAI,QACF,OAAM,MAAM,SAAS,EAAE,WAAW,MAAM,CAAC;CAG3C,MAAM,eAAe,GAAG,SAAS,KAAK,CAAC,GAAG,KAAK,KAAK,CAAC,GAAG,YAAY,EAAE,CAAC,SAAS,MAAM,CAAC;CACvF,MAAM,WAAW,UACb,KAAK,SAAS,aAAa,GAC3B,GAAG,KAAK,GAAG;AACf,iBAAgB,IAAI,SAAS;AAE7B,KAAI;AACF,QAAM,UAAU,UAAU,QAAQ;AAElC,MAAI,kBAAkB,QAAW;AAG/B,OAAI,oBAAoB,OACtB,KAAI;AACF,uBAAmB,MAAM,KAAK,SAAS,EAAE,OAAO;WAC1C;AAEV,OAAI,kBAAkB,gBACpB,OAAM,MAAM,UAAU,cAAc;;AAIxC,QAAM,OAAO,UAAU,KAAK;UACrB,OAAO;AACd,MAAI;AACF,SAAM,GAAG,UAAU,EAAE,OAAO,MAAM,CAAC;UAC7B;AACR,QAAM;WACE;AACR,kBAAgB,OAAO,SAAS;;AAGlC,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"createType.d.ts","names":[],"sources":["../../../src/createType/createType.ts"],"mappings":";;;;cASa,sBAAA,GACX,UAAA,EAAY,UAAA,GAAa,wBAAA;;AAD3B;;cAUa,WAAA,GACX,YAAA,GAAe,UAAA,GAAa,wBAAA,KAC5B,aAAA,EAAe,cAAA,KACd,OAAA"}
1
+ {"version":3,"file":"createType.d.ts","names":[],"sources":["../../../src/createType/createType.ts"],"mappings":";;;;cAUa,sBAAA,GACX,UAAA,EAAY,UAAA,GAAa,wBAAA;;AAD3B;;cAUa,WAAA,GACX,YAAA,GAAe,UAAA,GAAa,wBAAA,KAC5B,aAAA,EAAe,cAAA,KACd,OAAA"}
@@ -0,0 +1,51 @@
1
+ import { ContentAdapter, FormatCodec, SyncContent, SyncPluginContext } from "./types.js";
2
+ import { FilePathPattern } from "@intlayer/types/filePathPattern";
3
+
4
+ //#region src/syncPluginKit/createFileAdapter.d.ts
5
+ /**
6
+ * Discovery strategy of the file adapter:
7
+ * - `strict`: keep only files whose path rebuilds identically from the source
8
+ * pattern, and fabricate entries for missing locales/keys so every declared
9
+ * locale has a write-back target (sync plugins).
10
+ * - `inclusive`: keep every glob match without fabricating missing entries
11
+ * (read-only load plugins, whose patterns may contain free `**` globs).
12
+ */
13
+ type FileAdapterDiscovery = 'strict' | 'inclusive';
14
+ type CreateFileAdapterOptions = {
15
+ /**
16
+ * Location of the files, as a static string, a templated string
17
+ * (e.g. `./messages/{{locale}}/{{key}}.json`) or a function building the
18
+ * path from the key and locale.
19
+ */
20
+ source: FilePathPattern;
21
+ /**
22
+ * Format of the files: string payload ↔ structured content.
23
+ */
24
+ codec: FormatCodec;
25
+ /**
26
+ * Optional override of how one file is read and parsed. Defaults to a plain
27
+ * utf-8 read followed by `codec.parse`, returning `undefined` for missing or
28
+ * unreadable files. Adapters needing richer loading (e.g. JSON5 or
29
+ * transpiled TS sources) provide their own implementation here.
30
+ */
31
+ readEntry?: (absoluteFilePath: string, context: SyncPluginContext) => Promise<SyncContent | undefined>;
32
+ /**
33
+ * Discovery strategy. Defaults to `'strict'`.
34
+ */
35
+ discovery?: FileAdapterDiscovery;
36
+ };
37
+ /**
38
+ * Create a filesystem {@link ContentAdapter} for `createSyncPlugin`.
39
+ *
40
+ * Handles glob discovery of the source pattern, key/locale extraction from
41
+ * paths, base-directory resolution, and change-detected atomic writes.
42
+ */
43
+ declare const createFileAdapter: (options: CreateFileAdapterOptions) => ContentAdapter;
44
+ /**
45
+ * Default `readEntry` implementation: plain utf-8 read followed by the given
46
+ * parser. Returns `undefined` for missing or unreadable files.
47
+ */
48
+ declare const createFileReader: (parse: (raw: string) => SyncContent) => (absoluteFilePath: string) => Promise<SyncContent | undefined>;
49
+ //#endregion
50
+ export { CreateFileAdapterOptions, FileAdapterDiscovery, createFileAdapter, createFileReader };
51
+ //# sourceMappingURL=createFileAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createFileAdapter.d.ts","names":[],"sources":["../../../src/syncPluginKit/createFileAdapter.ts"],"mappings":";;;;;;AAyBA;;;;;AAEA;KAFY,oBAAA;AAAA,KAEA,wBAAA;EAMF;;;;;EAAR,MAAA,EAAQ,eAAA;EAqBwB;;;EAhBhC,KAAA,EAAO,WAAA;EAAP;;;;;;EAQA,SAAA,IACE,gBAAA,UACA,OAAA,EAAS,iBAAA,KACN,OAAA,CAAQ,WAAA;EAAA;;;EAKb,SAAA,GAAY,oBAAA;AAAA;AAYd;;;;;;AAAA,cAAa,iBAAA,GACX,OAAA,EAAS,wBAAA,KACR,cAAA;;;AAwNH;;cAAa,gBAAA,GACV,KAAA,GAAQ,GAAA,aAAgB,WAAA,MAClB,gBAAA,aAA2B,OAAA,CAAQ,WAAA"}
@@ -0,0 +1,18 @@
1
+ import { CreateSyncPluginOptions } from "./types.js";
2
+ import { Plugin } from "@intlayer/types/plugin";
3
+
4
+ //#region src/syncPluginKit/createSyncPlugin.d.ts
5
+ /**
6
+ * Build an Intlayer {@link Plugin} from a transport adapter.
7
+ *
8
+ * The factory implements the three plugin hooks once — ingestion
9
+ * (`loadDictionaries`), content-declaration reformatting (`formatOutput`) and
10
+ * write-back (`afterBuild`) — so adapters only describe *where* content lives
11
+ * (filesystem, TMS such as Crowdin, extra CMS…) and codecs only describe the
12
+ * payload format. Write-back only processes dictionaries whose `location`
13
+ * matches this plugin instance, so adapters never see foreign content.
14
+ */
15
+ declare const createSyncPlugin: (options: CreateSyncPluginOptions) => Plugin;
16
+ //#endregion
17
+ export { createSyncPlugin };
18
+ //# sourceMappingURL=createSyncPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createSyncPlugin.d.ts","names":[],"sources":["../../../src/syncPluginKit/createSyncPlugin.ts"],"mappings":";;;;;;AAuCA;;;;;;;;cAAa,gBAAA,GAAoB,OAAA,EAAS,uBAAA,KAA0B,MAAA"}
@@ -0,0 +1,20 @@
1
+ import { Locale } from "@intlayer/types/allLocales";
2
+
3
+ //#region src/syncPluginKit/extractKeyAndLocaleFromPath.d.ts
4
+ /**
5
+ * Extract the dictionary key and locale of a file path by matching it against
6
+ * a mask pattern where `{{__KEY__}}` and `{{__LOCALE__}}` mark the positions
7
+ * of the key and locale segments.
8
+ *
9
+ * Returns `null` when the path does not match the mask, or when the mask
10
+ * contains no placeholder at all (no named capture group can be produced).
11
+ * Missing placeholders fall back to `'index'` for the key and to
12
+ * `defaultLocale` for the locale.
13
+ */
14
+ declare const extractKeyAndLocaleFromPath: (filePath: string, maskPattern: string, locales: Locale[], defaultLocale: Locale) => {
15
+ key: string;
16
+ locale: Locale;
17
+ } | null;
18
+ //#endregion
19
+ export { extractKeyAndLocaleFromPath };
20
+ //# sourceMappingURL=extractKeyAndLocaleFromPath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractKeyAndLocaleFromPath.d.ts","names":[],"sources":["../../../src/syncPluginKit/extractKeyAndLocaleFromPath.ts"],"mappings":";;;;;AAeA;;;;;;;;cAAa,2BAAA,GACX,QAAA,UACA,WAAA,UACA,OAAA,EAAS,MAAA,IACT,aAAA,EAAe,MAAA;EACZ,GAAA;EAAa,MAAA,EAAQ,MAAA;AAAA"}
@@ -0,0 +1,16 @@
1
+ import { FilePathPatternContext } from "@intlayer/types/filePathPattern";
2
+
3
+ //#region src/syncPluginKit/filePathPatternHelpers.d.ts
4
+ /**
5
+ * Build the context handed to `parseFilePathPattern` from the only two
6
+ * variables sync plugins resolve: the dictionary key and the locale.
7
+ *
8
+ * `FilePathPatternContext` declares extra fields (`fileName`, `extension`,
9
+ * `format`…) that only apply to content-declaration paths; sync-plugin
10
+ * patterns never reference them, so this single cast keeps the partial
11
+ * context in one audited place instead of scattering `as any` at call sites.
12
+ */
13
+ declare const buildFilePathPatternContext: (key: string, locale?: string) => FilePathPatternContext;
14
+ //#endregion
15
+ export { buildFilePathPatternContext };
16
+ //# sourceMappingURL=filePathPatternHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filePathPatternHelpers.d.ts","names":[],"sources":["../../../src/syncPluginKit/filePathPatternHelpers.ts"],"mappings":";;;;;AAWA;;;;;;;cAAa,2BAAA,GACX,GAAA,UACA,MAAA,cACC,sBAAA"}
@@ -0,0 +1,6 @@
1
+ import { ContentAdapter, ContentEntry, ContentTarget, CreateSyncPluginOptions, FormatCodec, SyncContent, SyncDirection, SyncPluginContext } from "./types.js";
2
+ import { CreateFileAdapterOptions, FileAdapterDiscovery, createFileAdapter, createFileReader } from "./createFileAdapter.js";
3
+ import { createSyncPlugin } from "./createSyncPlugin.js";
4
+ import { extractKeyAndLocaleFromPath } from "./extractKeyAndLocaleFromPath.js";
5
+ import { buildFilePathPatternContext } from "./filePathPatternHelpers.js";
6
+ export { type ContentAdapter, type ContentEntry, type ContentTarget, type CreateFileAdapterOptions, type CreateSyncPluginOptions, type FileAdapterDiscovery, type FormatCodec, type SyncContent, type SyncDirection, type SyncPluginContext, buildFilePathPatternContext, createFileAdapter, createFileReader, createSyncPlugin, extractKeyAndLocaleFromPath };
@@ -0,0 +1,149 @@
1
+ import { IntlayerConfig } from "@intlayer/types/config";
2
+ import { DictionaryFormat } from "@intlayer/types/dictionary";
3
+ import { Locale } from "@intlayer/types/allLocales";
4
+
5
+ //#region src/syncPluginKit/types.d.ts
6
+ /**
7
+ * Structured dictionary content handled by sync plugins.
8
+ */
9
+ type SyncContent = Record<string, unknown>;
10
+ /**
11
+ * Context handed to every {@link ContentAdapter} call.
12
+ */
13
+ type SyncPluginContext = {
14
+ configuration: IntlayerConfig;
15
+ };
16
+ /**
17
+ * One translatable resource discovered in the external system.
18
+ */
19
+ type ContentEntry = {
20
+ /** Dictionary key the entry maps to. */key: string; /** Locale of the entry content. */
21
+ locale: Locale;
22
+ /**
23
+ * Transport-specific identifier of the entry: an absolute file path for the
24
+ * file adapter, a remote resource identifier for API-backed adapters.
25
+ */
26
+ uri: string;
27
+ /**
28
+ * Absolute file path when the entry lives on the local filesystem.
29
+ * Leave undefined for remote entries (CMS, TMS…): the generated dictionary
30
+ * then carries no `filePath` and file-based hooks are skipped for it.
31
+ */
32
+ filePath?: string;
33
+ };
34
+ /**
35
+ * Identifies where content must be written back by a {@link ContentAdapter}.
36
+ */
37
+ type ContentTarget = {
38
+ key: string;
39
+ locale: Locale;
40
+ };
41
+ /**
42
+ * Transport layer of a sync plugin: where the entries live and how to
43
+ * enumerate, read and persist them (filesystem, TMS such as Crowdin, extra
44
+ * CMS…). Format concerns belong to {@link FormatCodec}, orchestration to
45
+ * `createSyncPlugin`.
46
+ */
47
+ type ContentAdapter = {
48
+ /**
49
+ * Enumerate every entry available in the external system.
50
+ */
51
+ list: (context: SyncPluginContext) => Promise<ContentEntry[]>;
52
+ /**
53
+ * Read and parse one entry.
54
+ * Return `undefined` when the entry is missing or unreadable — the plugin
55
+ * treats it as empty content instead of failing the build.
56
+ */
57
+ read: (entry: ContentEntry, context: SyncPluginContext) => Promise<SyncContent | undefined>;
58
+ /**
59
+ * Serialize and persist the content of one key/locale back to the external
60
+ * system. Only called for plugins created with `direction: 'both'`.
61
+ */
62
+ write: (target: ContentTarget, content: SyncContent, context: SyncPluginContext) => Promise<void>;
63
+ /**
64
+ * Resolve the canonical URI of a key/locale.
65
+ * Used by the plugin to verify ownership before reformatting a content
66
+ * declaration in the `formatOutput` hook. Optional: adapters without a
67
+ * local-file identity (remote systems) can omit it.
68
+ */
69
+ resolveUri?: (target: ContentTarget, context: SyncPluginContext) => Promise<string>;
70
+ /**
71
+ * Template describing where entries live, with `{{key}}` / `{{locale}}`
72
+ * markers kept verbatim. Used as the `fill` field of generated dictionaries
73
+ * so Intlayer knows where translations flow back.
74
+ */
75
+ getFillPattern?: (context: SyncPluginContext) => Promise<string>;
76
+ /**
77
+ * Human-readable description of the adapter source, used in log messages
78
+ * (e.g. the file path pattern for the file adapter).
79
+ */
80
+ describeSource?: (context: SyncPluginContext) => Promise<string>;
81
+ /**
82
+ * Default value for the `splitKeys` behavior when the plugin options do not
83
+ * set it explicitly (e.g. the file adapter splits when the source pattern
84
+ * has no `{{key}}` segment).
85
+ */
86
+ detectSplitKeys?: () => Promise<boolean>;
87
+ };
88
+ /**
89
+ * Format layer of a sync plugin: how a raw string payload maps to structured
90
+ * dictionary content and back (JSON, PO, XLIFF…).
91
+ */
92
+ type FormatCodec = {
93
+ /**
94
+ * Parse a raw payload into dictionary content.
95
+ * May throw on malformed payloads — callers treat a throw as empty content.
96
+ */
97
+ parse: (raw: string) => SyncContent;
98
+ /**
99
+ * Serialize dictionary content into the target format.
100
+ */
101
+ serialize: (content: SyncContent, context: {
102
+ locale: Locale;
103
+ }) => string;
104
+ };
105
+ /**
106
+ * Direction of the synchronization:
107
+ * - `pull`: only ingest external content as dictionaries (read-only).
108
+ * - `both`: ingest and write the merged build output back to the source.
109
+ */
110
+ type SyncDirection = 'pull' | 'both';
111
+ /**
112
+ * Options of `createSyncPlugin`.
113
+ */
114
+ type CreateSyncPluginOptions = {
115
+ /** Plugin name reported to the Intlayer engine (e.g. `sync-json`). */name: string; /** Transport used to enumerate, read and write entries. */
116
+ adapter: ContentAdapter;
117
+ /**
118
+ * Synchronization direction. Defaults to `'both'`.
119
+ * With `'pull'` the plugin only implements `loadDictionaries`.
120
+ */
121
+ direction?: SyncDirection;
122
+ /**
123
+ * Identifier of the plugin instance stored on each generated dictionary.
124
+ * Dictionaries are routed back to their owning plugin instance through this
125
+ * value during write-back, so it must be unique per instance.
126
+ */
127
+ location: string;
128
+ /**
129
+ * Priority of the generated dictionaries when merging with other sources
130
+ * (`.content` files have priority 0). Defaults to 0.
131
+ */
132
+ priority?: number; /** Content format of the dictionaries (e.g. `'icu'`, `'po'`). */
133
+ format?: DictionaryFormat;
134
+ /**
135
+ * Whether each top-level key of an entry becomes its own dictionary instead
136
+ * of a single dictionary holding the whole entry (the `next-intl` /
137
+ * `react-intl` namespace model). When omitted, falls back to
138
+ * {@link ContentAdapter.detectSplitKeys}, then to `false`.
139
+ */
140
+ splitKeys?: boolean;
141
+ /**
142
+ * Fixed locale applied to every generated dictionary, overriding the locale
143
+ * discovered per entry.
144
+ */
145
+ localeOverride?: Locale;
146
+ };
147
+ //#endregion
148
+ export { ContentAdapter, ContentEntry, ContentTarget, CreateSyncPluginOptions, FormatCodec, SyncContent, SyncDirection, SyncPluginContext };
149
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/syncPluginKit/types.ts"],"mappings":";;;;;;;AAOA;KAAY,WAAA,GAAc,MAAA;;;;KAKd,iBAAA;EACV,aAAA,EAAe,cAAA;AAAA;;;AAMjB;KAAY,YAAA;0CAEV,GAAA;EAEA,MAAA,EAAQ,MAAA;EAAA;;;;EAKR,GAAA;EAYU;;;;;EANV,QAAA;AAAA;;;AAiBF;KAXY,aAAA;EACV,GAAA;EACA,MAAA,EAAQ,MAAA;AAAA;;;;;;;KASE,cAAA;EAuBC;;;EAnBX,IAAA,GAAO,OAAA,EAAS,iBAAA,KAAsB,OAAA,CAAQ,YAAA;EA+BzC;;;;;EAxBL,IAAA,GACE,KAAA,EAAO,YAAA,EACP,OAAA,EAAS,iBAAA,KACN,OAAA,CAAQ,WAAA;EAyCkB;;;;EAnC/B,KAAA,GACE,MAAA,EAAQ,aAAA,EACR,OAAA,EAAS,WAAA,EACT,OAAA,EAAS,iBAAA,KACN,OAAA;EApBiC;;;;;;EA4BtC,UAAA,IACE,MAAA,EAAQ,aAAA,EACR,OAAA,EAAS,iBAAA,KACN,OAAA;EArBA;;;;;EA4BL,cAAA,IAAkB,OAAA,EAAS,iBAAA,KAAsB,OAAA;EApB/C;;;;EA0BF,cAAA,IAAkB,OAAA,EAAS,iBAAA,KAAsB,OAAA;EAfvC;;;;;EAsBV,eAAA,SAAwB,OAAA;AAAA;;;;;KAOd,WAAA;EAduC;;;;EAmBjD,KAAA,GAAQ,GAAA,aAAgB,WAAA;EALd;;;EAUV,SAAA,GAAY,OAAA,EAAS,WAAA,EAAa,OAAA;IAAW,MAAA,EAAQ,MAAA;EAAA;AAAA;;;;;;KAQ3C,aAAA;;;;KAKA,uBAAA;EAbmD,sEAe7D,IAAA,UAPU;EAUV,OAAA,EAAS,cAAA;;;;AALX;EAWE,SAAA,GAAY,aAAA;;;;;;EAOZ,QAAA;EAuBuB;;;;EAjBvB,QAAA,WAbY;EAgBZ,MAAA,GAAS,gBAAA;EAHT;;;;;;EAWA,SAAA;EAMuB;;;;EAAvB,cAAA,GAAiB,MAAA;AAAA"}
@@ -1,6 +1,11 @@
1
1
  //#region src/utils/runParallel/index.d.ts
2
2
  type ParallelHandle = {
3
- kill: () => void;
3
+ /**
4
+ * Terminate the child and its whole process group.
5
+ * @param signal Signal to send (default `SIGTERM`). Pass `SIGKILL` to force-kill
6
+ * a child that ignores graceful termination.
7
+ */
8
+ kill: (signal?: NodeJS.Signals) => void;
4
9
  result: Promise<any>;
5
10
  commandText: string;
6
11
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../../src/utils/runParallel/index.ts"],"mappings":";KAIY,cAAA;EACV,IAAA;EACA,MAAA,EAAQ,OAAA;EACR,WAAA;AAAA;;;;;cAOW,WAAA,GAAe,IAAA,yBAA2B,cAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../../src/utils/runParallel/index.ts"],"mappings":";KAIY,cAAA;EAAA;;;;;EAMV,IAAA,GAAO,MAAA,GAAS,MAAA,CAAO,OAAA;EACvB,MAAA,EAAQ,OAAA;EACR,WAAA;AAAA;;;;;cAOW,WAAA,GAAe,IAAA,yBAA2B,cAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"watcher.d.ts","names":[],"sources":["../../src/watcher.ts"],"mappings":";;;;KAuDK,YAAA;EACH,aAAA,GAAgB,cAAA;EAChB,aAAA,GAAgB,uBAAA;EAChB,WAAA;EACA,UAAA;AAAA;AAAA,cAsBW,KAAA,GAAe,OAAA,GAAU,YAAA,KAAY,OAAA;qBAkDN,OAAA;AAAA;AAAA,cAyP/B,qBAAA,GAA+B,OAAA,GAAU,YAAA,KAAY,OAAA"}
1
+ {"version":3,"file":"watcher.d.ts","names":[],"sources":["../../src/watcher.ts"],"mappings":";;;;KAuDK,YAAA;EACH,aAAA,GAAgB,cAAA;EAChB,aAAA,GAAgB,uBAAA;EAChB,WAAA;EACA,UAAA;AAAA;AAAA,cAsBW,KAAA,GAAe,OAAA,GAAU,YAAA,KAAY,OAAA;qBAkDN,OAAA;AAAA;AAAA,cA8P/B,qBAAA,GAA+B,OAAA,GAAU,YAAA,KAAY,OAAA"}
@@ -1,11 +1,35 @@
1
1
  //#region src/writeFileIfChanged.d.ts
2
+ /**
3
+ * Options for {@link writeFileIfChanged}.
4
+ */
5
+ type WriteFileIfChangedOptions = {
6
+ /** Encoding used to turn `data` into bytes. Defaults to `'utf8'`. */encoding?: BufferEncoding; /** Directory to hold the temporary file used for the atomic swap. */
7
+ tempDir?: string;
8
+ /**
9
+ * Write atomically via a temp file + `rename` so readers never observe a
10
+ * half-written file. This creates a fresh inode on every write, which roughly
11
+ * doubles the cost of a changed write compared to overwriting in place. Set to
12
+ * `false` for callers whose consumers tolerate torn reads to write directly.
13
+ * Defaults to `true`.
14
+ */
15
+ atomic?: boolean;
16
+ };
17
+ /**
18
+ * Write `data` to `path` only when it differs from the file already on disk,
19
+ * preserving the existing file's permission mode.
20
+ *
21
+ * The whole file is read back and byte-compared because the files written here
22
+ * (dictionaries, entry points, types) are small, so reading beats streaming a
23
+ * hash. When the content is unchanged the write is skipped entirely, which both
24
+ * avoids inode churn and prevents downstream watchers from rebuilding.
25
+ *
26
+ * @returns `true` when the file was written, `false` when it was already up to date.
27
+ */
2
28
  declare const writeFileIfChanged: (path: string, data: string, {
3
29
  encoding,
4
- tempDir
5
- }?: {
6
- encoding?: BufferEncoding;
7
- tempDir?: string;
8
- }) => Promise<boolean>;
30
+ tempDir,
31
+ atomic
32
+ }?: WriteFileIfChangedOptions) => Promise<boolean>;
9
33
  //#endregion
10
- export { writeFileIfChanged };
34
+ export { WriteFileIfChangedOptions, writeFileIfChanged };
11
35
  //# sourceMappingURL=writeFileIfChanged.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"writeFileIfChanged.d.ts","names":[],"sources":["../../src/writeFileIfChanged.ts"],"mappings":";cA2Ba,kBAAA,GACX,IAAA,UACA,IAAA;EACA,QAAA;EAAA;AAAA;EAGK,QAAA,GAAW,cAAA;EAAgB,OAAA;AAAA,MAC/B,OAAA"}
1
+ {"version":3,"file":"writeFileIfChanged.d.ts","names":[],"sources":["../../src/writeFileIfChanged.ts"],"mappings":";;AAoCA;;KAAY,yBAAA;EAEe,qEAAzB,QAAA,GAAW,cAAA,EAAA;EAEX,OAAA;EAQA;;;AAcF;;;;EAdE,MAAA;AAAA;;;;;;;;;;;;cAcW,kBAAA,GACX,IAAA,UACA,IAAA;EACA,QAAA;EAAA,OAAA;EAAA;AAAA,IAA+C,yBAAA,KAC9C,OAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intlayer/engine",
3
- "version": "9.0.0-canary.13",
3
+ "version": "9.0.0-canary.14",
4
4
  "private": false,
5
5
  "description": "Scans and builds Intlayer declaration files into dictionaries based on Intlayer configuration.",
6
6
  "keywords": [
@@ -50,11 +50,21 @@
50
50
  "require": "./dist/cjs/cli.cjs",
51
51
  "import": "./dist/esm/cli.mjs"
52
52
  },
53
+ "./logConfigDetails": {
54
+ "types": "./dist/types/logConfigDetails.d.ts",
55
+ "require": "./dist/cjs/logConfigDetails.cjs",
56
+ "import": "./dist/esm/logConfigDetails.mjs"
57
+ },
53
58
  "./utils": {
54
59
  "types": "./dist/types/utils/index.d.ts",
55
60
  "require": "./dist/cjs/utils/index.cjs",
56
61
  "import": "./dist/esm/utils/index.mjs"
57
62
  },
63
+ "./syncPluginKit": {
64
+ "types": "./dist/types/syncPluginKit/index.d.ts",
65
+ "require": "./dist/cjs/syncPluginKit/index.cjs",
66
+ "import": "./dist/esm/syncPluginKit/index.mjs"
67
+ },
58
68
  "./listDictionaries": {
59
69
  "types": "./dist/types/listDictionariesPath.d.ts",
60
70
  "require": "./dist/cjs/listDictionariesPath.cjs",
@@ -86,9 +96,15 @@
86
96
  "cli": [
87
97
  "./dist/types/cli.d.ts"
88
98
  ],
99
+ "logConfigDetails": [
100
+ "./dist/types/logConfigDetails.d.ts"
101
+ ],
89
102
  "utils": [
90
103
  "./dist/types/utils/index.d.ts"
91
104
  ],
105
+ "syncPluginKit": [
106
+ "./dist/types/syncPluginKit/index.d.ts"
107
+ ],
92
108
  "listDictionaries": [
93
109
  "./dist/types/listDictionariesPath.d.ts"
94
110
  ],
@@ -126,13 +142,13 @@
126
142
  },
127
143
  "dependencies": {
128
144
  "@babel/parser": "7.29.7",
129
- "@intlayer/api": "9.0.0-canary.13",
130
- "@intlayer/config": "9.0.0-canary.13",
131
- "@intlayer/core": "9.0.0-canary.13",
132
- "@intlayer/dictionaries-entry": "9.0.0-canary.13",
133
- "@intlayer/remote-dictionaries-entry": "9.0.0-canary.13",
134
- "@intlayer/types": "9.0.0-canary.13",
135
- "@intlayer/unmerged-dictionaries-entry": "9.0.0-canary.13",
145
+ "@intlayer/api": "9.0.0-canary.14",
146
+ "@intlayer/config": "9.0.0-canary.14",
147
+ "@intlayer/core": "9.0.0-canary.14",
148
+ "@intlayer/dictionaries-entry": "9.0.0-canary.14",
149
+ "@intlayer/remote-dictionaries-entry": "9.0.0-canary.14",
150
+ "@intlayer/types": "9.0.0-canary.14",
151
+ "@intlayer/unmerged-dictionaries-entry": "9.0.0-canary.14",
136
152
  "@parcel/watcher": "2.5.6",
137
153
  "defu": "6.1.7",
138
154
  "fast-glob": "3.3.3",