@patterkit/compiler 0.8.0 → 0.9.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,276 @@ 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
+ for (const [id, manifest] of [...sound.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
559
+ const fields = {};
560
+ for (const kind of Object.keys(manifest.targets.patter ?? {}).sort()) {
561
+ const target = (0, import_extensions.targetOf)(manifest, "patter", kind);
562
+ if (target !== void 0) fields[kind] = (0, import_extensions.compiledTarget)(target);
563
+ }
564
+ const projectEntry = projectValues.get(id);
565
+ bundle[id] = { version: manifest.version, fields, ...projectEntry !== void 0 ? { project: projectEntry } : {} };
566
+ }
567
+ const sortedNodes = new Map([...values.entries()].map(([id, v]) => [id, sortRecord(v)]));
568
+ return { issues, compiled: { bundle, nodes: sortedNodes } };
569
+ }
570
+ function sortRecord(r) {
571
+ return Object.fromEntries(Object.keys(r).sort().map((k) => [k, r[k]]));
572
+ }
573
+ function claimIssues(input, nodes, claims) {
574
+ const issues = [];
575
+ const projectPath = input.projectPath ?? "";
576
+ const declared = input.project.gameDataFields ?? {};
577
+ for (const claim of claims) {
578
+ const decl = (declared[claim.kind] ?? []).find((d) => d.name === claim.field);
579
+ if (decl === void 0) {
580
+ issues.push({
581
+ severity: "warning",
582
+ file: projectPath,
583
+ message: `the Patter extension "${claim.extension}" claims the ${claim.kind} gameData field "${claim.field}", which the project doesn't declare`
584
+ });
585
+ continue;
586
+ }
587
+ if (decl.type !== claim.claim.type) {
588
+ issues.push({
589
+ severity: "error",
590
+ file: projectPath,
591
+ 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}`
592
+ });
593
+ continue;
594
+ }
595
+ for (const [id, node] of nodes) {
596
+ if (node.kind !== claim.kind || node.gameData === void 0 || !own(node.gameData, claim.field)) continue;
597
+ const file = (node.scene !== void 0 ? input.sceneFiles?.[node.scene] : void 0) ?? projectPath;
598
+ for (const p of (0, import_extensions.valueProblems)(claim.claim, node.gameData[claim.field])) {
599
+ issues.push({
600
+ severity: p.severity,
601
+ file,
602
+ node: id,
603
+ code: p.code,
604
+ message: `${claim.kind} '${id}' gameData "${claim.field}", as the Patter extension "${claim.extension}" claims it: ${p.message}`
605
+ });
606
+ }
607
+ }
608
+ }
609
+ return issues;
610
+ }
611
+
612
+ // src/export.ts
613
+ function compileBeat(beat, ctx) {
614
+ const values = ctx.extensions.get(beat.id);
615
+ if (values === void 0 && !Object.prototype.hasOwnProperty.call(beat, "extensions")) return beat;
616
+ const { extensions: _source, ...rest } = beat;
617
+ return values !== void 0 ? { ...rest, extensions: values } : rest;
618
+ }
353
619
  function compileEffect(e, foreign) {
354
620
  return { kind: "set", target: e.target, value: compileExpression(e.value, foreign) };
355
621
  }
356
- function compileNode(node, foreign) {
622
+ function compileNode(node, ctx) {
623
+ const { foreign } = ctx;
357
624
  if (node.type === "group") {
358
625
  return {
359
626
  id: node.id,
@@ -362,13 +629,15 @@ function compileNode(node, foreign) {
362
629
  selector: node.selector,
363
630
  shared: node.shared,
364
631
  options: node.options,
365
- children: node.children.map((c) => compileNode(c, foreign)),
632
+ children: node.children.map((c) => compileNode(c, ctx)),
366
633
  padAfterDefault: node.padAfterDefault,
367
634
  // line padding: the default for the beats inside
368
635
  gameData: node.gameData,
636
+ extensions: ctx.extensions.get(node.id),
637
+ // a choice's option's extension values
369
638
  tags: node.tags,
370
639
  // author tags (#215), accumulated at runtime
371
- prompt: node.prompt,
640
+ prompt: node.prompt && compileBeat(node.prompt, ctx),
372
641
  // option-position only (spec §5)
373
642
  secretUntilEligible: node.secretUntilEligible,
374
643
  sticky: node.sticky,
@@ -379,12 +648,13 @@ function compileNode(node, foreign) {
379
648
  id: node.id,
380
649
  type: "snippet",
381
650
  condition: node.condition !== void 0 ? compileExpression(node.condition, foreign) : void 0,
382
- beats: node.beats,
651
+ beats: node.beats && node.beats.map((b) => compileBeat(b, ctx)),
383
652
  onEnter: node.onEnter?.map((e) => compileEffect(e, foreign)),
384
653
  onExit: node.onExit?.map((e) => compileEffect(e, foreign)),
385
654
  padAfterDefault: node.padAfterDefault,
386
655
  // line padding: the default for the beats inside
387
656
  gameData: node.gameData,
657
+ extensions: ctx.extensions.get(node.id),
388
658
  tags: node.tags,
389
659
  // author tags (#215)
390
660
  jump: node.jump,
@@ -393,22 +663,24 @@ function compileNode(node, foreign) {
393
663
  fallback: node.fallback
394
664
  };
395
665
  }
396
- function compileBlock(block, foreign) {
666
+ function compileBlock(block, ctx) {
397
667
  return {
398
668
  id: block.id,
399
669
  type: "block",
400
670
  name: block.name,
401
671
  ...block.gameId ? { gameId: block.gameId } : {},
402
672
  // host-facing address; runtime falls back to the name slug
403
- children: block.children.map((c) => compileNode(c, foreign)),
673
+ children: block.children.map((c) => compileNode(c, ctx)),
404
674
  padAfterDefault: block.padAfterDefault,
405
675
  // line padding: the default for the beats inside
406
676
  gameData: block.gameData,
677
+ extensions: ctx.extensions.get(block.id),
407
678
  tags: block.tags
408
679
  // author tags (#215)
409
680
  };
410
681
  }
411
- function compileScene(scene, foreign) {
682
+ function compileScene(scene, ctx) {
683
+ const { foreign } = ctx;
412
684
  return {
413
685
  id: scene.id,
414
686
  type: "scene",
@@ -418,11 +690,12 @@ function compileScene(scene, foreign) {
418
690
  padAfterDefault: scene.padAfterDefault,
419
691
  // line padding: the default for the beats inside
420
692
  gameData: scene.gameData,
693
+ extensions: ctx.extensions.get(scene.id),
421
694
  tags: scene.tags,
422
695
  // author tags (#215)
423
696
  onEntry: scene.onEntry?.map((e) => compileEffect(e, foreign)),
424
697
  sceneProps: scene.sceneProps,
425
- blocks: scene.blocks.map((b) => compileBlock(b, foreign))
698
+ blocks: scene.blocks.map((b) => compileBlock(b, ctx))
426
699
  };
427
700
  }
428
701
  function externalScopesIn(scenes, external) {
@@ -456,12 +729,16 @@ function exportBundle(input) {
456
729
  version: foreignScopes?.version ?? 1,
457
730
  scopes: [...foreignScopes?.scopes ?? [], ...gameExternal.map((s) => ({ token: s.token }))]
458
731
  });
732
+ const ext = input.extensions;
733
+ 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;
734
+ const extensionSection = compiledExt !== void 0 && Object.keys(compiledExt.bundle).length > 0 ? compiledExt.bundle : void 0;
735
+ const ctx = { foreign: compileScopes, extensions: compiledExt?.nodes ?? /* @__PURE__ */ new Map() };
459
736
  const scenesOut = {};
460
- for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, compileScopes);
737
+ for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, ctx);
461
738
  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;
739
+ const qualifiers = (0, import_model3.usedQualifiers)(project, scenes).map((q) => ({ gameId: q.gameId, name: q.name }));
740
+ const usedPairs = new Set((0, import_model3.usedSpeakerPairs)(scenes).map((p) => `${p.character}\0${p.qualifier}`));
741
+ const speakerPattern = qualifiers.length > 0 && project.speakerPattern !== void 0 && project.speakerPattern !== import_model3.DEFAULT_SPEAKER_PATTERN ? project.speakerPattern : void 0;
465
742
  const strings = {};
466
743
  for (const loc of locales) {
467
744
  if (includeLocales && !includeLocales.includes(loc.locale)) continue;
@@ -488,7 +765,9 @@ function exportBundle(input) {
488
765
  properties: project.properties ?? [],
489
766
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
490
767
  ...qualifiers.length > 0 ? { qualifiers } : {},
491
- ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
768
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
769
+ // Each node's values are in `scenes`; the declarations and the project's own values are here.
770
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {}
492
771
  }, { trailingComma: false })),
493
772
  // Structure-only fingerprint (live bundle refresh): the same hash with the string tables left
494
773
  // out. Same structureHash + a different hash = a text-only edit, safe to swap in place with
@@ -499,7 +778,8 @@ function exportBundle(input) {
499
778
  properties: project.properties ?? [],
500
779
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
501
780
  ...qualifiers.length > 0 ? { qualifiers } : {},
502
- ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
781
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
782
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {}
503
783
  }, { trailingComma: false }))
504
784
  },
505
785
  voiced: project.voiced ?? false,
@@ -528,6 +808,9 @@ function exportBundle(input) {
528
808
  gameDataFields: project.gameDataFields && Object.fromEntries(
529
809
  Object.entries(project.gameDataFields).map(([kind, fields]) => [kind, resolved(fields)])
530
810
  ),
811
+ // The extensions the project takes: each one's version, its compiled declarations by node kind (every
812
+ // default resolved, editor-only fields and the editor's words left out) and the project node's values.
813
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {},
531
814
  // Closed-caption delimiters ride along only when the project pins a non-default pair (#214); the
532
815
  // runtime falls back to `[` / `]` when absent, so default projects keep their exact prior bundle.
533
816
  ...project.closedCaptions ? { closedCaptions: project.closedCaptions } : {},
@@ -543,6 +826,8 @@ function exportBundle(input) {
543
826
  PATTER_SCOPE,
544
827
  compileExpression,
545
828
  exportBundle,
829
+ extensionNodes,
830
+ extensionPass,
546
831
  externalGameScopes,
547
832
  hostScopesToSpec,
548
833
  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,286 @@ 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
+ entryProblems,
328
+ manifestProblems,
329
+ orphanedEntries,
330
+ resolvedClaims,
331
+ targetOf,
332
+ valueProblems
333
+ } from "@wildwinter/toolkit/extensions";
334
+ function extensionNodes(project, scenes) {
335
+ const nodes = /* @__PURE__ */ new Map();
336
+ const add = (id, kind, scene, gameData) => {
337
+ if (!nodes.has(id)) nodes.set(id, { kind, ...scene !== void 0 ? { scene } : {}, ...gameData !== void 0 ? { gameData } : {} });
338
+ };
339
+ add(project.project.id, "project", void 0, void 0);
340
+ for (const scene of scenes) {
341
+ const s = scene.id;
342
+ add(s, "scene", s, scene.gameData);
343
+ const walk = (children, inChoice) => {
344
+ for (const node of children ?? []) {
345
+ if (node.type === "group") {
346
+ if (inChoice) {
347
+ add(node.id, "option", s, node.gameData);
348
+ if (node.prompt) add(node.prompt.id, node.prompt.kind, s, node.prompt.gameData);
349
+ }
350
+ walk(node.children ?? [], node.selector === "choice");
351
+ continue;
352
+ }
353
+ add(node.id, "snippet", s, node.gameData);
354
+ for (const beat of node.beats ?? []) add(beat.id, beat.kind, s, beat.gameData);
355
+ }
356
+ };
357
+ for (const block of scene.blocks ?? []) {
358
+ add(block.id, "block", s, block.gameData);
359
+ walk(block.children ?? [], false);
360
+ }
361
+ }
362
+ return nodes;
363
+ }
364
+ var issueOf = (file, p, prefix = "", node) => ({
365
+ severity: p.severity,
366
+ file,
367
+ code: p.code,
368
+ ...node !== void 0 ? { node } : {},
369
+ message: p.path === "" ? `${prefix}${p.message}` : `${prefix}${p.path}: ${p.message}`
370
+ });
371
+ var folderOf = (path) => {
372
+ const parts = path.split(/[\\/]/);
373
+ return parts.length >= 2 ? parts[parts.length - 2] : void 0;
374
+ };
375
+ var own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
376
+ function extensionPass(input) {
377
+ const { project, scenes } = input;
378
+ const issues = [];
379
+ const projectPath = input.projectPath ?? "";
380
+ const taken = project.extensions ?? {};
381
+ const manifests = /* @__PURE__ */ new Map();
382
+ for (const m of input.manifests) {
383
+ const problems = manifestProblems(m.manifest);
384
+ for (const p of problems) issues.push(issueOf(m.path, p, "manifest "));
385
+ let sound2 = !problems.some((p) => p.severity === "error");
386
+ const id = m.manifest?.id;
387
+ if (sound2 && id !== m.id) {
388
+ issues.push({
389
+ severity: "error",
390
+ file: m.path,
391
+ message: `the manifest's id is "${String(id)}", but its file is named for "${m.id}"; a manifest is called <id>${MANIFEST_EXTENSION}`
392
+ });
393
+ sound2 = false;
394
+ }
395
+ if (sound2 && m.manifest.targets.patter === void 0) {
396
+ issues.push({
397
+ severity: "warning",
398
+ file: m.path,
399
+ message: `"${m.id}" isn't a Patter extension: its manifest targets no Patter node kind, so it adds nothing here`
400
+ });
401
+ }
402
+ manifests.set(m.id, { path: m.path, manifest: m.manifest, sound: sound2 });
403
+ }
404
+ const takenList = [];
405
+ for (const [id, entry] of Object.entries(taken)) {
406
+ const m = manifests.get(id);
407
+ const version = typeof entry?.version === "string" ? entry.version : "";
408
+ const settings = entry?.settings ?? {};
409
+ if (m === void 0) {
410
+ issues.push({
411
+ severity: "warning",
412
+ file: projectPath,
413
+ 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`
414
+ });
415
+ takenList.push({ id, settings });
416
+ continue;
417
+ }
418
+ if (!m.sound) {
419
+ takenList.push({ id, settings });
420
+ continue;
421
+ }
422
+ const manifest = m.manifest;
423
+ if (version !== manifest.version) {
424
+ issues.push({
425
+ severity: "warning",
426
+ file: projectPath,
427
+ 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)`
428
+ });
429
+ }
430
+ const known = new Set((manifest.settings ?? []).map((s) => s.name));
431
+ for (const [name, value] of Object.entries(settings)) {
432
+ if (!known.has(name)) {
433
+ issues.push({ severity: "warning", file: projectPath, message: `"${id}" has no setting "${name}"; it's ignored` });
434
+ } else if (typeof value !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
435
+ issues.push({
436
+ severity: "error",
437
+ file: projectPath,
438
+ message: `"${id}"'s setting "${name}" names a gameData field, so it's a field name, not ${JSON.stringify(value)}`
439
+ });
440
+ }
441
+ }
442
+ takenList.push({ id, settings, manifest });
443
+ }
444
+ const sound = new Map(takenList.filter((t) => t.manifest !== void 0).map((t) => [t.id, t.manifest]));
445
+ const nodes = extensionNodes(project, scenes);
446
+ const claims = takenList.flatMap((t) => t.manifest ? resolvedClaims(t.manifest, "patter", t.settings) : []);
447
+ for (const p of claimClashes(claims)) issues.push({ severity: p.severity, file: projectPath, code: p.code, message: p.message });
448
+ issues.push(...claimIssues(input, nodes, claims));
449
+ const exists = (kind, id) => kind === "cast" ? (project.cast ?? []).some((c) => c.name === id) : nodes.get(id)?.kind === kind;
450
+ const sceneIds = new Set(scenes.map((s) => s.id));
451
+ const values = /* @__PURE__ */ new Map();
452
+ const projectValues = /* @__PURE__ */ new Map();
453
+ for (const { path, shard } of [...input.shards].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) {
454
+ const folder = folderOf(path);
455
+ if (folder !== void 0 && shard.extension !== folder) {
456
+ issues.push({
457
+ severity: "error",
458
+ file: path,
459
+ message: `this shard says it holds "${String(shard.extension)}", but it sits in the folder for "${folder}"`
460
+ });
461
+ continue;
462
+ }
463
+ const ext = shard.extension;
464
+ if (!own(taken, ext)) {
465
+ issues.push({
466
+ severity: "warning",
467
+ file: path,
468
+ message: `data for the Patter extension "${ext}", which the project doesn't take; it's left as it is, and not compiled`
469
+ });
470
+ continue;
471
+ }
472
+ const entries = shard.entries;
473
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries)) {
474
+ issues.push({ severity: "error", file: path, message: "entries are an object, by node kind and then node id" });
475
+ continue;
476
+ }
477
+ const shardScene = shard.scene === PROJECT_LOCALE_SCENE ? void 0 : shard.scene;
478
+ const sceneKnown = shardScene === void 0 || sceneIds.has(shardScene);
479
+ if (!sceneKnown) {
480
+ issues.push({
481
+ severity: "warning",
482
+ file: path,
483
+ message: `this shard is for scene "${shard.scene}", which the project doesn't have; its entries are compiled where their nodes are`
484
+ });
485
+ }
486
+ for (const p of orphanedEntries(shard, exists)) issues.push(issueOf(path, p));
487
+ const manifest = sound.get(ext);
488
+ if (manifest === void 0) continue;
489
+ for (const [kind, byNode] of Object.entries(entries)) {
490
+ if (!HOSTS.patter.kinds.includes(kind)) {
491
+ issues.push({
492
+ severity: "warning",
493
+ file: path,
494
+ message: `entries.${kind}: "${kind}" isn't a Patter node kind; these entries are kept, and not compiled`
495
+ });
496
+ continue;
497
+ }
498
+ const target = targetOf(manifest, "patter", kind);
499
+ if (target === void 0) {
500
+ issues.push({
501
+ severity: "warning",
502
+ file: path,
503
+ message: `entries.${kind}: "${ext}" declares nothing for a ${kind}; these entries are kept, and not compiled`
504
+ });
505
+ continue;
506
+ }
507
+ if (typeof byNode !== "object" || byNode === null || Array.isArray(byNode)) continue;
508
+ for (const [id, entry] of Object.entries(byNode)) {
509
+ const node = nodes.get(id);
510
+ if (node === void 0 || node.kind !== kind) continue;
511
+ if (sceneKnown && node.scene !== shardScene) {
512
+ issues.push({
513
+ severity: "warning",
514
+ file: path,
515
+ node: id,
516
+ 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`
517
+ });
518
+ }
519
+ const problems = entryProblems(target, entry, { exists });
520
+ for (const p of problems) issues.push(issueOf(path, p, `entries.${kind}.${id}${p.path === "" ? ": " : "."}`, id));
521
+ if (problems.some((p) => p.severity === "error")) continue;
522
+ const compiled = compileEntry(target, entry);
523
+ if (compiled === void 0) continue;
524
+ if (kind === "project") {
525
+ projectValues.set(ext, compiled);
526
+ continue;
527
+ }
528
+ values.set(id, { ...values.get(id) ?? {}, [ext]: compiled });
529
+ }
530
+ }
531
+ }
532
+ const bundle = {};
533
+ for (const [id, manifest] of [...sound.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
534
+ const fields = {};
535
+ for (const kind of Object.keys(manifest.targets.patter ?? {}).sort()) {
536
+ const target = targetOf(manifest, "patter", kind);
537
+ if (target !== void 0) fields[kind] = compiledTarget(target);
538
+ }
539
+ const projectEntry = projectValues.get(id);
540
+ bundle[id] = { version: manifest.version, fields, ...projectEntry !== void 0 ? { project: projectEntry } : {} };
541
+ }
542
+ const sortedNodes = new Map([...values.entries()].map(([id, v]) => [id, sortRecord(v)]));
543
+ return { issues, compiled: { bundle, nodes: sortedNodes } };
544
+ }
545
+ function sortRecord(r) {
546
+ return Object.fromEntries(Object.keys(r).sort().map((k) => [k, r[k]]));
547
+ }
548
+ function claimIssues(input, nodes, claims) {
549
+ const issues = [];
550
+ const projectPath = input.projectPath ?? "";
551
+ const declared = input.project.gameDataFields ?? {};
552
+ for (const claim of claims) {
553
+ const decl = (declared[claim.kind] ?? []).find((d) => d.name === claim.field);
554
+ if (decl === void 0) {
555
+ issues.push({
556
+ severity: "warning",
557
+ file: projectPath,
558
+ message: `the Patter extension "${claim.extension}" claims the ${claim.kind} gameData field "${claim.field}", which the project doesn't declare`
559
+ });
560
+ continue;
561
+ }
562
+ if (decl.type !== claim.claim.type) {
563
+ issues.push({
564
+ severity: "error",
565
+ file: projectPath,
566
+ 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}`
567
+ });
568
+ continue;
569
+ }
570
+ for (const [id, node] of nodes) {
571
+ if (node.kind !== claim.kind || node.gameData === void 0 || !own(node.gameData, claim.field)) continue;
572
+ const file = (node.scene !== void 0 ? input.sceneFiles?.[node.scene] : void 0) ?? projectPath;
573
+ for (const p of valueProblems(claim.claim, node.gameData[claim.field])) {
574
+ issues.push({
575
+ severity: p.severity,
576
+ file,
577
+ node: id,
578
+ code: p.code,
579
+ message: `${claim.kind} '${id}' gameData "${claim.field}", as the Patter extension "${claim.extension}" claims it: ${p.message}`
580
+ });
581
+ }
582
+ }
583
+ }
584
+ return issues;
585
+ }
586
+
587
+ // src/export.ts
588
+ function compileBeat(beat, ctx) {
589
+ const values = ctx.extensions.get(beat.id);
590
+ if (values === void 0 && !Object.prototype.hasOwnProperty.call(beat, "extensions")) return beat;
591
+ const { extensions: _source, ...rest } = beat;
592
+ return values !== void 0 ? { ...rest, extensions: values } : rest;
593
+ }
319
594
  function compileEffect(e, foreign) {
320
595
  return { kind: "set", target: e.target, value: compileExpression(e.value, foreign) };
321
596
  }
322
- function compileNode(node, foreign) {
597
+ function compileNode(node, ctx) {
598
+ const { foreign } = ctx;
323
599
  if (node.type === "group") {
324
600
  return {
325
601
  id: node.id,
@@ -328,13 +604,15 @@ function compileNode(node, foreign) {
328
604
  selector: node.selector,
329
605
  shared: node.shared,
330
606
  options: node.options,
331
- children: node.children.map((c) => compileNode(c, foreign)),
607
+ children: node.children.map((c) => compileNode(c, ctx)),
332
608
  padAfterDefault: node.padAfterDefault,
333
609
  // line padding: the default for the beats inside
334
610
  gameData: node.gameData,
611
+ extensions: ctx.extensions.get(node.id),
612
+ // a choice's option's extension values
335
613
  tags: node.tags,
336
614
  // author tags (#215), accumulated at runtime
337
- prompt: node.prompt,
615
+ prompt: node.prompt && compileBeat(node.prompt, ctx),
338
616
  // option-position only (spec §5)
339
617
  secretUntilEligible: node.secretUntilEligible,
340
618
  sticky: node.sticky,
@@ -345,12 +623,13 @@ function compileNode(node, foreign) {
345
623
  id: node.id,
346
624
  type: "snippet",
347
625
  condition: node.condition !== void 0 ? compileExpression(node.condition, foreign) : void 0,
348
- beats: node.beats,
626
+ beats: node.beats && node.beats.map((b) => compileBeat(b, ctx)),
349
627
  onEnter: node.onEnter?.map((e) => compileEffect(e, foreign)),
350
628
  onExit: node.onExit?.map((e) => compileEffect(e, foreign)),
351
629
  padAfterDefault: node.padAfterDefault,
352
630
  // line padding: the default for the beats inside
353
631
  gameData: node.gameData,
632
+ extensions: ctx.extensions.get(node.id),
354
633
  tags: node.tags,
355
634
  // author tags (#215)
356
635
  jump: node.jump,
@@ -359,22 +638,24 @@ function compileNode(node, foreign) {
359
638
  fallback: node.fallback
360
639
  };
361
640
  }
362
- function compileBlock(block, foreign) {
641
+ function compileBlock(block, ctx) {
363
642
  return {
364
643
  id: block.id,
365
644
  type: "block",
366
645
  name: block.name,
367
646
  ...block.gameId ? { gameId: block.gameId } : {},
368
647
  // host-facing address; runtime falls back to the name slug
369
- children: block.children.map((c) => compileNode(c, foreign)),
648
+ children: block.children.map((c) => compileNode(c, ctx)),
370
649
  padAfterDefault: block.padAfterDefault,
371
650
  // line padding: the default for the beats inside
372
651
  gameData: block.gameData,
652
+ extensions: ctx.extensions.get(block.id),
373
653
  tags: block.tags
374
654
  // author tags (#215)
375
655
  };
376
656
  }
377
- function compileScene(scene, foreign) {
657
+ function compileScene(scene, ctx) {
658
+ const { foreign } = ctx;
378
659
  return {
379
660
  id: scene.id,
380
661
  type: "scene",
@@ -384,11 +665,12 @@ function compileScene(scene, foreign) {
384
665
  padAfterDefault: scene.padAfterDefault,
385
666
  // line padding: the default for the beats inside
386
667
  gameData: scene.gameData,
668
+ extensions: ctx.extensions.get(scene.id),
387
669
  tags: scene.tags,
388
670
  // author tags (#215)
389
671
  onEntry: scene.onEntry?.map((e) => compileEffect(e, foreign)),
390
672
  sceneProps: scene.sceneProps,
391
- blocks: scene.blocks.map((b) => compileBlock(b, foreign))
673
+ blocks: scene.blocks.map((b) => compileBlock(b, ctx))
392
674
  };
393
675
  }
394
676
  function externalScopesIn(scenes, external) {
@@ -422,8 +704,12 @@ function exportBundle(input) {
422
704
  version: foreignScopes?.version ?? 1,
423
705
  scopes: [...foreignScopes?.scopes ?? [], ...gameExternal.map((s) => ({ token: s.token }))]
424
706
  });
707
+ const ext = input.extensions;
708
+ 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;
709
+ const extensionSection = compiledExt !== void 0 && Object.keys(compiledExt.bundle).length > 0 ? compiledExt.bundle : void 0;
710
+ const ctx = { foreign: compileScopes, extensions: compiledExt?.nodes ?? /* @__PURE__ */ new Map() };
425
711
  const scenesOut = {};
426
- for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, compileScopes);
712
+ for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, ctx);
427
713
  const externalScopes = externalScopesIn(scenesOut, /* @__PURE__ */ new Set([...EXTERNAL_SCOPES, ...gameExternal.map((s) => s.token)]));
428
714
  const qualifiers = usedQualifiers(project, scenes).map((q) => ({ gameId: q.gameId, name: q.name }));
429
715
  const usedPairs = new Set(usedSpeakerPairs(scenes).map((p) => `${p.character}\0${p.qualifier}`));
@@ -454,7 +740,9 @@ function exportBundle(input) {
454
740
  properties: project.properties ?? [],
455
741
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
456
742
  ...qualifiers.length > 0 ? { qualifiers } : {},
457
- ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
743
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
744
+ // Each node's values are in `scenes`; the declarations and the project's own values are here.
745
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {}
458
746
  }, { trailingComma: false })),
459
747
  // Structure-only fingerprint (live bundle refresh): the same hash with the string tables left
460
748
  // out. Same structureHash + a different hash = a text-only edit, safe to swap in place with
@@ -465,7 +753,8 @@ function exportBundle(input) {
465
753
  properties: project.properties ?? [],
466
754
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
467
755
  ...qualifiers.length > 0 ? { qualifiers } : {},
468
- ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
756
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
757
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {}
469
758
  }, { trailingComma: false }))
470
759
  },
471
760
  voiced: project.voiced ?? false,
@@ -494,6 +783,9 @@ function exportBundle(input) {
494
783
  gameDataFields: project.gameDataFields && Object.fromEntries(
495
784
  Object.entries(project.gameDataFields).map(([kind, fields]) => [kind, resolved(fields)])
496
785
  ),
786
+ // The extensions the project takes: each one's version, its compiled declarations by node kind (every
787
+ // default resolved, editor-only fields and the editor's words left out) and the project node's values.
788
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {},
497
789
  // Closed-caption delimiters ride along only when the project pins a non-default pair (#214); the
498
790
  // runtime falls back to `[` / `]` when absent, so default projects keep their exact prior bundle.
499
791
  ...project.closedCaptions ? { closedCaptions: project.closedCaptions } : {},
@@ -508,6 +800,8 @@ export {
508
800
  PATTER_SCOPE,
509
801
  compileExpression,
510
802
  exportBundle,
803
+ extensionNodes,
804
+ extensionPass,
511
805
  externalGameScopes,
512
806
  hostScopesToSpec4 as hostScopesToSpec,
513
807
  projectScopes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patterkit/compiler",
3
- "version": "0.8.0",
3
+ "version": "0.9.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.16.0",
38
+ "@patterkit/core": "0.9.0",
39
+ "@patterkit/dialect": "0.2.13",
40
+ "@wildwinter/expr": "^0.6.0",
41
+ "@wildwinter/toolkit": "^0.2.0"
41
42
  },
42
43
  "peerDependencies": {
43
44
  "@wildwinter/scoperegistry": ">=0.9.0 <1.0.0"