create-ailk 0.26.0 → 0.28.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 +86 -9
- package/dist/add-module-core.js +2 -1
- package/dist/add-module.d.ts +2 -1
- package/dist/add-module.js +9 -4
- package/dist/add-route.js +46 -19
- package/dist/body-heading.d.ts +56 -0
- package/dist/body-heading.js +394 -0
- package/dist/cli.js +28 -1
- package/dist/content-payloads.js +2 -2
- package/dist/derive-architecture.js +8 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/lib/auth-secret-file.d.ts +6 -2
- package/dist/lib/auth-secret-file.js +46 -20
- package/dist/lib/env-catalog.js +20 -0
- package/dist/lib/site-modules.d.ts +8 -0
- package/dist/lib/site-modules.js +34 -0
- package/dist/module-architecture.js +25 -14
- package/dist/paid-sections.d.ts +17 -0
- package/dist/paid-sections.js +39 -0
- package/dist/programmatic.d.ts +16 -0
- package/dist/programmatic.js +73 -4
- package/dist/remove-module.d.ts +98 -0
- package/dist/remove-module.js +620 -0
- package/dist/seed-flows.d.ts +2 -2
- package/dist/seed-flows.js +16 -9
- package/dist/site-roster.js +4 -1
- package/dist/starter-grid.d.ts +26 -0
- package/dist/starter-grid.js +34 -0
- package/dist/surfaces.d.ts +26 -0
- package/dist/surfaces.js +112 -11
- package/dist/sync-routes.d.ts +7 -0
- package/dist/sync-routes.js +53 -6
- package/dist/template-pages.d.ts +36 -0
- package/dist/template-pages.js +129 -0
- package/dist/topology.d.ts +2 -1
- package/dist/topology.js +4 -2
- package/dist/update.d.ts +3 -1
- package/dist/update.js +90 -6
- package/package.json +3 -3
- package/release-scope.json +2 -1
- package/site-templates/aliases/hub.TEMPLATE-MANIFEST.json +115 -0
- package/templates/.env.example +12 -2
- package/templates/CLAUDE.md +20 -20
- package/templates/README.md +19 -19
- package/templates/apps/api/.env.example +16 -5
- package/templates/apps/api/CLAUDE.md +32 -11
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +37 -0
- package/templates/apps/api/src/vercel-handler.ts +8 -1
- package/templates/apps/api/vercel.json +7 -1
- package/templates/apps/mcp/CLAUDE.md +18 -10
- package/templates/apps/web/.env.example +27 -0
- package/templates/apps/web/CLAUDE.md +56 -8
- package/templates/apps/web/__tests__/setup.ts +7 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +7 -7
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/next-path.test.ts +24 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +51 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +13 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/page.test.tsx +197 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +23 -7
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +61 -2
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/__tests__/page.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/page.tsx +152 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +76 -21
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +46 -16
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +180 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +122 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/hub-children.test.tsx +65 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/consent.actions.ts +44 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +269 -42
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +88 -30
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/courses/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/courses/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/events/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/events/page.tsx +23 -7
- package/templates/apps/web/app/[locale]/features/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +95 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/save.actions.test.ts +193 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/email-moment.ts +92 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +10 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +513 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +122 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.no-commerce.test.tsx +195 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/landing.test.tsx +293 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +397 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/checkout-seam.ts +74 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +264 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/save.actions.ts +75 -0
- package/templates/apps/web/app/[locale]/for/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +5 -1
- package/templates/apps/web/app/[locale]/for/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/integrations/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/integrations/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/layout.tsx +161 -128
- package/templates/apps/web/app/[locale]/page.tsx +51 -3
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +16 -3
- package/templates/apps/web/app/[locale]/partners/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/partners/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/portfolio/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +321 -0
- package/templates/apps/web/app/[locale]/pricing/[slug]/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/pricing-pages.test.tsx +215 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +56 -341
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +21 -4
- package/templates/apps/web/app/[locale]/products/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/products/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/projects/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +18 -11
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/resources/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/resources/page.tsx +20 -10
- package/templates/apps/web/app/[locale]/services/[slug]/[child]/page.tsx +68 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +64 -9
- package/templates/apps/web/app/[locale]/services/__tests__/hub-children.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/services/__tests__/service-detail-sections.test.tsx +223 -0
- package/templates/apps/web/app/[locale]/services/__tests__/service-families.test.tsx +368 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +45 -19
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/team/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/team/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/templates/[slug]/__tests__/page.test.tsx +203 -0
- package/templates/apps/web/app/[locale]/templates/[slug]/page.tsx +81 -0
- package/templates/apps/web/app/[locale]/templates/__tests__/page.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/templates/page.tsx +108 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +322 -2
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +161 -29
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +51 -0
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +13 -0
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +233 -7
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +92 -41
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +77 -39
- package/templates/apps/web/app/llms.txt/route.ts +41 -3
- package/templates/apps/web/app/robots.txt/route.ts +5 -1
- package/templates/apps/web/app/sitemap.xml/route.ts +45 -3
- package/templates/apps/web/auth.ts +102 -50
- package/templates/apps/web/components/signed-in-nav.tsx +8 -1
- package/templates/apps/web/lib/__tests__/bot-check.test.ts +254 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +212 -13
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +13 -1
- package/templates/apps/web/lib/__tests__/docs-index.test.ts +93 -0
- package/templates/apps/web/lib/__tests__/docs-path.test.ts +168 -0
- package/templates/apps/web/lib/__tests__/hub-children.fixtures.ts +278 -0
- package/templates/apps/web/lib/__tests__/nav-label-keys.test.ts +138 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +178 -2
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +183 -0
- package/templates/apps/web/lib/__tests__/page-chrome.test.ts +412 -0
- package/templates/apps/web/lib/__tests__/pricing-page.test.ts +142 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +484 -4
- package/templates/apps/web/lib/__tests__/route-family.test.ts +57 -0
- package/templates/apps/web/lib/__tests__/service-listing.test.ts +111 -0
- package/templates/apps/web/lib/__tests__/services-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/site-header.test.ts +151 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +148 -6
- package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +84 -0
- package/templates/apps/web/lib/__tests__/template-listing.test.ts +158 -0
- package/templates/apps/web/lib/auth-secret.ts +57 -0
- package/templates/apps/web/lib/bot-check.ts +138 -0
- package/templates/apps/web/lib/consent-visitor.ts +81 -7
- package/templates/apps/web/lib/deck-page.ts +6 -3
- package/templates/apps/web/lib/docs-index.ts +100 -0
- package/templates/apps/web/lib/docs-path.ts +94 -0
- package/templates/apps/web/lib/hub-children.tsx +318 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +22 -2
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +11 -8
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/nav-label-check.ts +61 -0
- package/templates/apps/web/lib/nav-label-keys.ts +78 -0
- package/templates/apps/web/lib/newsletter.actions.ts +31 -6
- package/templates/apps/web/lib/offerings-pricing.ts +135 -6
- package/templates/apps/web/lib/page-chrome.ts +367 -0
- package/templates/apps/web/lib/posting-title.ts +26 -4
- package/templates/apps/web/lib/pricing-page.ts +430 -0
- package/templates/apps/web/lib/relay.ts +44 -4
- package/templates/apps/web/lib/route-family.ts +69 -0
- package/templates/apps/web/lib/service-listing.ts +89 -0
- package/templates/apps/web/lib/services-path.ts +97 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +17 -6
- package/templates/apps/web/lib/site-header.ts +94 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +68 -3
- package/templates/apps/web/lib/template-listing.ts +169 -0
- package/templates/apps/web/messages/en.json +144 -3
- package/templates/apps/web/middleware.ts +179 -12
- package/templates/apps/web/next.config.mjs +220 -3
- package/templates/apps/web/package.json +3 -2
- package/templates/apps/web/scripts/assert-content-bundled.ts +28 -6
- package/templates/apps/web/scripts/check-nav-labels.ts +39 -0
- package/templates/apps/web/sentry.server.config.ts +30 -2
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -9
- package/templates/apps/web/src/lib/rate-limit.ts +68 -27
- package/templates/apps/web/src/lib/routes.ts +15 -1
- package/versions.json +48 -48
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page body owns its H1 (#7495, spec docs/specs/7495-the-page-body-owns-its-spec.md
|
|
3
|
+
* D3–D5, D8): on a page `apps/web` renders in `bodyMode: "mdx-body"`, the one
|
|
4
|
+
* `<h1>` a reader sees is the body's `# ` heading — renderPage prints nothing
|
|
5
|
+
* from frontmatter `title` or `description` there.
|
|
6
|
+
*
|
|
7
|
+
* This module is the one owner of that rule, used by `create-ailk update`
|
|
8
|
+
* (the consumer's migration), `tools/content-compile` (the content check)
|
|
9
|
+
* and the tests:
|
|
10
|
+
*
|
|
11
|
+
* - which pages are mdx-body — read from the site's own `apps/web` routes,
|
|
12
|
+
* always or only when frontmatter `sections[]` is empty (`mdxBodyRule`,
|
|
13
|
+
* `isMdxBodyPage`);
|
|
14
|
+
* - how many `# ` headings a body has (`countBodyHeadings`);
|
|
15
|
+
* - the codemod that gives a page with none `# <title>` and its description
|
|
16
|
+
* paragraph (`addBodyHeading`, `planBodyHeadings`, `migrateBodyHeadings`).
|
|
17
|
+
* A page with one or more headings is never written.
|
|
18
|
+
*
|
|
19
|
+
* Frontmatter is read with a dependency-free subset of YAML for the three
|
|
20
|
+
* keys it needs — `create-ailk` ships exactly one runtime dependency (`tar`),
|
|
21
|
+
* the `design-package.ts` posture — and any form the subset does not cover
|
|
22
|
+
* is refused, never guessed at. No relative imports: `tools/` imports this
|
|
23
|
+
* file from source.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
// The `renderPage({ … })` discovery regexes of `tools/lint-mdx-body-compiles.ts`.
|
|
28
|
+
const RENDER_PAGE_CALL_RE = /renderPage\s*\(\s*\{[\s\S]*?\}\s*\)/g;
|
|
29
|
+
const TYPE_RE = /type\s*:\s*["']([A-Za-z]+)["']/;
|
|
30
|
+
const MDX_BODY_MODE_RE = /bodyMode\s*:\s*["']mdx-body["']/;
|
|
31
|
+
// `...(composed ? {} : { bodyMode: "mdx-body" … })` — the routes' sections test.
|
|
32
|
+
const CONDITIONAL_MDX_BODY_RE = /\?\s*\{\s*\}\s*:\s*\{[^}]*bodyMode\s*:\s*["']mdx-body["']/;
|
|
33
|
+
// A content hub's `HubConfig` (#10121): `hubPageInput` sets `bodyMode` from
|
|
34
|
+
// its `body` — `mdx-body` always, `composed` unless `sections[]` is non-empty.
|
|
35
|
+
// A hub page holds one config, so its one `listType` and one `body` pair in
|
|
36
|
+
// any order; a file with more of either is not read.
|
|
37
|
+
const HUB_INPUT_RE = /\bhubPageInput\s*\(/;
|
|
38
|
+
const HUB_LIST_TYPE_RE = /\blistType\s*:\s*["']([A-Za-z]+)["']/g;
|
|
39
|
+
const HUB_BODY_RE = /\bbody\s*:\s*["'](mdx-body|composed|sections)["']/g;
|
|
40
|
+
const LOCALE_RE = /^[a-z]{2}(-[A-Z]{2})?$/;
|
|
41
|
+
const SKIPPED_DIRS = new Set(["node_modules", ".next", "dist"]);
|
|
42
|
+
function pageFiles(dir, out = []) {
|
|
43
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
44
|
+
if (SKIPPED_DIRS.has(entry.name))
|
|
45
|
+
continue;
|
|
46
|
+
const path = join(dir, entry.name);
|
|
47
|
+
if (entry.isDirectory())
|
|
48
|
+
pageFiles(path, out);
|
|
49
|
+
else if (entry.isFile() && (entry.name === "page.tsx" || entry.name === "page.ts"))
|
|
50
|
+
out.push(path);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Scan `<appsRoot>/apps/web/app/**\/page.{ts,tsx}` for `renderPage({…})` calls
|
|
56
|
+
* naming `bodyMode: "mdx-body"`. A call whose `bodyMode` sits in a conditional
|
|
57
|
+
* spread is `"unless-sections"`, any other `"always"`; `"always"` wins when two
|
|
58
|
+
* routes name one type. A content hub's `HubConfig` reads the same way, from
|
|
59
|
+
* its `body`. A missing `apps/web/app` gives an empty map.
|
|
60
|
+
*/
|
|
61
|
+
export function mdxBodyRule(appsRoot) {
|
|
62
|
+
const rule = new Map();
|
|
63
|
+
const appDir = join(appsRoot, "apps", "web", "app");
|
|
64
|
+
if (!existsSync(appDir))
|
|
65
|
+
return rule;
|
|
66
|
+
for (const file of pageFiles(appDir)) {
|
|
67
|
+
const source = readFileSync(file, "utf8");
|
|
68
|
+
if (HUB_INPUT_RE.test(source)) {
|
|
69
|
+
const types = [...source.matchAll(HUB_LIST_TYPE_RE)];
|
|
70
|
+
const bodies = [...source.matchAll(HUB_BODY_RE)];
|
|
71
|
+
if (types.length === 1 && bodies.length === 1) {
|
|
72
|
+
const type = types[0][1];
|
|
73
|
+
const body = bodies[0][1];
|
|
74
|
+
if (body === "mdx-body")
|
|
75
|
+
rule.set(type, "always");
|
|
76
|
+
else if (body === "composed" && !rule.has(type))
|
|
77
|
+
rule.set(type, "unless-sections");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const call of source.matchAll(RENDER_PAGE_CALL_RE)) {
|
|
81
|
+
const block = call[0];
|
|
82
|
+
if (!MDX_BODY_MODE_RE.test(block))
|
|
83
|
+
continue;
|
|
84
|
+
const type = TYPE_RE.exec(block)?.[1];
|
|
85
|
+
if (type === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
if (CONDITIONAL_MDX_BODY_RE.test(block)) {
|
|
88
|
+
if (!rule.has(type))
|
|
89
|
+
rule.set(type, "unless-sections");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
rule.set(type, "always");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return rule;
|
|
97
|
+
}
|
|
98
|
+
/** The routes' own `composed` test: mdx-body unless `sections` is a non-empty array. */
|
|
99
|
+
export function isMdxBodyPage(type, frontmatter, rule) {
|
|
100
|
+
const mode = rule.get(type);
|
|
101
|
+
if (mode === "always")
|
|
102
|
+
return true;
|
|
103
|
+
if (mode !== "unless-sections")
|
|
104
|
+
return false;
|
|
105
|
+
const sections = frontmatter.sections;
|
|
106
|
+
return !(Array.isArray(sections) && sections.length > 0);
|
|
107
|
+
}
|
|
108
|
+
// ── The heading count (D4) ───────────────────────────────────────────────────
|
|
109
|
+
const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
|
|
110
|
+
const ATX_H1_RE = /^ {0,3}# +\S/;
|
|
111
|
+
/**
|
|
112
|
+
* ATX level-1 headings (`^ {0,3}# +\S`) outside fenced code blocks and
|
|
113
|
+
* multi-line `{/* … *\/}` comments. `##`, `#text` and Setext headings do not
|
|
114
|
+
* count. A fence closes, as in CommonMark, on a run of its own character at
|
|
115
|
+
* least as long as the opening one, with nothing after it.
|
|
116
|
+
*/
|
|
117
|
+
export function countBodyHeadings(body) {
|
|
118
|
+
let fence;
|
|
119
|
+
let inComment = false;
|
|
120
|
+
let count = 0;
|
|
121
|
+
for (const line of body.split(/\r?\n/)) {
|
|
122
|
+
if (inComment) {
|
|
123
|
+
if (line.includes("*/}"))
|
|
124
|
+
inComment = false;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const fenceMatch = FENCE_RE.exec(line);
|
|
128
|
+
if (fence !== undefined) {
|
|
129
|
+
const run = fenceMatch?.[1];
|
|
130
|
+
if (run !== undefined &&
|
|
131
|
+
run[0] === fence[0] &&
|
|
132
|
+
run.length >= fence.length &&
|
|
133
|
+
line.slice(fenceMatch[0].length).trim() === "") {
|
|
134
|
+
fence = undefined;
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (fenceMatch !== null) {
|
|
139
|
+
fence = fenceMatch[1];
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (ATX_H1_RE.test(line))
|
|
143
|
+
count += 1;
|
|
144
|
+
const open = line.lastIndexOf("{/*");
|
|
145
|
+
if (open !== -1 && !line.includes("*/", open + 3))
|
|
146
|
+
inComment = true;
|
|
147
|
+
}
|
|
148
|
+
return count;
|
|
149
|
+
}
|
|
150
|
+
function splitPage(raw) {
|
|
151
|
+
const open = /^\uFEFF?---[ \t]*\r?\n/.exec(raw);
|
|
152
|
+
if (open === null)
|
|
153
|
+
return undefined;
|
|
154
|
+
const close = /^---[ \t]*(?:\r?\n|$)/m;
|
|
155
|
+
const rest = raw.slice(open[0].length);
|
|
156
|
+
const match = close.exec(rest);
|
|
157
|
+
if (match === null)
|
|
158
|
+
return undefined;
|
|
159
|
+
return {
|
|
160
|
+
frontmatter: rest.slice(0, match.index).split(/\r?\n/),
|
|
161
|
+
bodyStart: open[0].length + match.index + match[0].length,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
class Refused extends Error {
|
|
165
|
+
}
|
|
166
|
+
const refusedKey = (key) => new Refused(`frontmatter the subset reader does not read: ${key}`);
|
|
167
|
+
/** The value after `<key>:` on its top-level line, and the lines after it. */
|
|
168
|
+
function topLevel(lines, key) {
|
|
169
|
+
const prefix = `${key}:`;
|
|
170
|
+
const index = lines.findIndex((line) => line.startsWith(prefix));
|
|
171
|
+
if (index === -1)
|
|
172
|
+
return undefined;
|
|
173
|
+
return { value: lines[index].slice(prefix.length).trim(), next: lines.slice(index + 1) };
|
|
174
|
+
}
|
|
175
|
+
const nextNonBlank = (lines) => lines.find((line) => line.trim() !== "");
|
|
176
|
+
/** The key's value goes on below it: its next non-blank line is indented. */
|
|
177
|
+
const continues = (next) => /^[ \t]/.test(nextNonBlank(next) ?? "");
|
|
178
|
+
const YAML_INDICATOR_RE = /^[-?:,[\]{}#&*!|>'"%@`]/;
|
|
179
|
+
/** A single-line plain or double-quoted scalar; `""` for an absent or empty key. */
|
|
180
|
+
function readScalar(lines, key) {
|
|
181
|
+
const entry = topLevel(lines, key);
|
|
182
|
+
if (entry === undefined)
|
|
183
|
+
return "";
|
|
184
|
+
const { value, next } = entry;
|
|
185
|
+
if (continues(next))
|
|
186
|
+
throw refusedKey(key);
|
|
187
|
+
if (value === "")
|
|
188
|
+
return "";
|
|
189
|
+
if (value.startsWith('"')) {
|
|
190
|
+
try {
|
|
191
|
+
const decoded = JSON.parse(value);
|
|
192
|
+
if (typeof decoded === "string")
|
|
193
|
+
return decoded;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// refused below
|
|
197
|
+
}
|
|
198
|
+
throw refusedKey(key);
|
|
199
|
+
}
|
|
200
|
+
if (YAML_INDICATOR_RE.test(value) || /\s#/.test(value))
|
|
201
|
+
throw refusedKey(key);
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* `sections` as empty (absent, `[]`) or non-empty: a one-line flow sequence
|
|
206
|
+
* with content, or an empty value whose next line opens a block sequence
|
|
207
|
+
* (`- `, indented or not) or, indented, a multi-line flow sequence (`[` and
|
|
208
|
+
* anything but `]`).
|
|
209
|
+
*/
|
|
210
|
+
function readSectionsNonEmpty(lines) {
|
|
211
|
+
const entry = topLevel(lines, "sections");
|
|
212
|
+
if (entry === undefined)
|
|
213
|
+
return false;
|
|
214
|
+
const value = entry.value.replace(/\s/g, "");
|
|
215
|
+
if (value === "[]")
|
|
216
|
+
return false;
|
|
217
|
+
if (value.startsWith("[") && value.endsWith("]"))
|
|
218
|
+
return true;
|
|
219
|
+
const rest = entry.next.filter((line) => line.trim() !== "");
|
|
220
|
+
if (value === "" && rest[0] !== undefined) {
|
|
221
|
+
const first = rest[0].trim();
|
|
222
|
+
if (first.startsWith("- "))
|
|
223
|
+
return true;
|
|
224
|
+
if (/^[ \t]/.test(rest[0])) {
|
|
225
|
+
if (first.replace(/\s/g, "") === "[]")
|
|
226
|
+
return false;
|
|
227
|
+
if (first.startsWith("[")) {
|
|
228
|
+
const after = first.slice(1).trim() || (rest[1]?.trim() ?? "");
|
|
229
|
+
if (after !== "" && !after.startsWith("]"))
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
throw refusedKey("sections");
|
|
235
|
+
}
|
|
236
|
+
// ── Inserting the heading (D5) ───────────────────────────────────────────────
|
|
237
|
+
const MDX_ESCAPED_RE = /[\\{<`*_[]/g;
|
|
238
|
+
/**
|
|
239
|
+
* Text for an MDX body line: whitespace runs collapsed, trimmed, and every
|
|
240
|
+
* `\`, `{`, `<`, `` ` ``, `*`, `_` and `[` backslash-escaped.
|
|
241
|
+
*/
|
|
242
|
+
export function mdxText(text) {
|
|
243
|
+
return text.replace(/\s+/g, " ").trim().replace(MDX_ESCAPED_RE, (char) => `\\${char}`);
|
|
244
|
+
}
|
|
245
|
+
const headingText = (title) => mdxText(title).replace(/#$/, "\\#");
|
|
246
|
+
// A leading block marker would start a heading, list or quote; a leading
|
|
247
|
+
// `import `/`export ` would make the line ESM, so its first letter becomes a
|
|
248
|
+
// character reference that renders as the same letter.
|
|
249
|
+
const paragraphText = (description) => mdxText(description)
|
|
250
|
+
.replace(/^[#+>-]|^(\d+)([.)])/, (marker, digits, delimiter) => digits === undefined ? `\\${marker}` : `${digits}\\${delimiter}`)
|
|
251
|
+
.replace(/^(?:import|export)\b/, (word) => `&#${word.charCodeAt(0)};${word.slice(1)}`);
|
|
252
|
+
/** Offset in `body` after leading blank lines and a leading ESM block. */
|
|
253
|
+
function insertionPoint(body) {
|
|
254
|
+
const lines = body.split(/(?<=\n)/);
|
|
255
|
+
let at = 0;
|
|
256
|
+
let i = 0;
|
|
257
|
+
const blank = (line) => line !== undefined && line.trim() === "";
|
|
258
|
+
while (blank(lines[i]))
|
|
259
|
+
at += lines[i++].length;
|
|
260
|
+
if (!/^(?:import|export) /.test(lines[i] ?? ""))
|
|
261
|
+
return { at, afterEsm: false };
|
|
262
|
+
while (i < lines.length && !blank(lines[i]))
|
|
263
|
+
at += lines[i++].length;
|
|
264
|
+
const sawBlank = blank(lines[i]);
|
|
265
|
+
while (blank(lines[i]))
|
|
266
|
+
at += lines[i++].length;
|
|
267
|
+
return { at, afterEsm: !sawBlank };
|
|
268
|
+
}
|
|
269
|
+
function planPage(raw, rule, type) {
|
|
270
|
+
const mode = rule.get(type);
|
|
271
|
+
if (mode === undefined)
|
|
272
|
+
return { kind: "skip" };
|
|
273
|
+
const split = splitPage(raw);
|
|
274
|
+
const body = split === undefined ? raw : raw.slice(split.bodyStart);
|
|
275
|
+
if (countBodyHeadings(body) > 0)
|
|
276
|
+
return { kind: "skip" };
|
|
277
|
+
if (split === undefined)
|
|
278
|
+
return { kind: "refuse", reason: "no frontmatter" };
|
|
279
|
+
try {
|
|
280
|
+
if (mode === "unless-sections" && readSectionsNonEmpty(split.frontmatter)) {
|
|
281
|
+
return { kind: "skip" };
|
|
282
|
+
}
|
|
283
|
+
const title = readScalar(split.frontmatter, "title");
|
|
284
|
+
if (title.trim() === "")
|
|
285
|
+
return { kind: "refuse", reason: "no title" };
|
|
286
|
+
const description = readScalar(split.frontmatter, "description");
|
|
287
|
+
if (description.trim() === "")
|
|
288
|
+
return { kind: "refuse", reason: "no description" };
|
|
289
|
+
const eol = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
290
|
+
const { at, afterEsm } = insertionPoint(body);
|
|
291
|
+
const rest = body.slice(at);
|
|
292
|
+
const front = raw.slice(0, split.bodyStart);
|
|
293
|
+
// A file that ends at its closing `---` with no line ending needs one first.
|
|
294
|
+
const lead = afterEsm
|
|
295
|
+
? body.slice(0, at).endsWith("\n") ? eol : eol + eol
|
|
296
|
+
: front.endsWith("\n") ? "" : eol;
|
|
297
|
+
const inserted = `${lead}# ${headingText(title)}${eol}${eol}${paragraphText(description)}${eol}` +
|
|
298
|
+
(rest === "" ? "" : eol);
|
|
299
|
+
return { kind: "write", text: front + body.slice(0, at) + inserted + rest };
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
if (err instanceof Refused)
|
|
303
|
+
return { kind: "refuse", reason: err.message };
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* The new file text for an mdx-body page with zero `# ` headings — its
|
|
309
|
+
* `# <title>` and description paragraph inserted at the top of the body —
|
|
310
|
+
* else `undefined`. A page the subset reader refuses, with no frontmatter, or
|
|
311
|
+
* with an empty `title` or `description`, is never written.
|
|
312
|
+
*/
|
|
313
|
+
export function addBodyHeading(raw, rule, type) {
|
|
314
|
+
const plan = planPage(raw, rule, type);
|
|
315
|
+
return plan.kind === "write" ? plan.text : undefined;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Every `<contentDir>/<locale>/<Type>/<slug>.mdx` whose `<Type>` the rule
|
|
319
|
+
* names, sorted. Symlinks are skipped, as `copy.ts` and `update.ts` skip
|
|
320
|
+
* them, so the codemod never reads or writes outside `contentDir`.
|
|
321
|
+
*/
|
|
322
|
+
function rulePages(contentDir, rule) {
|
|
323
|
+
if (!existsSync(contentDir))
|
|
324
|
+
return [];
|
|
325
|
+
const pages = [];
|
|
326
|
+
for (const locale of readdirSync(contentDir, { withFileTypes: true })) {
|
|
327
|
+
if (!locale.isDirectory() || !LOCALE_RE.test(locale.name))
|
|
328
|
+
continue;
|
|
329
|
+
for (const type of rule.keys()) {
|
|
330
|
+
const typeDir = join(contentDir, locale.name, type);
|
|
331
|
+
if (!existsSync(typeDir) || !lstatSync(typeDir).isDirectory())
|
|
332
|
+
continue;
|
|
333
|
+
for (const entry of readdirSync(typeDir, { withFileTypes: true })) {
|
|
334
|
+
if (entry.isFile() && entry.name.endsWith(".mdx")) {
|
|
335
|
+
pages.push({ path: join(typeDir, entry.name), type });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return pages.sort((a, b) => a.path.localeCompare(b.path));
|
|
341
|
+
}
|
|
342
|
+
/** What `migrateBodyHeadings` would write, and the heading-less pages it cannot. Never throws for a page. */
|
|
343
|
+
export function planBodyHeadings(contentDir, rule) {
|
|
344
|
+
const plan = { writes: [], refused: [] };
|
|
345
|
+
for (const { path, type } of rulePages(contentDir, rule)) {
|
|
346
|
+
let page;
|
|
347
|
+
try {
|
|
348
|
+
page = planPage(readFileSync(path, "utf8"), rule, type);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
plan.refused.push({ path, reason: `could not be read: ${String(err)}` });
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (page.kind === "write")
|
|
355
|
+
plan.writes.push({ path, text: page.text });
|
|
356
|
+
else if (page.kind === "refuse")
|
|
357
|
+
plan.refused.push({ path, reason: page.reason });
|
|
358
|
+
}
|
|
359
|
+
return plan;
|
|
360
|
+
}
|
|
361
|
+
/** Every mdx-body page whose body does not have exactly one `# ` heading. */
|
|
362
|
+
export function headingViolations(contentDir, rule) {
|
|
363
|
+
const violations = [];
|
|
364
|
+
for (const { path, type } of rulePages(contentDir, rule)) {
|
|
365
|
+
let raw;
|
|
366
|
+
try {
|
|
367
|
+
raw = readFileSync(path, "utf8");
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const split = splitPage(raw);
|
|
373
|
+
if (rule.get(type) === "unless-sections" && split !== undefined) {
|
|
374
|
+
try {
|
|
375
|
+
if (readSectionsNonEmpty(split.frontmatter))
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const count = countBodyHeadings(split === undefined ? raw : raw.slice(split.bodyStart));
|
|
383
|
+
if (count !== 1)
|
|
384
|
+
violations.push({ path, count });
|
|
385
|
+
}
|
|
386
|
+
return violations;
|
|
387
|
+
}
|
|
388
|
+
/** Write each planned page; the paths written and the refused rows. */
|
|
389
|
+
export function migrateBodyHeadings(contentDir, rule) {
|
|
390
|
+
const { writes, refused } = planBodyHeadings(contentDir, rule);
|
|
391
|
+
for (const { path, text } of writes)
|
|
392
|
+
writeFileSync(path, text);
|
|
393
|
+
return { written: writes.map((write) => write.path), refused };
|
|
394
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -60,7 +60,8 @@ import { withNpmTokenUserConfig } from "./lib/npm-auth.js";
|
|
|
60
60
|
import { FREE_PREREQUISITES_NOTE, LICENSED_PREREQUISITES_NOTE, isLicensedInstall, proCallout, } from "./lib/pro-callout.js";
|
|
61
61
|
import { LICENSE_SET_HELP, runLicenseSet } from "./license-set.js";
|
|
62
62
|
import { PLUGIN_INSTALL_LINES, helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
|
|
63
|
-
import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
|
|
63
|
+
import { addModule, removeModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
|
|
64
|
+
import { REMOVE_MODULE_HELP, RemoveModuleRefusedError, formatRemoveModuleReport, parseRemoveModuleArgs, } from "./remove-module.js";
|
|
64
65
|
import { SEED_FLOWS_HELP, SeedFlowsRefusedError, parseSeedFlowsArgs, seedFlows, } from "./seed-flows.js";
|
|
65
66
|
import { CORE_SURFACE_PATHS, SURFACE_ALIASES } from "./surfaces.js";
|
|
66
67
|
import { SYNC_ROUTES_HELP, formatSyncRoutesReport, parseSyncRoutesArgs, } from "./sync-routes.js";
|
|
@@ -146,6 +147,32 @@ async function main() {
|
|
|
146
147
|
}
|
|
147
148
|
process.exit(0);
|
|
148
149
|
}
|
|
150
|
+
// `remove-module <module> [dir] [--force]` (#10040): the inverse of
|
|
151
|
+
// add-module for `marketing` and `docs-blog`. A refusal it names (another
|
|
152
|
+
// module, a spoke, docs-blog still installed, the site's own files in the
|
|
153
|
+
// set without --force) exits 2 before anything is written.
|
|
154
|
+
if (argv[0] === "remove-module") {
|
|
155
|
+
const args = parseRemoveModuleArgs(argv.slice(1));
|
|
156
|
+
if (args.help || args.module === undefined) {
|
|
157
|
+
process.stdout.write(REMOVE_MODULE_HELP);
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const result = removeModule({
|
|
162
|
+
targetDir: args.target ?? process.cwd(),
|
|
163
|
+
moduleName: args.module,
|
|
164
|
+
force: args.force,
|
|
165
|
+
});
|
|
166
|
+
process.stdout.write(formatRemoveModuleReport(result));
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
if (!(err instanceof RemoveModuleRefusedError))
|
|
170
|
+
throw err;
|
|
171
|
+
process.stderr.write(`[create-ailk] ${err.message}\n`);
|
|
172
|
+
process.exit(err.exitCode);
|
|
173
|
+
}
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
149
176
|
// `add-route <route> [dir]` (#6947): add a route surface the site was
|
|
150
177
|
// scaffolded without — add-module's route, architecture and nav steps with
|
|
151
178
|
// no licence and no bundle. A refusal it names (an unknown route, one a
|
package/dist/content-payloads.js
CHANGED
|
@@ -29,8 +29,8 @@ export const CONTENT_PAYLOADS = Object.freeze([
|
|
|
29
29
|
kind: "template-set",
|
|
30
30
|
name: "scaffold-commerce",
|
|
31
31
|
feature: "commerce",
|
|
32
|
-
version: "1.0.
|
|
33
|
-
digest: "
|
|
32
|
+
version: "1.0.2",
|
|
33
|
+
digest: "af6faf7bd35b4316bfa4bab1560f68c740c27f3ad9cf9dd4feca3187e70c14ad",
|
|
34
34
|
sourceRoot: "packages/create-ailk/module-templates/commerce/.claude/skills/scaffold-commerce",
|
|
35
35
|
files: Object.freeze([
|
|
36
36
|
"references/lookup-keys-template.md",
|
|
@@ -239,7 +239,11 @@ export function deriveArchitecture(modules = ["marketing"], surfaces, options =
|
|
|
239
239
|
const spoke = options.topology === "spoke";
|
|
240
240
|
const keepMcp = options.mcp !== false;
|
|
241
241
|
const selected = normalizeModules(modules);
|
|
242
|
-
|
|
242
|
+
// #10040 D4 — `marketing`'s `webImports` and `packages` are the base web
|
|
243
|
+
// app's permissions (the `(authed)` pages import `@working-theory/ui` and
|
|
244
|
+
// `@working-theory/theme` too), so they are composed whatever the selected
|
|
245
|
+
// set; only the `# Modules:` header and the `surfaces:` block follow it.
|
|
246
|
+
const slices = normalizeModules(["marketing", ...selected]).map((m) => MODULE_SLICES[m]);
|
|
243
247
|
// The declared route surface (#4154). A scaffold's live-route set is a
|
|
244
248
|
// function of this list; the `surfaces:` block below is its machine-readable
|
|
245
249
|
// record, so "which routes does this site declare" is answerable without
|
|
@@ -392,7 +396,9 @@ export function deriveArchitecture(modules = ["marketing"], surfaces, options =
|
|
|
392
396
|
"# To change them, re-run create-ailk (or re-derive) with the new module",
|
|
393
397
|
"# set — do NOT hand-edit. Enforced by tools/lint-architecture.ts.",
|
|
394
398
|
"#",
|
|
395
|
-
|
|
399
|
+
// #10040 — a site that dropped every module (a keyless scaffold minus
|
|
400
|
+
// `marketing`) still needs a readable header: `none`, never a blank.
|
|
401
|
+
`# Modules: ${selected.length > 0 ? selected.join(", ") : "none"}`,
|
|
396
402
|
`# Surfaces: ${enabledSurfaces.length > 0 ? enabledSurfaces.join(", ") : "core only"}`,
|
|
397
403
|
...(spoke ? ["# Topology: spoke"] : []),
|
|
398
404
|
"",
|
package/dist/index.d.ts
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { copyTemplate, type CopyTemplateOptions } from "./copy.js";
|
|
7
7
|
export { parseCliArgs, parseModules, ParseError, helpText, RECOGNIZED_MODULES, type ModuleName, type CliOptions, } from "./parse-args.js";
|
|
8
|
-
export { CATALOG_TARGET_PATH, FREE_MODULES, addModule, LicenseRequiredError, PAID_MODULES, classifyModules, formatSurfaceDrift, scaffold, scaffoldSync, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, type AddModuleOptions, type ResolvePaidModulesOptions, type ScaffoldOptions, type ScaffoldResult, type SyncArchitectureOptions, type SyncArchitectureResult, type SyncCatalogResult, type SyncClaudeMdResult, type SyncRoutesSyncOptions, } from "./programmatic.js";
|
|
8
|
+
export { CATALOG_TARGET_PATH, FREE_MODULES, addModule, removeModule, LicenseRequiredError, PAID_MODULES, classifyModules, formatSurfaceDrift, scaffold, scaffoldSync, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, type AddModuleOptions, type RemoveModuleCliOptions, type ResolvePaidModulesOptions, type ScaffoldOptions, type ScaffoldResult, type SyncArchitectureOptions, type SyncArchitectureResult, type SyncCatalogResult, type SyncClaudeMdResult, type SyncRoutesSyncOptions, } from "./programmatic.js";
|
|
9
9
|
export { DEFAULT_AILK_MODULE_REGISTRY_URL, FetchModuleError, fetchModule, type FetchModuleOptions, } from "./lib/fetch-module.js";
|
|
10
10
|
export { extractModuleBundle, type ExistingFilePolicy, type ExtractModuleBundleOptions, type InstallReportRow, } from "./lib/extract-module-bundle.js";
|
|
11
11
|
export { ADD_MODULE_HELP, AddModuleRefusedError, applyModuleToScaffold, formatAddModuleReport, parseAddModuleArgs, type AddModuleReportRow, type AddModuleResult, type ApplyModuleOptions, } from "./add-module.js";
|
|
12
|
+
export { REMOVABLE_MODULES, REMOVE_MODULE_HELP, RemoveModuleRefusedError, formatRemoveModuleReport, parseRemoveModuleArgs, removeModuleFromScaffold, type RemoveModuleOptions, type RemoveModuleResult, } from "./remove-module.js";
|
|
12
13
|
export { APPLY_DESIGN_HELP, UnreadChromeError, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, type ApplyDesignArgs, type ApplyDesignChange, type ApplyDesignOptions, type ApplyDesignResult, } from "./apply-design.js";
|
|
13
14
|
export type { HomeSeedOutcome } from "./design-home.js";
|
|
14
15
|
export { SITE_ROSTER_PATH, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { copyTemplate } from "./copy.js";
|
|
7
7
|
export { parseCliArgs, parseModules, ParseError, helpText, RECOGNIZED_MODULES, } from "./parse-args.js";
|
|
8
|
-
export { CATALOG_TARGET_PATH, FREE_MODULES, addModule, LicenseRequiredError, PAID_MODULES, classifyModules, formatSurfaceDrift, scaffold, scaffoldSync, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
|
|
8
|
+
export { CATALOG_TARGET_PATH, FREE_MODULES, addModule, removeModule, LicenseRequiredError, PAID_MODULES, classifyModules, formatSurfaceDrift, scaffold, scaffoldSync, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
|
|
9
9
|
// The paid-module fetch/extract primitives (K1, #5089 — Flavor 3). Exported
|
|
10
10
|
// so tooling (and this repo's own evidence-run scripts) can drive the fetch
|
|
11
11
|
// path directly without re-implementing its shape.
|
|
@@ -14,6 +14,9 @@ export { extractModuleBundle, } from "./lib/extract-module-bundle.js";
|
|
|
14
14
|
// `create-ailk add-module` (#6653) — apply a purchased module to an existing
|
|
15
15
|
// site in place. The pure core, beside `sync-routes`'s.
|
|
16
16
|
export { ADD_MODULE_HELP, AddModuleRefusedError, applyModuleToScaffold, formatAddModuleReport, parseAddModuleArgs, } from "./add-module.js";
|
|
17
|
+
// `create-ailk remove-module` (#10040) — the inverse of add-module for the
|
|
18
|
+
// marketing and docs-blog modules. The pure core, beside add-module's.
|
|
19
|
+
export { REMOVABLE_MODULES, REMOVE_MODULE_HELP, RemoveModuleRefusedError, formatRemoveModuleReport, parseRemoveModuleArgs, removeModuleFromScaffold, } from "./remove-module.js";
|
|
17
20
|
// `create-ailk apply-design` (#6819) — make an existing site wear a design,
|
|
18
21
|
// changing only its look. The pure core, beside `add-module`'s.
|
|
19
22
|
export { APPLY_DESIGN_HELP, UnreadChromeError, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, } from "./apply-design.js";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export declare const AUTH_SECRET_ENV_VAR = "BETTER_AUTH_SECRET";
|
|
2
|
+
export declare const AUTH_URL_ENV_VAR = "BETTER_AUTH_URL";
|
|
2
3
|
/** The files the writer owns, web first. */
|
|
3
4
|
export declare const AUTH_SECRET_ENV_FILES: readonly [string, string];
|
|
5
|
+
/** Each app's loopback origin, in `AUTH_SECRET_ENV_FILES` order. */
|
|
6
|
+
export declare const AUTH_URL_DEFAULTS: readonly ["http://localhost:3000", "http://localhost:3001"];
|
|
4
7
|
/**
|
|
5
8
|
* `.gitignore` lines that already ignore `apps/<app>/.env.local`. An explicit
|
|
6
9
|
* set, never a glob evaluation; `/.env.local` is absent because, anchored to
|
|
@@ -13,7 +16,8 @@ export interface WriteAuthSecretOptions {
|
|
|
13
16
|
}
|
|
14
17
|
/**
|
|
15
18
|
* Give the scaffold at `dir` a `BETTER_AUTH_SECRET` (F1–F4 of the #6107
|
|
16
|
-
* spec)
|
|
17
|
-
*
|
|
19
|
+
* spec) and each app its loopback `BETTER_AUTH_URL` (#8620). Generates at
|
|
20
|
+
* most one secret, and only when no app already has one. Idempotent — a
|
|
21
|
+
* second call changes no byte.
|
|
18
22
|
*/
|
|
19
23
|
export declare function writeAuthSecretFile(dir: string, opts?: WriteAuthSecretOptions): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Where a scaffold keeps its `BETTER_AUTH_SECRET` (#6107)
|
|
2
|
+
* Where a scaffold keeps its `BETTER_AUTH_SECRET` (#6107) and its
|
|
3
|
+
* `BETTER_AUTH_URL` (#8620, spec 9965 D10).
|
|
3
4
|
*
|
|
4
5
|
* `apps/web/auth.ts` refuses to serve in production without the secret
|
|
5
6
|
* (#6044), and so does the Admin app's `auth.ts`. A fresh scaffold therefore
|
|
@@ -10,10 +11,15 @@
|
|
|
10
11
|
* - `apps/admin/.env.local` — when the scaffold carries `apps/admin`, with
|
|
11
12
|
* the SAME value (both apps sign cookies for one site).
|
|
12
13
|
*
|
|
14
|
+
* Both `auth.ts` files also refuse to serve in production without
|
|
15
|
+
* `BETTER_AUTH_URL`, so each file gets that app's loopback origin too
|
|
16
|
+
* (`AUTH_URL_DEFAULTS`) — the origin `next start` answers on before the
|
|
17
|
+
* operator sets a public one.
|
|
18
|
+
*
|
|
13
19
|
* Never clobber, never shadow: a value already assigned in any env file Next
|
|
14
20
|
* loads for the app under `next start` is kept, and no new `.env.local` is
|
|
15
|
-
* created above it. Never print: no message, log line or
|
|
16
|
-
* carries the secret.
|
|
21
|
+
* created above it — per variable. Never print: no message, log line or
|
|
22
|
+
* return value carries the secret.
|
|
17
23
|
*
|
|
18
24
|
* Pure module: no `import.meta.url`, so jest imports it directly.
|
|
19
25
|
*/
|
|
@@ -22,11 +28,17 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
22
28
|
import { dirname, join } from "node:path";
|
|
23
29
|
import { appendLine, writeOwnerOnly } from "./license-key-files.js";
|
|
24
30
|
export const AUTH_SECRET_ENV_VAR = "BETTER_AUTH_SECRET";
|
|
31
|
+
export const AUTH_URL_ENV_VAR = "BETTER_AUTH_URL";
|
|
25
32
|
/** The files the writer owns, web first. */
|
|
26
33
|
export const AUTH_SECRET_ENV_FILES = [
|
|
27
34
|
join("apps", "web", ".env.local"),
|
|
28
35
|
join("apps", "admin", ".env.local"),
|
|
29
36
|
];
|
|
37
|
+
/** Each app's loopback origin, in `AUTH_SECRET_ENV_FILES` order. */
|
|
38
|
+
export const AUTH_URL_DEFAULTS = [
|
|
39
|
+
"http://localhost:3000",
|
|
40
|
+
"http://localhost:3001",
|
|
41
|
+
];
|
|
30
42
|
/**
|
|
31
43
|
* `.gitignore` lines that already ignore `apps/<app>/.env.local`. An explicit
|
|
32
44
|
* set, never a glob evaluation; `/.env.local` is absent because, anchored to
|
|
@@ -45,17 +57,20 @@ const NEXT_START_ENV_FILES = [
|
|
|
45
57
|
".env.production",
|
|
46
58
|
".env",
|
|
47
59
|
];
|
|
48
|
-
/** A line assigning
|
|
49
|
-
|
|
60
|
+
/** A line assigning `name`, with or without `export`; group 1 is the value. */
|
|
61
|
+
function envLine(name) {
|
|
62
|
+
return new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=(.*)$`);
|
|
63
|
+
}
|
|
50
64
|
/**
|
|
51
65
|
* The value `text` assigns — the LAST assignment, as dotenv reads it — or
|
|
52
66
|
* `undefined` when it assigns none. An empty value counts as none, exactly as
|
|
53
67
|
* `auth.ts` treats it.
|
|
54
68
|
*/
|
|
55
|
-
function assignedValue(text) {
|
|
69
|
+
function assignedValue(text, name) {
|
|
70
|
+
const pattern = envLine(name);
|
|
56
71
|
let value;
|
|
57
72
|
for (const line of text.split("\n")) {
|
|
58
|
-
const m =
|
|
73
|
+
const m = pattern.exec(line);
|
|
59
74
|
if (m !== null) {
|
|
60
75
|
const raw = (m[1] ?? "").trim();
|
|
61
76
|
const quoted = /^(["'])(.*)\1$/.exec(raw);
|
|
@@ -64,13 +79,13 @@ function assignedValue(text) {
|
|
|
64
79
|
}
|
|
65
80
|
return value === "" ? undefined : value;
|
|
66
81
|
}
|
|
67
|
-
/** The effective value for the app at `appDir`, by Next's precedence. */
|
|
68
|
-
function effectiveValue(appDir) {
|
|
69
|
-
for (const
|
|
70
|
-
const path = join(appDir,
|
|
82
|
+
/** The effective value of `name` for the app at `appDir`, by Next's precedence. */
|
|
83
|
+
function effectiveValue(appDir, name) {
|
|
84
|
+
for (const file of NEXT_START_ENV_FILES) {
|
|
85
|
+
const path = join(appDir, file);
|
|
71
86
|
if (!existsSync(path))
|
|
72
87
|
continue;
|
|
73
|
-
const value = assignedValue(readFileSync(path, "utf8"));
|
|
88
|
+
const value = assignedValue(readFileSync(path, "utf8"), name);
|
|
74
89
|
if (value !== undefined)
|
|
75
90
|
return value;
|
|
76
91
|
}
|
|
@@ -78,29 +93,40 @@ function effectiveValue(appDir) {
|
|
|
78
93
|
}
|
|
79
94
|
/**
|
|
80
95
|
* Give the scaffold at `dir` a `BETTER_AUTH_SECRET` (F1–F4 of the #6107
|
|
81
|
-
* spec)
|
|
82
|
-
*
|
|
96
|
+
* spec) and each app its loopback `BETTER_AUTH_URL` (#8620). Generates at
|
|
97
|
+
* most one secret, and only when no app already has one. Idempotent — a
|
|
98
|
+
* second call changes no byte.
|
|
83
99
|
*/
|
|
84
100
|
export function writeAuthSecretFile(dir, opts = {}) {
|
|
85
101
|
// Web always; Admin only when the scaffold carries `apps/admin`.
|
|
86
|
-
const apps = AUTH_SECRET_ENV_FILES.map((file) => ({
|
|
102
|
+
const apps = AUTH_SECRET_ENV_FILES.map((file, i) => ({
|
|
87
103
|
file: join(dir, file),
|
|
88
104
|
appDir: dirname(join(dir, file)),
|
|
105
|
+
url: AUTH_URL_DEFAULTS[i],
|
|
89
106
|
})).filter(({ appDir }, i) => i === 0 || existsSync(appDir));
|
|
90
107
|
// An existing value — web's first — is shared; a fresh one is generated
|
|
91
108
|
// only when no app has one.
|
|
92
109
|
let secret = apps
|
|
93
|
-
.map(({ appDir }) => effectiveValue(appDir))
|
|
110
|
+
.map(({ appDir }) => effectiveValue(appDir, AUTH_SECRET_ENV_VAR))
|
|
94
111
|
.find((v) => v !== undefined);
|
|
95
112
|
const secretFor = () => {
|
|
96
113
|
secret ??= (opts.randomBytes ?? nodeRandomBytes)(32).toString("base64url");
|
|
97
114
|
return secret;
|
|
98
115
|
};
|
|
99
|
-
for (const { file, appDir } of apps) {
|
|
100
|
-
|
|
116
|
+
for (const { file, appDir, url } of apps) {
|
|
117
|
+
const lines = [];
|
|
118
|
+
if (effectiveValue(appDir, AUTH_SECRET_ENV_VAR) === undefined) {
|
|
119
|
+
lines.push(`${AUTH_SECRET_ENV_VAR}=${secretFor()}`);
|
|
120
|
+
}
|
|
121
|
+
if (effectiveValue(appDir, AUTH_URL_ENV_VAR) === undefined) {
|
|
122
|
+
lines.push(`${AUTH_URL_ENV_VAR}=${url}`);
|
|
123
|
+
}
|
|
124
|
+
if (lines.length === 0)
|
|
101
125
|
continue;
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
let text = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
127
|
+
for (const line of lines)
|
|
128
|
+
text = appendLine(text, line);
|
|
129
|
+
writeOwnerOnly(file, text);
|
|
104
130
|
}
|
|
105
131
|
const ignorePath = join(dir, ".gitignore");
|
|
106
132
|
const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
|