create-ailk 0.31.0 → 0.32.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.
Files changed (45) hide show
  1. package/README.md +41 -5
  2. package/dist/body-heading.d.ts +13 -0
  3. package/dist/body-heading.js +19 -5
  4. package/dist/cutover.d.ts +6 -1
  5. package/dist/cutover.js +23 -6
  6. package/dist/language-import.d.ts +16 -3
  7. package/dist/language-import.js +27 -11
  8. package/dist/lib/import-mdx.js +57 -24
  9. package/dist/lib/render-comparison.d.ts +51 -0
  10. package/dist/lib/render-comparison.js +205 -6
  11. package/dist/mdx-braces.d.ts +73 -0
  12. package/dist/mdx-braces.js +285 -0
  13. package/dist/migrate-content.d.ts +1 -1
  14. package/dist/migrate-content.js +4 -2
  15. package/dist/programmatic.js +2 -8
  16. package/dist/site-manifest.js +6 -38
  17. package/dist/site-roster.d.ts +46 -3
  18. package/dist/site-roster.js +124 -4
  19. package/dist/surfaces.d.ts +8 -0
  20. package/dist/surfaces.js +16 -0
  21. package/dist/update.d.ts +21 -1
  22. package/dist/update.js +142 -11
  23. package/package.json +3 -3
  24. package/templates/.github/workflows/ci.yml +2 -0
  25. package/templates/apps/mcp/.env.example +7 -1
  26. package/templates/apps/web/__tests__/setup.ts +9 -0
  27. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +3 -0
  28. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
  29. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +3 -0
  30. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -0
  31. package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +3 -0
  32. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
  33. package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +3 -0
  34. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +3 -0
  35. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +3 -0
  36. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -0
  37. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +3 -0
  38. package/templates/apps/web/lib/__tests__/page-chrome.test.ts +3 -0
  39. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +134 -10
  40. package/templates/apps/web/src/lib/routes.ts +5 -3
  41. package/templates/package.json +2 -1
  42. package/templates/tools/body-heading.ts +440 -0
  43. package/templates/tools/check-mdx-body.ts +123 -0
  44. package/templates/tools/mdx-braces.ts +303 -0
  45. package/versions.json +47 -47
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Bare `{…}` braces on mdx-body pages (#10773, spec
3
+ * docs/specs/10773-update-never-refreshes-0-19-spec.md D3, D4).
4
+ *
5
+ * MDX reads every `{` in body text as the start of a JavaScript expression.
6
+ * Under the renderer's `next-mdx-remote` defaults (`blockJS`), an expression
7
+ * that does not parse fails the page, and one that parses is deleted — so on
8
+ * an mdx-body page every `{` in text other than a `{/* … *\/}` comment is a
9
+ * defect. This module is the one lexical rule for that brace:
10
+ *
11
+ * - `findBareBraces` names every bare brace in a body, by offset, line and
12
+ * column, in one left-to-right pass;
13
+ * - `escapeBareBraces` inserts one `\` before each, and keeps the result only
14
+ * when the site's own MDX parser reads it with no expression left but
15
+ * comments — the lexer proposes, the parser disposes.
16
+ *
17
+ * Used by `create-ailk update` and, through a byte-identical copy in the
18
+ * scaffold's `tools/`, by `pnpm mdx-body:check`. Pure: no filesystem and no
19
+ * imports. It must never contain a double open brace, which the scaffold copy
20
+ * substitutes as a marker.
21
+ */
22
+
23
+ export interface BareBrace {
24
+ /** Offset of the `{` in the body. */
25
+ offset: number;
26
+ /** 1-based line within the body. */
27
+ line: number;
28
+ /** 1-based column within the line. */
29
+ column: number;
30
+ /** `comment-like` when the next non-whitespace character is `/`. */
31
+ kind: "placeholder" | "comment-like";
32
+ }
33
+
34
+ /** A minimal mdast node: what `escapeBareBraces` reads from the parser's tree. */
35
+ export interface MdastNode {
36
+ type: string;
37
+ value?: unknown;
38
+ position?: { start?: { line?: number; offset?: number }; end?: { offset?: number } };
39
+ children?: MdastNode[];
40
+ }
41
+
42
+ /** The site's MDX parser: throws the parser's message (carrying `line` and `reason`) on failure. */
43
+ export type MdxParse = (body: string) => MdastNode;
44
+
45
+ export type EscapeResult =
46
+ | { kind: "clean" }
47
+ | { kind: "escaped"; text: string; count: number }
48
+ | { kind: "refused"; reason: string };
49
+
50
+ const ESM_RE = /^(?:import|export)[ {*]/;
51
+ // MDX has no indented code, so a fence opens at any indentation — and inside
52
+ // a list item or a blockquote, after its markers.
53
+ const FENCE_RE = /^[ \t]*(?:(?:>|[-*+]|\d{1,9}[.)])[ \t]*)*(`{3,}|~{3,})/;
54
+ const COMMENT_CLOSE_RE = /\*\/\s*\}/g;
55
+ const WHITESPACE_RE = /\s/;
56
+ const TAG_START_RE = /[A-Za-z/>]/;
57
+
58
+ const isBlank = (line: string): boolean => line.trim() === "";
59
+
60
+ /**
61
+ * Every bare `{` in an MDX body. Skips, in this order of precedence: fenced
62
+ * code, ESM blocks, backslash escapes, inline code, JSX tags (attribute
63
+ * expressions included) and `{/* … *\/}` comments. Linear in the body.
64
+ */
65
+ export function findBareBraces(body: string): BareBrace[] {
66
+ const found: Array<Omit<BareBrace, "line" | "column">> = [];
67
+ const lineEnd = (from: number): number => {
68
+ const end = body.indexOf("\n", from);
69
+ return end === -1 ? body.length : end;
70
+ };
71
+ // A code span or a tag never crosses a blank line: true at a `\n` that ends
72
+ // the body or starts a blank line.
73
+ const paragraphBreak = (at: number): boolean => {
74
+ let j = at + 1;
75
+ while (body[j] === " " || body[j] === "\t" || body[j] === "\r") j += 1;
76
+ return j >= body.length || body[j] === "\n";
77
+ };
78
+ // Memo so a failed search is never repeated over the same text.
79
+ const spanFailedUntil = new Map<number, number>();
80
+ let tagFailedUntil = -1;
81
+ let commentClose: { from: number; at: number } | undefined;
82
+
83
+ let i = 0;
84
+ while (i < body.length) {
85
+ if (i === 0 || body[i - 1] === "\n") {
86
+ const end = lineEnd(i);
87
+ const line = body.slice(i, end);
88
+ const fence = FENCE_RE.exec(line)?.[1];
89
+ if (fence !== undefined) {
90
+ // Skip to after the closing fence line, or to the end of the body.
91
+ let at = end;
92
+ while (at < body.length) {
93
+ const next = lineEnd(at + 1);
94
+ const candidate = body.slice(at + 1, next);
95
+ const run = FENCE_RE.exec(candidate);
96
+ at = next;
97
+ if (
98
+ run !== null &&
99
+ run[1]![0] === fence[0] &&
100
+ run[1]!.length >= fence.length &&
101
+ candidate.slice(run[0].length).trim() === ""
102
+ ) {
103
+ break;
104
+ }
105
+ }
106
+ i = at + 1;
107
+ continue;
108
+ }
109
+ if (ESM_RE.test(line)) {
110
+ let at = end;
111
+ while (at < body.length) {
112
+ const next = lineEnd(at + 1);
113
+ if (isBlank(body.slice(at + 1, next))) break;
114
+ at = next;
115
+ }
116
+ i = at + 1;
117
+ continue;
118
+ }
119
+ }
120
+
121
+ const char = body[i]!;
122
+ if (char === "\\") {
123
+ // A backslash before a line break is a hard break; the next line is read as a line.
124
+ i += body[i + 1] === "\n" ? 1 : 2;
125
+ continue;
126
+ }
127
+ if (char === "`") {
128
+ let run = 1;
129
+ while (body[i + run] === "`") run += 1;
130
+ const close = spanFailedUntil.get(run) ?? -1;
131
+ let closed = -1;
132
+ if (close < i) {
133
+ let at = i + run;
134
+ for (; at < body.length; at += 1) {
135
+ if (body[at] === "\n" && (paragraphBreak(at) || FENCE_RE.test(body.slice(at + 1, lineEnd(at + 1))))) break;
136
+ if (body[at] !== "`") continue;
137
+ let length = 1;
138
+ while (body[at + length] === "`") length += 1;
139
+ if (length === run) {
140
+ closed = at + length;
141
+ break;
142
+ }
143
+ at += length - 1;
144
+ }
145
+ if (closed === -1) spanFailedUntil.set(run, at);
146
+ }
147
+ i = closed === -1 ? i + run : closed;
148
+ continue;
149
+ }
150
+ if (char === "<" && TAG_START_RE.test(body[i + 1] ?? "") && i >= tagFailedUntil) {
151
+ let depth = 0;
152
+ let quote = "";
153
+ let closed = -1;
154
+ let at = i + 1;
155
+ for (; at < body.length; at += 1) {
156
+ const c = body[at]!;
157
+ if (c === "\n" && paragraphBreak(at)) break;
158
+ if (quote !== "") {
159
+ if (c === quote) quote = "";
160
+ } else if (depth > 0) {
161
+ if (c === "{") depth += 1;
162
+ else if (c === "}") depth -= 1;
163
+ } else if (c === '"' || c === "'") {
164
+ quote = c;
165
+ } else if (c === "{") {
166
+ depth = 1;
167
+ } else if (c === ">") {
168
+ closed = at + 1;
169
+ break;
170
+ }
171
+ }
172
+ if (closed !== -1) {
173
+ i = closed;
174
+ continue;
175
+ }
176
+ // An unclosed tag already breaks the page; no later `<` before `at` is
177
+ // read as a tag, which keeps the scan linear.
178
+ tagFailedUntil = at;
179
+ i += 1;
180
+ continue;
181
+ }
182
+ if (char === "{") {
183
+ let next = i + 1;
184
+ while (next < body.length && WHITESPACE_RE.test(body[next]!)) next += 1;
185
+ if (body[next] === "/" && body[next + 1] === "*") {
186
+ if (commentClose === undefined || commentClose.from > i || (commentClose.at !== -1 && commentClose.at < i)) {
187
+ COMMENT_CLOSE_RE.lastIndex = next + 2;
188
+ const match = COMMENT_CLOSE_RE.exec(body);
189
+ commentClose = { from: i, at: match === null ? -1 : match.index + match[0].length };
190
+ }
191
+ if (commentClose.at !== -1) {
192
+ i = commentClose.at;
193
+ continue;
194
+ }
195
+ }
196
+ found.push({ offset: i, kind: body[next] === "/" ? "comment-like" : "placeholder" });
197
+ }
198
+ i += 1;
199
+ }
200
+
201
+ // Offsets to 1-based line and column, in one pass.
202
+ const braces: BareBrace[] = [];
203
+ let line = 1;
204
+ let lineStart = 0;
205
+ let scanned = 0;
206
+ for (const brace of found) {
207
+ for (; scanned < brace.offset; scanned += 1) {
208
+ if (body[scanned] === "\n") {
209
+ line += 1;
210
+ lineStart = scanned + 1;
211
+ }
212
+ }
213
+ braces.push({ ...brace, line, column: brace.offset - lineStart + 1 });
214
+ }
215
+ return braces;
216
+ }
217
+
218
+ const COMMENT_VALUE_RE = /^\s*\/\*[\s\S]*\*\/\s*$/;
219
+
220
+ /** The first expression node left in the tree that is not a comment. */
221
+ export function firstExpression(node: MdastNode): MdastNode | undefined {
222
+ if (
223
+ (node.type === "mdxFlowExpression" || node.type === "mdxTextExpression") &&
224
+ !(typeof node.value === "string" && COMMENT_VALUE_RE.test(node.value))
225
+ ) {
226
+ return node;
227
+ }
228
+ for (const child of node.children ?? []) {
229
+ const hit = firstExpression(child);
230
+ if (hit !== undefined) return hit;
231
+ }
232
+ return undefined;
233
+ }
234
+
235
+ /** The first `code` or `inlineCode` node whose span holds one of `offsets` (sorted). */
236
+ function codeNodeHolding(node: MdastNode, offsets: number[]): MdastNode | undefined {
237
+ if (node.type === "code" || node.type === "inlineCode") {
238
+ const start = node.position?.start?.offset;
239
+ const end = node.position?.end?.offset;
240
+ if (typeof start === "number" && typeof end === "number") {
241
+ let lo = 0;
242
+ let hi = offsets.length;
243
+ while (lo < hi) {
244
+ const mid = (lo + hi) >> 1;
245
+ if (offsets[mid]! < start) lo = mid + 1;
246
+ else hi = mid;
247
+ }
248
+ if (lo < offsets.length && offsets[lo]! < end) return node;
249
+ }
250
+ }
251
+ for (const child of node.children ?? []) {
252
+ const hit = codeNodeHolding(child, offsets);
253
+ if (hit !== undefined) return hit;
254
+ }
255
+ return undefined;
256
+ }
257
+
258
+ /**
259
+ * Escape every bare brace in `body`, all or nothing: `clean` when there is
260
+ * none (`parse` is not called), `refused` when a brace is a mangled comment,
261
+ * the escaped text does not parse, or an expression the escape did not reach
262
+ * remains; otherwise `escaped`.
263
+ */
264
+ export function escapeBareBraces(body: string, parse: MdxParse): EscapeResult {
265
+ const braces = findBareBraces(body);
266
+ if (braces.length === 0) return { kind: "clean" };
267
+ const mangled = braces.find((brace) => brace.kind === "comment-like");
268
+ if (mangled !== undefined) {
269
+ return { kind: "refused", reason: `{/ at line ${mangled.line} is not a {/* … */} comment` };
270
+ }
271
+ const parts: string[] = [];
272
+ let from = 0;
273
+ for (const { offset } of braces) {
274
+ parts.push(body.slice(from, offset), "\\");
275
+ from = offset;
276
+ }
277
+ parts.push(body.slice(from));
278
+ const text = parts.join("");
279
+ let tree: MdastNode;
280
+ try {
281
+ tree = parse(text);
282
+ } catch (err) {
283
+ const { line, reason } = (err ?? {}) as { line?: unknown; reason?: unknown };
284
+ const why = typeof reason === "string" ? reason : err instanceof Error ? err.message : String(err);
285
+ return { kind: "refused", reason: `line ${typeof line === "number" ? line : "?"}: ${why}` };
286
+ }
287
+ const escapes = braces.map((brace, k) => brace.offset + k);
288
+ const inCode = codeNodeHolding(tree, escapes);
289
+ if (inCode !== undefined) {
290
+ return {
291
+ kind: "refused",
292
+ reason: `a brace at line ${inCode.position?.start?.line ?? "?"} is inside code the lexer did not see`,
293
+ };
294
+ }
295
+ const left = firstExpression(tree);
296
+ if (left !== undefined) {
297
+ return {
298
+ kind: "refused",
299
+ reason: `an expression at line ${left.position?.start?.line ?? "?"} the escape did not reach`,
300
+ };
301
+ }
302
+ return { kind: "escaped", text, count: braces.length };
303
+ }
package/versions.json CHANGED
@@ -1,66 +1,66 @@
1
1
  {
2
- "@working-theory/admin-ui": "0.54.0",
3
- "@working-theory/aeo": "0.54.0",
4
- "@working-theory/agent-context": "0.54.0",
2
+ "@working-theory/admin-ui": "0.55.0",
3
+ "@working-theory/aeo": "0.55.0",
4
+ "@working-theory/agent-context": "0.55.0",
5
5
  "@working-theory/agent-contract": "0.1.0",
6
6
  "@working-theory/agent-memory": "0.0.0",
7
7
  "@working-theory/ai": "0.1.0",
8
- "@working-theory/analytics": "0.54.0",
9
- "@working-theory/api": "0.54.0",
10
- "@working-theory/api-client": "0.54.0",
8
+ "@working-theory/analytics": "0.55.0",
9
+ "@working-theory/api": "0.55.0",
10
+ "@working-theory/api-client": "0.55.0",
11
11
  "@working-theory/cda-filesystem": "0.0.0",
12
- "@working-theory/cli": "0.54.0",
13
- "@working-theory/common": "0.54.0",
12
+ "@working-theory/cli": "0.55.0",
13
+ "@working-theory/common": "0.55.0",
14
14
  "@working-theory/consent": "0.1.0",
15
- "@working-theory/content-adapters": "0.54.0",
16
- "@working-theory/content-projection": "0.54.0",
17
- "@working-theory/email": "0.54.0",
15
+ "@working-theory/content-adapters": "0.55.0",
16
+ "@working-theory/content-projection": "0.55.0",
17
+ "@working-theory/email": "0.55.0",
18
18
  "@working-theory/env": "0.0.0",
19
- "@working-theory/eslint-config": "0.54.0",
20
- "@working-theory/fumadocs-schema": "0.54.0",
19
+ "@working-theory/eslint-config": "0.55.0",
20
+ "@working-theory/fumadocs-schema": "0.55.0",
21
21
  "@working-theory/ids": "0.19.0",
22
- "@working-theory/jsonld": "0.54.0",
23
- "@working-theory/license": "0.54.0",
24
- "@working-theory/mcp-server": "0.54.0",
25
- "@working-theory/metadata": "0.54.0",
26
- "@working-theory/observability": "0.54.0",
27
- "@working-theory/page-renderer": "0.54.0",
22
+ "@working-theory/jsonld": "0.55.0",
23
+ "@working-theory/license": "0.55.0",
24
+ "@working-theory/mcp-server": "0.55.0",
25
+ "@working-theory/metadata": "0.55.0",
26
+ "@working-theory/observability": "0.55.0",
27
+ "@working-theory/page-renderer": "0.55.0",
28
28
  "@working-theory/plugin-mcp": "0.1.3",
29
- "@working-theory/schema": "0.54.0",
30
- "@working-theory/schema-pack-registry": "0.54.0",
31
- "@working-theory/schema-validator": "0.54.0",
32
- "@working-theory/sections-registry": "0.54.0",
33
- "@working-theory/site-config": "0.54.0",
34
- "@working-theory/spoke-checkout": "0.7.10",
35
- "@working-theory/stripe-client": "0.54.0",
36
- "@working-theory/templates": "0.54.0",
37
- "@working-theory/theme": "0.54.0",
38
- "@working-theory/ui": "0.54.0",
39
- "@working-theory/validation": "0.54.0",
40
- "@working-theory/web-i18n": "0.54.0",
41
- "@working-theory/admin": "0.10.0",
42
- "@working-theory/api-commerce": "0.1.8",
43
- "@working-theory/api-content-write": "0.5.0",
44
- "@working-theory/api-hub": "0.4.0",
45
- "@working-theory/api-multi-tenant": "0.2.3",
46
- "@working-theory/api-scheduling": "0.4.5",
47
- "@working-theory/api-signal-collectors": "0.3.1",
48
- "@working-theory/api-workspace": "0.5.0",
29
+ "@working-theory/schema": "0.55.0",
30
+ "@working-theory/schema-pack-registry": "0.55.0",
31
+ "@working-theory/schema-validator": "0.55.0",
32
+ "@working-theory/sections-registry": "0.55.0",
33
+ "@working-theory/site-config": "0.55.0",
34
+ "@working-theory/spoke-checkout": "0.7.11",
35
+ "@working-theory/stripe-client": "0.55.0",
36
+ "@working-theory/templates": "0.55.0",
37
+ "@working-theory/theme": "0.55.0",
38
+ "@working-theory/ui": "0.55.0",
39
+ "@working-theory/validation": "0.55.0",
40
+ "@working-theory/web-i18n": "0.55.0",
41
+ "@working-theory/admin": "0.10.1",
42
+ "@working-theory/api-commerce": "0.1.9",
43
+ "@working-theory/api-content-write": "0.5.1",
44
+ "@working-theory/api-hub": "0.4.1",
45
+ "@working-theory/api-multi-tenant": "0.2.4",
46
+ "@working-theory/api-scheduling": "0.4.6",
47
+ "@working-theory/api-signal-collectors": "0.3.2",
48
+ "@working-theory/api-workspace": "0.5.1",
49
49
  "@working-theory/billing-client": "0.0.0",
50
50
  "@working-theory/branding": "0.0.0",
51
51
  "@working-theory/license-client": "0.0.0",
52
52
  "@working-theory/license-manifest": "0.0.0",
53
- "@working-theory/mcp-scheduling": "0.3.7",
54
- "@working-theory/mcp-signal-collectors": "0.2.8",
55
- "@working-theory/mcp-workspace": "0.6.0",
53
+ "@working-theory/mcp-scheduling": "0.3.8",
54
+ "@working-theory/mcp-signal-collectors": "0.2.9",
55
+ "@working-theory/mcp-workspace": "0.6.1",
56
56
  "@working-theory/partner-session": "0.0.0",
57
57
  "@working-theory/preview-protocol": "0.0.0",
58
- "@working-theory/ui-commerce": "0.23.32",
59
- "@working-theory/ui-docs-blog": "0.23.32",
60
- "@working-theory/ui-scheduling": "0.25.6",
61
- "@working-theory/ui-signal-collectors": "0.27.10",
58
+ "@working-theory/ui-commerce": "0.23.33",
59
+ "@working-theory/ui-docs-blog": "0.23.33",
60
+ "@working-theory/ui-scheduling": "0.25.7",
61
+ "@working-theory/ui-signal-collectors": "0.27.11",
62
62
  "@working-theory/web": "0.0.0",
63
63
  "@working-theory/app-api": "1.0.0",
64
64
  "@working-theory/mcp": "0.0.0",
65
- "@working-theory/database": "0.54.0"
65
+ "@working-theory/database": "0.55.0"
66
66
  }