create-ailk 0.14.0 → 0.15.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/component-catalog.json +3 -3
- package/dist/add-module.d.ts +7 -0
- package/dist/add-module.js +34 -2
- package/dist/add-route.js +18 -2
- package/dist/apply-design.d.ts +12 -1
- package/dist/apply-design.js +36 -4
- package/dist/cli.js +23 -3
- package/dist/derive-architecture.d.ts +15 -2
- package/dist/derive-architecture.js +34 -6
- package/dist/design-package.js +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/chrome-readers.d.ts +21 -0
- package/dist/lib/chrome-readers.js +100 -0
- package/dist/lib/extract-module-bundle.d.ts +11 -0
- package/dist/lib/extract-module-bundle.js +36 -6
- package/dist/lib/license-key-files.d.ts +6 -1
- package/dist/lib/license-key-files.js +9 -4
- package/dist/module-architecture.d.ts +9 -0
- package/dist/module-architecture.js +6 -0
- package/dist/parse-args.d.ts +22 -0
- package/dist/parse-args.js +73 -0
- package/dist/programmatic.d.ts +20 -0
- package/dist/programmatic.js +110 -9
- package/dist/site-chrome.d.ts +4 -0
- package/dist/site-chrome.js +34 -3
- package/dist/site-manifest.d.ts +12 -5
- package/dist/site-manifest.js +25 -11
- package/dist/sync-routes.js +11 -2
- package/dist/topology.d.ts +131 -0
- package/dist/topology.js +533 -0
- package/package.json +3 -3
- package/site-templates/designs/minimalism/DESIGN.md +3 -3
- package/templates/apps/api/CLAUDE.md +4 -2
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +5 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +10 -0
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +114 -1
- package/templates/apps/api/src/lib/__tests__/site.test.ts +54 -0
- package/templates/apps/api/src/lib/agent-store.ts +95 -3
- package/templates/apps/api/src/lib/site.ts +37 -0
- package/templates/apps/api/src/lib/spoke-store.ts +18 -2
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +4 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +22 -0
- package/templates/apps/api/src/middleware/auth.ts +3 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +23 -0
- package/templates/apps/api/src/openapi/spec.ts +38 -2
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +50 -2
- package/templates/apps/api/src/routes/checkout/sessions.ts +7 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +275 -0
- package/templates/apps/api/src/routes/leads/index.ts +69 -6
- package/templates/apps/api/src/routes/webhooks/README.md +39 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +366 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +61 -1
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +199 -0
- package/templates/apps/api/src/services/default-tenant.ts +41 -7
- package/templates/apps/api/src/services/lead-routing.ts +148 -29
- package/templates/apps/api/src/services/public-site-resolver.ts +48 -0
- package/templates/apps/mcp/__tests__/index.test.ts +5 -5
- package/templates/apps/mcp/src/index.ts +6 -4
- package/templates/apps/mcp/src/lib/content-adapter.ts +2 -2
- package/templates/apps/mcp/src/lib/topology.generated.ts +24 -0
- package/templates/apps/web/CLAUDE.md +1 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +66 -14
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +105 -11
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +12 -8
- package/templates/apps/web/app/[locale]/layout.tsx +71 -19
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +5 -2
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +89 -1
- package/templates/apps/web/app/openapi.json/route.ts +69 -1
- package/templates/apps/web/instrumentation.ts +7 -4
- package/templates/apps/web/lib/__tests__/closing-footer-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +290 -0
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +52 -0
- package/templates/apps/web/lib/closing-footer-path.ts +50 -0
- package/templates/apps/web/lib/relay.ts +206 -2
- package/templates/apps/web/lib/site-brand.tsx +22 -1
- package/templates/apps/web/lib/site-chrome.ts +3 -0
- package/templates/apps/web/lib/topology.generated.ts +23 -0
- package/templates/apps/web/package.json +1 -1
- package/templates/database/CHANGELOG.md +25 -0
- package/templates/database/__tests__/agents.db.test.ts +65 -0
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +92 -0
- package/templates/database/auth/schema.prisma +23 -0
- package/templates/database/inbox/schema.prisma +13 -0
- package/templates/database/jest.config.cjs +7 -11
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +35 -0
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +20 -0
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +33 -0
- package/templates/database/package.json +1 -1
- package/versions.json +34 -33
package/component-catalog.json
CHANGED
|
@@ -5235,8 +5235,7 @@
|
|
|
5235
5235
|
}
|
|
5236
5236
|
},
|
|
5237
5237
|
"required": [
|
|
5238
|
-
"title"
|
|
5239
|
-
"description"
|
|
5238
|
+
"title"
|
|
5240
5239
|
]
|
|
5241
5240
|
}
|
|
5242
5241
|
},
|
|
@@ -7378,7 +7377,8 @@
|
|
|
7378
7377
|
"type": "string",
|
|
7379
7378
|
"enum": [
|
|
7380
7379
|
"default",
|
|
7381
|
-
"sm"
|
|
7380
|
+
"sm",
|
|
7381
|
+
"lg"
|
|
7382
7382
|
]
|
|
7383
7383
|
},
|
|
7384
7384
|
"surface": {
|
package/dist/add-module.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ import { type ModulePatch } from "./module-architecture.js";
|
|
|
4
4
|
import { type ModuleName } from "./parse-args.js";
|
|
5
5
|
import type { MarkerMap, WorkspaceVersionMap } from "./substitute.js";
|
|
6
6
|
import { type SyncRoutesResult } from "./sync-routes.js";
|
|
7
|
+
import { type Topology } from "./topology.js";
|
|
8
|
+
/**
|
|
9
|
+
* The site's topology for add-module (#7426 D13): an unreadable manifest is a
|
|
10
|
+
* refusal this command names (exit 2), before the first write — the same
|
|
11
|
+
* posture as its unreadable-roster refusal.
|
|
12
|
+
*/
|
|
13
|
+
export declare function addModuleSiteTopology(targetDir: string): Topology;
|
|
7
14
|
export { AddModuleRefusedError, readArchitectureHeader };
|
|
8
15
|
/**
|
|
9
16
|
* One report row: a bundle file written, patched or skipped, or a seed the
|
package/dist/add-module.js
CHANGED
|
@@ -39,6 +39,22 @@ import { ParseError, parseModules } from "./parse-args.js";
|
|
|
39
39
|
import { SITE_ROSTER_PATH, moduleRosterRows, planRosterAppend, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
|
|
40
40
|
import { MODULE_SURFACES, NAV_CONFIG_PATH, readNavConfigStamp, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
|
|
41
41
|
import { syncRoutes } from "./sync-routes.js";
|
|
42
|
+
import { TopologyError, isSpokeExcluded, readSiteTopology, spokeModuleRefusal, } from "./topology.js";
|
|
43
|
+
/**
|
|
44
|
+
* The site's topology for add-module (#7426 D13): an unreadable manifest is a
|
|
45
|
+
* refusal this command names (exit 2), before the first write — the same
|
|
46
|
+
* posture as its unreadable-roster refusal.
|
|
47
|
+
*/
|
|
48
|
+
export function addModuleSiteTopology(targetDir) {
|
|
49
|
+
try {
|
|
50
|
+
return readSiteTopology(targetDir);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err instanceof TopologyError)
|
|
54
|
+
throw new AddModuleRefusedError(err.message);
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
42
58
|
export { AddModuleRefusedError, readArchitectureHeader };
|
|
43
59
|
/** `package.json` paths a `json` patch may write under the plus-config policy. */
|
|
44
60
|
const PACKAGE_JSON_PATCH_KEYS = new Set([
|
|
@@ -86,6 +102,17 @@ export function checkPlusConfigPatches(patches) {
|
|
|
86
102
|
export async function applyModuleToScaffold(opts) {
|
|
87
103
|
const targetAbs = resolve(opts.targetDir);
|
|
88
104
|
const archPath = join(targetAbs, "architecture.yaml");
|
|
105
|
+
// #7426 D13 — a spoke stays a spoke: a module it does not admit is refused
|
|
106
|
+
// before the first write, and an admitted one lands through the spoke
|
|
107
|
+
// filter. `mcp` follows whether the spoke still carries apps/mcp.
|
|
108
|
+
const spoke = addModuleSiteTopology(targetAbs) === "spoke"
|
|
109
|
+
? { mcp: existsSync(join(targetAbs, "apps", "mcp")) }
|
|
110
|
+
: undefined;
|
|
111
|
+
if (spoke !== undefined) {
|
|
112
|
+
const refusal = spokeModuleRefusal(opts.moduleName);
|
|
113
|
+
if (refusal !== undefined)
|
|
114
|
+
throw new AddModuleRefusedError(refusal);
|
|
115
|
+
}
|
|
89
116
|
const current = readArchitectureHeader(readFileSync(archPath, "utf8"));
|
|
90
117
|
const modules = normalizeModules([...current.modules, opts.moduleName]);
|
|
91
118
|
// Resolved before the first write: a header surface that is unknown, or
|
|
@@ -112,6 +139,9 @@ export async function applyModuleToScaffold(opts) {
|
|
|
112
139
|
markers: opts.markers,
|
|
113
140
|
existingFilePolicy: "skip-existing",
|
|
114
141
|
checkPatches: checkPlusConfigPatches,
|
|
142
|
+
...(spoke !== undefined
|
|
143
|
+
? { skip: (destination) => isSpokeExcluded(destination, spoke) }
|
|
144
|
+
: {}),
|
|
115
145
|
});
|
|
116
146
|
const routes = syncRoutes({
|
|
117
147
|
targetDir: targetAbs,
|
|
@@ -123,10 +153,12 @@ export async function applyModuleToScaffold(opts) {
|
|
|
123
153
|
// #6107 D8 — a module that brings `apps/admin` brings its auth hard-fail;
|
|
124
154
|
// the Admin app gets the web app's effective secret, and a secret already
|
|
125
155
|
// held in any Next-loaded env file is never shadowed.
|
|
126
|
-
|
|
156
|
+
// A spoke has no sign-in (#7426 D13), so it gets no secret.
|
|
157
|
+
if (spoke === undefined)
|
|
158
|
+
writeAuthSecretFile(targetAbs);
|
|
127
159
|
// The same composition `syncArchitectureSync` writes — a pure function of
|
|
128
160
|
// the module set, so an unchanged set rewrites identical bytes.
|
|
129
|
-
writeFileSync(archPath, deriveArchitecture(modules, surfaces));
|
|
161
|
+
writeFileSync(archPath, deriveArchitecture(modules, surfaces, spoke !== undefined ? { topology: "spoke", mcp: spoke.mcp } : {}));
|
|
130
162
|
// #7536 D3 — the paid-sections registry follows `apps/web/package.json`,
|
|
131
163
|
// which the bundle's `patches` just edited. Generated and always
|
|
132
164
|
// overwritten, like `architecture.yaml` above.
|
package/dist/add-route.js
CHANGED
|
@@ -39,6 +39,7 @@ import { substituteMarkers } from "./substitute.js";
|
|
|
39
39
|
import { GATEABLE_SURFACES, MODULE_SURFACES, parseSurfaces, resolveSurfaces, } from "./surfaces.js";
|
|
40
40
|
import { syncRoutes } from "./sync-routes.js";
|
|
41
41
|
import { BASE_LAYER_DIR } from "./template-manifest.js";
|
|
42
|
+
import { TopologyError, readSiteTopology } from "./topology.js";
|
|
42
43
|
/** A refusal the CLI exits 2 on, thrown before anything is written. */
|
|
43
44
|
export class AddRouteRefusedError extends Error {
|
|
44
45
|
exitCode = 2;
|
|
@@ -64,6 +65,20 @@ export function addRouteToScaffold(opts) {
|
|
|
64
65
|
if (!existsSync(archPath)) {
|
|
65
66
|
throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
|
|
66
67
|
}
|
|
68
|
+
// #7426 D13 — a spoke stays a spoke. Read before the first write: an
|
|
69
|
+
// unreadable manifest is a refusal this command names (exit 2).
|
|
70
|
+
let spoke;
|
|
71
|
+
try {
|
|
72
|
+
spoke =
|
|
73
|
+
readSiteTopology(targetAbs) === "spoke"
|
|
74
|
+
? { mcp: existsSync(join(targetAbs, "apps", "mcp")) }
|
|
75
|
+
: undefined;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (err instanceof TopologyError)
|
|
79
|
+
throw new AddRouteRefusedError(err.message);
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
67
82
|
const route = canonicalRoute(opts.route);
|
|
68
83
|
const owner = Object.entries(MODULE_SURFACES).find(([, surfaces]) => surfaces.includes(route))?.[0];
|
|
69
84
|
let current;
|
|
@@ -106,8 +121,9 @@ export function addRouteToScaffold(opts) {
|
|
|
106
121
|
if (navRow !== undefined)
|
|
107
122
|
report.push(navRow);
|
|
108
123
|
// Last: the declared header is what makes a rerun a no-op, so it is
|
|
109
|
-
// written only once every other step has landed.
|
|
110
|
-
|
|
124
|
+
// written only once every other step has landed. A spoke's `syncRoutes`
|
|
125
|
+
// call above already skipped what it lacks (#7426 D13).
|
|
126
|
+
writeFileSync(archPath, deriveArchitecture(current.modules, surfaces, spoke !== undefined ? { topology: "spoke", mcp: spoke.mcp } : {}));
|
|
111
127
|
return { targetDir: targetAbs, route, surfaces, report, routes };
|
|
112
128
|
}
|
|
113
129
|
/** The canonical id for one route argument, refusing an unknown one. */
|
package/dist/apply-design.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type HomeSeedOutcome } from "./design-home.js";
|
|
2
|
+
import { type UnreadChromeKnob } from "./lib/chrome-readers.js";
|
|
2
3
|
export interface ApplyDesignOptions {
|
|
3
4
|
/** The design: a shipped name, or a directory holding a DESIGN.md. */
|
|
4
5
|
design: string;
|
|
@@ -10,6 +11,8 @@ export interface ApplyDesignOptions {
|
|
|
10
11
|
/** The overlay's `templates/` (#7743 D7) — where a treeless shape's home lives. Default: `templates` beside `siteTemplatesDir`. */
|
|
11
12
|
templatesDir?: string;
|
|
12
13
|
catalogPath: string;
|
|
14
|
+
/** #7781 — apply the design even when a reader file of the site ignores a chrome knob it sets; the pairs are reported instead of refused. */
|
|
15
|
+
allowUnreadChrome?: boolean;
|
|
13
16
|
/** Where a relative design path resolves from. Default process.cwd(). */
|
|
14
17
|
cwd?: string;
|
|
15
18
|
/** Read for AILK_LICENSE_KEY only (D6). Default process.env. */
|
|
@@ -60,13 +63,21 @@ export interface ApplyDesignResult {
|
|
|
60
63
|
fonts?: string[];
|
|
61
64
|
/** #7720 D4.2 — faces were written, but the site's layout.tsx does not read lib/site-fonts. */
|
|
62
65
|
fontsUnwired?: true;
|
|
66
|
+
/** #7781 — chrome knobs the design set that a reader file of the site does not read; present only when non-empty, which only `allowUnreadChrome` lets through. */
|
|
67
|
+
unreadChrome?: UnreadChromeKnob[];
|
|
68
|
+
}
|
|
69
|
+
/** #7781 — thrown before any write when the design sets a chrome knob that a reader file of the site does not read. */
|
|
70
|
+
export declare class UnreadChromeError extends Error {
|
|
71
|
+
readonly unread: readonly UnreadChromeKnob[];
|
|
72
|
+
constructor(unread: readonly UnreadChromeKnob[]);
|
|
63
73
|
}
|
|
64
74
|
export interface ApplyDesignArgs {
|
|
65
75
|
design: string | undefined;
|
|
66
76
|
target: string | undefined;
|
|
77
|
+
allowUnreadChrome: boolean;
|
|
67
78
|
help: boolean;
|
|
68
79
|
}
|
|
69
|
-
export declare const APPLY_DESIGN_HELP = "create-ailk apply-design \u2014 make an existing site wear a design.\n\nUsage:\n create-ailk apply-design <name> [--target <site-dir>]\n create-ailk apply-design <path> [--target <site-dir>]\n\nArguments:\n <name|path> A shipped design (standard, minimalism), or a\n directory holding a DESIGN.md. A relative path\n resolves from the current directory.\n\nOptions:\n --target <site-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n --help, -h Print this message.\n\nWrites only the look: DESIGN.md (replaced whole by the design's, keeping the\nsite's Source template and Version lines), the theme: block of\ncontent/_site.mdx, the generated apps/web/lib/site-chrome.ts, the generated\napps/web/lib/site-fonts.ts, the look fields of sections the site's pages\nalready place, and the design/layout/theme record in .ailk-site-manifest.json.\nNever adds, removes or renames a route or page, never touches a content field\n(except a placeholder home's sections, below) or the brand: block, and leaves\nevery other page's structured data unchanged.\n\nA design may seed the placeholder home's sections \u2014 its home arc \u2014 while they\nare still a placeholder: the shape's own, or a design's arc of them. It never\nchanges a home whose sections were edited; it reports it and leaves it. A\nseeded home takes the design's looks; look values on the sections it replaced\nare not kept. A design that ships no arc restores the shape's own sections on\na home that still wears another design's untouched arc.\n\nA design other than standard needs a Pro or Pro Plus key: the site's own\nailk.key (put one there with create-ailk license set <key>), else\nAILK_LICENSE_KEY. A malformed package, a preset the validator refuses or a\nmissing key is refused before any file is written.\n\nA preset sets the look fields it names and never unsets one: a field it does\nnot name keeps whatever value the page has. Running it again changes nothing.\n";
|
|
80
|
+
export declare const APPLY_DESIGN_HELP = "create-ailk apply-design \u2014 make an existing site wear a design.\n\nUsage:\n create-ailk apply-design <name> [--target <site-dir>]\n create-ailk apply-design <path> [--target <site-dir>]\n\nArguments:\n <name|path> A shipped design (standard, minimalism), or a\n directory holding a DESIGN.md. A relative path\n resolves from the current directory.\n\nOptions:\n --target <site-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n --allow-unread-chrome Apply the design even when a file of the site does\n not read a chrome setting the design sets (a site\n scaffolded before the setting existed). Without it\n that is refused; with it each such file is reported.\n --help, -h Print this message.\n\nWrites only the look: DESIGN.md (replaced whole by the design's, keeping the\nsite's Source template and Version lines), the theme: block of\ncontent/_site.mdx, the generated apps/web/lib/site-chrome.ts, the generated\napps/web/lib/site-fonts.ts, the look fields of sections the site's pages\nalready place, and the design/layout/theme record in .ailk-site-manifest.json.\nNever adds, removes or renames a route or page, never touches a content field\n(except a placeholder home's sections, below) or the brand: block, and leaves\nevery other page's structured data unchanged.\n\nA design may seed the placeholder home's sections \u2014 its home arc \u2014 while they\nare still a placeholder: the shape's own, or a design's arc of them. It never\nchanges a home whose sections were edited; it reports it and leaves it. A\nseeded home takes the design's looks; look values on the sections it replaced\nare not kept. A design that ships no arc restores the shape's own sections on\na home that still wears another design's untouched arc.\n\nA design other than standard needs a Pro or Pro Plus key: the site's own\nailk.key (put one there with create-ailk license set <key>), else\nAILK_LICENSE_KEY. A malformed package, a preset the validator refuses or a\nmissing key is refused before any file is written.\n\nA preset sets the look fields it names and never unsets one: a field it does\nnot name keeps whatever value the page has. Running it again changes nothing.\n";
|
|
70
81
|
/**
|
|
71
82
|
* Parse the argv AFTER the `apply-design` word (D1). Strict `node:util`
|
|
72
83
|
* parseArgs, like `parseSyncRoutesArgs`: exactly one positional, the design
|
package/dist/apply-design.js
CHANGED
|
@@ -22,6 +22,7 @@ import { compileDesignFonts, compileDesignTheme } from "./design-compile.js";
|
|
|
22
22
|
import { HOME_PAGE_PATH, designHomeSeeds, planDesignHome, shapeHomePath, shippedHomeSeeds, } from "./design-home.js";
|
|
23
23
|
import { applyDesignPresets, readSectionLookCatalog, shippedPageTypes, validateDesignPresets, } from "./design-presets.js";
|
|
24
24
|
import { resolveDesign, spliceThemeBlock } from "./design-scaffold.js";
|
|
25
|
+
import { chromeKnobReaders, unreadChromeKnobs } from "./lib/chrome-readers.js";
|
|
25
26
|
import { LICENSE_KEY_FILE } from "./lib/license-key-files.js";
|
|
26
27
|
import { CONVERSION_HINT_URL, LicenseRequiredError, assertDesignAllowed, deriveLevel, } from "./lib/module-license-gate.js";
|
|
27
28
|
import { LEVEL_LABEL } from "./lib/module-tier.js";
|
|
@@ -29,6 +30,17 @@ import { ParseError } from "./parse-args.js";
|
|
|
29
30
|
import { SITE_CHROME_PATH, renderSiteChrome, resolveSiteChrome, } from "./site-chrome.js";
|
|
30
31
|
import { SITE_FONTS_PATH, renderSiteFonts } from "./site-fonts.js";
|
|
31
32
|
import { SITE_ROSTER_PATH, planHomeRecord, planSiteManifestDesign, readSiteRoster, } from "./site-roster.js";
|
|
33
|
+
/** #7781 — thrown before any write when the design sets a chrome knob that a reader file of the site does not read. */
|
|
34
|
+
export class UnreadChromeError extends Error {
|
|
35
|
+
unread;
|
|
36
|
+
constructor(unread) {
|
|
37
|
+
super(`design chrome would not render: the site's own copy of these files does not read the knob the design sets (update them from the current create-ailk overlay, or pass --allow-unread-chrome to apply anyway):\n${unread
|
|
38
|
+
.map(({ knob, file }) => ` ${knob} — ${file}`)
|
|
39
|
+
.join("\n")}`);
|
|
40
|
+
this.unread = unread;
|
|
41
|
+
this.name = "UnreadChromeError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
32
44
|
/** The site's locale layout — the one reader of the generated site-fonts.ts. */
|
|
33
45
|
const SITE_LAYOUT_PATH = "apps/web/app/[locale]/layout.tsx";
|
|
34
46
|
export const APPLY_DESIGN_HELP = `create-ailk apply-design — make an existing site wear a design.
|
|
@@ -45,6 +57,10 @@ Arguments:
|
|
|
45
57
|
Options:
|
|
46
58
|
--target <site-dir> An already-scaffolded repo (contains
|
|
47
59
|
architecture.yaml). Defaults to the current dir.
|
|
60
|
+
--allow-unread-chrome Apply the design even when a file of the site does
|
|
61
|
+
not read a chrome setting the design sets (a site
|
|
62
|
+
scaffolded before the setting existed). Without it
|
|
63
|
+
that is refused; with it each such file is reported.
|
|
48
64
|
--help, -h Print this message.
|
|
49
65
|
|
|
50
66
|
Writes only the look: DESIGN.md (replaced whole by the design's, keeping the
|
|
@@ -83,6 +99,7 @@ export function parseApplyDesignArgs(argv) {
|
|
|
83
99
|
allowPositionals: true,
|
|
84
100
|
options: {
|
|
85
101
|
target: { type: "string" },
|
|
102
|
+
"allow-unread-chrome": { type: "boolean", default: false },
|
|
86
103
|
help: { type: "boolean", default: false, short: "h" },
|
|
87
104
|
},
|
|
88
105
|
});
|
|
@@ -94,7 +111,12 @@ export function parseApplyDesignArgs(argv) {
|
|
|
94
111
|
if (design === undefined && !help) {
|
|
95
112
|
throw new ParseError("apply-design needs a design. Usage: create-ailk apply-design <name|path> [--target <site-dir>]");
|
|
96
113
|
}
|
|
97
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
design,
|
|
116
|
+
target: parsed.values.target,
|
|
117
|
+
allowUnreadChrome: parsed.values["allow-unread-chrome"] === true,
|
|
118
|
+
help,
|
|
119
|
+
};
|
|
98
120
|
}
|
|
99
121
|
/** `abs` relative to `root`, with `/` separators. */
|
|
100
122
|
function siteRelative(root, abs) {
|
|
@@ -189,12 +211,18 @@ export async function applyDesign(opts) {
|
|
|
189
211
|
throw new LicenseRequiredError(`design "${resolved.display}" is a Pro feature — this site's licence level is ${LEVEL_LABEL[level]}; put a Pro or Pro Plus key in the site with create-ailk license set <key>, or set AILK_LICENSE_KEY → ${CONVERSION_HINT_URL}`);
|
|
190
212
|
}
|
|
191
213
|
}
|
|
214
|
+
// #7781 — after the licence gate, before any plan: every chrome knob the design sets must be read by
|
|
215
|
+
// the site's own copy of each overlay file that reads it. The reader map is derived from the overlay.
|
|
216
|
+
const roster = readSiteRoster(targetAbs);
|
|
217
|
+
const chrome = resolveSiteChrome(roster?.template.layout, pkg.layout);
|
|
218
|
+
const templatesDir = opts.templatesDir ?? join(dirname(opts.siteTemplatesDir), "templates");
|
|
219
|
+
const unreadChrome = unreadChromeKnobs(chrome, chromeKnobReaders(templatesDir), targetAbs);
|
|
220
|
+
if (unreadChrome.length > 0 && opts.allowUnreadChrome !== true)
|
|
221
|
+
throw new UnreadChromeError(unreadChrome);
|
|
192
222
|
// C6 / D4 — plan every write in memory.
|
|
193
223
|
const theme = compileDesignTheme(pkg);
|
|
194
224
|
const siteMdxCurrent = readFileSync(siteMdxPath, "utf8");
|
|
195
225
|
const siteMdxNext = spliceThemeBlock(siteMdxCurrent, theme, siteMdxPath);
|
|
196
|
-
const roster = readSiteRoster(targetAbs);
|
|
197
|
-
const chrome = resolveSiteChrome(roster?.template.layout, pkg.layout);
|
|
198
226
|
const chromePath = join(targetAbs, SITE_CHROME_PATH);
|
|
199
227
|
const chromeCurrent = existsSync(chromePath) ? readFileSync(chromePath, "utf8") : undefined;
|
|
200
228
|
const chromeNext = renderSiteChrome(chrome);
|
|
@@ -229,7 +257,7 @@ export async function applyDesign(opts) {
|
|
|
229
257
|
? ""
|
|
230
258
|
: readFileSync(shapeHomePath(shape, {
|
|
231
259
|
siteTemplatesDir: opts.siteTemplatesDir,
|
|
232
|
-
templatesDir
|
|
260
|
+
templatesDir,
|
|
233
261
|
}), "utf8"),
|
|
234
262
|
shippedSeeds,
|
|
235
263
|
catalog: catalog(),
|
|
@@ -328,6 +356,7 @@ export async function applyDesign(opts) {
|
|
|
328
356
|
...(roster !== undefined ? { rosterPath: roster.path } : {}),
|
|
329
357
|
...(fontFamilies !== undefined ? { fonts: fontFamilies } : {}),
|
|
330
358
|
...(fontsUnwired ? { fontsUnwired: true } : {}),
|
|
359
|
+
...(unreadChrome.length > 0 ? { unreadChrome } : {}),
|
|
331
360
|
};
|
|
332
361
|
}
|
|
333
362
|
/** The changed-file report (D9). */
|
|
@@ -364,6 +393,9 @@ export function formatApplyDesignReport(result) {
|
|
|
364
393
|
if (result.fontsUnwired === true) {
|
|
365
394
|
lines.push(`fonts not loaded: ${SITE_LAYOUT_PATH} predates lib/site-fonts — update the layout from the current create-ailk overlay`);
|
|
366
395
|
}
|
|
396
|
+
for (const { knob, file } of result.unreadChrome ?? []) {
|
|
397
|
+
lines.push(`chrome not rendered: ${knob} — ${file} does not read it`);
|
|
398
|
+
}
|
|
367
399
|
if (result.manifest === "legacy-roster") {
|
|
368
400
|
lines.push(`manifest not recorded: ${result.rosterPath} predates the design record`);
|
|
369
401
|
}
|
package/dist/cli.js
CHANGED
|
@@ -203,6 +203,7 @@ async function main() {
|
|
|
203
203
|
siteTemplatesDir: resolve(PKG_ROOT, "site-templates"),
|
|
204
204
|
templatesDir: resolve(PKG_ROOT, "templates"),
|
|
205
205
|
catalogPath: resolve(PKG_ROOT, "component-catalog.json"),
|
|
206
|
+
allowUnreadChrome: args.allowUnreadChrome,
|
|
206
207
|
});
|
|
207
208
|
process.stdout.write(formatApplyDesignReport(result));
|
|
208
209
|
process.exit(0);
|
|
@@ -318,7 +319,8 @@ async function main() {
|
|
|
318
319
|
// marketing-site-free` still rides the no-key case (record D11): its
|
|
319
320
|
// `requires` is all-free, so it installs from the shipped
|
|
320
321
|
// `site-templates/` tree with no key and no request.
|
|
321
|
-
const { targetDir: targetAbs, surfaces, starter, modules: effectiveModules, } = await scaffold(toScaffoldOptions(opts));
|
|
322
|
+
const { targetDir: targetAbs, surfaces, starter, modules: effectiveModules, droppedModules, hubOrigin, } = await scaffold(toScaffoldOptions(opts));
|
|
323
|
+
const isSpoke = opts.topology === "spoke";
|
|
322
324
|
// The starter scaffold() resolved — from --template, or from --shape (#6754 D4).
|
|
323
325
|
const templateName = starter?.template ?? opts.template;
|
|
324
326
|
if (opts.install) {
|
|
@@ -346,6 +348,22 @@ async function main() {
|
|
|
346
348
|
...(starter !== undefined
|
|
347
349
|
? [`Starter: shape ${starter.shape}, design ${starter.design}.`]
|
|
348
350
|
: []),
|
|
351
|
+
// #7426 — a spoke says so, and names what its hosts must carry (D3).
|
|
352
|
+
...(isSpoke
|
|
353
|
+
? [
|
|
354
|
+
`Topology: spoke — no apps/api, no database, no sign-in${opts.mcp === false ? ", no apps/mcp" : "; apps/mcp is a stateless front"}.`,
|
|
355
|
+
...(droppedModules !== undefined && droppedModules.length > 0
|
|
356
|
+
? [
|
|
357
|
+
`Not carried on a spoke: ${droppedModules.join(", ")} (your licence includes ${droppedModules.length === 1 ? "it" : "them"}; a spoke does not admit ${droppedModules.length === 1 ? "it" : "them"} yet).`,
|
|
358
|
+
]
|
|
359
|
+
: []),
|
|
360
|
+
"",
|
|
361
|
+
`Every host serving this spoke's ${opts.mcp === false ? "apps/web" : "apps/web and apps/mcp"} must carry:`,
|
|
362
|
+
` AILK_API_URL=${hubOrigin ?? "<your hub origin>"} # written to apps/web/.env.local`,
|
|
363
|
+
" AILK_SPOKE_KEY # issued by the hub; never written here",
|
|
364
|
+
" AILK_VISITOR_FORWARD_KEY # issued by the hub; never written here",
|
|
365
|
+
]
|
|
366
|
+
: []),
|
|
349
367
|
"",
|
|
350
368
|
];
|
|
351
369
|
// Prerequisites (#6440 AC4) — every one the scaffold enforces, and nothing
|
|
@@ -374,13 +392,15 @@ async function main() {
|
|
|
374
392
|
lines.push("Next steps:");
|
|
375
393
|
lines.push(` cd ${targetAbs}`);
|
|
376
394
|
if (!opts.install) {
|
|
377
|
-
lines.push(" pnpm install # also generates the Prisma client");
|
|
395
|
+
lines.push(isSpoke ? " pnpm install" : " pnpm install # also generates the Prisma client");
|
|
378
396
|
}
|
|
379
397
|
lines.push(" pnpm dev # http://localhost:3000");
|
|
380
398
|
lines.push(" pnpm build # production build, when you are ready to deploy");
|
|
381
399
|
lines.push("");
|
|
382
400
|
lines.push("Open http://localhost:3000 to see the home page.");
|
|
383
|
-
|
|
401
|
+
if (!isSpoke) {
|
|
402
|
+
lines.push("Adding auth, saved flows, checkout or database-backed content later? README.md \u00a7 \"When you need a database\" has the one-command setup.");
|
|
403
|
+
}
|
|
384
404
|
lines.push("");
|
|
385
405
|
lines.push("Claude Code: the AI Launch Kit skills come from the ailk plugin. Trust this folder and Claude Code loads it, or run in a session:");
|
|
386
406
|
for (const line of PLUGIN_INSTALL_LINES)
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { Topology } from "./topology.js";
|
|
2
|
+
/** The topology a document is derived for (#7426 D8). */
|
|
3
|
+
export interface DeriveArchitectureOptions {
|
|
4
|
+
/** `"hub"` (the default) or `"spoke"`. */
|
|
5
|
+
topology?: Topology;
|
|
6
|
+
/** Spoke only: whether the spoke keeps `apps/mcp` (default true). */
|
|
7
|
+
mcp?: boolean;
|
|
8
|
+
}
|
|
1
9
|
/**
|
|
2
10
|
* Derive the `architecture.yaml` document for a module set.
|
|
3
11
|
*
|
|
@@ -6,8 +14,13 @@
|
|
|
6
14
|
* @param surfaces the enabled gateable route surfaces. Omitted ⟹ derived from
|
|
7
15
|
* the module set alone (`resolveSurfaces`), so a caller that passes only
|
|
8
16
|
* modules gets exactly what the modules imply.
|
|
17
|
+
* @param options the topology (#7426 D8). Absent or `"hub"` ⟹ the output is
|
|
18
|
+
* byte-identical to the two-argument form. `"spoke"` ⟹ only the spoke's own
|
|
19
|
+
* apps (`web`, and `mcp` unless `mcp: false`), no `namespaces`, no
|
|
20
|
+
* `database` root, a top-level `topology: spoke` and the hub-facing
|
|
21
|
+
* composition tiers.
|
|
9
22
|
* @returns the complete `architecture.yaml` file content (a deterministic
|
|
10
|
-
* function of the module SET
|
|
23
|
+
* function of the module SET, the surface SET and the topology).
|
|
11
24
|
* @throws Error on an unrecognized module (defensive; parse-args rejects first).
|
|
12
25
|
*/
|
|
13
|
-
export declare function deriveArchitecture(modules?: string[], surfaces?: string[]): string;
|
|
26
|
+
export declare function deriveArchitecture(modules?: string[], surfaces?: string[], options?: DeriveArchitectureOptions): string;
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { ARCHITECTURE_BASE, MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
|
|
17
17
|
import { CORE_SURFACE_PATHS, RECOGNIZED_SURFACES, resolveSurfaces, } from "./surfaces.js";
|
|
18
|
+
/** A spoke's composition tiers (#7426 D8): its data and API are the hub's. */
|
|
19
|
+
const SPOKE_TIERS = {
|
|
20
|
+
data: "none — the hub's database",
|
|
21
|
+
api: "the hub at AILK_API_URL (apps/mcp is a stateless front)",
|
|
22
|
+
presentation: "apps/web",
|
|
23
|
+
};
|
|
18
24
|
function isScalar(v) {
|
|
19
25
|
return typeof v === "string" || typeof v === "number";
|
|
20
26
|
}
|
|
@@ -150,11 +156,18 @@ function namespacesToYaml(ns) {
|
|
|
150
156
|
* @param surfaces the enabled gateable route surfaces. Omitted ⟹ derived from
|
|
151
157
|
* the module set alone (`resolveSurfaces`), so a caller that passes only
|
|
152
158
|
* modules gets exactly what the modules imply.
|
|
159
|
+
* @param options the topology (#7426 D8). Absent or `"hub"` ⟹ the output is
|
|
160
|
+
* byte-identical to the two-argument form. `"spoke"` ⟹ only the spoke's own
|
|
161
|
+
* apps (`web`, and `mcp` unless `mcp: false`), no `namespaces`, no
|
|
162
|
+
* `database` root, a top-level `topology: spoke` and the hub-facing
|
|
163
|
+
* composition tiers.
|
|
153
164
|
* @returns the complete `architecture.yaml` file content (a deterministic
|
|
154
|
-
* function of the module SET
|
|
165
|
+
* function of the module SET, the surface SET and the topology).
|
|
155
166
|
* @throws Error on an unrecognized module (defensive; parse-args rejects first).
|
|
156
167
|
*/
|
|
157
|
-
export function deriveArchitecture(modules = ["marketing"], surfaces) {
|
|
168
|
+
export function deriveArchitecture(modules = ["marketing"], surfaces, options = {}) {
|
|
169
|
+
const spoke = options.topology === "spoke";
|
|
170
|
+
const keepMcp = options.mcp !== false;
|
|
158
171
|
const selected = normalizeModules(modules);
|
|
159
172
|
const slices = selected.map((m) => MODULE_SLICES[m]);
|
|
160
173
|
// The declared route surface (#4154). A scaffold's live-route set is a
|
|
@@ -180,12 +193,20 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
|
|
|
180
193
|
};
|
|
181
194
|
for (const s of slices)
|
|
182
195
|
Object.assign(declared, s.apps ?? {});
|
|
196
|
+
// A spoke carries only its own apps (#7426 D8): web, and mcp unless
|
|
197
|
+
// `--without-mcp` — never api, never a module's admin.
|
|
198
|
+
if (spoke) {
|
|
199
|
+
for (const key of Object.keys(declared)) {
|
|
200
|
+
if (key !== "web" && !(key === "mcp" && keepMcp))
|
|
201
|
+
delete declared[key];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
183
204
|
// A scaffold carries no `packages/` tree — `@working-theory/*` install from
|
|
184
205
|
// npm — so an entry naming a monorepo folder is dropped rather than shipped
|
|
185
206
|
// as a rule the linter can never apply (#6940). What survives names a path
|
|
186
207
|
// the scaffold has: a declared app, or `database/`.
|
|
187
208
|
const roots = [
|
|
188
|
-
"database",
|
|
209
|
+
...(spoke ? [] : ["database"]),
|
|
189
210
|
...Object.values(declared).map((a) => a.path),
|
|
190
211
|
];
|
|
191
212
|
const inScaffold = (p) => {
|
|
@@ -209,8 +230,11 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
|
|
|
209
230
|
forbidden_patterns: mergeForbidden([...ARCHITECTURE_BASE.appForbiddenPatterns], a.forbidden_patterns ?? []).map(scoped),
|
|
210
231
|
});
|
|
211
232
|
}
|
|
212
|
-
// namespaces — present only if a data-tier module contributes one
|
|
213
|
-
|
|
233
|
+
// namespaces — present only if a data-tier module contributes one, and
|
|
234
|
+
// never on a spoke, which has no database/ of its own.
|
|
235
|
+
const nsSlice = spoke
|
|
236
|
+
? undefined
|
|
237
|
+
: slices.find((s) => s.namespaces)?.namespaces;
|
|
214
238
|
// forbidden_patterns — base floor first, then the selected modules' patterns.
|
|
215
239
|
const extraForbidden = [];
|
|
216
240
|
for (const s of slices)
|
|
@@ -218,8 +242,11 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
|
|
|
218
242
|
const forbidden = mergeForbidden([...ARCHITECTURE_BASE.forbiddenPatterns], extraForbidden).map(scoped);
|
|
219
243
|
const doc = {
|
|
220
244
|
version: ARCHITECTURE_BASE.version,
|
|
245
|
+
...(spoke ? { topology: "spoke" } : {}),
|
|
221
246
|
composition_model: {
|
|
222
|
-
tiers:
|
|
247
|
+
tiers: spoke
|
|
248
|
+
? { ...SPOKE_TIERS }
|
|
249
|
+
: { ...ARCHITECTURE_BASE.composition_model.tiers },
|
|
223
250
|
},
|
|
224
251
|
surfaces: {
|
|
225
252
|
core: [...CORE_SURFACE_PATHS],
|
|
@@ -240,6 +267,7 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
|
|
|
240
267
|
"#",
|
|
241
268
|
`# Modules: ${selected.join(", ")}`,
|
|
242
269
|
`# Surfaces: ${enabledSurfaces.length > 0 ? enabledSurfaces.join(", ") : "core only"}`,
|
|
270
|
+
...(spoke ? ["# Topology: spoke"] : []),
|
|
243
271
|
"",
|
|
244
272
|
].join("\n");
|
|
245
273
|
return header + emitDocument(doc);
|
package/dist/design-package.js
CHANGED
|
@@ -207,7 +207,7 @@ function parseScalar(raw, lineNo, file) {
|
|
|
207
207
|
* The one dotted key path whose value may be a block sequence (#7719 D2) —
|
|
208
208
|
* the deck's ordered ground list. Every other `- ` line is refused as before.
|
|
209
209
|
*/
|
|
210
|
-
const SEQUENCE_PATHS = new Set(["layout.deckGrounds"]);
|
|
210
|
+
const SEQUENCE_PATHS = new Set(["layout.deckGrounds", "layout.closingFooterPaths"]);
|
|
211
211
|
/**
|
|
212
212
|
* Parse a block sequence of scalars whose `- ` items sit at `pos.i`'s indent
|
|
213
213
|
* (#7719 D2). Each item's text is read by `parseScalar`, with every refusal it
|
|
@@ -487,7 +487,7 @@ function validateStringMap(raw, file, field) {
|
|
|
487
487
|
const LAYOUT_KEYS = ["nav", "footer", "announcementBanner", "home"];
|
|
488
488
|
/** Every key a package's `layout` may carry — #7719 D3 adds the optional `deckGrounds` (a deck home only),
|
|
489
489
|
* #7721 D1 the optional `innerPages`, #7740 D1 the optional `deckPresentation` (a deck home only),
|
|
490
|
-
* #7742 D1 the optional `colorMode` and `chromeStyle`. */
|
|
490
|
+
* #7742 D1 the optional `colorMode` and `chromeStyle`, #7797 D1 the optional `closingFooterPaths`. */
|
|
491
491
|
const ALLOWED_LAYOUT_KEYS = new Set([
|
|
492
492
|
...LAYOUT_KEYS,
|
|
493
493
|
"deckGrounds",
|
|
@@ -495,6 +495,7 @@ const ALLOWED_LAYOUT_KEYS = new Set([
|
|
|
495
495
|
"deckPresentation",
|
|
496
496
|
"colorMode",
|
|
497
497
|
"chromeStyle",
|
|
498
|
+
"closingFooterPaths",
|
|
498
499
|
]);
|
|
499
500
|
function validateLayout(raw, file) {
|
|
500
501
|
const obj = requireObject(raw, file, "layout");
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { CATALOG_TARGET_PATH, FREE_MODULES, addModule, LicenseRequiredError, PAI
|
|
|
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 { APPLY_DESIGN_HELP, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, type ApplyDesignArgs, type ApplyDesignChange, type ApplyDesignOptions, type ApplyDesignResult, } from "./apply-design.js";
|
|
12
|
+
export { APPLY_DESIGN_HELP, UnreadChromeError, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, type ApplyDesignArgs, type ApplyDesignChange, type ApplyDesignOptions, type ApplyDesignResult, } from "./apply-design.js";
|
|
13
13
|
export type { HomeSeedOutcome } from "./design-home.js";
|
|
14
14
|
export { SITE_ROSTER_PATH, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
|
|
15
15
|
export { SYNC_ROUTES_HELP, formatSyncRoutesReport, listTemplateRouteDirs, parseSyncRoutesArgs, syncRoutes, type RouteReport, type SyncRoutesOptions, type SyncRoutesResult, } from "./sync-routes.js";
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ export { extractModuleBundle, } from "./lib/extract-module-bundle.js";
|
|
|
16
16
|
export { ADD_MODULE_HELP, AddModuleRefusedError, applyModuleToScaffold, formatAddModuleReport, parseAddModuleArgs, } from "./add-module.js";
|
|
17
17
|
// `create-ailk apply-design` (#6819) — make an existing site wear a design,
|
|
18
18
|
// changing only its look. The pure core, beside `add-module`'s.
|
|
19
|
-
export { APPLY_DESIGN_HELP, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, } from "./apply-design.js";
|
|
19
|
+
export { APPLY_DESIGN_HELP, UnreadChromeError, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, } from "./apply-design.js";
|
|
20
20
|
// The site's page roster of record and its install-diff check (#6682).
|
|
21
21
|
export { SITE_ROSTER_PATH, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
|
|
22
22
|
// `create-ailk sync-routes` (#4986) — the template-route drift check + copy.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ResolvedSiteChrome } from "../site-chrome.js";
|
|
2
|
+
/** One knob a design sets that one of the site's reader files does not read. */
|
|
3
|
+
export interface UnreadChromeKnob {
|
|
4
|
+
knob: string;
|
|
5
|
+
/** Overlay-relative path of the reader file, with `/` separators. */
|
|
6
|
+
file: string;
|
|
7
|
+
}
|
|
8
|
+
/** The pattern that means "this text reads `knob`". */
|
|
9
|
+
export declare function chromeReadPattern(knob: string): RegExp;
|
|
10
|
+
/**
|
|
11
|
+
* knob → the overlay files that read it, sorted. Scans `<templatesDir>/apps/web`;
|
|
12
|
+
* fails closed (throws, naming the directory) when it is absent or unreadable,
|
|
13
|
+
* so a broken overlay can never read as "nothing reads any knob".
|
|
14
|
+
*/
|
|
15
|
+
export declare function chromeKnobReaders(templatesDir: string): Map<string, string[]>;
|
|
16
|
+
/**
|
|
17
|
+
* The `(knob, file)` pairs where the resolved chrome sets a non-default `knob`,
|
|
18
|
+
* the shipped overlay's `file` reads it, the site has its own `file`, and the
|
|
19
|
+
* site's `file` does not read it. A reader file the site lacks is skipped.
|
|
20
|
+
*/
|
|
21
|
+
export declare function unreadChromeKnobs(resolved: ResolvedSiteChrome, readers: ReadonlyMap<string, readonly string[]>, siteDir: string): UnreadChromeKnob[];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which generated app files read which chrome knob (#7781, spec
|
|
3
|
+
* docs/specs/apply-design-unread-chrome-spec.md).
|
|
4
|
+
*
|
|
5
|
+
* `apply-design` writes a design's chrome knobs into the site's generated
|
|
6
|
+
* `apps/web/lib/site-chrome.ts`; other generated files read them. A site
|
|
7
|
+
* scaffolded before a knob existed has a reader file that ignores it, so the
|
|
8
|
+
* design is half-applied. These pure functions find that gap:
|
|
9
|
+
*
|
|
10
|
+
* - `chromeKnobReaders` derives, from the shipped overlay, the files that read
|
|
11
|
+
* each knob — nothing is hard-coded, so a new reader file is picked up.
|
|
12
|
+
* - `unreadChromeKnobs` compares the site's own copy of each reader file.
|
|
13
|
+
*
|
|
14
|
+
* "Reads `knob`" means the file's text matches `(?:siteChrome|chrome)\.<knob>\b`.
|
|
15
|
+
* A file that reads a knob under another name (a destructured alias) is not
|
|
16
|
+
* recognised — accepted (spec § Clarifications, detection).
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { DEFAULT_SITE_CHROME, } from "../site-chrome.js";
|
|
21
|
+
/** The overlay tree the generated app files live under. */
|
|
22
|
+
const OVERLAY_WEB_DIR = ["apps", "web"];
|
|
23
|
+
/** What precedes a knob name in a read: `siteChrome.` or `chrome.` — the one definition both the overlay scan and the per-site check use. */
|
|
24
|
+
const READ_HEAD = "(?:siteChrome|chrome)\\.";
|
|
25
|
+
/** The pattern that means "this text reads `knob`". */
|
|
26
|
+
export function chromeReadPattern(knob) {
|
|
27
|
+
return new RegExp(`${READ_HEAD}${knob.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
28
|
+
}
|
|
29
|
+
/** Every `.ts`/`.tsx` file under `dir` outside a `__tests__` segment, as `/`-joined paths relative to `root`. */
|
|
30
|
+
function listSources(root, dir, out) {
|
|
31
|
+
for (const entry of readdirSync(join(root, dir), { withFileTypes: true })) {
|
|
32
|
+
const rel = `${dir}/${entry.name}`;
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
if (entry.name !== "__tests__")
|
|
35
|
+
listSources(root, rel, out);
|
|
36
|
+
}
|
|
37
|
+
else if (/\.tsx?$/.test(entry.name)) {
|
|
38
|
+
out.push(rel);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* knob → the overlay files that read it, sorted. Scans `<templatesDir>/apps/web`;
|
|
44
|
+
* fails closed (throws, naming the directory) when it is absent or unreadable,
|
|
45
|
+
* so a broken overlay can never read as "nothing reads any knob".
|
|
46
|
+
*/
|
|
47
|
+
export function chromeKnobReaders(templatesDir) {
|
|
48
|
+
const webDir = join(templatesDir, ...OVERLAY_WEB_DIR);
|
|
49
|
+
const readers = new Map();
|
|
50
|
+
try {
|
|
51
|
+
if (!existsSync(webDir))
|
|
52
|
+
throw new Error("missing");
|
|
53
|
+
const files = [];
|
|
54
|
+
listSources(templatesDir, OVERLAY_WEB_DIR.join("/"), files);
|
|
55
|
+
for (const file of files.sort()) {
|
|
56
|
+
const text = readFileSync(join(templatesDir, file), "utf8");
|
|
57
|
+
for (const match of text.matchAll(new RegExp(`${READ_HEAD}([A-Za-z_$][\\w$]*)\\b`, "g"))) {
|
|
58
|
+
const knob = match[1];
|
|
59
|
+
const list = readers.get(knob) ?? [];
|
|
60
|
+
if (!list.includes(file))
|
|
61
|
+
list.push(file);
|
|
62
|
+
readers.set(knob, list);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error(`cannot check chrome knobs: the overlay has no readable apps/web at ${webDir}. Reinstall create-ailk.`);
|
|
68
|
+
}
|
|
69
|
+
return readers;
|
|
70
|
+
}
|
|
71
|
+
/** A knob is set when it differs from its default; an optional knob (no default) when present. */
|
|
72
|
+
function isSet(resolved, knob) {
|
|
73
|
+
const value = resolved[knob];
|
|
74
|
+
if (value === undefined)
|
|
75
|
+
return false;
|
|
76
|
+
if (!(knob in DEFAULT_SITE_CHROME))
|
|
77
|
+
return true;
|
|
78
|
+
return (value !== DEFAULT_SITE_CHROME[knob]);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The `(knob, file)` pairs where the resolved chrome sets a non-default `knob`,
|
|
82
|
+
* the shipped overlay's `file` reads it, the site has its own `file`, and the
|
|
83
|
+
* site's `file` does not read it. A reader file the site lacks is skipped.
|
|
84
|
+
*/
|
|
85
|
+
export function unreadChromeKnobs(resolved, readers, siteDir) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const knob of Object.keys(resolved).sort()) {
|
|
88
|
+
if (!isSet(resolved, knob))
|
|
89
|
+
continue;
|
|
90
|
+
const pattern = chromeReadPattern(knob);
|
|
91
|
+
for (const file of readers.get(knob) ?? []) {
|
|
92
|
+
const abs = join(siteDir, file);
|
|
93
|
+
if (!existsSync(abs))
|
|
94
|
+
continue;
|
|
95
|
+
if (!pattern.test(readFileSync(abs, "utf8")))
|
|
96
|
+
out.push({ knob, file });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|