@pantheon-systems/create-p1-starter-kit 0.7.0 → 0.8.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/package.json +7 -4
- package/template/.env.example +6 -0
- package/template/CHANGELOG.md +20 -0
- package/template/__tests__/chatbot-flag-wiring.test.ts +1 -1
- package/template/__tests__/editor-integration.test.ts +1 -1
- package/template/__tests__/editor-route-group.test.ts +44 -0
- package/template/__tests__/page-seo.test.ts +127 -0
- package/template/__tests__/paragraph-block.test.ts +37 -0
- package/template/__tests__/sanitize-richtext.test.ts +58 -0
- package/template/__tests__/seo-metadata.test.ts +105 -0
- package/template/app/[...puckPath]/page.tsx +5 -26
- package/template/app/layout.tsx +14 -0
- package/template/app/p1/{[[...p1]] → (editor)/[[...p1]]}/editor-client.tsx +20 -21
- package/template/app/p1/{[[...p1]]/page.tsx → (editor)/[[...p1]]/p1-pages.tsx} +2 -7
- package/template/app/p1/(editor)/[[...p1]]/page.tsx +5 -0
- package/template/app/p1/(editor)/layout.tsx +13 -0
- package/template/app/page.tsx +3 -21
- package/template/app/styles.css +1 -0
- package/template/ci-examples/github-actions-sync-puck-registry.yml +9 -3
- package/template/components/puck/media-figure-block.tsx +12 -0
- package/template/components/puck/paragraph-block.tsx +11 -31
- package/template/components/puck/sanitize-richtext.ts +44 -0
- package/template/lib/page-seo.ts +79 -0
- package/template/lib/seo-metadata.ts +48 -0
- package/template/package.json +9 -5
- package/template/pnpm-workspace.yaml +3 -0
- package/template/puck.config.tsx +3 -1
- package/template/scripts/__tests__/asset-stub-hooks.test.ts +116 -3
- package/template/scripts/__tests__/sync-puck-registry.test.ts +107 -1
- package/template/scripts/asset-stub-hooks.mjs +25 -1
- package/template/scripts/sync-puck-registry.ts +84 -7
- package/template/tsconfig.test.json +6 -0
- package/template/vitest.config.ts +5 -0
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Wire in with node:module's register() — not the --import flag, which
|
|
7
7
|
* does not auto-install a resolve/load-only hooks file.
|
|
8
|
+
*
|
|
9
|
+
* The stub is a *branded* sentinel, not a bare {}: the browser bundler
|
|
10
|
+
* resolves these same imports to real URLs/StaticImageData that this script
|
|
11
|
+
* cannot compute. The brand (`__p1AssetStub`) and the marker string (returned
|
|
12
|
+
* for every property read, so `placeholder.src` stays detectable) let the
|
|
13
|
+
* sync filter recognize and skip such components instead of writing wrong
|
|
14
|
+
* descriptor content and a hash the editor will forever disagree with.
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
17
|
const ASSET_EXTENSION_PATTERN =
|
|
@@ -12,6 +19,23 @@ const ASSET_EXTENSION_PATTERN =
|
|
|
12
19
|
|
|
13
20
|
const ASSET_STUB_PROTOCOL = 'asset-stub:';
|
|
14
21
|
|
|
22
|
+
export const ASSET_STUB_MARKER = '__p1_asset_stub__';
|
|
23
|
+
|
|
24
|
+
// The get trap resolves every unknown property (including well-known symbols)
|
|
25
|
+
// to the marker string, so exotic usage of a stub (spread, iteration) throws
|
|
26
|
+
// during extraction — deliberately loud, rather than silently producing
|
|
27
|
+
// garbage.
|
|
28
|
+
const ASSET_STUB_SOURCE = `
|
|
29
|
+
const target = {
|
|
30
|
+
__p1AssetStub: true,
|
|
31
|
+
toString: () => '${ASSET_STUB_MARKER}',
|
|
32
|
+
[Symbol.toPrimitive]: () => '${ASSET_STUB_MARKER}',
|
|
33
|
+
};
|
|
34
|
+
export default new Proxy(target, {
|
|
35
|
+
get: (t, prop) => (prop in t ? t[prop] : '${ASSET_STUB_MARKER}'),
|
|
36
|
+
});
|
|
37
|
+
`;
|
|
38
|
+
|
|
15
39
|
export async function resolve(specifier, context, nextResolve) {
|
|
16
40
|
if (ASSET_EXTENSION_PATTERN.test(specifier)) {
|
|
17
41
|
return {
|
|
@@ -26,7 +50,7 @@ export async function load(url, context, nextLoad) {
|
|
|
26
50
|
if (url.startsWith(ASSET_STUB_PROTOCOL)) {
|
|
27
51
|
return {
|
|
28
52
|
format: 'module',
|
|
29
|
-
source:
|
|
53
|
+
source: ASSET_STUB_SOURCE,
|
|
30
54
|
shortCircuit: true,
|
|
31
55
|
};
|
|
32
56
|
}
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Required env vars (see validateEnv below for the full fallback contract):
|
|
10
10
|
* CSS_BASE_URL, CSS_SITE_ID, CSS_REGISTRY_API_KEY
|
|
11
|
+
* Optional:
|
|
12
|
+
* CSS_BRANCH_ID — CSS branch to target (in CI: the pushed git ref's name)
|
|
13
|
+
* CSS_DEFAULT_BRANCH — the repo's default branch name (defaults to "main");
|
|
14
|
+
* when CSS_BRANCH_ID equals it, the site's isMain branch is targeted
|
|
15
|
+
* regardless of naming (see resolveBranchId for the resolution contract)
|
|
11
16
|
*
|
|
12
17
|
* CSS_REGISTRY_API_KEY must be a sat_ site token scoped to write:registry
|
|
13
18
|
* only — do not reuse a read-scoped token (P1_CSS_API_KEY). Because that
|
|
@@ -22,12 +27,14 @@ import { pathToFileURL } from "node:url";
|
|
|
22
27
|
import { P1Client } from "@pantheon-systems/css-client";
|
|
23
28
|
import type { Branch } from "@pantheon-systems/css-client";
|
|
24
29
|
import { extractDescriptors, syncComponentRegistryWriteOnly } from "@pantheon-systems/puck-css/registry-sync";
|
|
30
|
+
import { ASSET_STUB_MARKER } from "./asset-stub-hooks.mjs";
|
|
25
31
|
|
|
26
32
|
export interface ValidatedEnv {
|
|
27
33
|
baseUrl: string;
|
|
28
34
|
siteId: string;
|
|
29
35
|
apiKey: string;
|
|
30
36
|
branchOverride?: string;
|
|
37
|
+
defaultBranchName?: string;
|
|
31
38
|
puckConfigPath: string;
|
|
32
39
|
}
|
|
33
40
|
|
|
@@ -36,6 +43,11 @@ export function validateEnv(env: Record<string, string | undefined>): ValidatedE
|
|
|
36
43
|
const siteId = env.CSS_SITE_ID ?? env.NEXT_PUBLIC_CSS_SITE_ID;
|
|
37
44
|
const apiKey = env.CSS_REGISTRY_API_KEY;
|
|
38
45
|
const branchOverride = env.CSS_BRANCH_ID ?? env.NEXT_PUBLIC_CSS_BRANCH_ID;
|
|
46
|
+
// Defaults to "main": the CSS main content branch is always literally
|
|
47
|
+
// named "main", so for repos whose default git branch is also "main" this
|
|
48
|
+
// resolves to the same branch it would have matched by name. Repos with a
|
|
49
|
+
// differently-named default branch (master, trunk) must set it explicitly.
|
|
50
|
+
const defaultBranchName = env.CSS_DEFAULT_BRANCH ?? "main";
|
|
39
51
|
const puckConfigPath = env.PUCK_CONFIG_PATH ?? "puck.config.tsx";
|
|
40
52
|
|
|
41
53
|
const missing: string[] = [];
|
|
@@ -65,6 +77,7 @@ export function validateEnv(env: Record<string, string | undefined>): ValidatedE
|
|
|
65
77
|
siteId: siteId as string,
|
|
66
78
|
apiKey: apiKey as string,
|
|
67
79
|
branchOverride,
|
|
80
|
+
defaultBranchName,
|
|
68
81
|
puckConfigPath,
|
|
69
82
|
};
|
|
70
83
|
}
|
|
@@ -82,8 +95,60 @@ export function resolveConfigModule(mod: unknown): unknown {
|
|
|
82
95
|
*/
|
|
83
96
|
export class NoBranchMatchError extends Error {}
|
|
84
97
|
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
type Descriptor = ReturnType<typeof extractDescriptors>[number];
|
|
99
|
+
|
|
100
|
+
function containsAssetStubValue(value: unknown, seen = new Set<object>()): boolean {
|
|
101
|
+
if (typeof value === "string") return value.includes(ASSET_STUB_MARKER);
|
|
102
|
+
if (typeof value !== "object" || value === null) return false;
|
|
103
|
+
if (seen.has(value)) return false;
|
|
104
|
+
seen.add(value);
|
|
105
|
+
if ((value as Record<string, unknown>).__p1AssetStub === true) return true;
|
|
106
|
+
return Object.values(value).some((nested) => containsAssetStubValue(nested, seen));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Partitions descriptors into those this script can faithfully write and
|
|
111
|
+
* those it must skip. Under the asset-stub loader, any config
|
|
112
|
+
* value derived from an asset import is a branded sentinel — the browser
|
|
113
|
+
* bundler resolves the same import to a real URL this script cannot know.
|
|
114
|
+
* Writing such a descriptor stores wrong default values and an index hash
|
|
115
|
+
* the editor will disagree with on every load (a perpetual re-register
|
|
116
|
+
* flip-flop between CI and editor). Skipped components stay editor-owned:
|
|
117
|
+
* one writer per component, no disagreement.
|
|
118
|
+
*
|
|
119
|
+
* Scans the whole descriptor, not just defaultProps, so a stub value that
|
|
120
|
+
* reaches any future descriptor field is still caught.
|
|
121
|
+
*/
|
|
122
|
+
export function filterAssetStubbedDescriptors(descriptors: Descriptor[]): {
|
|
123
|
+
writable: Descriptor[];
|
|
124
|
+
skipped: Descriptor[];
|
|
125
|
+
} {
|
|
126
|
+
const writable: Descriptor[] = [];
|
|
127
|
+
const skipped: Descriptor[] = [];
|
|
128
|
+
for (const descriptor of descriptors) {
|
|
129
|
+
(containsAssetStubValue(descriptor) ? skipped : writable).push(descriptor);
|
|
130
|
+
}
|
|
131
|
+
return { writable, skipped };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolution contract: an override (in CI, always the pushed git ref's name)
|
|
136
|
+
* matches a CSS branch by id or name — EXCEPT when it equals the repo's
|
|
137
|
+
* default branch name, which always resolves the site's isMain branch. CSS
|
|
138
|
+
* main is always literally named "main", so without that rule a repo whose
|
|
139
|
+
* default branch is "master"/"trunk" would silently skip on every push. The
|
|
140
|
+
* default branch means the main registry, even over a coincidental CSS
|
|
141
|
+
* branch named e.g. "master".
|
|
142
|
+
*/
|
|
143
|
+
export function resolveBranchId(
|
|
144
|
+
branches: Branch[],
|
|
145
|
+
siteId: string,
|
|
146
|
+
override?: string,
|
|
147
|
+
defaultBranchName?: string,
|
|
148
|
+
): string {
|
|
149
|
+
const isDefaultBranch =
|
|
150
|
+
override !== undefined && override !== "" && override === defaultBranchName;
|
|
151
|
+
if (override !== undefined && override !== "" && !isDefaultBranch) {
|
|
87
152
|
const match = branches.find((b) => b.id === override || b.name === override);
|
|
88
153
|
if (match === undefined) {
|
|
89
154
|
throw new NoBranchMatchError(`No branch matching "${override}" found for site ${siteId}`);
|
|
@@ -104,29 +169,41 @@ async function main(): Promise<void> {
|
|
|
104
169
|
// never installs a process-wide loader hook.
|
|
105
170
|
register("./asset-stub-hooks.mjs", import.meta.url);
|
|
106
171
|
|
|
107
|
-
const { baseUrl, siteId, apiKey, branchOverride, puckConfigPath } =
|
|
172
|
+
const { baseUrl, siteId, apiKey, branchOverride, defaultBranchName, puckConfigPath } =
|
|
173
|
+
validateEnv(process.env);
|
|
108
174
|
|
|
109
175
|
const configUrl = pathToFileURL(path.resolve(process.cwd(), puckConfigPath)).href;
|
|
110
176
|
const mod: unknown = await import(configUrl);
|
|
111
177
|
const puckConfig = resolveConfigModule(mod);
|
|
112
178
|
|
|
113
179
|
const descriptors = extractDescriptors(puckConfig);
|
|
180
|
+
const { writable, skipped } = filterAssetStubbedDescriptors(descriptors);
|
|
181
|
+
|
|
182
|
+
for (const descriptor of skipped) {
|
|
183
|
+
console.warn(
|
|
184
|
+
`[sync-puck-registry] SKIPPED ${descriptor.name}: its defaults use bundler-resolved asset imports ` +
|
|
185
|
+
`this script cannot faithfully compute — it will register from the editor instead. ` +
|
|
186
|
+
`To include it in CI sync, replace imported assets in defaultProps with plain string paths.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
114
189
|
|
|
115
190
|
const client = new P1Client({ baseUrl, apiKey });
|
|
116
191
|
const branches = await client.branches.list(siteId);
|
|
117
|
-
const branchId = resolveBranchId(branches, siteId, branchOverride);
|
|
192
|
+
const branchId = resolveBranchId(branches, siteId, branchOverride, defaultBranchName);
|
|
118
193
|
|
|
119
194
|
if (dryRun) {
|
|
120
195
|
console.log(
|
|
121
|
-
`[sync-puck-registry] Dry run: ${String(
|
|
196
|
+
`[sync-puck-registry] Dry run: ${String(writable.length)} component descriptor(s) to write ` +
|
|
197
|
+
`(${String(skipped.length)} skipped) for site ${siteId}, branch ${branchId}. No writes performed.`,
|
|
122
198
|
);
|
|
123
199
|
return;
|
|
124
200
|
}
|
|
125
201
|
|
|
126
|
-
const result = await syncComponentRegistryWriteOnly(client, siteId, branchId,
|
|
202
|
+
const result = await syncComponentRegistryWriteOnly(client, siteId, branchId, writable);
|
|
127
203
|
console.log(
|
|
128
204
|
`[sync-puck-registry] Synced site ${siteId}, branch ${branchId}: ` +
|
|
129
|
-
`wrote ${String(result.total)} component descriptor(s) + registry index
|
|
205
|
+
`wrote ${String(result.total)} component descriptor(s) + registry index` +
|
|
206
|
+
(skipped.length > 0 ? ` (${String(skipped.length)} asset-bearing component(s) left to the editor)` : ""),
|
|
130
207
|
);
|
|
131
208
|
}
|
|
132
209
|
|