@patterkit/compiler 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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
- hostScopesToSpec: () => import_dialect3.hostScopesToSpec,
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, import_dialect.dialectWithForeignScopes)(foreign));
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 = (0, import_dialect.withEngineScopes)(options.foreignScopes);
41
- const dialect = (0, import_dialect.dialectWithForeignScopes)(foreign);
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, import_dialect.buildSchema)(input.project, sceneProps, foreign);
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
- for (const iss of (0, import_expr.validateExpr)(ast, schema, dialect)) {
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, import_dialect.splitRef)(target, isScopeToken);
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 = (0, import_dialect.withEngineScopes)(options.foreignScopes);
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, import_dialect.buildSchema)(input.project, scene.sceneProps, foreign);
244
+ const schema = (0, import_dialect2.buildSchema)(input.project, scene.sceneProps, foreign);
140
245
  const known = (ref) => {
141
- const { scope, name } = (0, import_dialect.splitRef)(ref, isScopeToken);
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, import_dialect.extractSlots)(text)) {
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
- if (!known(slot.ref)) {
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 import_dialect3 = require("@patterkit/dialect");
344
+ var import_dialect4 = require("@patterkit/dialect");
236
345
 
237
346
  // src/export.ts
238
347
  var import_core = require("@patterkit/core");
239
- var import_dialect2 = require("@patterkit/dialect");
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 foreignScopes = input.foreignScopes ?? (0, import_dialect2.hostScopesToSpec)(project.scopeRegistry);
330
- const compileScopes = (0, import_dialect2.withEngineScopes)(foreignScopes);
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: project.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 { Expression, ProjectFile, Scene, LocaleFile, Bundle } from '@patterkit/model';
3
- export { hostScopesToSpec } from '@patterkit/dialect';
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
- export { type ConditionIssue, type ExportInput, compileExpression, exportBundle, validateConditions, validateInterpolation };
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 { Expression, ProjectFile, Scene, LocaleFile, Bundle } from '@patterkit/model';
3
- export { hostScopesToSpec } from '@patterkit/dialect';
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
- export { type ConditionIssue, type ExportInput, compileExpression, exportBundle, validateConditions, validateInterpolation };
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 = withEngineScopes(options.foreignScopes);
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
- for (const iss of validateExpr(ast, schema, dialect)) {
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 = withEngineScopes(options.foreignScopes);
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
- if (!known(slot.ref)) {
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 hostScopesToSpec2 } from "@patterkit/dialect";
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 foreignScopes = input.foreignScopes ?? hostScopesToSpec(project.scopeRegistry);
300
- const compileScopes = withEngineScopes2(foreignScopes);
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: project.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
- hostScopesToSpec2 as hostScopesToSpec,
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.0",
3
+ "version": "0.4.1",
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.6.0",
38
- "@patterkit/core": "0.2.5",
39
- "@patterkit/dialect": "0.2.0",
37
+ "@patterkit/model": "0.7.0",
38
+ "@patterkit/core": "0.3.0",
39
+ "@patterkit/dialect": "0.2.1",
40
40
  "@wildwinter/expr": "^0.5.0",
41
- "@wildwinter/scoperegistry": "^0.7.0"
41
+ "@wildwinter/scoperegistry": "^0.8.0"
42
42
  }
43
43
  }