@jxsuite/schema 5.5.0 → 6.0.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/README.md CHANGED
@@ -48,6 +48,7 @@ Two subpaths let any host edit and write a Jx document the way Studio does, with
48
48
 
49
49
  - **`@jxsuite/schema/doc-ops`** is the document-op vocabulary. A `JxDocOp` (`set-key`, `insert-child`, `remove-child`, `set-child`, `move-child`) is a value-carrying mutation addressed by `JxPath`. `applyDocOpToDoc(doc, op)` applies one to a plain tree; `inverseOf(doc, op)` computes the op that undoes it, against the document as it stands before the op, in the coordinates of the document after it; `applyDocOpsWithInverse(doc, ops)` applies a sequence and answers each forward/inverse pair. Studio's history, the canvas iframe's shadow document and the collab bridge all replay through this one implementation, and `@jxsuite/collab/ops` re-exports it. Undoing an insert or move into a node that had no `children` leaves `children: []` behind, as Studio's history always has.
50
50
  - **`@jxsuite/schema/json-layout`** is the layout-preserving JSON serializer behind every save. `parseJsonDocument(text)` answers the document and the facts about its text the formatter keeps; `serializeJson(document, layout)` writes it back with those honoured, so a one-value edit is a one-line diff.
51
+ - **`@jxsuite/schema/dynamic-tag`** reads the structured subset of template expressions an editor can show as fields: `parseDynamicTag(expr)` takes the text inside one `${…}` and answers a tag (a path, optionally through `state.<formula>(…)`, optionally negated, optionally with a `??` or `||` fallback literal) or `null` for anything else; `printDynamicTag(tag)` writes it back, byte for byte for canonical input; `dynamicTagLabel(tag)` names it; `templateSegments(template)` splits a whole template string into literal text, tags and custom expressions, each tag with its verbatim source.
51
52
 
52
53
  ## Component schema coverage
53
54
 
package/class-schema.json CHANGED
@@ -8,6 +8,7 @@
8
8
  "type": "string"
9
9
  },
10
10
  "body": {
11
+ "description": "The member's body in erasable TypeScript, which plain JavaScript already is (spec §23.1), as one string or as an array of lines.",
11
12
  "oneOf": [
12
13
  {
13
14
  "type": "string"
@@ -120,6 +121,7 @@
120
121
  "type": "string"
121
122
  },
122
123
  "body": {
124
+ "description": "The member's body in erasable TypeScript, which plain JavaScript already is (spec §23.1), as one string or as an array of lines.",
123
125
  "oneOf": [
124
126
  {
125
127
  "type": "string"
@@ -67,6 +67,8 @@ export const classConstructorDefSchema = {
67
67
  properties: {
68
68
  $prototype: { const: "Function", type: "string" },
69
69
  body: {
70
+ description:
71
+ "The member's body in erasable TypeScript, which plain JavaScript already is (spec §23.1), as one string or as an array of lines.",
70
72
  oneOf: [{ type: "string" }, { items: { type: "string" }, type: "array" }],
71
73
  },
72
74
  description: { type: "string" },
@@ -103,6 +105,8 @@ export const classMethodDefSchema = {
103
105
  $prototype: { const: "Function", type: "string" },
104
106
  access: { enum: ["public", "private", "protected"], type: "string" },
105
107
  body: {
108
+ description:
109
+ "The member's body in erasable TypeScript, which plain JavaScript already is (spec §23.1), as one string or as an array of lines.",
106
110
  oneOf: [{ type: "string" }, { items: { type: "string" }, type: "array" }],
107
111
  },
108
112
  description: { type: "string" },
@@ -2,8 +2,9 @@ export const functionDefSchema = {
2
2
  additionalProperties: false,
3
3
  description:
4
4
  'A function declaration. $prototype must be "Function". ' +
5
- "body (inline) and $src (external) are mutually exclusive. " +
5
+ "body (inline) and $src (external) are mutually exclusive; declaring both is an error. " +
6
6
  "First parameter is always state (the reactive scope).",
7
+ not: { required: ["body", "$src"] },
7
8
  properties: {
8
9
  $export: {
9
10
  description: "Named export in $src module. Defaults to the state key name.",
@@ -32,11 +33,12 @@ export const functionDefSchema = {
32
33
  },
33
34
  body: {
34
35
  description:
35
- "Inline function body: an opaque JS source string (first implicit parameter is state), or a structured statement array (spec §20) — explicit structured function declaration, analyzable by tooling and editable visually. The escalation ladder within the shape is structured statements → JS string.",
36
+ "Inline function body: a source string in erasable TypeScript, which plain JavaScript already is (spec §23.1; types are erased before it is analysed or run; first implicit parameter is state), or a structured statement array (spec §20) — explicit structured function declaration, analyzable by tooling and editable visually. The escalation ladder within the shape is structured statements → source string.",
36
37
  examples: [
37
38
  "state.count++",
38
39
  'state.items.push({ id: Date.now(), text: "", done: false })',
39
40
  'return state.score >= 90 ? "gold" : "silver"',
41
+ "const input = event.target as HTMLInputElement; state.label = input.value",
40
42
  [
41
43
  { operator: "push", target: { $ref: "#/state/cart" }, value: 1 },
42
44
  { dispatchEvent: "cart-changed", detail: { $ref: "#/state/cart" } },
package/package.json CHANGED
@@ -18,6 +18,7 @@
18
18
  "./project-schemas": "./src/project-schemas.ts",
19
19
  "./validate-project": "./src/validate-project.ts",
20
20
  "./types": "./types.ts",
21
+ "./function-role": "./src/function-role.ts",
21
22
  "./guards": "./src/guards.ts",
22
23
  "./locale": "./src/locale.ts",
23
24
  "./intl": "./src/intl.ts",
@@ -29,9 +30,14 @@
29
30
  "./ijson": "./src/ijson.ts",
30
31
  "./parse": "./src/parse.ts",
31
32
  "./jxn": "./src/jxn/index.ts",
33
+ "./virtual-types": "./src/jxn/virtual-types.ts",
34
+ "./virtual-document": "./src/jxn/virtual-document.ts",
35
+ "./jxn-completion": "./src/jxn/complete.ts",
32
36
  "./project-file": "./src/project-file.ts",
33
37
  "./project-file/node": "./src/project-file-node.ts",
34
38
  "./doc-ops": "./src/doc-ops.ts",
39
+ "./document-scripts": "./src/document-scripts.ts",
40
+ "./dynamic-tag": "./src/dynamic-tag.ts",
35
41
  "./json-layout": "./src/json-layout.ts",
36
42
  "./defs": "./defs/index.ts",
37
43
  "./schema.json": "./schema.json",
@@ -66,7 +72,7 @@
66
72
  "upgrade": "bun update --latest && bun install"
67
73
  },
68
74
  "type": "module",
69
- "version": "5.5.0",
75
+ "version": "6.0.0",
70
76
  "devDependencies": {
71
77
  "json-schema-to-ts": "^3.1.1"
72
78
  }
package/schema.json CHANGED
@@ -205,7 +205,13 @@
205
205
  },
206
206
  "FunctionDef": {
207
207
  "additionalProperties": false,
208
- "description": "A function declaration. $prototype must be \"Function\". body (inline) and $src (external) are mutually exclusive. First parameter is always state (the reactive scope).",
208
+ "description": "A function declaration. $prototype must be \"Function\". body (inline) and $src (external) are mutually exclusive; declaring both is an error. First parameter is always state (the reactive scope).",
209
+ "not": {
210
+ "required": [
211
+ "body",
212
+ "$src"
213
+ ]
214
+ },
209
215
  "properties": {
210
216
  "$export": {
211
217
  "description": "Named export in $src module. Defaults to the state key name.",
@@ -241,11 +247,12 @@
241
247
  "type": "array"
242
248
  },
243
249
  "body": {
244
- "description": "Inline function body: an opaque JS source string (first implicit parameter is state), or a structured statement array (spec §20) — explicit structured function declaration, analyzable by tooling and editable visually. The escalation ladder within the shape is structured statements → JS string.",
250
+ "description": "Inline function body: a source string in erasable TypeScript, which plain JavaScript already is (spec §23.1; types are erased before it is analysed or run; first implicit parameter is state), or a structured statement array (spec §20) — explicit structured function declaration, analyzable by tooling and editable visually. The escalation ladder within the shape is structured statements → source string.",
245
251
  "examples": [
246
252
  "state.count++",
247
253
  "state.items.push({ id: Date.now(), text: \"\", done: false })",
248
254
  "return state.score >= 90 ? \"gold\" : \"silver\"",
255
+ "const input = event.target as HTMLInputElement; state.label = input.value",
249
256
  [
250
257
  {
251
258
  "operator": "push",
@@ -506,6 +513,7 @@
506
513
  "type": "string"
507
514
  },
508
515
  "body": {
516
+ "description": "The member's body in erasable TypeScript, which plain JavaScript already is (spec §23.1), as one string or as an array of lines.",
509
517
  "oneOf": [
510
518
  {
511
519
  "type": "string"
@@ -577,6 +585,7 @@
577
585
  "type": "string"
578
586
  },
579
587
  "body": {
588
+ "description": "The member's body in erasable TypeScript, which plain JavaScript already is (spec §23.1), as one string or as an array of lines.",
580
589
  "oneOf": [
581
590
  {
582
591
  "type": "string"
@@ -28500,6 +28509,21 @@
28500
28509
  }
28501
28510
  ]
28502
28511
  },
28512
+ "$script": {
28513
+ "additionalProperties": false,
28514
+ "description": "This document's own modules, as TypeScript module text (spec.md §23.2). A state entry names an export of one with `$src: \"#client\"` or `$src: \"#server\"`. `client` runs in the browser; `server` runs only on the server and is never sent to one.",
28515
+ "properties": {
28516
+ "client": {
28517
+ "description": "The module the browser runs. A Function entry with `$src: \"#client\"` is one of its exports.",
28518
+ "type": "string"
28519
+ },
28520
+ "server": {
28521
+ "description": "The module the server runs. A `timing: \"server\"` entry with `$src: \"#server\"` is one of its exports, called with `(args, env)`.",
28522
+ "type": "string"
28523
+ }
28524
+ },
28525
+ "type": "object"
28526
+ },
28503
28527
  "$shadow": {
28504
28528
  "description": "Render this component into a shadow root instead of the light DOM (spec.md §16.6). Off unless set: a shadow root isolates the component's styles from the page and replaces the <slot> emulation with real slot distribution, so it changes how the component composes. Overrides `defaults.shadow` in both directions — `false` opts one component out of a project that opted in.",
28505
28529
  "oneOf": [
@@ -32,6 +32,11 @@ export interface ComponentPropDef {
32
32
  /** Absent for a full-form entry that declares no `default`. */
33
33
  default?: JsonValue | undefined;
34
34
  format?: unknown;
35
+ /**
36
+ * The values the entry admits, read from its JSON Schema `enum` (spec.md §5.3). Absent when the
37
+ * entry declares none, so a prop with a closed set of values is told apart from free text.
38
+ */
39
+ enum?: JsonValue[];
35
40
  }
36
41
 
37
42
  /** What discovery reports for one component document. */
@@ -150,12 +155,16 @@ function propFrom(name: string, def: JsonValue): ComponentPropDef | null {
150
155
  if (fields.$prototype || fields.$handler || fields.$compute) {
151
156
  return null;
152
157
  }
153
- return {
158
+ const prop: ComponentPropDef = {
154
159
  default: fields.default,
155
160
  format: fields.format,
156
161
  name,
157
162
  type: fields.type,
158
163
  };
164
+ if (Array.isArray(fields.enum)) {
165
+ prop.enum = fields.enum;
166
+ }
167
+ return prop;
159
168
  }
160
169
 
161
170
  /**
@@ -0,0 +1,304 @@
1
+ /**
2
+ * A document's own modules (spec.md §23.2): how a `$src` names them, and how that name is given the
3
+ * document it belongs to.
4
+ *
5
+ * A document carries its client and server modules as text in `$script`, and an entry names one
6
+ * with `$src: "#client"` or `$src: "#server"`. Those two strings say nothing about WHICH document,
7
+ * and everything downstream keys a module on the specifier it was spelled with: the interpreter's
8
+ * module cache, the build's bundle table, the site worker's one-route-per-export rule. A page and
9
+ * its layout each naming `#client` would be one module to all of them.
10
+ *
11
+ * So a document is **qualified** once, as it is read and before anything merges it with another:
12
+ * `#client` becomes `jx-script:<path>#client`, where `<path>` is the document's own address. After
13
+ * that a page's module and its layout's are different strings, and nothing downstream needs to know
14
+ * that document scripts exist.
15
+ *
16
+ * Pure string and tree work, with no node imports, so every host can use it.
17
+ *
18
+ * @docs framework/concepts/functions
19
+ */
20
+
21
+ import { isJsonObject, isString } from "./guards";
22
+ import type { JsonValue, JxDocument, JxDocumentValue, JxElement } from "../types";
23
+
24
+ export type { JxDocumentScripts } from "../types";
25
+
26
+ /** The two modules a document may carry: one the browser runs, one the server runs. */
27
+ export type ScriptSide = "client" | "server";
28
+
29
+ /** What a qualified script specifier starts with. */
30
+ export const SCRIPT_SPECIFIER_PREFIX = "jx-script:";
31
+
32
+ /** The authored `$src` of an entry naming its own document's client module. */
33
+ export const CLIENT_SCRIPT_REF = "#client";
34
+
35
+ /** The authored `$src` of an entry naming its own document's server module. */
36
+ export const SERVER_SCRIPT_REF = "#server";
37
+
38
+ /** A qualified specifier, read back: the document it names, and which of its modules. */
39
+ /** A URL path naming a TypeScript module. */
40
+ const TYPESCRIPT_PATH = /\.[cm]?ts$/iu;
41
+
42
+ /**
43
+ * True when a URL path names a TypeScript module: text a browser cannot import as written, which a
44
+ * page's module loader erases first.
45
+ *
46
+ * @param pathname A URL's path, without its query or fragment.
47
+ */
48
+ export function isTypeScriptPath(pathname: string): boolean {
49
+ return TYPESCRIPT_PATH.test(pathname);
50
+ }
51
+
52
+ /**
53
+ * A document as a browser may have it: without its server module. The module is called over the
54
+ * network and never imported by a page, so its text is nothing a reader should be sent.
55
+ *
56
+ * The document given is not changed, and one with no server module is returned as it is. `$script`
57
+ * itself stays, however little is left in it: it is what says the document has modules of its own,
58
+ * which is what has a host qualify the entries that name them.
59
+ *
60
+ * @param doc The document as read.
61
+ */
62
+ export function withoutServerScript<T extends JxDocument>(doc: T): T {
63
+ const script = doc.$script;
64
+ if (script?.server === undefined) {
65
+ return doc;
66
+ }
67
+ const { server: _server, ...kept } = script;
68
+ return { ...doc, $script: kept };
69
+ }
70
+
71
+ export interface ScriptSpecifier {
72
+ /** The document's address, as it was qualified: `/`-separated and without a leading `./`. */
73
+ path: string;
74
+ side: ScriptSide;
75
+ }
76
+
77
+ /** The side an authored `$src` names when it names its own document's module, else `null`. */
78
+ export function scriptRefSide(src: string): ScriptSide | null {
79
+ if (src === CLIENT_SCRIPT_REF) {
80
+ return "client";
81
+ }
82
+ if (src === SERVER_SCRIPT_REF) {
83
+ return "server";
84
+ }
85
+ return null;
86
+ }
87
+
88
+ /** A document address in the one spelling a specifier uses: `/` separators, no leading `./`. */
89
+ function normalizedPath(path: string): string {
90
+ return path.replaceAll("\\", "/").replace(/^(?:\.\/)+/, "");
91
+ }
92
+
93
+ /**
94
+ * The specifier naming one of a document's modules: `jx-script:components/counter.jxn#client`.
95
+ *
96
+ * @param path The document's address, project-relative in a project.
97
+ * @param side Which module.
98
+ */
99
+ export function scriptSpecifier(path: string, side: ScriptSide): string {
100
+ return `${SCRIPT_SPECIFIER_PREFIX}${normalizedPath(path)}#${side}`;
101
+ }
102
+
103
+ /** True when a `$src` is a qualified script specifier. */
104
+ export function isScriptSpecifier(src: string): boolean {
105
+ return src.startsWith(SCRIPT_SPECIFIER_PREFIX);
106
+ }
107
+
108
+ /**
109
+ * Read a qualified specifier back into the document it names and the side. `null` for anything
110
+ * else, including a `jx-script:` specifier that names no document or neither side.
111
+ */
112
+ export function parseScriptSpecifier(src: string): ScriptSpecifier | null {
113
+ if (!isScriptSpecifier(src)) {
114
+ return null;
115
+ }
116
+ const rest = src.slice(SCRIPT_SPECIFIER_PREFIX.length);
117
+ const hash = rest.lastIndexOf("#");
118
+ if (hash <= 0) {
119
+ return null;
120
+ }
121
+ const side = rest.slice(hash + 1);
122
+ if (side !== "client" && side !== "server") {
123
+ return null;
124
+ }
125
+ return { path: rest.slice(0, hash), side };
126
+ }
127
+
128
+ /**
129
+ * One value of a document with every `$src` naming its own document's module qualified. A value
130
+ * with nothing to qualify is returned as it was given, so an untouched subtree is shared rather
131
+ * than copied.
132
+ */
133
+ function qualifiedValue(value: JxDocumentValue, path: string): JxDocumentValue {
134
+ if (Array.isArray(value)) {
135
+ const items = value.map((item) => qualifiedValue(item, path));
136
+ /* SAFETY: `items` holds what `value` held, each member either itself or its qualified copy, so
137
+ it is the same kind of array `value` was. */
138
+ return items.some((item, index) => item !== value[index]) ? (items as JsonValue[]) : value;
139
+ }
140
+ if (!isJsonObject(value)) {
141
+ return value;
142
+ }
143
+ let copy: Record<string, JxDocumentValue> | null = null;
144
+ for (const [key, member] of Object.entries(value)) {
145
+ let next: JxDocumentValue = member;
146
+ if (key === "$src" && isString(member)) {
147
+ const side = scriptRefSide(member);
148
+ next = side === null ? member : scriptSpecifier(path, side);
149
+ } else if (key !== "$script") {
150
+ next = qualifiedValue(member, path);
151
+ }
152
+ if (next !== member) {
153
+ copy ??= { ...value };
154
+ copy[key] = next;
155
+ }
156
+ }
157
+ /* SAFETY: `copy` is `value` with some members replaced by their qualified copies, which are the
158
+ same kinds of value, so it is the kind of object `value` was. */
159
+ return copy === null ? value : (copy as JxDocumentValue);
160
+ }
161
+
162
+ /**
163
+ * A document with every `$src: "#client"` and `$src: "#server"` in it naming THIS document:
164
+ * `jx-script:<path>#client`. Run once as a document is read, before a layout merge or a project
165
+ * state merge, because after either nothing records which file an entry was written in.
166
+ *
167
+ * The document given is not changed, and one with no such entry is returned as it is. `$script`
168
+ * itself is left alone: it is the text the specifier names. Qualifying twice changes nothing.
169
+ *
170
+ * @param doc The document as read.
171
+ * @param path The document's address: project-relative in a project, with either separator.
172
+ */
173
+ export function qualifyScripts<T extends JxDocument>(doc: T, path: string): T {
174
+ /* SAFETY: `qualifiedValue` returns its argument or a copy of it with `$src` strings replaced, so
175
+ a document goes in and the same kind of document comes out. */
176
+ return qualifiedValue(doc, path) as T;
177
+ }
178
+
179
+ // ─── Advice ─────────────────────────────────────────────────────────────────────
180
+
181
+ /** The two things worth saying about how a document names its modules. */
182
+ export type ScriptAdviceRule = "relative-src" | "lazy-shares-script";
183
+
184
+ /**
185
+ * One piece of advice about a `$src`. Advice, not a defect: the document is correct and runs as
186
+ * written everywhere, so nothing that reports one may fail on it.
187
+ */
188
+ export interface ScriptAdvice {
189
+ rule: ScriptAdviceRule;
190
+ /** The entry the `$src` is written on, in the segment vocabulary a document path uses. */
191
+ path: (string | number)[];
192
+ /** One sentence naming what was found, in the author's language. */
193
+ message: string;
194
+ /** What to do instead, and what happens when nothing is done. */
195
+ detail: string;
196
+ }
197
+
198
+ /** Matches a `$src` naming a file beside its document: `./counter.js`, `../lib/format.ts`. */
199
+ const RELATIVE_SRC = /^\.{1,2}\//u;
200
+
201
+ /** Matches a `$src` naming a JSON document, which is a class schema and never a script (§12.2). */
202
+ const JSON_SRC = /\.json$/iu;
203
+
204
+ /**
205
+ * True when a `$src` names a script file by a relative path: the one spelling a document's own
206
+ * script can stand in for. An `npm:` package, a kit behaviour (`jx-ui:/…`), an absolute URL and a
207
+ * `.class.json` schema have no other spelling, and `#client` is already the script.
208
+ *
209
+ * @param src An entry's `$src`.
210
+ */
211
+ export function isRelativeScriptSrc(src: string): boolean {
212
+ return RELATIVE_SRC.test(src) && !JSON_SRC.test(src);
213
+ }
214
+
215
+ /** True when a `$src` names its document's client module, as authored or once qualified. */
216
+ function namesClientScript(src: string): boolean {
217
+ return scriptRefSide(src) === "client" || parseScriptSpecifier(src)?.side === "client";
218
+ }
219
+
220
+ /** An entry carrying a `$src`, as the walk found it. */
221
+ interface SrcEntry {
222
+ path: (string | number)[];
223
+ src: string;
224
+ lazy: boolean;
225
+ }
226
+
227
+ /** Every object in a document that carries a string `$src`, in document order. */
228
+ function srcEntries(
229
+ value: JxDocumentValue,
230
+ path: (string | number)[],
231
+ found: SrcEntry[],
232
+ ): SrcEntry[] {
233
+ if (Array.isArray(value)) {
234
+ for (const [index, item] of value.entries()) {
235
+ srcEntries(item, [...path, index], found);
236
+ }
237
+ return found;
238
+ }
239
+ if (!isJsonObject(value)) {
240
+ return found;
241
+ }
242
+ if (isString(value.$src)) {
243
+ found.push({ lazy: value.$lazy === true, path, src: value.$src });
244
+ }
245
+ for (const [key, member] of Object.entries(value)) {
246
+ // `$script` is the modules' text, not entries that name one.
247
+ if (key !== "$script") {
248
+ srcEntries(member, [...path, key], found);
249
+ }
250
+ }
251
+ return found;
252
+ }
253
+
254
+ /** What an entry is called in a sentence: its key, or `this entry` for one in a list. */
255
+ function entryName(path: (string | number)[]): string {
256
+ const last = path.at(-1);
257
+ return isString(last) ? `"${last}"` : "this entry";
258
+ }
259
+
260
+ /**
261
+ * What there is to say about how a document names its modules (spec.md §23.2).
262
+ *
263
+ * - **`relative-src`**: an entry imports a file beside the document. A module only that document uses
264
+ * can be the document's own script, which makes the document one file. A `$lazy` entry is left
265
+ * alone: a module of its own is exactly what it needs.
266
+ * - **`lazy-shares-script`**: a `$lazy` entry names the client script that another entry loads up
267
+ * front. A document has one client script, so the deferral defers nothing.
268
+ *
269
+ * `jx validate` and Studio's Problems both report from here, so the two say the same thing about
270
+ * the same document.
271
+ *
272
+ * @param doc The document as written, or as qualified: both spellings of `#client` are read.
273
+ */
274
+ export function findScriptAdvice(doc: JxDocument | JxElement): ScriptAdvice[] {
275
+ const entries = srcEntries(doc, [], []);
276
+ const eagerClient = entries.some((entry) => !entry.lazy && namesClientScript(entry.src));
277
+ const advice: ScriptAdvice[] = [];
278
+ for (const entry of entries) {
279
+ const name = entryName(entry.path);
280
+ if (!entry.lazy && isRelativeScriptSrc(entry.src)) {
281
+ advice.push({
282
+ detail:
283
+ "A module only this document uses can be the document's own script: move the code " +
284
+ 'into the client script (the server script for a `timing: "server"` entry) and name ' +
285
+ 'it with `$src: "#client"` or `$src: "#server"`. A module several documents import ' +
286
+ "stays a file, and this one keeps working as it is.",
287
+ message: `${name} imports ${entry.src}, which the document could carry as its own script`,
288
+ path: entry.path,
289
+ rule: "relative-src",
290
+ });
291
+ } else if (entry.lazy && eagerClient && namesClientScript(entry.src)) {
292
+ advice.push({
293
+ detail:
294
+ "A document has one client script, and another entry imports it as the page loads, " +
295
+ "so `$lazy` defers nothing here. Move this export to a `$src` module of its own to " +
296
+ "load it on its first call.",
297
+ message: `${name} is $lazy, but the script it is in loads with the page`,
298
+ path: entry.path,
299
+ rule: "lazy-shares-script",
300
+ });
301
+ }
302
+ }
303
+ return advice;
304
+ }