@let-value/translate-extract 1.1.6-beta.3 → 1.2.3

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.
@@ -12,52 +12,25 @@ import { getFormula, getNPlurals } from "plural-forms";
12
12
  import { isDeepStrictEqual } from "node:util";
13
13
  import glob from "fast-glob";
14
14
  //#region src/plugins/cleanup/cleanup.ts
15
- const namespace$2 = "cleanup";
16
15
  function cleanup() {
17
16
  return {
18
17
  name: "cleanup",
19
18
  setup(build) {
20
19
  build.context.logger?.debug("cleanup plugin initialized");
21
- const processed = /* @__PURE__ */ new Set();
22
- const generated = /* @__PURE__ */ new Set();
23
- const dirs = /* @__PURE__ */ new Set();
24
- let dispatched = false;
25
- build.onResolve({
26
- namespace: namespace$2,
27
- filter: /.*/
28
- }, ({ path }) => {
29
- generated.add(path);
30
- dirs.add(dirname(path));
31
- Promise.all([
32
- build.defer("source"),
33
- build.defer("translate"),
34
- build.defer(namespace$2)
35
- ]).then(() => {
36
- if (dispatched) return;
37
- dispatched = true;
38
- for (const path of dirs.values()) build.process({
39
- entrypoint: path,
40
- path,
41
- namespace: namespace$2,
42
- data: void 0
43
- });
44
- });
45
- });
46
- build.onProcess({
47
- namespace: namespace$2,
48
- filter: /.*/
49
- }, async ({ path }) => {
50
- if (processed.has(path)) return;
51
- processed.add(path);
52
- const files = await fs.readdir(path).catch(() => []);
53
- for (const f of files.filter((p) => p.endsWith(".po"))) {
54
- const full = join(path, f);
55
- const contents = await fs.readFile(full).catch(() => void 0);
56
- if (!contents) continue;
57
- const parsed = gettextParser.po.parse(contents);
58
- const hasTranslations = Object.entries(parsed.translations || {}).some(([ctx, msgs]) => Object.keys(msgs).some((id) => !(ctx === "" && id === "")));
59
- if (!hasTranslations && generated.has(full)) await fs.unlink(full);
60
- if (hasTranslations && !generated.has(full)) build.context.logger?.warn({ path: full }, "stray translation file");
20
+ build.onOutputs(async ({ outputs }) => {
21
+ const generated = new Set(outputs);
22
+ const dirs = new Set(outputs.map((path) => dirname(path)));
23
+ for (const dir of dirs) {
24
+ const files = await fs.readdir(dir).catch(() => []);
25
+ for (const file of files.filter((name) => name.endsWith(".po"))) {
26
+ const full = join(dir, file);
27
+ const contents = await fs.readFile(full).catch(() => void 0);
28
+ if (!contents) continue;
29
+ const parsed = gettextParser.po.parse(contents);
30
+ const hasTranslations = Object.entries(parsed.translations || {}).some(([ctx, msgs]) => Object.keys(msgs).some((id) => !(ctx === "" && id === "")));
31
+ if (!hasTranslations && generated.has(full)) await fs.unlink(full);
32
+ if (hasTranslations && !generated.has(full)) build.context.logger?.warn({ path: full }, "stray translation file");
33
+ }
61
34
  }
62
35
  });
63
36
  }
@@ -258,7 +231,7 @@ const messageQuery$1 = notInPlural(withComment({
258
231
  pattern: callPattern("message", messageArgs),
259
232
  extract: extractMessage("message")
260
233
  }));
261
- const allowed$1 = new Set([
234
+ const allowed$1 = /* @__PURE__ */ new Set([
262
235
  "string",
263
236
  "object",
264
237
  "template_string"
@@ -401,7 +374,7 @@ const gettextQuery = withComment({
401
374
  pattern: callPattern("gettext", messageArgs),
402
375
  extract: extractMessage("gettext")
403
376
  });
404
- const allowed = new Set([
377
+ const allowed = /* @__PURE__ */ new Set([
405
378
  "string",
406
379
  "object",
407
380
  "template_string",
@@ -573,7 +546,7 @@ function findTsconfig(dir) {
573
546
  }
574
547
  }
575
548
  const resolverCache = /* @__PURE__ */ new Map();
576
- const builtins = new Set([...builtinModules, ...builtinModules.map((name) => `node:${name}`)]);
549
+ const builtins = /* @__PURE__ */ new Set([...builtinModules, ...builtinModules.map((name) => `node:${name}`)]);
577
550
  function isBuiltin(spec) {
578
551
  if (builtins.has(spec)) return true;
579
552
  const [base, subpath] = (spec.startsWith("node:") ? spec.slice(5) : spec).split("/", 2);
@@ -650,58 +623,29 @@ function resolveImportResults(file, imports) {
650
623
  //#endregion
651
624
  //#region src/plugins/core/core.ts
652
625
  const filter$1 = /\.([cm]?tsx?|jsx?)$/;
653
- const namespace$1 = "source";
654
626
  function core() {
655
627
  return {
656
628
  name: "core",
657
629
  setup(build) {
658
630
  build.context.logger?.debug("core plugin initialized");
659
- build.onResolve({
660
- filter: filter$1,
661
- namespace: namespace$1
662
- }, ({ entrypoint, path, import: imp, data }) => {
663
- return {
664
- entrypoint,
665
- namespace: namespace$1,
666
- path: resolve(path),
667
- import: imp,
668
- data
669
- };
670
- });
671
- build.onLoad({
672
- filter: filter$1,
673
- namespace: namespace$1
674
- }, async ({ entrypoint, path }) => {
675
- return {
676
- entrypoint,
677
- path,
678
- namespace: namespace$1,
679
- data: await readFile(path, "utf8")
680
- };
681
- });
682
- build.onProcess({
683
- filter: filter$1,
684
- namespace: namespace$1
685
- }, ({ entrypoint, path, data }) => {
686
- const result = parseSource$1(data, path);
631
+ build.onLoad(filter$1, ({ path }) => readFile(path, "utf8"));
632
+ build.onProcess(filter$1, ({ entrypoint, path, contents, emit }) => {
633
+ const result = parseSource$1(contents, path);
687
634
  if (result.entrypoint && entrypoint !== path) {
688
- build.source(path);
689
- return {
690
- entrypoint,
691
- path,
692
- namespace: namespace$1,
693
- data: []
694
- };
635
+ build.source({
636
+ entrypoint: path,
637
+ path
638
+ });
639
+ return true;
695
640
  }
696
641
  const { translations, imports, warnings } = result;
697
642
  if (build.context.config.walk) {
698
643
  const { resolved, unresolved } = resolveImportResults(path, imports);
699
644
  for (const result of resolved) {
700
645
  if (build.context.paths.has(result.path)) continue;
701
- build.resolve({
646
+ build.source({
702
647
  entrypoint,
703
648
  path: result.path,
704
- namespace: namespace$1,
705
649
  import: result.import
706
650
  });
707
651
  }
@@ -710,19 +654,14 @@ function core() {
710
654
  if (imp && isExcluded({
711
655
  entrypoint,
712
656
  path: spec,
713
- namespace: namespace$1,
657
+ namespace: "source",
714
658
  import: imp
715
659
  }, build.context.config.exclude)) continue;
716
660
  build.context.logger?.warn(`Unable to resolve import "${spec}" from ${path}${error ? `: ${error}` : ""}`);
717
661
  }
718
662
  }
719
663
  for (const warning of warnings) build.context.logger?.warn(`${warning.error} at ${warning.reference}`);
720
- build.resolve({
721
- entrypoint,
722
- path,
723
- namespace: "translate",
724
- data: translations
725
- });
664
+ emit(translations);
726
665
  });
727
666
  }
728
667
  };
@@ -763,7 +702,7 @@ function collect(source, locale) {
763
702
  }
764
703
  //#endregion
765
704
  //#region src/plugins/po/hasChanges.ts
766
- const IGNORED_HEADER_KEYS = new Set(["pot-creation-date", "po-revision-date"]);
705
+ const IGNORED_HEADER_KEYS = /* @__PURE__ */ new Set(["pot-creation-date", "po-revision-date"]);
767
706
  const IGNORED_HEADER_LINE_PREFIXES = ["pot-creation-date:", "po-revision-date:"];
768
707
  function normalizeHeaderString(value) {
769
708
  const lines = value.split("\n");
@@ -779,7 +718,7 @@ function normalizeHeaderString(value) {
779
718
  if (hadTrailingNewline && filtered[filtered.length - 1] !== "") filtered.push("");
780
719
  return filtered.join("\n");
781
720
  }
782
- function normalize(translations) {
721
+ function normalize$1(translations) {
783
722
  const compiled = gettextParser.po.compile(translations);
784
723
  const parsed = gettextParser.po.parse(compiled);
785
724
  if (parsed.headers) {
@@ -793,7 +732,9 @@ function normalize(translations) {
793
732
  }
794
733
  function hasChanges(left, right) {
795
734
  if (!right) return true;
796
- return !isDeepStrictEqual(normalize(left), normalize(right));
735
+ const normalizedLeft = normalize$1(left);
736
+ const normalizedRight = normalize$1(right);
737
+ return !isDeepStrictEqual(normalizedLeft, normalizedRight);
797
738
  }
798
739
  //#endregion
799
740
  //#region src/plugins/po/merge.ts
@@ -890,74 +831,39 @@ function merge(sources, existing, obsolete, locale, generatedAt) {
890
831
  }
891
832
  //#endregion
892
833
  //#region src/plugins/po/po.ts
893
- const namespace = "translate";
894
834
  function po() {
895
835
  return {
896
836
  name: "po",
897
837
  setup(build) {
898
838
  build.context.logger?.debug("po plugin initialized");
899
839
  const collections = /* @__PURE__ */ new Map();
900
- let dispatched = false;
901
- build.onResolve({
902
- filter: /.*/,
903
- namespace
904
- }, async ({ entrypoint, path, data }) => {
905
- if (!data || !Array.isArray(data)) return;
906
- for (const locale of build.context.config.locales) {
840
+ build.onCollected(({ entrypoint, files, output }) => {
841
+ for (const { path, translations } of files) for (const locale of build.context.config.locales) {
907
842
  const destination = build.context.config.destination({
908
843
  entrypoint,
909
844
  locale,
910
845
  path
911
846
  });
912
- if (!collections.has(destination)) collections.set(destination, {
847
+ const collection = collections.get(destination);
848
+ if (collection) {
849
+ collection.translations.push(...translations);
850
+ continue;
851
+ }
852
+ const created = {
913
853
  locale,
914
- translations: []
915
- });
916
- collections.get(destination)?.translations.push(...data);
917
- }
918
- Promise.all([build.defer("source"), build.defer(namespace)]).then(() => {
919
- if (dispatched) return;
920
- dispatched = true;
921
- for (const path of collections.keys()) build.load({
922
- entrypoint,
923
- path,
924
- namespace
854
+ translations: [...translations]
855
+ };
856
+ collections.set(destination, created);
857
+ output(destination, async () => {
858
+ const contents = await fs.readFile(destination).catch(() => void 0);
859
+ const existing = contents ? gettextParser.po.parse(contents) : void 0;
860
+ const out = merge([{ translations: collect(created.translations, created.locale) }], existing, build.context.config.obsolete, created.locale, build.context.generatedAt);
861
+ if (hasChanges(out, existing)) {
862
+ await fs.mkdir(dirname(destination), { recursive: true });
863
+ await fs.writeFile(destination, gettextParser.po.compile(out));
864
+ }
925
865
  });
926
- });
927
- });
928
- build.onLoad({
929
- filter: /.*\.po$/,
930
- namespace
931
- }, async ({ entrypoint, path }) => {
932
- const contents = await fs.readFile(path).catch(() => void 0);
933
- return {
934
- entrypoint,
935
- path,
936
- namespace,
937
- data: contents ? gettextParser.po.parse(contents) : void 0
938
- };
939
- });
940
- build.onProcess({
941
- filter: /.*\.po$/,
942
- namespace
943
- }, async ({ entrypoint, path, data }) => {
944
- const collected = collections.get(path);
945
- if (!collected) {
946
- build.context.logger?.warn({ path }, "no translations collected for this path");
947
- return;
948
- }
949
- const { locale, translations } = collected;
950
- const out = merge([{ translations: collect(translations, locale) }], data, build.context.config.obsolete, locale, build.context.generatedAt);
951
- if (hasChanges(out, data)) {
952
- await fs.mkdir(dirname(path), { recursive: true });
953
- await fs.writeFile(path, gettextParser.po.compile(out));
954
866
  }
955
- build.resolve({
956
- entrypoint,
957
- path,
958
- namespace: "cleanup",
959
- data: translations
960
- });
961
867
  });
962
868
  }
963
869
  };
@@ -1258,35 +1164,576 @@ function react() {
1258
1164
  name: "react",
1259
1165
  setup(build) {
1260
1166
  build.context.logger?.debug("react plugin initialized");
1261
- build.onProcess({
1262
- filter,
1263
- namespace: "source"
1264
- }, ({ entrypoint, path, data }) => {
1265
- const { translations, warnings } = parseSource(data, path);
1167
+ build.onProcess(filter, ({ path, contents, emit }) => {
1168
+ const { translations, warnings } = parseSource(contents, path);
1266
1169
  for (const warning of warnings) build.context.logger?.warn(`${warning.error} at ${warning.reference}`);
1267
- build.resolve({
1268
- entrypoint,
1269
- path,
1270
- namespace: "translate",
1271
- data: translations
1272
- });
1170
+ emit(translations);
1273
1171
  });
1274
1172
  }
1275
1173
  };
1276
1174
  }
1277
1175
  //#endregion
1278
- //#region src/defer.ts
1279
- var Defer = class {
1280
- pending = 0;
1281
- promise = Promise.resolve();
1282
- resolve;
1283
- enqueue() {
1284
- if (this.pending++ === 0) this.promise = new Promise((res) => {
1285
- this.resolve = res;
1176
+ //#region ../graph/dist/index.mjs
1177
+ var CycleError = class extends Error {
1178
+ constructor(dependency, dependent) {
1179
+ super(`Adding edge "${dependency}" -> "${dependent}" would create a cycle`);
1180
+ this.name = "CycleError";
1181
+ }
1182
+ };
1183
+ var SkippedError = class extends Error {
1184
+ constructor(id) {
1185
+ super(`Node "${id}" was skipped because a dependency failed or the run was aborted`);
1186
+ this.name = "SkippedError";
1187
+ }
1188
+ };
1189
+ var NodeState = class {
1190
+ id;
1191
+ status = "pending";
1192
+ result;
1193
+ reason;
1194
+ lazy = false;
1195
+ /**
1196
+ * Dependencies that have not fulfilled yet. Readiness is maintained as
1197
+ * edges are added and dependencies settle, so scheduling never has to
1198
+ * rescan the pending set.
1199
+ */
1200
+ unresolved = 0;
1201
+ /** Whether the node is already sitting in the ready queue. */
1202
+ queued = false;
1203
+ /** Resumers of demanders suspended on this node, flushed on settle. */
1204
+ waiters;
1205
+ /** Ordered data dependencies, passed as run arguments. */
1206
+ arguments_;
1207
+ /** All dependencies, including ordering-only edges added via connect(). */
1208
+ dependencies = /* @__PURE__ */ new Set();
1209
+ dependents = /* @__PURE__ */ new Set();
1210
+ execute;
1211
+ constructor(id, arguments_, execute) {
1212
+ this.id = id;
1213
+ this.arguments_ = arguments_;
1214
+ this.execute = execute;
1215
+ for (const dependency of arguments_) this.dependencies.add(dependency);
1216
+ }
1217
+ get value() {
1218
+ if (this.status !== "fulfilled") throw new Error(`Node "${this.id}" has no value (status: ${this.status})`);
1219
+ return this.result;
1220
+ }
1221
+ get error() {
1222
+ return this.reason;
1223
+ }
1224
+ };
1225
+ /**
1226
+ * A dynamic, asynchronous DAG of computations.
1227
+ *
1228
+ * Nodes can be added before or during a run, so a running node can discover
1229
+ * and schedule new work. Edges added with {@link Graph.connect} impose
1230
+ * ordering without contributing run arguments, which lets consumers build
1231
+ * barriers: a node that only starts once a dynamically growing set of
1232
+ * predecessors has finished.
1233
+ */
1234
+ var Graph = class {
1235
+ #nodes = /* @__PURE__ */ new Map();
1236
+ #root;
1237
+ #pending = /* @__PURE__ */ new Set();
1238
+ #dormant = /* @__PURE__ */ new Set();
1239
+ /** Pending nodes whose dependencies have all fulfilled. */
1240
+ #ready = [];
1241
+ /** Demanders that finished waiting and need a slot before resuming. */
1242
+ #slotWaiters = [];
1243
+ #runningCount = 0;
1244
+ #suspendedCount = 0;
1245
+ #concurrency = Infinity;
1246
+ #controller;
1247
+ #finish;
1248
+ #started = false;
1249
+ #finished = false;
1250
+ #errors = [];
1251
+ constructor() {
1252
+ INTERNALS.set(this, {
1253
+ kinds: /* @__PURE__ */ new Map(),
1254
+ workers: /* @__PURE__ */ new Map(),
1255
+ wrappers: /* @__PURE__ */ new Map(),
1256
+ frozen: false
1257
+ });
1258
+ this.#root = new Scope(this, void 0, "");
1259
+ }
1260
+ /** The root scope. Nodes added here belong to no named scope. */
1261
+ get root() {
1262
+ return this.#root;
1263
+ }
1264
+ /** Creates or returns a named child scope of the root scope. */
1265
+ scope(name) {
1266
+ return this.#root.scope(name);
1267
+ }
1268
+ /** Declares a typed node kind. Kind names are unique per graph. */
1269
+ kind(name) {
1270
+ const internals = INTERNALS.get(this);
1271
+ if (internals.kinds.has(name)) throw new Error(`Kind "${name}" is already registered`);
1272
+ const kind = { name };
1273
+ internals.kinds.set(name, kind);
1274
+ return kind;
1275
+ }
1276
+ /**
1277
+ * Registers a worker that runs for every node of `source`, in the same
1278
+ * scope as that node. Worker results form their own kind, so they can be
1279
+ * awaited and collected with scope.completion(). Part of the graph's
1280
+ * definition: allowed until run() starts, and applies to nodes added
1281
+ * before the registration as well.
1282
+ */
1283
+ each(source, name, worker) {
1284
+ const internals = INTERNALS.get(this);
1285
+ if (internals.frozen) throw new Error(`Cannot register worker "${name}" after run() started`);
1286
+ const kind = this.kind(name);
1287
+ const registrations = internals.workers.get(source.name) ?? [];
1288
+ registrations.push({
1289
+ kind,
1290
+ worker
1291
+ });
1292
+ internals.workers.set(source.name, registrations);
1293
+ return kind;
1294
+ }
1295
+ /**
1296
+ * Interposes on every node of a kind: consumers of the node receive the
1297
+ * outermost wrapper's value instead of the raw computation. The chain is
1298
+ * materialized as real intermediate nodes ("<id>@raw", "<id>@<name>"),
1299
+ * evaluated on demand: a wrapper that never calls next() (e.g. a cache
1300
+ * hit) keeps the inner nodes from running at all. Wrappers compose; the
1301
+ * last registered one is outermost. Part of the graph's definition:
1302
+ * allowed until run() starts.
1303
+ */
1304
+ wrap(kind, name, wrapper) {
1305
+ const internals = INTERNALS.get(this);
1306
+ if (internals.frozen) throw new Error(`Cannot register wrapper "${name}" after run() started`);
1307
+ const registrations = internals.wrappers.get(kind.name) ?? [];
1308
+ registrations.push({
1309
+ name,
1310
+ wrap: wrapper
1311
+ });
1312
+ internals.wrappers.set(kind.name, registrations);
1313
+ }
1314
+ /** Whether `node` transitively depends on `dependency`. */
1315
+ dependsOn(node, dependency) {
1316
+ const target = this.#resolve(dependency);
1317
+ const stack = [...this.#resolve(node).dependencies];
1318
+ const seen = /* @__PURE__ */ new Set();
1319
+ while (stack.length > 0) {
1320
+ const current = stack.pop();
1321
+ if (current === target) return true;
1322
+ if (seen.has(current)) continue;
1323
+ seen.add(current);
1324
+ stack.push(...current.dependencies);
1325
+ }
1326
+ return false;
1327
+ }
1328
+ add(id, options) {
1329
+ if (this.#finished) throw new Error(`Cannot add node "${id}": the graph has already finished`);
1330
+ if (this.#nodes.has(id)) throw new Error(`Node "${id}" already exists`);
1331
+ const dependencies = (options.dependencies ?? []).map((dependency) => this.#resolve(dependency));
1332
+ const node = new NodeState(id, dependencies, options.run);
1333
+ node.lazy = options.lazy ?? false;
1334
+ let doomed;
1335
+ for (const dependency of dependencies) {
1336
+ dependency.dependents.add(node);
1337
+ if (dependency.status === "rejected" || dependency.status === "skipped") doomed ??= dependency;
1338
+ else if (dependency.status !== "fulfilled") node.unresolved += 1;
1339
+ }
1340
+ this.#nodes.set(id, node);
1341
+ if (node.lazy) this.#dormant.add(node);
1342
+ else {
1343
+ this.#pending.add(node);
1344
+ for (const dependency of dependencies) this.#wake(dependency);
1345
+ }
1346
+ if (doomed) this.#settle(node, "skipped", void 0, new SkippedError(node.id));
1347
+ else if (!node.lazy) this.#enqueue(node);
1348
+ if (this.#started) queueMicrotask(() => this.#pump());
1349
+ return node;
1350
+ }
1351
+ /**
1352
+ * Adds an ordering-only edge: `dependent` will not start until
1353
+ * `dependency` has fulfilled. The dependency's value is not passed to the
1354
+ * dependent's run function; read it from the node handle if needed.
1355
+ */
1356
+ connect(dependency, dependent) {
1357
+ const from = this.#resolve(dependency);
1358
+ const to = this.#resolve(dependent);
1359
+ if (to.status !== "pending") throw new Error(`Cannot add dependency to node "${to.id}": it has already started`);
1360
+ if (from === to || this.#reaches(to, from)) throw new CycleError(from.id, to.id);
1361
+ if (to.dependencies.has(from)) return;
1362
+ to.dependencies.add(from);
1363
+ from.dependents.add(to);
1364
+ if (from.status === "rejected" || from.status === "skipped") {
1365
+ this.#settle(to, "skipped", void 0, new SkippedError(to.id));
1366
+ return;
1367
+ }
1368
+ if (from.status !== "fulfilled") to.unresolved += 1;
1369
+ if (!this.#dormant.has(to)) this.#wake(from);
1370
+ if (this.#started) queueMicrotask(() => this.#pump());
1371
+ }
1372
+ get(id) {
1373
+ return this.#nodes.get(id);
1374
+ }
1375
+ get nodes() {
1376
+ return this.#nodes;
1377
+ }
1378
+ /**
1379
+ * Runs the graph until every node has settled, including nodes added
1380
+ * while running. Rejects with an AggregateError if any node rejected, or
1381
+ * with the abort reason if the signal aborted. Can only be called once.
1382
+ */
1383
+ async run(options = {}) {
1384
+ if (this.#started) throw new Error("Graph is already running or has finished");
1385
+ Scope.materialize(this.#root);
1386
+ INTERNALS.get(this).frozen = true;
1387
+ this.#started = true;
1388
+ this.#concurrency = options.concurrency ?? Infinity;
1389
+ if (this.#concurrency < 1) throw new Error("concurrency must be at least 1");
1390
+ this.#controller = new AbortController();
1391
+ const signal = options.signal;
1392
+ if (signal) if (signal.aborted) this.#controller.abort(signal.reason);
1393
+ else signal.addEventListener("abort", () => {
1394
+ this.#controller?.abort(signal.reason);
1395
+ this.#pump();
1396
+ });
1397
+ return new Promise((resolve, reject) => {
1398
+ this.#finish = {
1399
+ resolve,
1400
+ reject
1401
+ };
1402
+ this.#pump();
1403
+ });
1404
+ }
1405
+ #resolve(handle) {
1406
+ const node = this.#nodes.get(handle.id);
1407
+ if (!node || node !== handle) throw new Error(`Node "${handle.id}" does not belong to this graph`);
1408
+ return node;
1409
+ }
1410
+ #reaches(from, target) {
1411
+ const stack = [...from.dependents];
1412
+ const seen = /* @__PURE__ */ new Set();
1413
+ while (stack.length > 0) {
1414
+ const node = stack.pop();
1415
+ if (node === target) return true;
1416
+ if (seen.has(node)) continue;
1417
+ seen.add(node);
1418
+ stack.push(...node.dependents);
1419
+ }
1420
+ return false;
1421
+ }
1422
+ /** Offers a pending node to the scheduler once its dependencies are in. */
1423
+ #enqueue(node) {
1424
+ if (node.queued || node.unresolved > 0 || node.status !== "pending" || this.#dormant.has(node)) return;
1425
+ node.queued = true;
1426
+ this.#ready.push(node);
1427
+ }
1428
+ #pump() {
1429
+ if (!this.#started || this.#finished || !this.#finish) return;
1430
+ if (this.#controller?.signal.aborted) {
1431
+ while (this.#slotWaiters.length > 0) {
1432
+ this.#runningCount += 1;
1433
+ this.#slotWaiters.shift()();
1434
+ }
1435
+ for (const node of [...this.#pending]) this.#settle(node, "skipped", void 0, new SkippedError(node.id));
1436
+ } else {
1437
+ while (this.#slotWaiters.length > 0 && this.#runningCount < this.#concurrency) {
1438
+ this.#runningCount += 1;
1439
+ this.#slotWaiters.shift()();
1440
+ }
1441
+ while (this.#ready.length > 0 && this.#runningCount < this.#concurrency) {
1442
+ const node = this.#ready.shift();
1443
+ node.queued = false;
1444
+ if (node.unresolved === 0 && node.status === "pending") this.#start(node);
1445
+ }
1446
+ }
1447
+ if (this.#pending.size === 0 && this.#runningCount === 0 && this.#suspendedCount === 0) this.#complete();
1448
+ }
1449
+ /** Moves a dormant lazy node (and its dormant dependencies) into scheduling. */
1450
+ #wake(node) {
1451
+ const stack = [node];
1452
+ while (stack.length > 0) {
1453
+ const current = stack.pop();
1454
+ if (!this.#dormant.delete(current)) continue;
1455
+ this.#pending.add(current);
1456
+ this.#enqueue(current);
1457
+ stack.push(...current.dependencies);
1458
+ }
1459
+ }
1460
+ /**
1461
+ * Takes a concurrency slot, waiting for one when the graph is at its
1462
+ * limit. The slot is reserved by #pump at hand-off, so a granted slot
1463
+ * cannot be taken by anything else in the meantime.
1464
+ */
1465
+ #acquire() {
1466
+ if (this.#runningCount < this.#concurrency) {
1467
+ this.#runningCount += 1;
1468
+ return;
1469
+ }
1470
+ return new Promise((resume) => {
1471
+ this.#slotWaiters.push(resume);
1472
+ });
1473
+ }
1474
+ async #demand(demander, handle) {
1475
+ if (demander.status !== "running") throw new Error(`Node "${demander.id}" called demand() while it is "${demander.status}": a node context is only usable for as long as its node runs`);
1476
+ const target = this.#resolve(handle);
1477
+ if (target === demander || this.#reaches(demander, target)) throw new CycleError(target.id, demander.id);
1478
+ if (!demander.dependencies.has(target)) {
1479
+ demander.dependencies.add(target);
1480
+ target.dependents.add(demander);
1481
+ }
1482
+ this.#wake(target);
1483
+ if (target.status === "pending" || target.status === "running") this.#pump();
1484
+ if (target.status === "pending" || target.status === "running") {
1485
+ this.#runningCount -= 1;
1486
+ this.#suspendedCount += 1;
1487
+ this.#pump();
1488
+ await new Promise((resume) => {
1489
+ (target.waiters ??= []).push(resume);
1490
+ });
1491
+ await this.#acquire();
1492
+ this.#suspendedCount -= 1;
1493
+ }
1494
+ if (target.status === "fulfilled") return target.result;
1495
+ if (target.status === "rejected") throw target.reason;
1496
+ throw new SkippedError(target.id);
1497
+ }
1498
+ #start(node) {
1499
+ this.#pending.delete(node);
1500
+ node.status = "running";
1501
+ this.#runningCount += 1;
1502
+ const context = {
1503
+ id: node.id,
1504
+ signal: this.#controller?.signal,
1505
+ demand: (handle) => this.#demand(node, handle)
1506
+ };
1507
+ const values = node.arguments_.map((dependency) => dependency.value);
1508
+ Promise.resolve().then(() => node.execute(context, ...values)).then((value) => {
1509
+ this.#runningCount -= 1;
1510
+ this.#settle(node, "fulfilled", value, void 0);
1511
+ this.#pump();
1512
+ }, (reason) => {
1513
+ this.#runningCount -= 1;
1514
+ this.#errors.push(reason);
1515
+ this.#settle(node, "rejected", void 0, reason);
1516
+ this.#pump();
1517
+ });
1518
+ }
1519
+ /**
1520
+ * Settles a node and propagates the consequences to its dependents: a
1521
+ * fulfilled node makes them one dependency readier, a failed or skipped
1522
+ * one skips them in turn. Iterative, so a long chain of skips cannot
1523
+ * overflow the stack.
1524
+ */
1525
+ #settle(node, status, value, reason) {
1526
+ const stack = [{
1527
+ node,
1528
+ status,
1529
+ value,
1530
+ reason
1531
+ }];
1532
+ while (stack.length > 0) {
1533
+ const current = stack.pop();
1534
+ const settled = current.node;
1535
+ if (settled.status !== "pending" && settled.status !== "running") continue;
1536
+ this.#pending.delete(settled);
1537
+ this.#dormant.delete(settled);
1538
+ settled.status = current.status;
1539
+ settled.result = current.value;
1540
+ settled.reason = current.reason;
1541
+ const waiters = settled.waiters;
1542
+ settled.waiters = void 0;
1543
+ waiters?.forEach((resume) => resume());
1544
+ const doomed = current.status === "rejected" || current.status === "skipped";
1545
+ for (const dependent of settled.dependents) {
1546
+ if (doomed) {
1547
+ if (dependent.status === "pending") stack.push({
1548
+ node: dependent,
1549
+ status: "skipped",
1550
+ value: void 0,
1551
+ reason: new SkippedError(dependent.id)
1552
+ });
1553
+ continue;
1554
+ }
1555
+ if (dependent.unresolved > 0) {
1556
+ dependent.unresolved -= 1;
1557
+ this.#enqueue(dependent);
1558
+ }
1559
+ }
1560
+ }
1561
+ }
1562
+ #complete() {
1563
+ const finish = this.#finish;
1564
+ if (!finish) return;
1565
+ for (const node of [...this.#dormant]) this.#settle(node, "skipped", void 0, new SkippedError(node.id));
1566
+ this.#finished = true;
1567
+ this.#finish = void 0;
1568
+ if (this.#controller?.signal.aborted) finish.reject(this.#controller.signal.reason);
1569
+ else if (this.#errors.length > 0) finish.reject(new AggregateError(this.#errors, "One or more graph nodes failed"));
1570
+ else finish.resolve();
1571
+ }
1572
+ };
1573
+ const INTERNALS = /* @__PURE__ */ new WeakMap();
1574
+ function normalize(spec) {
1575
+ return typeof spec === "function" ? { run: spec } : spec;
1576
+ }
1577
+ /**
1578
+ * A hierarchical namespace of kinded nodes. Scopes make phase completion
1579
+ * automatic: scope.completion(kind) fulfills once every node in the scope
1580
+ * (and its descendants) has settled — including nodes that are created
1581
+ * dynamically while the graph runs — without the producers having to wire
1582
+ * any edges themselves.
1583
+ */
1584
+ var Scope = class Scope {
1585
+ name;
1586
+ path;
1587
+ #graph;
1588
+ #parent;
1589
+ #children = /* @__PURE__ */ new Map();
1590
+ #entries = /* @__PURE__ */ new Map();
1591
+ /** Nodes this scope's completions must wait for (kinded nodes + workers). */
1592
+ #members = [];
1593
+ #completions = /* @__PURE__ */ new Map();
1594
+ /** @internal Use graph.scope() or scope.scope() instead. */
1595
+ constructor(graph, parent, name) {
1596
+ this.#graph = graph;
1597
+ this.#parent = parent;
1598
+ this.name = name;
1599
+ this.path = parent && parent.path ? `${parent.path}/${name}` : name;
1600
+ }
1601
+ /** Creates or returns a named child scope. */
1602
+ scope(name) {
1603
+ let child = this.#children.get(name);
1604
+ if (!child) {
1605
+ child = new Scope(this.#graph, this, name);
1606
+ this.#children.set(name, child);
1607
+ }
1608
+ return child;
1609
+ }
1610
+ get(kind, key) {
1611
+ return this.#entries.get(kind.name)?.get(key);
1612
+ }
1613
+ add(kind, key, spec) {
1614
+ if (this.get(kind, key)) throw new Error(`Node "${kind.name}:${key}" already exists in scope "${this.path || "(root)"}"`);
1615
+ return this.#create(kind, key, normalize(spec));
1616
+ }
1617
+ /** Like add(), but returns the existing node if the key is already present. */
1618
+ ensure(kind, key, spec) {
1619
+ return this.get(kind, key) ?? this.#create(kind, key, normalize(spec));
1620
+ }
1621
+ /**
1622
+ * A barrier node that fulfills once this scope is quiescent: every node
1623
+ * in the scope and its descendants has settled, except nodes that
1624
+ * transitively depend on the barrier itself (its consumers). Its value is
1625
+ * the collected values of all `kind` nodes in the scope subtree. After it
1626
+ * fires the scope is sealed: adding non-consumer nodes throws.
1627
+ */
1628
+ completion(kind) {
1629
+ const existing = this.#completions.get(kind.name);
1630
+ if (existing) return existing;
1631
+ const completion = this.#graph.add(`${this.#prefix()}${kind.name}:$completion`, { run: () => this.#collect(kind) });
1632
+ this.#completions.set(kind.name, completion);
1633
+ for (const member of this.#allMembers()) this.#attach(member, completion);
1634
+ return completion;
1635
+ }
1636
+ #prefix() {
1637
+ return this.path ? `${this.path}/` : "";
1638
+ }
1639
+ #create(kind, key, options) {
1640
+ const internals = INTERNALS.get(this.#graph);
1641
+ const dependencies = options.dependencies ?? [];
1642
+ this.#assertOpen(`${kind.name}:${key}`, dependencies);
1643
+ const scope = this;
1644
+ const graph = this.#graph;
1645
+ const id = `${this.#prefix()}${kind.name}:${key}`;
1646
+ const node = graph.add(id, {
1647
+ dependencies,
1648
+ run: (context, ...values) => {
1649
+ const scoped = {
1650
+ id: context.id,
1651
+ key,
1652
+ signal: context.signal,
1653
+ scope,
1654
+ demand: context.demand
1655
+ };
1656
+ const compute = () => options.run(scoped, ...values);
1657
+ const wrappers = internals.wrappers.get(kind.name) ?? [];
1658
+ if (wrappers.length === 0) return compute();
1659
+ let inner = graph.add(`${id}@raw`, {
1660
+ lazy: true,
1661
+ run: () => compute()
1662
+ });
1663
+ for (const registration of wrappers.slice(0, -1)) {
1664
+ const previous = inner;
1665
+ inner = graph.add(`${id}@${registration.name}`, {
1666
+ lazy: true,
1667
+ run: (innerContext) => registration.wrap({
1668
+ ...scoped,
1669
+ id: innerContext.id,
1670
+ demand: innerContext.demand
1671
+ }, () => {
1672
+ return innerContext.demand(previous);
1673
+ })
1674
+ });
1675
+ }
1676
+ return wrappers[wrappers.length - 1].wrap(scoped, () => context.demand(inner));
1677
+ }
1286
1678
  });
1679
+ let byKey = this.#entries.get(kind.name);
1680
+ if (!byKey) {
1681
+ byKey = /* @__PURE__ */ new Map();
1682
+ this.#entries.set(kind.name, byKey);
1683
+ }
1684
+ byKey.set(key, node);
1685
+ this.#members.push(node);
1686
+ for (let ancestor = this; ancestor; ancestor = ancestor.#parent) for (const completion of ancestor.#completions.values()) this.#attach(node, completion);
1687
+ for (const registration of internals.workers.get(kind.name) ?? []) this.#create(registration.kind, key, {
1688
+ dependencies: [node],
1689
+ run: (context, value) => registration.worker(context, value, node)
1690
+ });
1691
+ return node;
1692
+ }
1693
+ /**
1694
+ * Attaches workers registered after some nodes of their kind were
1695
+ * already added. Runs once when the definition phase ends (run()).
1696
+ */
1697
+ static materialize(scope) {
1698
+ const internals = INTERNALS.get(scope.#graph);
1699
+ for (const [kindName, byKey] of scope.#entries) for (const registration of internals.workers.get(kindName) ?? []) for (const [key, node] of [...byKey]) {
1700
+ if (scope.#entries.get(registration.kind.name)?.has(key)) continue;
1701
+ scope.#create(registration.kind, key, {
1702
+ dependencies: [node],
1703
+ run: (context, value) => registration.worker(context, value, node)
1704
+ });
1705
+ }
1706
+ for (const child of scope.#children.values()) Scope.materialize(child);
1287
1707
  }
1288
- dequeue() {
1289
- if (this.pending > 0 && --this.pending === 0) this.resolve?.();
1708
+ /**
1709
+ * A scope seals once a completion fires: new nodes are only allowed if
1710
+ * they are consumers of that completion (depend on it), since the
1711
+ * completion has already reported the scope as done.
1712
+ */
1713
+ #assertOpen(label, dependencies) {
1714
+ for (let ancestor = this; ancestor; ancestor = ancestor.#parent) for (const completion of ancestor.#completions.values()) {
1715
+ if (completion.status === "pending") continue;
1716
+ if (!dependencies.some((dependency) => dependency === completion || this.#graph.dependsOn(dependency, completion))) throw new Error(`Cannot add "${label}" to scope "${ancestor.path || "(root)"}": its completion "${completion.id}" has already fired`);
1717
+ }
1718
+ }
1719
+ #attach(member, completion) {
1720
+ if (completion.status !== "pending") return;
1721
+ if (member === completion || this.#graph.dependsOn(member, completion)) return;
1722
+ this.#graph.connect(member, completion);
1723
+ }
1724
+ #allMembers() {
1725
+ const members = [...this.#members];
1726
+ for (const child of this.#children.values()) members.push(...child.#allMembers());
1727
+ return members;
1728
+ }
1729
+ #collect(kind) {
1730
+ const values = [];
1731
+ const byKey = this.#entries.get(kind.name);
1732
+ if (byKey) {
1733
+ for (const node of byKey.values()) if (node.status === "fulfilled") values.push(node.value);
1734
+ }
1735
+ for (const child of this.#children.values()) values.push(...child.#collect(kind));
1736
+ return values;
1290
1737
  }
1291
1738
  };
1292
1739
  //#endregion
@@ -1308,13 +1755,19 @@ async function getPaths(entrypoint) {
1308
1755
  const paths = glob.isDynamicPattern(pattern) ? await glob(pattern, { onlyFiles: true }) : [entrypoint.entrypoint];
1309
1756
  return new Set(paths.map((path) => resolve(path)));
1310
1757
  }
1758
+ function toRealPath(path) {
1759
+ const abs = resolve(path);
1760
+ try {
1761
+ return realpathSync(abs);
1762
+ } catch {
1763
+ return abs;
1764
+ }
1765
+ }
1311
1766
  async function run(entrypoint, { config, logger }) {
1312
1767
  const destination = entrypoint.destination ?? config.destination;
1313
1768
  const obsolete = entrypoint.obsolete ?? config.obsolete;
1314
1769
  const exclude = entrypoint.exclude ?? config.exclude;
1315
1770
  const walk = entrypoint.walk ?? config.walk;
1316
- const paths = /* @__PURE__ */ new Set();
1317
- const resolved = /* @__PURE__ */ new Set();
1318
1771
  const context = {
1319
1772
  config: {
1320
1773
  ...config,
@@ -1324,110 +1777,139 @@ async function run(entrypoint, { config, logger }) {
1324
1777
  walk
1325
1778
  },
1326
1779
  generatedAt: /* @__PURE__ */ new Date(),
1327
- paths,
1780
+ paths: /* @__PURE__ */ new Set(),
1328
1781
  logger
1329
1782
  };
1330
1783
  logger?.info(entrypoint, "starting extraction");
1331
- const resolvers = [];
1332
1784
  const loaders = [];
1333
1785
  const processors = [];
1334
- const hooks = {
1335
- resolve: resolvers,
1336
- load: loaders,
1337
- process: processors
1338
- };
1339
- const pending = /* @__PURE__ */ new Map();
1340
- const queue = [];
1341
- function getDeferred(namespace) {
1342
- let defer = pending.get(namespace);
1343
- if (defer === void 0) {
1344
- defer = new Defer();
1345
- pending.set(namespace, defer);
1346
- }
1347
- return defer;
1786
+ const collectors = [];
1787
+ const finalizers = [];
1788
+ const failures = [];
1789
+ const failed = /* @__PURE__ */ new Set();
1790
+ /** Records a failure that leaves `scope`'s translations incomplete. */
1791
+ function fail(scope, path, error) {
1792
+ failed.add(scope);
1793
+ record({
1794
+ entrypoint: scope,
1795
+ path
1796
+ }, error);
1348
1797
  }
1349
- function defer(namespace) {
1350
- return getDeferred(namespace).promise;
1798
+ /** Records a failure that no longer has an entrypoint to invalidate. */
1799
+ function record(where, error) {
1800
+ failures.push(error);
1801
+ logger?.error({
1802
+ ...where,
1803
+ error
1804
+ }, "extraction failed");
1351
1805
  }
1352
- function source(path) {
1353
- const abs = resolve(path);
1354
- let resolvedPath;
1806
+ const graph = new Graph();
1807
+ const source = graph.kind("source");
1808
+ const collect = graph.kind("collect");
1809
+ const plan = graph.kind("plan");
1810
+ const output = graph.kind("output");
1811
+ const finalize = graph.kind("finalize");
1812
+ const processed = graph.each(source, "process", async (node, contents) => {
1813
+ if (contents === void 0) return;
1814
+ const path = node.key;
1815
+ let emitted;
1816
+ const args = {
1817
+ entrypoint: node.scope.name,
1818
+ path,
1819
+ contents,
1820
+ emit(translations) {
1821
+ emitted ??= {
1822
+ path,
1823
+ translations: []
1824
+ };
1825
+ emitted.translations.push(...translations);
1826
+ }
1827
+ };
1355
1828
  try {
1356
- resolvedPath = realpathSync(abs);
1357
- } catch {
1358
- resolvedPath = abs;
1829
+ for (const { filter, hook } of processors) {
1830
+ if (!filter.test(path)) continue;
1831
+ if (await hook(args) !== void 0) break;
1832
+ }
1833
+ } catch (error) {
1834
+ fail(node.scope.name, path, error);
1835
+ return;
1359
1836
  }
1360
- if (paths.has(resolvedPath)) return;
1361
- logger?.debug({
1362
- entrypoint: entrypoint.entrypoint,
1363
- path: resolvedPath
1364
- }, "resolved path");
1365
- paths.add(resolvedPath);
1366
- resolve$1({
1367
- entrypoint: resolvedPath,
1368
- path: resolvedPath,
1369
- namespace: "source"
1370
- });
1371
- }
1372
- function resolve$1(args) {
1373
- const { entrypoint, path, namespace } = args;
1374
- const key = `${entrypoint}:${namespace}:${path}`;
1375
- const visited = resolved.has(key);
1376
- const skipped = isExcluded(args, context.config.exclude);
1377
- logger?.debug({
1378
- entrypoint,
1837
+ return emitted;
1838
+ });
1839
+ function addSource(scope, path, importReference) {
1840
+ if (scope.get(source, path)) return;
1841
+ const args = {
1842
+ entrypoint: scope.name,
1379
1843
  path,
1380
- namespace,
1381
- skipped,
1382
- visited
1383
- }, "resolve");
1384
- if (namespace === "source" && visited) return;
1385
- resolved.add(key);
1386
- if (skipped) return;
1387
- queue.push({
1388
- type: "resolve",
1389
- args
1390
- });
1391
- getDeferred(namespace).enqueue();
1392
- }
1393
- function load(args) {
1394
- const { entrypoint, path, namespace } = args;
1844
+ namespace: "source",
1845
+ import: importReference
1846
+ };
1847
+ if (isExcluded(args, context.config.exclude)) {
1848
+ logger?.debug(args, "excluded");
1849
+ return;
1850
+ }
1395
1851
  logger?.debug({
1396
- entrypoint,
1397
- path,
1398
- namespace
1399
- }, "load");
1400
- queue.push({
1401
- type: "load",
1402
- args
1852
+ entrypoint: scope.name,
1853
+ path
1854
+ }, "source");
1855
+ scope.add(source, path, async () => {
1856
+ try {
1857
+ for (const { filter, hook } of loaders) {
1858
+ if (!filter.test(path)) continue;
1859
+ const contents = await hook({
1860
+ entrypoint: scope.name,
1861
+ path
1862
+ });
1863
+ if (contents !== void 0) return contents;
1864
+ }
1865
+ } catch (error) {
1866
+ fail(scope.name, path, error);
1867
+ }
1403
1868
  });
1404
- getDeferred(namespace).enqueue();
1405
1869
  }
1406
- function process(args) {
1407
- const { entrypoint, path, namespace } = args;
1408
- logger?.debug({
1409
- entrypoint,
1410
- path,
1411
- namespace
1412
- }, "process");
1413
- queue.push({
1414
- type: "process",
1415
- args
1870
+ function pipeline(path) {
1871
+ if (context.paths.has(path)) return;
1872
+ context.paths.add(path);
1873
+ const scope = graph.scope(path);
1874
+ addSource(scope, path);
1875
+ const collected = scope.completion(processed);
1876
+ collectors.forEach((hook, index) => {
1877
+ scope.add(collect, String(index), {
1878
+ dependencies: [collected],
1879
+ run: async (_node, files) => {
1880
+ if (failed.has(path)) {
1881
+ logger?.warn({ entrypoint: path }, "skipping outputs: entrypoint failed");
1882
+ return [];
1883
+ }
1884
+ const contributions = [];
1885
+ try {
1886
+ await hook({
1887
+ entrypoint: path,
1888
+ files: files.filter((file) => file !== void 0),
1889
+ output: (outputPath, produce) => {
1890
+ contributions.push({
1891
+ path: outputPath,
1892
+ produce
1893
+ });
1894
+ }
1895
+ });
1896
+ } catch (error) {
1897
+ fail(path, path, error);
1898
+ return [];
1899
+ }
1900
+ return contributions;
1901
+ }
1902
+ });
1416
1903
  });
1417
- getDeferred(namespace).enqueue();
1418
1904
  }
1419
1905
  const build = {
1420
1906
  context,
1421
- source,
1422
- resolve: resolve$1,
1423
- load,
1424
- process,
1425
- defer,
1426
- onResolve(filter, hook) {
1427
- resolvers.push({
1428
- filter,
1429
- hook
1430
- });
1907
+ source({ entrypoint: sourceEntrypoint, path, import: importReference }) {
1908
+ if (sourceEntrypoint === path) {
1909
+ pipeline(toRealPath(path));
1910
+ return;
1911
+ }
1912
+ addSource(graph.scope(sourceEntrypoint), path, importReference);
1431
1913
  },
1432
1914
  onLoad(filter, hook) {
1433
1915
  loaders.push({
@@ -1440,6 +1922,12 @@ async function run(entrypoint, { config, logger }) {
1440
1922
  filter,
1441
1923
  hook
1442
1924
  });
1925
+ },
1926
+ onCollected(hook) {
1927
+ collectors.push(hook);
1928
+ },
1929
+ onOutputs(hook) {
1930
+ finalizers.push(hook);
1443
1931
  }
1444
1932
  };
1445
1933
  for (const item of config.plugins) {
@@ -1447,44 +1935,51 @@ async function run(entrypoint, { config, logger }) {
1447
1935
  logger?.debug({ plugin: plugin.name }, "setting up plugin");
1448
1936
  plugin.setup(build);
1449
1937
  }
1450
- for (const path of await getPaths(entrypoint)) source(path);
1451
- async function processTask(task) {
1452
- const { type } = task;
1453
- let args = task.args;
1454
- const { entrypoint, path, namespace } = args;
1455
- logger?.trace({
1456
- type,
1457
- entrypoint,
1458
- path,
1459
- namespace
1460
- }, "processing task");
1461
- for (const { filter, hook } of hooks[type]) {
1462
- if (filter.namespace !== namespace) continue;
1463
- if (filter.filter && !filter.filter.test(path)) continue;
1464
- const result = await hook(args);
1465
- if (result !== void 0) {
1466
- args = result;
1467
- break;
1938
+ const contributed = graph.root.completion(collect);
1939
+ graph.root.add(plan, "outputs", {
1940
+ dependencies: [contributed],
1941
+ run: (_node, collections) => {
1942
+ const byPath = /* @__PURE__ */ new Map();
1943
+ for (const contribution of collections.flat()) {
1944
+ const group = byPath.get(contribution.path) ?? [];
1945
+ group.push(contribution);
1946
+ byPath.set(contribution.path, group);
1468
1947
  }
1948
+ for (const [outputPath, contributions] of byPath) graph.root.add(output, outputPath, {
1949
+ dependencies: [contributed],
1950
+ run: async () => {
1951
+ try {
1952
+ for (const { produce } of contributions) await produce();
1953
+ } catch (error) {
1954
+ record({ output: outputPath }, error);
1955
+ return;
1956
+ }
1957
+ return outputPath;
1958
+ }
1959
+ });
1469
1960
  }
1470
- if (args !== void 0) {
1471
- if (type === "resolve") load(args);
1472
- else if (type === "load") process(args);
1473
- }
1474
- getDeferred(namespace).dequeue();
1475
- }
1476
- while (queue.length || Array.from(pending.values()).some((d) => d.pending > 0)) {
1477
- while (queue.length) {
1478
- const task = queue.shift();
1479
- if (!task) break;
1480
- await processTask(task);
1481
- }
1482
- await Promise.all(Array.from(pending.values()).map((d) => d.promise));
1483
- await Promise.resolve();
1484
- }
1961
+ });
1962
+ const outputs = graph.root.completion(output);
1963
+ finalizers.forEach((hook, index) => {
1964
+ graph.root.add(finalize, String(index), {
1965
+ dependencies: [outputs],
1966
+ run: async (_node, produced) => {
1967
+ try {
1968
+ await hook({ outputs: produced.filter((path) => path !== void 0) });
1969
+ } catch (error) {
1970
+ record({ finalizer: String(index) }, error);
1971
+ }
1972
+ }
1973
+ });
1974
+ });
1975
+ for (const path of await getPaths(entrypoint)) pipeline(toRealPath(path));
1976
+ await graph.run().catch((error) => {
1977
+ record({ graph: "run" }, error);
1978
+ });
1979
+ if (failures.length > 0) throw new AggregateError(failures, `Extraction of "${entrypoint.entrypoint}" failed`);
1485
1980
  logger?.info(entrypoint, "extraction completed");
1486
1981
  }
1487
1982
  //#endregion
1488
1983
  export { cleanup as a, core as i, react as n, po as r, run as t };
1489
1984
 
1490
- //# sourceMappingURL=run-Bv3fv1N8.mjs.map
1985
+ //# sourceMappingURL=run-CzeTVOug.mjs.map