create-ailk 0.11.5 → 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.
Files changed (76) hide show
  1. package/component-catalog.json +35 -2
  2. package/component-catalog.md +9 -6
  3. package/dist/add-module.js +13 -0
  4. package/dist/module-architecture.d.ts +8 -0
  5. package/dist/module-architecture.js +33 -0
  6. package/dist/paid-sections.d.ts +23 -0
  7. package/dist/paid-sections.js +119 -0
  8. package/dist/programmatic.js +8 -0
  9. package/package.json +3 -3
  10. package/templates/.env.example +69 -3
  11. package/templates/apps/api/.env.example +22 -0
  12. package/templates/apps/api/CLAUDE.md +19 -7
  13. package/templates/apps/api/package.json +1 -0
  14. package/templates/apps/api/src/bin/spokes.ts +137 -0
  15. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +96 -0
  16. package/templates/apps/api/src/config/index.ts +97 -0
  17. package/templates/apps/api/src/lib/__mocks__/prisma.ts +35 -0
  18. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +152 -0
  19. package/templates/apps/api/src/lib/spoke-store.ts +131 -0
  20. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +331 -0
  21. package/templates/apps/api/src/routes/leads/index.ts +33 -3
  22. package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +528 -0
  23. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +204 -0
  24. package/templates/apps/api/src/routes/project-listings/index.ts +14 -2
  25. package/templates/apps/api/src/routes/project-listings/public.ts +42 -1
  26. package/templates/apps/api/src/routes/project-listings/submit.ts +3 -0
  27. package/templates/apps/api/src/routes/webhooks/index.ts +29 -0
  28. package/templates/apps/api/src/server.ts +48 -0
  29. package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +147 -0
  30. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +70 -0
  31. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +374 -0
  32. package/templates/apps/api/src/services/default-tenant.ts +51 -28
  33. package/templates/apps/api/src/services/deliverable-fulfillment.ts +89 -3
  34. package/templates/apps/api/src/services/lead-routing.ts +13 -1
  35. package/templates/apps/api/src/services/project-listings.ts +209 -18
  36. package/templates/apps/api/src/services/public-site-resolver.ts +149 -28
  37. package/templates/apps/api/src/services/slack-notify.ts +54 -3
  38. package/templates/apps/web/.env.example +14 -0
  39. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +137 -0
  40. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -20
  41. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +5 -16
  42. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +8 -0
  43. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +5 -16
  44. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +8 -0
  45. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +5 -19
  46. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -0
  47. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +5 -14
  48. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -0
  49. package/templates/apps/web/app/[locale]/layout.tsx +4 -0
  50. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +5 -16
  51. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +8 -0
  52. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +4 -7
  53. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +5 -14
  54. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -0
  55. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +2 -2
  56. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +5 -22
  57. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +8 -0
  58. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +5 -14
  59. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +8 -0
  60. package/templates/apps/web/jest.config.cjs +1 -1
  61. package/templates/apps/web/lib/__tests__/listings.test.tsx +352 -0
  62. package/templates/apps/web/lib/listings.tsx +472 -0
  63. package/templates/apps/web/lib/paid-section-contracts.ts +22 -0
  64. package/templates/apps/web/lib/paid-sections.generated.ts +16 -0
  65. package/templates/apps/web/messages/en.json +10 -0
  66. package/templates/apps/web/src/lib/__tests__/commerce-sections.test.ts +57 -0
  67. package/templates/apps/web/src/lib/commerce-sections.ts +33 -71
  68. package/templates/database/CHANGELOG.md +24 -0
  69. package/templates/database/__tests__/spoke-registry.db.test.ts +191 -0
  70. package/templates/database/auth/schema.prisma +31 -0
  71. package/templates/database/config/schema.prisma +26 -0
  72. package/templates/database/inbox/schema.prisma +21 -0
  73. package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +80 -0
  74. package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +49 -0
  75. package/templates/database/package.json +1 -1
  76. package/versions.json +34 -34
@@ -63,6 +63,10 @@ import { siteLogo } from "../../lib/site-logo";
63
63
  // Registers the `_site.mdx` identity with page-renderer at module load, so
64
64
  // every page rendered under this layout carries the sitewide JSON-LD (#6597).
65
65
  import "../../lib/site-identity";
66
+ // Registers the listicle directory with page-renderer at module load, so a
67
+ // live listicle's landing page gets its shelves and each entry its page
68
+ // (#7521). Inert without AILK_LISTING_PUBLIC_KEY or the paid package.
69
+ import "../../lib/listings";
66
70
  import { loadSiteBrandTokens, loadSiteShapeCss } from "../../lib/site-theme";
67
71
  import { resolveBaseUrl } from "../../src/lib/routes";
68
72
 
@@ -1,12 +1,8 @@
1
1
  // Partner detail page. Reads the Partner MDX for the given slug, renders via
2
- // next-mdx-remote/rsc, and emits a JSON-LD <script>. Unknown slugs trigger 404.
3
- import {
4
- getMdxSource as defaultGetMdxSource,
5
- renderPage,
6
- renderPageMetadata,
7
- } from "@working-theory/page-renderer";
2
+ // next-mdx-remote/rsc, and emits a JSON-LD <script>. Unknown slugs 404 through renderPage.
3
+ import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
4
+ import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
8
5
  import type { Metadata } from "next";
9
- import { notFound } from "next/navigation";
10
6
  import type { ReactElement } from "react";
11
7
 
12
8
  interface Props {
@@ -23,21 +19,14 @@ export async function generateMetadata({
23
19
 
24
20
  export default async function Page({
25
21
  params,
26
- getMdxSource = defaultGetMdxSource,
22
+ getMdxSource,
27
23
  }: Props): Promise<ReactElement> {
28
24
  const { locale, slug } = await params;
29
- const source = await getMdxSource("Partner", slug, locale);
30
-
31
- if (!source) {
32
- notFound();
33
- }
34
-
35
25
  return renderPage({
36
26
  type: "Partner",
37
27
  slug,
38
28
  locale,
39
- translationsNamespace: "partners",
40
29
  bodyMode: "mdx-body",
41
- getMdxSource: async () => source,
30
+ ...(getMdxSource !== undefined && { getMdxSource }),
42
31
  });
43
32
  }
@@ -32,6 +32,7 @@ jest.mock("next-mdx-remote/rsc", () => ({
32
32
  // ── Import page components ─────────────────────────────────────────────────────
33
33
  import PartnersPage from "../page";
34
34
  import PartnerSlugPage from "../[slug]/page";
35
+ import * as pageRenderer from "@working-theory/page-renderer";
35
36
 
36
37
  // ── Shared stubs ───────────────────────────────────────────────────────────────
37
38
  const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
@@ -97,6 +98,7 @@ describe("partners/[slug]/page — snapshot regression", () => {
97
98
  });
98
99
 
99
100
  it("null source triggers notFound()", async () => {
101
+ const renderPageSpy = jest.spyOn(pageRenderer, "renderPage");
100
102
  const { notFound } = jest.requireMock("next/navigation") as {
101
103
  notFound: jest.MockedFunction<() => never>;
102
104
  };
@@ -108,6 +110,12 @@ describe("partners/[slug]/page — snapshot regression", () => {
108
110
  getMdxSource: getMdxSourceEmpty,
109
111
  }),
110
112
  ).rejects.toThrow("NEXT_NOT_FOUND");
113
+ // The 404 is renderPage's detail branch, not a route pre-check (#7537).
114
+ expect(renderPageSpy).toHaveBeenCalledTimes(1);
115
+ expect(
116
+ renderPageSpy.mock.calls[0]![0].translationsNamespace,
117
+ ).toBeUndefined();
118
+ renderPageSpy.mockRestore();
111
119
  expect(notFound).toHaveBeenCalled();
112
120
  });
113
121
  });
@@ -1,5 +1,5 @@
1
1
  // Portfolio item page (#6687). Reads the PortfolioItem MDX for the given slug and emits a
2
- // JSON-LD <script>. Unknown slugs trigger 404. Renders `sections[]` when the
2
+ // JSON-LD <script>. Unknown slugs 404 through renderPage. Renders `sections[]` when the
3
3
  // frontmatter carries them (the /for composed pattern), MDX prose otherwise.
4
4
  // NO_HAND_IMPLEMENT_OK: /build-starter operator-directed live pass (#6687).
5
5
  import {
@@ -8,7 +8,6 @@ import {
8
8
  renderPageMetadata,
9
9
  } from "@working-theory/page-renderer";
10
10
  import type { Metadata } from "next";
11
- import { notFound } from "next/navigation";
12
11
  import type { ReactElement } from "react";
13
12
 
14
13
  interface Props {
@@ -28,13 +27,12 @@ export default async function Page({
28
27
  getMdxSource = defaultGetMdxSource,
29
28
  }: Props): Promise<ReactElement> {
30
29
  const { locale, slug } = await params;
30
+ // Read once to pick the body mode. A null source still goes to renderPage,
31
+ // which owns the unknown-slug branch (#7537).
31
32
  const source = await getMdxSource("PortfolioItem", slug, locale);
32
33
 
33
- if (!source) {
34
- notFound();
35
- }
36
-
37
34
  const composed =
35
+ source !== null &&
38
36
  Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
39
37
  ((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) > 0;
40
38
 
@@ -42,7 +40,6 @@ export default async function Page({
42
40
  type: "PortfolioItem",
43
41
  slug,
44
42
  locale,
45
- translationsNamespace: "portfolio",
46
43
  ...(composed ? {} : { bodyMode: "mdx-body" as const }),
47
44
  getMdxSource: async () => source,
48
45
  });
@@ -1,12 +1,8 @@
1
1
  // Product detail page. Reads the Product MDX for the given slug, renders via
2
- // next-mdx-remote/rsc, and emits a JSON-LD <script>. Unknown slugs trigger 404.
3
- import {
4
- renderPage,
5
- renderPageMetadata,
6
- getMdxSource as defaultGetMdxSource,
7
- } from "@working-theory/page-renderer";
2
+ // next-mdx-remote/rsc, and emits a JSON-LD <script>. Unknown slugs 404 through renderPage.
3
+ import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
4
+ import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
8
5
  import type { Metadata } from "next";
9
- import { notFound } from "next/navigation";
10
6
  import type { ReactElement } from "react";
11
7
 
12
8
  interface Props {
@@ -23,20 +19,15 @@ export async function generateMetadata({
23
19
 
24
20
  export default async function Page({
25
21
  params,
26
- getMdxSource = defaultGetMdxSource,
22
+ getMdxSource,
27
23
  }: Props): Promise<ReactElement> {
28
24
  const { locale, slug } = await params;
29
- const source = await getMdxSource("Product", slug, locale);
30
- if (!source) {
31
- notFound();
32
- }
33
25
  return renderPage({
34
26
  type: "Product",
35
27
  slug,
36
28
  locale,
37
- translationsNamespace: "products",
38
29
  bodyMode: "mdx-body",
39
30
  populatedDescriptionTestId: "product-description",
40
- getMdxSource: async () => source,
31
+ ...(getMdxSource !== undefined && { getMdxSource }),
41
32
  });
42
33
  }
@@ -29,6 +29,7 @@ jest.mock("next/navigation", () => ({
29
29
  import { notFound } from "next/navigation";
30
30
  import ProductsPage from "../page";
31
31
  import ProductSlugPage from "../[slug]/page";
32
+ import * as pageRenderer from "@working-theory/page-renderer";
32
33
 
33
34
  const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
34
35
 
@@ -117,6 +118,7 @@ describe("ProductSlugPage — snapshot regression", () => {
117
118
  });
118
119
 
119
120
  it("null source calls notFound()", async () => {
121
+ const renderPageSpy = jest.spyOn(pageRenderer, "renderPage");
120
122
  const getMdxSourceNull = jest.fn().mockResolvedValue(null);
121
123
  await expect(
122
124
  ProductSlugPage({
@@ -124,6 +126,12 @@ describe("ProductSlugPage — snapshot regression", () => {
124
126
  getMdxSource: getMdxSourceNull,
125
127
  }),
126
128
  ).rejects.toThrow("NEXT_NOT_FOUND");
129
+ // The 404 is renderPage's detail branch, not a route pre-check (#7537).
130
+ expect(renderPageSpy).toHaveBeenCalledTimes(1);
131
+ expect(
132
+ renderPageSpy.mock.calls[0]![0].translationsNamespace,
133
+ ).toBeUndefined();
134
+ renderPageSpy.mockRestore();
127
135
  expect(mockNotFound).toHaveBeenCalled();
128
136
  });
129
137
  });
@@ -18,8 +18,8 @@
18
18
  *
19
19
  * The fix — `../../../../src/lib/commerce-sections.ts`'s
20
20
  * `ensureCommerceSectionsRegistered()` — registers the module LAZILY, via a
21
- * `webpackIgnore`-annotated `import()` wrapped in try/catch, so the build
22
- * never needs the package to resolve. This test now pins the ABSENCE of the
21
+ * generated paid-sections registry (`lib/paid-sections.generated.ts`, #7536),
22
+ * so the build never needs the package to resolve. This test now pins the ABSENCE of the
23
23
  * static import (the regression this file exists to catch) and the
24
24
  * PRESENCE of the lazy-registration call site instead.
25
25
  */
@@ -1,15 +1,8 @@
1
1
  // Resource detail page. Reads the ResourceDetail MDX for the given slug, renders
2
- // via renderPage (mdx-body mode), and emits a JSON-LD <script>. Unknown slugs trigger 404.
3
- import type {
4
- getMdxSource as defaultGetMdxSource,
5
- } from "@working-theory/page-renderer";
6
- import {
7
- getMdxSource as defaultGetMdxSourceImpl,
8
- renderPage,
9
- renderPageMetadata,
10
- } from "@working-theory/page-renderer";
2
+ // via renderPage (mdx-body mode), and emits a JSON-LD <script>. Unknown slugs 404 through renderPage.
3
+ import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
4
+ import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
11
5
  import type { Metadata } from "next";
12
- import { notFound } from "next/navigation";
13
6
  import type { ReactElement } from "react";
14
7
 
15
8
  interface Props {
@@ -26,25 +19,15 @@ export async function generateMetadata({
26
19
 
27
20
  export default async function Page({
28
21
  params,
29
- getMdxSource = defaultGetMdxSourceImpl,
22
+ getMdxSource,
30
23
  }: Props): Promise<ReactElement> {
31
24
  const { locale, slug } = await params;
32
-
33
- // Pre-check: verify slug exists; call notFound() on null (not empty-state JSX).
34
- const source = await getMdxSource("ResourceDetail", slug, locale);
35
- if (!source) {
36
- notFound();
37
- }
38
-
39
- // translationsNamespace is required by RenderPageInput but unreachable here:
40
- // notFound() above ensures renderPage's empty-state branch never executes.
41
25
  return renderPage({
42
26
  type: "ResourceDetail",
43
27
  slug,
44
28
  locale,
45
- translationsNamespace: slug,
46
29
  bodyMode: "mdx-body",
47
30
  populatedDescriptionTestId: "resource-description",
48
- getMdxSource,
31
+ ...(getMdxSource !== undefined && { getMdxSource }),
49
32
  });
50
33
  }
@@ -45,6 +45,7 @@ jest.mock("next/navigation", () => ({
45
45
 
46
46
  import ResourcesPage from "../page";
47
47
  import ResourceDetailPage from "../[slug]/page";
48
+ import * as pageRenderer from "@working-theory/page-renderer";
48
49
 
49
50
  const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
50
51
 
@@ -115,12 +116,19 @@ describe("ResourceDetailPage — snapshot regression", () => {
115
116
  });
116
117
 
117
118
  it("notFound state calls notFound() when source is null", async () => {
119
+ const renderPageSpy = jest.spyOn(pageRenderer, "renderPage");
118
120
  await expect(
119
121
  ResourceDetailPage({
120
122
  params: Promise.resolve({ locale: "en", slug: "missing" }),
121
123
  getMdxSource: getMdxSourceEmpty,
122
124
  }),
123
125
  ).rejects.toThrow("NEXT_NOT_FOUND");
126
+ // The 404 is renderPage's detail branch, not a route pre-check (#7537).
127
+ expect(renderPageSpy).toHaveBeenCalledTimes(1);
128
+ expect(
129
+ renderPageSpy.mock.calls[0]![0].translationsNamespace,
130
+ ).toBeUndefined();
131
+ renderPageSpy.mockRestore();
124
132
  expect(mockNotFound).toHaveBeenCalledTimes(1);
125
133
  });
126
134
  });
@@ -1,12 +1,8 @@
1
1
  // Service detail page. Reads the Service MDX for the given slug, renders via
2
- // next-mdx-remote/rsc, and emits a JSON-LD <script>. Unknown slugs trigger 404.
3
- import {
4
- renderPage,
5
- renderPageMetadata,
6
- getMdxSource as defaultGetMdxSource,
7
- } from "@working-theory/page-renderer";
2
+ // next-mdx-remote/rsc, and emits a JSON-LD <script>. Unknown slugs 404 through renderPage.
3
+ import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
4
+ import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
8
5
  import type { Metadata } from "next";
9
- import { notFound } from "next/navigation";
10
6
  import type { ReactElement } from "react";
11
7
 
12
8
  interface Props {
@@ -23,20 +19,15 @@ export async function generateMetadata({
23
19
 
24
20
  export default async function Page({
25
21
  params,
26
- getMdxSource = defaultGetMdxSource,
22
+ getMdxSource,
27
23
  }: Props): Promise<ReactElement> {
28
24
  const { locale, slug } = await params;
29
- const source = await getMdxSource("Service", slug, locale);
30
- if (!source) {
31
- notFound();
32
- }
33
25
  return renderPage({
34
26
  type: "Service",
35
27
  slug,
36
28
  locale,
37
- translationsNamespace: "services",
38
29
  bodyMode: "mdx-body",
39
30
  populatedDescriptionTestId: "service-description",
40
- getMdxSource: async () => source,
31
+ ...(getMdxSource !== undefined && { getMdxSource }),
41
32
  });
42
33
  }
@@ -29,6 +29,7 @@ jest.mock("next/navigation", () => ({
29
29
  import { notFound } from "next/navigation";
30
30
  import ServicesPage from "../page";
31
31
  import ServiceSlugPage from "../[slug]/page";
32
+ import * as pageRenderer from "@working-theory/page-renderer";
32
33
 
33
34
  const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
34
35
 
@@ -117,6 +118,7 @@ describe("ServiceSlugPage — snapshot regression", () => {
117
118
  });
118
119
 
119
120
  it("null source calls notFound()", async () => {
121
+ const renderPageSpy = jest.spyOn(pageRenderer, "renderPage");
120
122
  const getMdxSourceNull = jest.fn().mockResolvedValue(null);
121
123
  await expect(
122
124
  ServiceSlugPage({
@@ -124,6 +126,12 @@ describe("ServiceSlugPage — snapshot regression", () => {
124
126
  getMdxSource: getMdxSourceNull,
125
127
  }),
126
128
  ).rejects.toThrow("NEXT_NOT_FOUND");
129
+ // The 404 is renderPage's detail branch, not a route pre-check (#7537).
130
+ expect(renderPageSpy).toHaveBeenCalledTimes(1);
131
+ expect(
132
+ renderPageSpy.mock.calls[0]![0].translationsNamespace,
133
+ ).toBeUndefined();
134
+ renderPageSpy.mockRestore();
127
135
  expect(mockNotFound).toHaveBeenCalled();
128
136
  });
129
137
  });
@@ -117,7 +117,7 @@ module.exports = {
117
117
  // Collapse every copy onto the one this workspace resolves, so the
118
118
  // existing per-test mocks cover page-renderer's call too. Resolved at
119
119
  // config load — no hard-coded virtual-store path.
120
- "^next/navigation$": require.resolve("next/navigation"),
120
+ "^next/navigation(\\.js)?$": require.resolve("next/navigation"),
121
121
  "^next-intl/server$": "<rootDir>/__tests__/__mocks__/next-intl-server.ts",
122
122
  "^next-intl$": "<rootDir>/__tests__/__mocks__/next-intl.ts",
123
123
  // next-mdx-remote/rsc is ESM-only; map to a CJS stub so ts-jest can load