@mapled/cli 0.11.0 → 0.13.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 +5 -2
- package/dist/commands.js +23 -2
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +145 -1
- package/dist/live.d.ts +264 -0
- package/dist/live.js +2346 -0
- package/dist/manifest.d.ts +14 -0
- package/dist/manifest.js +54 -2
- package/dist/pin.js +7 -0
- package/dist/scan.d.ts +30 -5
- package/dist/scan.js +194 -35
- package/dist/schema.d.ts +3 -0
- package/dist/schema.js +2 -0
- package/package.json +1 -1
package/dist/manifest.d.ts
CHANGED
|
@@ -22,12 +22,21 @@ export type ManifestPage = {
|
|
|
22
22
|
route: string;
|
|
23
23
|
file?: string;
|
|
24
24
|
};
|
|
25
|
+
/** What the site can do beyond reading (§21.2): `realtime` — open tabs
|
|
26
|
+
follow Publish (§18.8), through `releaseRoute` when the site answers
|
|
27
|
+
the release itself. Not part of the integration hash: Mapled grades
|
|
28
|
+
nothing by it, it only tells the next agent (MAPLED.md). */
|
|
29
|
+
export type ManifestCapabilities = {
|
|
30
|
+
realtime?: boolean;
|
|
31
|
+
releaseRoute?: string;
|
|
32
|
+
};
|
|
25
33
|
export type Manifest = {
|
|
26
34
|
framework?: string;
|
|
27
35
|
integrationMode?: string;
|
|
28
36
|
pages?: ManifestPage[];
|
|
29
37
|
bindings: ManifestBinding[];
|
|
30
38
|
notes?: string[];
|
|
39
|
+
capabilities?: ManifestCapabilities;
|
|
31
40
|
};
|
|
32
41
|
export declare const KEY_PATTERN: RegExp;
|
|
33
42
|
export declare const LIMITS: {
|
|
@@ -48,6 +57,7 @@ export type Problem = {
|
|
|
48
57
|
path: string;
|
|
49
58
|
message: string;
|
|
50
59
|
};
|
|
60
|
+
export declare const CAPABILITY_KEYS: readonly ["realtime", "releaseRoute"];
|
|
51
61
|
/** Parses the file's text into a manifest with only the known fields,
|
|
52
62
|
reporting every shape problem the API would refuse (as errors) and the
|
|
53
63
|
parts it would silently drop (as warnings). */
|
|
@@ -83,15 +93,19 @@ export declare function readManifestFile(dir: string): Promise<{
|
|
|
83
93
|
the order they came in. */
|
|
84
94
|
export declare function writeManifestFile(dir: string, manifest: Manifest): Promise<string>;
|
|
85
95
|
export declare function orderManifest(manifest: Manifest): Manifest;
|
|
96
|
+
/** Whether two manifests declare the same capabilities (missing and empty are the same). */
|
|
97
|
+
export declare function sameCapabilities(a: ManifestCapabilities | undefined, b: ManifestCapabilities | undefined): boolean;
|
|
86
98
|
export declare function orderBinding(b: ManifestBinding): ManifestBinding;
|
|
87
99
|
export declare function plural(n: number, word: string, pluralWord?: string): string;
|
|
88
100
|
export declare function manifestSummary(manifest: Manifest): string;
|
|
89
101
|
/** Bindings that mean the same thing to Mapled (what a push stores). */
|
|
90
102
|
export declare function sameBinding(a: ManifestBinding, b: ManifestBinding): boolean;
|
|
103
|
+
/** `same` is about the bindings; `capabilitiesDiffer` says whether the capabilities do. */
|
|
91
104
|
export type ManifestComparison = {
|
|
92
105
|
same: boolean;
|
|
93
106
|
onlyLocal: string[];
|
|
94
107
|
onlyRemote: string[];
|
|
95
108
|
changed: string[];
|
|
109
|
+
capabilitiesDiffer: boolean;
|
|
96
110
|
};
|
|
97
111
|
export declare function compareManifests(local: Manifest, remote: Manifest): ManifestComparison;
|
package/dist/manifest.js
CHANGED
|
@@ -91,7 +91,8 @@ export function typeLabel(type) {
|
|
|
91
91
|
}
|
|
92
92
|
const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
93
93
|
const isText = (v, max, min = 1) => typeof v === "string" && v.length >= min && v.length <= max;
|
|
94
|
-
const TOP_KEYS = new Set(["framework", "integrationMode", "pages", "bindings", "notes"]);
|
|
94
|
+
const TOP_KEYS = new Set(["framework", "integrationMode", "pages", "bindings", "notes", "capabilities"]);
|
|
95
|
+
export const CAPABILITY_KEYS = ["realtime", "releaseRoute"];
|
|
95
96
|
const BINDING_KEYS = new Set(["key", "page", "component", "file", "collection", "field", "target", "required"]);
|
|
96
97
|
/** Parses the file's text into a manifest with only the known fields,
|
|
97
98
|
reporting every shape problem the API would refuse (as errors) and the
|
|
@@ -236,6 +237,40 @@ export function parseManifest(raw, file) {
|
|
|
236
237
|
manifest.notes = data.notes;
|
|
237
238
|
}
|
|
238
239
|
}
|
|
240
|
+
if (data.capabilities !== undefined) {
|
|
241
|
+
const caps = data.capabilities;
|
|
242
|
+
if (!isObject(caps)) {
|
|
243
|
+
error("capabilities", `"capabilities" must be an object — { realtime, releaseRoute }.`);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const kept = {};
|
|
247
|
+
let ok = true;
|
|
248
|
+
for (const key of Object.keys(caps)) {
|
|
249
|
+
if (!CAPABILITY_KEYS.includes(key)) {
|
|
250
|
+
error(`capabilities.${key}`, `capabilities.${key} isn't a capability Mapled knows — only ${CAPABILITY_KEYS.join(" and ")}; Mapled refuses the push.`);
|
|
251
|
+
ok = false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (caps.realtime !== undefined) {
|
|
255
|
+
if (typeof caps.realtime === "boolean")
|
|
256
|
+
kept.realtime = caps.realtime;
|
|
257
|
+
else {
|
|
258
|
+
error("capabilities.realtime", "capabilities.realtime must be true or false.");
|
|
259
|
+
ok = false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (caps.releaseRoute !== undefined) {
|
|
263
|
+
if (isText(caps.releaseRoute, 200) && caps.releaseRoute.startsWith("/"))
|
|
264
|
+
kept.releaseRoute = caps.releaseRoute;
|
|
265
|
+
else {
|
|
266
|
+
error("capabilities.releaseRoute", `capabilities.releaseRoute must be the route's path on the site, e.g. "/api/mapled/release".`);
|
|
267
|
+
ok = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (ok)
|
|
271
|
+
manifest.capabilities = kept;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
239
274
|
return { manifest, problems };
|
|
240
275
|
}
|
|
241
276
|
export function indexSchema(schema) {
|
|
@@ -345,8 +380,19 @@ export function orderManifest(manifest) {
|
|
|
345
380
|
...(manifest.pages ? { pages: manifest.pages.map((p) => ({ route: p.route, ...(p.file !== undefined ? { file: p.file } : {}) })) } : {}),
|
|
346
381
|
bindings: manifest.bindings.map(orderBinding),
|
|
347
382
|
...(manifest.notes && manifest.notes.length > 0 ? { notes: [...manifest.notes] } : {}),
|
|
383
|
+
...(manifest.capabilities ? { capabilities: orderCapabilities(manifest.capabilities) } : {}),
|
|
348
384
|
};
|
|
349
385
|
}
|
|
386
|
+
function orderCapabilities(c) {
|
|
387
|
+
return {
|
|
388
|
+
...(c.realtime !== undefined ? { realtime: c.realtime } : {}),
|
|
389
|
+
...(c.releaseRoute !== undefined ? { releaseRoute: c.releaseRoute } : {}),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/** Whether two manifests declare the same capabilities (missing and empty are the same). */
|
|
393
|
+
export function sameCapabilities(a, b) {
|
|
394
|
+
return (a?.realtime ?? null) === (b?.realtime ?? null) && (a?.releaseRoute ?? null) === (b?.releaseRoute ?? null);
|
|
395
|
+
}
|
|
350
396
|
export function orderBinding(b) {
|
|
351
397
|
const out = { key: b.key, page: b.page, collection: b.collection, target: b.target };
|
|
352
398
|
if (b.field !== undefined)
|
|
@@ -382,5 +428,11 @@ export function compareManifests(local, remote) {
|
|
|
382
428
|
const onlyLocal = local.bindings.filter((b) => !remoteByKey.has(b.key)).map((b) => b.key);
|
|
383
429
|
const onlyRemote = remote.bindings.filter((b) => !localByKey.has(b.key)).map((b) => b.key);
|
|
384
430
|
const changed = local.bindings.filter((b) => remoteByKey.has(b.key) && !sameBinding(b, remoteByKey.get(b.key))).map((b) => b.key);
|
|
385
|
-
return {
|
|
431
|
+
return {
|
|
432
|
+
same: onlyLocal.length === 0 && onlyRemote.length === 0 && changed.length === 0,
|
|
433
|
+
onlyLocal,
|
|
434
|
+
onlyRemote,
|
|
435
|
+
changed,
|
|
436
|
+
capabilitiesDiffer: !sameCapabilities(local.capabilities, remote.capabilities),
|
|
437
|
+
};
|
|
386
438
|
}
|
package/dist/pin.js
CHANGED
|
@@ -47,6 +47,9 @@ function pinField(f) {
|
|
|
47
47
|
// the formula is part of the schema hash: a pin without it would never match the live schema
|
|
48
48
|
if (f.type === "computed" && f.computed)
|
|
49
49
|
out.computed = { expression: f.computed.expression, resultType: f.computed.resultType };
|
|
50
|
+
// translation is part of the schema hash too (§28.2)
|
|
51
|
+
if (f.localized === true)
|
|
52
|
+
out.localized = true;
|
|
50
53
|
if (f.aliases && f.aliases.length > 0)
|
|
51
54
|
out.aliases = [...f.aliases];
|
|
52
55
|
return out;
|
|
@@ -195,6 +198,10 @@ export function diffSchema(before, after) {
|
|
|
195
198
|
const under = af.key;
|
|
196
199
|
const push = (op, detail, severity) => changes.push({ ...base, scope: "field", field: under, op, detail, severity });
|
|
197
200
|
diffCommon(bf, af, names, push);
|
|
201
|
+
// §28.2: the site reads the default language either way until it asks for a locale
|
|
202
|
+
if ((bf.localized === true) !== (af.localized === true)) {
|
|
203
|
+
push("changed", af.localized === true ? "now translated — the site still reads the default language" : "no longer translated — the default language's value stays", "safe");
|
|
204
|
+
}
|
|
198
205
|
if (bf.type === "relation" && af.type === "relation" && bf.relation && af.relation) {
|
|
199
206
|
if (bf.relation.target !== af.relation.target) {
|
|
200
207
|
push("changed", `relation to ${names.get(bf.relation.target) ?? bf.relation.target} → ${names.get(af.relation.target) ?? af.relation.target}`, "breaking");
|
package/dist/scan.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type * as TS from "typescript";
|
|
2
|
-
import { type Manifest, type ManifestBinding, type ManifestPage } from "./manifest.js";
|
|
2
|
+
import { type Manifest, type ManifestBinding, type ManifestCapabilities, type ManifestPage } from "./manifest.js";
|
|
3
3
|
import type { Schema } from "./schema.js";
|
|
4
4
|
/** `mapled scan` (§19.5, §21.4 — the standalone scanner of wave 2): reads
|
|
5
5
|
the site's source and writes down what the code reads from Mapled —
|
|
@@ -37,24 +37,47 @@ export type ScanResult = {
|
|
|
37
37
|
pages: ManifestPage[];
|
|
38
38
|
bindings: ManifestBinding[];
|
|
39
39
|
notes: string[];
|
|
40
|
+
/** what the code says the site can do (live updates of a Next.js site); undefined keeps the manifest's */
|
|
41
|
+
capabilities?: ManifestCapabilities;
|
|
40
42
|
};
|
|
41
43
|
export declare const SDK_METHODS: Set<string>;
|
|
42
44
|
/** The repository's TypeScript, when it has one; the CLI ships none. */
|
|
43
45
|
export declare function loadTypeScript(dir: string): TypeScriptModule | null;
|
|
46
|
+
export declare function listFiles(dir: string, max: number): Promise<{
|
|
47
|
+
files: string[];
|
|
48
|
+
truncated: boolean;
|
|
49
|
+
}>;
|
|
44
50
|
export type RouteMap = {
|
|
45
51
|
pages: Map<string, string>;
|
|
46
52
|
layouts: Map<string, string>;
|
|
47
53
|
};
|
|
54
|
+
/** Next.js App Router segments: groups and slots vanish, params keep
|
|
55
|
+
the [param] form the manifest uses. */
|
|
56
|
+
export declare function appRoute(segments: string[]): string | null;
|
|
48
57
|
export declare function detectRoutes(files: string[], framework: string | null): RouteMap;
|
|
58
|
+
/** `nl`: a line break comes before the token — a `return` followed by one returns nothing. */
|
|
59
|
+
/** `nl`: a line break stands before the token; `escaped`: a string written
|
|
60
|
+
with a backslash escape, or an identifier with a `\u` escape in its
|
|
61
|
+
name; `unclosed`: a quoted string a line break (or the end) cut off
|
|
62
|
+
before its closing quote — JSX lets an attribute run on. */
|
|
49
63
|
export type Token = {
|
|
50
|
-
kind: "ident" | "string" | "punct";
|
|
64
|
+
kind: "ident" | "string" | "punct" | "regex";
|
|
51
65
|
value: string;
|
|
66
|
+
nl?: true;
|
|
67
|
+
escaped?: true;
|
|
68
|
+
unclosed?: true;
|
|
52
69
|
};
|
|
53
70
|
/** JavaScript tokens: identifiers, string literals and punctuation, with
|
|
54
71
|
comments and template bodies dropped. A regex literal after an
|
|
55
|
-
operator is skipped
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
operator is skipped — only one that closes on its line, and never the
|
|
73
|
+
`/>` after a JSX attribute's `}`; JSX text is read as code, so an apostrophe in
|
|
74
|
+
prose can swallow a stretch — a false negative, never a crash.
|
|
75
|
+
`hidden`, when given, gets the code of each template's `${…}`, which
|
|
76
|
+
the tokens leave out; `ambiguous`, where a `/` right after `}` was read
|
|
77
|
+
as a regex — after an object, JavaScript reads a division there, and
|
|
78
|
+
what follows is code (`{} / f(x) / 1`). With `regexes`, a regex is a
|
|
79
|
+
token of its own rather than skipped. */
|
|
80
|
+
export declare function tokenize(text: string, hidden?: string[], ambiguous?: number[], regexes?: boolean): Token[];
|
|
58
81
|
export declare function routeKey(route: string): string;
|
|
59
82
|
export declare function scanRepository(dir: string, opts: ScanOptions): Promise<ScanResult>;
|
|
60
83
|
export type MergeResult = {
|
|
@@ -65,6 +88,8 @@ export type MergeResult = {
|
|
|
65
88
|
/** Bindings the scan didn't produce: kept (or dropped with prune). */
|
|
66
89
|
kept: string[];
|
|
67
90
|
dropped: string[];
|
|
91
|
+
/** the scan changed what an existing manifest says the site can do */
|
|
92
|
+
capabilitiesChanged: boolean;
|
|
68
93
|
};
|
|
69
94
|
/** The scan's bindings replace the ones with the same key; bindings the
|
|
70
95
|
scanner can't see (an agent's, hand-written) stay unless pruned. */
|
package/dist/scan.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { targetFor } from "./manifest.js";
|
|
4
|
+
import { sameCapabilities, targetFor } from "./manifest.js";
|
|
5
5
|
const SKIP_DIRS = new Set([
|
|
6
6
|
"node_modules",
|
|
7
7
|
".git",
|
|
@@ -33,7 +33,7 @@ export function loadTypeScript(dir) {
|
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
async function listFiles(dir, max) {
|
|
36
|
+
export async function listFiles(dir, max) {
|
|
37
37
|
const files = [];
|
|
38
38
|
let truncated = false;
|
|
39
39
|
const walk = async (rel) => {
|
|
@@ -73,7 +73,7 @@ async function listFiles(dir, max) {
|
|
|
73
73
|
const PAGE_EXT = /\.(tsx|jsx|ts|js|mjs|mdx|md|astro|svelte|vue)$/;
|
|
74
74
|
/** Next.js App Router segments: groups and slots vanish, params keep
|
|
75
75
|
the [param] form the manifest uses. */
|
|
76
|
-
function appRoute(segments) {
|
|
76
|
+
export function appRoute(segments) {
|
|
77
77
|
const kept = [];
|
|
78
78
|
for (const s of segments) {
|
|
79
79
|
if (s.startsWith("_"))
|
|
@@ -753,18 +753,145 @@ function scopeOf(ts, id) {
|
|
|
753
753
|
}
|
|
754
754
|
return id.getSourceFile();
|
|
755
755
|
}
|
|
756
|
+
// what JavaScript reads as an identifier: Unicode letters and digits, `$`, `_`, and the two joiners inside one
|
|
757
|
+
const ID_START = /^[\p{ID_Start}$_]$/u;
|
|
758
|
+
const ID_PART = /^[\p{ID_Continue}$\p{Join_Control}]$/u;
|
|
759
|
+
/** The identifier character at `at` as JavaScript reads it — itself, or a
|
|
760
|
+
`\u` escape that reads as one — and the index after it; null when none
|
|
761
|
+
is there. */
|
|
762
|
+
function identCharAt(text, at, start) {
|
|
763
|
+
const c = text.charCodeAt(at);
|
|
764
|
+
if (Number.isNaN(c))
|
|
765
|
+
return null;
|
|
766
|
+
const test = start ? ID_START : ID_PART;
|
|
767
|
+
if (c === 92) {
|
|
768
|
+
if (text[at + 1] !== "u")
|
|
769
|
+
return null;
|
|
770
|
+
const [read, next] = unescapeAt(text, at);
|
|
771
|
+
// `\u` without its hex digits is no escape
|
|
772
|
+
return next > at + 2 && test.test(read) ? [read, next] : null;
|
|
773
|
+
}
|
|
774
|
+
// ASCII by its code: letters, `$`, `_`, and digits after the first
|
|
775
|
+
if (c < 128)
|
|
776
|
+
return (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c === 36 || c === 95 || (!start && c >= 48 && c <= 57) ? [text[at], at + 1] : null;
|
|
777
|
+
const ch = String.fromCodePoint(text.codePointAt(at));
|
|
778
|
+
return test.test(ch) ? [ch, at + ch.length] : null;
|
|
779
|
+
}
|
|
780
|
+
// `\n`, `\t` … by the letter, as character codes
|
|
781
|
+
const SIMPLE_ESCAPES = { n: 10, r: 13, t: 9, b: 8, f: 12, v: 11 };
|
|
782
|
+
/** The escape at `at` (a backslash) as JavaScript reads it in a string —
|
|
783
|
+
`\x6c`, `l`, `\u{6c}`, `\154`, `\n`, a line continuation, any other
|
|
784
|
+
character as itself — and the index after it. */
|
|
785
|
+
function unescapeAt(text, at) {
|
|
786
|
+
const e = text[at + 1];
|
|
787
|
+
if (e === undefined)
|
|
788
|
+
return ["", at + 1];
|
|
789
|
+
const simple = SIMPLE_ESCAPES[e];
|
|
790
|
+
if (simple !== undefined)
|
|
791
|
+
return [String.fromCharCode(simple), at + 2];
|
|
792
|
+
const code = e.charCodeAt(0);
|
|
793
|
+
// a line continuation: the backslash and the line break read as nothing
|
|
794
|
+
if (e === "\r")
|
|
795
|
+
return ["", text[at + 2] === "\n" ? at + 3 : at + 2];
|
|
796
|
+
if (e === "\n" || code === 0x2028 || code === 0x2029)
|
|
797
|
+
return ["", at + 2];
|
|
798
|
+
if (e === "x" && /^[\da-f]{2}$/i.test(text.slice(at + 2, at + 4)))
|
|
799
|
+
return [String.fromCharCode(parseInt(text.slice(at + 2, at + 4), 16)), at + 4];
|
|
800
|
+
if (e === "u") {
|
|
801
|
+
if (text[at + 2] === "{") {
|
|
802
|
+
const close = text.indexOf("}", at + 3);
|
|
803
|
+
const hex = close === -1 ? "" : text.slice(at + 3, close);
|
|
804
|
+
if (/^[\da-f]{1,6}$/i.test(hex) && parseInt(hex, 16) <= 0x10ffff)
|
|
805
|
+
return [String.fromCodePoint(parseInt(hex, 16)), close + 1];
|
|
806
|
+
}
|
|
807
|
+
else if (/^[\da-f]{4}$/i.test(text.slice(at + 2, at + 6)))
|
|
808
|
+
return [String.fromCharCode(parseInt(text.slice(at + 2, at + 6), 16)), at + 6];
|
|
809
|
+
}
|
|
810
|
+
// a legacy octal escape: up to three digits, the value under 256
|
|
811
|
+
const octal = /^[0-3][0-7]{0,2}|^[4-7][0-7]?/.exec(text.slice(at + 1, at + 4));
|
|
812
|
+
if (octal)
|
|
813
|
+
return [String.fromCharCode(parseInt(octal[0], 8)), at + 1 + octal[0].length];
|
|
814
|
+
return [e, at + 2];
|
|
815
|
+
}
|
|
816
|
+
/** The index of the `}` that closes the template substitution whose code
|
|
817
|
+
starts at `from`, with strings, templates and comments in it read as
|
|
818
|
+
JavaScript reads them — `${"}"}` closes at the second brace; the end of
|
|
819
|
+
the text when nothing closes it. A `/` in it that starts no comment may
|
|
820
|
+
start a regex, which can hold a brace or a quote: `doubt` hears of it. */
|
|
821
|
+
function substitutionEnd(text, from, doubt) {
|
|
822
|
+
// what the scan is in: code with that many open braces, or a template's text (0), innermost last
|
|
823
|
+
const stack = [1];
|
|
824
|
+
for (let j = from; j < text.length; j++) {
|
|
825
|
+
const c = text[j];
|
|
826
|
+
const top = stack[stack.length - 1];
|
|
827
|
+
if (top === 0) {
|
|
828
|
+
if (c === "\\")
|
|
829
|
+
j++;
|
|
830
|
+
else if (c === "`")
|
|
831
|
+
stack.pop();
|
|
832
|
+
else if (c === "$" && text[j + 1] === "{") {
|
|
833
|
+
stack.push(1);
|
|
834
|
+
j++;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
else if (c === "{")
|
|
838
|
+
stack[stack.length - 1] = top + 1;
|
|
839
|
+
else if (c === "}") {
|
|
840
|
+
if (top > 1)
|
|
841
|
+
stack[stack.length - 1] = top - 1;
|
|
842
|
+
else {
|
|
843
|
+
stack.pop();
|
|
844
|
+
if (stack.length === 0)
|
|
845
|
+
return j;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
else if (c === '"' || c === "'") {
|
|
849
|
+
for (j++; j < text.length && text[j] !== c && text[j] !== "\n" && text[j] !== "\r"; j++)
|
|
850
|
+
if (text[j] === "\\")
|
|
851
|
+
j++;
|
|
852
|
+
}
|
|
853
|
+
else if (c === "`")
|
|
854
|
+
stack.push(0);
|
|
855
|
+
else if (c === "/" && text[j + 1] === "/") {
|
|
856
|
+
while (j + 1 < text.length && text[j + 1] !== "\n" && text[j + 1] !== "\r")
|
|
857
|
+
j++;
|
|
858
|
+
}
|
|
859
|
+
else if (c === "/" && text[j + 1] === "*") {
|
|
860
|
+
const end = text.indexOf("*/", j + 2);
|
|
861
|
+
j = end === -1 ? text.length : end + 1;
|
|
862
|
+
}
|
|
863
|
+
else if (c === "/")
|
|
864
|
+
doubt?.();
|
|
865
|
+
}
|
|
866
|
+
return text.length;
|
|
867
|
+
}
|
|
756
868
|
/** JavaScript tokens: identifiers, string literals and punctuation, with
|
|
757
869
|
comments and template bodies dropped. A regex literal after an
|
|
758
|
-
operator is skipped
|
|
759
|
-
|
|
760
|
-
|
|
870
|
+
operator is skipped — only one that closes on its line, and never the
|
|
871
|
+
`/>` after a JSX attribute's `}`; JSX text is read as code, so an apostrophe in
|
|
872
|
+
prose can swallow a stretch — a false negative, never a crash.
|
|
873
|
+
`hidden`, when given, gets the code of each template's `${…}`, which
|
|
874
|
+
the tokens leave out; `ambiguous`, where a `/` right after `}` was read
|
|
875
|
+
as a regex — after an object, JavaScript reads a division there, and
|
|
876
|
+
what follows is code (`{} / f(x) / 1`). With `regexes`, a regex is a
|
|
877
|
+
token of its own rather than skipped. */
|
|
878
|
+
export function tokenize(text, hidden, ambiguous, regexes = false) {
|
|
761
879
|
const out = [];
|
|
762
880
|
let i = 0;
|
|
763
881
|
const n = text.length;
|
|
764
882
|
const REGEX_BEFORE = new Set(["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";", "return", "typeof", "=>"]);
|
|
883
|
+
let nl = false;
|
|
884
|
+
const push = (token) => {
|
|
885
|
+
if (nl)
|
|
886
|
+
token.nl = true;
|
|
887
|
+
nl = false;
|
|
888
|
+
out.push(token);
|
|
889
|
+
};
|
|
765
890
|
while (i < n) {
|
|
766
891
|
const c = text[i];
|
|
767
892
|
if (c === " " || c === "\t" || c === "\n" || c === "\r") {
|
|
893
|
+
if (c === "\n" || c === "\r")
|
|
894
|
+
nl = true;
|
|
768
895
|
i++;
|
|
769
896
|
continue;
|
|
770
897
|
}
|
|
@@ -775,55 +902,78 @@ export function tokenize(text) {
|
|
|
775
902
|
}
|
|
776
903
|
if (c === "/" && text[i + 1] === "*") {
|
|
777
904
|
const end = text.indexOf("*/", i + 2);
|
|
905
|
+
// a comment across lines is a line break
|
|
906
|
+
if (/[\n\r]/.test(text.slice(i + 2, end === -1 ? n : end)))
|
|
907
|
+
nl = true;
|
|
778
908
|
i = end === -1 ? n : end + 2;
|
|
779
909
|
continue;
|
|
780
910
|
}
|
|
781
911
|
if (c === '"' || c === "'" || c === "`") {
|
|
782
912
|
let j = i + 1;
|
|
783
913
|
let value = "";
|
|
914
|
+
let escaped = false;
|
|
784
915
|
while (j < n && text[j] !== c) {
|
|
785
916
|
if (text[j] === "\\") {
|
|
786
|
-
|
|
787
|
-
|
|
917
|
+
const [read, next] = unescapeAt(text, j);
|
|
918
|
+
value += read;
|
|
919
|
+
j = next;
|
|
920
|
+
escaped = true;
|
|
788
921
|
continue;
|
|
789
922
|
}
|
|
790
923
|
if (c === "`" && text[j] === "$" && text[j + 1] === "{") {
|
|
791
|
-
// a substitution:
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
depth++;
|
|
797
|
-
else if (text[j] === "}")
|
|
798
|
-
depth--;
|
|
799
|
-
j++;
|
|
800
|
-
}
|
|
924
|
+
// a substitution: its code, to the brace that closes it as JavaScript reads it
|
|
925
|
+
const from = j + 2;
|
|
926
|
+
const end = substitutionEnd(text, from, ambiguous ? () => ambiguous.push(from) : undefined);
|
|
927
|
+
hidden?.push(text.slice(from, end));
|
|
928
|
+
j = end + 1;
|
|
801
929
|
value += "${}";
|
|
802
930
|
continue;
|
|
803
931
|
}
|
|
804
|
-
if (c !== "`" && text[j] === "\n")
|
|
932
|
+
if (c !== "`" && (text[j] === "\n" || text[j] === "\r"))
|
|
805
933
|
break;
|
|
806
934
|
value += text[j];
|
|
807
935
|
j++;
|
|
808
936
|
}
|
|
809
|
-
|
|
937
|
+
const token = { kind: "string", value: c === "`" && value.includes("${}") ? "${}" : value };
|
|
938
|
+
if (escaped)
|
|
939
|
+
token.escaped = true;
|
|
940
|
+
push(token);
|
|
941
|
+
// cut off by a line break: the break stays the next token's
|
|
942
|
+
if (text[j] !== c) {
|
|
943
|
+
token.unclosed = true;
|
|
944
|
+
i = j;
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
810
947
|
i = j + 1;
|
|
811
948
|
continue;
|
|
812
949
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
950
|
+
// an identifier, escapes decoded: `end\u0070oint` is `endpoint`
|
|
951
|
+
const first = identCharAt(text, i, true);
|
|
952
|
+
if (first) {
|
|
953
|
+
let value = first[0];
|
|
954
|
+
let escaped = c === "\\";
|
|
955
|
+
let j = first[1];
|
|
956
|
+
for (let part = identCharAt(text, j, false); part; part = identCharAt(text, j, false)) {
|
|
957
|
+
if (text[j] === "\\")
|
|
958
|
+
escaped = true;
|
|
959
|
+
value += part[0];
|
|
960
|
+
j = part[1];
|
|
961
|
+
}
|
|
962
|
+
const token = { kind: "ident", value };
|
|
963
|
+
if (escaped)
|
|
964
|
+
token.escaped = true;
|
|
965
|
+
push(token);
|
|
818
966
|
i = j;
|
|
819
967
|
continue;
|
|
820
968
|
}
|
|
821
969
|
if (c === "/") {
|
|
822
970
|
const prev = out[out.length - 1];
|
|
823
|
-
|
|
971
|
+
// `/>` after `}` closes a JSX element whose last attribute is an expression
|
|
972
|
+
const selfClosing = prev?.kind === "punct" && prev.value === "}" && text[i + 1] === ">";
|
|
973
|
+
if (!selfClosing && (!prev || (prev.kind === "punct" && REGEX_BEFORE.has(prev.value)) || (prev.kind === "ident" && REGEX_BEFORE.has(prev.value)))) {
|
|
824
974
|
let j = i + 1;
|
|
825
975
|
let inClass = false;
|
|
826
|
-
while (j < n && (inClass || text[j] !== "/") && text[j] !== "\n") {
|
|
976
|
+
while (j < n && (inClass || text[j] !== "/") && text[j] !== "\n" && text[j] !== "\r") {
|
|
827
977
|
if (text[j] === "\\")
|
|
828
978
|
j++;
|
|
829
979
|
else if (text[j] === "[")
|
|
@@ -832,19 +982,26 @@ export function tokenize(text) {
|
|
|
832
982
|
inClass = false;
|
|
833
983
|
j++;
|
|
834
984
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
985
|
+
// a regex closes on its own line; a `/` that doesn't is the code's
|
|
986
|
+
if (text[j] === "/") {
|
|
987
|
+
if (prev?.kind === "punct" && prev.value === "}")
|
|
988
|
+
ambiguous?.push(i);
|
|
989
|
+
j++;
|
|
990
|
+
while (j < n && /[a-z]/.test(text[j]))
|
|
991
|
+
j++; // flags
|
|
992
|
+
if (regexes)
|
|
993
|
+
push({ kind: "regex", value: text.slice(i, j) });
|
|
994
|
+
i = j;
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
840
997
|
}
|
|
841
998
|
}
|
|
842
999
|
if (c === "=" && text[i + 1] === ">") {
|
|
843
|
-
|
|
1000
|
+
push({ kind: "punct", value: "=>" });
|
|
844
1001
|
i += 2;
|
|
845
1002
|
continue;
|
|
846
1003
|
}
|
|
847
|
-
|
|
1004
|
+
push({ kind: "punct", value: c });
|
|
848
1005
|
i++;
|
|
849
1006
|
}
|
|
850
1007
|
return out;
|
|
@@ -1293,6 +1450,8 @@ export function mergeManifest(existing, scan, opts) {
|
|
|
1293
1450
|
pages: scan.pages,
|
|
1294
1451
|
bindings: out,
|
|
1295
1452
|
...(existing?.notes && existing.notes.length > 0 ? { notes: existing.notes } : {}),
|
|
1453
|
+
...(scan.capabilities ? { capabilities: scan.capabilities } : existing?.capabilities ? { capabilities: existing.capabilities } : {}),
|
|
1296
1454
|
};
|
|
1297
|
-
|
|
1455
|
+
const capabilitiesChanged = existing !== null && !sameCapabilities(existing.capabilities, manifest.capabilities);
|
|
1456
|
+
return { manifest, added, changed, unchanged, kept, dropped, capabilitiesChanged };
|
|
1298
1457
|
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -41,6 +41,9 @@ export type SchemaField = {
|
|
|
41
41
|
accepts it under these too until they are removed. Not part of the
|
|
42
42
|
schema hash — a rename changes the key, which is. */
|
|
43
43
|
aliases?: string[];
|
|
44
|
+
/** Translated (§28.2): the field holds a value per language; the site
|
|
45
|
+
reads the default language's. Absent on older servers. */
|
|
46
|
+
localized?: boolean;
|
|
44
47
|
};
|
|
45
48
|
export type SchemaCollection = {
|
|
46
49
|
key: string;
|
package/dist/schema.js
CHANGED
|
@@ -31,6 +31,8 @@ export function schemaHash(schema) {
|
|
|
31
31
|
: null,
|
|
32
32
|
// present only for computed fields, so schemas without formulas hash as before
|
|
33
33
|
...(f.type === "computed" && f.computed ? { computed: { expression: f.computed.expression, resultType: f.computed.resultType } } : {}),
|
|
34
|
+
// present only for translated fields, so schemas without translations hash as before
|
|
35
|
+
...(f.localized === true ? { localized: true } : {}),
|
|
34
36
|
})),
|
|
35
37
|
}));
|
|
36
38
|
return createHash("sha256").update(JSON.stringify(projection)).digest("hex").slice(0, 12);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mapled/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Mapled CLI — sign in, link a project, generate TypeScript types for its content, create forms, preview drafts and check the site's integration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://mapled.io",
|