@patterkit/compiler 0.7.1 → 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) {
@@ -154,7 +156,7 @@ function validateConditions(input, options = {}) {
154
156
  const validateElement = (blocks, sceneProps, onEntry, elementId) => {
155
157
  const schema = (0, import_dialect2.buildSchema)(input.project, sceneProps, foreign);
156
158
  const checkedSchema = checked ? (0, import_dialect2.buildSchema)(input.project, sceneProps, checked) : void 0;
157
- const check = (nodeId, field, src) => {
159
+ const check = (nodeId, field, src, assignedTo) => {
158
160
  let ast;
159
161
  try {
160
162
  ast = (0, import_expr.parse)(src, dialect);
@@ -162,7 +164,7 @@ function validateConditions(input, options = {}) {
162
164
  issues.push({ nodeId, field, src, severity: "error", message: e instanceof Error ? e.message : String(e) });
163
165
  return;
164
166
  }
165
- const strict = (0, import_expr.validateExpr)(ast, schema, dialect);
167
+ const strict = [...(0, import_expr.validateExpr)(ast, schema, dialect), ...assignedTo ? (0, import_expr.validateAssignment)(assignedTo, ast, schema, dialect) : []];
166
168
  for (const iss of strict) {
167
169
  issues.push({ nodeId, field, src, severity: iss.severity, message: iss.message });
168
170
  }
@@ -215,7 +217,7 @@ function validateConditions(input, options = {}) {
215
217
  const checkEffects = (nodeId, list, label) => {
216
218
  for (const e of list ?? []) {
217
219
  checkTarget(nodeId, `${label}.set`, e.target);
218
- check(nodeId, `${label}.set`, e.value);
220
+ check(nodeId, `${label}.set`, e.value, TARGET_REF.test(e.target) ? (0, import_dialect2.splitRef)(e.target, isScopeToken) : void 0);
219
221
  }
220
222
  };
221
223
  checkEffects(elementId, onEntry, "onEntry");
@@ -346,13 +348,279 @@ function readOnlyForeignTargets(spec) {
346
348
  var import_dialect4 = require("@patterkit/dialect");
347
349
 
348
350
  // src/export.ts
351
+ var import_scoperegistry = require("@wildwinter/scoperegistry");
349
352
  var import_core = require("@patterkit/core");
350
353
  var import_dialect3 = require("@patterkit/dialect");
354
+ var import_model3 = require("@patterkit/model");
355
+
356
+ // src/extensions.ts
351
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
+ }
352
619
  function compileEffect(e, foreign) {
353
620
  return { kind: "set", target: e.target, value: compileExpression(e.value, foreign) };
354
621
  }
355
- function compileNode(node, foreign) {
622
+ function compileNode(node, ctx) {
623
+ const { foreign } = ctx;
356
624
  if (node.type === "group") {
357
625
  return {
358
626
  id: node.id,
@@ -361,13 +629,15 @@ function compileNode(node, foreign) {
361
629
  selector: node.selector,
362
630
  shared: node.shared,
363
631
  options: node.options,
364
- children: node.children.map((c) => compileNode(c, foreign)),
632
+ children: node.children.map((c) => compileNode(c, ctx)),
365
633
  padAfterDefault: node.padAfterDefault,
366
634
  // line padding: the default for the beats inside
367
635
  gameData: node.gameData,
636
+ extensions: ctx.extensions.get(node.id),
637
+ // a choice's option's extension values
368
638
  tags: node.tags,
369
639
  // author tags (#215), accumulated at runtime
370
- prompt: node.prompt,
640
+ prompt: node.prompt && compileBeat(node.prompt, ctx),
371
641
  // option-position only (spec §5)
372
642
  secretUntilEligible: node.secretUntilEligible,
373
643
  sticky: node.sticky,
@@ -378,12 +648,13 @@ function compileNode(node, foreign) {
378
648
  id: node.id,
379
649
  type: "snippet",
380
650
  condition: node.condition !== void 0 ? compileExpression(node.condition, foreign) : void 0,
381
- beats: node.beats,
651
+ beats: node.beats && node.beats.map((b) => compileBeat(b, ctx)),
382
652
  onEnter: node.onEnter?.map((e) => compileEffect(e, foreign)),
383
653
  onExit: node.onExit?.map((e) => compileEffect(e, foreign)),
384
654
  padAfterDefault: node.padAfterDefault,
385
655
  // line padding: the default for the beats inside
386
656
  gameData: node.gameData,
657
+ extensions: ctx.extensions.get(node.id),
387
658
  tags: node.tags,
388
659
  // author tags (#215)
389
660
  jump: node.jump,
@@ -392,22 +663,24 @@ function compileNode(node, foreign) {
392
663
  fallback: node.fallback
393
664
  };
394
665
  }
395
- function compileBlock(block, foreign) {
666
+ function compileBlock(block, ctx) {
396
667
  return {
397
668
  id: block.id,
398
669
  type: "block",
399
670
  name: block.name,
400
671
  ...block.gameId ? { gameId: block.gameId } : {},
401
672
  // host-facing address; runtime falls back to the name slug
402
- children: block.children.map((c) => compileNode(c, foreign)),
673
+ children: block.children.map((c) => compileNode(c, ctx)),
403
674
  padAfterDefault: block.padAfterDefault,
404
675
  // line padding: the default for the beats inside
405
676
  gameData: block.gameData,
677
+ extensions: ctx.extensions.get(block.id),
406
678
  tags: block.tags
407
679
  // author tags (#215)
408
680
  };
409
681
  }
410
- function compileScene(scene, foreign) {
682
+ function compileScene(scene, ctx) {
683
+ const { foreign } = ctx;
411
684
  return {
412
685
  id: scene.id,
413
686
  type: "scene",
@@ -417,11 +690,12 @@ function compileScene(scene, foreign) {
417
690
  padAfterDefault: scene.padAfterDefault,
418
691
  // line padding: the default for the beats inside
419
692
  gameData: scene.gameData,
693
+ extensions: ctx.extensions.get(scene.id),
420
694
  tags: scene.tags,
421
695
  // author tags (#215)
422
696
  onEntry: scene.onEntry?.map((e) => compileEffect(e, foreign)),
423
697
  sceneProps: scene.sceneProps,
424
- blocks: scene.blocks.map((b) => compileBlock(b, foreign))
698
+ blocks: scene.blocks.map((b) => compileBlock(b, ctx))
425
699
  };
426
700
  }
427
701
  function externalScopesIn(scenes, external) {
@@ -443,6 +717,9 @@ function externalScopesIn(scenes, external) {
443
717
  walk(scenes);
444
718
  return [...found].sort();
445
719
  }
720
+ function resolved(decls) {
721
+ return decls?.map((d) => d.default === void 0 ? { ...d, default: (0, import_scoperegistry.defaultFor)(d) } : d);
722
+ }
446
723
  function exportBundle(input) {
447
724
  const { project, scenes, locales = [], includeLocales } = input;
448
725
  const hostScopes = projectScopes(project, input.gameScopes).host;
@@ -452,12 +729,16 @@ function exportBundle(input) {
452
729
  version: foreignScopes?.version ?? 1,
453
730
  scopes: [...foreignScopes?.scopes ?? [], ...gameExternal.map((s) => ({ token: s.token }))]
454
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() };
455
736
  const scenesOut = {};
456
- for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, compileScopes);
737
+ for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, ctx);
457
738
  const externalScopes = externalScopesIn(scenesOut, /* @__PURE__ */ new Set([...import_dialect3.EXTERNAL_SCOPES, ...gameExternal.map((s) => s.token)]));
458
- const qualifiers = (0, import_model2.usedQualifiers)(project, scenes).map((q) => ({ gameId: q.gameId, name: q.name }));
459
- const usedPairs = new Set((0, import_model2.usedSpeakerPairs)(scenes).map((p) => `${p.character}\0${p.qualifier}`));
460
- 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;
461
742
  const strings = {};
462
743
  for (const loc of locales) {
463
744
  if (includeLocales && !includeLocales.includes(loc.locale)) continue;
@@ -484,7 +765,9 @@ function exportBundle(input) {
484
765
  properties: project.properties ?? [],
485
766
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
486
767
  ...qualifiers.length > 0 ? { qualifiers } : {},
487
- ...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 } : {}
488
771
  }, { trailingComma: false })),
489
772
  // Structure-only fingerprint (live bundle refresh): the same hash with the string tables left
490
773
  // out. Same structureHash + a different hash = a text-only edit, safe to swap in place with
@@ -495,7 +778,8 @@ function exportBundle(input) {
495
778
  properties: project.properties ?? [],
496
779
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
497
780
  ...qualifiers.length > 0 ? { qualifiers } : {},
498
- ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
781
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
782
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {}
499
783
  }, { trailingComma: false }))
500
784
  },
501
785
  voiced: project.voiced ?? false,
@@ -517,13 +801,20 @@ function exportBundle(input) {
517
801
  ...speakerPattern !== void 0 ? { speakerPattern } : {},
518
802
  // The project's own line-padding default, only when it sets one: absent, every runtime uses the built-in one.
519
803
  ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
520
- properties: project.properties,
804
+ properties: resolved(project.properties),
805
+ // A host scope's declarations ship as written: there, "no default" is the game's to supply, and saying
806
+ // so is what an integrator reads the bundle for.
521
807
  scopeRegistry: hostScopes,
522
- gameDataFields: project.gameDataFields,
808
+ gameDataFields: project.gameDataFields && Object.fromEntries(
809
+ Object.entries(project.gameDataFields).map(([kind, fields]) => [kind, resolved(fields)])
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 } : {},
523
814
  // Closed-caption delimiters ride along only when the project pins a non-default pair (#214); the
524
815
  // runtime falls back to `[` / `]` when absent, so default projects keep their exact prior bundle.
525
816
  ...project.closedCaptions ? { closedCaptions: project.closedCaptions } : {},
526
- scenes: scenesOut,
817
+ scenes: Object.fromEntries(Object.entries(scenesOut).map(([id, sc]) => [id, sc.sceneProps ? { ...sc, sceneProps: resolved(sc.sceneProps) } : sc])),
527
818
  strings,
528
819
  // Other engines' scopes the content names, so the engine can say when the game has not registered one.
529
820
  ...externalScopes.length > 0 ? { externalScopes } : {}
@@ -535,6 +826,8 @@ function exportBundle(input) {
535
826
  PATTER_SCOPE,
536
827
  compileExpression,
537
828
  exportBundle,
829
+ extensionNodes,
830
+ extensionPass,
538
831
  externalGameScopes,
539
832
  hostScopesToSpec,
540
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
@@ -1,5 +1,5 @@
1
1
  // src/expressions.ts
2
- import { compile, parse, validateExpr } from "@wildwinter/expr";
2
+ import { compile, parse, validateAssignment, validateExpr } from "@wildwinter/expr";
3
3
  import { dialectWithForeignScopes, buildSchema, extractSlots, splitRef, withEngineScopes, hostScopesToSpec as hostScopesToSpec2 } from "@patterkit/dialect";
4
4
  import { referenceNote } from "@wildwinter/scoperegistry/scopes";
5
5
 
@@ -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) {
@@ -120,7 +120,7 @@ function validateConditions(input, options = {}) {
120
120
  const validateElement = (blocks, sceneProps, onEntry, elementId) => {
121
121
  const schema = buildSchema(input.project, sceneProps, foreign);
122
122
  const checkedSchema = checked ? buildSchema(input.project, sceneProps, checked) : void 0;
123
- const check = (nodeId, field, src) => {
123
+ const check = (nodeId, field, src, assignedTo) => {
124
124
  let ast;
125
125
  try {
126
126
  ast = parse(src, dialect);
@@ -128,7 +128,7 @@ function validateConditions(input, options = {}) {
128
128
  issues.push({ nodeId, field, src, severity: "error", message: e instanceof Error ? e.message : String(e) });
129
129
  return;
130
130
  }
131
- const strict = validateExpr(ast, schema, dialect);
131
+ const strict = [...validateExpr(ast, schema, dialect), ...assignedTo ? validateAssignment(assignedTo, ast, schema, dialect) : []];
132
132
  for (const iss of strict) {
133
133
  issues.push({ nodeId, field, src, severity: iss.severity, message: iss.message });
134
134
  }
@@ -181,7 +181,7 @@ function validateConditions(input, options = {}) {
181
181
  const checkEffects = (nodeId, list, label) => {
182
182
  for (const e of list ?? []) {
183
183
  checkTarget(nodeId, `${label}.set`, e.target);
184
- check(nodeId, `${label}.set`, e.value);
184
+ check(nodeId, `${label}.set`, e.value, TARGET_REF.test(e.target) ? splitRef(e.target, isScopeToken) : void 0);
185
185
  }
186
186
  };
187
187
  checkEffects(elementId, onEntry, "onEntry");
@@ -312,13 +312,290 @@ function readOnlyForeignTargets(spec) {
312
312
  import { hostScopesToSpec as hostScopesToSpec4, EXTERNAL_SCOPES as EXTERNAL_SCOPES2 } from "@patterkit/dialect";
313
313
 
314
314
  // src/export.ts
315
+ import { defaultFor } from "@wildwinter/scoperegistry";
315
316
  import { canonicalStringify, hash32 } from "@patterkit/core";
316
317
  import { hostScopesToSpec as hostScopesToSpec3, withEngineScopes as withEngineScopes2, EXTERNAL_SCOPES } from "@patterkit/dialect";
317
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
+ }
318
594
  function compileEffect(e, foreign) {
319
595
  return { kind: "set", target: e.target, value: compileExpression(e.value, foreign) };
320
596
  }
321
- function compileNode(node, foreign) {
597
+ function compileNode(node, ctx) {
598
+ const { foreign } = ctx;
322
599
  if (node.type === "group") {
323
600
  return {
324
601
  id: node.id,
@@ -327,13 +604,15 @@ function compileNode(node, foreign) {
327
604
  selector: node.selector,
328
605
  shared: node.shared,
329
606
  options: node.options,
330
- children: node.children.map((c) => compileNode(c, foreign)),
607
+ children: node.children.map((c) => compileNode(c, ctx)),
331
608
  padAfterDefault: node.padAfterDefault,
332
609
  // line padding: the default for the beats inside
333
610
  gameData: node.gameData,
611
+ extensions: ctx.extensions.get(node.id),
612
+ // a choice's option's extension values
334
613
  tags: node.tags,
335
614
  // author tags (#215), accumulated at runtime
336
- prompt: node.prompt,
615
+ prompt: node.prompt && compileBeat(node.prompt, ctx),
337
616
  // option-position only (spec §5)
338
617
  secretUntilEligible: node.secretUntilEligible,
339
618
  sticky: node.sticky,
@@ -344,12 +623,13 @@ function compileNode(node, foreign) {
344
623
  id: node.id,
345
624
  type: "snippet",
346
625
  condition: node.condition !== void 0 ? compileExpression(node.condition, foreign) : void 0,
347
- beats: node.beats,
626
+ beats: node.beats && node.beats.map((b) => compileBeat(b, ctx)),
348
627
  onEnter: node.onEnter?.map((e) => compileEffect(e, foreign)),
349
628
  onExit: node.onExit?.map((e) => compileEffect(e, foreign)),
350
629
  padAfterDefault: node.padAfterDefault,
351
630
  // line padding: the default for the beats inside
352
631
  gameData: node.gameData,
632
+ extensions: ctx.extensions.get(node.id),
353
633
  tags: node.tags,
354
634
  // author tags (#215)
355
635
  jump: node.jump,
@@ -358,22 +638,24 @@ function compileNode(node, foreign) {
358
638
  fallback: node.fallback
359
639
  };
360
640
  }
361
- function compileBlock(block, foreign) {
641
+ function compileBlock(block, ctx) {
362
642
  return {
363
643
  id: block.id,
364
644
  type: "block",
365
645
  name: block.name,
366
646
  ...block.gameId ? { gameId: block.gameId } : {},
367
647
  // host-facing address; runtime falls back to the name slug
368
- children: block.children.map((c) => compileNode(c, foreign)),
648
+ children: block.children.map((c) => compileNode(c, ctx)),
369
649
  padAfterDefault: block.padAfterDefault,
370
650
  // line padding: the default for the beats inside
371
651
  gameData: block.gameData,
652
+ extensions: ctx.extensions.get(block.id),
372
653
  tags: block.tags
373
654
  // author tags (#215)
374
655
  };
375
656
  }
376
- function compileScene(scene, foreign) {
657
+ function compileScene(scene, ctx) {
658
+ const { foreign } = ctx;
377
659
  return {
378
660
  id: scene.id,
379
661
  type: "scene",
@@ -383,11 +665,12 @@ function compileScene(scene, foreign) {
383
665
  padAfterDefault: scene.padAfterDefault,
384
666
  // line padding: the default for the beats inside
385
667
  gameData: scene.gameData,
668
+ extensions: ctx.extensions.get(scene.id),
386
669
  tags: scene.tags,
387
670
  // author tags (#215)
388
671
  onEntry: scene.onEntry?.map((e) => compileEffect(e, foreign)),
389
672
  sceneProps: scene.sceneProps,
390
- blocks: scene.blocks.map((b) => compileBlock(b, foreign))
673
+ blocks: scene.blocks.map((b) => compileBlock(b, ctx))
391
674
  };
392
675
  }
393
676
  function externalScopesIn(scenes, external) {
@@ -409,6 +692,9 @@ function externalScopesIn(scenes, external) {
409
692
  walk(scenes);
410
693
  return [...found].sort();
411
694
  }
695
+ function resolved(decls) {
696
+ return decls?.map((d) => d.default === void 0 ? { ...d, default: defaultFor(d) } : d);
697
+ }
412
698
  function exportBundle(input) {
413
699
  const { project, scenes, locales = [], includeLocales } = input;
414
700
  const hostScopes = projectScopes(project, input.gameScopes).host;
@@ -418,8 +704,12 @@ function exportBundle(input) {
418
704
  version: foreignScopes?.version ?? 1,
419
705
  scopes: [...foreignScopes?.scopes ?? [], ...gameExternal.map((s) => ({ token: s.token }))]
420
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() };
421
711
  const scenesOut = {};
422
- for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, compileScopes);
712
+ for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, ctx);
423
713
  const externalScopes = externalScopesIn(scenesOut, /* @__PURE__ */ new Set([...EXTERNAL_SCOPES, ...gameExternal.map((s) => s.token)]));
424
714
  const qualifiers = usedQualifiers(project, scenes).map((q) => ({ gameId: q.gameId, name: q.name }));
425
715
  const usedPairs = new Set(usedSpeakerPairs(scenes).map((p) => `${p.character}\0${p.qualifier}`));
@@ -450,7 +740,9 @@ function exportBundle(input) {
450
740
  properties: project.properties ?? [],
451
741
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
452
742
  ...qualifiers.length > 0 ? { qualifiers } : {},
453
- ...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 } : {}
454
746
  }, { trailingComma: false })),
455
747
  // Structure-only fingerprint (live bundle refresh): the same hash with the string tables left
456
748
  // out. Same structureHash + a different hash = a text-only edit, safe to swap in place with
@@ -461,7 +753,8 @@ function exportBundle(input) {
461
753
  properties: project.properties ?? [],
462
754
  ...foreignScopes ? { scopeRegistry: foreignScopes } : {},
463
755
  ...qualifiers.length > 0 ? { qualifiers } : {},
464
- ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {}
756
+ ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
757
+ ...extensionSection !== void 0 ? { extensions: extensionSection } : {}
465
758
  }, { trailingComma: false }))
466
759
  },
467
760
  voiced: project.voiced ?? false,
@@ -483,13 +776,20 @@ function exportBundle(input) {
483
776
  ...speakerPattern !== void 0 ? { speakerPattern } : {},
484
777
  // The project's own line-padding default, only when it sets one: absent, every runtime uses the built-in one.
485
778
  ...typeof project.padAfterDefault === "number" ? { padAfterDefault: project.padAfterDefault } : {},
486
- properties: project.properties,
779
+ properties: resolved(project.properties),
780
+ // A host scope's declarations ship as written: there, "no default" is the game's to supply, and saying
781
+ // so is what an integrator reads the bundle for.
487
782
  scopeRegistry: hostScopes,
488
- gameDataFields: project.gameDataFields,
783
+ gameDataFields: project.gameDataFields && Object.fromEntries(
784
+ Object.entries(project.gameDataFields).map(([kind, fields]) => [kind, resolved(fields)])
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 } : {},
489
789
  // Closed-caption delimiters ride along only when the project pins a non-default pair (#214); the
490
790
  // runtime falls back to `[` / `]` when absent, so default projects keep their exact prior bundle.
491
791
  ...project.closedCaptions ? { closedCaptions: project.closedCaptions } : {},
492
- scenes: scenesOut,
792
+ scenes: Object.fromEntries(Object.entries(scenesOut).map(([id, sc]) => [id, sc.sceneProps ? { ...sc, sceneProps: resolved(sc.sceneProps) } : sc])),
493
793
  strings,
494
794
  // Other engines' scopes the content names, so the engine can say when the game has not registered one.
495
795
  ...externalScopes.length > 0 ? { externalScopes } : {}
@@ -500,6 +800,8 @@ export {
500
800
  PATTER_SCOPE,
501
801
  compileExpression,
502
802
  exportBundle,
803
+ extensionNodes,
804
+ extensionPass,
503
805
  externalGameScopes,
504
806
  hostScopesToSpec4 as hostScopesToSpec,
505
807
  projectScopes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patterkit/compiler",
3
- "version": "0.7.1",
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,15 +34,16 @@
34
34
  "build": "tsup src/index.ts --format esm,cjs --dts --clean"
35
35
  },
36
36
  "dependencies": {
37
- "@patterkit/model": "0.14.0",
38
- "@patterkit/core": "0.7.1",
39
- "@patterkit/dialect": "0.2.11",
40
- "@wildwinter/expr": "^0.5.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
- "@wildwinter/scoperegistry": ">=0.8.0 <1.0.0"
44
+ "@wildwinter/scoperegistry": ">=0.9.0 <1.0.0"
44
45
  },
45
46
  "devDependencies": {
46
- "@wildwinter/scoperegistry": ">=0.8.0 <1.0.0"
47
+ "@wildwinter/scoperegistry": ">=0.9.0 <1.0.0"
47
48
  }
48
49
  }