@patterkit/compiler 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,6 +24,8 @@ __export(index_exports, {
24
24
  PATTER_SCOPE: () => PATTER_SCOPE,
25
25
  compileExpression: () => compileExpression,
26
26
  exportBundle: () => exportBundle,
27
+ extensionNodes: () => extensionNodes,
28
+ extensionPass: () => extensionPass,
27
29
  externalGameScopes: () => externalGameScopes,
28
30
  hostScopesToSpec: () => import_dialect4.hostScopesToSpec,
29
31
  projectScopes: () => projectScopes,
@@ -53,14 +55,14 @@ function sameScope(a, b) {
53
55
  return norm(a) === norm(b);
54
56
  }
55
57
  function projectScopes(project, merged) {
56
- const own = project.scopeRegistry;
57
- if (!merged) return { host: own, external: [], notes: [] };
58
+ const own2 = project.scopeRegistry;
59
+ if (!merged) return { host: own2, external: [], notes: [] };
58
60
  const notes = [];
59
61
  const hosted = [];
60
62
  const bundled = /* @__PURE__ */ new Set();
61
63
  let changed = false;
62
64
  const shared = (token) => token === PATTER_SCOPE ? void 0 : merged.spec.scopes.find((s) => s.token === token);
63
- for (const h of own?.scopes ?? []) {
65
+ for (const h of own2?.scopes ?? []) {
64
66
  const s = shared(h.token);
65
67
  const who = merged.owners.get(h.token);
66
68
  if (!s || !who) {
@@ -96,7 +98,7 @@ function projectScopes(project, merged) {
96
98
  changed = true;
97
99
  }
98
100
  const external = merged.spec.scopes.filter((s) => s.token !== PATTER_SCOPE && !bundled.has(s.token));
99
- const host = !changed ? own : hosted.length ? { version: own?.version ?? 1, scopes: hosted } : void 0;
101
+ const host = !changed ? own2 : hosted.length ? { version: own2?.version ?? 1, scopes: hosted } : void 0;
100
102
  return { host, external, notes };
101
103
  }
102
104
  function externalGameScopes(strict, merged) {
@@ -349,11 +351,284 @@ var import_dialect4 = require("@patterkit/dialect");
349
351
  var import_scoperegistry = require("@wildwinter/scoperegistry");
350
352
  var import_core = require("@patterkit/core");
351
353
  var import_dialect3 = require("@patterkit/dialect");
354
+ var import_model3 = require("@patterkit/model");
355
+
356
+ // src/extensions.ts
352
357
  var import_model2 = require("@patterkit/model");
358
+ var import_extensions = require("@wildwinter/toolkit/extensions");
359
+ function extensionNodes(project, scenes) {
360
+ const nodes = /* @__PURE__ */ new Map();
361
+ const add = (id, kind, scene, gameData) => {
362
+ if (!nodes.has(id)) nodes.set(id, { kind, ...scene !== void 0 ? { scene } : {}, ...gameData !== void 0 ? { gameData } : {} });
363
+ };
364
+ add(project.project.id, "project", void 0, void 0);
365
+ for (const scene of scenes) {
366
+ const s = scene.id;
367
+ add(s, "scene", s, scene.gameData);
368
+ const walk = (children, inChoice) => {
369
+ for (const node of children ?? []) {
370
+ if (node.type === "group") {
371
+ if (inChoice) {
372
+ add(node.id, "option", s, node.gameData);
373
+ if (node.prompt) add(node.prompt.id, node.prompt.kind, s, node.prompt.gameData);
374
+ }
375
+ walk(node.children ?? [], node.selector === "choice");
376
+ continue;
377
+ }
378
+ add(node.id, "snippet", s, node.gameData);
379
+ for (const beat of node.beats ?? []) add(beat.id, beat.kind, s, beat.gameData);
380
+ }
381
+ };
382
+ for (const block of scene.blocks ?? []) {
383
+ add(block.id, "block", s, block.gameData);
384
+ walk(block.children ?? [], false);
385
+ }
386
+ }
387
+ return nodes;
388
+ }
389
+ var issueOf = (file, p, prefix = "", node) => ({
390
+ severity: p.severity,
391
+ file,
392
+ code: p.code,
393
+ ...node !== void 0 ? { node } : {},
394
+ message: p.path === "" ? `${prefix}${p.message}` : `${prefix}${p.path}: ${p.message}`
395
+ });
396
+ var folderOf = (path) => {
397
+ const parts = path.split(/[\\/]/);
398
+ return parts.length >= 2 ? parts[parts.length - 2] : void 0;
399
+ };
400
+ var own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
401
+ function extensionPass(input) {
402
+ const { project, scenes } = input;
403
+ const issues = [];
404
+ const projectPath = input.projectPath ?? "";
405
+ const taken = project.extensions ?? {};
406
+ const manifests = /* @__PURE__ */ new Map();
407
+ for (const m of input.manifests) {
408
+ const problems = (0, import_extensions.manifestProblems)(m.manifest);
409
+ for (const p of problems) issues.push(issueOf(m.path, p, "manifest "));
410
+ let sound2 = !problems.some((p) => p.severity === "error");
411
+ const id = m.manifest?.id;
412
+ if (sound2 && id !== m.id) {
413
+ issues.push({
414
+ severity: "error",
415
+ file: m.path,
416
+ message: `the manifest's id is "${String(id)}", but its file is named for "${m.id}"; a manifest is called <id>${import_model2.MANIFEST_EXTENSION}`
417
+ });
418
+ sound2 = false;
419
+ }
420
+ if (sound2 && m.manifest.targets.patter === void 0) {
421
+ issues.push({
422
+ severity: "warning",
423
+ file: m.path,
424
+ message: `"${m.id}" isn't a Patter extension: its manifest targets no Patter node kind, so it adds nothing here`
425
+ });
426
+ }
427
+ manifests.set(m.id, { path: m.path, manifest: m.manifest, sound: sound2 });
428
+ }
429
+ const takenList = [];
430
+ for (const [id, entry] of Object.entries(taken)) {
431
+ const m = manifests.get(id);
432
+ const version = typeof entry?.version === "string" ? entry.version : "";
433
+ const settings = entry?.settings ?? {};
434
+ if (m === void 0) {
435
+ issues.push({
436
+ severity: "warning",
437
+ file: projectPath,
438
+ message: `the project takes the Patter extension "${id}", but has no manifest for it at ${import_model2.EXTENSIONS_DIR}/${id}${import_model2.MANIFEST_EXTENSION}; its data is left as it is, and not compiled`
439
+ });
440
+ takenList.push({ id, settings });
441
+ continue;
442
+ }
443
+ if (!m.sound) {
444
+ takenList.push({ id, settings });
445
+ continue;
446
+ }
447
+ const manifest = m.manifest;
448
+ if (version !== manifest.version) {
449
+ issues.push({
450
+ severity: "warning",
451
+ file: projectPath,
452
+ message: `the project takes "${id}" ${version === "" ? "with no version" : `at version ${version}`}, but its manifest is version ${manifest.version}; run \`patter format\` to take it (renamed stages are renamed in the data as it does)`
453
+ });
454
+ }
455
+ const known = new Set((manifest.settings ?? []).map((s) => s.name));
456
+ for (const [name, value] of Object.entries(settings)) {
457
+ if (!known.has(name)) {
458
+ issues.push({ severity: "warning", file: projectPath, message: `"${id}" has no setting "${name}"; it's ignored` });
459
+ } else if (typeof value !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
460
+ issues.push({
461
+ severity: "error",
462
+ file: projectPath,
463
+ message: `"${id}"'s setting "${name}" names a gameData field, so it's a field name, not ${JSON.stringify(value)}`
464
+ });
465
+ }
466
+ }
467
+ takenList.push({ id, settings, manifest });
468
+ }
469
+ const sound = new Map(takenList.filter((t) => t.manifest !== void 0).map((t) => [t.id, t.manifest]));
470
+ const nodes = extensionNodes(project, scenes);
471
+ const claims = takenList.flatMap((t) => t.manifest ? (0, import_extensions.resolvedClaims)(t.manifest, "patter", t.settings) : []);
472
+ for (const p of (0, import_extensions.claimClashes)(claims)) issues.push({ severity: p.severity, file: projectPath, code: p.code, message: p.message });
473
+ issues.push(...claimIssues(input, nodes, claims));
474
+ const exists = (kind, id) => kind === "cast" ? (project.cast ?? []).some((c) => c.name === id) : nodes.get(id)?.kind === kind;
475
+ const sceneIds = new Set(scenes.map((s) => s.id));
476
+ const values = /* @__PURE__ */ new Map();
477
+ const projectValues = /* @__PURE__ */ new Map();
478
+ for (const { path, shard } of [...input.shards].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) {
479
+ const folder = folderOf(path);
480
+ if (folder !== void 0 && shard.extension !== folder) {
481
+ issues.push({
482
+ severity: "error",
483
+ file: path,
484
+ message: `this shard says it holds "${String(shard.extension)}", but it sits in the folder for "${folder}"`
485
+ });
486
+ continue;
487
+ }
488
+ const ext = shard.extension;
489
+ if (!own(taken, ext)) {
490
+ issues.push({
491
+ severity: "warning",
492
+ file: path,
493
+ message: `data for the Patter extension "${ext}", which the project doesn't take; it's left as it is, and not compiled`
494
+ });
495
+ continue;
496
+ }
497
+ const entries = shard.entries;
498
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries)) {
499
+ issues.push({ severity: "error", file: path, message: "entries are an object, by node kind and then node id" });
500
+ continue;
501
+ }
502
+ const shardScene = shard.scene === import_model2.PROJECT_LOCALE_SCENE ? void 0 : shard.scene;
503
+ const sceneKnown = shardScene === void 0 || sceneIds.has(shardScene);
504
+ if (!sceneKnown) {
505
+ issues.push({
506
+ severity: "warning",
507
+ file: path,
508
+ message: `this shard is for scene "${shard.scene}", which the project doesn't have; its entries are compiled where their nodes are`
509
+ });
510
+ }
511
+ for (const p of (0, import_extensions.orphanedEntries)(shard, exists)) issues.push(issueOf(path, p));
512
+ const manifest = sound.get(ext);
513
+ if (manifest === void 0) continue;
514
+ for (const [kind, byNode] of Object.entries(entries)) {
515
+ if (!import_extensions.HOSTS.patter.kinds.includes(kind)) {
516
+ issues.push({
517
+ severity: "warning",
518
+ file: path,
519
+ message: `entries.${kind}: "${kind}" isn't a Patter node kind; these entries are kept, and not compiled`
520
+ });
521
+ continue;
522
+ }
523
+ const target = (0, import_extensions.targetOf)(manifest, "patter", kind);
524
+ if (target === void 0) {
525
+ issues.push({
526
+ severity: "warning",
527
+ file: path,
528
+ message: `entries.${kind}: "${ext}" declares nothing for a ${kind}; these entries are kept, and not compiled`
529
+ });
530
+ continue;
531
+ }
532
+ if (typeof byNode !== "object" || byNode === null || Array.isArray(byNode)) continue;
533
+ for (const [id, entry] of Object.entries(byNode)) {
534
+ const node = nodes.get(id);
535
+ if (node === void 0 || node.kind !== kind) continue;
536
+ if (sceneKnown && node.scene !== shardScene) {
537
+ issues.push({
538
+ severity: "warning",
539
+ file: path,
540
+ node: id,
541
+ message: `entries.${kind}.${id} belongs ${node.scene === void 0 ? "in the project's own shard" : `in scene "${node.scene}"'s shard`}; it's compiled from here all the same`
542
+ });
543
+ }
544
+ const problems = (0, import_extensions.entryProblems)(target, entry, { exists });
545
+ for (const p of problems) issues.push(issueOf(path, p, `entries.${kind}.${id}${p.path === "" ? ": " : "."}`, id));
546
+ if (problems.some((p) => p.severity === "error")) continue;
547
+ const compiled = (0, import_extensions.compileEntry)(target, entry);
548
+ if (compiled === void 0) continue;
549
+ if (kind === "project") {
550
+ projectValues.set(ext, compiled);
551
+ continue;
552
+ }
553
+ values.set(id, { ...values.get(id) ?? {}, [ext]: compiled });
554
+ }
555
+ }
556
+ }
557
+ const bundle = {};
558
+ const chosen = new Map(takenList.map((t) => [t.id, t.settings]));
559
+ for (const [id, manifest] of [...sound.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
560
+ const fields = {};
561
+ for (const kind of Object.keys(manifest.targets.patter ?? {}).sort()) {
562
+ const target = (0, import_extensions.targetOf)(manifest, "patter", kind);
563
+ if (target !== void 0) fields[kind] = (0, import_extensions.compiledTarget)(target);
564
+ }
565
+ const projectEntry = projectValues.get(id);
566
+ const settings = (manifest.settings ?? []).length > 0 ? sortRecord((0, import_extensions.settingValues)(manifest, chosen.get(id))) : void 0;
567
+ bundle[id] = {
568
+ version: manifest.version,
569
+ fields,
570
+ ...projectEntry !== void 0 ? { project: projectEntry } : {},
571
+ ...settings !== void 0 ? { settings } : {}
572
+ };
573
+ }
574
+ const sortedNodes = new Map([...values.entries()].map(([id, v]) => [id, sortRecord(v)]));
575
+ return { issues, compiled: { bundle, nodes: sortedNodes } };
576
+ }
577
+ function sortRecord(r) {
578
+ return Object.fromEntries(Object.keys(r).sort().map((k) => [k, r[k]]));
579
+ }
580
+ function claimIssues(input, nodes, claims) {
581
+ const issues = [];
582
+ const projectPath = input.projectPath ?? "";
583
+ const declared = input.project.gameDataFields ?? {};
584
+ for (const claim of claims) {
585
+ const decl = (declared[claim.kind] ?? []).find((d) => d.name === claim.field);
586
+ if (decl === void 0) {
587
+ issues.push({
588
+ severity: "warning",
589
+ file: projectPath,
590
+ message: `the Patter extension "${claim.extension}" claims the ${claim.kind} gameData field "${claim.field}", which the project doesn't declare`
591
+ });
592
+ continue;
593
+ }
594
+ if (decl.type !== claim.claim.type) {
595
+ issues.push({
596
+ severity: "error",
597
+ file: projectPath,
598
+ message: `the Patter extension "${claim.extension}" claims the ${claim.kind} gameData field "${claim.field}" as ${claim.claim.type}, but the project declares it as ${decl.type}`
599
+ });
600
+ continue;
601
+ }
602
+ const effective = (0, import_extensions.effectiveClaim)(claim.claim, decl);
603
+ for (const [id, node] of nodes) {
604
+ if (node.kind !== claim.kind || node.gameData === void 0 || !own(node.gameData, claim.field)) continue;
605
+ const file = (node.scene !== void 0 ? input.sceneFiles?.[node.scene] : void 0) ?? projectPath;
606
+ for (const p of (0, import_extensions.valueProblems)(effective, node.gameData[claim.field])) {
607
+ issues.push({
608
+ severity: p.severity,
609
+ file,
610
+ node: id,
611
+ code: p.code,
612
+ message: `${claim.kind} '${id}' gameData "${claim.field}", as the Patter extension "${claim.extension}" claims it: ${p.message}`
613
+ });
614
+ }
615
+ }
616
+ }
617
+ return issues;
618
+ }
619
+
620
+ // src/export.ts
621
+ function compileBeat(beat, ctx) {
622
+ const values = ctx.extensions.get(beat.id);
623
+ if (values === void 0 && !Object.prototype.hasOwnProperty.call(beat, "extensions")) return beat;
624
+ const { extensions: _source, ...rest } = beat;
625
+ return values !== void 0 ? { ...rest, extensions: values } : rest;
626
+ }
353
627
  function compileEffect(e, foreign) {
354
628
  return { kind: "set", target: e.target, value: compileExpression(e.value, foreign) };
355
629
  }
356
- function compileNode(node, foreign) {
630
+ function compileNode(node, ctx) {
631
+ const { foreign } = ctx;
357
632
  if (node.type === "group") {
358
633
  return {
359
634
  id: node.id,
@@ -362,13 +637,15 @@ function compileNode(node, foreign) {
362
637
  selector: node.selector,
363
638
  shared: node.shared,
364
639
  options: node.options,
365
- children: node.children.map((c) => compileNode(c, foreign)),
640
+ children: node.children.map((c) => compileNode(c, ctx)),
366
641
  padAfterDefault: node.padAfterDefault,
367
642
  // line padding: the default for the beats inside
368
643
  gameData: node.gameData,
644
+ extensions: ctx.extensions.get(node.id),
645
+ // a choice's option's extension values
369
646
  tags: node.tags,
370
647
  // author tags (#215), accumulated at runtime
371
- prompt: node.prompt,
648
+ prompt: node.prompt && compileBeat(node.prompt, ctx),
372
649
  // option-position only (spec §5)
373
650
  secretUntilEligible: node.secretUntilEligible,
374
651
  sticky: node.sticky,
@@ -379,12 +656,13 @@ function compileNode(node, foreign) {
379
656
  id: node.id,
380
657
  type: "snippet",
381
658
  condition: node.condition !== void 0 ? compileExpression(node.condition, foreign) : void 0,
382
- beats: node.beats,
659
+ beats: node.beats && node.beats.map((b) => compileBeat(b, ctx)),
383
660
  onEnter: node.onEnter?.map((e) => compileEffect(e, foreign)),
384
661
  onExit: node.onExit?.map((e) => compileEffect(e, foreign)),
385
662
  padAfterDefault: node.padAfterDefault,
386
663
  // line padding: the default for the beats inside
387
664
  gameData: node.gameData,
665
+ extensions: ctx.extensions.get(node.id),
388
666
  tags: node.tags,
389
667
  // author tags (#215)
390
668
  jump: node.jump,
@@ -393,22 +671,24 @@ function compileNode(node, foreign) {
393
671
  fallback: node.fallback
394
672
  };
395
673
  }
396
- function compileBlock(block, foreign) {
674
+ function compileBlock(block, ctx) {
397
675
  return {
398
676
  id: block.id,
399
677
  type: "block",
400
678
  name: block.name,
401
679
  ...block.gameId ? { gameId: block.gameId } : {},
402
680
  // host-facing address; runtime falls back to the name slug
403
- children: block.children.map((c) => compileNode(c, foreign)),
681
+ children: block.children.map((c) => compileNode(c, ctx)),
404
682
  padAfterDefault: block.padAfterDefault,
405
683
  // line padding: the default for the beats inside
406
684
  gameData: block.gameData,
685
+ extensions: ctx.extensions.get(block.id),
407
686
  tags: block.tags
408
687
  // author tags (#215)
409
688
  };
410
689
  }
411
- function compileScene(scene, foreign) {
690
+ function compileScene(scene, ctx) {
691
+ const { foreign } = ctx;
412
692
  return {
413
693
  id: scene.id,
414
694
  type: "scene",
@@ -418,11 +698,12 @@ function compileScene(scene, foreign) {
418
698
  padAfterDefault: scene.padAfterDefault,
419
699
  // line padding: the default for the beats inside
420
700
  gameData: scene.gameData,
701
+ extensions: ctx.extensions.get(scene.id),
421
702
  tags: scene.tags,
422
703
  // author tags (#215)
423
704
  onEntry: scene.onEntry?.map((e) => compileEffect(e, foreign)),
424
705
  sceneProps: scene.sceneProps,
425
- blocks: scene.blocks.map((b) => compileBlock(b, foreign))
706
+ blocks: scene.blocks.map((b) => compileBlock(b, ctx))
426
707
  };
427
708
  }
428
709
  function externalScopesIn(scenes, external) {
@@ -456,12 +737,16 @@ function exportBundle(input) {
456
737
  version: foreignScopes?.version ?? 1,
457
738
  scopes: [...foreignScopes?.scopes ?? [], ...gameExternal.map((s) => ({ token: s.token }))]
458
739
  });
740
+ const ext = input.extensions;
741
+ const compiledExt = project.extensions !== void 0 || ext !== void 0 && (ext.manifests.length > 0 || ext.shards.length > 0) ? extensionPass({ project, scenes, manifests: ext?.manifests ?? [], shards: ext?.shards ?? [] }).compiled : void 0;
742
+ const extensionSection = compiledExt !== void 0 && Object.keys(compiledExt.bundle).length > 0 ? compiledExt.bundle : void 0;
743
+ const ctx = { foreign: compileScopes, extensions: compiledExt?.nodes ?? /* @__PURE__ */ new Map() };
459
744
  const scenesOut = {};
460
- for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, compileScopes);
745
+ for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, ctx);
461
746
  const externalScopes = externalScopesIn(scenesOut, /* @__PURE__ */ new Set([...import_dialect3.EXTERNAL_SCOPES, ...gameExternal.map((s) => s.token)]));
462
- const qualifiers = (0, import_model2.usedQualifiers)(project, scenes).map((q) => ({ gameId: q.gameId, name: q.name }));
463
- const usedPairs = new Set((0, import_model2.usedSpeakerPairs)(scenes).map((p) => `${p.character}\0${p.qualifier}`));
464
- const speakerPattern = qualifiers.length > 0 && project.speakerPattern !== void 0 && project.speakerPattern !== import_model2.DEFAULT_SPEAKER_PATTERN ? project.speakerPattern : void 0;
747
+ const qualifiers = (0, import_model3.usedQualifiers)(project, scenes).map((q) => ({ gameId: q.gameId, name: q.name }));
748
+ const usedPairs = new Set((0, import_model3.usedSpeakerPairs)(scenes).map((p) => `${p.character}\0${p.qualifier}`));
749
+ const speakerPattern = qualifiers.length > 0 && project.speakerPattern !== void 0 && project.speakerPattern !== import_model3.DEFAULT_SPEAKER_PATTERN ? project.speakerPattern : void 0;
465
750
  const strings = {};
466
751
  for (const loc of locales) {
467
752
  if (includeLocales && !includeLocales.includes(loc.locale)) continue;
@@ -488,7 +773,9 @@ function exportBundle(input) {
488
773
  properties: project.properties ?? [],
489
774
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
490
775
  ...qualifiers.length > 0 ? { qualifiers } : {},
491
- ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
776
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
777
+ // Each node's values are in `scenes`; the declarations and the project's own values are here.
778
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {}
492
779
  }, { trailingComma: false })),
493
780
  // Structure-only fingerprint (live bundle refresh): the same hash with the string tables left
494
781
  // out. Same structureHash + a different hash = a text-only edit, safe to swap in place with
@@ -499,7 +786,8 @@ function exportBundle(input) {
499
786
  properties: project.properties ?? [],
500
787
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
501
788
  ...qualifiers.length > 0 ? { qualifiers } : {},
502
- ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
789
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
790
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {}
503
791
  }, { trailingComma: false }))
504
792
  },
505
793
  voiced: project.voiced ?? false,
@@ -528,6 +816,10 @@ function exportBundle(input) {
528
816
  gameDataFields: project.gameDataFields && Object.fromEntries(
529
817
  Object.entries(project.gameDataFields).map(([kind, fields]) => [kind, resolved(fields)])
530
818
  ),
819
+ // The extensions the project takes: each one's version, its compiled declarations by node kind (every
820
+ // default resolved, editor-only fields and the editor's words left out), the project node's values, and
821
+ // its settings, each the project's value or else the default.
822
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {},
531
823
  // Closed-caption delimiters ride along only when the project pins a non-default pair (#214); the
532
824
  // runtime falls back to `[` / `]` when absent, so default projects keep their exact prior bundle.
533
825
  ...project.closedCaptions ? { closedCaptions: project.closedCaptions } : {},
@@ -543,6 +835,8 @@ function exportBundle(input) {
543
835
  PATTER_SCOPE,
544
836
  compileExpression,
545
837
  exportBundle,
838
+ extensionNodes,
839
+ extensionPass,
546
840
  externalGameScopes,
547
841
  hostScopesToSpec,
548
842
  projectScopes,
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ScopeRegistrySpec, ScopeSpec } from '@wildwinter/scoperegistry';
2
2
  import { MergedScopes } from '@wildwinter/scoperegistry/scopes';
3
- import { Expression, ProjectFile, Scene, LocaleFile, Bundle, HostScopeRegistry } from '@patterkit/model';
3
+ import { Expression, ProjectFile, Scene, LocaleFile, BundleExtension, NodeExtensions, ExtensionShard, GameData, Bundle, HostScopeRegistry } from '@patterkit/model';
4
4
  export { EXTERNAL_SCOPES, hostScopesToSpec } from '@patterkit/dialect';
5
5
 
6
6
  /**
@@ -62,6 +62,70 @@ declare function validateInterpolation(input: {
62
62
  locales?: LocaleFile[];
63
63
  }, options?: ValidateOptions): ConditionIssue[];
64
64
 
65
+ /** One extension's manifest, as a project keeps it (`extensions/<id>.extension.json`). */
66
+ interface ExtensionManifestFile {
67
+ /** Where it is, for the report. */
68
+ path: string;
69
+ /** The file's stem, which names the extension it should be. */
70
+ id: string;
71
+ /** The parsed JSON, unchecked: the pass says whether it is sound. */
72
+ manifest: unknown;
73
+ }
74
+ /** One extension data shard (`.patterextension`), with where it is. */
75
+ interface ExtensionShardFile {
76
+ /** `<layout.extensions>/<extension id>/<scene file stem>.patterextension`: the folder names the extension. */
77
+ path: string;
78
+ shard: ExtensionShard;
79
+ }
80
+ /** What the pass reads: the project, its scenes, and its extension files. */
81
+ interface ExtensionPassInput {
82
+ project: ProjectFile;
83
+ scenes: Scene[];
84
+ manifests: readonly ExtensionManifestFile[];
85
+ shards: readonly ExtensionShardFile[];
86
+ /** The project file's path, for the report (default `""`). */
87
+ projectPath?: string;
88
+ /** Each scene's file, by scene id, for the report. */
89
+ sceneFiles?: Readonly<Record<string, string>>;
90
+ }
91
+ /** One thing the pass found. `file` is the file to open; `node` the node id, when it is about one. */
92
+ interface ExtensionIssue {
93
+ severity: "error" | "warning";
94
+ file: string;
95
+ node?: string;
96
+ /** The toolkit's stable name for the problem, when it is one of the toolkit's. */
97
+ code?: string;
98
+ message: string;
99
+ }
100
+ /** What the pass hands the bundle. */
101
+ interface CompiledExtensions {
102
+ /** The bundle's `extensions` section, by extension id. Empty when the project takes none. */
103
+ readonly bundle: Record<string, BundleExtension>;
104
+ /** Each node's compiled values, by node id: what goes on the node as `extensions`. */
105
+ readonly nodes: ReadonlyMap<string, NodeExtensions>;
106
+ }
107
+ /** A node the project has: its kind, the scene it belongs to (none for the project's own), and its gameData. */
108
+ interface ExtensionNode {
109
+ readonly kind: string;
110
+ readonly scene?: string;
111
+ readonly gameData?: GameData;
112
+ }
113
+ /**
114
+ * Every node an extension can attach to, by id (ids are project-unique): the project, each scene, block and
115
+ * snippet, a choice's options (the groups directly inside a `choice` group, as gameData's `option` is), and
116
+ * every beat by its kind, an option's prompt beat included. A bare snippet in a choice stays a snippet.
117
+ */
118
+ declare function extensionNodes(project: ProjectFile, scenes: readonly Scene[]): Map<string, ExtensionNode>;
119
+ /**
120
+ * Validate every extension, and compile what may be compiled. `issues` is the pass's report; `compiled` is
121
+ * what the bundle carries. Data whose manifest is missing or unsound is reported and left out of the bundle,
122
+ * never changed; an entry with an error is left out on its own.
123
+ */
124
+ declare function extensionPass(input: ExtensionPassInput): {
125
+ issues: ExtensionIssue[];
126
+ compiled: CompiledExtensions;
127
+ };
128
+
65
129
  interface ExportInput {
66
130
  project: ProjectFile;
67
131
  scenes: Scene[];
@@ -82,6 +146,17 @@ interface ExportInput {
82
146
  * does: named in `externalScopes`, never baked in.
83
147
  */
84
148
  gameScopes?: MergedScopes;
149
+ /**
150
+ * The project's Patter extensions: its manifests and its extension data shards. Each node's values are
151
+ * compiled inline under `extensions`, and the bundle's `extensions` section carries each extension the
152
+ * project takes. Absent or empty, with no extension taken, the bundle and its hashes are exactly what they
153
+ * were. What is wrong with the data is validate's to report (`extensionPass`); here an entry with an
154
+ * error is simply left out.
155
+ */
156
+ extensions?: {
157
+ manifests: readonly ExtensionManifestFile[];
158
+ shards: readonly ExtensionShardFile[];
159
+ };
85
160
  }
86
161
  /** Compile a project's source into the runtime bundle. */
87
162
  declare function exportBundle(input: ExportInput): Bundle;
@@ -116,4 +191,4 @@ declare function projectScopes(project: ProjectFile, merged?: MergedScopes): Pro
116
191
  */
117
192
  declare function externalGameScopes(strict: ScopeRegistrySpec | undefined, merged?: MergedScopes): ScopeSpec[];
118
193
 
119
- export { type ConditionIssue, type ExportInput, type HostScopeNote, PATTER_SCOPE, type ProjectScopes, type ValidateOptions, compileExpression, exportBundle, externalGameScopes, projectScopes, validateConditions, validateInterpolation };
194
+ export { type CompiledExtensions, type ConditionIssue, type ExportInput, type ExtensionIssue, type ExtensionManifestFile, type ExtensionNode, type ExtensionPassInput, type ExtensionShardFile, type HostScopeNote, PATTER_SCOPE, type ProjectScopes, type ValidateOptions, compileExpression, exportBundle, extensionNodes, extensionPass, externalGameScopes, projectScopes, validateConditions, validateInterpolation };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ScopeRegistrySpec, ScopeSpec } from '@wildwinter/scoperegistry';
2
2
  import { MergedScopes } from '@wildwinter/scoperegistry/scopes';
3
- import { Expression, ProjectFile, Scene, LocaleFile, Bundle, HostScopeRegistry } from '@patterkit/model';
3
+ import { Expression, ProjectFile, Scene, LocaleFile, BundleExtension, NodeExtensions, ExtensionShard, GameData, Bundle, HostScopeRegistry } from '@patterkit/model';
4
4
  export { EXTERNAL_SCOPES, hostScopesToSpec } from '@patterkit/dialect';
5
5
 
6
6
  /**
@@ -62,6 +62,70 @@ declare function validateInterpolation(input: {
62
62
  locales?: LocaleFile[];
63
63
  }, options?: ValidateOptions): ConditionIssue[];
64
64
 
65
+ /** One extension's manifest, as a project keeps it (`extensions/<id>.extension.json`). */
66
+ interface ExtensionManifestFile {
67
+ /** Where it is, for the report. */
68
+ path: string;
69
+ /** The file's stem, which names the extension it should be. */
70
+ id: string;
71
+ /** The parsed JSON, unchecked: the pass says whether it is sound. */
72
+ manifest: unknown;
73
+ }
74
+ /** One extension data shard (`.patterextension`), with where it is. */
75
+ interface ExtensionShardFile {
76
+ /** `<layout.extensions>/<extension id>/<scene file stem>.patterextension`: the folder names the extension. */
77
+ path: string;
78
+ shard: ExtensionShard;
79
+ }
80
+ /** What the pass reads: the project, its scenes, and its extension files. */
81
+ interface ExtensionPassInput {
82
+ project: ProjectFile;
83
+ scenes: Scene[];
84
+ manifests: readonly ExtensionManifestFile[];
85
+ shards: readonly ExtensionShardFile[];
86
+ /** The project file's path, for the report (default `""`). */
87
+ projectPath?: string;
88
+ /** Each scene's file, by scene id, for the report. */
89
+ sceneFiles?: Readonly<Record<string, string>>;
90
+ }
91
+ /** One thing the pass found. `file` is the file to open; `node` the node id, when it is about one. */
92
+ interface ExtensionIssue {
93
+ severity: "error" | "warning";
94
+ file: string;
95
+ node?: string;
96
+ /** The toolkit's stable name for the problem, when it is one of the toolkit's. */
97
+ code?: string;
98
+ message: string;
99
+ }
100
+ /** What the pass hands the bundle. */
101
+ interface CompiledExtensions {
102
+ /** The bundle's `extensions` section, by extension id. Empty when the project takes none. */
103
+ readonly bundle: Record<string, BundleExtension>;
104
+ /** Each node's compiled values, by node id: what goes on the node as `extensions`. */
105
+ readonly nodes: ReadonlyMap<string, NodeExtensions>;
106
+ }
107
+ /** A node the project has: its kind, the scene it belongs to (none for the project's own), and its gameData. */
108
+ interface ExtensionNode {
109
+ readonly kind: string;
110
+ readonly scene?: string;
111
+ readonly gameData?: GameData;
112
+ }
113
+ /**
114
+ * Every node an extension can attach to, by id (ids are project-unique): the project, each scene, block and
115
+ * snippet, a choice's options (the groups directly inside a `choice` group, as gameData's `option` is), and
116
+ * every beat by its kind, an option's prompt beat included. A bare snippet in a choice stays a snippet.
117
+ */
118
+ declare function extensionNodes(project: ProjectFile, scenes: readonly Scene[]): Map<string, ExtensionNode>;
119
+ /**
120
+ * Validate every extension, and compile what may be compiled. `issues` is the pass's report; `compiled` is
121
+ * what the bundle carries. Data whose manifest is missing or unsound is reported and left out of the bundle,
122
+ * never changed; an entry with an error is left out on its own.
123
+ */
124
+ declare function extensionPass(input: ExtensionPassInput): {
125
+ issues: ExtensionIssue[];
126
+ compiled: CompiledExtensions;
127
+ };
128
+
65
129
  interface ExportInput {
66
130
  project: ProjectFile;
67
131
  scenes: Scene[];
@@ -82,6 +146,17 @@ interface ExportInput {
82
146
  * does: named in `externalScopes`, never baked in.
83
147
  */
84
148
  gameScopes?: MergedScopes;
149
+ /**
150
+ * The project's Patter extensions: its manifests and its extension data shards. Each node's values are
151
+ * compiled inline under `extensions`, and the bundle's `extensions` section carries each extension the
152
+ * project takes. Absent or empty, with no extension taken, the bundle and its hashes are exactly what they
153
+ * were. What is wrong with the data is validate's to report (`extensionPass`); here an entry with an
154
+ * error is simply left out.
155
+ */
156
+ extensions?: {
157
+ manifests: readonly ExtensionManifestFile[];
158
+ shards: readonly ExtensionShardFile[];
159
+ };
85
160
  }
86
161
  /** Compile a project's source into the runtime bundle. */
87
162
  declare function exportBundle(input: ExportInput): Bundle;
@@ -116,4 +191,4 @@ declare function projectScopes(project: ProjectFile, merged?: MergedScopes): Pro
116
191
  */
117
192
  declare function externalGameScopes(strict: ScopeRegistrySpec | undefined, merged?: MergedScopes): ScopeSpec[];
118
193
 
119
- export { type ConditionIssue, type ExportInput, type HostScopeNote, PATTER_SCOPE, type ProjectScopes, type ValidateOptions, compileExpression, exportBundle, externalGameScopes, projectScopes, validateConditions, validateInterpolation };
194
+ export { type CompiledExtensions, type ConditionIssue, type ExportInput, type ExtensionIssue, type ExtensionManifestFile, type ExtensionNode, type ExtensionPassInput, type ExtensionShardFile, type HostScopeNote, PATTER_SCOPE, type ProjectScopes, type ValidateOptions, compileExpression, exportBundle, extensionNodes, extensionPass, externalGameScopes, projectScopes, validateConditions, validateInterpolation };
package/dist/index.js CHANGED
@@ -19,14 +19,14 @@ function sameScope(a, b) {
19
19
  return norm(a) === norm(b);
20
20
  }
21
21
  function projectScopes(project, merged) {
22
- const own = project.scopeRegistry;
23
- if (!merged) return { host: own, external: [], notes: [] };
22
+ const own2 = project.scopeRegistry;
23
+ if (!merged) return { host: own2, external: [], notes: [] };
24
24
  const notes = [];
25
25
  const hosted = [];
26
26
  const bundled = /* @__PURE__ */ new Set();
27
27
  let changed = false;
28
28
  const shared = (token) => token === PATTER_SCOPE ? void 0 : merged.spec.scopes.find((s) => s.token === token);
29
- for (const h of own?.scopes ?? []) {
29
+ for (const h of own2?.scopes ?? []) {
30
30
  const s = shared(h.token);
31
31
  const who = merged.owners.get(h.token);
32
32
  if (!s || !who) {
@@ -62,7 +62,7 @@ function projectScopes(project, merged) {
62
62
  changed = true;
63
63
  }
64
64
  const external = merged.spec.scopes.filter((s) => s.token !== PATTER_SCOPE && !bundled.has(s.token));
65
- const host = !changed ? own : hosted.length ? { version: own?.version ?? 1, scopes: hosted } : void 0;
65
+ const host = !changed ? own2 : hosted.length ? { version: own2?.version ?? 1, scopes: hosted } : void 0;
66
66
  return { host, external, notes };
67
67
  }
68
68
  function externalGameScopes(strict, merged) {
@@ -316,10 +316,296 @@ import { defaultFor } from "@wildwinter/scoperegistry";
316
316
  import { canonicalStringify, hash32 } from "@patterkit/core";
317
317
  import { hostScopesToSpec as hostScopesToSpec3, withEngineScopes as withEngineScopes2, EXTERNAL_SCOPES } from "@patterkit/dialect";
318
318
  import { usedQualifiers, usedSpeakerPairs, DEFAULT_SPEAKER_PATTERN } from "@patterkit/model";
319
+
320
+ // src/extensions.ts
321
+ import { EXTENSIONS_DIR, MANIFEST_EXTENSION, PROJECT_LOCALE_SCENE } from "@patterkit/model";
322
+ import {
323
+ HOSTS,
324
+ claimClashes,
325
+ compileEntry,
326
+ compiledTarget,
327
+ effectiveClaim,
328
+ entryProblems,
329
+ manifestProblems,
330
+ orphanedEntries,
331
+ resolvedClaims,
332
+ settingValues,
333
+ targetOf,
334
+ valueProblems
335
+ } from "@wildwinter/toolkit/extensions";
336
+ function extensionNodes(project, scenes) {
337
+ const nodes = /* @__PURE__ */ new Map();
338
+ const add = (id, kind, scene, gameData) => {
339
+ if (!nodes.has(id)) nodes.set(id, { kind, ...scene !== void 0 ? { scene } : {}, ...gameData !== void 0 ? { gameData } : {} });
340
+ };
341
+ add(project.project.id, "project", void 0, void 0);
342
+ for (const scene of scenes) {
343
+ const s = scene.id;
344
+ add(s, "scene", s, scene.gameData);
345
+ const walk = (children, inChoice) => {
346
+ for (const node of children ?? []) {
347
+ if (node.type === "group") {
348
+ if (inChoice) {
349
+ add(node.id, "option", s, node.gameData);
350
+ if (node.prompt) add(node.prompt.id, node.prompt.kind, s, node.prompt.gameData);
351
+ }
352
+ walk(node.children ?? [], node.selector === "choice");
353
+ continue;
354
+ }
355
+ add(node.id, "snippet", s, node.gameData);
356
+ for (const beat of node.beats ?? []) add(beat.id, beat.kind, s, beat.gameData);
357
+ }
358
+ };
359
+ for (const block of scene.blocks ?? []) {
360
+ add(block.id, "block", s, block.gameData);
361
+ walk(block.children ?? [], false);
362
+ }
363
+ }
364
+ return nodes;
365
+ }
366
+ var issueOf = (file, p, prefix = "", node) => ({
367
+ severity: p.severity,
368
+ file,
369
+ code: p.code,
370
+ ...node !== void 0 ? { node } : {},
371
+ message: p.path === "" ? `${prefix}${p.message}` : `${prefix}${p.path}: ${p.message}`
372
+ });
373
+ var folderOf = (path) => {
374
+ const parts = path.split(/[\\/]/);
375
+ return parts.length >= 2 ? parts[parts.length - 2] : void 0;
376
+ };
377
+ var own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
378
+ function extensionPass(input) {
379
+ const { project, scenes } = input;
380
+ const issues = [];
381
+ const projectPath = input.projectPath ?? "";
382
+ const taken = project.extensions ?? {};
383
+ const manifests = /* @__PURE__ */ new Map();
384
+ for (const m of input.manifests) {
385
+ const problems = manifestProblems(m.manifest);
386
+ for (const p of problems) issues.push(issueOf(m.path, p, "manifest "));
387
+ let sound2 = !problems.some((p) => p.severity === "error");
388
+ const id = m.manifest?.id;
389
+ if (sound2 && id !== m.id) {
390
+ issues.push({
391
+ severity: "error",
392
+ file: m.path,
393
+ message: `the manifest's id is "${String(id)}", but its file is named for "${m.id}"; a manifest is called <id>${MANIFEST_EXTENSION}`
394
+ });
395
+ sound2 = false;
396
+ }
397
+ if (sound2 && m.manifest.targets.patter === void 0) {
398
+ issues.push({
399
+ severity: "warning",
400
+ file: m.path,
401
+ message: `"${m.id}" isn't a Patter extension: its manifest targets no Patter node kind, so it adds nothing here`
402
+ });
403
+ }
404
+ manifests.set(m.id, { path: m.path, manifest: m.manifest, sound: sound2 });
405
+ }
406
+ const takenList = [];
407
+ for (const [id, entry] of Object.entries(taken)) {
408
+ const m = manifests.get(id);
409
+ const version = typeof entry?.version === "string" ? entry.version : "";
410
+ const settings = entry?.settings ?? {};
411
+ if (m === void 0) {
412
+ issues.push({
413
+ severity: "warning",
414
+ file: projectPath,
415
+ message: `the project takes the Patter extension "${id}", but has no manifest for it at ${EXTENSIONS_DIR}/${id}${MANIFEST_EXTENSION}; its data is left as it is, and not compiled`
416
+ });
417
+ takenList.push({ id, settings });
418
+ continue;
419
+ }
420
+ if (!m.sound) {
421
+ takenList.push({ id, settings });
422
+ continue;
423
+ }
424
+ const manifest = m.manifest;
425
+ if (version !== manifest.version) {
426
+ issues.push({
427
+ severity: "warning",
428
+ file: projectPath,
429
+ message: `the project takes "${id}" ${version === "" ? "with no version" : `at version ${version}`}, but its manifest is version ${manifest.version}; run \`patter format\` to take it (renamed stages are renamed in the data as it does)`
430
+ });
431
+ }
432
+ const known = new Set((manifest.settings ?? []).map((s) => s.name));
433
+ for (const [name, value] of Object.entries(settings)) {
434
+ if (!known.has(name)) {
435
+ issues.push({ severity: "warning", file: projectPath, message: `"${id}" has no setting "${name}"; it's ignored` });
436
+ } else if (typeof value !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
437
+ issues.push({
438
+ severity: "error",
439
+ file: projectPath,
440
+ message: `"${id}"'s setting "${name}" names a gameData field, so it's a field name, not ${JSON.stringify(value)}`
441
+ });
442
+ }
443
+ }
444
+ takenList.push({ id, settings, manifest });
445
+ }
446
+ const sound = new Map(takenList.filter((t) => t.manifest !== void 0).map((t) => [t.id, t.manifest]));
447
+ const nodes = extensionNodes(project, scenes);
448
+ const claims = takenList.flatMap((t) => t.manifest ? resolvedClaims(t.manifest, "patter", t.settings) : []);
449
+ for (const p of claimClashes(claims)) issues.push({ severity: p.severity, file: projectPath, code: p.code, message: p.message });
450
+ issues.push(...claimIssues(input, nodes, claims));
451
+ const exists = (kind, id) => kind === "cast" ? (project.cast ?? []).some((c) => c.name === id) : nodes.get(id)?.kind === kind;
452
+ const sceneIds = new Set(scenes.map((s) => s.id));
453
+ const values = /* @__PURE__ */ new Map();
454
+ const projectValues = /* @__PURE__ */ new Map();
455
+ for (const { path, shard } of [...input.shards].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) {
456
+ const folder = folderOf(path);
457
+ if (folder !== void 0 && shard.extension !== folder) {
458
+ issues.push({
459
+ severity: "error",
460
+ file: path,
461
+ message: `this shard says it holds "${String(shard.extension)}", but it sits in the folder for "${folder}"`
462
+ });
463
+ continue;
464
+ }
465
+ const ext = shard.extension;
466
+ if (!own(taken, ext)) {
467
+ issues.push({
468
+ severity: "warning",
469
+ file: path,
470
+ message: `data for the Patter extension "${ext}", which the project doesn't take; it's left as it is, and not compiled`
471
+ });
472
+ continue;
473
+ }
474
+ const entries = shard.entries;
475
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries)) {
476
+ issues.push({ severity: "error", file: path, message: "entries are an object, by node kind and then node id" });
477
+ continue;
478
+ }
479
+ const shardScene = shard.scene === PROJECT_LOCALE_SCENE ? void 0 : shard.scene;
480
+ const sceneKnown = shardScene === void 0 || sceneIds.has(shardScene);
481
+ if (!sceneKnown) {
482
+ issues.push({
483
+ severity: "warning",
484
+ file: path,
485
+ message: `this shard is for scene "${shard.scene}", which the project doesn't have; its entries are compiled where their nodes are`
486
+ });
487
+ }
488
+ for (const p of orphanedEntries(shard, exists)) issues.push(issueOf(path, p));
489
+ const manifest = sound.get(ext);
490
+ if (manifest === void 0) continue;
491
+ for (const [kind, byNode] of Object.entries(entries)) {
492
+ if (!HOSTS.patter.kinds.includes(kind)) {
493
+ issues.push({
494
+ severity: "warning",
495
+ file: path,
496
+ message: `entries.${kind}: "${kind}" isn't a Patter node kind; these entries are kept, and not compiled`
497
+ });
498
+ continue;
499
+ }
500
+ const target = targetOf(manifest, "patter", kind);
501
+ if (target === void 0) {
502
+ issues.push({
503
+ severity: "warning",
504
+ file: path,
505
+ message: `entries.${kind}: "${ext}" declares nothing for a ${kind}; these entries are kept, and not compiled`
506
+ });
507
+ continue;
508
+ }
509
+ if (typeof byNode !== "object" || byNode === null || Array.isArray(byNode)) continue;
510
+ for (const [id, entry] of Object.entries(byNode)) {
511
+ const node = nodes.get(id);
512
+ if (node === void 0 || node.kind !== kind) continue;
513
+ if (sceneKnown && node.scene !== shardScene) {
514
+ issues.push({
515
+ severity: "warning",
516
+ file: path,
517
+ node: id,
518
+ message: `entries.${kind}.${id} belongs ${node.scene === void 0 ? "in the project's own shard" : `in scene "${node.scene}"'s shard`}; it's compiled from here all the same`
519
+ });
520
+ }
521
+ const problems = entryProblems(target, entry, { exists });
522
+ for (const p of problems) issues.push(issueOf(path, p, `entries.${kind}.${id}${p.path === "" ? ": " : "."}`, id));
523
+ if (problems.some((p) => p.severity === "error")) continue;
524
+ const compiled = compileEntry(target, entry);
525
+ if (compiled === void 0) continue;
526
+ if (kind === "project") {
527
+ projectValues.set(ext, compiled);
528
+ continue;
529
+ }
530
+ values.set(id, { ...values.get(id) ?? {}, [ext]: compiled });
531
+ }
532
+ }
533
+ }
534
+ const bundle = {};
535
+ const chosen = new Map(takenList.map((t) => [t.id, t.settings]));
536
+ for (const [id, manifest] of [...sound.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
537
+ const fields = {};
538
+ for (const kind of Object.keys(manifest.targets.patter ?? {}).sort()) {
539
+ const target = targetOf(manifest, "patter", kind);
540
+ if (target !== void 0) fields[kind] = compiledTarget(target);
541
+ }
542
+ const projectEntry = projectValues.get(id);
543
+ const settings = (manifest.settings ?? []).length > 0 ? sortRecord(settingValues(manifest, chosen.get(id))) : void 0;
544
+ bundle[id] = {
545
+ version: manifest.version,
546
+ fields,
547
+ ...projectEntry !== void 0 ? { project: projectEntry } : {},
548
+ ...settings !== void 0 ? { settings } : {}
549
+ };
550
+ }
551
+ const sortedNodes = new Map([...values.entries()].map(([id, v]) => [id, sortRecord(v)]));
552
+ return { issues, compiled: { bundle, nodes: sortedNodes } };
553
+ }
554
+ function sortRecord(r) {
555
+ return Object.fromEntries(Object.keys(r).sort().map((k) => [k, r[k]]));
556
+ }
557
+ function claimIssues(input, nodes, claims) {
558
+ const issues = [];
559
+ const projectPath = input.projectPath ?? "";
560
+ const declared = input.project.gameDataFields ?? {};
561
+ for (const claim of claims) {
562
+ const decl = (declared[claim.kind] ?? []).find((d) => d.name === claim.field);
563
+ if (decl === void 0) {
564
+ issues.push({
565
+ severity: "warning",
566
+ file: projectPath,
567
+ message: `the Patter extension "${claim.extension}" claims the ${claim.kind} gameData field "${claim.field}", which the project doesn't declare`
568
+ });
569
+ continue;
570
+ }
571
+ if (decl.type !== claim.claim.type) {
572
+ issues.push({
573
+ severity: "error",
574
+ file: projectPath,
575
+ message: `the Patter extension "${claim.extension}" claims the ${claim.kind} gameData field "${claim.field}" as ${claim.claim.type}, but the project declares it as ${decl.type}`
576
+ });
577
+ continue;
578
+ }
579
+ const effective = effectiveClaim(claim.claim, decl);
580
+ for (const [id, node] of nodes) {
581
+ if (node.kind !== claim.kind || node.gameData === void 0 || !own(node.gameData, claim.field)) continue;
582
+ const file = (node.scene !== void 0 ? input.sceneFiles?.[node.scene] : void 0) ?? projectPath;
583
+ for (const p of valueProblems(effective, node.gameData[claim.field])) {
584
+ issues.push({
585
+ severity: p.severity,
586
+ file,
587
+ node: id,
588
+ code: p.code,
589
+ message: `${claim.kind} '${id}' gameData "${claim.field}", as the Patter extension "${claim.extension}" claims it: ${p.message}`
590
+ });
591
+ }
592
+ }
593
+ }
594
+ return issues;
595
+ }
596
+
597
+ // src/export.ts
598
+ function compileBeat(beat, ctx) {
599
+ const values = ctx.extensions.get(beat.id);
600
+ if (values === void 0 && !Object.prototype.hasOwnProperty.call(beat, "extensions")) return beat;
601
+ const { extensions: _source, ...rest } = beat;
602
+ return values !== void 0 ? { ...rest, extensions: values } : rest;
603
+ }
319
604
  function compileEffect(e, foreign) {
320
605
  return { kind: "set", target: e.target, value: compileExpression(e.value, foreign) };
321
606
  }
322
- function compileNode(node, foreign) {
607
+ function compileNode(node, ctx) {
608
+ const { foreign } = ctx;
323
609
  if (node.type === "group") {
324
610
  return {
325
611
  id: node.id,
@@ -328,13 +614,15 @@ function compileNode(node, foreign) {
328
614
  selector: node.selector,
329
615
  shared: node.shared,
330
616
  options: node.options,
331
- children: node.children.map((c) => compileNode(c, foreign)),
617
+ children: node.children.map((c) => compileNode(c, ctx)),
332
618
  padAfterDefault: node.padAfterDefault,
333
619
  // line padding: the default for the beats inside
334
620
  gameData: node.gameData,
621
+ extensions: ctx.extensions.get(node.id),
622
+ // a choice's option's extension values
335
623
  tags: node.tags,
336
624
  // author tags (#215), accumulated at runtime
337
- prompt: node.prompt,
625
+ prompt: node.prompt && compileBeat(node.prompt, ctx),
338
626
  // option-position only (spec §5)
339
627
  secretUntilEligible: node.secretUntilEligible,
340
628
  sticky: node.sticky,
@@ -345,12 +633,13 @@ function compileNode(node, foreign) {
345
633
  id: node.id,
346
634
  type: "snippet",
347
635
  condition: node.condition !== void 0 ? compileExpression(node.condition, foreign) : void 0,
348
- beats: node.beats,
636
+ beats: node.beats && node.beats.map((b) => compileBeat(b, ctx)),
349
637
  onEnter: node.onEnter?.map((e) => compileEffect(e, foreign)),
350
638
  onExit: node.onExit?.map((e) => compileEffect(e, foreign)),
351
639
  padAfterDefault: node.padAfterDefault,
352
640
  // line padding: the default for the beats inside
353
641
  gameData: node.gameData,
642
+ extensions: ctx.extensions.get(node.id),
354
643
  tags: node.tags,
355
644
  // author tags (#215)
356
645
  jump: node.jump,
@@ -359,22 +648,24 @@ function compileNode(node, foreign) {
359
648
  fallback: node.fallback
360
649
  };
361
650
  }
362
- function compileBlock(block, foreign) {
651
+ function compileBlock(block, ctx) {
363
652
  return {
364
653
  id: block.id,
365
654
  type: "block",
366
655
  name: block.name,
367
656
  ...block.gameId ? { gameId: block.gameId } : {},
368
657
  // host-facing address; runtime falls back to the name slug
369
- children: block.children.map((c) => compileNode(c, foreign)),
658
+ children: block.children.map((c) => compileNode(c, ctx)),
370
659
  padAfterDefault: block.padAfterDefault,
371
660
  // line padding: the default for the beats inside
372
661
  gameData: block.gameData,
662
+ extensions: ctx.extensions.get(block.id),
373
663
  tags: block.tags
374
664
  // author tags (#215)
375
665
  };
376
666
  }
377
- function compileScene(scene, foreign) {
667
+ function compileScene(scene, ctx) {
668
+ const { foreign } = ctx;
378
669
  return {
379
670
  id: scene.id,
380
671
  type: "scene",
@@ -384,11 +675,12 @@ function compileScene(scene, foreign) {
384
675
  padAfterDefault: scene.padAfterDefault,
385
676
  // line padding: the default for the beats inside
386
677
  gameData: scene.gameData,
678
+ extensions: ctx.extensions.get(scene.id),
387
679
  tags: scene.tags,
388
680
  // author tags (#215)
389
681
  onEntry: scene.onEntry?.map((e) => compileEffect(e, foreign)),
390
682
  sceneProps: scene.sceneProps,
391
- blocks: scene.blocks.map((b) => compileBlock(b, foreign))
683
+ blocks: scene.blocks.map((b) => compileBlock(b, ctx))
392
684
  };
393
685
  }
394
686
  function externalScopesIn(scenes, external) {
@@ -422,8 +714,12 @@ function exportBundle(input) {
422
714
  version: foreignScopes?.version ?? 1,
423
715
  scopes: [...foreignScopes?.scopes ?? [], ...gameExternal.map((s) => ({ token: s.token }))]
424
716
  });
717
+ const ext = input.extensions;
718
+ const compiledExt = project.extensions !== void 0 || ext !== void 0 && (ext.manifests.length > 0 || ext.shards.length > 0) ? extensionPass({ project, scenes, manifests: ext?.manifests ?? [], shards: ext?.shards ?? [] }).compiled : void 0;
719
+ const extensionSection = compiledExt !== void 0 && Object.keys(compiledExt.bundle).length > 0 ? compiledExt.bundle : void 0;
720
+ const ctx = { foreign: compileScopes, extensions: compiledExt?.nodes ?? /* @__PURE__ */ new Map() };
425
721
  const scenesOut = {};
426
- for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, compileScopes);
722
+ for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, ctx);
427
723
  const externalScopes = externalScopesIn(scenesOut, /* @__PURE__ */ new Set([...EXTERNAL_SCOPES, ...gameExternal.map((s) => s.token)]));
428
724
  const qualifiers = usedQualifiers(project, scenes).map((q) => ({ gameId: q.gameId, name: q.name }));
429
725
  const usedPairs = new Set(usedSpeakerPairs(scenes).map((p) => `${p.character}\0${p.qualifier}`));
@@ -454,7 +750,9 @@ function exportBundle(input) {
454
750
  properties: project.properties ?? [],
455
751
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
456
752
  ...qualifiers.length > 0 ? { qualifiers } : {},
457
- ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
753
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
754
+ // Each node's values are in `scenes`; the declarations and the project's own values are here.
755
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {}
458
756
  }, { trailingComma: false })),
459
757
  // Structure-only fingerprint (live bundle refresh): the same hash with the string tables left
460
758
  // out. Same structureHash + a different hash = a text-only edit, safe to swap in place with
@@ -465,7 +763,8 @@ function exportBundle(input) {
465
763
  properties: project.properties ?? [],
466
764
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
467
765
  ...qualifiers.length > 0 ? { qualifiers } : {},
468
- ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
766
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
767
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {}
469
768
  }, { trailingComma: false }))
470
769
  },
471
770
  voiced: project.voiced ?? false,
@@ -494,6 +793,10 @@ function exportBundle(input) {
494
793
  gameDataFields: project.gameDataFields && Object.fromEntries(
495
794
  Object.entries(project.gameDataFields).map(([kind, fields]) => [kind, resolved(fields)])
496
795
  ),
796
+ // The extensions the project takes: each one's version, its compiled declarations by node kind (every
797
+ // default resolved, editor-only fields and the editor's words left out), the project node's values, and
798
+ // its settings, each the project's value or else the default.
799
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {},
497
800
  // Closed-caption delimiters ride along only when the project pins a non-default pair (#214); the
498
801
  // runtime falls back to `[` / `]` when absent, so default projects keep their exact prior bundle.
499
802
  ...project.closedCaptions ? { closedCaptions: project.closedCaptions } : {},
@@ -508,6 +811,8 @@ export {
508
811
  PATTER_SCOPE,
509
812
  compileExpression,
510
813
  exportBundle,
814
+ extensionNodes,
815
+ extensionPass,
511
816
  externalGameScopes,
512
817
  hostScopesToSpec4 as hostScopesToSpec,
513
818
  projectScopes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patterkit/compiler",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Patter compiler: compile condition/effect expressions, validate them, and export source to the runtime bundle.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,10 +34,11 @@
34
34
  "build": "tsup src/index.ts --format esm,cjs --dts --clean"
35
35
  },
36
36
  "dependencies": {
37
- "@patterkit/model": "0.15.0",
38
- "@patterkit/core": "0.8.0",
39
- "@patterkit/dialect": "0.2.12",
40
- "@wildwinter/expr": "^0.6.0"
37
+ "@patterkit/model": "0.17.0",
38
+ "@patterkit/core": "0.9.1",
39
+ "@patterkit/dialect": "0.2.14",
40
+ "@wildwinter/expr": "^0.6.0",
41
+ "@wildwinter/toolkit": "^0.2.1"
41
42
  },
42
43
  "peerDependencies": {
43
44
  "@wildwinter/scoperegistry": ">=0.9.0 <1.0.0"