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
@@ -45,6 +45,9 @@ import { createSitemapHandler } from "../../../sitemap.xml/route";
45
45
  import { createLlmsHandler } from "../../../llms.txt/route";
46
46
  import type { ContentAdapter } from "@working-theory/content-adapters";
47
47
 
48
+ // #10704 — a fixture suite: its content is shaped for the mdx source, whatever the site serves from.
49
+ process.env.CONTENT_ADAPTER = "mdx";
50
+
48
51
  type JsonLd = Record<string, unknown>;
49
52
 
50
53
  function jsonLdScripts(container: HTMLElement): JsonLd[] {
@@ -58,6 +58,9 @@ import CareersPage from "../page";
58
58
  import CareersSlugPage from "../[slug]/page";
59
59
  import * as pageRenderer from "@working-theory/page-renderer";
60
60
 
61
+ // #10704 — a fixture suite: its content is shaped for the mdx source, whatever the site serves from.
62
+ process.env.CONTENT_ADAPTER = "mdx";
63
+
61
64
  // ── Shared stubs ───────────────────────────────────────────────────────────────
62
65
  const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
63
66
 
@@ -32,6 +32,9 @@ jest.mock("next/navigation", () => ({
32
32
 
33
33
  import OfferingPricingPage, { dynamic, generateMetadata } from "../page";
34
34
 
35
+ // #10704 — a fixture suite: its content is shaped for the mdx source, whatever the site serves from.
36
+ process.env.CONTENT_ADAPTER = "mdx";
37
+
35
38
  const SLUG = "website-rebuild";
36
39
  const SITE_EMAIL = "hello@example.com";
37
40
 
@@ -27,6 +27,9 @@ import {
27
27
 
28
28
  import PricingPage, { generateMetadata } from "../page";
29
29
 
30
+ // #10704 — a fixture suite: its content is shaped for the mdx source, whatever the site serves from.
31
+ process.env.CONTENT_ADAPTER = "mdx";
32
+
30
33
  const SITE_EMAIL = "hello@example.com";
31
34
  const HOSTILE = "</script><script>alert(1)</script>";
32
35
 
@@ -15,6 +15,9 @@ import { act, render, screen, within } from "@testing-library/react";
15
15
 
16
16
  import PricingPage from "../page";
17
17
 
18
+ // #10704 — a fixture suite: its content is shaped for the mdx source, whatever the site serves from.
19
+ process.env.CONTENT_ADAPTER = "mdx";
20
+
18
21
  const PLANS = [
19
22
  {
20
23
  key: "basic",
@@ -27,6 +27,9 @@ import { TOPOLOGY } from "../../../../lib/topology.generated";
27
27
  import * as pricingPage from "../page";
28
28
  import PricingPage, { generateMetadata } from "../page";
29
29
 
30
+ // #10704 — a fixture suite: its content is shaped for the mdx source, whatever the site serves from.
31
+ process.env.CONTENT_ADAPTER = "mdx";
32
+
30
33
  const PLANS = [
31
34
  {
32
35
  key: "basic",
@@ -51,6 +51,9 @@ import { notFound } from "next/navigation";
51
51
  import ProjectsPage from "../page";
52
52
  import ProjectSlugPage from "../[slug]/page";
53
53
 
54
+ // #10704 — a fixture suite: its content is shaped for the mdx source, whatever the site serves from.
55
+ process.env.CONTENT_ADAPTER = "mdx";
56
+
54
57
  const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
55
58
 
56
59
  // ── Shared stub helpers ────────────────────────────────────────────────────────
@@ -31,6 +31,9 @@ import ServicesPage from "../page";
31
31
  import ServiceSlugPage from "../[slug]/page";
32
32
  import * as pageRenderer from "@working-theory/page-renderer";
33
33
 
34
+ // #10704 — a fixture suite: its content is shaped for the mdx source, whatever the site serves from.
35
+ process.env.CONTENT_ADAPTER = "mdx";
36
+
34
37
  const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
35
38
 
36
39
  // ── Shared stub helpers ────────────────────────────────────────────────────────
@@ -31,6 +31,9 @@ import TeamPage from "../page";
31
31
  import TeamMemberPage from "../[slug]/page";
32
32
  import * as pageRenderer from "@working-theory/page-renderer";
33
33
 
34
+ // #10704 — a fixture suite: its content is shaped for the mdx source, whatever the site serves from.
35
+ process.env.CONTENT_ADAPTER = "mdx";
36
+
34
37
  type JsonLd = Record<string, unknown>;
35
38
 
36
39
  function jsonLdScripts(container: HTMLElement): JsonLd[] {
@@ -29,6 +29,9 @@ import {
29
29
  } from "../page-chrome";
30
30
  import type { SiteChrome } from "../site-chrome";
31
31
 
32
+ // #10704 — a fixture suite: its content is shaped for the mdx source, whatever the site serves from.
33
+ process.env.CONTENT_ADAPTER = "mdx";
34
+
32
35
  // ── AC-2 — the route families ────────────────────────────────────────────
33
36
 
34
37
  function camel(prefix: string): string {
@@ -29,6 +29,16 @@
29
29
  * `tools/content-compile`, which the scaffold does not ship, and the adapter
30
30
  * reads the same files with the same frontmatter schema the manifest is
31
31
  * compiled from. See docs/internal/decisions/scaffold-structured-data-check.md.
32
+ *
33
+ * The pages come from the site's active content source (#10704):
34
+ * `resolvedAdapterKind()`, the resolver every page reads through. Under `mdx`
35
+ * (the default) they are the files under `content/`. Under `database` — a
36
+ * spoke that serves every page from its hub and may hold no page file — they
37
+ * are the hub's published pages as the build's last-good snapshot holds them
38
+ * (`.ailk/content-snapshot.json`, written by `pnpm --filter web build` under
39
+ * `CONTENT_ADAPTER=database`), read through the site's own content source
40
+ * with the API origin pinned to a refused loopback, so a test run reads no
41
+ * hub and needs no key.
32
42
  */
33
43
  import {
34
44
  copyFileSync,
@@ -51,8 +61,11 @@ import type {
51
61
  import {
52
62
  SCHEMA_ORG_TYPES,
53
63
  createMdxAdapter,
64
+ resolvedAdapterKind,
54
65
  } from "@working-theory/content-adapters";
55
66
  import {
67
+ getContentAdapter,
68
+ getMdxSource,
56
69
  getPageSections,
57
70
  pageToJsonLd,
58
71
  siteOrigin,
@@ -158,6 +171,94 @@ const adapter = createMdxAdapter({
158
171
  defaultLocale: DEFAULT_LOCALE,
159
172
  });
160
173
 
174
+ // ── #10704 D2 — a site that serves its pages from its hub ──────────────────
175
+
176
+ const SOURCE = resolvedAdapterKind();
177
+ // page-renderer's `CONTENT_SNAPSHOT_PATH`, which this jest config maps no
178
+ // subpath to read; create-ailk's structured-data-check suite pins the two
179
+ // equal. Relative to cwd, as the content source reads it.
180
+ const SNAPSHOT_REL = ".ailk/content-snapshot.json";
181
+
182
+ // No read may reach a hub: every page and list read fails as unavailable and
183
+ // the content source answers from the snapshot, the path a deployed spoke
184
+ // takes while its hub is down.
185
+ if (SOURCE === "database") process.env.AILK_API_URL = "http://127.0.0.1:9";
186
+
187
+ /**
188
+ * Every page the snapshot holds, keyed by its cache tag
189
+ * (`content:<workspace>:<type>:<slug>:<locale>`, `cache-tags.ts`), or why it
190
+ * holds none. Read only under `database`.
191
+ */
192
+ function readHubPages(): {
193
+ pages: Array<{ locale: string; type: string; slug: string }>;
194
+ problem: string | null;
195
+ } {
196
+ const path = join(process.cwd(), SNAPSHOT_REL);
197
+ const build = "run `pnpm --filter web build` under CONTENT_ADAPTER=database to write it";
198
+ if (!existsSync(path)) {
199
+ return { pages: [], problem: `${path} does not exist — ${build}` };
200
+ }
201
+ let parsed: { version?: unknown; pages?: unknown; lists?: unknown } | null;
202
+ try {
203
+ parsed = JSON.parse(readFileSync(path, "utf8")) as typeof parsed;
204
+ } catch {
205
+ parsed = null;
206
+ }
207
+ if (
208
+ parsed === null ||
209
+ typeof parsed !== "object" ||
210
+ parsed.version !== 1 ||
211
+ typeof parsed.pages !== "object" ||
212
+ parsed.pages === null ||
213
+ typeof parsed.lists !== "object" ||
214
+ parsed.lists === null
215
+ ) {
216
+ return { pages: [], problem: `${path} is not a version-1 content snapshot — ${build}` };
217
+ }
218
+ const pages = Object.keys(parsed.pages).map((tag) => {
219
+ const parts = tag.split(":");
220
+ return {
221
+ type: parts[2] ?? "",
222
+ slug: parts.slice(3, -1).join(":"),
223
+ locale: parts[parts.length - 1] ?? "",
224
+ };
225
+ });
226
+ if (pages.length === 0) {
227
+ return { pages, problem: `${path} holds no page — ${build}` };
228
+ }
229
+ return { pages, problem: null };
230
+ }
231
+
232
+ const HUB = SOURCE === "database" ? readHubPages() : { pages: [], problem: null };
233
+
234
+ /** The snapshot's (locale, type) pairs, in the shape the content walk takes. */
235
+ function hubTypeDirs(): Array<{ locale: string; typeDir: string }> {
236
+ const seen = new Set<string>();
237
+ const out: Array<{ locale: string; typeDir: string }> = [];
238
+ for (const { locale, type } of HUB.pages) {
239
+ if (seen.has(`${locale}/${type}`)) continue;
240
+ seen.add(`${locale}/${type}`);
241
+ out.push({ locale, typeDir: type });
242
+ }
243
+ return out.sort((a, b) =>
244
+ `${a.locale}/${a.typeDir}`.localeCompare(`${b.locale}/${b.typeDir}`),
245
+ );
246
+ }
247
+
248
+ /** A type's pages in a locale, from the active source. */
249
+ function listDetailFor(type: PageType, locale: string): Promise<ContentPage[]> {
250
+ return SOURCE === "database"
251
+ ? getContentAdapter().listDetail(type, locale)
252
+ : adapter.listDetail(type, locale);
253
+ }
254
+
255
+ /** One page's source, from the active source (`ContentAdapter` has no `getPageSource`). */
256
+ function pageSourceFor(type: PageType, slug: string, locale: string) {
257
+ return SOURCE === "database"
258
+ ? getMdxSource(type, slug, locale)
259
+ : adapter.getPageSource(type, slug, locale);
260
+ }
261
+
161
262
  // ── JSON-LD flatten + validate (the ten lines from tools/scrape-jsonld.ts) ──
162
263
 
163
264
  function flattenBlocks(parsed: unknown): JsonLdObject[] {
@@ -244,9 +345,21 @@ function identityFailure(blocks: JsonLdObject[], label: string): string | null {
244
345
  // ── 1. Content-driven: every content page builds valid JSON-LD ──────────────
245
346
 
246
347
  describe("structured data — every content page builds valid JSON-LD", () => {
247
- const typeDirs = contentTypeDirs();
348
+ const typeDirs = SOURCE === "database" ? hubTypeDirs() : contentTypeDirs();
349
+ let hubPagesChecked = 0;
350
+
351
+ afterAll(() => {
352
+ if (SOURCE === "database") {
353
+ console.info(`structured-data: ${hubPagesChecked} hub page(s) checked from ${SNAPSHOT_REL}`);
354
+ }
355
+ });
248
356
 
249
357
  it("finds content to check", () => {
358
+ if (SOURCE === "database") {
359
+ // A hub-served site with no snapshot has nothing to check: misconfigured.
360
+ expect(HUB.problem).toBeNull();
361
+ return;
362
+ }
250
363
  // A site with no content directory at all is misconfigured, not clean.
251
364
  expect(existsSync(CONTENT_ROOT)).toBe(true);
252
365
  expect(typeDirs.length).toBeGreaterThan(0);
@@ -274,14 +387,17 @@ describe("structured data — every content page builds valid JSON-LD", () => {
274
387
  }
275
388
  const type = typeDir as PageType;
276
389
 
277
- const pages: ContentPage[] = await adapter.listDetail(type, locale);
278
- const dirPath = join(CONTENT_ROOT, locale, typeDir);
279
- const mdxCount = readdirSync(dirPath).filter(
280
- (n) => n.endsWith(".mdx") && !n.startsWith("_"),
281
- ).length;
390
+ const pages: ContentPage[] = await listDetailFor(type, locale);
282
391
  // The adapter drops a page whose frontmatter fails the schema; that
283
392
  // page would render nothing, so count it here rather than lose it.
284
- expect(pages.length).toBe(mdxCount);
393
+ // Under `database` the count is the pages the hub published (#10704).
394
+ const expected =
395
+ SOURCE === "database"
396
+ ? HUB.pages.filter((p) => p.locale === locale && p.type === type).length
397
+ : readdirSync(join(CONTENT_ROOT, locale, typeDir)).filter(
398
+ (n) => n.endsWith(".mdx") && !n.startsWith("_"),
399
+ ).length;
400
+ expect(pages.length).toBe(expected);
285
401
 
286
402
  const failures: string[] = [];
287
403
  for (const page of pages) {
@@ -295,6 +411,7 @@ describe("structured data — every content page builds valid JSON-LD", () => {
295
411
  if (failure !== null) failures.push(failure);
296
412
  }
297
413
  expect(failures).toEqual([]);
414
+ if (SOURCE === "database") hubPagesChecked += pages.length;
298
415
  },
299
416
  );
300
417
  });
@@ -674,14 +791,16 @@ describe("structured data — every route emits a valid JSON-LD script", () => {
674
791
  // #10043 D7 — a Service's category, for the nested route's params.
675
792
  const parents = new Map<string, string>();
676
793
  if (identity.slug !== null) {
677
- const page = await adapter.getPageSource(
794
+ const page = await pageSourceFor(
678
795
  type,
679
796
  identity.slug,
680
797
  DEFAULT_LOCALE,
681
798
  );
682
799
  if (page === null) {
683
800
  throw new Error(
684
- `${label}: renders ${type}/${identity.slug}, but content/${DEFAULT_LOCALE}/${type}/${identity.slug}.mdx does not exist — the route serves 404 in production.`,
801
+ SOURCE === "database"
802
+ ? `${label}: renders ${type}/${identity.slug}, but the hub serves no such page (${SNAPSHOT_REL}) — the route serves 404 in production.`
803
+ : `${label}: renders ${type}/${identity.slug}, but content/${DEFAULT_LOCALE}/${type}/${identity.slug}.mdx does not exist — the route serves 404 in production.`,
685
804
  );
686
805
  }
687
806
  slugs = [identity.slug];
@@ -689,7 +808,7 @@ describe("structured data — every route emits a valid JSON-LD script", () => {
689
808
  // The hub's own page shares this route's type but is served at the hub
690
809
  // (#10044 D13), so it is never rendered here.
691
810
  const hubSlug = hubOwnSlug(file, type);
692
- const pages = (await adapter.listDetail(type, DEFAULT_LOCALE)).filter(
811
+ const pages = (await listDetailFor(type, DEFAULT_LOCALE)).filter(
693
812
  (p: ContentPage) => p.slug !== hubSlug,
694
813
  );
695
814
  for (const p of pages) if (p.parent !== undefined) parents.set(p.slug, p.parent);
@@ -772,6 +891,8 @@ describe("structured data — a hub's own page is not rendered at its detail rou
772
891
  async () => {
773
892
  const root = mkdtempSync(join(tmpdir(), "ailk-10044-sd-"));
774
893
  const prior = process.env.AILK_CONTENT_ROOT;
894
+ // #10704 D2 — the fixture is files, whatever source the site serves from.
895
+ const priorSource = process.env.CONTENT_ADAPTER;
775
896
  try {
776
897
  mkdirSync(join(root, DEFAULT_LOCALE, "Pricing"), { recursive: true });
777
898
  copyFileSync(join(CONTENT_ROOT, "_site.mdx"), join(root, "_site.mdx"));
@@ -784,6 +905,7 @@ describe("structured data — a hub's own page is not rendered at its detail rou
784
905
  '---\ntype: "Pricing"\nslug: "website-rebuild"\ntitle: "Website Rebuild"\ndescription: "What a rebuild costs."\nplans: ["ss-website-rebuild"]\n---\n',
785
906
  );
786
907
  process.env.AILK_CONTENT_ROOT = root;
908
+ process.env.CONTENT_ADAPTER = "mdx";
787
909
  const fixtureAdapter = createMdxAdapter({
788
910
  contentRoot: root,
789
911
  defaultLocale: DEFAULT_LOCALE,
@@ -807,6 +929,8 @@ describe("structured data — a hub's own page is not rendered at its detail rou
807
929
  } finally {
808
930
  if (prior === undefined) delete process.env.AILK_CONTENT_ROOT;
809
931
  else process.env.AILK_CONTENT_ROOT = prior;
932
+ if (priorSource === undefined) delete process.env.CONTENT_ADAPTER;
933
+ else process.env.CONTENT_ADAPTER = priorSource;
810
934
  rmSync(root, { recursive: true, force: true });
811
935
  }
812
936
  },
@@ -39,9 +39,11 @@ export function resolveBaseUrl(): string {
39
39
  * `packages/templates/src/__tests__/routes-map.test.ts`'s table test and
40
40
  * `apps/web/__tests__/routes/routes-map-identity.test.ts`'s differential
41
41
  * test over the live content tree). The signature is annotated explicitly so
42
- * this stays the exact `(type: string, slug: string) => string` contract the
43
- * three call sites (`sitemap.xml`, `llms.txt`, `docs-blog/llms-route.ts`)
44
- * depend on, even though `routeFor`'s own `slug` parameter is optional.
42
+ * this stays the exact `(type: string, slug: string) => string` contract its
43
+ * callers depend on, even though `routeFor`'s own `slug` parameter is
44
+ * optional. `sitemap.xml`, `llms.txt` and, since #10775, the docs-blog
45
+ * module's `docs-blog/llms-route.ts` call `pagePathFor` instead; the export
46
+ * stays for a docs-blog site that installed the older copy of that route.
45
47
  */
46
48
  export const slugToPath: (type: string, slug: string) => string = routeFor;
47
49
 
@@ -21,7 +21,8 @@
21
21
  "architecture": "tsx tools/lint-architecture.ts",
22
22
  "claude-md:check": "tsx tools/check-claude-md.ts",
23
23
  "content-source:check": "tsx tools/check-content-source.ts",
24
- "preflight": "pnpm architecture && pnpm claude-md:check && pnpm content-source:check && pnpm type-check && pnpm lint && pnpm build"
24
+ "mdx-body:check": "tsx tools/check-mdx-body.ts",
25
+ "preflight": "pnpm architecture && pnpm claude-md:check && pnpm content-source:check && pnpm mdx-body:check && pnpm type-check && pnpm lint && pnpm build"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@arethetypeswrong/cli": "^0.18.5",