create-ailk 0.13.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/README.md +12 -4
- package/component-catalog.json +250 -36
- package/component-catalog.md +10 -6
- 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 +32 -2
- package/dist/apply-design.js +155 -22
- package/dist/cli.js +24 -3
- package/dist/derive-architecture.d.ts +15 -2
- package/dist/derive-architecture.js +34 -6
- package/dist/design-compile.d.ts +70 -2
- package/dist/design-compile.js +97 -1
- package/dist/design-home.d.ts +74 -0
- package/dist/design-home.js +256 -0
- package/dist/design-package.d.ts +39 -2
- package/dist/design-package.js +224 -9
- package/dist/design-presets.d.ts +17 -1
- package/dist/design-presets.js +17 -5
- package/dist/design-scaffold.d.ts +6 -25
- package/dist/design-scaffold.js +26 -74
- package/dist/index.d.ts +2 -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/design-checks.d.ts +12 -1
- package/dist/lib/design-checks.js +38 -2
- 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/lib/module-license-gate.d.ts +9 -0
- package/dist/lib/module-license-gate.js +18 -0
- 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 +180 -35
- package/dist/site-chrome.d.ts +42 -0
- package/dist/site-chrome.js +140 -2
- package/dist/site-fonts.d.ts +21 -0
- package/dist/site-fonts.js +54 -0
- package/dist/site-manifest.d.ts +12 -5
- package/dist/site-manifest.js +25 -11
- package/dist/site-roster.d.ts +16 -0
- package/dist/site-roster.js +81 -0
- package/dist/starter-grid.js +15 -0
- package/dist/surfaces.js +12 -0
- package/dist/sync-routes.js +11 -2
- package/dist/team-profiles.d.ts +29 -0
- package/dist/team-profiles.js +43 -0
- package/dist/topology.d.ts +131 -0
- package/dist/topology.js +533 -0
- package/package.json +3 -3
- package/release-scope.json +1 -1
- package/site-templates/_base/content/en/TeamList/team.mdx +68 -0
- package/site-templates/_base/content/en/TeamMember/first-member.mdx +22 -0
- package/site-templates/_base/content/en/TeamMember/second-member.mdx +22 -0
- package/site-templates/_base/content/en/TeamMember/third-member.mdx +22 -0
- package/site-templates/designs/minimalism/DESIGN.md +166 -36
- package/site-templates/designs/minimalism/home/local-business-service.mdx +119 -0
- package/site-templates/designs/minimalism/home/organization-product.mdx +132 -0
- package/site-templates/designs/minimalism/home/organization-service.mdx +131 -0
- package/site-templates/designs/minimalism/home/person-service.mdx +101 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +20 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +22 -1
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -7
- package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -7
- package/site-templates/shapes/organization-service/ailk-template.json +1 -1
- package/templates/README.md +2 -0
- package/templates/apps/api/CLAUDE.md +6 -1
- 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/package.json +1 -1
- 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 +12 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +10 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts +42 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +8 -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]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +14 -1
- 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 +163 -0
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +68 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +15 -3
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +17 -6
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +14 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +4 -6
- package/templates/apps/web/app/[locale]/layout.tsx +212 -57
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/page.tsx +37 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +4 -6
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +5 -2
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +15 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +43 -6
- package/templates/apps/web/app/[locale]/services/page.tsx +14 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/team/[slug]/page.tsx +45 -0
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +155 -0
- package/templates/apps/web/app/[locale]/team/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +14 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/not-found.tsx +47 -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__/profile-back-link.test.ts +95 -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/__tests__/site-theme-head.test.ts +70 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +64 -1
- package/templates/apps/web/lib/closing-footer-path.ts +50 -0
- package/templates/apps/web/lib/profile-back-link.ts +51 -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 +15 -0
- package/templates/apps/web/lib/site-fonts.ts +15 -0
- package/templates/apps/web/lib/site-logo.tsx +39 -1
- package/templates/apps/web/lib/site-theme-head.ts +33 -0
- package/templates/apps/web/lib/site-theme.ts +13 -1
- package/templates/apps/web/lib/topology.generated.ts +23 -0
- package/templates/apps/web/messages/en.json +7 -1
- package/templates/apps/web/package.json +1 -1
- package/templates/database/CHANGELOG.md +32 -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 +35 -34
- package/site-templates/designs/minimalism/sample/README.md +0 -48
- package/site-templates/designs/minimalism/sample/content/en/HomePage/home.mdx +0 -244
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-1.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-2.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-3.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-4.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-5.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-6.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/hero-backdrop.svg +0 -17
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-1.svg +0 -6
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-2.svg +0 -6
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-3.svg +0 -6
- package/site-templates/designs/minimalism/sample/public/wordmark-placeholder.svg +0 -28
package/dist/design-presets.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { type DesignSections } from "./design-package.js";
|
|
|
2
2
|
export interface SectionLookCatalogEntry {
|
|
3
3
|
/** The look group, in registry order (D2). */
|
|
4
4
|
readonly look: readonly string[];
|
|
5
|
+
/** The type's rendering component (#7743 D6 — what the site manifest records), when the catalog names one. */
|
|
6
|
+
readonly component: string | undefined;
|
|
5
7
|
/** The type's `data` JSON Schema `properties` (the catalog's `schema.properties`). */
|
|
6
8
|
readonly properties: Readonly<Record<string, unknown>>;
|
|
7
9
|
}
|
|
@@ -37,6 +39,9 @@ export interface LocatedDataEntry {
|
|
|
37
39
|
export interface LocatedSection {
|
|
38
40
|
readonly type: string;
|
|
39
41
|
readonly form: "flow" | "block";
|
|
42
|
+
/** #7743 D3 — the element's `[start, end)` span: flow, its `{ … }`; block, from its `- ` to its last line's end. */
|
|
43
|
+
readonly start: number;
|
|
44
|
+
readonly end: number;
|
|
40
45
|
/** Flow: the index of `data`'s `{`. Block: the end of the `data:` line (before its newline). */
|
|
41
46
|
readonly dataOpen: number;
|
|
42
47
|
/** Flow: the index of `data`'s `}`. Block: `dataOpen`. */
|
|
@@ -45,6 +50,15 @@ export interface LocatedSection {
|
|
|
45
50
|
readonly entryColumn: number;
|
|
46
51
|
readonly entries: readonly LocatedDataEntry[];
|
|
47
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* A page's located sections, with the `[valueStart, valueEnd)` span of the
|
|
55
|
+
* whole `sections:` value (#7743 D3): from just after `sections:` (and the
|
|
56
|
+
* spaces after it) to the flow form's `]`, or the block form's last line end.
|
|
57
|
+
*/
|
|
58
|
+
export type LocatedSections = LocatedSection[] & {
|
|
59
|
+
readonly valueStart: number;
|
|
60
|
+
readonly valueEnd: number;
|
|
61
|
+
};
|
|
48
62
|
/**
|
|
49
63
|
* Locate a page's `sections[]` in its frontmatter — the prettier-formatted
|
|
50
64
|
* flow form (`sections:\n [ { "id": …, "type": …, "data": { … } }, … ]`)
|
|
@@ -52,7 +66,7 @@ export interface LocatedSection {
|
|
|
52
66
|
* Undefined when the page has no frontmatter or no column-0 `sections:` key.
|
|
53
67
|
* Any other shape throws `TemplateError` naming the element (D5).
|
|
54
68
|
*/
|
|
55
|
-
export declare function locatePageSections(text: string, file: string):
|
|
69
|
+
export declare function locatePageSections(text: string, file: string): LocatedSections | undefined;
|
|
56
70
|
export interface PresetPage {
|
|
57
71
|
readonly path: string;
|
|
58
72
|
readonly text: string;
|
|
@@ -64,6 +78,8 @@ export interface PresetApplication {
|
|
|
64
78
|
readonly sectionType: string;
|
|
65
79
|
readonly field: string;
|
|
66
80
|
}
|
|
81
|
+
/** The frontmatter's column-0 `type:` value, or undefined when it has none. */
|
|
82
|
+
export declare function pageTypeOf(text: string, file: string): string | undefined;
|
|
67
83
|
/**
|
|
68
84
|
* Set one look field on section `index` of a page, splicing only its value
|
|
69
85
|
* span (D5): an existing value is replaced; an absent field is inserted as
|
package/dist/design-presets.js
CHANGED
|
@@ -44,6 +44,7 @@ export function readSectionLookCatalog(catalogPath) {
|
|
|
44
44
|
const properties = entry.schema["properties"];
|
|
45
45
|
out[type] = {
|
|
46
46
|
look: entry.look.map(String),
|
|
47
|
+
component: typeof entry.component === "string" ? entry.component : undefined,
|
|
47
48
|
properties: isPlainObject(properties) ? properties : {},
|
|
48
49
|
};
|
|
49
50
|
}
|
|
@@ -397,6 +398,8 @@ function locateFlow(text, open, end, file) {
|
|
|
397
398
|
out.push({
|
|
398
399
|
type,
|
|
399
400
|
form: "flow",
|
|
401
|
+
start: element.open,
|
|
402
|
+
end: element.close + 1,
|
|
400
403
|
dataOpen: data.open,
|
|
401
404
|
dataClose: data.close,
|
|
402
405
|
entryColumn: -1,
|
|
@@ -410,7 +413,7 @@ function locateFlow(text, open, end, file) {
|
|
|
410
413
|
}
|
|
411
414
|
if (text[i] !== "]")
|
|
412
415
|
cannotLocate(file, undefined, "the closing ]");
|
|
413
|
-
return out;
|
|
416
|
+
return { sections: out, close: i };
|
|
414
417
|
}
|
|
415
418
|
function splitLines(text, start, end) {
|
|
416
419
|
const lines = [];
|
|
@@ -510,9 +513,12 @@ function locateBlock(text, lines, file) {
|
|
|
510
513
|
lastDeep = l.end;
|
|
511
514
|
}
|
|
512
515
|
close();
|
|
516
|
+
const last = item.filter((l) => !l.blank).at(-1);
|
|
513
517
|
return {
|
|
514
518
|
type,
|
|
515
519
|
form: "block",
|
|
520
|
+
start: item[0].start + itemIndent,
|
|
521
|
+
end: last.end,
|
|
516
522
|
dataOpen: data.end,
|
|
517
523
|
dataClose: data.end,
|
|
518
524
|
entryColumn,
|
|
@@ -545,20 +551,26 @@ export function locatePageSections(text, file) {
|
|
|
545
551
|
const nextKey = /\n(?=[^\s#-])/.exec(text.slice(afterKey, fmEnd));
|
|
546
552
|
const valueEnd = nextKey ? afterKey + nextKey.index : fmEnd;
|
|
547
553
|
const first = skipSpace(text, afterKey, valueEnd);
|
|
548
|
-
if (text[first] === "[")
|
|
549
|
-
|
|
554
|
+
if (text[first] === "[") {
|
|
555
|
+
const { sections, close } = locateFlow(text, first, valueEnd, file);
|
|
556
|
+
return Object.assign(sections, { valueStart: afterKey, valueEnd: close + 1 });
|
|
557
|
+
}
|
|
550
558
|
const lineEnd = text.indexOf("\n", afterKey);
|
|
551
559
|
if (lineEnd >= 0 && text.slice(afterKey, lineEnd).trim() === "") {
|
|
552
560
|
const lines = splitLines(text, lineEnd + 1, valueEnd);
|
|
553
561
|
const firstLine = lines.find((l) => !l.blank);
|
|
554
562
|
if (firstLine === undefined || text.startsWith("- ", firstLine.start + firstLine.indent)) {
|
|
555
|
-
|
|
563
|
+
const lastLine = lines.filter((l) => !l.blank).at(-1);
|
|
564
|
+
return Object.assign(locateBlock(text, lines, file), {
|
|
565
|
+
valueStart: afterKey,
|
|
566
|
+
valueEnd: lastLine?.end ?? afterKey,
|
|
567
|
+
});
|
|
556
568
|
}
|
|
557
569
|
}
|
|
558
570
|
return cannotLocate(file, undefined, "a flow or block sequence");
|
|
559
571
|
}
|
|
560
572
|
/** The frontmatter's column-0 `type:` value, or undefined when it has none. */
|
|
561
|
-
function pageTypeOf(text, file) {
|
|
573
|
+
export function pageTypeOf(text, file) {
|
|
562
574
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?$/m.exec(text);
|
|
563
575
|
if (!fm || fm.index !== 0)
|
|
564
576
|
return undefined;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type CompiledDesignTheme } from "./design-compile.js";
|
|
2
2
|
import { type DesignPackage } from "./design-package.js";
|
|
3
|
-
import type { LayerOwnership } from "./lib/extract-module-bundle.js";
|
|
4
3
|
/** One resolved `--design` reference (D2). */
|
|
5
4
|
export interface ResolvedDesign {
|
|
6
5
|
/** "name" when the reference was a registry name, "path" when a directory. */
|
|
@@ -38,9 +37,12 @@ export declare function resolveDesign(ref: string, opts: {
|
|
|
38
37
|
}): ResolvedDesign;
|
|
39
38
|
/**
|
|
40
39
|
* Render a compiled theme's `theme:` frontmatter block (D5) — `theme:`, each
|
|
41
|
-
* treatment present as a bare `key: value
|
|
42
|
-
* `
|
|
43
|
-
*
|
|
40
|
+
* treatment present as a bare `key: value` (#7739's `darkInk`/`figures` after
|
|
41
|
+
* `checkmarks`), the `typography:` group when compiled — every role and field
|
|
42
|
+
* in the compile's order, role keys unquoted, sizes as plain scalars (#7720
|
|
43
|
+
* D2, #7739 D2.5) — then `colors:` with `light:`/`dark:` sub-blocks, each
|
|
44
|
+
* colour slot as `slot: <JSON.stringify(value)>` in `DESIGN_COLOR_MAP`'s
|
|
45
|
+
* order. Ends with a trailing newline.
|
|
44
46
|
*/
|
|
45
47
|
export declare function renderThemeBlock(theme: CompiledDesignTheme): string;
|
|
46
48
|
/**
|
|
@@ -53,24 +55,3 @@ export declare function renderThemeBlock(theme: CompiledDesignTheme): string;
|
|
|
53
55
|
* @throws TemplateError when `text` carries no frontmatter to write into.
|
|
54
56
|
*/
|
|
55
57
|
export declare function spliceThemeBlock(text: string, theme: CompiledDesignTheme, file: string): string;
|
|
56
|
-
/**
|
|
57
|
-
* Install a design's `sample/` directory onto a scaffold (spec 6892 D4) — a
|
|
58
|
-
* FIXED, undeclared convention, not a design-package field: `sample/content/**`
|
|
59
|
-
* lands at the scaffold's `content/**`, `sample/public/**` at
|
|
60
|
-
* `apps/web/public/**`. A design with no `sample/` directory (e.g. `standard`)
|
|
61
|
-
* installs nothing.
|
|
62
|
-
*
|
|
63
|
-
* Refuses — before its first write, so a refused sample leaves `targetDir`
|
|
64
|
-
* untouched — any file outside those two roots, or any `.ts`/`.tsx`/`.js`/
|
|
65
|
-
* `.mjs` file (content only, the template §5.2 rule this design package
|
|
66
|
-
* amendment carries over). Every installed file's destination is recorded in
|
|
67
|
-
* `ownership` as `design:<name>` (`readDesignPackage(designDir).name`), the
|
|
68
|
-
* same call-scoped registry `installBundleTree` writes into.
|
|
69
|
-
*
|
|
70
|
-
* @throws Error on a file outside `content/`/`public/`, or a forbidden extension.
|
|
71
|
-
*/
|
|
72
|
-
export declare function installDesignSample(opts: {
|
|
73
|
-
designDir: string;
|
|
74
|
-
targetDir: string;
|
|
75
|
-
ownership: LayerOwnership;
|
|
76
|
-
}): void;
|
package/dist/design-scaffold.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* directory it reads is supplied by the caller — so it stays importable from
|
|
11
11
|
* Jest's CJS transform, unlike `programmatic.ts`.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import { join,
|
|
13
|
+
import { statSync } from "node:fs";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
15
|
import { isDeepStrictEqual } from "node:util";
|
|
16
16
|
import { DESIGN_COLOR_MAP } from "./design-compile.js";
|
|
17
17
|
import { readDesignPackage } from "./design-package.js";
|
|
@@ -82,9 +82,12 @@ export function resolveDesign(ref, opts) {
|
|
|
82
82
|
const RUNTIME_COLOR_SLOT_ORDER = DESIGN_COLOR_MAP.filter((row) => row.mode === "light").map((row) => row.runtime);
|
|
83
83
|
/**
|
|
84
84
|
* Render a compiled theme's `theme:` frontmatter block (D5) — `theme:`, each
|
|
85
|
-
* treatment present as a bare `key: value
|
|
86
|
-
* `
|
|
87
|
-
*
|
|
85
|
+
* treatment present as a bare `key: value` (#7739's `darkInk`/`figures` after
|
|
86
|
+
* `checkmarks`), the `typography:` group when compiled — every role and field
|
|
87
|
+
* in the compile's order, role keys unquoted, sizes as plain scalars (#7720
|
|
88
|
+
* D2, #7739 D2.5) — then `colors:` with `light:`/`dark:` sub-blocks, each
|
|
89
|
+
* colour slot as `slot: <JSON.stringify(value)>` in `DESIGN_COLOR_MAP`'s
|
|
90
|
+
* order. Ends with a trailing newline.
|
|
88
91
|
*/
|
|
89
92
|
export function renderThemeBlock(theme) {
|
|
90
93
|
const lines = ["theme:"];
|
|
@@ -96,6 +99,24 @@ export function renderThemeBlock(theme) {
|
|
|
96
99
|
lines.push(` placeholders: ${theme.placeholders}`);
|
|
97
100
|
if (theme.checkmarks !== undefined)
|
|
98
101
|
lines.push(` checkmarks: ${theme.checkmarks}`);
|
|
102
|
+
if (theme.buttonShape !== undefined)
|
|
103
|
+
lines.push(` buttonShape: ${theme.buttonShape}`);
|
|
104
|
+
if (theme.darkInk !== undefined)
|
|
105
|
+
lines.push(` darkInk: ${theme.darkInk}`);
|
|
106
|
+
if (theme.figures !== undefined)
|
|
107
|
+
lines.push(` figures: ${theme.figures}`);
|
|
108
|
+
if (theme.typography !== undefined) {
|
|
109
|
+
lines.push(" typography:");
|
|
110
|
+
for (const [role, fields] of Object.entries(theme.typography)) {
|
|
111
|
+
lines.push(` ${role}:`);
|
|
112
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
113
|
+
lines.push(` ${field}: ${String(value)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// #7719 D6 — present only for a design that uses `solid-dark`, or (#7739 D2.3) sets `darkInk: accent`.
|
|
118
|
+
if (theme.darkBands !== undefined)
|
|
119
|
+
lines.push(` darkBands: ${theme.darkBands}`);
|
|
99
120
|
lines.push(" colors:");
|
|
100
121
|
for (const mode of ["light", "dark"]) {
|
|
101
122
|
lines.push(` ${mode}:`);
|
|
@@ -149,72 +170,3 @@ export function spliceThemeBlock(text, theme, file) {
|
|
|
149
170
|
: [...lines.slice(0, themeStart), ...rendered, ...lines.slice(themeEnd)];
|
|
150
171
|
return next.join("\n");
|
|
151
172
|
}
|
|
152
|
-
/** Forbidden extensions for a design sample's files (spec 6892 D4) — content only, never code. */
|
|
153
|
-
const SAMPLE_FORBIDDEN_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs"];
|
|
154
|
-
function hasSampleForbiddenExtension(p) {
|
|
155
|
-
return SAMPLE_FORBIDDEN_EXTENSIONS.some((ext) => p.toLowerCase().endsWith(ext));
|
|
156
|
-
}
|
|
157
|
-
/** Every file under `dir`, as `/`-separated paths relative to `dir`. */
|
|
158
|
-
function listFiles(dir) {
|
|
159
|
-
const out = [];
|
|
160
|
-
const stack = [dir];
|
|
161
|
-
for (let cur = stack.pop(); cur !== undefined; cur = stack.pop()) {
|
|
162
|
-
for (const entry of readdirSync(cur, { withFileTypes: true })) {
|
|
163
|
-
const abs = join(cur, entry.name);
|
|
164
|
-
if (entry.isDirectory())
|
|
165
|
-
stack.push(abs);
|
|
166
|
-
else if (entry.isFile())
|
|
167
|
-
out.push(relative(dir, abs).split(sep).join("/"));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return out;
|
|
171
|
-
}
|
|
172
|
-
/** Where one `sample/` file lands on a scaffold (spec 6892 D4's fixed convention). */
|
|
173
|
-
function sampleDestination(rel) {
|
|
174
|
-
if (rel.startsWith("content/"))
|
|
175
|
-
return rel;
|
|
176
|
-
return `apps/web/public/${rel.slice("public/".length)}`;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Install a design's `sample/` directory onto a scaffold (spec 6892 D4) — a
|
|
180
|
-
* FIXED, undeclared convention, not a design-package field: `sample/content/**`
|
|
181
|
-
* lands at the scaffold's `content/**`, `sample/public/**` at
|
|
182
|
-
* `apps/web/public/**`. A design with no `sample/` directory (e.g. `standard`)
|
|
183
|
-
* installs nothing.
|
|
184
|
-
*
|
|
185
|
-
* Refuses — before its first write, so a refused sample leaves `targetDir`
|
|
186
|
-
* untouched — any file outside those two roots, or any `.ts`/`.tsx`/`.js`/
|
|
187
|
-
* `.mjs` file (content only, the template §5.2 rule this design package
|
|
188
|
-
* amendment carries over). Every installed file's destination is recorded in
|
|
189
|
-
* `ownership` as `design:<name>` (`readDesignPackage(designDir).name`), the
|
|
190
|
-
* same call-scoped registry `installBundleTree` writes into.
|
|
191
|
-
*
|
|
192
|
-
* @throws Error on a file outside `content/`/`public/`, or a forbidden extension.
|
|
193
|
-
*/
|
|
194
|
-
export function installDesignSample(opts) {
|
|
195
|
-
const sampleDir = join(opts.designDir, "sample");
|
|
196
|
-
if (!existsSync(sampleDir))
|
|
197
|
-
return;
|
|
198
|
-
const name = readDesignPackage(opts.designDir).name;
|
|
199
|
-
const subject = `design:${name}`;
|
|
200
|
-
// README.md at the sample root is the ONE documented exception (spec 6892
|
|
201
|
-
// D5: "the narrative sections move to designs/<name>/sample/README.md,
|
|
202
|
-
// which is not installed") — every other file is real content and is
|
|
203
|
-
// validated against the two roots below.
|
|
204
|
-
const files = listFiles(sampleDir).filter((rel) => rel !== "README.md");
|
|
205
|
-
for (const rel of files) {
|
|
206
|
-
if (!rel.startsWith("content/") && !rel.startsWith("public/")) {
|
|
207
|
-
throw new Error(`${subject}: sample file "${rel}" is outside content/ and public/ (spec 6892 D4)`);
|
|
208
|
-
}
|
|
209
|
-
if (hasSampleForbiddenExtension(rel)) {
|
|
210
|
-
throw new Error(`${subject}: sample file "${rel}" is a code file — a design sample is content only (spec 6892 D4)`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
for (const rel of files) {
|
|
214
|
-
const destinationRel = sampleDestination(rel);
|
|
215
|
-
const destinationAbs = resolve(opts.targetDir, destinationRel);
|
|
216
|
-
mkdirSync(join(destinationAbs, ".."), { recursive: true });
|
|
217
|
-
copyFileSync(join(sampleDir, rel), destinationAbs);
|
|
218
|
-
opts.ownership.set(destinationRel, subject);
|
|
219
|
-
}
|
|
220
|
-
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ 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
|
+
export type { HomeSeedOutcome } from "./design-home.js";
|
|
13
14
|
export { SITE_ROSTER_PATH, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
|
|
14
15
|
export { SYNC_ROUTES_HELP, formatSyncRoutesReport, listTemplateRouteDirs, parseSyncRoutesArgs, syncRoutes, type RouteReport, type SyncRoutesOptions, type SyncRoutesResult, } from "./sync-routes.js";
|
|
15
16
|
export { deriveArchitecture } from "./derive-architecture.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
|
+
}
|
|
@@ -46,7 +46,18 @@ export type JsonLdDriftVerdict = {
|
|
|
46
46
|
changed: DriftNode[];
|
|
47
47
|
reason: string;
|
|
48
48
|
};
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* `after` with the standard home put back when the seeded home's JSON-LD is
|
|
51
|
+
* exactly the standard home's node followed by one `Person` per
|
|
52
|
+
* `TEAM_SEED_MEMBERS` member, in order (the people slide's `team` section,
|
|
53
|
+
* #7744 D8). Any other home, or a home the people slide did not change, is
|
|
54
|
+
* returned as it is.
|
|
55
|
+
*/
|
|
56
|
+
export declare function admitSeededPeopleSlide(before: ComposedPages, after: ComposedPages): ComposedPages;
|
|
57
|
+
/**
|
|
58
|
+
* Decide whether `apply-design <design>` left every page's JSON-LD unchanged
|
|
59
|
+
* (logo excluded), the seeded people slide's Persons admitted.
|
|
60
|
+
*/
|
|
50
61
|
export declare function assessJsonLdDrift(design: string, before: ComposedPages, after: ComposedPages): JsonLdDriftVerdict;
|
|
51
62
|
export interface MeasureApplyDesignDriftOptions {
|
|
52
63
|
/** create-ailk's built `dist/cli.js`. */
|
|
@@ -16,10 +16,17 @@
|
|
|
16
16
|
* touches the filesystem and processes only through the paths it is handed,
|
|
17
17
|
* so the driver and `__tests__/verify-scaffold-checks.test.ts` run the same
|
|
18
18
|
* procedure. Nothing here imports `@working-theory/theme`.
|
|
19
|
+
*
|
|
20
|
+
* One change is content, not drift (#7744 D8): a design that seeds an
|
|
21
|
+
* organization home's people slide adds one `Person` per seeded team member to
|
|
22
|
+
* the home's graph. `admitSeededPeopleSlide` puts the standard home back only
|
|
23
|
+
* when the seeded home is exactly that, so any other change still counts.
|
|
19
24
|
*/
|
|
20
25
|
import { execFileSync } from "node:child_process";
|
|
21
26
|
import { writeFileSync } from "node:fs";
|
|
22
27
|
import { join } from "node:path";
|
|
28
|
+
import { isDeepStrictEqual } from "node:util";
|
|
29
|
+
import { TEAM_SEED_MEMBERS } from "../team-profiles.js";
|
|
23
30
|
function describePair(row) {
|
|
24
31
|
const measured = row.status === "unmeasurable" || row.ratio === null
|
|
25
32
|
? "unmeasurable"
|
|
@@ -107,15 +114,44 @@ export function jsonLdDrift(before, after) {
|
|
|
107
114
|
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
108
115
|
return nodes.sort((x, y) => cmp(x.page, y.page) || cmp(x.path, y.path));
|
|
109
116
|
}
|
|
117
|
+
const HOME_PAGE = "content/en/HomePage/home.mdx";
|
|
118
|
+
/**
|
|
119
|
+
* `after` with the standard home put back when the seeded home's JSON-LD is
|
|
120
|
+
* exactly the standard home's node followed by one `Person` per
|
|
121
|
+
* `TEAM_SEED_MEMBERS` member, in order (the people slide's `team` section,
|
|
122
|
+
* #7744 D8). Any other home, or a home the people slide did not change, is
|
|
123
|
+
* returned as it is.
|
|
124
|
+
*/
|
|
125
|
+
export function admitSeededPeopleSlide(before, after) {
|
|
126
|
+
const standard = before[HOME_PAGE];
|
|
127
|
+
const seeded = after[HOME_PAGE];
|
|
128
|
+
if (standard === undefined || seeded === undefined || !seeded.sectionsValid)
|
|
129
|
+
return after;
|
|
130
|
+
const graph = JSON.parse(seeded.jsonld)["@graph"];
|
|
131
|
+
if (!Array.isArray(graph))
|
|
132
|
+
return after;
|
|
133
|
+
const [page, ...people] = graph;
|
|
134
|
+
const expected = TEAM_SEED_MEMBERS.map((m) => ({
|
|
135
|
+
"@context": "https://schema.org",
|
|
136
|
+
"@type": "Person",
|
|
137
|
+
name: m.name,
|
|
138
|
+
jobTitle: m.role,
|
|
139
|
+
}));
|
|
140
|
+
const isPeopleSlide = isDeepStrictEqual(page, JSON.parse(standard.jsonld)) && isDeepStrictEqual(people, expected);
|
|
141
|
+
return isPeopleSlide ? { ...after, [HOME_PAGE]: standard } : after;
|
|
142
|
+
}
|
|
110
143
|
/** How many changed nodes a drift reason lists before it stops. */
|
|
111
144
|
const REASON_NODE_LIMIT = 10;
|
|
112
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Decide whether `apply-design <design>` left every page's JSON-LD unchanged
|
|
147
|
+
* (logo excluded), the seeded people slide's Persons admitted.
|
|
148
|
+
*/
|
|
113
149
|
export function assessJsonLdDrift(design, before, after) {
|
|
114
150
|
const pages = Object.keys(before).length;
|
|
115
151
|
if (pages === 0) {
|
|
116
152
|
return { ok: false, pages, changed: [], reason: `design ${design}: no page was composed` };
|
|
117
153
|
}
|
|
118
|
-
const changed = jsonLdDrift(before, after);
|
|
154
|
+
const changed = jsonLdDrift(before, admitSeededPeopleSlide(before, after));
|
|
119
155
|
if (changed.length === 0)
|
|
120
156
|
return { ok: true, pages };
|
|
121
157
|
const listed = changed
|
|
@@ -49,6 +49,8 @@ export interface ExtractModuleBundleOptions {
|
|
|
49
49
|
existingFilePolicy?: ExistingFilePolicy;
|
|
50
50
|
/** A caller check on the manifest's `patches` (#6653 D3). See `InstallBundleTreeOptions`. */
|
|
51
51
|
checkPatches?: (patches: readonly ModulePatch[]) => void;
|
|
52
|
+
/** Destinations this scaffold does not carry (#7426 D4). See `InstallBundleTreeOptions`. */
|
|
53
|
+
skip?: (destination: string) => boolean;
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
56
|
* Extract a fetched module bundle into `targetDir`, merging its files onto
|
|
@@ -131,6 +133,15 @@ export interface InstallBundleTreeOptions {
|
|
|
131
133
|
* bundle's patches to the plus-config allowlist.
|
|
132
134
|
*/
|
|
133
135
|
checkPatches?: (patches: readonly ModulePatch[]) => void;
|
|
136
|
+
/**
|
|
137
|
+
* Scaffold-relative destinations this scaffold does not carry (#7426 D4) —
|
|
138
|
+
* a spoke passes `topology.ts` `isSpokeExcluded`. A skipped destination is
|
|
139
|
+
* never written: a plain file or an `installMap` destination it names is
|
|
140
|
+
* dropped, and so is a `patches` entry whose target it names. It is NOT an
|
|
141
|
+
* ownership claim, and a skipped patch target is not a missing one. Every
|
|
142
|
+
* entry is still validated as before. Absent ⟹ nothing is skipped.
|
|
143
|
+
*/
|
|
144
|
+
skip?: (destination: string) => boolean;
|
|
134
145
|
}
|
|
135
146
|
/** See `InstallBundleTreeOptions.existingFilePolicy`. */
|
|
136
147
|
export type ExistingFilePolicy = "overwrite" | "skip-existing";
|
|
@@ -385,6 +385,24 @@ function validateLayerOwnership(destinations, ownership, subject, isTemplate, pa
|
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
|
+
/** Every regular file under `srcRoot`, as a `/`-separated relative path. */
|
|
389
|
+
function stagedFiles(srcRoot) {
|
|
390
|
+
const out = [];
|
|
391
|
+
const stack = [srcRoot];
|
|
392
|
+
while (stack.length > 0) {
|
|
393
|
+
const dir = stack.pop();
|
|
394
|
+
if (dir === undefined)
|
|
395
|
+
break;
|
|
396
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
397
|
+
const srcPath = join(dir, entry.name);
|
|
398
|
+
if (entry.isDirectory())
|
|
399
|
+
stack.push(srcPath);
|
|
400
|
+
else if (entry.isFile())
|
|
401
|
+
out.push(relative(srcRoot, srcPath).split(sep).join("/"));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
388
406
|
/**
|
|
389
407
|
* Every scaffold-relative destination this tree will write: each unmapped
|
|
390
408
|
* staged file at its own path (exactly the set `copyTree` lands, same walk,
|
|
@@ -457,6 +475,7 @@ export async function extractModuleBundle(options) {
|
|
|
457
475
|
...(options.checkPatches !== undefined
|
|
458
476
|
? { checkPatches: options.checkPatches }
|
|
459
477
|
: {}),
|
|
478
|
+
...(options.skip !== undefined ? { skip: options.skip } : {}),
|
|
460
479
|
});
|
|
461
480
|
}
|
|
462
481
|
finally {
|
|
@@ -484,13 +503,18 @@ export function installBundleTree(options) {
|
|
|
484
503
|
const manifest = isTemplate
|
|
485
504
|
? readAilkTemplateManifest(sourceRoot, name, templateRequires, templateExtends)
|
|
486
505
|
: readAilkModuleManifest(sourceRoot);
|
|
487
|
-
const
|
|
488
|
-
validateInstallMap(
|
|
506
|
+
const declaredInstallMap = manifest?.installMap ?? {};
|
|
507
|
+
validateInstallMap(declaredInstallMap, sourceRoot, targetAbs, subject, isTemplate);
|
|
508
|
+
// #7426 D4 — a destination the scaffold does not carry is dropped here,
|
|
509
|
+
// after validation, so a spoke never receives it and never records it.
|
|
510
|
+
const skip = options.skip;
|
|
511
|
+
const isSkipped = (destination) => skip !== undefined && skip(normalize(destination).split(sep).join("/"));
|
|
512
|
+
const installMap = Object.fromEntries(Object.entries(declaredInstallMap).filter(([, destination]) => !isSkipped(destination)));
|
|
489
513
|
// patches validation (spec 5418 § 3 D6) runs at the same point, before
|
|
490
514
|
// any copy — a bad target/kind/op rejects the whole tree with nothing
|
|
491
515
|
// written to targetDir, same posture as installMap and the tar-entry
|
|
492
516
|
// validation above.
|
|
493
|
-
const
|
|
517
|
+
const declaredPatches = manifest?.patches ?? [];
|
|
494
518
|
// #5514 (Medium) — content, never code (spec 5462 D9/D16): every `patches`
|
|
495
519
|
// kind either edits a code file directly (`next-config`, `jest-config`)
|
|
496
520
|
// or a manifest a template has no legitimate reason to touch
|
|
@@ -498,10 +522,13 @@ export function installBundleTree(options) {
|
|
|
498
522
|
// content template) — no shipped template uses it, so the simplest
|
|
499
523
|
// fail-closed reading refuses it outright rather than enumerating an
|
|
500
524
|
// allowed subset that has no current user.
|
|
501
|
-
if (isTemplate &&
|
|
525
|
+
if (isTemplate && declaredPatches.length > 0) {
|
|
502
526
|
throw new Error(`${subject}: a template must not carry patches — content, never code (spec 5462 D9)`);
|
|
503
527
|
}
|
|
504
|
-
|
|
528
|
+
// Every declared patch is validated, skipped or not (#7426 D4), exactly as
|
|
529
|
+
// the installMap is above; only then are the skipped ones dropped.
|
|
530
|
+
validatePatches(declaredPatches, targetAbs, subject);
|
|
531
|
+
const patches = declaredPatches.filter((patch) => !isSkipped(patch.target));
|
|
505
532
|
options.checkPatches?.(patches);
|
|
506
533
|
// The exclusion set copyTree/the content-only guard both use — built from
|
|
507
534
|
// NORMALIZED installMap keys (#5514) so a "./x" key excludes the same
|
|
@@ -512,7 +539,10 @@ export function installBundleTree(options) {
|
|
|
512
539
|
const excluded = new Set([
|
|
513
540
|
AILK_TEMPLATE_JSON,
|
|
514
541
|
AILK_MODULE_JSON,
|
|
515
|
-
...Object.keys(
|
|
542
|
+
...Object.keys(declaredInstallMap).map((key) => normalize(key).split(sep).join("/")),
|
|
543
|
+
// A plain file lands at its own staged path, so that path IS its
|
|
544
|
+
// destination: a skipped one is excluded from the copy like a mapped source.
|
|
545
|
+
...(skip !== undefined ? stagedFiles(sourceRoot).filter(isSkipped) : []),
|
|
516
546
|
]);
|
|
517
547
|
if (isTemplate) {
|
|
518
548
|
// #6658 D7 — a template's roster of record lands as JSON, through its
|
|
@@ -20,5 +20,10 @@ export declare function appendLine(text: string, line: string): string;
|
|
|
20
20
|
* `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
|
|
21
21
|
* `ailk.key` when no existing line covers it. Idempotent — a second call with
|
|
22
22
|
* the same key changes no byte.
|
|
23
|
+
*
|
|
24
|
+
* `apiEnv: false` skips the `apps/api/.env` line — a spoke (#7426 D6) has no
|
|
25
|
+
* `apps/api`, so it keeps the key in `ailk.key` alone.
|
|
23
26
|
*/
|
|
24
|
-
export declare function writeLicenseKeyFiles(dir: string, key: string
|
|
27
|
+
export declare function writeLicenseKeyFiles(dir: string, key: string, opts?: {
|
|
28
|
+
apiEnv?: boolean;
|
|
29
|
+
}): void;
|