@inlang/paraglide-js 2.24.1 → 2.25.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.
Files changed (85) hide show
  1. package/dist/bundler-plugins/esbuild.d.ts +1 -1
  2. package/dist/bundler-plugins/per-locale-build/index.d.ts +3 -3
  3. package/dist/bundler-plugins/per-locale-build/index.js +59 -15
  4. package/dist/bundler-plugins/per-locale-build/index.js.map +1 -1
  5. package/dist/bundler-plugins/rolldown.d.ts +1 -1
  6. package/dist/bundler-plugins/rollup.d.ts +1 -1
  7. package/dist/bundler-plugins/rspack.d.ts +1 -1
  8. package/dist/bundler-plugins/unplugin.d.ts +35 -1
  9. package/dist/bundler-plugins/unplugin.js +404 -123
  10. package/dist/bundler-plugins/unplugin.js.map +1 -1
  11. package/dist/bundler-plugins/unplugin.test.js +357 -202
  12. package/dist/bundler-plugins/unplugin.test.js.map +1 -1
  13. package/dist/bundler-plugins/vite.d.ts +2 -2
  14. package/dist/bundler-plugins/vite.js.map +1 -1
  15. package/dist/bundler-plugins/vite.test.js +2 -1
  16. package/dist/bundler-plugins/vite.test.js.map +1 -1
  17. package/dist/bundler-plugins/webpack.d.ts +1 -1
  18. package/dist/cli/commands/compile/command.js +156 -36
  19. package/dist/cli/commands/compile/command.js.map +1 -1
  20. package/dist/cli/commands/compile/command.test.js +311 -7
  21. package/dist/cli/commands/compile/command.test.js.map +1 -1
  22. package/dist/cli/defaults.d.ts +0 -2
  23. package/dist/cli/defaults.js +0 -2
  24. package/dist/cli/defaults.js.map +1 -1
  25. package/dist/cli/steps/initialize-inlang-project.js +2 -1
  26. package/dist/cli/steps/initialize-inlang-project.js.map +1 -1
  27. package/dist/cli/steps/prompt-for-outdir.js +1 -1
  28. package/dist/cli/steps/prompt-for-outdir.js.map +1 -1
  29. package/dist/compiler/compile-annotation.d.ts +2 -2
  30. package/dist/compiler/compile-annotation.js +9 -9
  31. package/dist/compiler/compile-annotation.js.map +1 -1
  32. package/dist/compiler/compile-local-variable.d.ts +2 -1
  33. package/dist/compiler/compile-local-variable.js +4 -4
  34. package/dist/compiler/compile-local-variable.js.map +1 -1
  35. package/dist/compiler/compile-local-variable.test.js +82 -0
  36. package/dist/compiler/compile-local-variable.test.js.map +1 -1
  37. package/dist/compiler/compile-message.js +10 -2
  38. package/dist/compiler/compile-message.js.map +1 -1
  39. package/dist/compiler/compile-message.test.js +132 -2
  40. package/dist/compiler/compile-message.test.js.map +1 -1
  41. package/dist/compiler/compile-pattern.js +4 -2
  42. package/dist/compiler/compile-pattern.js.map +1 -1
  43. package/dist/compiler/compile-pattern.test.js +31 -0
  44. package/dist/compiler/compile-pattern.test.js.map +1 -1
  45. package/dist/compiler/compiler-options.d.ts +1 -1
  46. package/dist/compiler/compiler-options.js.map +1 -1
  47. package/dist/compiler/server/create-server-file.js +18 -4
  48. package/dist/compiler/server/create-server-file.js.map +1 -1
  49. package/dist/compiler/server/create-server-file.test.js +76 -0
  50. package/dist/compiler/server/create-server-file.test.js.map +1 -1
  51. package/dist/compiler/variable-access.d.ts +2 -0
  52. package/dist/compiler/variable-access.js +6 -0
  53. package/dist/compiler/variable-access.js.map +1 -1
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.js +1 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/services/config/config-schema.d.ts +69 -0
  58. package/dist/services/config/config-schema.js +78 -0
  59. package/dist/services/config/config-schema.js.map +1 -0
  60. package/dist/services/config/defaults.d.ts +11 -0
  61. package/dist/services/config/defaults.js +12 -0
  62. package/dist/services/config/defaults.js.map +1 -0
  63. package/dist/services/config/discover-config-file.d.ts +19 -0
  64. package/dist/services/config/discover-config-file.js +39 -0
  65. package/dist/services/config/discover-config-file.js.map +1 -0
  66. package/dist/services/config/index.d.ts +24 -0
  67. package/dist/services/config/index.js +25 -0
  68. package/dist/services/config/index.js.map +1 -0
  69. package/dist/services/config/load-paraglide-config.d.ts +44 -0
  70. package/dist/services/config/load-paraglide-config.js +109 -0
  71. package/dist/services/config/load-paraglide-config.js.map +1 -0
  72. package/dist/services/config/load-paraglide-config.test.d.ts +1 -0
  73. package/dist/services/config/load-paraglide-config.test.js +201 -0
  74. package/dist/services/config/load-paraglide-config.test.js.map +1 -0
  75. package/dist/services/config/resolve-compiler-options.d.ts +24 -0
  76. package/dist/services/config/resolve-compiler-options.js +51 -0
  77. package/dist/services/config/resolve-compiler-options.js.map +1 -0
  78. package/dist/services/config/resolve-compiler-options.test.d.ts +1 -0
  79. package/dist/services/config/resolve-compiler-options.test.js +60 -0
  80. package/dist/services/config/resolve-compiler-options.test.js.map +1 -0
  81. package/dist/services/env-variables/index.js +1 -1
  82. package/dist/services/env-variables/index.js.map +1 -1
  83. package/dist/services/logger/index.js +2 -0
  84. package/dist/services/logger/index.js.map +1 -1
  85. package/package.json +4 -2
@@ -1,5 +1,5 @@
1
1
  import { compile } from "../compiler/compile.js";
2
- import { dirname, relative, resolve } from "node:path";
2
+ import { dirname, join, relative, resolve } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
4
  import nodeFs from "node:fs";
5
5
  import { Logger } from "../services/logger/index.js";
@@ -7,6 +7,7 @@ import { ENV_VARIABLES } from "../services/env-variables/index.js";
7
7
  import { createTrackedFs, getWatchTargets, isPathWithinDirectories, } from "../services/file-watching/tracked-fs.js";
8
8
  import { nodeNormalizePath } from "../utilities/node-normalize-path.js";
9
9
  import { seedPreviousCompilationFromOutdir } from "../compiler/seed-previous-compilation.js";
10
+ import { CONFIG_FILE_NAMES, clearParaglideConfigCache, isConfigFileName, loadParaglideConfig, resolveCompilerOptions, resolveConfigCandidate, } from "../services/config/index.js";
10
11
  const PLUGIN_NAME = "unplugin-paraglide-js";
11
12
  const logger = new Logger();
12
13
  const PERSISTENT_CACHE_VERSION = 1;
@@ -35,6 +36,8 @@ function getPluginState(args) {
35
36
  clearReadFiles: tracked.clearReadFiles,
36
37
  previousCompilation: undefined,
37
38
  previousInputsDigest: undefined,
39
+ previousProject: undefined,
40
+ previousOutdir: undefined,
38
41
  };
39
42
  }
40
43
  return pluginState;
@@ -222,130 +225,310 @@ function rethrowUnlessEnoent(error) {
222
225
  }
223
226
  throw error;
224
227
  }
225
- export const unpluginFactory = (args) => {
226
- const state = getPluginState(args);
227
- const { trackedFs, readFiles, clearReadFiles } = state;
228
+ // default to locale-modules for development to speed up the dev server
229
+ // https://github.com/opral/inlang-paraglide-js/issues/486
230
+ function resolveOutputStructure(options) {
231
+ const isProduction = process.env.NODE_ENV === "production";
232
+ return (options.outputStructure ??
233
+ (isProduction ? "message-modules" : "locale-modules"));
234
+ }
235
+ function hasUnchangedInputs(args) {
236
+ if (args.state.previousCompilation === undefined) {
237
+ return Promise.resolve(false);
238
+ }
239
+ if (args.state.previousInputsDigest === undefined) {
240
+ return Promise.resolve(false);
241
+ }
242
+ return computeInputsDigest(args.state, args.compilerOptions, args.outputStructure).then((currentDigest) => currentDigest === args.state.previousInputsDigest);
243
+ }
244
+ function maybeInvalidateForOptionsChange(state, compilerOptions) {
245
+ if (state.previousProject !== undefined &&
246
+ (state.previousProject !== compilerOptions.project ||
247
+ state.previousOutdir !== compilerOptions.outdir)) {
248
+ state.previousCompilation = undefined;
249
+ state.previousInputsDigest = undefined;
250
+ state.clearReadFiles();
251
+ }
252
+ state.previousProject = compilerOptions.project;
253
+ state.previousOutdir = compilerOptions.outdir;
254
+ }
255
+ // Compiles and stores the result plus its inputs digest on the plugin
256
+ // state. Callers decide whether to persist the compilation cache.
257
+ async function compileAndUpdateState(args) {
258
+ args.state.previousCompilation = await compile({
259
+ previousCompilation: args.previousCompilation,
260
+ outputStructure: args.outputStructure,
261
+ isServer: args.isServer,
262
+ ...withoutCleanOutdir(args.compilerOptions),
263
+ cleanOutdir: false,
264
+ // after the options spread so a user-provided fs doesn't bypass
265
+ // the read tracking (trackedFs wraps options.fs when provided)
266
+ fs: args.state.trackedFs,
267
+ });
268
+ args.state.previousInputsDigest = await computeInputsDigest(args.state, args.compilerOptions, args.outputStructure);
269
+ }
270
+ /**
271
+ * Runs the compilation with the given, already-resolved options. Shared by
272
+ * buildStart and watch-mode rebuilds.
273
+ */
274
+ async function runCompilation(args) {
275
+ const compilerOptions = args.compilerOptions;
276
+ const { state } = args;
277
+ const outputStructure = resolveOutputStructure(compilerOptions);
278
+ maybeInvalidateForOptionsChange(state, compilerOptions);
279
+ try {
280
+ // Stabilize project directory listings before the first digest. The
281
+ // ignored cache directory must not invalidate the cache that creates it.
282
+ await preparePersistentCacheDirectory(compilerOptions, outputStructure);
283
+ if (state.previousCompilation === undefined &&
284
+ (await restorePersistentCache({
285
+ state,
286
+ compilerOptions,
287
+ outputStructure,
288
+ }))) {
289
+ logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
290
+ return;
291
+ }
292
+ if (await hasUnchangedInputs({
293
+ state,
294
+ compilerOptions,
295
+ outputStructure,
296
+ })) {
297
+ logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
298
+ return;
299
+ }
300
+ // On a fresh process, seed previousCompilation from on-disk hashes
301
+ // so the first compile is a no-op when inputs are unchanged. Avoids
302
+ // racing concurrent readers that wiping outdir would interrupt.
303
+ const seededPrevious = state.previousCompilation ??
304
+ (await seedPreviousCompilationFromOutdir({
305
+ outdir: compilerOptions.outdir,
306
+ fs: compilerOptions.fs?.promises,
307
+ }));
308
+ await compileAndUpdateState({
309
+ state,
310
+ compilerOptions,
311
+ outputStructure,
312
+ previousCompilation: seededPrevious,
313
+ isServer,
314
+ });
315
+ await writePersistentCache(state, compilerOptions, outputStructure);
316
+ logger.success(`Compilation complete (${outputStructure})`);
317
+ }
318
+ catch (error) {
319
+ state.previousInputsDigest = undefined;
320
+ logger.error("Failed to compile project:", error.message);
321
+ logger.info("Please check your translation files for syntax errors.");
322
+ if (process.env.NODE_ENV === "production")
323
+ throw error;
324
+ }
325
+ }
326
+ export const unpluginFactory = (userArgs) => {
327
+ const state = getPluginState(userArgs);
328
+ const { readFiles, clearReadFiles } = state;
329
+ // Project root as reported by the bundler (vite `root`, webpack/rspack
330
+ // `context`). Falls back to the process working directory when the
331
+ // bundler doesn't provide one. The config file is resolved inside the
332
+ // project directory.
333
+ let discoveryRoot;
334
+ // Bound to the hook context in buildStart so that watchChange can
335
+ // register additional files.
336
+ let addWatchFile;
337
+ // A new plugin instance must not be served by config cache entries
338
+ // from a previous instance (e.g. vite.config.ts reloads reuse the
339
+ // process). Wiping the cache here makes every instance perform one
340
+ // fresh load; subsequent loads within the instance stay cached.
341
+ clearParaglideConfigCache();
342
+ function safeAddWatchFile(thisArg, path) {
343
+ const fn = thisArg?.addWatchFile ?? addWatchFile;
344
+ if (!fn)
345
+ return;
346
+ if (watchRegistrationUnsupported)
347
+ return;
348
+ try {
349
+ fn.call(thisArg ?? undefined, path);
350
+ }
351
+ catch (error) {
352
+ // Watch-file registration is best-effort: backends differ in what
353
+ // they support (unplugin's esbuild context throws for every call),
354
+ // and watcher resource limits can surface as errors. A failed
355
+ // registration must never fail the build.
356
+ watchRegistrationUnsupported = true;
357
+ logger.warn(`Registering additional watch files is not supported by this bundler context — skipping further registrations (${error?.message ?? String(error)})`);
358
+ }
359
+ }
360
+ // Whether a config file was present at the last load — drives the
361
+ // one-time warning when every config candidate disappears.
362
+ let hadActiveConfig = false;
363
+ // Watch-rebuild serialization state (see watchChange).
364
+ let compiling = false;
365
+ const pendingWatchPaths = new Set();
366
+ // Options from the most recent successful resolution — reused by input
367
+ // events that cannot have changed them.
368
+ let lastOptions;
369
+ // Set once a bundler context rejects watch-file registrations (esbuild);
370
+ // further registration attempts are skipped to avoid warning floods.
371
+ let watchRegistrationUnsupported = false;
372
+ const getProjectDir = () => nodeNormalizePath(resolve(discoveryRoot ?? process.cwd(), userArgs.project));
373
+ const resolveOptionsWith = (loaded) => resolveCompilerOptions({
374
+ config: loaded?.config,
375
+ overrides: userArgs,
376
+ root: discoveryRoot ?? process.cwd(),
377
+ });
378
+ /**
379
+ * Loads the active config, following candidate renames and keeping the
380
+ * last known-good configuration while the file is missing or invalid.
381
+ * Callers that know the config content changed should clear the cache
382
+ * for the project directory first.
383
+ */
384
+ async function loadActiveConfig() {
385
+ const dir = getProjectDir();
386
+ const winner = await resolveConfigCandidate(dir);
387
+ if (winner === undefined) {
388
+ if (hadActiveConfig) {
389
+ hadActiveConfig = false;
390
+ logger.warn("Paraglide config file was removed, continuing with default options.");
391
+ }
392
+ return undefined;
393
+ }
394
+ hadActiveConfig = true;
395
+ // Throws when the file exists but is invalid — callers decide the
396
+ // policy: watch modes skip and surface the error in development,
397
+ // build paths fail loudly in production.
398
+ return loadParaglideConfig({ projectDir: dir, logger });
399
+ }
400
+ /**
401
+ * Rebuild for a single watch event. Config-named events reload the
402
+ * config; input events reuse the last known options — options cannot
403
+ * have changed without a config event.
404
+ */
405
+ async function performWatchRecompile(path) {
406
+ const normalizedPath = nodeNormalizePath(path);
407
+ const projectDir = getProjectDir();
408
+ const withinProjectDir = normalizedPath === projectDir ||
409
+ normalizedPath.startsWith(`${projectDir}/`);
410
+ const configEvent = withinProjectDir && isConfigFileName(normalizedPath);
411
+ let args;
412
+ if (!configEvent && lastOptions !== undefined) {
413
+ args = lastOptions;
414
+ const targets = getWatchTargets(readFiles, { outdir: args.outdir });
415
+ if (targets.isIgnoredPath(normalizedPath))
416
+ return;
417
+ const shouldCompile = targets.files.has(normalizedPath) ||
418
+ isPathWithinDirectories(normalizedPath, targets.directories);
419
+ if (!shouldCompile)
420
+ return;
421
+ }
422
+ else {
423
+ if (configEvent) {
424
+ logger.info("Paraglide configuration changed, re-compiling.");
425
+ clearParaglideConfigCache({ projectDir });
426
+ }
427
+ try {
428
+ const loaded = await loadActiveConfig();
429
+ args = resolveOptionsWith(loaded);
430
+ }
431
+ catch (error) {
432
+ // Invalid config: skip the compilation entirely and surface
433
+ // the problem. The previously compiled output stays on disk.
434
+ logger.error(error.message);
435
+ logger.error("Skipping compilation because of the invalid paraglide config.");
436
+ return;
437
+ }
438
+ lastOptions = args;
439
+ }
440
+ maybeInvalidateForOptionsChange(state, args);
441
+ const outputStructure = resolveOutputStructure(args);
442
+ const previouslyReadFiles = new Set(readFiles);
443
+ try {
444
+ if (!configEvent) {
445
+ logger.info(`Re-compiling inlang project... File "${relative(process.cwd(), path)}" has changed.`);
446
+ }
447
+ clearReadFiles();
448
+ await compileAndUpdateState({
449
+ state,
450
+ compilerOptions: args,
451
+ outputStructure,
452
+ previousCompilation: state.previousCompilation,
453
+ isServer,
454
+ });
455
+ await writePersistentCache(state, args, outputStructure);
456
+ logger.success(`Re-compilation complete (${outputStructure})`);
457
+ const nextTargets = getWatchTargets(readFiles, { outdir: args.outdir });
458
+ for (const filePath of nextTargets.files)
459
+ safeAddWatchFile(undefined, filePath);
460
+ for (const directoryPath of nextTargets.directories)
461
+ safeAddWatchFile(undefined, directoryPath);
462
+ }
463
+ catch (e) {
464
+ clearReadFiles();
465
+ for (const filePath of previouslyReadFiles)
466
+ readFiles.add(filePath);
467
+ state.previousCompilation = undefined;
468
+ state.previousInputsDigest = undefined;
469
+ logger.warn("Failed to re-compile project:", e.message);
470
+ }
471
+ }
228
472
  return {
229
473
  name: PLUGIN_NAME,
230
474
  enforce: "pre",
231
475
  async buildStart() {
232
- const isProduction = process.env.NODE_ENV === "production";
233
- // default to locale-modules for development to speed up the dev server
234
- // https://github.com/opral/inlang-paraglide-js/issues/486
235
- const outputStructure = args.outputStructure ??
236
- (isProduction ? "message-modules" : "locale-modules");
476
+ addWatchFile = this.addWatchFile?.bind(this);
237
477
  try {
238
- // Stabilize project directory listings before the first digest. The
239
- // ignored cache directory must not invalidate the cache that creates it.
240
- await preparePersistentCacheDirectory(args, outputStructure);
241
- if (state.previousCompilation === undefined &&
242
- (await restorePersistentCache({
243
- state,
244
- compilerOptions: args,
245
- outputStructure,
246
- }))) {
247
- logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
248
- return;
249
- }
250
- // `vite build` calls buildStart once per environment (client, ssr).
251
- // Skip the expensive compile when the inputs haven't changed.
252
- if (state.previousCompilation && state.previousInputsDigest) {
253
- const currentDigest = await computeInputsDigest(state, args, outputStructure);
254
- if (currentDigest === state.previousInputsDigest) {
255
- logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
256
- return;
257
- }
258
- }
259
- // On a fresh process, seed previousCompilation from on-disk hashes
260
- // so the first compile is a no-op when inputs are unchanged. Avoids
261
- // racing concurrent readers that wiping outdir would interrupt.
262
- const seededPrevious = state.previousCompilation ??
263
- (await seedPreviousCompilationFromOutdir({
264
- outdir: args.outdir,
265
- fs: args.fs?.promises,
266
- }));
267
- state.previousCompilation = await compile({
268
- previousCompilation: seededPrevious,
269
- outputStructure,
270
- isServer,
271
- ...withoutCleanOutdir(args),
272
- cleanOutdir: false,
273
- // after the args spread so a user-provided fs doesn't bypass
274
- // the read tracking (trackedFs wraps args.fs when provided)
275
- fs: trackedFs,
276
- });
277
- state.previousInputsDigest = await computeInputsDigest(state, args, outputStructure);
278
- await writePersistentCache(state, args, outputStructure);
279
- logger.success(`Compilation complete (${outputStructure})`);
478
+ const loaded = await loadActiveConfig();
479
+ const args = resolveOptionsWith(loaded);
480
+ lastOptions = args;
481
+ await runCompilation({ state, compilerOptions: args });
280
482
  }
281
483
  catch (error) {
282
- state.previousInputsDigest = undefined;
283
- logger.error("Failed to compile project:", error.message);
284
- logger.info("Please check your translation files for syntax errors.");
285
- if (isProduction)
484
+ // Config errors are converted to last-good options by
485
+ // loadActiveConfig; compilation errors are handled inside
486
+ // runCompilation. Anything escaping is unexpected — fail the
487
+ // build in production, keep serving in development.
488
+ if (process.env.NODE_ENV === "production")
286
489
  throw error;
490
+ logger.error("Failed to prepare paraglide compilation:", error.message);
491
+ return;
287
492
  }
288
493
  finally {
289
- // in any case add the files to watch
290
- const targets = getWatchTargets(readFiles, { outdir: args.outdir });
291
- for (const filePath of targets.files) {
292
- this.addWatchFile(filePath);
494
+ if (lastOptions !== undefined) {
495
+ const targets = getWatchTargets(readFiles, {
496
+ outdir: lastOptions.outdir,
497
+ });
498
+ for (const filePath of targets.files)
499
+ safeAddWatchFile(this, filePath);
500
+ for (const directoryPath of targets.directories)
501
+ safeAddWatchFile(this, directoryPath);
293
502
  }
294
- for (const directoryPath of targets.directories) {
295
- this.addWatchFile(directoryPath);
503
+ // Register every config candidate inside the project directory
504
+ // so renames and format switches are reported even by backends
505
+ // that only report watched files (webpack's modifiedFiles).
506
+ const projectDir = getProjectDir();
507
+ for (const name of CONFIG_FILE_NAMES) {
508
+ safeAddWatchFile(this, join(projectDir, name));
296
509
  }
297
510
  }
298
511
  },
299
512
  async watchChange(path) {
300
- const normalizedPath = nodeNormalizePath(path);
301
- const targets = getWatchTargets(readFiles, { outdir: args.outdir });
302
- if (targets.isIgnoredPath(normalizedPath)) {
513
+ // Serialize rebuilds: bundlers may deliver several watchChange
514
+ // events concurrently (webpack fires once per modified file).
515
+ // Every path is queued — a config event must never be starved by
516
+ // later input events.
517
+ pendingWatchPaths.add(nodeNormalizePath(path));
518
+ if (compiling)
303
519
  return;
304
- }
305
- const shouldCompile = targets.files.has(normalizedPath) ||
306
- isPathWithinDirectories(normalizedPath, targets.directories);
307
- if (shouldCompile === false) {
308
- return;
309
- }
310
- const isProduction = process.env.NODE_ENV === "production";
311
- // default to locale-modules for development to speed up the dev server
312
- // https://github.com/opral/inlang-paraglide-js/issues/486
313
- const outputStructure = args.outputStructure ??
314
- (isProduction ? "message-modules" : "locale-modules");
315
- const previouslyReadFiles = new Set(readFiles);
520
+ compiling = true;
316
521
  try {
317
- logger.info(`Re-compiling inlang project... File "${relative(process.cwd(), path)}" has changed.`);
318
- // Clear readFiles to track fresh file reads
319
- clearReadFiles();
320
- state.previousCompilation = await compile({
321
- previousCompilation: state.previousCompilation,
322
- outputStructure,
323
- isServer,
324
- ...withoutCleanOutdir(args),
325
- cleanOutdir: false,
326
- fs: trackedFs,
327
- });
328
- state.previousInputsDigest = await computeInputsDigest(state, args, outputStructure);
329
- await writePersistentCache(state, args, outputStructure);
330
- logger.success(`Re-compilation complete (${outputStructure})`);
331
- // Add any new files to watch
332
- const nextTargets = getWatchTargets(readFiles, { outdir: args.outdir });
333
- for (const filePath of nextTargets.files) {
334
- this.addWatchFile(filePath);
335
- }
336
- for (const directoryPath of nextTargets.directories) {
337
- this.addWatchFile(directoryPath);
522
+ while (pendingWatchPaths.size > 0) {
523
+ const batch = [...pendingWatchPaths];
524
+ pendingWatchPaths.clear();
525
+ for (const current of batch) {
526
+ await performWatchRecompile(current);
527
+ }
338
528
  }
339
529
  }
340
- catch (e) {
341
- clearReadFiles();
342
- for (const filePath of previouslyReadFiles) {
343
- readFiles.add(filePath);
344
- }
345
- // Reset compilation result on error
346
- state.previousCompilation = undefined;
347
- state.previousInputsDigest = undefined;
348
- logger.warn("Failed to re-compile project:", e.message);
530
+ finally {
531
+ compiling = false;
349
532
  }
350
533
  },
351
534
  vite: {
@@ -359,8 +542,14 @@ export const unpluginFactory = (args) => {
359
542
  isServer = "import.meta.env?.SSR ?? typeof window === 'undefined'";
360
543
  },
361
544
  },
545
+ configResolved: {
546
+ handler: (config) => {
547
+ discoveryRoot = config.root;
548
+ },
549
+ },
362
550
  },
363
551
  webpack(compiler) {
552
+ discoveryRoot = compiler.context;
364
553
  compiler.options.resolve = {
365
554
  ...compiler.options.resolve,
366
555
  fallback: {
@@ -370,41 +559,133 @@ export const unpluginFactory = (args) => {
370
559
  },
371
560
  };
372
561
  compiler.hooks.beforeRun.tapPromise(PLUGIN_NAME, async () => {
373
- const isProduction = process.env.NODE_ENV === "production";
374
- // default to locale-modules for development to speed up the dev server
375
- // https://github.com/opral/inlang-paraglide-js/issues/486
376
- const outputStructure = args.outputStructure ??
377
- (isProduction ? "message-modules" : "locale-modules");
562
+ let args;
378
563
  try {
564
+ const loaded = await loadActiveConfig();
565
+ args = resolveOptionsWith(loaded);
566
+ lastOptions = args;
567
+ }
568
+ catch (error) {
569
+ state.previousInputsDigest = undefined;
570
+ state.previousCompilation = undefined;
571
+ logger.error("Failed to load paraglide config:", error.message);
572
+ if (process.env.NODE_ENV === "production")
573
+ throw error;
574
+ return;
575
+ }
576
+ const outputStructure = resolveOutputStructure(args);
577
+ maybeInvalidateForOptionsChange(state, args);
578
+ try {
579
+ await preparePersistentCacheDirectory(args, outputStructure);
580
+ if (state.previousCompilation === undefined &&
581
+ (await restorePersistentCache({
582
+ state,
583
+ compilerOptions: args,
584
+ outputStructure,
585
+ }))) {
586
+ logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
587
+ return;
588
+ }
379
589
  // Multi-compiler webpack setups (client + server) trigger
380
590
  // beforeRun once per compiler — skip when inputs are unchanged.
381
- if (state.previousCompilation && state.previousInputsDigest) {
382
- const currentDigest = await computeInputsDigest(state, args, outputStructure);
383
- if (currentDigest === state.previousInputsDigest) {
384
- logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
385
- return;
386
- }
591
+ if (await hasUnchangedInputs({
592
+ state,
593
+ compilerOptions: args,
594
+ outputStructure,
595
+ })) {
596
+ logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
597
+ return;
387
598
  }
388
599
  const seededPrevious = state.previousCompilation ??
389
600
  (await seedPreviousCompilationFromOutdir({
390
601
  outdir: args.outdir,
391
602
  fs: args.fs?.promises,
392
603
  }));
393
- state.previousCompilation = await compile({
604
+ await compileAndUpdateState({
605
+ state,
606
+ compilerOptions: args,
607
+ outputStructure,
394
608
  previousCompilation: seededPrevious,
609
+ // webpack has no vite-style SSR expression; rely on the
610
+ // compiler default.
611
+ isServer: undefined,
612
+ });
613
+ await writePersistentCache(state, args, outputStructure);
614
+ logger.success(`Compilation complete (${outputStructure})`);
615
+ }
616
+ catch (error) {
617
+ state.previousInputsDigest = undefined;
618
+ logger.warn("Failed to compile project:", error.message);
619
+ logger.warn("Please check your translation files for syntax errors.");
620
+ if (process.env.NODE_ENV === "production")
621
+ throw error;
622
+ }
623
+ });
624
+ },
625
+ rspack(compiler) {
626
+ discoveryRoot = compiler.context;
627
+ // Rspack mirrors webpack's hook API but ships no usable types,
628
+ // so hooks are tapped structurally.
629
+ const tapRspackBeforeRun = (fn) => {
630
+ const target = compiler.hooks?.beforeRun;
631
+ target?.tapPromise?.(PLUGIN_NAME, fn);
632
+ };
633
+ tapRspackBeforeRun(async () => {
634
+ let args;
635
+ try {
636
+ const loaded = await loadActiveConfig();
637
+ args = resolveOptionsWith(loaded);
638
+ lastOptions = args;
639
+ }
640
+ catch (error) {
641
+ state.previousInputsDigest = undefined;
642
+ state.previousCompilation = undefined;
643
+ logger.error("Failed to load paraglide config:", error.message);
644
+ if (process.env.NODE_ENV === "production")
645
+ throw error;
646
+ return;
647
+ }
648
+ const outputStructure = resolveOutputStructure(args);
649
+ maybeInvalidateForOptionsChange(state, args);
650
+ try {
651
+ await preparePersistentCacheDirectory(args, outputStructure);
652
+ if (state.previousCompilation === undefined &&
653
+ (await restorePersistentCache({
654
+ state,
655
+ compilerOptions: args,
656
+ outputStructure,
657
+ }))) {
658
+ logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
659
+ return;
660
+ }
661
+ if (await hasUnchangedInputs({
662
+ state,
663
+ compilerOptions: args,
395
664
  outputStructure,
396
- ...withoutCleanOutdir(args),
397
- cleanOutdir: false,
398
- fs: trackedFs,
665
+ })) {
666
+ logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
667
+ return;
668
+ }
669
+ const seededPrevious = state.previousCompilation ??
670
+ (await seedPreviousCompilationFromOutdir({
671
+ outdir: args.outdir,
672
+ fs: args.fs?.promises,
673
+ }));
674
+ await compileAndUpdateState({
675
+ state,
676
+ compilerOptions: args,
677
+ outputStructure,
678
+ previousCompilation: seededPrevious,
679
+ isServer: undefined,
399
680
  });
400
- state.previousInputsDigest = await computeInputsDigest(state, args, outputStructure);
681
+ await writePersistentCache(state, args, outputStructure);
401
682
  logger.success(`Compilation complete (${outputStructure})`);
402
683
  }
403
684
  catch (error) {
404
685
  state.previousInputsDigest = undefined;
405
686
  logger.warn("Failed to compile project:", error.message);
406
687
  logger.warn("Please check your translation files for syntax errors.");
407
- if (isProduction)
688
+ if (process.env.NODE_ENV === "production")
408
689
  throw error;
409
690
  }
410
691
  });