create-ailk 0.27.0 → 0.28.1
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 +84 -7
- 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/chrome-readers.d.ts +21 -5
- package/dist/lib/chrome-readers.js +128 -20
- package/dist/lib/declared-env.d.ts +6 -0
- package/dist/lib/declared-env.js +2 -0
- package/dist/lib/env-catalog.js +20 -0
- package/dist/lib/import-graph.d.ts +16 -0
- package/dist/lib/import-graph.js +53 -0
- package/dist/lib/site-modules.d.ts +8 -0
- package/dist/lib/site-modules.js +34 -0
- package/dist/module-architecture.js +23 -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 +105 -9
- 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/template-record.d.ts +13 -4
- package/dist/template-record.js +8 -4
- package/dist/topology.d.ts +2 -1
- package/dist/topology.js +4 -2
- package/dist/update.d.ts +34 -2
- package/dist/update.js +395 -48
- 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 +5 -2
- package/templates/apps/api/.env.example +9 -5
- package/templates/apps/api/CLAUDE.md +14 -9
- 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/mcp/CLAUDE.md +18 -10
- package/templates/apps/web/.env.example +25 -11
- package/templates/apps/web/CLAUDE.md +45 -4
- 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]/(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 +157 -139
- 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 +224 -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 +237 -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 +204 -2
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +151 -33
- 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 +279 -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 +202 -10
- 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/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 +31 -106
- 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/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 +86 -30
- package/templates/apps/web/middleware.ts +171 -11
- package/templates/apps/web/next.config.mjs +211 -2
- package/templates/apps/web/package.json +1 -1
- 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/templates/tools/declared-env.ts +7 -0
- package/versions.json +48 -48
|
@@ -5,17 +5,33 @@ export interface UnreadChromeKnob {
|
|
|
5
5
|
/** Overlay-relative path of the reader file, with `/` separators. */
|
|
6
6
|
file: string;
|
|
7
7
|
}
|
|
8
|
+
/** One overlay file that reads a knob. */
|
|
9
|
+
export interface ChromeReader {
|
|
10
|
+
/** Overlay-relative path, with `/` separators. */
|
|
11
|
+
file: string;
|
|
12
|
+
/** Present when `file` reads the knob only through its imports: the files it reaches that read it directly. */
|
|
13
|
+
via?: string[];
|
|
14
|
+
}
|
|
8
15
|
/** The pattern that means "this text reads `knob`". */
|
|
9
16
|
export declare function chromeReadPattern(knob: string): RegExp;
|
|
10
17
|
/**
|
|
11
|
-
* knob → the overlay files that read it, sorted
|
|
12
|
-
* fails closed (throws, naming the directory)
|
|
13
|
-
* so a broken overlay can never read as
|
|
18
|
+
* knob → the overlay files that read it, sorted by file; one that reads it
|
|
19
|
+
* only through relative imports carries `via`. Scans `<templatesDir>/apps/web`; fails closed (throws, naming the directory)
|
|
20
|
+
* when it is absent or unreadable, so a broken overlay can never read as
|
|
21
|
+
* "nothing reads any knob".
|
|
14
22
|
*/
|
|
15
|
-
export declare function chromeKnobReaders(templatesDir: string): Map<string,
|
|
23
|
+
export declare function chromeKnobReaders(templatesDir: string): Map<string, ChromeReader[]>;
|
|
16
24
|
/**
|
|
17
25
|
* The `(knob, file)` pairs where the resolved chrome sets a non-default `knob`,
|
|
18
26
|
* the shipped overlay's `file` reads it, the site has its own `file`, and the
|
|
19
27
|
* site's `file` does not read it. A reader file the site lacks is skipped.
|
|
28
|
+
*
|
|
29
|
+
* A pair the overlay credits only through an import is also satisfied when
|
|
30
|
+
* some site file outside its `via` reads the knob directly: a site from before
|
|
31
|
+
* the read moved into the helper renders it in its own place (#10156). So the
|
|
32
|
+
* import credit adds a pair only for a knob the site reads nowhere but its
|
|
33
|
+
* helpers. It is per file, not per export: a site whose home page drops the
|
|
34
|
+
* helper import is named for every knob that helper reads — a refusal the
|
|
35
|
+
* operator passes with `--allow-unread-chrome`, never a silent miss.
|
|
20
36
|
*/
|
|
21
|
-
export declare function unreadChromeKnobs(resolved: ResolvedSiteChrome, readers: ReadonlyMap<string, readonly
|
|
37
|
+
export declare function unreadChromeKnobs(resolved: ResolvedSiteChrome, readers: ReadonlyMap<string, readonly ChromeReader[]>, siteDir: string): UnreadChromeKnob[];
|
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
* each knob — nothing is hard-coded, so a new reader file is picked up.
|
|
12
12
|
* - `unreadChromeKnobs` compares the site's own copy of each reader file.
|
|
13
13
|
*
|
|
14
|
-
* "
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* A file "reads `knob` directly" when its text matches
|
|
15
|
+
* `(?:siteChrome|chrome)\.<knob>\b`, and "reads" it when it or a file it
|
|
16
|
+
* imports by a relative path, at any depth, reads it directly — so the layout
|
|
17
|
+
* stays a reader of `innerPages` after #10106 moved that read into
|
|
18
|
+
* `lib/page-chrome.ts` (#10156). A file that reads a knob under another name
|
|
19
|
+
* (a destructured alias) is not recognised — accepted (spec § Clarifications,
|
|
20
|
+
* detection).
|
|
17
21
|
*/
|
|
18
22
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
19
|
-
import { join } from "node:path";
|
|
23
|
+
import { join, posix } from "node:path";
|
|
20
24
|
import { DEFAULT_SITE_CHROME, } from "../site-chrome.js";
|
|
21
25
|
/** The overlay tree the generated app files live under. */
|
|
22
26
|
const OVERLAY_WEB_DIR = ["apps", "web"];
|
|
@@ -31,41 +35,126 @@ function listSources(root, dir, out) {
|
|
|
31
35
|
for (const entry of readdirSync(join(root, dir), { withFileTypes: true })) {
|
|
32
36
|
const rel = `${dir}/${entry.name}`;
|
|
33
37
|
if (entry.isDirectory()) {
|
|
34
|
-
if (entry.name !== "__tests__"
|
|
38
|
+
if (entry.name !== "__tests__" &&
|
|
39
|
+
entry.name !== "node_modules" &&
|
|
40
|
+
!entry.name.startsWith(".")) {
|
|
35
41
|
listSources(root, rel, out);
|
|
42
|
+
}
|
|
36
43
|
}
|
|
37
44
|
else if (/\.tsx?$/.test(entry.name)) {
|
|
38
45
|
out.push(rel);
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
}
|
|
49
|
+
/** A relative module specifier in an import, export-from or dynamic import. */
|
|
50
|
+
const RELATIVE_IMPORT = /(?:\bfrom|\bimport)\s*\(?\s*["'](\.{1,2}\/[^"']+)["']/g;
|
|
51
|
+
function sourceTree(root) {
|
|
52
|
+
const texts = new Map();
|
|
53
|
+
const closures = new Map();
|
|
54
|
+
const importLists = new Map();
|
|
55
|
+
const text = (file) => {
|
|
56
|
+
if (!texts.has(file)) {
|
|
57
|
+
const abs = join(root, file);
|
|
58
|
+
texts.set(file, existsSync(abs) ? readFileSync(abs, "utf8") : null);
|
|
59
|
+
}
|
|
60
|
+
return texts.get(file);
|
|
61
|
+
};
|
|
62
|
+
/** The files `file` imports by a relative path that exist under the root; `./x.js` may name `x.ts`. */
|
|
63
|
+
const imports = (file) => {
|
|
64
|
+
const cached = importLists.get(file);
|
|
65
|
+
if (cached !== undefined)
|
|
66
|
+
return cached;
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const match of (text(file) ?? "").matchAll(RELATIVE_IMPORT)) {
|
|
69
|
+
const base = posix
|
|
70
|
+
.normalize(posix.join(posix.dirname(file), match[1]))
|
|
71
|
+
.replace(/\/$/, "");
|
|
72
|
+
if (base.startsWith(".."))
|
|
73
|
+
continue;
|
|
74
|
+
const stem = base.replace(/\.(?:js|jsx)$/, "");
|
|
75
|
+
const hit = [
|
|
76
|
+
base,
|
|
77
|
+
`${stem}.ts`,
|
|
78
|
+
`${stem}.tsx`,
|
|
79
|
+
`${stem}/index.ts`,
|
|
80
|
+
`${stem}/index.tsx`,
|
|
81
|
+
].find((candidate) => /\.tsx?$/.test(candidate) && text(candidate) !== null);
|
|
82
|
+
if (hit !== undefined)
|
|
83
|
+
out.push(hit);
|
|
84
|
+
}
|
|
85
|
+
importLists.set(file, out);
|
|
86
|
+
return out;
|
|
87
|
+
};
|
|
88
|
+
const reached = (file) => {
|
|
89
|
+
let closure = closures.get(file);
|
|
90
|
+
if (closure === undefined) {
|
|
91
|
+
closure = new Set();
|
|
92
|
+
const stack = [...imports(file)];
|
|
93
|
+
while (stack.length > 0) {
|
|
94
|
+
const next = stack.pop();
|
|
95
|
+
if (next === file || closure.has(next))
|
|
96
|
+
continue;
|
|
97
|
+
closure.add(next);
|
|
98
|
+
stack.push(...imports(next));
|
|
99
|
+
}
|
|
100
|
+
closures.set(file, closure);
|
|
101
|
+
}
|
|
102
|
+
return closure;
|
|
103
|
+
};
|
|
104
|
+
const readsDirectly = (file, pattern) => pattern.test(text(file) ?? "");
|
|
105
|
+
return {
|
|
106
|
+
text,
|
|
107
|
+
reached,
|
|
108
|
+
readsDirectly,
|
|
109
|
+
reads: (file, pattern) => readsDirectly(file, pattern) ||
|
|
110
|
+
[...reached(file)].some((dep) => readsDirectly(dep, pattern)),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
42
113
|
/**
|
|
43
|
-
* knob → the overlay files that read it, sorted
|
|
44
|
-
* fails closed (throws, naming the directory)
|
|
45
|
-
* so a broken overlay can never read as
|
|
114
|
+
* knob → the overlay files that read it, sorted by file; one that reads it
|
|
115
|
+
* only through relative imports carries `via`. Scans `<templatesDir>/apps/web`; fails closed (throws, naming the directory)
|
|
116
|
+
* when it is absent or unreadable, so a broken overlay can never read as
|
|
117
|
+
* "nothing reads any knob".
|
|
46
118
|
*/
|
|
47
119
|
export function chromeKnobReaders(templatesDir) {
|
|
48
120
|
const webDir = join(templatesDir, ...OVERLAY_WEB_DIR);
|
|
49
|
-
const
|
|
121
|
+
const direct = new Map();
|
|
122
|
+
const files = [];
|
|
123
|
+
const tree = sourceTree(templatesDir);
|
|
50
124
|
try {
|
|
51
125
|
if (!existsSync(webDir))
|
|
52
126
|
throw new Error("missing");
|
|
53
|
-
const files = [];
|
|
54
127
|
listSources(templatesDir, OVERLAY_WEB_DIR.join("/"), files);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
for (const match of
|
|
128
|
+
files.sort();
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
for (const match of tree
|
|
131
|
+
.text(file)
|
|
132
|
+
.matchAll(new RegExp(`${READ_HEAD}([A-Za-z_$][\\w$]*)\\b`, "g"))) {
|
|
58
133
|
const knob = match[1];
|
|
59
|
-
const list =
|
|
134
|
+
const list = direct.get(knob) ?? [];
|
|
60
135
|
if (!list.includes(file))
|
|
61
136
|
list.push(file);
|
|
62
|
-
|
|
137
|
+
direct.set(knob, list);
|
|
63
138
|
}
|
|
64
139
|
}
|
|
65
140
|
}
|
|
66
141
|
catch {
|
|
67
142
|
throw new Error(`cannot check chrome knobs: the overlay has no readable apps/web at ${webDir}. Reinstall create-ailk.`);
|
|
68
143
|
}
|
|
144
|
+
const readers = new Map();
|
|
145
|
+
for (const [knob, directFiles] of direct) {
|
|
146
|
+
const list = [];
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
if (directFiles.includes(file)) {
|
|
149
|
+
list.push({ file });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const via = directFiles.filter((reader) => tree.reached(file).has(reader));
|
|
153
|
+
if (via.length > 0)
|
|
154
|
+
list.push({ file, via });
|
|
155
|
+
}
|
|
156
|
+
readers.set(knob, list);
|
|
157
|
+
}
|
|
69
158
|
return readers;
|
|
70
159
|
}
|
|
71
160
|
/** A knob is set when it differs from its default; an optional knob (no default) when present. */
|
|
@@ -81,19 +170,38 @@ function isSet(resolved, knob) {
|
|
|
81
170
|
* The `(knob, file)` pairs where the resolved chrome sets a non-default `knob`,
|
|
82
171
|
* the shipped overlay's `file` reads it, the site has its own `file`, and the
|
|
83
172
|
* site's `file` does not read it. A reader file the site lacks is skipped.
|
|
173
|
+
*
|
|
174
|
+
* A pair the overlay credits only through an import is also satisfied when
|
|
175
|
+
* some site file outside its `via` reads the knob directly: a site from before
|
|
176
|
+
* the read moved into the helper renders it in its own place (#10156). So the
|
|
177
|
+
* import credit adds a pair only for a knob the site reads nowhere but its
|
|
178
|
+
* helpers. It is per file, not per export: a site whose home page drops the
|
|
179
|
+
* helper import is named for every knob that helper reads — a refusal the
|
|
180
|
+
* operator passes with `--allow-unread-chrome`, never a silent miss.
|
|
84
181
|
*/
|
|
85
182
|
export function unreadChromeKnobs(resolved, readers, siteDir) {
|
|
86
183
|
const out = [];
|
|
184
|
+
const site = sourceTree(siteDir);
|
|
185
|
+
let siteFiles;
|
|
87
186
|
for (const knob of Object.keys(resolved).sort()) {
|
|
88
187
|
if (!isSet(resolved, knob))
|
|
89
188
|
continue;
|
|
90
189
|
const pattern = chromeReadPattern(knob);
|
|
91
|
-
for (const file of readers.get(knob) ?? []) {
|
|
92
|
-
|
|
93
|
-
if (!existsSync(abs))
|
|
190
|
+
for (const { file, via } of readers.get(knob) ?? []) {
|
|
191
|
+
if (site.text(file) === null || site.reads(file, pattern))
|
|
94
192
|
continue;
|
|
95
|
-
if (
|
|
96
|
-
|
|
193
|
+
if (via !== undefined) {
|
|
194
|
+
if (siteFiles === undefined) {
|
|
195
|
+
siteFiles = [];
|
|
196
|
+
if (existsSync(join(siteDir, ...OVERLAY_WEB_DIR))) {
|
|
197
|
+
listSources(siteDir, OVERLAY_WEB_DIR.join("/"), siteFiles);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const elsewhere = siteFiles.some((other) => !via.includes(other) && site.readsDirectly(other, pattern));
|
|
201
|
+
if (elsewhere)
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
out.push({ knob, file });
|
|
97
205
|
}
|
|
98
206
|
}
|
|
99
207
|
return out;
|
|
@@ -109,6 +109,12 @@ export interface PlanEnvRowsOptions {
|
|
|
109
109
|
* with no reader and so no `scope`; a platform or refused name is skipped.
|
|
110
110
|
*/
|
|
111
111
|
declare?: readonly string[];
|
|
112
|
+
/**
|
|
113
|
+
* When set, only reads in these site-relative files get a row (#10173):
|
|
114
|
+
* `update` declares what the files it wrote read, never a name only the
|
|
115
|
+
* site's own code reads.
|
|
116
|
+
*/
|
|
117
|
+
onlyFiles?: ReadonlySet<string>;
|
|
112
118
|
}
|
|
113
119
|
/**
|
|
114
120
|
* `projectYamlText` with a row appended for every name the site at `root`
|
package/dist/lib/declared-env.js
CHANGED
|
@@ -904,6 +904,8 @@ export function planEnvRows(root, projectYamlText, opts) {
|
|
|
904
904
|
const refused = new Set(opts.topology === "spoke" ? SPOKE_REFUSED_ENV : []);
|
|
905
905
|
const files = new Map();
|
|
906
906
|
for (const read of collectSiteReads(root).reads) {
|
|
907
|
+
if (opts.onlyFiles !== undefined && !opts.onlyFiles.has(read.file))
|
|
908
|
+
continue;
|
|
907
909
|
if (isPlatformEnvName(read.name) || refused.has(read.name))
|
|
908
910
|
continue;
|
|
909
911
|
if (declared.has(read.name))
|
package/dist/lib/env-catalog.js
CHANGED
|
@@ -23,6 +23,14 @@ export const ENV_CATALOG = {
|
|
|
23
23
|
description: "This site's public origin as the MCP server sees it: the same-origin check for audits and the purchase return URLs.",
|
|
24
24
|
secret: false,
|
|
25
25
|
},
|
|
26
|
+
AILK_BOOKING_BOT_CHECK: {
|
|
27
|
+
description: 'Set to "off" to turn off the booking relay\'s Vercel BotID check (#9889) — the booking alone; AILK_BOT_CHECK=off turns every check off. It runs only on Vercel; unset, empty or any other value leaves it on.',
|
|
28
|
+
secret: false,
|
|
29
|
+
},
|
|
30
|
+
AILK_BOT_CHECK: {
|
|
31
|
+
description: 'Set to "off" to turn off the site\'s Vercel BotID check on its public writes — lead, newsletter, callback, flow, checkout and booking (#9255). On by default; it runs only on Vercel.',
|
|
32
|
+
secret: false,
|
|
33
|
+
},
|
|
26
34
|
AILK_CONTENT_ROOT: {
|
|
27
35
|
description: "Overrides the on-disk content root (absolute, or relative to the working directory). Unset, the MCP server tries <cwd>/content, then <cwd>/../../content; the web app defaults to <repo-root>/content.",
|
|
28
36
|
secret: false,
|
|
@@ -107,6 +115,10 @@ export const ENV_CATALOG = {
|
|
|
107
115
|
description: "Better Stack heartbeat URL pinged on cron and startup; unset turns the heartbeat off.",
|
|
108
116
|
secret: true,
|
|
109
117
|
},
|
|
118
|
+
CONTENT_ADAPTER: {
|
|
119
|
+
description: 'Where pages are read from: unset reads the MDX files under content/; "database" reads the database. The build checks Service categories (parent:) only for MDX content.',
|
|
120
|
+
secret: false,
|
|
121
|
+
},
|
|
110
122
|
CONTENT_REVALIDATE_SECRET: {
|
|
111
123
|
description: "The shared secret /api/revalidate checks the API's publish webhook against; the same value as the API's.",
|
|
112
124
|
secret: true,
|
|
@@ -163,6 +175,14 @@ export const ENV_CATALOG = {
|
|
|
163
175
|
description: "The API origin the browser reads on a hub. A spoke never sets it: its hub origin is server-only (AILK_API_URL).",
|
|
164
176
|
secret: false,
|
|
165
177
|
},
|
|
178
|
+
NEXT_PUBLIC_AILK_DOCS_PATH: {
|
|
179
|
+
description: "The docs' public path, baked at build from content/_site.mdx's docsPath; do not set by hand.",
|
|
180
|
+
secret: false,
|
|
181
|
+
},
|
|
182
|
+
NEXT_PUBLIC_AILK_SERVICES_PATH: {
|
|
183
|
+
description: "The services' public base, baked at build from content/_site.mdx's services.path; do not set by hand.",
|
|
184
|
+
secret: false,
|
|
185
|
+
},
|
|
166
186
|
NEXT_PUBLIC_AILK_LISTING_SITE_KEY: {
|
|
167
187
|
description: "The site's public listing key, inlined at build; it switches the listicle directory on. Public by design.",
|
|
168
188
|
secret: false,
|
|
@@ -27,3 +27,19 @@ export declare function resolveRelativeImport(fromFile: string, spec: string, ca
|
|
|
27
27
|
* @param seeds repo-relative, "/"-separated files the closure starts from.
|
|
28
28
|
*/
|
|
29
29
|
export declare function importClosure(repoRoot: string, roots: readonly string[], seeds: Iterable<string>): Set<string>;
|
|
30
|
+
/**
|
|
31
|
+
* The names a module imports from each specifier, textually (#10173): the
|
|
32
|
+
* members of a `{ … }` list (an `a as b` member names `a`, a `type a` member
|
|
33
|
+
* `a`) and `default` for a default import. A namespace import (`* as x`) and
|
|
34
|
+
* a side-effect import name nothing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function namedImportsOf(text: string): Array<{
|
|
37
|
+
specifier: string;
|
|
38
|
+
names: string[];
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Whether module `text` exports `name`, textually. Fails toward `true` — an
|
|
42
|
+
* `export *`, or a destructuring export, could export anything — so a caller
|
|
43
|
+
* that holds a file back on `false` never holds one that does export it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function exportsName(text: string, name: string): boolean;
|
package/dist/lib/import-graph.js
CHANGED
|
@@ -138,3 +138,56 @@ export function importClosure(repoRoot, roots, seeds) {
|
|
|
138
138
|
}
|
|
139
139
|
return closed;
|
|
140
140
|
}
|
|
141
|
+
const IDENT = "[A-Za-z_$][\\w$]*";
|
|
142
|
+
/** `import { a, b as c }` / `export { a } from` / `import X from` — the bound names per specifier. */
|
|
143
|
+
const NAMED_IMPORT = new RegExp(`\\b(?:import|export)\\s+(?:type\\s+)?(?:(${IDENT})\\s*,\\s*)?\\{([^}]*)\\}\\s*from\\s*["']([^"']+)["']` +
|
|
144
|
+
`|\\bimport\\s+(?:type\\s+)?(${IDENT})\\s+from\\s*["']([^"']+)["']`, "g");
|
|
145
|
+
/**
|
|
146
|
+
* The names a module imports from each specifier, textually (#10173): the
|
|
147
|
+
* members of a `{ … }` list (an `a as b` member names `a`, a `type a` member
|
|
148
|
+
* `a`) and `default` for a default import. A namespace import (`* as x`) and
|
|
149
|
+
* a side-effect import name nothing.
|
|
150
|
+
*/
|
|
151
|
+
export function namedImportsOf(text) {
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const m of text.matchAll(NAMED_IMPORT)) {
|
|
154
|
+
if (m[3] !== undefined) {
|
|
155
|
+
const names = (m[2] ?? "")
|
|
156
|
+
.split(",")
|
|
157
|
+
.map((member) => member.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim())
|
|
158
|
+
.filter((name) => new RegExp(`^${IDENT}$`).test(name));
|
|
159
|
+
if (m[1] !== undefined)
|
|
160
|
+
names.push("default");
|
|
161
|
+
out.push({ specifier: m[3], names });
|
|
162
|
+
}
|
|
163
|
+
else if (m[5] !== undefined) {
|
|
164
|
+
out.push({ specifier: m[5], names: ["default"] });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Whether module `text` exports `name`, textually. Fails toward `true` — an
|
|
171
|
+
* `export *`, or a destructuring export, could export anything — so a caller
|
|
172
|
+
* that holds a file back on `false` never holds one that does export it.
|
|
173
|
+
*/
|
|
174
|
+
export function exportsName(text, name) {
|
|
175
|
+
if (/\bexport\s*\*/.test(text))
|
|
176
|
+
return true;
|
|
177
|
+
if (/\bexport\s+(?:const|let|var)\s*[{[]/.test(text))
|
|
178
|
+
return true;
|
|
179
|
+
if (name === "default" && /\bexport\s+default\b/.test(text))
|
|
180
|
+
return true;
|
|
181
|
+
const declared = new RegExp(`\\bexport\\s+(?:declare\\s+)?(?:async\\s+)?(?:abstract\\s+)?` +
|
|
182
|
+
`(?:const\\s+enum|const|let|var|function\\s*\\*?|class|type|interface|enum|namespace)\\s*${name.replace(/\$/g, "\\$")}\\b`);
|
|
183
|
+
if (declared.test(text))
|
|
184
|
+
return true;
|
|
185
|
+
for (const m of text.matchAll(/\bexport\s*(?:type\s*)?\{([^}]*)\}/g)) {
|
|
186
|
+
for (const member of (m[1] ?? "").split(",")) {
|
|
187
|
+
const parts = member.trim().replace(/^type\s+/, "").split(/\s+as\s+/);
|
|
188
|
+
if ((parts[1] ?? parts[0])?.trim() === name)
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** `.ailk-site-manifest.json`'s `modules`, or undefined when the site has no manifest or it records none. */
|
|
2
|
+
export declare function readSiteManifestModules(targetDir: string): string[] | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Whether the site has dropped the `marketing` module: its manifest records a
|
|
5
|
+
* module list without it. A site with no manifest, or whose manifest records
|
|
6
|
+
* no list, keeps its public tree (today's behaviour).
|
|
7
|
+
*/
|
|
8
|
+
export declare function publicTreeRemoved(targetDir: string): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The site's own module list, as `.ailk-site-manifest.json` records it
|
|
3
|
+
* (#10040 D10). The one reader `sync-routes`, `update`, the module flags and
|
|
4
|
+
* the roster check share to answer "has this site dropped the marketing
|
|
5
|
+
* module?" — never the architecture header alone, because a bare
|
|
6
|
+
* `--modules <paid>` keep run writes a header without `marketing` while its
|
|
7
|
+
* public pages are all on disk, and such a site has no manifest.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
/** `.ailk-site-manifest.json`'s `modules`, or undefined when the site has no manifest or it records none. */
|
|
12
|
+
export function readSiteManifestModules(targetDir) {
|
|
13
|
+
const path = join(targetDir, ".ailk-site-manifest.json");
|
|
14
|
+
if (!existsSync(path))
|
|
15
|
+
return undefined;
|
|
16
|
+
try {
|
|
17
|
+
const doc = JSON.parse(readFileSync(path, "utf8"));
|
|
18
|
+
return Array.isArray(doc.modules) && doc.modules.every((m) => typeof m === "string")
|
|
19
|
+
? doc.modules
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether the site has dropped the `marketing` module: its manifest records a
|
|
28
|
+
* module list without it. A site with no manifest, or whose manifest records
|
|
29
|
+
* no list, keeps its public tree (today's behaviour).
|
|
30
|
+
*/
|
|
31
|
+
export function publicTreeRemoved(targetDir) {
|
|
32
|
+
const modules = readSiteManifestModules(targetDir);
|
|
33
|
+
return modules !== undefined && !modules.includes("marketing");
|
|
34
|
+
}
|
|
@@ -1362,16 +1362,15 @@ export const MODULE_SLICES = {
|
|
|
1362
1362
|
// routes, named exactly as `surfaces.ts`'s ROUTE_SINCE keys name them.
|
|
1363
1363
|
// ROWS AND SEED ONLY: unlike docs-blog and scheduling above, this module's
|
|
1364
1364
|
// route code is deliberately NOT in `ownedFiles` (see the comment inside
|
|
1365
|
-
// that array).
|
|
1366
|
-
// `templates/` snapshot, retained because the `flows` surface is enabled
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1373
|
-
// of
|
|
1374
|
-
// is a property of the module, not of the contribution.
|
|
1365
|
+
// that array). All three rows' code reaches a scaffold from the
|
|
1366
|
+
// `templates/` snapshot, retained because the `flows` surface is enabled.
|
|
1367
|
+
// The results row's code was `commerce`'s until #6786, so a Pro scaffold
|
|
1368
|
+
// without commerce 404'd a finished recommending flow; it now ships here,
|
|
1369
|
+
// and commerce's bundle adds only the checkout half
|
|
1370
|
+
// (`commerce.ownedFiles`: `results/[payload]/layout.tsx` and
|
|
1371
|
+
// `checkout.actions.ts`). Declaring the rows here changes none of it: a
|
|
1372
|
+
// contribution is data, and which delivery shape carries the code is a
|
|
1373
|
+
// property of the module, not of the contribution.
|
|
1375
1374
|
pageContributions: [
|
|
1376
1375
|
{ route: "/flows/[slug]" },
|
|
1377
1376
|
{ route: "/flows/[slug]/start" },
|
|
@@ -1500,7 +1499,8 @@ export const MODULE_SLICES = {
|
|
|
1500
1499
|
// filter (`isSpokeExcluded`), which drops every destination under
|
|
1501
1500
|
// apps/api or database and the web files that call the hub's own
|
|
1502
1501
|
// checkout (`SPOKE_HUB_CHECKOUT_SEEDS`: flows/[slug]/checkout.actions.ts,
|
|
1503
|
-
// the flow results
|
|
1502
|
+
// the flow results directory — the results layout among it — and
|
|
1503
|
+
// dev/purchase, spoke-intake Rule 3). What lands
|
|
1504
1504
|
// is the scaffold-commerce skill, the commerce-boundary rule and the
|
|
1505
1505
|
// @working-theory/ui-commerce dependency (`patches` below). A spoke sells
|
|
1506
1506
|
// through its hub's Offerings buy route (#9142); its remaining checkout
|
|
@@ -1583,10 +1583,13 @@ export const MODULE_SLICES = {
|
|
|
1583
1583
|
// + the slice's own re-export entry point) moved to
|
|
1584
1584
|
// packages-paid/ui-commerce; the package is delivered to a keyed
|
|
1585
1585
|
// scaffold via `patches` below, not via `ownedFiles`/`installMap` (D5).
|
|
1586
|
+
// #6786 — the flow results page is signal-collectors' (it rides the
|
|
1587
|
+
// `flows` surface's directory, see that slice); commerce owns only its
|
|
1588
|
+
// checkout half: Pay's action and the results layout that provides the
|
|
1589
|
+
// price card + Pay through the base tree's `checkout-seam.ts`. Without
|
|
1590
|
+
// them the page renders, offering Book a Call for every fit.
|
|
1586
1591
|
"apps/web/app/[locale]/flows/[slug]/checkout.actions.ts",
|
|
1587
|
-
"apps/web/app/[locale]/flows/[slug]/results/[payload]/
|
|
1588
|
-
"apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx",
|
|
1589
|
-
"apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx",
|
|
1592
|
+
"apps/web/app/[locale]/flows/[slug]/results/[payload]/layout.tsx",
|
|
1590
1593
|
"apps/web/app/[locale]/dev/purchase/page.tsx",
|
|
1591
1594
|
"apps/web/app/[locale]/dev/purchase/error.tsx",
|
|
1592
1595
|
"apps/web/app/[locale]/dev/purchase/purchase.actions.ts",
|
|
@@ -1823,6 +1826,11 @@ export function coOwnedModuleDestinations() {
|
|
|
1823
1826
|
// named by one file. A scaffold is not a git repository. Only those two
|
|
1824
1827
|
// cases moved there; `connection.test.ts` and
|
|
1825
1828
|
// `checkout-session-record.test.ts` keep the stores' coverage and ship.
|
|
1829
|
+
// - the content hubs' `lib/__tests__/hub-children.test.tsx` (#10121) — it
|
|
1830
|
+
// imports the hub page of thirteen route families across surfaces, and a
|
|
1831
|
+
// scaffold carries only the families its surfaces select, so a gated
|
|
1832
|
+
// scaffold would ship it with dangling imports. Its coverage of the
|
|
1833
|
+
// generated child cards runs in this monorepo only.
|
|
1826
1834
|
export const EXCLUDE_TEMPLATE_DIRS = [
|
|
1827
1835
|
"apps/web/__tests__",
|
|
1828
1836
|
"apps/web/app/__tests__",
|
|
@@ -1847,6 +1855,7 @@ export const EXCLUDE_TEMPLATE_DIRS = [
|
|
|
1847
1855
|
"apps/mcp/__tests__/parity.test.ts",
|
|
1848
1856
|
"apps/web/app/[locale]/pricing/__tests__/monorepo-only.test.tsx",
|
|
1849
1857
|
"apps/web/app/[locale]/pricing/__tests__/__snapshots__/monorepo-only.test.tsx.snap",
|
|
1858
|
+
"apps/web/lib/__tests__/hub-children.test.tsx",
|
|
1850
1859
|
];
|
|
1851
1860
|
export function paidModuleTemplateExclusions(snapshotRoots, repoRoot) {
|
|
1852
1861
|
const roots = new Set(snapshotRoots);
|
package/dist/paid-sections.d.ts
CHANGED
|
@@ -26,7 +26,24 @@ export declare function renderPaidSections(present: ReadonlySet<string>): string
|
|
|
26
26
|
*/
|
|
27
27
|
export declare function renderModuleFlags(flags: {
|
|
28
28
|
schedulingPages: boolean;
|
|
29
|
+
/** The `marketing` module is in the architecture header (#10040 D6). Absent → `true`. */
|
|
30
|
+
marketingPages?: boolean;
|
|
31
|
+
/** The `docs-blog` module is in the architecture header (#10040 D6). Absent → `false`. */
|
|
32
|
+
docsBlogPages?: boolean;
|
|
29
33
|
}): string;
|
|
34
|
+
/**
|
|
35
|
+
* The `marketing` / `docs-blog` presence the flags render (#10040 D6).
|
|
36
|
+
* `marketing` is read from the site's manifest (`lib/site-modules.ts`): a
|
|
37
|
+
* site with no manifest keeps its public tree, whatever its header says — a
|
|
38
|
+
* bare `--modules <paid>` keep run writes a header without `marketing` while
|
|
39
|
+
* every public page is on disk. `docs-blog` is read from the architecture
|
|
40
|
+
* header, which lists it on every site that carries it; an unreadable header
|
|
41
|
+
* reads as absent.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readPublicPageFlags(targetDir: string): {
|
|
44
|
+
marketingPages: boolean;
|
|
45
|
+
docsBlogPages: boolean;
|
|
46
|
+
};
|
|
30
47
|
/**
|
|
31
48
|
* Write the module-presence flags into a scaffold from its own
|
|
32
49
|
* `apps/web/package.json` (#8843 D10) — never copied from a template, so a
|
package/dist/paid-sections.js
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
27
27
|
import { dirname, join } from "node:path";
|
|
28
|
+
import { readArchitectureHeader } from "./add-module-core.js";
|
|
29
|
+
import { readSiteManifestModules } from "./lib/site-modules.js";
|
|
28
30
|
import { MODULE_SLICES } from "./module-architecture.js";
|
|
29
31
|
import { RECOGNIZED_MODULES } from "./parse-args.js";
|
|
30
32
|
/** Scaffold-relative path of the generated registry. */
|
|
@@ -119,15 +121,51 @@ const MODULE_FLAGS_HEADER = `// modules.generated.ts — which module pages this
|
|
|
119
121
|
* tree does not carry.
|
|
120
122
|
*/
|
|
121
123
|
export function renderModuleFlags(flags) {
|
|
124
|
+
const marketingPages = flags.marketingPages ?? true;
|
|
125
|
+
const docsBlogPages = flags.docsBlogPages ?? false;
|
|
122
126
|
const lines = [];
|
|
127
|
+
// The line-1 marker stays keyed on the scheduling pages alone (#10040 D6).
|
|
123
128
|
if (flags.schedulingPages)
|
|
124
129
|
lines.push("// oss: false");
|
|
125
130
|
lines.push(MODULE_FLAGS_HEADER.trimEnd());
|
|
126
131
|
lines.push("");
|
|
127
132
|
lines.push("/** The scheduling module's booking pages (/meet, /private) exist. */");
|
|
128
133
|
lines.push(`export const SCHEDULING_PAGES: boolean = ${String(flags.schedulingPages)};`);
|
|
134
|
+
lines.push("");
|
|
135
|
+
lines.push("/** The marketing module's public pages exist (#10040): false on a hub. */");
|
|
136
|
+
lines.push(`export const MARKETING_PAGES: boolean = ${String(marketingPages)};`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
lines.push("/** The docs-blog module's /docs and /blog pages exist (#10040). */");
|
|
139
|
+
lines.push(`export const DOCS_BLOG_PAGES: boolean = ${String(docsBlogPages)};`);
|
|
140
|
+
lines.push("");
|
|
141
|
+
lines.push("/** Any public page exists: robots, sitemap, llms.txt and the noindex header read this (#10040 D6/D7). */");
|
|
142
|
+
lines.push(`export const PUBLIC_PAGES: boolean = ${String(marketingPages || docsBlogPages)};`);
|
|
129
143
|
return `${lines.join("\n")}\n`;
|
|
130
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* The `marketing` / `docs-blog` presence the flags render (#10040 D6).
|
|
147
|
+
* `marketing` is read from the site's manifest (`lib/site-modules.ts`): a
|
|
148
|
+
* site with no manifest keeps its public tree, whatever its header says — a
|
|
149
|
+
* bare `--modules <paid>` keep run writes a header without `marketing` while
|
|
150
|
+
* every public page is on disk. `docs-blog` is read from the architecture
|
|
151
|
+
* header, which lists it on every site that carries it; an unreadable header
|
|
152
|
+
* reads as absent.
|
|
153
|
+
*/
|
|
154
|
+
export function readPublicPageFlags(targetDir) {
|
|
155
|
+
const siteModules = readSiteManifestModules(targetDir);
|
|
156
|
+
const marketingPages = siteModules === undefined || siteModules.includes("marketing");
|
|
157
|
+
const archPath = join(targetDir, "architecture.yaml");
|
|
158
|
+
let docsBlogPages = false;
|
|
159
|
+
if (existsSync(archPath)) {
|
|
160
|
+
try {
|
|
161
|
+
docsBlogPages = readArchitectureHeader(readFileSync(archPath, "utf8")).modules.includes("docs-blog");
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
docsBlogPages = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { marketingPages, docsBlogPages };
|
|
168
|
+
}
|
|
131
169
|
/** The web app's `dependencies`, or `undefined` for a scaffold with no web app. */
|
|
132
170
|
function webDependencies(targetDir, writing) {
|
|
133
171
|
const pkgPath = join(targetDir, "apps/web/package.json");
|
|
@@ -158,6 +196,7 @@ export function writeModuleFlags(targetDir) {
|
|
|
158
196
|
const before = existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
159
197
|
const next = renderModuleFlags({
|
|
160
198
|
schedulingPages: Object.hasOwn(deps, SCHEDULING_PAGES_PACKAGE),
|
|
199
|
+
...readPublicPageFlags(targetDir),
|
|
161
200
|
});
|
|
162
201
|
if (before === next)
|
|
163
202
|
return "unchanged";
|
package/dist/programmatic.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { type AddModuleResult } from "./add-module.js";
|
|
|
19
19
|
import { type SyncClaudeMdResult } from "./claude-md.js";
|
|
20
20
|
import { CATALOG_TARGET_PATH } from "./component-catalog.js";
|
|
21
21
|
import { type ModuleName } from "./parse-args.js";
|
|
22
|
+
import { type RemoveModuleResult } from "./remove-module.js";
|
|
22
23
|
import { type TemplateLayout } from "./site-chrome.js";
|
|
23
24
|
import { type SyncRoutesResult } from "./sync-routes.js";
|
|
24
25
|
import { type StarterShape } from "./template-manifest.js";
|
|
@@ -367,6 +368,21 @@ export interface AddModuleOptions {
|
|
|
367
368
|
* 4. the fetched bundle is merged under the plus-config policy.
|
|
368
369
|
*/
|
|
369
370
|
export declare function addModule(opts: AddModuleOptions): Promise<AddModuleResult>;
|
|
371
|
+
export interface RemoveModuleCliOptions {
|
|
372
|
+
/** The already-scaffolded site (absolute or relative). */
|
|
373
|
+
targetDir: string;
|
|
374
|
+
/** The module to remove (validated upstream by `parseRemoveModuleArgs`). */
|
|
375
|
+
moduleName: ModuleName;
|
|
376
|
+
/** Delete the site's own edits in the removal set too. */
|
|
377
|
+
force?: boolean;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Take a module's public pages off an already-scaffolded site in place
|
|
381
|
+
* (`create-ailk remove-module`, #10040) — the thin package-rooted wrapper
|
|
382
|
+
* around `remove-module.ts`'s `removeModuleFromScaffold`, the `addModule`
|
|
383
|
+
* split. Every refusal is the core's and throws before the first write.
|
|
384
|
+
*/
|
|
385
|
+
export declare function removeModule(opts: RemoveModuleCliOptions): RemoveModuleResult;
|
|
370
386
|
/** Re-exported so the CLI can name the file it wrote without duplicating it. */
|
|
371
387
|
export { CATALOG_TARGET_PATH };
|
|
372
388
|
/**
|