@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.
@@ -15,7 +15,7 @@ var __copyProps = (to, from, except, desc) => {
15
15
  }
16
16
  return to;
17
17
  };
18
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
19
19
  value: mod,
20
20
  enumerable: true
21
21
  }) : target, mod));
@@ -42,52 +42,25 @@ let node_util = require("node:util");
42
42
  let fast_glob = require("fast-glob");
43
43
  fast_glob = __toESM(fast_glob, 1);
44
44
  //#region src/plugins/cleanup/cleanup.ts
45
- const namespace$2 = "cleanup";
46
45
  function cleanup() {
47
46
  return {
48
47
  name: "cleanup",
49
48
  setup(build) {
50
49
  build.context.logger?.debug("cleanup plugin initialized");
51
- const processed = /* @__PURE__ */ new Set();
52
- const generated = /* @__PURE__ */ new Set();
53
- const dirs = /* @__PURE__ */ new Set();
54
- let dispatched = false;
55
- build.onResolve({
56
- namespace: namespace$2,
57
- filter: /.*/
58
- }, ({ path }) => {
59
- generated.add(path);
60
- dirs.add((0, node_path.dirname)(path));
61
- Promise.all([
62
- build.defer("source"),
63
- build.defer("translate"),
64
- build.defer(namespace$2)
65
- ]).then(() => {
66
- if (dispatched) return;
67
- dispatched = true;
68
- for (const path of dirs.values()) build.process({
69
- entrypoint: path,
70
- path,
71
- namespace: namespace$2,
72
- data: void 0
73
- });
74
- });
75
- });
76
- build.onProcess({
77
- namespace: namespace$2,
78
- filter: /.*/
79
- }, async ({ path }) => {
80
- if (processed.has(path)) return;
81
- processed.add(path);
82
- const files = await node_fs_promises.default.readdir(path).catch(() => []);
83
- for (const f of files.filter((p) => p.endsWith(".po"))) {
84
- const full = (0, node_path.join)(path, f);
85
- const contents = await node_fs_promises.default.readFile(full).catch(() => void 0);
86
- if (!contents) continue;
87
- const parsed = gettext_parser.po.parse(contents);
88
- const hasTranslations = Object.entries(parsed.translations || {}).some(([ctx, msgs]) => Object.keys(msgs).some((id) => !(ctx === "" && id === "")));
89
- if (!hasTranslations && generated.has(full)) await node_fs_promises.default.unlink(full);
90
- if (hasTranslations && !generated.has(full)) build.context.logger?.warn({ path: full }, "stray translation file");
50
+ build.onOutputs(async ({ outputs }) => {
51
+ const generated = new Set(outputs);
52
+ const dirs = new Set(outputs.map((path) => (0, node_path.dirname)(path)));
53
+ for (const dir of dirs) {
54
+ const files = await node_fs_promises.default.readdir(dir).catch(() => []);
55
+ for (const file of files.filter((name) => name.endsWith(".po"))) {
56
+ const full = (0, node_path.join)(dir, file);
57
+ const contents = await node_fs_promises.default.readFile(full).catch(() => void 0);
58
+ if (!contents) continue;
59
+ const parsed = gettext_parser.po.parse(contents);
60
+ const hasTranslations = Object.entries(parsed.translations || {}).some(([ctx, msgs]) => Object.keys(msgs).some((id) => !(ctx === "" && id === "")));
61
+ if (!hasTranslations && generated.has(full)) await node_fs_promises.default.unlink(full);
62
+ if (hasTranslations && !generated.has(full)) build.context.logger?.warn({ path: full }, "stray translation file");
63
+ }
91
64
  }
92
65
  });
93
66
  }
@@ -288,7 +261,7 @@ const messageQuery$1 = notInPlural(withComment({
288
261
  pattern: callPattern("message", messageArgs),
289
262
  extract: extractMessage("message")
290
263
  }));
291
- const allowed$1 = new Set([
264
+ const allowed$1 = /* @__PURE__ */ new Set([
292
265
  "string",
293
266
  "object",
294
267
  "template_string"
@@ -431,7 +404,7 @@ const gettextQuery = withComment({
431
404
  pattern: callPattern("gettext", messageArgs),
432
405
  extract: extractMessage("gettext")
433
406
  });
434
- const allowed = new Set([
407
+ const allowed = /* @__PURE__ */ new Set([
435
408
  "string",
436
409
  "object",
437
410
  "template_string",
@@ -603,7 +576,7 @@ function findTsconfig(dir) {
603
576
  }
604
577
  }
605
578
  const resolverCache = /* @__PURE__ */ new Map();
606
- const builtins = new Set([...node_module.builtinModules, ...node_module.builtinModules.map((name) => `node:${name}`)]);
579
+ const builtins = /* @__PURE__ */ new Set([...node_module.builtinModules, ...node_module.builtinModules.map((name) => `node:${name}`)]);
607
580
  function isBuiltin(spec) {
608
581
  if (builtins.has(spec)) return true;
609
582
  const [base, subpath] = (spec.startsWith("node:") ? spec.slice(5) : spec).split("/", 2);
@@ -680,58 +653,29 @@ function resolveImportResults(file, imports) {
680
653
  //#endregion
681
654
  //#region src/plugins/core/core.ts
682
655
  const filter$1 = /\.([cm]?tsx?|jsx?)$/;
683
- const namespace$1 = "source";
684
656
  function core() {
685
657
  return {
686
658
  name: "core",
687
659
  setup(build) {
688
660
  build.context.logger?.debug("core plugin initialized");
689
- build.onResolve({
690
- filter: filter$1,
691
- namespace: namespace$1
692
- }, ({ entrypoint, path, import: imp, data }) => {
693
- return {
694
- entrypoint,
695
- namespace: namespace$1,
696
- path: (0, node_path.resolve)(path),
697
- import: imp,
698
- data
699
- };
700
- });
701
- build.onLoad({
702
- filter: filter$1,
703
- namespace: namespace$1
704
- }, async ({ entrypoint, path }) => {
705
- return {
706
- entrypoint,
707
- path,
708
- namespace: namespace$1,
709
- data: await (0, node_fs_promises.readFile)(path, "utf8")
710
- };
711
- });
712
- build.onProcess({
713
- filter: filter$1,
714
- namespace: namespace$1
715
- }, ({ entrypoint, path, data }) => {
716
- const result = parseSource$1(data, path);
661
+ build.onLoad(filter$1, ({ path }) => (0, node_fs_promises.readFile)(path, "utf8"));
662
+ build.onProcess(filter$1, ({ entrypoint, path, contents, emit }) => {
663
+ const result = parseSource$1(contents, path);
717
664
  if (result.entrypoint && entrypoint !== path) {
718
- build.source(path);
719
- return {
720
- entrypoint,
721
- path,
722
- namespace: namespace$1,
723
- data: []
724
- };
665
+ build.source({
666
+ entrypoint: path,
667
+ path
668
+ });
669
+ return true;
725
670
  }
726
671
  const { translations, imports, warnings } = result;
727
672
  if (build.context.config.walk) {
728
673
  const { resolved, unresolved } = resolveImportResults(path, imports);
729
674
  for (const result of resolved) {
730
675
  if (build.context.paths.has(result.path)) continue;
731
- build.resolve({
676
+ build.source({
732
677
  entrypoint,
733
678
  path: result.path,
734
- namespace: namespace$1,
735
679
  import: result.import
736
680
  });
737
681
  }
@@ -740,19 +684,14 @@ function core() {
740
684
  if (imp && require_exclude.isExcluded({
741
685
  entrypoint,
742
686
  path: spec,
743
- namespace: namespace$1,
687
+ namespace: "source",
744
688
  import: imp
745
689
  }, build.context.config.exclude)) continue;
746
690
  build.context.logger?.warn(`Unable to resolve import "${spec}" from ${path}${error ? `: ${error}` : ""}`);
747
691
  }
748
692
  }
749
693
  for (const warning of warnings) build.context.logger?.warn(`${warning.error} at ${warning.reference}`);
750
- build.resolve({
751
- entrypoint,
752
- path,
753
- namespace: "translate",
754
- data: translations
755
- });
694
+ emit(translations);
756
695
  });
757
696
  }
758
697
  };
@@ -793,7 +732,7 @@ function collect(source, locale) {
793
732
  }
794
733
  //#endregion
795
734
  //#region src/plugins/po/hasChanges.ts
796
- const IGNORED_HEADER_KEYS = new Set(["pot-creation-date", "po-revision-date"]);
735
+ const IGNORED_HEADER_KEYS = /* @__PURE__ */ new Set(["pot-creation-date", "po-revision-date"]);
797
736
  const IGNORED_HEADER_LINE_PREFIXES = ["pot-creation-date:", "po-revision-date:"];
798
737
  function normalizeHeaderString(value) {
799
738
  const lines = value.split("\n");
@@ -809,7 +748,7 @@ function normalizeHeaderString(value) {
809
748
  if (hadTrailingNewline && filtered[filtered.length - 1] !== "") filtered.push("");
810
749
  return filtered.join("\n");
811
750
  }
812
- function normalize(translations) {
751
+ function normalize$1(translations) {
813
752
  const compiled = gettext_parser.po.compile(translations);
814
753
  const parsed = gettext_parser.po.parse(compiled);
815
754
  if (parsed.headers) {
@@ -823,7 +762,9 @@ function normalize(translations) {
823
762
  }
824
763
  function hasChanges(left, right) {
825
764
  if (!right) return true;
826
- return !(0, node_util.isDeepStrictEqual)(normalize(left), normalize(right));
765
+ const normalizedLeft = normalize$1(left);
766
+ const normalizedRight = normalize$1(right);
767
+ return !(0, node_util.isDeepStrictEqual)(normalizedLeft, normalizedRight);
827
768
  }
828
769
  //#endregion
829
770
  //#region src/plugins/po/merge.ts
@@ -920,74 +861,39 @@ function merge(sources, existing, obsolete, locale, generatedAt) {
920
861
  }
921
862
  //#endregion
922
863
  //#region src/plugins/po/po.ts
923
- const namespace = "translate";
924
864
  function po() {
925
865
  return {
926
866
  name: "po",
927
867
  setup(build) {
928
868
  build.context.logger?.debug("po plugin initialized");
929
869
  const collections = /* @__PURE__ */ new Map();
930
- let dispatched = false;
931
- build.onResolve({
932
- filter: /.*/,
933
- namespace
934
- }, async ({ entrypoint, path, data }) => {
935
- if (!data || !Array.isArray(data)) return;
936
- for (const locale of build.context.config.locales) {
870
+ build.onCollected(({ entrypoint, files, output }) => {
871
+ for (const { path, translations } of files) for (const locale of build.context.config.locales) {
937
872
  const destination = build.context.config.destination({
938
873
  entrypoint,
939
874
  locale,
940
875
  path
941
876
  });
942
- if (!collections.has(destination)) collections.set(destination, {
877
+ const collection = collections.get(destination);
878
+ if (collection) {
879
+ collection.translations.push(...translations);
880
+ continue;
881
+ }
882
+ const created = {
943
883
  locale,
944
- translations: []
945
- });
946
- collections.get(destination)?.translations.push(...data);
947
- }
948
- Promise.all([build.defer("source"), build.defer(namespace)]).then(() => {
949
- if (dispatched) return;
950
- dispatched = true;
951
- for (const path of collections.keys()) build.load({
952
- entrypoint,
953
- path,
954
- namespace
884
+ translations: [...translations]
885
+ };
886
+ collections.set(destination, created);
887
+ output(destination, async () => {
888
+ const contents = await node_fs_promises.default.readFile(destination).catch(() => void 0);
889
+ const existing = contents ? gettext_parser.po.parse(contents) : void 0;
890
+ const out = merge([{ translations: collect(created.translations, created.locale) }], existing, build.context.config.obsolete, created.locale, build.context.generatedAt);
891
+ if (hasChanges(out, existing)) {
892
+ await node_fs_promises.default.mkdir((0, node_path.dirname)(destination), { recursive: true });
893
+ await node_fs_promises.default.writeFile(destination, gettext_parser.po.compile(out));
894
+ }
955
895
  });
956
- });
957
- });
958
- build.onLoad({
959
- filter: /.*\.po$/,
960
- namespace
961
- }, async ({ entrypoint, path }) => {
962
- const contents = await node_fs_promises.default.readFile(path).catch(() => void 0);
963
- return {
964
- entrypoint,
965
- path,
966
- namespace,
967
- data: contents ? gettext_parser.po.parse(contents) : void 0
968
- };
969
- });
970
- build.onProcess({
971
- filter: /.*\.po$/,
972
- namespace
973
- }, async ({ entrypoint, path, data }) => {
974
- const collected = collections.get(path);
975
- if (!collected) {
976
- build.context.logger?.warn({ path }, "no translations collected for this path");
977
- return;
978
- }
979
- const { locale, translations } = collected;
980
- const out = merge([{ translations: collect(translations, locale) }], data, build.context.config.obsolete, locale, build.context.generatedAt);
981
- if (hasChanges(out, data)) {
982
- await node_fs_promises.default.mkdir((0, node_path.dirname)(path), { recursive: true });
983
- await node_fs_promises.default.writeFile(path, gettext_parser.po.compile(out));
984
896
  }
985
- build.resolve({
986
- entrypoint,
987
- path,
988
- namespace: "cleanup",
989
- data: translations
990
- });
991
897
  });
992
898
  }
993
899
  };
@@ -1288,35 +1194,576 @@ function react() {
1288
1194
  name: "react",
1289
1195
  setup(build) {
1290
1196
  build.context.logger?.debug("react plugin initialized");
1291
- build.onProcess({
1292
- filter,
1293
- namespace: "source"
1294
- }, ({ entrypoint, path, data }) => {
1295
- const { translations, warnings } = parseSource(data, path);
1197
+ build.onProcess(filter, ({ path, contents, emit }) => {
1198
+ const { translations, warnings } = parseSource(contents, path);
1296
1199
  for (const warning of warnings) build.context.logger?.warn(`${warning.error} at ${warning.reference}`);
1297
- build.resolve({
1298
- entrypoint,
1299
- path,
1300
- namespace: "translate",
1301
- data: translations
1302
- });
1200
+ emit(translations);
1303
1201
  });
1304
1202
  }
1305
1203
  };
1306
1204
  }
1307
1205
  //#endregion
1308
- //#region src/defer.ts
1309
- var Defer = class {
1310
- pending = 0;
1311
- promise = Promise.resolve();
1312
- resolve;
1313
- enqueue() {
1314
- if (this.pending++ === 0) this.promise = new Promise((res) => {
1315
- this.resolve = res;
1206
+ //#region ../graph/dist/index.mjs
1207
+ var CycleError = class extends Error {
1208
+ constructor(dependency, dependent) {
1209
+ super(`Adding edge "${dependency}" -> "${dependent}" would create a cycle`);
1210
+ this.name = "CycleError";
1211
+ }
1212
+ };
1213
+ var SkippedError = class extends Error {
1214
+ constructor(id) {
1215
+ super(`Node "${id}" was skipped because a dependency failed or the run was aborted`);
1216
+ this.name = "SkippedError";
1217
+ }
1218
+ };
1219
+ var NodeState = class {
1220
+ id;
1221
+ status = "pending";
1222
+ result;
1223
+ reason;
1224
+ lazy = false;
1225
+ /**
1226
+ * Dependencies that have not fulfilled yet. Readiness is maintained as
1227
+ * edges are added and dependencies settle, so scheduling never has to
1228
+ * rescan the pending set.
1229
+ */
1230
+ unresolved = 0;
1231
+ /** Whether the node is already sitting in the ready queue. */
1232
+ queued = false;
1233
+ /** Resumers of demanders suspended on this node, flushed on settle. */
1234
+ waiters;
1235
+ /** Ordered data dependencies, passed as run arguments. */
1236
+ arguments_;
1237
+ /** All dependencies, including ordering-only edges added via connect(). */
1238
+ dependencies = /* @__PURE__ */ new Set();
1239
+ dependents = /* @__PURE__ */ new Set();
1240
+ execute;
1241
+ constructor(id, arguments_, execute) {
1242
+ this.id = id;
1243
+ this.arguments_ = arguments_;
1244
+ this.execute = execute;
1245
+ for (const dependency of arguments_) this.dependencies.add(dependency);
1246
+ }
1247
+ get value() {
1248
+ if (this.status !== "fulfilled") throw new Error(`Node "${this.id}" has no value (status: ${this.status})`);
1249
+ return this.result;
1250
+ }
1251
+ get error() {
1252
+ return this.reason;
1253
+ }
1254
+ };
1255
+ /**
1256
+ * A dynamic, asynchronous DAG of computations.
1257
+ *
1258
+ * Nodes can be added before or during a run, so a running node can discover
1259
+ * and schedule new work. Edges added with {@link Graph.connect} impose
1260
+ * ordering without contributing run arguments, which lets consumers build
1261
+ * barriers: a node that only starts once a dynamically growing set of
1262
+ * predecessors has finished.
1263
+ */
1264
+ var Graph = class {
1265
+ #nodes = /* @__PURE__ */ new Map();
1266
+ #root;
1267
+ #pending = /* @__PURE__ */ new Set();
1268
+ #dormant = /* @__PURE__ */ new Set();
1269
+ /** Pending nodes whose dependencies have all fulfilled. */
1270
+ #ready = [];
1271
+ /** Demanders that finished waiting and need a slot before resuming. */
1272
+ #slotWaiters = [];
1273
+ #runningCount = 0;
1274
+ #suspendedCount = 0;
1275
+ #concurrency = Infinity;
1276
+ #controller;
1277
+ #finish;
1278
+ #started = false;
1279
+ #finished = false;
1280
+ #errors = [];
1281
+ constructor() {
1282
+ INTERNALS.set(this, {
1283
+ kinds: /* @__PURE__ */ new Map(),
1284
+ workers: /* @__PURE__ */ new Map(),
1285
+ wrappers: /* @__PURE__ */ new Map(),
1286
+ frozen: false
1287
+ });
1288
+ this.#root = new Scope(this, void 0, "");
1289
+ }
1290
+ /** The root scope. Nodes added here belong to no named scope. */
1291
+ get root() {
1292
+ return this.#root;
1293
+ }
1294
+ /** Creates or returns a named child scope of the root scope. */
1295
+ scope(name) {
1296
+ return this.#root.scope(name);
1297
+ }
1298
+ /** Declares a typed node kind. Kind names are unique per graph. */
1299
+ kind(name) {
1300
+ const internals = INTERNALS.get(this);
1301
+ if (internals.kinds.has(name)) throw new Error(`Kind "${name}" is already registered`);
1302
+ const kind = { name };
1303
+ internals.kinds.set(name, kind);
1304
+ return kind;
1305
+ }
1306
+ /**
1307
+ * Registers a worker that runs for every node of `source`, in the same
1308
+ * scope as that node. Worker results form their own kind, so they can be
1309
+ * awaited and collected with scope.completion(). Part of the graph's
1310
+ * definition: allowed until run() starts, and applies to nodes added
1311
+ * before the registration as well.
1312
+ */
1313
+ each(source, name, worker) {
1314
+ const internals = INTERNALS.get(this);
1315
+ if (internals.frozen) throw new Error(`Cannot register worker "${name}" after run() started`);
1316
+ const kind = this.kind(name);
1317
+ const registrations = internals.workers.get(source.name) ?? [];
1318
+ registrations.push({
1319
+ kind,
1320
+ worker
1321
+ });
1322
+ internals.workers.set(source.name, registrations);
1323
+ return kind;
1324
+ }
1325
+ /**
1326
+ * Interposes on every node of a kind: consumers of the node receive the
1327
+ * outermost wrapper's value instead of the raw computation. The chain is
1328
+ * materialized as real intermediate nodes ("<id>@raw", "<id>@<name>"),
1329
+ * evaluated on demand: a wrapper that never calls next() (e.g. a cache
1330
+ * hit) keeps the inner nodes from running at all. Wrappers compose; the
1331
+ * last registered one is outermost. Part of the graph's definition:
1332
+ * allowed until run() starts.
1333
+ */
1334
+ wrap(kind, name, wrapper) {
1335
+ const internals = INTERNALS.get(this);
1336
+ if (internals.frozen) throw new Error(`Cannot register wrapper "${name}" after run() started`);
1337
+ const registrations = internals.wrappers.get(kind.name) ?? [];
1338
+ registrations.push({
1339
+ name,
1340
+ wrap: wrapper
1341
+ });
1342
+ internals.wrappers.set(kind.name, registrations);
1343
+ }
1344
+ /** Whether `node` transitively depends on `dependency`. */
1345
+ dependsOn(node, dependency) {
1346
+ const target = this.#resolve(dependency);
1347
+ const stack = [...this.#resolve(node).dependencies];
1348
+ const seen = /* @__PURE__ */ new Set();
1349
+ while (stack.length > 0) {
1350
+ const current = stack.pop();
1351
+ if (current === target) return true;
1352
+ if (seen.has(current)) continue;
1353
+ seen.add(current);
1354
+ stack.push(...current.dependencies);
1355
+ }
1356
+ return false;
1357
+ }
1358
+ add(id, options) {
1359
+ if (this.#finished) throw new Error(`Cannot add node "${id}": the graph has already finished`);
1360
+ if (this.#nodes.has(id)) throw new Error(`Node "${id}" already exists`);
1361
+ const dependencies = (options.dependencies ?? []).map((dependency) => this.#resolve(dependency));
1362
+ const node = new NodeState(id, dependencies, options.run);
1363
+ node.lazy = options.lazy ?? false;
1364
+ let doomed;
1365
+ for (const dependency of dependencies) {
1366
+ dependency.dependents.add(node);
1367
+ if (dependency.status === "rejected" || dependency.status === "skipped") doomed ??= dependency;
1368
+ else if (dependency.status !== "fulfilled") node.unresolved += 1;
1369
+ }
1370
+ this.#nodes.set(id, node);
1371
+ if (node.lazy) this.#dormant.add(node);
1372
+ else {
1373
+ this.#pending.add(node);
1374
+ for (const dependency of dependencies) this.#wake(dependency);
1375
+ }
1376
+ if (doomed) this.#settle(node, "skipped", void 0, new SkippedError(node.id));
1377
+ else if (!node.lazy) this.#enqueue(node);
1378
+ if (this.#started) queueMicrotask(() => this.#pump());
1379
+ return node;
1380
+ }
1381
+ /**
1382
+ * Adds an ordering-only edge: `dependent` will not start until
1383
+ * `dependency` has fulfilled. The dependency's value is not passed to the
1384
+ * dependent's run function; read it from the node handle if needed.
1385
+ */
1386
+ connect(dependency, dependent) {
1387
+ const from = this.#resolve(dependency);
1388
+ const to = this.#resolve(dependent);
1389
+ if (to.status !== "pending") throw new Error(`Cannot add dependency to node "${to.id}": it has already started`);
1390
+ if (from === to || this.#reaches(to, from)) throw new CycleError(from.id, to.id);
1391
+ if (to.dependencies.has(from)) return;
1392
+ to.dependencies.add(from);
1393
+ from.dependents.add(to);
1394
+ if (from.status === "rejected" || from.status === "skipped") {
1395
+ this.#settle(to, "skipped", void 0, new SkippedError(to.id));
1396
+ return;
1397
+ }
1398
+ if (from.status !== "fulfilled") to.unresolved += 1;
1399
+ if (!this.#dormant.has(to)) this.#wake(from);
1400
+ if (this.#started) queueMicrotask(() => this.#pump());
1401
+ }
1402
+ get(id) {
1403
+ return this.#nodes.get(id);
1404
+ }
1405
+ get nodes() {
1406
+ return this.#nodes;
1407
+ }
1408
+ /**
1409
+ * Runs the graph until every node has settled, including nodes added
1410
+ * while running. Rejects with an AggregateError if any node rejected, or
1411
+ * with the abort reason if the signal aborted. Can only be called once.
1412
+ */
1413
+ async run(options = {}) {
1414
+ if (this.#started) throw new Error("Graph is already running or has finished");
1415
+ Scope.materialize(this.#root);
1416
+ INTERNALS.get(this).frozen = true;
1417
+ this.#started = true;
1418
+ this.#concurrency = options.concurrency ?? Infinity;
1419
+ if (this.#concurrency < 1) throw new Error("concurrency must be at least 1");
1420
+ this.#controller = new AbortController();
1421
+ const signal = options.signal;
1422
+ if (signal) if (signal.aborted) this.#controller.abort(signal.reason);
1423
+ else signal.addEventListener("abort", () => {
1424
+ this.#controller?.abort(signal.reason);
1425
+ this.#pump();
1426
+ });
1427
+ return new Promise((resolve, reject) => {
1428
+ this.#finish = {
1429
+ resolve,
1430
+ reject
1431
+ };
1432
+ this.#pump();
1433
+ });
1434
+ }
1435
+ #resolve(handle) {
1436
+ const node = this.#nodes.get(handle.id);
1437
+ if (!node || node !== handle) throw new Error(`Node "${handle.id}" does not belong to this graph`);
1438
+ return node;
1439
+ }
1440
+ #reaches(from, target) {
1441
+ const stack = [...from.dependents];
1442
+ const seen = /* @__PURE__ */ new Set();
1443
+ while (stack.length > 0) {
1444
+ const node = stack.pop();
1445
+ if (node === target) return true;
1446
+ if (seen.has(node)) continue;
1447
+ seen.add(node);
1448
+ stack.push(...node.dependents);
1449
+ }
1450
+ return false;
1451
+ }
1452
+ /** Offers a pending node to the scheduler once its dependencies are in. */
1453
+ #enqueue(node) {
1454
+ if (node.queued || node.unresolved > 0 || node.status !== "pending" || this.#dormant.has(node)) return;
1455
+ node.queued = true;
1456
+ this.#ready.push(node);
1457
+ }
1458
+ #pump() {
1459
+ if (!this.#started || this.#finished || !this.#finish) return;
1460
+ if (this.#controller?.signal.aborted) {
1461
+ while (this.#slotWaiters.length > 0) {
1462
+ this.#runningCount += 1;
1463
+ this.#slotWaiters.shift()();
1464
+ }
1465
+ for (const node of [...this.#pending]) this.#settle(node, "skipped", void 0, new SkippedError(node.id));
1466
+ } else {
1467
+ while (this.#slotWaiters.length > 0 && this.#runningCount < this.#concurrency) {
1468
+ this.#runningCount += 1;
1469
+ this.#slotWaiters.shift()();
1470
+ }
1471
+ while (this.#ready.length > 0 && this.#runningCount < this.#concurrency) {
1472
+ const node = this.#ready.shift();
1473
+ node.queued = false;
1474
+ if (node.unresolved === 0 && node.status === "pending") this.#start(node);
1475
+ }
1476
+ }
1477
+ if (this.#pending.size === 0 && this.#runningCount === 0 && this.#suspendedCount === 0) this.#complete();
1478
+ }
1479
+ /** Moves a dormant lazy node (and its dormant dependencies) into scheduling. */
1480
+ #wake(node) {
1481
+ const stack = [node];
1482
+ while (stack.length > 0) {
1483
+ const current = stack.pop();
1484
+ if (!this.#dormant.delete(current)) continue;
1485
+ this.#pending.add(current);
1486
+ this.#enqueue(current);
1487
+ stack.push(...current.dependencies);
1488
+ }
1489
+ }
1490
+ /**
1491
+ * Takes a concurrency slot, waiting for one when the graph is at its
1492
+ * limit. The slot is reserved by #pump at hand-off, so a granted slot
1493
+ * cannot be taken by anything else in the meantime.
1494
+ */
1495
+ #acquire() {
1496
+ if (this.#runningCount < this.#concurrency) {
1497
+ this.#runningCount += 1;
1498
+ return;
1499
+ }
1500
+ return new Promise((resume) => {
1501
+ this.#slotWaiters.push(resume);
1502
+ });
1503
+ }
1504
+ async #demand(demander, handle) {
1505
+ 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`);
1506
+ const target = this.#resolve(handle);
1507
+ if (target === demander || this.#reaches(demander, target)) throw new CycleError(target.id, demander.id);
1508
+ if (!demander.dependencies.has(target)) {
1509
+ demander.dependencies.add(target);
1510
+ target.dependents.add(demander);
1511
+ }
1512
+ this.#wake(target);
1513
+ if (target.status === "pending" || target.status === "running") this.#pump();
1514
+ if (target.status === "pending" || target.status === "running") {
1515
+ this.#runningCount -= 1;
1516
+ this.#suspendedCount += 1;
1517
+ this.#pump();
1518
+ await new Promise((resume) => {
1519
+ (target.waiters ??= []).push(resume);
1520
+ });
1521
+ await this.#acquire();
1522
+ this.#suspendedCount -= 1;
1523
+ }
1524
+ if (target.status === "fulfilled") return target.result;
1525
+ if (target.status === "rejected") throw target.reason;
1526
+ throw new SkippedError(target.id);
1527
+ }
1528
+ #start(node) {
1529
+ this.#pending.delete(node);
1530
+ node.status = "running";
1531
+ this.#runningCount += 1;
1532
+ const context = {
1533
+ id: node.id,
1534
+ signal: this.#controller?.signal,
1535
+ demand: (handle) => this.#demand(node, handle)
1536
+ };
1537
+ const values = node.arguments_.map((dependency) => dependency.value);
1538
+ Promise.resolve().then(() => node.execute(context, ...values)).then((value) => {
1539
+ this.#runningCount -= 1;
1540
+ this.#settle(node, "fulfilled", value, void 0);
1541
+ this.#pump();
1542
+ }, (reason) => {
1543
+ this.#runningCount -= 1;
1544
+ this.#errors.push(reason);
1545
+ this.#settle(node, "rejected", void 0, reason);
1546
+ this.#pump();
1547
+ });
1548
+ }
1549
+ /**
1550
+ * Settles a node and propagates the consequences to its dependents: a
1551
+ * fulfilled node makes them one dependency readier, a failed or skipped
1552
+ * one skips them in turn. Iterative, so a long chain of skips cannot
1553
+ * overflow the stack.
1554
+ */
1555
+ #settle(node, status, value, reason) {
1556
+ const stack = [{
1557
+ node,
1558
+ status,
1559
+ value,
1560
+ reason
1561
+ }];
1562
+ while (stack.length > 0) {
1563
+ const current = stack.pop();
1564
+ const settled = current.node;
1565
+ if (settled.status !== "pending" && settled.status !== "running") continue;
1566
+ this.#pending.delete(settled);
1567
+ this.#dormant.delete(settled);
1568
+ settled.status = current.status;
1569
+ settled.result = current.value;
1570
+ settled.reason = current.reason;
1571
+ const waiters = settled.waiters;
1572
+ settled.waiters = void 0;
1573
+ waiters?.forEach((resume) => resume());
1574
+ const doomed = current.status === "rejected" || current.status === "skipped";
1575
+ for (const dependent of settled.dependents) {
1576
+ if (doomed) {
1577
+ if (dependent.status === "pending") stack.push({
1578
+ node: dependent,
1579
+ status: "skipped",
1580
+ value: void 0,
1581
+ reason: new SkippedError(dependent.id)
1582
+ });
1583
+ continue;
1584
+ }
1585
+ if (dependent.unresolved > 0) {
1586
+ dependent.unresolved -= 1;
1587
+ this.#enqueue(dependent);
1588
+ }
1589
+ }
1590
+ }
1591
+ }
1592
+ #complete() {
1593
+ const finish = this.#finish;
1594
+ if (!finish) return;
1595
+ for (const node of [...this.#dormant]) this.#settle(node, "skipped", void 0, new SkippedError(node.id));
1596
+ this.#finished = true;
1597
+ this.#finish = void 0;
1598
+ if (this.#controller?.signal.aborted) finish.reject(this.#controller.signal.reason);
1599
+ else if (this.#errors.length > 0) finish.reject(new AggregateError(this.#errors, "One or more graph nodes failed"));
1600
+ else finish.resolve();
1601
+ }
1602
+ };
1603
+ const INTERNALS = /* @__PURE__ */ new WeakMap();
1604
+ function normalize(spec) {
1605
+ return typeof spec === "function" ? { run: spec } : spec;
1606
+ }
1607
+ /**
1608
+ * A hierarchical namespace of kinded nodes. Scopes make phase completion
1609
+ * automatic: scope.completion(kind) fulfills once every node in the scope
1610
+ * (and its descendants) has settled — including nodes that are created
1611
+ * dynamically while the graph runs — without the producers having to wire
1612
+ * any edges themselves.
1613
+ */
1614
+ var Scope = class Scope {
1615
+ name;
1616
+ path;
1617
+ #graph;
1618
+ #parent;
1619
+ #children = /* @__PURE__ */ new Map();
1620
+ #entries = /* @__PURE__ */ new Map();
1621
+ /** Nodes this scope's completions must wait for (kinded nodes + workers). */
1622
+ #members = [];
1623
+ #completions = /* @__PURE__ */ new Map();
1624
+ /** @internal Use graph.scope() or scope.scope() instead. */
1625
+ constructor(graph, parent, name) {
1626
+ this.#graph = graph;
1627
+ this.#parent = parent;
1628
+ this.name = name;
1629
+ this.path = parent && parent.path ? `${parent.path}/${name}` : name;
1630
+ }
1631
+ /** Creates or returns a named child scope. */
1632
+ scope(name) {
1633
+ let child = this.#children.get(name);
1634
+ if (!child) {
1635
+ child = new Scope(this.#graph, this, name);
1636
+ this.#children.set(name, child);
1637
+ }
1638
+ return child;
1639
+ }
1640
+ get(kind, key) {
1641
+ return this.#entries.get(kind.name)?.get(key);
1642
+ }
1643
+ add(kind, key, spec) {
1644
+ if (this.get(kind, key)) throw new Error(`Node "${kind.name}:${key}" already exists in scope "${this.path || "(root)"}"`);
1645
+ return this.#create(kind, key, normalize(spec));
1646
+ }
1647
+ /** Like add(), but returns the existing node if the key is already present. */
1648
+ ensure(kind, key, spec) {
1649
+ return this.get(kind, key) ?? this.#create(kind, key, normalize(spec));
1650
+ }
1651
+ /**
1652
+ * A barrier node that fulfills once this scope is quiescent: every node
1653
+ * in the scope and its descendants has settled, except nodes that
1654
+ * transitively depend on the barrier itself (its consumers). Its value is
1655
+ * the collected values of all `kind` nodes in the scope subtree. After it
1656
+ * fires the scope is sealed: adding non-consumer nodes throws.
1657
+ */
1658
+ completion(kind) {
1659
+ const existing = this.#completions.get(kind.name);
1660
+ if (existing) return existing;
1661
+ const completion = this.#graph.add(`${this.#prefix()}${kind.name}:$completion`, { run: () => this.#collect(kind) });
1662
+ this.#completions.set(kind.name, completion);
1663
+ for (const member of this.#allMembers()) this.#attach(member, completion);
1664
+ return completion;
1665
+ }
1666
+ #prefix() {
1667
+ return this.path ? `${this.path}/` : "";
1668
+ }
1669
+ #create(kind, key, options) {
1670
+ const internals = INTERNALS.get(this.#graph);
1671
+ const dependencies = options.dependencies ?? [];
1672
+ this.#assertOpen(`${kind.name}:${key}`, dependencies);
1673
+ const scope = this;
1674
+ const graph = this.#graph;
1675
+ const id = `${this.#prefix()}${kind.name}:${key}`;
1676
+ const node = graph.add(id, {
1677
+ dependencies,
1678
+ run: (context, ...values) => {
1679
+ const scoped = {
1680
+ id: context.id,
1681
+ key,
1682
+ signal: context.signal,
1683
+ scope,
1684
+ demand: context.demand
1685
+ };
1686
+ const compute = () => options.run(scoped, ...values);
1687
+ const wrappers = internals.wrappers.get(kind.name) ?? [];
1688
+ if (wrappers.length === 0) return compute();
1689
+ let inner = graph.add(`${id}@raw`, {
1690
+ lazy: true,
1691
+ run: () => compute()
1692
+ });
1693
+ for (const registration of wrappers.slice(0, -1)) {
1694
+ const previous = inner;
1695
+ inner = graph.add(`${id}@${registration.name}`, {
1696
+ lazy: true,
1697
+ run: (innerContext) => registration.wrap({
1698
+ ...scoped,
1699
+ id: innerContext.id,
1700
+ demand: innerContext.demand
1701
+ }, () => {
1702
+ return innerContext.demand(previous);
1703
+ })
1704
+ });
1705
+ }
1706
+ return wrappers[wrappers.length - 1].wrap(scoped, () => context.demand(inner));
1707
+ }
1316
1708
  });
1709
+ let byKey = this.#entries.get(kind.name);
1710
+ if (!byKey) {
1711
+ byKey = /* @__PURE__ */ new Map();
1712
+ this.#entries.set(kind.name, byKey);
1713
+ }
1714
+ byKey.set(key, node);
1715
+ this.#members.push(node);
1716
+ for (let ancestor = this; ancestor; ancestor = ancestor.#parent) for (const completion of ancestor.#completions.values()) this.#attach(node, completion);
1717
+ for (const registration of internals.workers.get(kind.name) ?? []) this.#create(registration.kind, key, {
1718
+ dependencies: [node],
1719
+ run: (context, value) => registration.worker(context, value, node)
1720
+ });
1721
+ return node;
1722
+ }
1723
+ /**
1724
+ * Attaches workers registered after some nodes of their kind were
1725
+ * already added. Runs once when the definition phase ends (run()).
1726
+ */
1727
+ static materialize(scope) {
1728
+ const internals = INTERNALS.get(scope.#graph);
1729
+ for (const [kindName, byKey] of scope.#entries) for (const registration of internals.workers.get(kindName) ?? []) for (const [key, node] of [...byKey]) {
1730
+ if (scope.#entries.get(registration.kind.name)?.has(key)) continue;
1731
+ scope.#create(registration.kind, key, {
1732
+ dependencies: [node],
1733
+ run: (context, value) => registration.worker(context, value, node)
1734
+ });
1735
+ }
1736
+ for (const child of scope.#children.values()) Scope.materialize(child);
1317
1737
  }
1318
- dequeue() {
1319
- if (this.pending > 0 && --this.pending === 0) this.resolve?.();
1738
+ /**
1739
+ * A scope seals once a completion fires: new nodes are only allowed if
1740
+ * they are consumers of that completion (depend on it), since the
1741
+ * completion has already reported the scope as done.
1742
+ */
1743
+ #assertOpen(label, dependencies) {
1744
+ for (let ancestor = this; ancestor; ancestor = ancestor.#parent) for (const completion of ancestor.#completions.values()) {
1745
+ if (completion.status === "pending") continue;
1746
+ 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`);
1747
+ }
1748
+ }
1749
+ #attach(member, completion) {
1750
+ if (completion.status !== "pending") return;
1751
+ if (member === completion || this.#graph.dependsOn(member, completion)) return;
1752
+ this.#graph.connect(member, completion);
1753
+ }
1754
+ #allMembers() {
1755
+ const members = [...this.#members];
1756
+ for (const child of this.#children.values()) members.push(...child.#allMembers());
1757
+ return members;
1758
+ }
1759
+ #collect(kind) {
1760
+ const values = [];
1761
+ const byKey = this.#entries.get(kind.name);
1762
+ if (byKey) {
1763
+ for (const node of byKey.values()) if (node.status === "fulfilled") values.push(node.value);
1764
+ }
1765
+ for (const child of this.#children.values()) values.push(...child.#collect(kind));
1766
+ return values;
1320
1767
  }
1321
1768
  };
1322
1769
  //#endregion
@@ -1338,13 +1785,19 @@ async function getPaths(entrypoint) {
1338
1785
  const paths = fast_glob.default.isDynamicPattern(pattern) ? await (0, fast_glob.default)(pattern, { onlyFiles: true }) : [entrypoint.entrypoint];
1339
1786
  return new Set(paths.map((path) => (0, node_path.resolve)(path)));
1340
1787
  }
1788
+ function toRealPath(path) {
1789
+ const abs = (0, node_path.resolve)(path);
1790
+ try {
1791
+ return (0, node_fs.realpathSync)(abs);
1792
+ } catch {
1793
+ return abs;
1794
+ }
1795
+ }
1341
1796
  async function run(entrypoint, { config, logger }) {
1342
1797
  const destination = entrypoint.destination ?? config.destination;
1343
1798
  const obsolete = entrypoint.obsolete ?? config.obsolete;
1344
1799
  const exclude = entrypoint.exclude ?? config.exclude;
1345
1800
  const walk = entrypoint.walk ?? config.walk;
1346
- const paths = /* @__PURE__ */ new Set();
1347
- const resolved = /* @__PURE__ */ new Set();
1348
1801
  const context = {
1349
1802
  config: {
1350
1803
  ...config,
@@ -1354,110 +1807,139 @@ async function run(entrypoint, { config, logger }) {
1354
1807
  walk
1355
1808
  },
1356
1809
  generatedAt: /* @__PURE__ */ new Date(),
1357
- paths,
1810
+ paths: /* @__PURE__ */ new Set(),
1358
1811
  logger
1359
1812
  };
1360
1813
  logger?.info(entrypoint, "starting extraction");
1361
- const resolvers = [];
1362
1814
  const loaders = [];
1363
1815
  const processors = [];
1364
- const hooks = {
1365
- resolve: resolvers,
1366
- load: loaders,
1367
- process: processors
1368
- };
1369
- const pending = /* @__PURE__ */ new Map();
1370
- const queue = [];
1371
- function getDeferred(namespace) {
1372
- let defer = pending.get(namespace);
1373
- if (defer === void 0) {
1374
- defer = new Defer();
1375
- pending.set(namespace, defer);
1376
- }
1377
- return defer;
1816
+ const collectors = [];
1817
+ const finalizers = [];
1818
+ const failures = [];
1819
+ const failed = /* @__PURE__ */ new Set();
1820
+ /** Records a failure that leaves `scope`'s translations incomplete. */
1821
+ function fail(scope, path, error) {
1822
+ failed.add(scope);
1823
+ record({
1824
+ entrypoint: scope,
1825
+ path
1826
+ }, error);
1378
1827
  }
1379
- function defer(namespace) {
1380
- return getDeferred(namespace).promise;
1828
+ /** Records a failure that no longer has an entrypoint to invalidate. */
1829
+ function record(where, error) {
1830
+ failures.push(error);
1831
+ logger?.error({
1832
+ ...where,
1833
+ error
1834
+ }, "extraction failed");
1381
1835
  }
1382
- function source(path) {
1383
- const abs = (0, node_path.resolve)(path);
1384
- let resolvedPath;
1836
+ const graph = new Graph();
1837
+ const source = graph.kind("source");
1838
+ const collect = graph.kind("collect");
1839
+ const plan = graph.kind("plan");
1840
+ const output = graph.kind("output");
1841
+ const finalize = graph.kind("finalize");
1842
+ const processed = graph.each(source, "process", async (node, contents) => {
1843
+ if (contents === void 0) return;
1844
+ const path = node.key;
1845
+ let emitted;
1846
+ const args = {
1847
+ entrypoint: node.scope.name,
1848
+ path,
1849
+ contents,
1850
+ emit(translations) {
1851
+ emitted ??= {
1852
+ path,
1853
+ translations: []
1854
+ };
1855
+ emitted.translations.push(...translations);
1856
+ }
1857
+ };
1385
1858
  try {
1386
- resolvedPath = (0, node_fs.realpathSync)(abs);
1387
- } catch {
1388
- resolvedPath = abs;
1859
+ for (const { filter, hook } of processors) {
1860
+ if (!filter.test(path)) continue;
1861
+ if (await hook(args) !== void 0) break;
1862
+ }
1863
+ } catch (error) {
1864
+ fail(node.scope.name, path, error);
1865
+ return;
1389
1866
  }
1390
- if (paths.has(resolvedPath)) return;
1391
- logger?.debug({
1392
- entrypoint: entrypoint.entrypoint,
1393
- path: resolvedPath
1394
- }, "resolved path");
1395
- paths.add(resolvedPath);
1396
- resolve({
1397
- entrypoint: resolvedPath,
1398
- path: resolvedPath,
1399
- namespace: "source"
1400
- });
1401
- }
1402
- function resolve(args) {
1403
- const { entrypoint, path, namespace } = args;
1404
- const key = `${entrypoint}:${namespace}:${path}`;
1405
- const visited = resolved.has(key);
1406
- const skipped = require_exclude.isExcluded(args, context.config.exclude);
1407
- logger?.debug({
1408
- entrypoint,
1867
+ return emitted;
1868
+ });
1869
+ function addSource(scope, path, importReference) {
1870
+ if (scope.get(source, path)) return;
1871
+ const args = {
1872
+ entrypoint: scope.name,
1409
1873
  path,
1410
- namespace,
1411
- skipped,
1412
- visited
1413
- }, "resolve");
1414
- if (namespace === "source" && visited) return;
1415
- resolved.add(key);
1416
- if (skipped) return;
1417
- queue.push({
1418
- type: "resolve",
1419
- args
1420
- });
1421
- getDeferred(namespace).enqueue();
1422
- }
1423
- function load(args) {
1424
- const { entrypoint, path, namespace } = args;
1874
+ namespace: "source",
1875
+ import: importReference
1876
+ };
1877
+ if (require_exclude.isExcluded(args, context.config.exclude)) {
1878
+ logger?.debug(args, "excluded");
1879
+ return;
1880
+ }
1425
1881
  logger?.debug({
1426
- entrypoint,
1427
- path,
1428
- namespace
1429
- }, "load");
1430
- queue.push({
1431
- type: "load",
1432
- args
1882
+ entrypoint: scope.name,
1883
+ path
1884
+ }, "source");
1885
+ scope.add(source, path, async () => {
1886
+ try {
1887
+ for (const { filter, hook } of loaders) {
1888
+ if (!filter.test(path)) continue;
1889
+ const contents = await hook({
1890
+ entrypoint: scope.name,
1891
+ path
1892
+ });
1893
+ if (contents !== void 0) return contents;
1894
+ }
1895
+ } catch (error) {
1896
+ fail(scope.name, path, error);
1897
+ }
1433
1898
  });
1434
- getDeferred(namespace).enqueue();
1435
1899
  }
1436
- function process(args) {
1437
- const { entrypoint, path, namespace } = args;
1438
- logger?.debug({
1439
- entrypoint,
1440
- path,
1441
- namespace
1442
- }, "process");
1443
- queue.push({
1444
- type: "process",
1445
- args
1900
+ function pipeline(path) {
1901
+ if (context.paths.has(path)) return;
1902
+ context.paths.add(path);
1903
+ const scope = graph.scope(path);
1904
+ addSource(scope, path);
1905
+ const collected = scope.completion(processed);
1906
+ collectors.forEach((hook, index) => {
1907
+ scope.add(collect, String(index), {
1908
+ dependencies: [collected],
1909
+ run: async (_node, files) => {
1910
+ if (failed.has(path)) {
1911
+ logger?.warn({ entrypoint: path }, "skipping outputs: entrypoint failed");
1912
+ return [];
1913
+ }
1914
+ const contributions = [];
1915
+ try {
1916
+ await hook({
1917
+ entrypoint: path,
1918
+ files: files.filter((file) => file !== void 0),
1919
+ output: (outputPath, produce) => {
1920
+ contributions.push({
1921
+ path: outputPath,
1922
+ produce
1923
+ });
1924
+ }
1925
+ });
1926
+ } catch (error) {
1927
+ fail(path, path, error);
1928
+ return [];
1929
+ }
1930
+ return contributions;
1931
+ }
1932
+ });
1446
1933
  });
1447
- getDeferred(namespace).enqueue();
1448
1934
  }
1449
1935
  const build = {
1450
1936
  context,
1451
- source,
1452
- resolve,
1453
- load,
1454
- process,
1455
- defer,
1456
- onResolve(filter, hook) {
1457
- resolvers.push({
1458
- filter,
1459
- hook
1460
- });
1937
+ source({ entrypoint: sourceEntrypoint, path, import: importReference }) {
1938
+ if (sourceEntrypoint === path) {
1939
+ pipeline(toRealPath(path));
1940
+ return;
1941
+ }
1942
+ addSource(graph.scope(sourceEntrypoint), path, importReference);
1461
1943
  },
1462
1944
  onLoad(filter, hook) {
1463
1945
  loaders.push({
@@ -1470,6 +1952,12 @@ async function run(entrypoint, { config, logger }) {
1470
1952
  filter,
1471
1953
  hook
1472
1954
  });
1955
+ },
1956
+ onCollected(hook) {
1957
+ collectors.push(hook);
1958
+ },
1959
+ onOutputs(hook) {
1960
+ finalizers.push(hook);
1473
1961
  }
1474
1962
  };
1475
1963
  for (const item of config.plugins) {
@@ -1477,41 +1965,48 @@ async function run(entrypoint, { config, logger }) {
1477
1965
  logger?.debug({ plugin: plugin.name }, "setting up plugin");
1478
1966
  plugin.setup(build);
1479
1967
  }
1480
- for (const path of await getPaths(entrypoint)) source(path);
1481
- async function processTask(task) {
1482
- const { type } = task;
1483
- let args = task.args;
1484
- const { entrypoint, path, namespace } = args;
1485
- logger?.trace({
1486
- type,
1487
- entrypoint,
1488
- path,
1489
- namespace
1490
- }, "processing task");
1491
- for (const { filter, hook } of hooks[type]) {
1492
- if (filter.namespace !== namespace) continue;
1493
- if (filter.filter && !filter.filter.test(path)) continue;
1494
- const result = await hook(args);
1495
- if (result !== void 0) {
1496
- args = result;
1497
- break;
1968
+ const contributed = graph.root.completion(collect);
1969
+ graph.root.add(plan, "outputs", {
1970
+ dependencies: [contributed],
1971
+ run: (_node, collections) => {
1972
+ const byPath = /* @__PURE__ */ new Map();
1973
+ for (const contribution of collections.flat()) {
1974
+ const group = byPath.get(contribution.path) ?? [];
1975
+ group.push(contribution);
1976
+ byPath.set(contribution.path, group);
1498
1977
  }
1978
+ for (const [outputPath, contributions] of byPath) graph.root.add(output, outputPath, {
1979
+ dependencies: [contributed],
1980
+ run: async () => {
1981
+ try {
1982
+ for (const { produce } of contributions) await produce();
1983
+ } catch (error) {
1984
+ record({ output: outputPath }, error);
1985
+ return;
1986
+ }
1987
+ return outputPath;
1988
+ }
1989
+ });
1499
1990
  }
1500
- if (args !== void 0) {
1501
- if (type === "resolve") load(args);
1502
- else if (type === "load") process(args);
1503
- }
1504
- getDeferred(namespace).dequeue();
1505
- }
1506
- while (queue.length || Array.from(pending.values()).some((d) => d.pending > 0)) {
1507
- while (queue.length) {
1508
- const task = queue.shift();
1509
- if (!task) break;
1510
- await processTask(task);
1511
- }
1512
- await Promise.all(Array.from(pending.values()).map((d) => d.promise));
1513
- await Promise.resolve();
1514
- }
1991
+ });
1992
+ const outputs = graph.root.completion(output);
1993
+ finalizers.forEach((hook, index) => {
1994
+ graph.root.add(finalize, String(index), {
1995
+ dependencies: [outputs],
1996
+ run: async (_node, produced) => {
1997
+ try {
1998
+ await hook({ outputs: produced.filter((path) => path !== void 0) });
1999
+ } catch (error) {
2000
+ record({ finalizer: String(index) }, error);
2001
+ }
2002
+ }
2003
+ });
2004
+ });
2005
+ for (const path of await getPaths(entrypoint)) pipeline(toRealPath(path));
2006
+ await graph.run().catch((error) => {
2007
+ record({ graph: "run" }, error);
2008
+ });
2009
+ if (failures.length > 0) throw new AggregateError(failures, `Extraction of "${entrypoint.entrypoint}" failed`);
1515
2010
  logger?.info(entrypoint, "extraction completed");
1516
2011
  }
1517
2012
  //#endregion