@mapled/cli 0.11.0 → 0.12.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.
@@ -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 { same: onlyLocal.length === 0 && onlyRemote.length === 0 && changed.length === 0, onlyLocal, onlyRemote, changed };
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,22 +37,40 @@ 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
64
  kind: "ident" | "string" | "punct";
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; JSX text is read as code, so an apostrophe in
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
56
74
  prose can swallow a stretch — a false negative, never a crash. */
57
75
  export declare function tokenize(text: string): Token[];
58
76
  export declare function routeKey(route: string): string;
@@ -65,6 +83,8 @@ export type MergeResult = {
65
83
  /** Bindings the scan didn't produce: kept (or dropped with prune). */
66
84
  kept: string[];
67
85
  dropped: string[];
86
+ /** the scan changed what an existing manifest says the site can do */
87
+ capabilitiesChanged: boolean;
68
88
  };
69
89
  /** The scan's bindings replace the ones with the same key; bindings the
70
90
  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,88 @@ 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
+ }
756
816
  /** JavaScript tokens: identifiers, string literals and punctuation, with
757
817
  comments and template bodies dropped. A regex literal after an
758
- operator is skipped; JSX text is read as code, so an apostrophe in
818
+ operator is skipped — only one that closes on its line, and never the
819
+ `/>` after a JSX attribute's `}`; JSX text is read as code, so an apostrophe in
759
820
  prose can swallow a stretch — a false negative, never a crash. */
760
821
  export function tokenize(text) {
761
822
  const out = [];
762
823
  let i = 0;
763
824
  const n = text.length;
764
825
  const REGEX_BEFORE = new Set(["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";", "return", "typeof", "=>"]);
826
+ let nl = false;
827
+ const push = (token) => {
828
+ if (nl)
829
+ token.nl = true;
830
+ nl = false;
831
+ out.push(token);
832
+ };
765
833
  while (i < n) {
766
834
  const c = text[i];
767
835
  if (c === " " || c === "\t" || c === "\n" || c === "\r") {
836
+ if (c === "\n" || c === "\r")
837
+ nl = true;
768
838
  i++;
769
839
  continue;
770
840
  }
@@ -775,16 +845,22 @@ export function tokenize(text) {
775
845
  }
776
846
  if (c === "/" && text[i + 1] === "*") {
777
847
  const end = text.indexOf("*/", i + 2);
848
+ // a comment across lines is a line break
849
+ if (/[\n\r]/.test(text.slice(i + 2, end === -1 ? n : end)))
850
+ nl = true;
778
851
  i = end === -1 ? n : end + 2;
779
852
  continue;
780
853
  }
781
854
  if (c === '"' || c === "'" || c === "`") {
782
855
  let j = i + 1;
783
856
  let value = "";
857
+ let escaped = false;
784
858
  while (j < n && text[j] !== c) {
785
859
  if (text[j] === "\\") {
786
- value += text[j + 1] ?? "";
787
- j += 2;
860
+ const [read, next] = unescapeAt(text, j);
861
+ value += read;
862
+ j = next;
863
+ escaped = true;
788
864
  continue;
789
865
  }
790
866
  if (c === "`" && text[j] === "$" && text[j + 1] === "{") {
@@ -801,29 +877,51 @@ export function tokenize(text) {
801
877
  value += "${}";
802
878
  continue;
803
879
  }
804
- if (c !== "`" && text[j] === "\n")
880
+ if (c !== "`" && (text[j] === "\n" || text[j] === "\r"))
805
881
  break;
806
882
  value += text[j];
807
883
  j++;
808
884
  }
809
- out.push({ kind: "string", value: c === "`" && value.includes("${}") ? "${}" : value });
885
+ const token = { kind: "string", value: c === "`" && value.includes("${}") ? "${}" : value };
886
+ if (escaped)
887
+ token.escaped = true;
888
+ push(token);
889
+ // cut off by a line break: the break stays the next token's
890
+ if (text[j] !== c) {
891
+ token.unclosed = true;
892
+ i = j;
893
+ continue;
894
+ }
810
895
  i = j + 1;
811
896
  continue;
812
897
  }
813
- if (/[A-Za-z_$]/.test(c)) {
814
- let j = i + 1;
815
- while (j < n && /[A-Za-z0-9_$]/.test(text[j]))
816
- j++;
817
- out.push({ kind: "ident", value: text.slice(i, j) });
898
+ // an identifier, escapes decoded: `end\u0070oint` is `endpoint`
899
+ const first = identCharAt(text, i, true);
900
+ if (first) {
901
+ let value = first[0];
902
+ let escaped = c === "\\";
903
+ let j = first[1];
904
+ for (let part = identCharAt(text, j, false); part; part = identCharAt(text, j, false)) {
905
+ if (text[j] === "\\")
906
+ escaped = true;
907
+ value += part[0];
908
+ j = part[1];
909
+ }
910
+ const token = { kind: "ident", value };
911
+ if (escaped)
912
+ token.escaped = true;
913
+ push(token);
818
914
  i = j;
819
915
  continue;
820
916
  }
821
917
  if (c === "/") {
822
918
  const prev = out[out.length - 1];
823
- if (!prev || (prev.kind === "punct" && REGEX_BEFORE.has(prev.value)) || (prev.kind === "ident" && REGEX_BEFORE.has(prev.value))) {
919
+ // `/>` after `}` closes a JSX element whose last attribute is an expression
920
+ const selfClosing = prev?.kind === "punct" && prev.value === "}" && text[i + 1] === ">";
921
+ if (!selfClosing && (!prev || (prev.kind === "punct" && REGEX_BEFORE.has(prev.value)) || (prev.kind === "ident" && REGEX_BEFORE.has(prev.value)))) {
824
922
  let j = i + 1;
825
923
  let inClass = false;
826
- while (j < n && (inClass || text[j] !== "/") && text[j] !== "\n") {
924
+ while (j < n && (inClass || text[j] !== "/") && text[j] !== "\n" && text[j] !== "\r") {
827
925
  if (text[j] === "\\")
828
926
  j++;
829
927
  else if (text[j] === "[")
@@ -832,19 +930,22 @@ export function tokenize(text) {
832
930
  inClass = false;
833
931
  j++;
834
932
  }
835
- j++;
836
- while (j < n && /[a-z]/.test(text[j]))
837
- j++; // flags
838
- i = j;
839
- continue;
933
+ // a regex closes on its own line; a `/` that doesn't is the code's
934
+ if (text[j] === "/") {
935
+ j++;
936
+ while (j < n && /[a-z]/.test(text[j]))
937
+ j++; // flags
938
+ i = j;
939
+ continue;
940
+ }
840
941
  }
841
942
  }
842
943
  if (c === "=" && text[i + 1] === ">") {
843
- out.push({ kind: "punct", value: "=>" });
944
+ push({ kind: "punct", value: "=>" });
844
945
  i += 2;
845
946
  continue;
846
947
  }
847
- out.push({ kind: "punct", value: c });
948
+ push({ kind: "punct", value: c });
848
949
  i++;
849
950
  }
850
951
  return out;
@@ -1293,6 +1394,8 @@ export function mergeManifest(existing, scan, opts) {
1293
1394
  pages: scan.pages,
1294
1395
  bindings: out,
1295
1396
  ...(existing?.notes && existing.notes.length > 0 ? { notes: existing.notes } : {}),
1397
+ ...(scan.capabilities ? { capabilities: scan.capabilities } : existing?.capabilities ? { capabilities: existing.capabilities } : {}),
1296
1398
  };
1297
- return { manifest, added, changed, unchanged, kept, dropped };
1399
+ const capabilitiesChanged = existing !== null && !sameCapabilities(existing.capabilities, manifest.capabilities);
1400
+ return { manifest, added, changed, unchanged, kept, dropped, capabilitiesChanged };
1298
1401
  }
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.11.0",
3
+ "version": "0.12.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",