@jxsuite/erase 0.0.0 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/erase",
3
- "version": "0.0.0",
3
+ "version": "0.1.0",
4
4
  "description": "Erases TypeScript from Jx function bodies, so every tier that analyses or runs one sees JavaScript",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,14 +17,14 @@
17
17
  "./browser": "./src/browser.ts"
18
18
  },
19
19
  "publishConfig": {
20
- "provenance": false
20
+ "provenance": true
21
21
  },
22
22
  "scripts": {
23
23
  "test": "bun test --isolate",
24
24
  "test:coverage": "bun test --isolate --coverage"
25
25
  },
26
26
  "dependencies": {
27
- "@jxsuite/schema": "workspace:^",
27
+ "@jxsuite/schema": "^5.5.0",
28
28
  "ts-blank-space": "^0.9.0"
29
29
  }
30
30
  }
package/src/browser.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * The browser's eraser: `eraseBody` and nothing else, for a host to build into a file of its own
3
3
  * beside the runtime and hand to it with `setBodyEraser` or `setBodyEraserLoader` (`spec.md`
4
- * §23.1). It imports no runtime, so the runtime a page already has is the one that receives it.
4
+ * §23.1). It imports no runtime, so the runtime a page already has is the one that receives it. The
5
+ * stripper is imported, not required: a page loads this whole file only when a body needs it.
5
6
  *
6
7
  * @docs framework/concepts/functions
7
8
  */
8
- export { eraseBody } from "./index.ts";
9
- export type { EraseProblem, ErasedBody } from "./index.ts";
9
+ import tsBlankSpace from "ts-blank-space";
10
+ import { setStripperLoader } from "./core.ts";
11
+
12
+ setStripperLoader(() => tsBlankSpace);
13
+
14
+ export { eraseBody } from "./core.ts";
15
+ export type { EraseProblem, ErasedBody } from "./core.ts";
package/src/core.ts ADDED
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Erasing TypeScript from Jx function bodies (`spec.md` §23.1): everything but loading the
3
+ * stripper, which each entry does its own way (`index.ts` on a server, `browser.ts` in a page).
4
+ *
5
+ * A string `body` may use erasable TypeScript in any document format. Every tier that analyses or
6
+ * runs one wants JavaScript, so the types are erased once, as a document is read, and everything
7
+ * after that sees JavaScript: the analysis that classifies a body, every emitter, the interpreter.
8
+ * The stored document keeps the authored text; only the copy that runs is erased.
9
+ *
10
+ * Erasing replaces each type with spaces, so a line and column in a stack trace match the body as
11
+ * written. Only erasable syntax is accepted: `enum`, a value `namespace`, a parameter property and
12
+ * a module `import` or `export` need code generated for them, and a body that needs imports belongs
13
+ * in a module of its own (`spec.md` §23.2).
14
+ *
15
+ * The stripper is `ts-blank-space`, kept behind this package so it can be replaced. This package
16
+ * never imports `@jxsuite/runtime`: a host builds it as a file beside the runtime and hands the
17
+ * runtime the function.
18
+ *
19
+ * @docs framework/concepts/functions
20
+ */
21
+ import { isJsonObject, isString, maskCode } from "@jxsuite/schema/guards";
22
+ import type { JsonObject, JsonValue, JxClassDef, JxDocument } from "@jxsuite/schema/types";
23
+
24
+ /** Something about a body worth saying, at a place in it. */
25
+ export interface EraseProblem {
26
+ /** One-based. */
27
+ column: number;
28
+ /**
29
+ * `unsupported`: TypeScript that cannot be erased, so the body cannot run. `meaning`: the text
30
+ * was already JavaScript, and reading it as TypeScript changed what it does (`a < b > (c)`).
31
+ */
32
+ kind: "meaning" | "unsupported";
33
+ /** One-based. */
34
+ line: number;
35
+ message: string;
36
+ /** Zero-based, into the authored text. */
37
+ offset: number;
38
+ }
39
+
40
+ /** A body's JavaScript, and what erasing it found. */
41
+ export interface ErasedBody {
42
+ code: string;
43
+ problems: EraseProblem[];
44
+ }
45
+
46
+ /** A one-based line and column. */
47
+ interface Place {
48
+ column: number;
49
+ line: number;
50
+ }
51
+
52
+ /** Line and column of an offset. */
53
+ function place(text: string, offset: number): Place {
54
+ const before = text.slice(0, offset);
55
+ const line = before.split("\n").length;
56
+ return { column: offset - before.lastIndexOf("\n"), line };
57
+ }
58
+
59
+ /** A problem at an offset. */
60
+ function problemAt(
61
+ text: string,
62
+ offset: number,
63
+ kind: EraseProblem["kind"],
64
+ message: string,
65
+ ): EraseProblem {
66
+ return { ...place(text, offset), kind, message, offset };
67
+ }
68
+
69
+ /** The first non-blank offset at or after `from`. */
70
+ function startAt(text: string, from: number): number {
71
+ let offset = from;
72
+ while (offset < text.length && /\s/u.test(text.charAt(offset))) {
73
+ offset += 1;
74
+ }
75
+ return offset;
76
+ }
77
+
78
+ /** A module `import` or `export` at statement position, in masked code. */
79
+ const MODULE_STATEMENT = /(^|[\n;{}])(\s*)(import|export)(?=[\s{*"'])/gu;
80
+
81
+ /**
82
+ * Whether text parses as a JavaScript function body, sync or async. Parsed and never called:
83
+ * whether it parses is the whole question. An `await` needs the body inside an async function,
84
+ * which the second form gives it.
85
+ */
86
+ function isJavaScriptBody(text: string): boolean {
87
+ for (const source of [text, `return async function () {\n${text}\n}`]) {
88
+ try {
89
+ const parsed: unknown = Reflect.construct(Function, [source]);
90
+ return parsed !== null;
91
+ } catch {
92
+ // Not as this kind of function; the other may still take it.
93
+ }
94
+ }
95
+ return false;
96
+ }
97
+
98
+ const cache = new Map<string, ErasedBody>();
99
+
100
+ /** Removes the types from a body, calling back at each node it cannot remove. */
101
+ export type Stripper = (text: string, onError: (node: { pos: number }) => void) => string;
102
+
103
+ let stripper: Stripper | null = null;
104
+ let loadStripper: (() => Stripper) | null = null;
105
+
106
+ /**
107
+ * Say how to get the stripper. It is asked for only when a body could be TypeScript: it is all of
108
+ * TypeScript's parser, and having it loaded made every later compile in the process slower, whether
109
+ * or not a body needed it.
110
+ *
111
+ * @param load Returns the stripper, once.
112
+ */
113
+ export function setStripperLoader(load: () => Stripper): void {
114
+ loadStripper = load;
115
+ stripper = null;
116
+ }
117
+
118
+ /** The stripper, loaded the first time a body needs it. */
119
+ function strip(text: string, onError: (node: { pos: number }) => void): string {
120
+ if (!stripper) {
121
+ if (!loadStripper) {
122
+ throw new Error("@jxsuite/erase was imported without an entry that loads its stripper");
123
+ }
124
+ stripper = loadStripper();
125
+ }
126
+ return stripper(text, onError);
127
+ }
128
+
129
+ /**
130
+ * Whether a body could read differently as TypeScript. A body that already parses as JavaScript and
131
+ * has no `<` in its code cannot: every other TypeScript form either fails to parse as JavaScript
132
+ * (`x!`, `(a: T)`, `enum`) or, like `as`, `satisfies`, `type` and `declare`, is TypeScript only
133
+ * with no line break before it, where JavaScript would not parse it either. `<` is the exception,
134
+ * as a generic call or arrow, so it always goes to the stripper.
135
+ */
136
+ function couldBeTypeScript(text: string): boolean {
137
+ return maskCode(text).includes("<") || !isJavaScriptBody(text);
138
+ }
139
+
140
+ /**
141
+ * Erase the TypeScript from one function body.
142
+ *
143
+ * Erasing twice changes nothing, so a document that crosses two hosts is safe.
144
+ *
145
+ * @param text The body as authored.
146
+ */
147
+ export function eraseBody(text: string): ErasedBody {
148
+ const known = cache.get(text);
149
+ if (known) {
150
+ return known;
151
+ }
152
+ if (!couldBeTypeScript(text)) {
153
+ const plain: ErasedBody = { code: text, problems: [] };
154
+ cache.set(text, plain);
155
+ return plain;
156
+ }
157
+ const problems: EraseProblem[] = [];
158
+ const code = strip(text, (node) => {
159
+ const offset = startAt(text, node.pos);
160
+ const [word = "this"] = /^[\w$]+/u.exec(text.slice(offset)) ?? [];
161
+ problems.push(
162
+ problemAt(
163
+ text,
164
+ offset,
165
+ "unsupported",
166
+ `\`${word}\` is TypeScript that has to be compiled rather than erased. A function body may use only types that can be removed: no enum, value namespace or parameter property`,
167
+ ),
168
+ );
169
+ });
170
+ const masked = maskCode(code);
171
+ for (const match of masked.matchAll(MODULE_STATEMENT)) {
172
+ const offset = match.index + (match[1]?.length ?? 0) + (match[2]?.length ?? 0);
173
+ problems.push(
174
+ problemAt(
175
+ text,
176
+ offset,
177
+ "unsupported",
178
+ `A function body cannot ${match[3] ?? "import"}. Move code that needs a module into one of its own, named by $src`,
179
+ ),
180
+ );
181
+ }
182
+ if (code !== text && problems.length === 0 && isJavaScriptBody(text)) {
183
+ const offset = [...text].findIndex((char, index) => char !== code[index]);
184
+ problems.push(
185
+ problemAt(
186
+ text,
187
+ Math.max(offset, 0),
188
+ "meaning",
189
+ "This was already JavaScript, and reading it as TypeScript changes it: `a < b > (c)` is a generic call in TypeScript and two comparisons in JavaScript. Add parentheses if a comparison is meant",
190
+ ),
191
+ );
192
+ }
193
+ const erased: ErasedBody = { code, problems };
194
+ cache.set(text, erased);
195
+ return erased;
196
+ }
197
+
198
+ /** Where a body sits, for a message: the file and the JSON Pointer of the body. */
199
+ export interface BodySite {
200
+ path: string;
201
+ pointer: string;
202
+ }
203
+
204
+ /** A body that cannot be erased, named by where it is. */
205
+ export class EraseError extends Error {
206
+ readonly problems: readonly EraseProblem[];
207
+ readonly site: BodySite;
208
+
209
+ constructor(site: BodySite, problems: readonly [EraseProblem, ...EraseProblem[]]) {
210
+ const [first] = problems;
211
+ super(
212
+ `${site.path}${site.pointer}: ${first.message} (line ${first.line}, column ${first.column})`,
213
+ );
214
+ this.name = "EraseError";
215
+ this.problems = problems;
216
+ this.site = site;
217
+ }
218
+ }
219
+
220
+ /** What erasing a whole document or class found that did not stop it. */
221
+ export interface EraseWarning extends BodySite {
222
+ problem: EraseProblem;
223
+ }
224
+
225
+ /** Escape a key for a JSON Pointer. */
226
+ function pointerKey(key: string | number): string {
227
+ return String(key).replaceAll("~", "~0").replaceAll("/", "~1");
228
+ }
229
+
230
+ /** Erase one body value, a string or an array of lines, or leave it when it is neither. */
231
+ function eraseValue(value: JsonValue, site: BodySite, warnings: EraseWarning[]): JsonValue {
232
+ if (isString(value)) {
233
+ return eraseOne(value, site, warnings);
234
+ }
235
+ if (Array.isArray(value) && value.length > 0 && value.every((line) => isString(line))) {
236
+ /* A class body may be lines. They are erased as one text, because a type can span them, and
237
+ erasing keeps every newline, so the lines come back one for one. */
238
+ return eraseOne(value.join("\n"), site, warnings).split("\n");
239
+ }
240
+ // A structured body (`spec.md` §20) has no text to erase.
241
+ return value;
242
+ }
243
+
244
+ /** Erase one text, throwing when it cannot be and collecting what can be said. */
245
+ function eraseOne(text: string, site: BodySite, warnings: EraseWarning[]): string {
246
+ const { code, problems } = eraseBody(text);
247
+ const [first, ...rest] = problems.filter((problem) => problem.kind === "unsupported");
248
+ if (first) {
249
+ throw new EraseError(site, [first, ...rest]);
250
+ }
251
+ for (const problem of problems) {
252
+ warnings.push({ ...site, problem });
253
+ }
254
+ return code;
255
+ }
256
+
257
+ /** Copy a tree, erasing every function body in it. */
258
+ function walk(value: JsonValue, site: BodySite, warnings: EraseWarning[]): JsonValue {
259
+ if (Array.isArray(value)) {
260
+ return value.map((item, index) =>
261
+ walk(item, { ...site, pointer: `${site.pointer}/${index}` }, warnings),
262
+ );
263
+ }
264
+ if (!isJsonObject(value)) {
265
+ return value;
266
+ }
267
+ const isFunction = value.$prototype === "Function";
268
+ /* Built as entries: assigning `__proto__` would set the copy's prototype, where `JSON.parse`
269
+ made it an own key. */
270
+ const entries: [string, JsonValue][] = [];
271
+ const copy = {
272
+ set: (key: string, child: JsonValue): void => {
273
+ entries.push([key, child]);
274
+ },
275
+ };
276
+ for (const [key, child] of Object.entries(value)) {
277
+ const at = { ...site, pointer: `${site.pointer}/${pointerKey(key)}` };
278
+ if (isFunction && key === "body") {
279
+ copy.set(key, eraseValue(child, at, warnings));
280
+ } else if (isFunction && (key === "getter" || key === "setter") && isJsonObject(child)) {
281
+ // An accessor's halves hold a body of their own, with no `$prototype` to say so.
282
+ const half: JsonObject = { ...child };
283
+ if (child.body !== undefined) {
284
+ half.body = eraseValue(child.body, { ...at, pointer: `${at.pointer}/body` }, warnings);
285
+ }
286
+ copy.set(key, half);
287
+ } else {
288
+ copy.set(key, walk(child, at, warnings));
289
+ }
290
+ }
291
+ return Object.fromEntries(entries);
292
+ }
293
+
294
+ /**
295
+ * An erased copy of a document: every `Function` entry's string body, wherever it sits (state, an
296
+ * inline handler, `$map`, `cases`, a class member), as JavaScript. Any other `body` is left alone,
297
+ * because one may be data (a `Request`'s payload, a component's own state).
298
+ *
299
+ * @param document The document as read.
300
+ * @param path Its path, which names it in an error.
301
+ * @param warnings Collects what erasing found that did not stop it.
302
+ * @throws {EraseError} When a body uses TypeScript that cannot be erased.
303
+ */
304
+ export function eraseDocument<T extends JsonValue | JxDocument | JxClassDef>(
305
+ document: T,
306
+ path: string,
307
+ warnings: EraseWarning[] = [],
308
+ ): T {
309
+ // SAFETY: a Jx document is JSON data; its type says only which members it may hold.
310
+ const tree = document as JsonValue;
311
+ // SAFETY: the walk copies the tree shape for shape, replacing only string bodies with strings.
312
+ return walk(tree, { path, pointer: "" }, warnings) as T;
313
+ }
314
+
315
+ /**
316
+ * An erased copy of a class definition: its constructor's, methods' and accessors' bodies. The same
317
+ * walk as {@link eraseDocument}, because a member is a `Function` entry by the same rule.
318
+ *
319
+ * @param definition The class definition as read.
320
+ * @param path Its path, which names it in an error.
321
+ * @param warnings Collects what erasing found that did not stop it.
322
+ * @throws {EraseError} When a body uses TypeScript that cannot be erased.
323
+ */
324
+ export function eraseClass<T extends JsonValue | JxDocument | JxClassDef>(
325
+ definition: T,
326
+ path: string,
327
+ warnings: EraseWarning[] = [],
328
+ ): T {
329
+ return eraseDocument(definition, path, warnings);
330
+ }
package/src/index.ts CHANGED
@@ -1,285 +1,21 @@
1
1
  /**
2
- * Erasing TypeScript from Jx function bodies (`spec.md` §23.1).
2
+ * Erasing TypeScript from Jx function bodies (`spec.md` §23.1), for a server, a build or a CLI.
3
3
  *
4
- * A string `body` may use erasable TypeScript in any document format. Every tier that analyses or
5
- * runs one wants JavaScript, so the types are erased once, as a document is read, and everything
6
- * after that sees JavaScript: the analysis that classifies a body, every emitter, the interpreter.
7
- * The stored document keeps the authored text; only the copy that runs is erased.
8
- *
9
- * Erasing replaces each type with spaces, so a line and column in a stack trace match the body as
10
- * written. Only erasable syntax is accepted: `enum`, a value `namespace`, a parameter property and
11
- * a module `import` or `export` need code generated for them, and a body that needs imports belongs
12
- * in a module of its own (`spec.md` §23.2).
13
- *
14
- * The stripper is `ts-blank-space`, kept behind this package so it can be replaced. This package
15
- * never imports `@jxsuite/runtime`: a host builds it as a file beside the runtime and hands the
16
- * runtime the function.
4
+ * The stripper is `ts-blank-space`, kept behind this package so it can be replaced, and it is
5
+ * required the first time a body could be TypeScript rather than imported here: a project whose
6
+ * bodies are all JavaScript never loads TypeScript's parser, and one that does pays for it once.
17
7
  *
18
8
  * @docs framework/concepts/functions
19
9
  */
20
- import tsBlankSpace from "ts-blank-space";
21
- import { isJsonObject, isString, maskCode } from "@jxsuite/schema/guards";
22
- import type { JsonObject, JsonValue, JxClassDef, JxDocument } from "@jxsuite/schema/types";
23
-
24
- /** Something about a body worth saying, at a place in it. */
25
- export interface EraseProblem {
26
- /** One-based. */
27
- column: number;
28
- /**
29
- * `unsupported`: TypeScript that cannot be erased, so the body cannot run. `meaning`: the text
30
- * was already JavaScript, and reading it as TypeScript changed what it does (`a < b > (c)`).
31
- */
32
- kind: "meaning" | "unsupported";
33
- /** One-based. */
34
- line: number;
35
- message: string;
36
- /** Zero-based, into the authored text. */
37
- offset: number;
38
- }
39
-
40
- /** A body's JavaScript, and what erasing it found. */
41
- export interface ErasedBody {
42
- code: string;
43
- problems: EraseProblem[];
44
- }
45
-
46
- /** A one-based line and column. */
47
- interface Place {
48
- column: number;
49
- line: number;
50
- }
51
-
52
- /** Line and column of an offset. */
53
- function place(text: string, offset: number): Place {
54
- const before = text.slice(0, offset);
55
- const line = before.split("\n").length;
56
- return { column: offset - before.lastIndexOf("\n"), line };
57
- }
58
-
59
- /** A problem at an offset. */
60
- function problemAt(
61
- text: string,
62
- offset: number,
63
- kind: EraseProblem["kind"],
64
- message: string,
65
- ): EraseProblem {
66
- return { ...place(text, offset), kind, message, offset };
67
- }
68
-
69
- /** The first non-blank offset at or after `from`. */
70
- function startAt(text: string, from: number): number {
71
- let offset = from;
72
- while (offset < text.length && /\s/u.test(text.charAt(offset))) {
73
- offset += 1;
74
- }
75
- return offset;
76
- }
77
-
78
- /** A module `import` or `export` at statement position, in masked code. */
79
- const MODULE_STATEMENT = /(^|[\n;{}])(\s*)(import|export)(?=[\s{*"'])/gu;
80
-
81
- /**
82
- * Whether text parses as a JavaScript function body, sync or async. Parsed and never called:
83
- * whether it parses is the whole question. An `await` needs the body inside an async function,
84
- * which the second form gives it.
85
- */
86
- function isJavaScriptBody(text: string): boolean {
87
- for (const source of [text, `return async function () {\n${text}\n}`]) {
88
- try {
89
- const parsed: unknown = Reflect.construct(Function, [source]);
90
- return parsed !== null;
91
- } catch {
92
- // Not as this kind of function; the other may still take it.
93
- }
94
- }
95
- return false;
96
- }
97
-
98
- const cache = new Map<string, ErasedBody>();
99
-
100
- /**
101
- * Erase the TypeScript from one function body.
102
- *
103
- * Erasing twice changes nothing, so a document that crosses two hosts is safe.
104
- *
105
- * @param text The body as authored.
106
- */
107
- export function eraseBody(text: string): ErasedBody {
108
- const known = cache.get(text);
109
- if (known) {
110
- return known;
111
- }
112
- const problems: EraseProblem[] = [];
113
- const code = tsBlankSpace(text, (node) => {
114
- const offset = startAt(text, node.pos);
115
- const [word = "this"] = /^[\w$]+/u.exec(text.slice(offset)) ?? [];
116
- problems.push(
117
- problemAt(
118
- text,
119
- offset,
120
- "unsupported",
121
- `\`${word}\` is TypeScript that has to be compiled rather than erased. A function body may use only types that can be removed: no enum, value namespace or parameter property`,
122
- ),
123
- );
124
- });
125
- const masked = maskCode(code);
126
- for (const match of masked.matchAll(MODULE_STATEMENT)) {
127
- const offset = match.index + (match[1]?.length ?? 0) + (match[2]?.length ?? 0);
128
- problems.push(
129
- problemAt(
130
- text,
131
- offset,
132
- "unsupported",
133
- `A function body cannot ${match[3] ?? "import"}. Move code that needs a module into one of its own, named by $src`,
134
- ),
135
- );
136
- }
137
- if (code !== text && problems.length === 0 && isJavaScriptBody(text)) {
138
- const offset = [...text].findIndex((char, index) => char !== code[index]);
139
- problems.push(
140
- problemAt(
141
- text,
142
- Math.max(offset, 0),
143
- "meaning",
144
- "This was already JavaScript, and reading it as TypeScript changes it: `a < b > (c)` is a generic call in TypeScript and two comparisons in JavaScript. Add parentheses if a comparison is meant",
145
- ),
146
- );
147
- }
148
- const erased: ErasedBody = { code, problems };
149
- cache.set(text, erased);
150
- return erased;
151
- }
152
-
153
- /** Where a body sits, for a message: the file and the JSON Pointer of the body. */
154
- export interface BodySite {
155
- path: string;
156
- pointer: string;
157
- }
158
-
159
- /** A body that cannot be erased, named by where it is. */
160
- export class EraseError extends Error {
161
- readonly problems: readonly EraseProblem[];
162
- readonly site: BodySite;
163
-
164
- constructor(site: BodySite, problems: readonly [EraseProblem, ...EraseProblem[]]) {
165
- const [first] = problems;
166
- super(
167
- `${site.path}${site.pointer}: ${first.message} (line ${first.line}, column ${first.column})`,
168
- );
169
- this.name = "EraseError";
170
- this.problems = problems;
171
- this.site = site;
172
- }
173
- }
174
-
175
- /** What erasing a whole document or class found that did not stop it. */
176
- export interface EraseWarning extends BodySite {
177
- problem: EraseProblem;
178
- }
179
-
180
- /** Escape a key for a JSON Pointer. */
181
- function pointerKey(key: string | number): string {
182
- return String(key).replaceAll("~", "~0").replaceAll("/", "~1");
183
- }
184
-
185
- /** Erase one body value, a string or an array of lines, or leave it when it is neither. */
186
- function eraseValue(value: JsonValue, site: BodySite, warnings: EraseWarning[]): JsonValue {
187
- if (isString(value)) {
188
- return eraseOne(value, site, warnings);
189
- }
190
- if (Array.isArray(value) && value.length > 0 && value.every((line) => isString(line))) {
191
- /* A class body may be lines. They are erased as one text, because a type can span them, and
192
- erasing keeps every newline, so the lines come back one for one. */
193
- return eraseOne(value.join("\n"), site, warnings).split("\n");
194
- }
195
- // A structured body (`spec.md` §20) has no text to erase.
196
- return value;
197
- }
198
-
199
- /** Erase one text, throwing when it cannot be and collecting what can be said. */
200
- function eraseOne(text: string, site: BodySite, warnings: EraseWarning[]): string {
201
- const { code, problems } = eraseBody(text);
202
- const [first, ...rest] = problems.filter((problem) => problem.kind === "unsupported");
203
- if (first) {
204
- throw new EraseError(site, [first, ...rest]);
205
- }
206
- for (const problem of problems) {
207
- warnings.push({ ...site, problem });
208
- }
209
- return code;
210
- }
211
-
212
- /** Copy a tree, erasing every function body in it. */
213
- function walk(value: JsonValue, site: BodySite, warnings: EraseWarning[]): JsonValue {
214
- if (Array.isArray(value)) {
215
- return value.map((item, index) =>
216
- walk(item, { ...site, pointer: `${site.pointer}/${index}` }, warnings),
217
- );
218
- }
219
- if (!isJsonObject(value)) {
220
- return value;
221
- }
222
- const isFunction = value.$prototype === "Function";
223
- /* Built as entries: assigning `__proto__` would set the copy's prototype, where `JSON.parse`
224
- made it an own key. */
225
- const entries: [string, JsonValue][] = [];
226
- const copy = {
227
- set: (key: string, child: JsonValue): void => {
228
- entries.push([key, child]);
229
- },
230
- };
231
- for (const [key, child] of Object.entries(value)) {
232
- const at = { ...site, pointer: `${site.pointer}/${pointerKey(key)}` };
233
- if (isFunction && key === "body") {
234
- copy.set(key, eraseValue(child, at, warnings));
235
- } else if (isFunction && (key === "getter" || key === "setter") && isJsonObject(child)) {
236
- // An accessor's halves hold a body of their own, with no `$prototype` to say so.
237
- const half: JsonObject = { ...child };
238
- if (child.body !== undefined) {
239
- half.body = eraseValue(child.body, { ...at, pointer: `${at.pointer}/body` }, warnings);
240
- }
241
- copy.set(key, half);
242
- } else {
243
- copy.set(key, walk(child, at, warnings));
244
- }
245
- }
246
- return Object.fromEntries(entries);
247
- }
248
-
249
- /**
250
- * An erased copy of a document: every `Function` entry's string body, wherever it sits (state, an
251
- * inline handler, `$map`, `cases`, a class member), as JavaScript. Any other `body` is left alone,
252
- * because one may be data (a `Request`'s payload, a component's own state).
253
- *
254
- * @param document The document as read.
255
- * @param path Its path, which names it in an error.
256
- * @param warnings Collects what erasing found that did not stop it.
257
- * @throws {EraseError} When a body uses TypeScript that cannot be erased.
258
- */
259
- export function eraseDocument<T extends JsonValue | JxDocument | JxClassDef>(
260
- document: T,
261
- path: string,
262
- warnings: EraseWarning[] = [],
263
- ): T {
264
- // SAFETY: a Jx document is JSON data; its type says only which members it may hold.
265
- const tree = document as JsonValue;
266
- // SAFETY: the walk copies the tree shape for shape, replacing only string bodies with strings.
267
- return walk(tree, { path, pointer: "" }, warnings) as T;
268
- }
269
-
270
- /**
271
- * An erased copy of a class definition: its constructor's, methods' and accessors' bodies. The same
272
- * walk as {@link eraseDocument}, because a member is a `Function` entry by the same rule.
273
- *
274
- * @param definition The class definition as read.
275
- * @param path Its path, which names it in an error.
276
- * @param warnings Collects what erasing found that did not stop it.
277
- * @throws {EraseError} When a body uses TypeScript that cannot be erased.
278
- */
279
- export function eraseClass<T extends JsonValue | JxDocument | JxClassDef>(
280
- definition: T,
281
- path: string,
282
- warnings: EraseWarning[] = [],
283
- ): T {
284
- return eraseDocument(definition, path, warnings);
285
- }
10
+ import { createRequire } from "node:module";
11
+ import { setStripperLoader } from "./core.ts";
12
+ import type { Stripper } from "./core.ts";
13
+
14
+ setStripperLoader(() => {
15
+ /* SAFETY: `ts-blank-space` is a dependency of this package, and its default export is the
16
+ function the `Stripper` type describes. */
17
+ const loaded = createRequire(import.meta.url)("ts-blank-space") as { default: Stripper };
18
+ return loaded.default;
19
+ });
20
+
21
+ export * from "./core.ts";