@patterkit/compiler 0.3.0 → 0.4.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 +137 -20
- package/dist/index.d.cts +55 -10
- package/dist/index.d.ts +55 -10
- package/dist/index.js +127 -14
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -20,9 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
EXTERNAL_SCOPES: () => import_dialect4.EXTERNAL_SCOPES,
|
|
24
|
+
PATTER_SCOPE: () => PATTER_SCOPE,
|
|
23
25
|
compileExpression: () => compileExpression,
|
|
24
26
|
exportBundle: () => exportBundle,
|
|
25
|
-
|
|
27
|
+
externalGameScopes: () => externalGameScopes,
|
|
28
|
+
hostScopesToSpec: () => import_dialect4.hostScopesToSpec,
|
|
29
|
+
projectScopes: () => projectScopes,
|
|
26
30
|
validateConditions: () => validateConditions,
|
|
27
31
|
validateInterpolation: () => validateInterpolation
|
|
28
32
|
});
|
|
@@ -30,15 +34,103 @@ module.exports = __toCommonJS(index_exports);
|
|
|
30
34
|
|
|
31
35
|
// src/expressions.ts
|
|
32
36
|
var import_expr = require("@wildwinter/expr");
|
|
37
|
+
var import_dialect2 = require("@patterkit/dialect");
|
|
38
|
+
var import_scopes2 = require("@wildwinter/scoperegistry/scopes");
|
|
39
|
+
|
|
40
|
+
// src/game-scopes.ts
|
|
41
|
+
var import_scopes = require("@wildwinter/scoperegistry/scopes");
|
|
33
42
|
var import_dialect = require("@patterkit/dialect");
|
|
43
|
+
var PATTER_SCOPE = "patter";
|
|
44
|
+
function asHostScope(s) {
|
|
45
|
+
return {
|
|
46
|
+
token: s.token,
|
|
47
|
+
...s.writable !== void 0 ? { writable: s.writable } : {},
|
|
48
|
+
...s.declarations ? { declarations: s.declarations.map((d) => ({ ...d })) } : {}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function sameScope(a, b) {
|
|
52
|
+
const norm = (s) => JSON.stringify((0, import_dialect.hostScopesToSpec)({ version: 1, scopes: [s] }));
|
|
53
|
+
return norm(a) === norm(b);
|
|
54
|
+
}
|
|
55
|
+
function projectScopes(project, merged) {
|
|
56
|
+
const own = project.scopeRegistry;
|
|
57
|
+
if (!merged) return { host: own, external: [], notes: [] };
|
|
58
|
+
const notes = [];
|
|
59
|
+
const hosted = [];
|
|
60
|
+
const bundled = /* @__PURE__ */ new Set();
|
|
61
|
+
let changed = false;
|
|
62
|
+
const shared = (token) => token === PATTER_SCOPE ? void 0 : merged.spec.scopes.find((s) => s.token === token);
|
|
63
|
+
for (const h of own?.scopes ?? []) {
|
|
64
|
+
const s = shared(h.token);
|
|
65
|
+
const who = merged.owners.get(h.token);
|
|
66
|
+
if (!s || !who) {
|
|
67
|
+
hosted.push(h);
|
|
68
|
+
bundled.add(h.token);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (who.fileName === import_scopes.GAME_SCOPES_FILE) {
|
|
72
|
+
const fromFile = asHostScope(s);
|
|
73
|
+
hosted.push(fromFile);
|
|
74
|
+
bundled.add(h.token);
|
|
75
|
+
changed = true;
|
|
76
|
+
if (!sameScope(h, fromFile)) {
|
|
77
|
+
notes.push({
|
|
78
|
+
token: h.token,
|
|
79
|
+
file: who.fileName,
|
|
80
|
+
message: `@${h.token} in World properties differs from ${import_scopes.GAME_SCOPES_DIR}/${who.fileName}, and the shared file wins: saving Project Settings in Patterpad brings the project's copy up to date`
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
changed = true;
|
|
85
|
+
notes.push({
|
|
86
|
+
token: h.token,
|
|
87
|
+
file: who.fileName,
|
|
88
|
+
message: `@${h.token} is declared in ${import_scopes.GAME_SCOPES_DIR}/${who.fileName}; remove it from World properties`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const world = shared("world");
|
|
93
|
+
if (world && merged.owners.get("world")?.fileName === import_scopes.GAME_SCOPES_FILE && !bundled.has("world")) {
|
|
94
|
+
hosted.push(asHostScope(world));
|
|
95
|
+
bundled.add("world");
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
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;
|
|
100
|
+
return { host, external, notes };
|
|
101
|
+
}
|
|
102
|
+
function externalGameScopes(strict, merged) {
|
|
103
|
+
if (!merged) return [];
|
|
104
|
+
const hostTokens = new Set((strict?.scopes ?? []).map((s) => s.token));
|
|
105
|
+
return merged.spec.scopes.filter((s) => s.token !== PATTER_SCOPE && !hostTokens.has(s.token));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/expressions.ts
|
|
34
109
|
var import_model = require("@patterkit/model");
|
|
35
110
|
function compileExpression(src, foreign) {
|
|
36
|
-
return (0, import_expr.compile)(src, (0,
|
|
111
|
+
return (0, import_expr.compile)(src, (0, import_dialect2.dialectWithForeignScopes)(foreign));
|
|
112
|
+
}
|
|
113
|
+
function validationScopes(project, options) {
|
|
114
|
+
const merged = options.gameScopes;
|
|
115
|
+
const strict = options.foreignScopes ?? (merged ? (0, import_dialect2.hostScopesToSpec)(projectScopes(project, merged).host) : void 0);
|
|
116
|
+
const external = externalGameScopes(strict, merged);
|
|
117
|
+
const base = strict?.scopes ?? [];
|
|
118
|
+
const foreign = (0, import_dialect2.withEngineScopes)({ version: strict?.version ?? 1, scopes: [...base, ...external.map((s) => ({ token: s.token }))] });
|
|
119
|
+
const checked = external.some((s) => s.declarations?.length) ? (0, import_dialect2.withEngineScopes)({ version: strict?.version ?? 1, scopes: [...base, ...external] }) : void 0;
|
|
120
|
+
return { foreign, checked, external: new Set(external.map((s) => s.token)), merged };
|
|
121
|
+
}
|
|
122
|
+
var issueKey = (i) => `${i.kind}|${JSON.stringify(i.path)}|${i.message}`;
|
|
123
|
+
function externalMessage(i, merged) {
|
|
124
|
+
if (merged && i.kind === "unresolved-scoped-property" && i.reference) {
|
|
125
|
+
const [token, name] = i.reference.split(".");
|
|
126
|
+
if (token && name) return (0, import_scopes2.referenceNote)(merged, token, name) ?? i.message;
|
|
127
|
+
}
|
|
128
|
+
return i.message;
|
|
37
129
|
}
|
|
38
130
|
function validateConditions(input, options = {}) {
|
|
39
131
|
const issues = [];
|
|
40
|
-
const foreign = (
|
|
41
|
-
const dialect = (0,
|
|
132
|
+
const { foreign, checked, external, merged } = validationScopes(input.project, options);
|
|
133
|
+
const dialect = (0, import_dialect2.dialectWithForeignScopes)(foreign);
|
|
42
134
|
const readOnly = readOnlyForeignTargets(foreign);
|
|
43
135
|
const opaqueForeign = opaqueForeignTokens(foreign);
|
|
44
136
|
const isScopeToken = scopeTokenTest(foreign);
|
|
@@ -60,7 +152,8 @@ function validateConditions(input, options = {}) {
|
|
|
60
152
|
}
|
|
61
153
|
};
|
|
62
154
|
const validateElement = (blocks, sceneProps, onEntry, elementId) => {
|
|
63
|
-
const schema = (0,
|
|
155
|
+
const schema = (0, import_dialect2.buildSchema)(input.project, sceneProps, foreign);
|
|
156
|
+
const checkedSchema = checked ? (0, import_dialect2.buildSchema)(input.project, sceneProps, checked) : void 0;
|
|
64
157
|
const check = (nodeId, field, src) => {
|
|
65
158
|
let ast;
|
|
66
159
|
try {
|
|
@@ -69,9 +162,16 @@ function validateConditions(input, options = {}) {
|
|
|
69
162
|
issues.push({ nodeId, field, src, severity: "error", message: e instanceof Error ? e.message : String(e) });
|
|
70
163
|
return;
|
|
71
164
|
}
|
|
72
|
-
|
|
165
|
+
const strict = (0, import_expr.validateExpr)(ast, schema, dialect);
|
|
166
|
+
for (const iss of strict) {
|
|
73
167
|
issues.push({ nodeId, field, src, severity: iss.severity, message: iss.message });
|
|
74
168
|
}
|
|
169
|
+
if (checkedSchema) {
|
|
170
|
+
const seen = new Set(strict.map(issueKey));
|
|
171
|
+
for (const iss of (0, import_expr.validateExpr)(ast, checkedSchema, dialect)) {
|
|
172
|
+
if (!seen.has(issueKey(iss))) issues.push({ nodeId, field, src, severity: "warning", message: externalMessage(iss, merged) });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
75
175
|
checkVisitIds(ast, nodeId, field, src);
|
|
76
176
|
};
|
|
77
177
|
const checkTarget = (nodeId, field, target) => {
|
|
@@ -95,7 +195,12 @@ function validateConditions(input, options = {}) {
|
|
|
95
195
|
});
|
|
96
196
|
return;
|
|
97
197
|
}
|
|
98
|
-
const { scope, name } = (0,
|
|
198
|
+
const { scope, name } = (0, import_dialect2.splitRef)(target, isScopeToken);
|
|
199
|
+
if (external.has(scope)) {
|
|
200
|
+
const note = merged ? (0, import_scopes2.referenceNote)(merged, scope, name, { write: true }) : void 0;
|
|
201
|
+
if (note) issues.push({ nodeId, field, src: target, severity: "warning", message: note });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
99
204
|
if (opaqueForeign.has(scope)) return;
|
|
100
205
|
if (!schema.properties.get(scope)?.has(name)) {
|
|
101
206
|
issues.push({
|
|
@@ -132,13 +237,14 @@ function validateInterpolation(input, options = {}) {
|
|
|
132
237
|
const voiced = input.project.voiced ?? false;
|
|
133
238
|
const tables = (input.locales ?? []).map((l) => ({ locale: l.locale, strings: l.strings }));
|
|
134
239
|
const defaultLocale = input.project.locales.default;
|
|
135
|
-
const foreign = (
|
|
240
|
+
const { foreign, external, merged } = validationScopes(input.project, options);
|
|
136
241
|
const opaqueForeign = opaqueForeignTokens(foreign);
|
|
137
242
|
const isScopeToken = scopeTokenTest(foreign);
|
|
138
243
|
for (const scene of input.scenes) {
|
|
139
|
-
const schema = (0,
|
|
244
|
+
const schema = (0, import_dialect2.buildSchema)(input.project, scene.sceneProps, foreign);
|
|
140
245
|
const known = (ref) => {
|
|
141
|
-
const { scope, name } = (0,
|
|
246
|
+
const { scope, name } = (0, import_dialect2.splitRef)(ref, isScopeToken);
|
|
247
|
+
if (external.has(scope)) return (merged ? (0, import_scopes2.referenceNote)(merged, scope, name) : void 0) ?? true;
|
|
142
248
|
if (opaqueForeign.has(scope)) return true;
|
|
143
249
|
return schema.properties.get(scope)?.has(name) ?? false;
|
|
144
250
|
};
|
|
@@ -147,7 +253,7 @@ function validateInterpolation(input, options = {}) {
|
|
|
147
253
|
const text = t.strings[id];
|
|
148
254
|
if (text === void 0) continue;
|
|
149
255
|
const field = `text[${t.locale}]`;
|
|
150
|
-
for (const slot of (0,
|
|
256
|
+
for (const slot of (0, import_dialect2.extractSlots)(text)) {
|
|
151
257
|
if (voicedLine) {
|
|
152
258
|
issues.push({
|
|
153
259
|
nodeId,
|
|
@@ -168,7 +274,10 @@ function validateInterpolation(input, options = {}) {
|
|
|
168
274
|
});
|
|
169
275
|
continue;
|
|
170
276
|
}
|
|
171
|
-
|
|
277
|
+
const ok = known(slot.ref);
|
|
278
|
+
if (typeof ok === "string") {
|
|
279
|
+
issues.push({ nodeId, field, src: slot.raw, severity: "warning", message: ok });
|
|
280
|
+
} else if (!ok) {
|
|
172
281
|
issues.push({
|
|
173
282
|
nodeId,
|
|
174
283
|
field,
|
|
@@ -232,11 +341,11 @@ function readOnlyForeignTargets(spec) {
|
|
|
232
341
|
}
|
|
233
342
|
|
|
234
343
|
// src/index.ts
|
|
235
|
-
var
|
|
344
|
+
var import_dialect4 = require("@patterkit/dialect");
|
|
236
345
|
|
|
237
346
|
// src/export.ts
|
|
238
347
|
var import_core = require("@patterkit/core");
|
|
239
|
-
var
|
|
348
|
+
var import_dialect3 = require("@patterkit/dialect");
|
|
240
349
|
function compileEffect(e, foreign) {
|
|
241
350
|
return { kind: "set", target: e.target, value: compileExpression(e.value, foreign) };
|
|
242
351
|
}
|
|
@@ -304,9 +413,8 @@ function compileScene(scene, foreign) {
|
|
|
304
413
|
blocks: scene.blocks.map((b) => compileBlock(b, foreign))
|
|
305
414
|
};
|
|
306
415
|
}
|
|
307
|
-
function externalScopesIn(scenes) {
|
|
416
|
+
function externalScopesIn(scenes, external) {
|
|
308
417
|
const found = /* @__PURE__ */ new Set();
|
|
309
|
-
const external = new Set(import_dialect2.EXTERNAL_SCOPES);
|
|
310
418
|
const walk = (node) => {
|
|
311
419
|
if (!Array.isArray(node) && (node === null || typeof node !== "object")) return;
|
|
312
420
|
if (Array.isArray(node)) {
|
|
@@ -326,11 +434,16 @@ function externalScopesIn(scenes) {
|
|
|
326
434
|
}
|
|
327
435
|
function exportBundle(input) {
|
|
328
436
|
const { project, scenes, locales = [], includeLocales } = input;
|
|
329
|
-
const
|
|
330
|
-
const
|
|
437
|
+
const hostScopes = projectScopes(project, input.gameScopes).host;
|
|
438
|
+
const foreignScopes = input.foreignScopes ?? (0, import_dialect3.hostScopesToSpec)(hostScopes);
|
|
439
|
+
const gameExternal = externalGameScopes(foreignScopes, input.gameScopes);
|
|
440
|
+
const compileScopes = (0, import_dialect3.withEngineScopes)({
|
|
441
|
+
version: foreignScopes?.version ?? 1,
|
|
442
|
+
scopes: [...foreignScopes?.scopes ?? [], ...gameExternal.map((s) => ({ token: s.token }))]
|
|
443
|
+
});
|
|
331
444
|
const scenesOut = {};
|
|
332
445
|
for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, compileScopes);
|
|
333
|
-
const externalScopes = externalScopesIn(scenesOut);
|
|
446
|
+
const externalScopes = externalScopesIn(scenesOut, /* @__PURE__ */ new Set([...import_dialect3.EXTERNAL_SCOPES, ...gameExternal.map((s) => s.token)]));
|
|
334
447
|
const strings = {};
|
|
335
448
|
for (const loc of locales) {
|
|
336
449
|
if (includeLocales && !includeLocales.includes(loc.locale)) continue;
|
|
@@ -380,7 +493,7 @@ function exportBundle(input) {
|
|
|
380
493
|
return m;
|
|
381
494
|
}),
|
|
382
495
|
properties: project.properties,
|
|
383
|
-
scopeRegistry:
|
|
496
|
+
scopeRegistry: hostScopes,
|
|
384
497
|
gameDataFields: project.gameDataFields,
|
|
385
498
|
// Closed-caption delimiters ride along only when the project pins a non-default pair (#214); the
|
|
386
499
|
// runtime falls back to `[` / `]` when absent, so default projects keep their exact prior bundle.
|
|
@@ -393,9 +506,13 @@ function exportBundle(input) {
|
|
|
393
506
|
}
|
|
394
507
|
// Annotate the CommonJS export names for ESM import in node:
|
|
395
508
|
0 && (module.exports = {
|
|
509
|
+
EXTERNAL_SCOPES,
|
|
510
|
+
PATTER_SCOPE,
|
|
396
511
|
compileExpression,
|
|
397
512
|
exportBundle,
|
|
513
|
+
externalGameScopes,
|
|
398
514
|
hostScopesToSpec,
|
|
515
|
+
projectScopes,
|
|
399
516
|
validateConditions,
|
|
400
517
|
validateInterpolation
|
|
401
518
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { ScopeRegistrySpec } from '@wildwinter/scoperegistry';
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { ScopeRegistrySpec, ScopeSpec } from '@wildwinter/scoperegistry';
|
|
2
|
+
import { MergedScopes } from '@wildwinter/scoperegistry/scopes';
|
|
3
|
+
import { Expression, ProjectFile, Scene, LocaleFile, Bundle, HostScopeRegistry } from '@patterkit/model';
|
|
4
|
+
export { EXTERNAL_SCOPES, hostScopesToSpec } from '@patterkit/dialect';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Compile a condition/effect `src` string to the bundle `{ src, ast }` envelope.
|
|
@@ -8,6 +9,17 @@ export { hostScopesToSpec } from '@patterkit/dialect';
|
|
|
8
9
|
* a storylet's `@world`); the parser needs every referenced scope registered.
|
|
9
10
|
*/
|
|
10
11
|
declare function compileExpression(src: string, foreign?: ScopeRegistrySpec): Expression;
|
|
12
|
+
/**
|
|
13
|
+
* What the validators take besides the project. `foreignScopes` is the host spec, checked strictly (a
|
|
14
|
+
* project's own `@world`, or another owner's spec). `gameScopes` is the game's merged `game-scopes/`
|
|
15
|
+
* folder: every scope in it the host spec doesn't hold is checked too, but only ever with WARNINGS
|
|
16
|
+
* (patterkit/design/shared-scopes.md decision 3). Given `gameScopes` and no `foreignScopes`, the host
|
|
17
|
+
* spec is the project's host scopes as the folder leaves them (`projectScopes`).
|
|
18
|
+
*/
|
|
19
|
+
interface ValidateOptions {
|
|
20
|
+
foreignScopes?: ScopeRegistrySpec;
|
|
21
|
+
gameScopes?: MergedScopes;
|
|
22
|
+
}
|
|
11
23
|
interface ConditionIssue {
|
|
12
24
|
/** Id of the node whose expression has the issue. */
|
|
13
25
|
nodeId: string;
|
|
@@ -31,9 +43,7 @@ interface ConditionIssue {
|
|
|
31
43
|
declare function validateConditions(input: {
|
|
32
44
|
project: ProjectFile;
|
|
33
45
|
scenes: Scene[];
|
|
34
|
-
}, options?:
|
|
35
|
-
foreignScopes?: ScopeRegistrySpec;
|
|
36
|
-
}): ConditionIssue[];
|
|
46
|
+
}, options?: ValidateOptions): ConditionIssue[];
|
|
37
47
|
/**
|
|
38
48
|
* Validate inline `{@ref}` interpolation slots in a project's localised strings
|
|
39
49
|
* (spec §16). Enforces the VO-safety guarantee - **a voiced project rejects any
|
|
@@ -50,9 +60,7 @@ declare function validateInterpolation(input: {
|
|
|
50
60
|
project: ProjectFile;
|
|
51
61
|
scenes: Scene[];
|
|
52
62
|
locales?: LocaleFile[];
|
|
53
|
-
}, options?:
|
|
54
|
-
foreignScopes?: ScopeRegistrySpec;
|
|
55
|
-
}): ConditionIssue[];
|
|
63
|
+
}, options?: ValidateOptions): ConditionIssue[];
|
|
56
64
|
|
|
57
65
|
interface ExportInput {
|
|
58
66
|
project: ProjectFile;
|
|
@@ -67,8 +75,45 @@ interface ExportInput {
|
|
|
67
75
|
* unregistered scope token, so cross-engine refs need this.
|
|
68
76
|
*/
|
|
69
77
|
foreignScopes?: ScopeRegistrySpec;
|
|
78
|
+
/**
|
|
79
|
+
* The game's merged `game-scopes/` folder, when the project has one (patterkit/design/shared-scopes.md).
|
|
80
|
+
* Its `game.scopes.json` wins over the project's copy of a host scope, and its `@world` is baked in
|
|
81
|
+
* even when the project doesn't declare it; every other scope in it compiles as another engine's
|
|
82
|
+
* does: named in `externalScopes`, never baked in.
|
|
83
|
+
*/
|
|
84
|
+
gameScopes?: MergedScopes;
|
|
70
85
|
}
|
|
71
86
|
/** Compile a project's source into the runtime bundle. */
|
|
72
87
|
declare function exportBundle(input: ExportInput): Bundle;
|
|
73
88
|
|
|
74
|
-
|
|
89
|
+
/** Patter's own game-wide token: the one scope Patter writes to the folder and never reads back. */
|
|
90
|
+
declare const PATTER_SCOPE = "patter";
|
|
91
|
+
/** Something the folder says about the project's host scopes: a warning, anchored to a scopes file. */
|
|
92
|
+
interface HostScopeNote {
|
|
93
|
+
token: string;
|
|
94
|
+
/** The scopes file it concerns, by name (`game.scopes.json`). */
|
|
95
|
+
file: string;
|
|
96
|
+
message: string;
|
|
97
|
+
}
|
|
98
|
+
/** A project's scopes once its game's folder is taken into account. */
|
|
99
|
+
interface ProjectScopes {
|
|
100
|
+
/** The host scopes the compile treats as the project's: validated strictly, baked into the bundle. */
|
|
101
|
+
host?: HostScopeRegistry;
|
|
102
|
+
/** Every other scope the folder declares, checked with warnings and never bundled. */
|
|
103
|
+
external: ScopeSpec[];
|
|
104
|
+
/** Where the project's host scopes and the folder disagree. */
|
|
105
|
+
notes: HostScopeNote[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The project's host scopes and the folder's other scopes, from a project and its game's merged
|
|
109
|
+
* scopes. With no folder the host scopes are the project's own, untouched (the same object, so a
|
|
110
|
+
* bundle's hash doesn't move), and there is nothing external.
|
|
111
|
+
*/
|
|
112
|
+
declare function projectScopes(project: ProjectFile, merged?: MergedScopes): ProjectScopes;
|
|
113
|
+
/**
|
|
114
|
+
* The folder's scopes that a compile treats as external, given the host spec it is using: every merged
|
|
115
|
+
* scope except Patter's own and the host spec's tokens. Empty with no folder.
|
|
116
|
+
*/
|
|
117
|
+
declare function externalGameScopes(strict: ScopeRegistrySpec | undefined, merged?: MergedScopes): ScopeSpec[];
|
|
118
|
+
|
|
119
|
+
export { type ConditionIssue, type ExportInput, type HostScopeNote, PATTER_SCOPE, type ProjectScopes, type ValidateOptions, compileExpression, exportBundle, externalGameScopes, projectScopes, validateConditions, validateInterpolation };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { ScopeRegistrySpec } from '@wildwinter/scoperegistry';
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { ScopeRegistrySpec, ScopeSpec } from '@wildwinter/scoperegistry';
|
|
2
|
+
import { MergedScopes } from '@wildwinter/scoperegistry/scopes';
|
|
3
|
+
import { Expression, ProjectFile, Scene, LocaleFile, Bundle, HostScopeRegistry } from '@patterkit/model';
|
|
4
|
+
export { EXTERNAL_SCOPES, hostScopesToSpec } from '@patterkit/dialect';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Compile a condition/effect `src` string to the bundle `{ src, ast }` envelope.
|
|
@@ -8,6 +9,17 @@ export { hostScopesToSpec } from '@patterkit/dialect';
|
|
|
8
9
|
* a storylet's `@world`); the parser needs every referenced scope registered.
|
|
9
10
|
*/
|
|
10
11
|
declare function compileExpression(src: string, foreign?: ScopeRegistrySpec): Expression;
|
|
12
|
+
/**
|
|
13
|
+
* What the validators take besides the project. `foreignScopes` is the host spec, checked strictly (a
|
|
14
|
+
* project's own `@world`, or another owner's spec). `gameScopes` is the game's merged `game-scopes/`
|
|
15
|
+
* folder: every scope in it the host spec doesn't hold is checked too, but only ever with WARNINGS
|
|
16
|
+
* (patterkit/design/shared-scopes.md decision 3). Given `gameScopes` and no `foreignScopes`, the host
|
|
17
|
+
* spec is the project's host scopes as the folder leaves them (`projectScopes`).
|
|
18
|
+
*/
|
|
19
|
+
interface ValidateOptions {
|
|
20
|
+
foreignScopes?: ScopeRegistrySpec;
|
|
21
|
+
gameScopes?: MergedScopes;
|
|
22
|
+
}
|
|
11
23
|
interface ConditionIssue {
|
|
12
24
|
/** Id of the node whose expression has the issue. */
|
|
13
25
|
nodeId: string;
|
|
@@ -31,9 +43,7 @@ interface ConditionIssue {
|
|
|
31
43
|
declare function validateConditions(input: {
|
|
32
44
|
project: ProjectFile;
|
|
33
45
|
scenes: Scene[];
|
|
34
|
-
}, options?:
|
|
35
|
-
foreignScopes?: ScopeRegistrySpec;
|
|
36
|
-
}): ConditionIssue[];
|
|
46
|
+
}, options?: ValidateOptions): ConditionIssue[];
|
|
37
47
|
/**
|
|
38
48
|
* Validate inline `{@ref}` interpolation slots in a project's localised strings
|
|
39
49
|
* (spec §16). Enforces the VO-safety guarantee - **a voiced project rejects any
|
|
@@ -50,9 +60,7 @@ declare function validateInterpolation(input: {
|
|
|
50
60
|
project: ProjectFile;
|
|
51
61
|
scenes: Scene[];
|
|
52
62
|
locales?: LocaleFile[];
|
|
53
|
-
}, options?:
|
|
54
|
-
foreignScopes?: ScopeRegistrySpec;
|
|
55
|
-
}): ConditionIssue[];
|
|
63
|
+
}, options?: ValidateOptions): ConditionIssue[];
|
|
56
64
|
|
|
57
65
|
interface ExportInput {
|
|
58
66
|
project: ProjectFile;
|
|
@@ -67,8 +75,45 @@ interface ExportInput {
|
|
|
67
75
|
* unregistered scope token, so cross-engine refs need this.
|
|
68
76
|
*/
|
|
69
77
|
foreignScopes?: ScopeRegistrySpec;
|
|
78
|
+
/**
|
|
79
|
+
* The game's merged `game-scopes/` folder, when the project has one (patterkit/design/shared-scopes.md).
|
|
80
|
+
* Its `game.scopes.json` wins over the project's copy of a host scope, and its `@world` is baked in
|
|
81
|
+
* even when the project doesn't declare it; every other scope in it compiles as another engine's
|
|
82
|
+
* does: named in `externalScopes`, never baked in.
|
|
83
|
+
*/
|
|
84
|
+
gameScopes?: MergedScopes;
|
|
70
85
|
}
|
|
71
86
|
/** Compile a project's source into the runtime bundle. */
|
|
72
87
|
declare function exportBundle(input: ExportInput): Bundle;
|
|
73
88
|
|
|
74
|
-
|
|
89
|
+
/** Patter's own game-wide token: the one scope Patter writes to the folder and never reads back. */
|
|
90
|
+
declare const PATTER_SCOPE = "patter";
|
|
91
|
+
/** Something the folder says about the project's host scopes: a warning, anchored to a scopes file. */
|
|
92
|
+
interface HostScopeNote {
|
|
93
|
+
token: string;
|
|
94
|
+
/** The scopes file it concerns, by name (`game.scopes.json`). */
|
|
95
|
+
file: string;
|
|
96
|
+
message: string;
|
|
97
|
+
}
|
|
98
|
+
/** A project's scopes once its game's folder is taken into account. */
|
|
99
|
+
interface ProjectScopes {
|
|
100
|
+
/** The host scopes the compile treats as the project's: validated strictly, baked into the bundle. */
|
|
101
|
+
host?: HostScopeRegistry;
|
|
102
|
+
/** Every other scope the folder declares, checked with warnings and never bundled. */
|
|
103
|
+
external: ScopeSpec[];
|
|
104
|
+
/** Where the project's host scopes and the folder disagree. */
|
|
105
|
+
notes: HostScopeNote[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The project's host scopes and the folder's other scopes, from a project and its game's merged
|
|
109
|
+
* scopes. With no folder the host scopes are the project's own, untouched (the same object, so a
|
|
110
|
+
* bundle's hash doesn't move), and there is nothing external.
|
|
111
|
+
*/
|
|
112
|
+
declare function projectScopes(project: ProjectFile, merged?: MergedScopes): ProjectScopes;
|
|
113
|
+
/**
|
|
114
|
+
* The folder's scopes that a compile treats as external, given the host spec it is using: every merged
|
|
115
|
+
* scope except Patter's own and the host spec's tokens. Empty with no folder.
|
|
116
|
+
*/
|
|
117
|
+
declare function externalGameScopes(strict: ScopeRegistrySpec | undefined, merged?: MergedScopes): ScopeSpec[];
|
|
118
|
+
|
|
119
|
+
export { type ConditionIssue, type ExportInput, type HostScopeNote, PATTER_SCOPE, type ProjectScopes, type ValidateOptions, compileExpression, exportBundle, externalGameScopes, projectScopes, validateConditions, validateInterpolation };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,101 @@
|
|
|
1
1
|
// src/expressions.ts
|
|
2
2
|
import { compile, parse, validateExpr } from "@wildwinter/expr";
|
|
3
|
-
import { dialectWithForeignScopes, buildSchema, extractSlots, splitRef, withEngineScopes } from "@patterkit/dialect";
|
|
3
|
+
import { dialectWithForeignScopes, buildSchema, extractSlots, splitRef, withEngineScopes, hostScopesToSpec as hostScopesToSpec2 } from "@patterkit/dialect";
|
|
4
|
+
import { referenceNote } from "@wildwinter/scoperegistry/scopes";
|
|
5
|
+
|
|
6
|
+
// src/game-scopes.ts
|
|
7
|
+
import { GAME_SCOPES_DIR, GAME_SCOPES_FILE } from "@wildwinter/scoperegistry/scopes";
|
|
8
|
+
import { hostScopesToSpec } from "@patterkit/dialect";
|
|
9
|
+
var PATTER_SCOPE = "patter";
|
|
10
|
+
function asHostScope(s) {
|
|
11
|
+
return {
|
|
12
|
+
token: s.token,
|
|
13
|
+
...s.writable !== void 0 ? { writable: s.writable } : {},
|
|
14
|
+
...s.declarations ? { declarations: s.declarations.map((d) => ({ ...d })) } : {}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function sameScope(a, b) {
|
|
18
|
+
const norm = (s) => JSON.stringify(hostScopesToSpec({ version: 1, scopes: [s] }));
|
|
19
|
+
return norm(a) === norm(b);
|
|
20
|
+
}
|
|
21
|
+
function projectScopes(project, merged) {
|
|
22
|
+
const own = project.scopeRegistry;
|
|
23
|
+
if (!merged) return { host: own, external: [], notes: [] };
|
|
24
|
+
const notes = [];
|
|
25
|
+
const hosted = [];
|
|
26
|
+
const bundled = /* @__PURE__ */ new Set();
|
|
27
|
+
let changed = false;
|
|
28
|
+
const shared = (token) => token === PATTER_SCOPE ? void 0 : merged.spec.scopes.find((s) => s.token === token);
|
|
29
|
+
for (const h of own?.scopes ?? []) {
|
|
30
|
+
const s = shared(h.token);
|
|
31
|
+
const who = merged.owners.get(h.token);
|
|
32
|
+
if (!s || !who) {
|
|
33
|
+
hosted.push(h);
|
|
34
|
+
bundled.add(h.token);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (who.fileName === GAME_SCOPES_FILE) {
|
|
38
|
+
const fromFile = asHostScope(s);
|
|
39
|
+
hosted.push(fromFile);
|
|
40
|
+
bundled.add(h.token);
|
|
41
|
+
changed = true;
|
|
42
|
+
if (!sameScope(h, fromFile)) {
|
|
43
|
+
notes.push({
|
|
44
|
+
token: h.token,
|
|
45
|
+
file: who.fileName,
|
|
46
|
+
message: `@${h.token} in World properties differs from ${GAME_SCOPES_DIR}/${who.fileName}, and the shared file wins: saving Project Settings in Patterpad brings the project's copy up to date`
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
changed = true;
|
|
51
|
+
notes.push({
|
|
52
|
+
token: h.token,
|
|
53
|
+
file: who.fileName,
|
|
54
|
+
message: `@${h.token} is declared in ${GAME_SCOPES_DIR}/${who.fileName}; remove it from World properties`
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const world = shared("world");
|
|
59
|
+
if (world && merged.owners.get("world")?.fileName === GAME_SCOPES_FILE && !bundled.has("world")) {
|
|
60
|
+
hosted.push(asHostScope(world));
|
|
61
|
+
bundled.add("world");
|
|
62
|
+
changed = true;
|
|
63
|
+
}
|
|
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;
|
|
66
|
+
return { host, external, notes };
|
|
67
|
+
}
|
|
68
|
+
function externalGameScopes(strict, merged) {
|
|
69
|
+
if (!merged) return [];
|
|
70
|
+
const hostTokens = new Set((strict?.scopes ?? []).map((s) => s.token));
|
|
71
|
+
return merged.spec.scopes.filter((s) => s.token !== PATTER_SCOPE && !hostTokens.has(s.token));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/expressions.ts
|
|
4
75
|
import { walkNodes } from "@patterkit/model";
|
|
5
76
|
function compileExpression(src, foreign) {
|
|
6
77
|
return compile(src, dialectWithForeignScopes(foreign));
|
|
7
78
|
}
|
|
79
|
+
function validationScopes(project, options) {
|
|
80
|
+
const merged = options.gameScopes;
|
|
81
|
+
const strict = options.foreignScopes ?? (merged ? hostScopesToSpec2(projectScopes(project, merged).host) : void 0);
|
|
82
|
+
const external = externalGameScopes(strict, merged);
|
|
83
|
+
const base = strict?.scopes ?? [];
|
|
84
|
+
const foreign = withEngineScopes({ version: strict?.version ?? 1, scopes: [...base, ...external.map((s) => ({ token: s.token }))] });
|
|
85
|
+
const checked = external.some((s) => s.declarations?.length) ? withEngineScopes({ version: strict?.version ?? 1, scopes: [...base, ...external] }) : void 0;
|
|
86
|
+
return { foreign, checked, external: new Set(external.map((s) => s.token)), merged };
|
|
87
|
+
}
|
|
88
|
+
var issueKey = (i) => `${i.kind}|${JSON.stringify(i.path)}|${i.message}`;
|
|
89
|
+
function externalMessage(i, merged) {
|
|
90
|
+
if (merged && i.kind === "unresolved-scoped-property" && i.reference) {
|
|
91
|
+
const [token, name] = i.reference.split(".");
|
|
92
|
+
if (token && name) return referenceNote(merged, token, name) ?? i.message;
|
|
93
|
+
}
|
|
94
|
+
return i.message;
|
|
95
|
+
}
|
|
8
96
|
function validateConditions(input, options = {}) {
|
|
9
97
|
const issues = [];
|
|
10
|
-
const foreign =
|
|
98
|
+
const { foreign, checked, external, merged } = validationScopes(input.project, options);
|
|
11
99
|
const dialect = dialectWithForeignScopes(foreign);
|
|
12
100
|
const readOnly = readOnlyForeignTargets(foreign);
|
|
13
101
|
const opaqueForeign = opaqueForeignTokens(foreign);
|
|
@@ -31,6 +119,7 @@ function validateConditions(input, options = {}) {
|
|
|
31
119
|
};
|
|
32
120
|
const validateElement = (blocks, sceneProps, onEntry, elementId) => {
|
|
33
121
|
const schema = buildSchema(input.project, sceneProps, foreign);
|
|
122
|
+
const checkedSchema = checked ? buildSchema(input.project, sceneProps, checked) : void 0;
|
|
34
123
|
const check = (nodeId, field, src) => {
|
|
35
124
|
let ast;
|
|
36
125
|
try {
|
|
@@ -39,9 +128,16 @@ function validateConditions(input, options = {}) {
|
|
|
39
128
|
issues.push({ nodeId, field, src, severity: "error", message: e instanceof Error ? e.message : String(e) });
|
|
40
129
|
return;
|
|
41
130
|
}
|
|
42
|
-
|
|
131
|
+
const strict = validateExpr(ast, schema, dialect);
|
|
132
|
+
for (const iss of strict) {
|
|
43
133
|
issues.push({ nodeId, field, src, severity: iss.severity, message: iss.message });
|
|
44
134
|
}
|
|
135
|
+
if (checkedSchema) {
|
|
136
|
+
const seen = new Set(strict.map(issueKey));
|
|
137
|
+
for (const iss of validateExpr(ast, checkedSchema, dialect)) {
|
|
138
|
+
if (!seen.has(issueKey(iss))) issues.push({ nodeId, field, src, severity: "warning", message: externalMessage(iss, merged) });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
45
141
|
checkVisitIds(ast, nodeId, field, src);
|
|
46
142
|
};
|
|
47
143
|
const checkTarget = (nodeId, field, target) => {
|
|
@@ -66,6 +162,11 @@ function validateConditions(input, options = {}) {
|
|
|
66
162
|
return;
|
|
67
163
|
}
|
|
68
164
|
const { scope, name } = splitRef(target, isScopeToken);
|
|
165
|
+
if (external.has(scope)) {
|
|
166
|
+
const note = merged ? referenceNote(merged, scope, name, { write: true }) : void 0;
|
|
167
|
+
if (note) issues.push({ nodeId, field, src: target, severity: "warning", message: note });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
69
170
|
if (opaqueForeign.has(scope)) return;
|
|
70
171
|
if (!schema.properties.get(scope)?.has(name)) {
|
|
71
172
|
issues.push({
|
|
@@ -102,13 +203,14 @@ function validateInterpolation(input, options = {}) {
|
|
|
102
203
|
const voiced = input.project.voiced ?? false;
|
|
103
204
|
const tables = (input.locales ?? []).map((l) => ({ locale: l.locale, strings: l.strings }));
|
|
104
205
|
const defaultLocale = input.project.locales.default;
|
|
105
|
-
const foreign =
|
|
206
|
+
const { foreign, external, merged } = validationScopes(input.project, options);
|
|
106
207
|
const opaqueForeign = opaqueForeignTokens(foreign);
|
|
107
208
|
const isScopeToken = scopeTokenTest(foreign);
|
|
108
209
|
for (const scene of input.scenes) {
|
|
109
210
|
const schema = buildSchema(input.project, scene.sceneProps, foreign);
|
|
110
211
|
const known = (ref) => {
|
|
111
212
|
const { scope, name } = splitRef(ref, isScopeToken);
|
|
213
|
+
if (external.has(scope)) return (merged ? referenceNote(merged, scope, name) : void 0) ?? true;
|
|
112
214
|
if (opaqueForeign.has(scope)) return true;
|
|
113
215
|
return schema.properties.get(scope)?.has(name) ?? false;
|
|
114
216
|
};
|
|
@@ -138,7 +240,10 @@ function validateInterpolation(input, options = {}) {
|
|
|
138
240
|
});
|
|
139
241
|
continue;
|
|
140
242
|
}
|
|
141
|
-
|
|
243
|
+
const ok = known(slot.ref);
|
|
244
|
+
if (typeof ok === "string") {
|
|
245
|
+
issues.push({ nodeId, field, src: slot.raw, severity: "warning", message: ok });
|
|
246
|
+
} else if (!ok) {
|
|
142
247
|
issues.push({
|
|
143
248
|
nodeId,
|
|
144
249
|
field,
|
|
@@ -202,11 +307,11 @@ function readOnlyForeignTargets(spec) {
|
|
|
202
307
|
}
|
|
203
308
|
|
|
204
309
|
// src/index.ts
|
|
205
|
-
import { hostScopesToSpec as
|
|
310
|
+
import { hostScopesToSpec as hostScopesToSpec4, EXTERNAL_SCOPES as EXTERNAL_SCOPES2 } from "@patterkit/dialect";
|
|
206
311
|
|
|
207
312
|
// src/export.ts
|
|
208
313
|
import { canonicalStringify, hash32 } from "@patterkit/core";
|
|
209
|
-
import { hostScopesToSpec, withEngineScopes as withEngineScopes2, EXTERNAL_SCOPES } from "@patterkit/dialect";
|
|
314
|
+
import { hostScopesToSpec as hostScopesToSpec3, withEngineScopes as withEngineScopes2, EXTERNAL_SCOPES } from "@patterkit/dialect";
|
|
210
315
|
function compileEffect(e, foreign) {
|
|
211
316
|
return { kind: "set", target: e.target, value: compileExpression(e.value, foreign) };
|
|
212
317
|
}
|
|
@@ -274,9 +379,8 @@ function compileScene(scene, foreign) {
|
|
|
274
379
|
blocks: scene.blocks.map((b) => compileBlock(b, foreign))
|
|
275
380
|
};
|
|
276
381
|
}
|
|
277
|
-
function externalScopesIn(scenes) {
|
|
382
|
+
function externalScopesIn(scenes, external) {
|
|
278
383
|
const found = /* @__PURE__ */ new Set();
|
|
279
|
-
const external = new Set(EXTERNAL_SCOPES);
|
|
280
384
|
const walk = (node) => {
|
|
281
385
|
if (!Array.isArray(node) && (node === null || typeof node !== "object")) return;
|
|
282
386
|
if (Array.isArray(node)) {
|
|
@@ -296,11 +400,16 @@ function externalScopesIn(scenes) {
|
|
|
296
400
|
}
|
|
297
401
|
function exportBundle(input) {
|
|
298
402
|
const { project, scenes, locales = [], includeLocales } = input;
|
|
299
|
-
const
|
|
300
|
-
const
|
|
403
|
+
const hostScopes = projectScopes(project, input.gameScopes).host;
|
|
404
|
+
const foreignScopes = input.foreignScopes ?? hostScopesToSpec3(hostScopes);
|
|
405
|
+
const gameExternal = externalGameScopes(foreignScopes, input.gameScopes);
|
|
406
|
+
const compileScopes = withEngineScopes2({
|
|
407
|
+
version: foreignScopes?.version ?? 1,
|
|
408
|
+
scopes: [...foreignScopes?.scopes ?? [], ...gameExternal.map((s) => ({ token: s.token }))]
|
|
409
|
+
});
|
|
301
410
|
const scenesOut = {};
|
|
302
411
|
for (const scene of scenes) scenesOut[scene.id] = compileScene(scene, compileScopes);
|
|
303
|
-
const externalScopes = externalScopesIn(scenesOut);
|
|
412
|
+
const externalScopes = externalScopesIn(scenesOut, /* @__PURE__ */ new Set([...EXTERNAL_SCOPES, ...gameExternal.map((s) => s.token)]));
|
|
304
413
|
const strings = {};
|
|
305
414
|
for (const loc of locales) {
|
|
306
415
|
if (includeLocales && !includeLocales.includes(loc.locale)) continue;
|
|
@@ -350,7 +459,7 @@ function exportBundle(input) {
|
|
|
350
459
|
return m;
|
|
351
460
|
}),
|
|
352
461
|
properties: project.properties,
|
|
353
|
-
scopeRegistry:
|
|
462
|
+
scopeRegistry: hostScopes,
|
|
354
463
|
gameDataFields: project.gameDataFields,
|
|
355
464
|
// Closed-caption delimiters ride along only when the project pins a non-default pair (#214); the
|
|
356
465
|
// runtime falls back to `[` / `]` when absent, so default projects keep their exact prior bundle.
|
|
@@ -362,9 +471,13 @@ function exportBundle(input) {
|
|
|
362
471
|
};
|
|
363
472
|
}
|
|
364
473
|
export {
|
|
474
|
+
EXTERNAL_SCOPES2 as EXTERNAL_SCOPES,
|
|
475
|
+
PATTER_SCOPE,
|
|
365
476
|
compileExpression,
|
|
366
477
|
exportBundle,
|
|
367
|
-
|
|
478
|
+
externalGameScopes,
|
|
479
|
+
hostScopesToSpec4 as hostScopesToSpec,
|
|
480
|
+
projectScopes,
|
|
368
481
|
validateConditions,
|
|
369
482
|
validateInterpolation
|
|
370
483
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patterkit/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Patter compiler: compile condition/effect expressions, validate them, and export source to the runtime bundle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,10 +34,10 @@
|
|
|
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.2.
|
|
39
|
-
"@patterkit/dialect": "0.2.
|
|
37
|
+
"@patterkit/model": "0.7.0",
|
|
38
|
+
"@patterkit/core": "0.2.6",
|
|
39
|
+
"@patterkit/dialect": "0.2.1",
|
|
40
40
|
"@wildwinter/expr": "^0.5.0",
|
|
41
|
-
"@wildwinter/scoperegistry": "^0.
|
|
41
|
+
"@wildwinter/scoperegistry": "^0.8.0"
|
|
42
42
|
}
|
|
43
43
|
}
|