@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 +317 -24
- package/dist/index.d.cts +77 -2
- package/dist/index.d.ts +77 -2
- package/dist/index.js +324 -22
- package/package.json +8 -7
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
|
|
57
|
-
if (!merged) return { host:
|
|
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
|
|
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 ?
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
459
|
-
const usedPairs = new Set((0,
|
|
460
|
-
const speakerPattern = qualifiers.length > 0 && project.speakerPattern !== void 0 && project.speakerPattern !==
|
|
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
|
|
23
|
-
if (!merged) return { host:
|
|
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
|
|
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 ?
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
38
|
-
"@patterkit/core": "0.
|
|
39
|
-
"@patterkit/dialect": "0.2.
|
|
40
|
-
"@wildwinter/expr": "^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.
|
|
44
|
+
"@wildwinter/scoperegistry": ">=0.9.0 <1.0.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
|
-
"@wildwinter/scoperegistry": ">=0.
|
|
47
|
+
"@wildwinter/scoperegistry": ">=0.9.0 <1.0.0"
|
|
47
48
|
}
|
|
48
49
|
}
|