create-ailk 0.30.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.
- package/README.md +108 -4
- package/component-catalog.json +524 -8
- package/component-catalog.md +8 -5
- package/dist/body-heading.d.ts +13 -0
- package/dist/body-heading.js +19 -5
- package/dist/cli.js +99 -0
- package/dist/cutover.d.ts +114 -0
- package/dist/cutover.js +511 -0
- package/dist/design-presets.js +20 -4
- package/dist/export-content.d.ts +55 -0
- package/dist/export-content.js +303 -0
- package/dist/inventory.d.ts +216 -0
- package/dist/inventory.js +833 -0
- package/dist/language-import.d.ts +127 -0
- package/dist/language-import.js +451 -0
- package/dist/lib/env-catalog.js +1 -1
- package/dist/lib/import-mdx.d.ts +38 -0
- package/dist/lib/import-mdx.js +445 -65
- package/dist/lib/render-comparison.d.ts +251 -0
- package/dist/lib/render-comparison.js +573 -0
- package/dist/mdx-braces.d.ts +73 -0
- package/dist/mdx-braces.js +285 -0
- package/dist/migrate-content.d.ts +6 -2
- package/dist/migrate-content.js +62 -14
- package/dist/programmatic.js +2 -8
- package/dist/seed-workspace.d.ts +3 -1
- package/dist/seed-workspace.js +45 -16
- package/dist/site-manifest.js +6 -38
- package/dist/site-roster.d.ts +46 -3
- package/dist/site-roster.js +124 -4
- package/dist/surfaces.d.ts +8 -0
- package/dist/surfaces.js +16 -0
- package/dist/topology.d.ts +12 -0
- package/dist/topology.js +24 -10
- package/dist/update.d.ts +21 -1
- package/dist/update.js +142 -11
- package/package.json +3 -3
- package/templates/.claude/cloudparity-repo-hooks.json +5 -0
- package/templates/.claude/hooks/content-source-guard.sh +75 -0
- package/templates/.claude/settings.json +9 -0
- package/templates/.github/workflows/ci.yml +4 -0
- package/templates/CLAUDE.md +5 -0
- package/templates/apps/mcp/.env.example +7 -1
- package/templates/apps/mcp/CLAUDE.md +8 -4
- package/templates/apps/web/.env.example +4 -2
- package/templates/apps/web/CLAUDE.md +16 -5
- package/templates/apps/web/__tests__/setup.ts +9 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +156 -4
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +45 -2
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +49 -0
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +6 -4
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/events/__tests__/program-event.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +11 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +44 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.thanks-by-tier.test.tsx +306 -0
- package/templates/apps/web/app/[locale]/layout.tsx +122 -27
- package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +21 -2
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/llms.txt/route.ts +16 -9
- package/templates/apps/web/app/sitemap.xml/route.ts +3 -0
- package/templates/apps/web/jest.config.cjs +46 -1
- package/templates/apps/web/lib/__tests__/bot-check.test.ts +247 -4
- package/templates/apps/web/lib/__tests__/in-page-magnet-self-request.test.ts +92 -0
- package/templates/apps/web/lib/__tests__/page-chrome.test.ts +93 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +33 -8
- package/templates/apps/web/lib/__tests__/site-identity-feature-list.test.ts +196 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +145 -11
- package/templates/apps/web/lib/bot-check.ts +113 -13
- package/templates/apps/web/lib/in-page-magnet.ts +4 -1
- package/templates/apps/web/lib/page-chrome.ts +65 -5
- package/templates/apps/web/lib/relay.ts +22 -3
- package/templates/apps/web/lib/site-identity.ts +75 -24
- package/templates/apps/web/messages/en.json +5 -1
- package/templates/apps/web/next.config.mjs +15 -1
- package/templates/apps/web/package.json +1 -1
- package/templates/apps/web/scripts/assert-content-bundled.ts +36 -8
- package/templates/apps/web/scripts/content-snapshot.ts +33 -0
- package/templates/apps/web/src/lib/routes.ts +5 -3
- package/templates/package.json +3 -1
- package/templates/tools/body-heading.ts +440 -0
- package/templates/tools/check-content-source.ts +108 -0
- package/templates/tools/check-mdx-body.ts +123 -0
- package/templates/tools/mdx-braces.ts +303 -0
- package/versions.json +48 -48
package/dist/site-manifest.js
CHANGED
|
@@ -38,13 +38,10 @@ import { join, relative, sep } from "node:path";
|
|
|
38
38
|
import { isDeepStrictEqual } from "node:util";
|
|
39
39
|
import { AddModuleRefusedError } from "./add-module-core.js";
|
|
40
40
|
import { MODULE_SLICES, normalizeModules } from "./module-architecture.js";
|
|
41
|
-
import { SITE_ROSTER_PATH, moduleRosterRows, parseSiteRoster, planRosterAppend, resolveRosterTemplate, } from "./site-roster.js";
|
|
41
|
+
import { SITE_ROSTER_PATH, moduleRosterRows, parseRosterFile, parseSiteRoster, planRosterAppend, resolveRosterTemplate, servedRoutes, withoutUnservedRouteRows, } from "./site-roster.js";
|
|
42
42
|
import { TEMPLATE_MANIFESTS, rosterDeclares, templateTreeDir, } from "./template-manifest.js";
|
|
43
43
|
/** Prettier's default print width — the generator formats the manifest with it. */
|
|
44
44
|
const PRINT_WIDTH = 80;
|
|
45
|
-
/** Where the site's App Router pages live. */
|
|
46
|
-
const ROUTE_ROOT = join("apps", "web", "app", "[locale]");
|
|
47
|
-
const PAGE_FILE = /^page\.(tsx|ts|jsx|js|mdx)$/;
|
|
48
45
|
/**
|
|
49
46
|
* The generator's row for a content-less route (`build-template-manifest.mjs`:
|
|
50
47
|
* every row renders under the locale layout's MarketingShell, chrome on).
|
|
@@ -89,7 +86,10 @@ export function planSiteManifest(opts) {
|
|
|
89
86
|
templatesDir: opts.templatesDir,
|
|
90
87
|
markers: opts.markers,
|
|
91
88
|
});
|
|
92
|
-
|
|
89
|
+
// #10774 D4.2 — the template's own code-route rows a spoke does not serve
|
|
90
|
+
// (`/sign-in`, `/sign-up`) leave with the files the spoke filter dropped.
|
|
91
|
+
const planned = plan.next ?? roster.text;
|
|
92
|
+
const appended = withoutUnservedRouteRows(parseRosterFile(planned, roster.path), opts.targetDir).next ?? planned;
|
|
93
93
|
const declared = new Set(JSON.parse(appended).rows.map((row) => row["route"]));
|
|
94
94
|
const extra = [
|
|
95
95
|
...sampleRows(opts, declared),
|
|
@@ -226,42 +226,10 @@ function sampleRows(opts, declared) {
|
|
|
226
226
|
}
|
|
227
227
|
/** A code-route row for every page route on disk that `declared` does not name. */
|
|
228
228
|
function codeRouteRows(targetDir, declared) {
|
|
229
|
-
return
|
|
229
|
+
return servedRoutes(targetDir)
|
|
230
230
|
.filter((route) => !declared.has(route))
|
|
231
231
|
.map((route) => ({ route, ...CODE_ROUTE_ROW }));
|
|
232
232
|
}
|
|
233
|
-
/**
|
|
234
|
-
* Every route the site's App Router serves from a page file, sorted: route
|
|
235
|
-
* groups `(x)` dropped, a trailing optional catch-all `[[...x]]` folded into
|
|
236
|
-
* its parent (docs-blog's `/blog/[[...slug]]` serves `/blog`). The root
|
|
237
|
-
* content renderers (`/[slug]`, `/[slug]/[detail]`) are not routes of their
|
|
238
|
-
* own — they are how the content rows render — so they are left out.
|
|
239
|
-
*/
|
|
240
|
-
function routesOnDisk(targetDir) {
|
|
241
|
-
const root = join(targetDir, ROUTE_ROOT);
|
|
242
|
-
const routes = new Set();
|
|
243
|
-
const stack = existsSync(root) ? [root] : [];
|
|
244
|
-
for (let cur = stack.pop(); cur !== undefined; cur = stack.pop()) {
|
|
245
|
-
const entries = readdirSync(cur, { withFileTypes: true });
|
|
246
|
-
for (const entry of entries) {
|
|
247
|
-
if (entry.isDirectory())
|
|
248
|
-
stack.push(join(cur, entry.name));
|
|
249
|
-
}
|
|
250
|
-
if (!entries.some((entry) => entry.isFile() && PAGE_FILE.test(entry.name))) {
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
const segments = relative(root, cur)
|
|
254
|
-
.split(sep)
|
|
255
|
-
.filter((segment) => segment !== "" && !/^\(.+\)$/.test(segment));
|
|
256
|
-
const last = segments[segments.length - 1];
|
|
257
|
-
if (last !== undefined && /^\[\[\.\.\..+\]\]$/.test(last))
|
|
258
|
-
segments.pop();
|
|
259
|
-
if (segments[0]?.startsWith("["))
|
|
260
|
-
continue;
|
|
261
|
-
routes.add(`/${segments.join("/")}`);
|
|
262
|
-
}
|
|
263
|
-
return [...routes].sort();
|
|
264
|
-
}
|
|
265
233
|
/** Every `.mdx` under `dir`, as sorted `/`-separated paths relative to `base`. */
|
|
266
234
|
function listMdx(dir, base) {
|
|
267
235
|
const out = [];
|
package/dist/site-roster.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface RosterRowSlice {
|
|
|
20
20
|
text: string;
|
|
21
21
|
}
|
|
22
22
|
/** A parsed manifest file, with the `rows` array located in its text. */
|
|
23
|
-
interface RosterFile {
|
|
23
|
+
export interface RosterFile {
|
|
24
24
|
doc: Record<string, unknown>;
|
|
25
25
|
text: string;
|
|
26
26
|
rows: RosterRowSlice[];
|
|
@@ -52,6 +52,12 @@ export interface ModuleRosterRow {
|
|
|
52
52
|
/** A `contentDir` contribution's seeded page form. */
|
|
53
53
|
seeded?: RosterRowSlice;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse a manifest file and locate its `rows` array in the text. The element
|
|
57
|
+
* spans are cross-checked against `JSON.parse`, so a splice can only ever
|
|
58
|
+
* move bytes the parser agrees are those rows.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseRosterFile(text: string, label: string): RosterFile;
|
|
55
61
|
/**
|
|
56
62
|
* The site's roster of record, or `undefined` when the site has none (a
|
|
57
63
|
* scaffold that named no template installs no manifest). A file that is not
|
|
@@ -176,6 +182,42 @@ export interface PlanRosterAppendOptions {
|
|
|
176
182
|
* other row on a contributed route is the site's own and is kept as is.
|
|
177
183
|
*/
|
|
178
184
|
export declare function planRosterAppend(opts: PlanRosterAppendOptions): RosterAppendPlan;
|
|
185
|
+
/**
|
|
186
|
+
* Every route the site's App Router serves from a page file, sorted: route
|
|
187
|
+
* groups `(x)` dropped, a trailing optional catch-all `[[...x]]` folded into
|
|
188
|
+
* its parent (docs-blog's `/blog/[[...slug]]` serves `/blog`). The root
|
|
189
|
+
* content renderers (`/[slug]`, `/[slug]/[detail]`) are not routes of their
|
|
190
|
+
* own — they are how the content rows render — so they are left out.
|
|
191
|
+
*/
|
|
192
|
+
export declare function servedRoutes(targetDir: string): string[];
|
|
193
|
+
/**
|
|
194
|
+
* `file`'s next text with its unserved code-route rows removed — every other
|
|
195
|
+
* byte verbatim — and the dropped routes in file order; `next` is undefined
|
|
196
|
+
* when none is dropped.
|
|
197
|
+
*/
|
|
198
|
+
export declare function withoutUnservedRouteRows(file: RosterFile, targetDir: string): {
|
|
199
|
+
next: string | undefined;
|
|
200
|
+
dropped: string[];
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* The routes of the site roster's page rows — the rows that carry content —
|
|
204
|
+
* or undefined when it has none. A code-route row is left out: it records a
|
|
205
|
+
* route already on disk (signal-collectors' `/careers/apply` walk under the
|
|
206
|
+
* unrelated careers segment), never a surface the site serves. The template
|
|
207
|
+
* is not resolved, so a retired template name reads as any other.
|
|
208
|
+
* @throws AddModuleRefusedError when the file's `rows` cannot be located.
|
|
209
|
+
*/
|
|
210
|
+
export declare function readRosterPageRoutes(targetDir: string): string[] | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* The site roster with its unserved code-route rows removed, or undefined
|
|
213
|
+
* when the site has no roster.
|
|
214
|
+
* @throws AddModuleRefusedError when the file's `rows` cannot be located.
|
|
215
|
+
*/
|
|
216
|
+
export declare function planServedRoster(targetDir: string): {
|
|
217
|
+
path: string;
|
|
218
|
+
next: string | undefined;
|
|
219
|
+
dropped: string[];
|
|
220
|
+
} | undefined;
|
|
179
221
|
export interface SiteRosterCheckOptions {
|
|
180
222
|
targetDir: string;
|
|
181
223
|
siteTemplatesDir: string;
|
|
@@ -191,9 +233,10 @@ export interface SiteRosterCheckOptions {
|
|
|
191
233
|
* the shipped reference row;
|
|
192
234
|
* 3. an installed seed — one some layer lands, present in the site — that
|
|
193
235
|
* the effective roster does not declare. A page the customer wrote is
|
|
194
|
-
* installed by no layer and is never reported
|
|
236
|
+
* installed by no layer and is never reported;
|
|
237
|
+
* 4. a code-route row no page file serves (#10774). A module contribution
|
|
238
|
+
* whose page file the site lacks is not expected under 2.
|
|
195
239
|
*
|
|
196
240
|
* A site with no roster (a template-less scaffold) has no problems.
|
|
197
241
|
*/
|
|
198
242
|
export declare function siteRosterProblems(opts: SiteRosterCheckOptions): string[];
|
|
199
|
-
export {};
|
package/dist/site-roster.js
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* - `planRosterAppend` — the next `rows`, spliced into the file's text so
|
|
20
20
|
* every untouched byte stays prettier's;
|
|
21
21
|
* - `siteRosterProblems` — the scaffold-grained twin of the generator's
|
|
22
|
-
* `--check`, which cannot target a site
|
|
22
|
+
* `--check`, which cannot target a site;
|
|
23
|
+
* - `planServedRoster` — the roster without the code-route rows no page
|
|
24
|
+
* file serves (#10774), which `update` writes.
|
|
23
25
|
*
|
|
24
26
|
* Pure module: no `import.meta.url`. Package-rooted directories are supplied
|
|
25
27
|
* by the caller, the same split as `add-module.ts` / `programmatic.ts`.
|
|
@@ -46,6 +48,9 @@ export const SITE_ROSTER_PATH = ".ailk-site-manifest.json";
|
|
|
46
48
|
export const LEGACY_SITE_ROSTER_PATH = "TEMPLATE-MANIFEST.json";
|
|
47
49
|
/** Prettier's default print width — the generator formats the manifest with it. */
|
|
48
50
|
const PRINT_WIDTH = 80;
|
|
51
|
+
/** Where the site's App Router pages live. */
|
|
52
|
+
const ROUTE_ROOT = join("apps", "web", "app", "[locale]");
|
|
53
|
+
const PAGE_FILE = /^page\.(tsx|ts|jsx|js|mdx)$/;
|
|
49
54
|
// ── A string-aware JSON scanner, for the text splice ──────────────────────
|
|
50
55
|
function skipWhitespace(text, i) {
|
|
51
56
|
while (i < text.length && /\s/.test(text[i]))
|
|
@@ -120,7 +125,7 @@ function arrayElements(text, open) {
|
|
|
120
125
|
* spans are cross-checked against `JSON.parse`, so a splice can only ever
|
|
121
126
|
* move bytes the parser agrees are those rows.
|
|
122
127
|
*/
|
|
123
|
-
function parseRosterFile(text, label) {
|
|
128
|
+
export function parseRosterFile(text, label) {
|
|
124
129
|
const refuse = (why) => new AddModuleRefusedError(`${label} ${why}, so the page roster is unknown`);
|
|
125
130
|
let doc;
|
|
126
131
|
try {
|
|
@@ -538,6 +543,7 @@ export function planRosterAppend(opts) {
|
|
|
538
543
|
const refs = opts.references.filter((ref) => inSet.has(ref.module));
|
|
539
544
|
const contributed = new Set(refs.map((ref) => ref.contribution.route));
|
|
540
545
|
const shipped = readShippedManifest(opts.siteTemplatesDir, roster.template.name);
|
|
546
|
+
const unserved = unservedRouteRow(targetDir);
|
|
541
547
|
const templateRows = [];
|
|
542
548
|
const moduleRows = new Map();
|
|
543
549
|
const superseded = [];
|
|
@@ -567,7 +573,12 @@ export function planRosterAppend(opts) {
|
|
|
567
573
|
next.push(...kept);
|
|
568
574
|
}
|
|
569
575
|
else {
|
|
570
|
-
|
|
576
|
+
const row = expectedRow(ref, targetDir);
|
|
577
|
+
// #10774 D4.1 — a contribution whose page file the site lacks (a
|
|
578
|
+
// spoke's `/appointments`) is not the site's to declare.
|
|
579
|
+
if (unserved?.(row.value) === true)
|
|
580
|
+
continue;
|
|
581
|
+
next.push(row);
|
|
571
582
|
appended.push(route);
|
|
572
583
|
}
|
|
573
584
|
}
|
|
@@ -616,6 +627,104 @@ function spliceRows(file, rows) {
|
|
|
616
627
|
: `${lead}${rows.map((r) => r.text).join(`,${lead}`)}${trail}`;
|
|
617
628
|
return text.slice(0, open + 1) + body + text.slice(close);
|
|
618
629
|
}
|
|
630
|
+
// ── #10774 D4: a code-route row names a route the site serves ─────────────
|
|
631
|
+
/**
|
|
632
|
+
* Every route the site's App Router serves from a page file, sorted: route
|
|
633
|
+
* groups `(x)` dropped, a trailing optional catch-all `[[...x]]` folded into
|
|
634
|
+
* its parent (docs-blog's `/blog/[[...slug]]` serves `/blog`). The root
|
|
635
|
+
* content renderers (`/[slug]`, `/[slug]/[detail]`) are not routes of their
|
|
636
|
+
* own — they are how the content rows render — so they are left out.
|
|
637
|
+
*/
|
|
638
|
+
export function servedRoutes(targetDir) {
|
|
639
|
+
const root = join(targetDir, ROUTE_ROOT);
|
|
640
|
+
const routes = new Set();
|
|
641
|
+
const stack = existsSync(root) ? [root] : [];
|
|
642
|
+
for (let cur = stack.pop(); cur !== undefined; cur = stack.pop()) {
|
|
643
|
+
const entries = readdirSync(cur, { withFileTypes: true });
|
|
644
|
+
for (const entry of entries) {
|
|
645
|
+
if (entry.isDirectory())
|
|
646
|
+
stack.push(join(cur, entry.name));
|
|
647
|
+
}
|
|
648
|
+
if (!entries.some((entry) => entry.isFile() && PAGE_FILE.test(entry.name))) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const segments = relative(root, cur)
|
|
652
|
+
.split(sep)
|
|
653
|
+
.filter((segment) => segment !== "" && !/^\(.+\)$/.test(segment));
|
|
654
|
+
const last = segments[segments.length - 1];
|
|
655
|
+
if (last !== undefined && /^\[\[\.\.\..+\]\]$/.test(last))
|
|
656
|
+
segments.pop();
|
|
657
|
+
if (segments[0]?.startsWith("["))
|
|
658
|
+
continue;
|
|
659
|
+
routes.add(`/${segments.join("/")}`);
|
|
660
|
+
}
|
|
661
|
+
return [...routes].sort();
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Whether a row is a code-route row no page file of the site serves — or
|
|
665
|
+
* `undefined` when the site has no route tree (a roster-only fixture, a web
|
|
666
|
+
* app elsewhere), where no row is judged. A route whose first segment is
|
|
667
|
+
* dynamic is never judged either: the root renderers are not in
|
|
668
|
+
* `servedRoutes`.
|
|
669
|
+
*/
|
|
670
|
+
function unservedRouteRow(targetDir) {
|
|
671
|
+
if (!existsSync(join(targetDir, ROUTE_ROOT)))
|
|
672
|
+
return undefined;
|
|
673
|
+
const served = new Set(servedRoutes(targetDir));
|
|
674
|
+
return (row) => {
|
|
675
|
+
const route = row["route"];
|
|
676
|
+
return (row["kind"] === "route" &&
|
|
677
|
+
typeof route === "string" &&
|
|
678
|
+
!route.split("/").filter(Boolean)[0]?.startsWith("[") &&
|
|
679
|
+
!served.has(route));
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* `file`'s next text with its unserved code-route rows removed — every other
|
|
684
|
+
* byte verbatim — and the dropped routes in file order; `next` is undefined
|
|
685
|
+
* when none is dropped.
|
|
686
|
+
*/
|
|
687
|
+
export function withoutUnservedRouteRows(file, targetDir) {
|
|
688
|
+
const unserved = unservedRouteRow(targetDir);
|
|
689
|
+
const dropped = file.rows.filter((row) => unserved?.(row.value) === true);
|
|
690
|
+
if (dropped.length === 0)
|
|
691
|
+
return { next: undefined, dropped: [] };
|
|
692
|
+
return {
|
|
693
|
+
next: spliceRows(file, file.rows.filter((row) => !dropped.includes(row))),
|
|
694
|
+
dropped: dropped.map((row) => row.value["route"]),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
/** The site's roster file, read without resolving its template, or undefined when it has none. */
|
|
698
|
+
function readRosterFile(targetDir) {
|
|
699
|
+
const path = [SITE_ROSTER_PATH, LEGACY_SITE_ROSTER_PATH].find((name) => existsSync(join(targetDir, name)));
|
|
700
|
+
if (path === undefined)
|
|
701
|
+
return undefined;
|
|
702
|
+
return { ...parseRosterFile(readFileSync(join(targetDir, path), "utf8"), path), path };
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* The routes of the site roster's page rows — the rows that carry content —
|
|
706
|
+
* or undefined when it has none. A code-route row is left out: it records a
|
|
707
|
+
* route already on disk (signal-collectors' `/careers/apply` walk under the
|
|
708
|
+
* unrelated careers segment), never a surface the site serves. The template
|
|
709
|
+
* is not resolved, so a retired template name reads as any other.
|
|
710
|
+
* @throws AddModuleRefusedError when the file's `rows` cannot be located.
|
|
711
|
+
*/
|
|
712
|
+
export function readRosterPageRoutes(targetDir) {
|
|
713
|
+
return readRosterFile(targetDir)
|
|
714
|
+
?.rows.filter((row) => row.value["kind"] === "page")
|
|
715
|
+
.map((row) => row.value["route"]);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* The site roster with its unserved code-route rows removed, or undefined
|
|
719
|
+
* when the site has no roster.
|
|
720
|
+
* @throws AddModuleRefusedError when the file's `rows` cannot be located.
|
|
721
|
+
*/
|
|
722
|
+
export function planServedRoster(targetDir) {
|
|
723
|
+
const file = readRosterFile(targetDir);
|
|
724
|
+
if (file === undefined)
|
|
725
|
+
return undefined;
|
|
726
|
+
return { path: file.path, ...withoutUnservedRouteRows(file, targetDir) };
|
|
727
|
+
}
|
|
619
728
|
/** Every `content/en/**.mdx` some layer installs into a site of this template and module set. */
|
|
620
729
|
function layerInstalledSeeds(template, modules, opts, surfaces) {
|
|
621
730
|
const out = new Set();
|
|
@@ -670,7 +779,9 @@ function layerInstalledSeeds(template, modules, opts, surfaces) {
|
|
|
670
779
|
* the shipped reference row;
|
|
671
780
|
* 3. an installed seed — one some layer lands, present in the site — that
|
|
672
781
|
* the effective roster does not declare. A page the customer wrote is
|
|
673
|
-
* installed by no layer and is never reported
|
|
782
|
+
* installed by no layer and is never reported;
|
|
783
|
+
* 4. a code-route row no page file serves (#10774). A module contribution
|
|
784
|
+
* whose page file the site lacks is not expected under 2.
|
|
674
785
|
*
|
|
675
786
|
* A site with no roster (a template-less scaffold) has no problems.
|
|
676
787
|
*/
|
|
@@ -695,6 +806,13 @@ export function siteRosterProblems(opts) {
|
|
|
695
806
|
problems.push(`${route}: appears on more than one row`);
|
|
696
807
|
seen.add(route);
|
|
697
808
|
}
|
|
809
|
+
// #10774 D4.4 — a code-route row no page file serves.
|
|
810
|
+
const unserved = unservedRouteRow(opts.targetDir);
|
|
811
|
+
for (const row of roster.rows) {
|
|
812
|
+
if (unserved?.(row.value) === true) {
|
|
813
|
+
problems.push(`${row.value["route"]}: a route row, but no page file serves it`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
698
816
|
const contributions = new Map();
|
|
699
817
|
// #10040 D3 — a site that dropped `marketing` carries no public route tree,
|
|
700
818
|
// so no module's public pages are expected on its roster either.
|
|
@@ -711,6 +829,8 @@ export function siteRosterProblems(opts) {
|
|
|
711
829
|
const route = ref.contribution.route;
|
|
712
830
|
contributions.set(route, ref.contribution);
|
|
713
831
|
const wanted = expectedRow(ref, opts.targetDir).value;
|
|
832
|
+
if (unserved?.(wanted) === true)
|
|
833
|
+
continue;
|
|
714
834
|
const rows = roster.rows.filter((r) => r.value["route"] === route);
|
|
715
835
|
if (rows.length === 0) {
|
|
716
836
|
problems.push(`${route}: no row, but module "${moduleName}" contributes it`);
|
package/dist/surfaces.d.ts
CHANGED
|
@@ -136,6 +136,14 @@ export declare function resolveSurfaces(modules?: string[], explicit?: string[])
|
|
|
136
136
|
* on Windows too.
|
|
137
137
|
*/
|
|
138
138
|
export declare function excludedRoutePaths(enabled: string[]): string[];
|
|
139
|
+
/**
|
|
140
|
+
* The module-less gateable surfaces a roster's `routes` name (#5469 D10,
|
|
141
|
+
* #10774 D2), in `GATEABLE_SURFACES` order. A route names a surface when its
|
|
142
|
+
* first path segment is one of the surface's top-level route directories
|
|
143
|
+
* (`/pricing` → `apps/web/app/[locale]/pricing`). A module-owned surface is
|
|
144
|
+
* never named: it comes only with its module (#5365 FR5).
|
|
145
|
+
*/
|
|
146
|
+
export declare function surfacesNamedByRoutes(routes: readonly string[]): string[];
|
|
139
147
|
/**
|
|
140
148
|
* The content directories belonging to gateable surfaces that are NOT enabled,
|
|
141
149
|
* with `<locale>` left in place for the caller to expand.
|
package/dist/surfaces.js
CHANGED
|
@@ -540,6 +540,22 @@ export function excludedRoutePaths(enabled) {
|
|
|
540
540
|
}
|
|
541
541
|
return out.sort();
|
|
542
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* The module-less gateable surfaces a roster's `routes` name (#5469 D10,
|
|
545
|
+
* #10774 D2), in `GATEABLE_SURFACES` order. A route names a surface when its
|
|
546
|
+
* first path segment is one of the surface's top-level route directories
|
|
547
|
+
* (`/pricing` → `apps/web/app/[locale]/pricing`). A module-owned surface is
|
|
548
|
+
* never named: it comes only with its module (#5365 FR5).
|
|
549
|
+
*/
|
|
550
|
+
export function surfacesNamedByRoutes(routes) {
|
|
551
|
+
const routeRoot = "apps/web/app/[locale]";
|
|
552
|
+
const moduleOwned = new Set(Object.values(MODULE_SURFACES).flat());
|
|
553
|
+
const topSegments = new Set(routes
|
|
554
|
+
.map((route) => route.split("/").filter(Boolean)[0])
|
|
555
|
+
.filter((segment) => segment !== undefined));
|
|
556
|
+
return GATEABLE_SURFACES.filter((surface) => !moduleOwned.has(surface.id) &&
|
|
557
|
+
surface.routes.some((dir) => topSegments.has(dir.slice(routeRoot.length + 1)))).map((surface) => surface.id);
|
|
558
|
+
}
|
|
543
559
|
/**
|
|
544
560
|
* The content directories belonging to gateable surfaces that are NOT enabled,
|
|
545
561
|
* with `<locale>` left in place for the caller to expand.
|
package/dist/topology.d.ts
CHANGED
|
@@ -215,6 +215,18 @@ export declare const HUB_ORIGIN_ENV_VAR = "AILK_API_URL";
|
|
|
215
215
|
* the hub issues those.
|
|
216
216
|
*/
|
|
217
217
|
export declare function writeHubOriginEnv(targetDir: string, origin: string): void;
|
|
218
|
+
/**
|
|
219
|
+
* `<name>=<value()>` into `apps/web/.env.local`, unless any env file Next
|
|
220
|
+
* loads for `apps/web` already assigns `name` (`value` is then never called);
|
|
221
|
+
* and `.env.local` gitignored either way. `true` when the line was written.
|
|
222
|
+
*/
|
|
223
|
+
export declare function writeWebEnv(targetDir: string, name: string, value: () => string): boolean;
|
|
224
|
+
/**
|
|
225
|
+
* `<name>=<value()>` into `apps/api/.env` — the one file the API's
|
|
226
|
+
* `dotenv.config()` reads — under `writeWebEnv`'s never-clobber rule, and
|
|
227
|
+
* `.env` gitignored either way (#10659 D6). `true` when the line was written.
|
|
228
|
+
*/
|
|
229
|
+
export declare function writeApiEnv(targetDir: string, name: string, value: () => string): boolean;
|
|
218
230
|
/** The env var naming the hub's web origin a hub-mode spoke signs in at (#7894 D11). */
|
|
219
231
|
export declare const HUB_SIGN_IN_URL_ENV_VAR = "AILK_HUB_SIGN_IN_URL";
|
|
220
232
|
/** The env var holding a hub-mode spoke's own cookie key material (#7894 D7, D11). */
|
package/dist/topology.js
CHANGED
|
@@ -903,19 +903,32 @@ export function writeHubOriginEnv(targetDir, origin) {
|
|
|
903
903
|
writeWebEnv(targetDir, HUB_ORIGIN_ENV_VAR, () => origin);
|
|
904
904
|
}
|
|
905
905
|
/**
|
|
906
|
-
* `<name>=<value()>` into
|
|
907
|
-
*
|
|
908
|
-
*
|
|
906
|
+
* `<name>=<value()>` into `apps/web/.env.local`, unless any env file Next
|
|
907
|
+
* loads for `apps/web` already assigns `name` (`value` is then never called);
|
|
908
|
+
* and `.env.local` gitignored either way. `true` when the line was written.
|
|
909
909
|
*/
|
|
910
|
-
function writeWebEnv(targetDir, name, value) {
|
|
911
|
-
|
|
910
|
+
export function writeWebEnv(targetDir, name, value) {
|
|
911
|
+
return writeEnvLine(join(targetDir, "apps", "web"), NEXT_ENV_FILES, ".env.local", targetDir, ENV_LOCAL_IGNORE_LINES, name, value);
|
|
912
|
+
}
|
|
913
|
+
/** The lines that gitignore `apps/api/.env`; `.env` is appended when none is there. */
|
|
914
|
+
const ENV_IGNORE_LINES = new Set([".env", "**/.env", ".env*"]);
|
|
915
|
+
/**
|
|
916
|
+
* `<name>=<value()>` into `apps/api/.env` — the one file the API's
|
|
917
|
+
* `dotenv.config()` reads — under `writeWebEnv`'s never-clobber rule, and
|
|
918
|
+
* `.env` gitignored either way (#10659 D6). `true` when the line was written.
|
|
919
|
+
*/
|
|
920
|
+
export function writeApiEnv(targetDir, name, value) {
|
|
921
|
+
return writeEnvLine(join(targetDir, "apps", "api"), [".env"], ".env", targetDir, ENV_IGNORE_LINES, name, value);
|
|
922
|
+
}
|
|
923
|
+
/** One app's never-clobber env append, and the root `.gitignore` guard. */
|
|
924
|
+
function writeEnvLine(appDir, files, target, targetDir, ignoreLines, name, value) {
|
|
912
925
|
const assigned = new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=\\s*\\S`, "m");
|
|
913
|
-
const already =
|
|
914
|
-
const path = join(
|
|
926
|
+
const already = files.some((file) => {
|
|
927
|
+
const path = join(appDir, file);
|
|
915
928
|
return existsSync(path) && assigned.test(readFileSync(path, "utf8"));
|
|
916
929
|
});
|
|
917
930
|
if (!already) {
|
|
918
|
-
const file = join(
|
|
931
|
+
const file = join(appDir, target);
|
|
919
932
|
const existing = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
920
933
|
writeOwnerOnly(file, appendLine(existing, `${name}=${value()}`));
|
|
921
934
|
}
|
|
@@ -923,9 +936,10 @@ function writeWebEnv(targetDir, name, value) {
|
|
|
923
936
|
const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
|
|
924
937
|
const covered = ignore
|
|
925
938
|
.split("\n")
|
|
926
|
-
.some((line) =>
|
|
939
|
+
.some((line) => ignoreLines.has(line.trim()));
|
|
927
940
|
if (!covered)
|
|
928
|
-
writeFileSync(ignorePath, appendLine(ignore,
|
|
941
|
+
writeFileSync(ignorePath, appendLine(ignore, target));
|
|
942
|
+
return !already;
|
|
929
943
|
}
|
|
930
944
|
/** The env var naming the hub's web origin a hub-mode spoke signs in at (#7894 D11). */
|
|
931
945
|
export const HUB_SIGN_IN_URL_ENV_VAR = "AILK_HUB_SIGN_IN_URL";
|
package/dist/update.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MdxParse } from "./mdx-braces.js";
|
|
1
2
|
import type { WorkspaceVersionMap } from "./substitute.js";
|
|
2
3
|
/** A refusal the CLI exits 2 on, thrown before anything is written. */
|
|
3
4
|
export declare class UpdateRefusedError extends Error {
|
|
@@ -23,6 +24,11 @@ export interface UpdateSiteOptions {
|
|
|
23
24
|
* at. A site already on both boot sets, or a spoke, never reads it.
|
|
24
25
|
*/
|
|
25
26
|
versions?: WorkspaceVersionMap;
|
|
27
|
+
/**
|
|
28
|
+
* The MDX parser the brace pass verifies an escape with (#10773 D5);
|
|
29
|
+
* `null` acts as "not installed". Absent: `loadSiteMdxParse(targetDir)`.
|
|
30
|
+
*/
|
|
31
|
+
mdxParse?: MdxParse | null;
|
|
26
32
|
}
|
|
27
33
|
export interface UpdateResult {
|
|
28
34
|
targetDir: string;
|
|
@@ -58,8 +64,14 @@ export interface UpdateResult {
|
|
|
58
64
|
envRowsAdded?: string[];
|
|
59
65
|
/** Record rows dropped because the path is no longer recordable (#10173). */
|
|
60
66
|
droppedRows?: string[];
|
|
67
|
+
/** The module-less surfaces the roster names that `architecture.yaml` now declares (#10774 D3). */
|
|
68
|
+
surfacesDeclared?: string[];
|
|
69
|
+
/** The roster's code-route rows dropped because no page file serves them, in roster order (#10774 D4). */
|
|
70
|
+
rosterRowsDropped?: string[];
|
|
61
71
|
/** The mdx-body pages under `content/` given a `# ` heading this run (#7495). */
|
|
62
72
|
headingsAdded: string[];
|
|
73
|
+
/** The mdx-body pages under `content/` whose bare `{` were escaped this run (#10773). */
|
|
74
|
+
bracesEscaped: string[];
|
|
63
75
|
backfilled?: {
|
|
64
76
|
version: string;
|
|
65
77
|
files: number;
|
|
@@ -83,12 +95,20 @@ export declare function mergeMessagesAdditively(ours: string, theirs: string): {
|
|
|
83
95
|
reason: string;
|
|
84
96
|
};
|
|
85
97
|
export declare function updateSite(opts: UpdateSiteOptions): Promise<UpdateResult>;
|
|
98
|
+
/**
|
|
99
|
+
* The site's own MDX parser (#10773 D5): `@mdx-js/mdx` resolved through the
|
|
100
|
+
* `next-mdx-remote` its `apps/web` renders with — never create-ailk's own
|
|
101
|
+
* copy, and never fetched. `undefined` when it is not installed or fails to
|
|
102
|
+
* load; never throws.
|
|
103
|
+
*/
|
|
104
|
+
export declare function loadSiteMdxParse(targetDir: string): Promise<MdxParse | undefined>;
|
|
105
|
+
export type { MdxParse } from "./mdx-braces.js";
|
|
86
106
|
/** The printed report (D9): one section per non-empty list, in order. */
|
|
87
107
|
export declare function formatUpdateReport(result: UpdateResult): string;
|
|
88
108
|
export interface UpdateArgs {
|
|
89
109
|
target: string | undefined;
|
|
90
110
|
help: boolean;
|
|
91
111
|
}
|
|
92
|
-
export declare const UPDATE_HELP = "create-ailk update \u2014 bring template fixes into a site's copied app code.\n\nUsage:\n create-ailk update [<target-dir>]\n\nArguments:\n <target-dir> The existing site. Defaults to the current dir.\n\nOptions:\n --help, -h Print this message.\n\nReads .ailk-template-files.json (written beside .ailk-site-manifest.json by\nevery template scaffold) to tell an untouched copy from an edit. An\nuntouched apps/web, apps/api or apps/mcp file, or a root tools/jest file the\napp jest configs require, whose template copy changed is overwritten. An\nedited file whose template copy also changed is three-way merged with\n`git` (git must be on PATH); a conflict leaves the file alone and is\nlisted under \"Needs review\"; an apps/web/messages catalog instead gains the\ntemplate's new keys and keeps every value it has, and is reviewed only when a\nkey is an object on one side and a value on the other. Every template file,\ntest or not, that imports a file left under \"Needs review\" or \"Skipped\" (by a\nrelative or @/ import, transitively) is held at the site's version with it,\nits record entry is not advanced, and it is listed under \"Held back with\n<file>\" \u2014 reconcile the file, then run update again to bring it forward. A\nwritten file whose import names a file the site will not have is held the\nsame way, under \"Held back with <file> (not in this site)\". Every other file\nin the run still updates. When project.yaml has an env_contract block, each\nenv var the written files read that has no row gains one; no row is edited.\nA record row at a path create-ailk no longer records is dropped and listed.\nA site with no record gets one on its first run, from the version its\n.ailk-site-manifest.json already records. A site scaffolded before\n@working-theory/mcp-server has its copied apps/mcp moved to the boot set:\nthe server and each module's paid MCP pack join apps/mcp/package.json, the\ncopied server files go, and apps/mcp/src/lib/paid-tools.generated.ts is\nrewritten on every run. A site scaffolded before @working-theory/api has its\ncopied apps/api moved to the boot file: the app becomes @working-theory/app-api,\nthe package and the six @working-theory/api-<module> packages join\napps/api/package.json, its bins and build:vercel delegate to the package, and\nthe copied server files go. Run `pnpm install` after either move, since the\napp's package.json gained dependencies. Each mdx-body page in content/ with no\n'# ' heading gets its title and description written into its body as its\nheading and intro, listed under \"Page heading added\"; a page with one heading\nis left as it is, and one with several, or one whose frontmatter it cannot\nread, is listed under \"Needs review\". Commit before running, and review the
|
|
112
|
+
export declare const UPDATE_HELP = "create-ailk update \u2014 bring template fixes into a site's copied app code.\n\nUsage:\n create-ailk update [<target-dir>]\n\nArguments:\n <target-dir> The existing site. Defaults to the current dir.\n\nOptions:\n --help, -h Print this message.\n\nReads .ailk-template-files.json (written beside .ailk-site-manifest.json by\nevery template scaffold) to tell an untouched copy from an edit. An\nuntouched apps/web, apps/api or apps/mcp file, or a root tools/jest file the\napp jest configs require, whose template copy changed is overwritten. An\nedited file whose template copy also changed is three-way merged with\n`git` (git must be on PATH); a conflict leaves the file alone and is\nlisted under \"Needs review\"; an apps/web/messages catalog instead gains the\ntemplate's new keys and keeps every value it has, and is reviewed only when a\nkey is an object on one side and a value on the other. Every template file,\ntest or not, that imports a file left under \"Needs review\" or \"Skipped\" (by a\nrelative or @/ import, transitively) is held at the site's version with it,\nits record entry is not advanced, and it is listed under \"Held back with\n<file>\" \u2014 reconcile the file, then run update again to bring it forward. A\nwritten file whose import names a file the site will not have is held the\nsame way, under \"Held back with <file> (not in this site)\". Every other file\nin the run still updates. When project.yaml has an env_contract block, each\nenv var the written files read that has no row gains one; no row is edited.\nA record row at a path create-ailk no longer records is dropped and listed.\nA surface the site's .ailk-site-manifest.json names a page on but\narchitecture.yaml does not declare (one no module owns, such as projects)\ngets its route files added and is declared, listed under \"Surfaces declared\nfrom the roster\". A code-route row of that roster no page file serves is\ndropped, listed under \"Roster rows dropped, no route serves them\".\nA site with no record gets one on its first run, from the version its\n.ailk-site-manifest.json already records. A site scaffolded before\n@working-theory/mcp-server has its copied apps/mcp moved to the boot set:\nthe server and each module's paid MCP pack join apps/mcp/package.json, the\ncopied server files go, and apps/mcp/src/lib/paid-tools.generated.ts is\nrewritten on every run. A site scaffolded before @working-theory/api has its\ncopied apps/api moved to the boot file: the app becomes @working-theory/app-api,\nthe package and the six @working-theory/api-<module> packages join\napps/api/package.json, its bins and build:vercel delegate to the package, and\nthe copied server files go. Run `pnpm install` after either move, since the\napp's package.json gained dependencies. Each mdx-body page in content/ with no\n'# ' heading gets its title and description written into its body as its\nheading and intro, listed under \"Page heading added\"; a page with one heading\nis left as it is, and one with several, or one whose frontmatter it cannot\nread, is listed under \"Needs review\". Each bare { on an mdx-body page is\nescaped as \\{ when the site's own MDX parser then reads the page clean, listed\nunder \"Placeholder brace escaped\"; a page it cannot escape safely, or any page\nwith one while the site's dependencies are not installed, is listed under\n\"Needs review\". Commit before running, and review the diff after.\n";
|
|
93
113
|
/** Parse the argv AFTER the `update` word. Strict `node:util` parseArgs, like `parseAdoptArgs`. */
|
|
94
114
|
export declare function parseUpdateArgs(argv: string[]): UpdateArgs;
|