@pantheon-systems/create-p1-starter-kit 0.5.0 → 0.7.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 +4 -2
- package/template/.env.example +6 -0
- package/template/CHANGELOG.md +19 -0
- package/template/__tests__/chatbot-flag-gate.test.ts +25 -0
- package/template/__tests__/chatbot-flag-wiring.test.ts +45 -0
- package/template/app/p1/[[...p1]]/editor-client.tsx +25 -3
- package/template/ci-examples/github-actions-sync-puck-registry.yml +51 -0
- package/template/components/ChatbotFlagProvider.tsx +49 -0
- package/template/constants/assets.ts +3 -0
- package/template/lib/chatbot-flag/feature-gate.ts +17 -0
- package/template/package.json +9 -3
- package/template/public/images/p1_logo.svg +12 -0
- package/template/scripts/__tests__/asset-stub-hooks.test.ts +64 -0
- package/template/scripts/__tests__/sync-puck-registry.test.ts +124 -0
- package/template/scripts/asset-stub-hooks.mjs +34 -0
- package/template/scripts/sync-puck-registry.ts +148 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pantheon-systems/create-p1-starter-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Scaffold a new P1 starter project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"node": ">=20.12.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"vitest": "^4.1.5"
|
|
36
|
+
"vitest": "^4.1.5",
|
|
37
|
+
"@pantheon-systems/css-client": "0.7.0",
|
|
38
|
+
"@pantheon-systems/puck-css": "0.7.0"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
41
|
"@clack/prompts": "^1.5.1",
|
package/template/.env.example
CHANGED
|
@@ -19,3 +19,9 @@ CSS_API_KEY=your-api-key
|
|
|
19
19
|
# Show the RoleSwitcher dropdown in the P1 editor for local testing of
|
|
20
20
|
# admin/editor/junior-editor permissions. Never enable this in production.
|
|
21
21
|
# NEXT_PUBLIC_ENABLE_ROLE_SWITCHER=true
|
|
22
|
+
|
|
23
|
+
# --- AI chatbot (optional) ---
|
|
24
|
+
# LaunchDarkly client-side ID used to evaluate the `p1-chatbot` flag. It is
|
|
25
|
+
# public by design (safe to expose in the browser). When unset, the chatbot
|
|
26
|
+
# stays hidden.
|
|
27
|
+
# NEXT_PUBLIC_LD_CLIENT_ID=your-launchdarkly-client-side-id
|
package/template/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @pantheon-systems/p1-starter
|
|
2
2
|
|
|
3
|
+
## 1.0.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [b0254ff]
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- Updated dependencies [e937842]
|
|
10
|
+
- Updated dependencies [e937842]
|
|
11
|
+
- @pantheon-systems/puck-css@0.7.0
|
|
12
|
+
- @pantheon-systems/css-client@0.7.0
|
|
13
|
+
- @pantheon-systems/p1-next-sdk@0.7.0
|
|
14
|
+
|
|
15
|
+
## 1.0.5
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- @pantheon-systems/puck-css@0.6.0
|
|
20
|
+
- @pantheon-systems/p1-next-sdk@0.6.0
|
|
21
|
+
|
|
3
22
|
## 1.0.4
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { shouldShowChatbot, CHATBOT_FLAG_KEY } from "../lib/chatbot-flag/feature-gate";
|
|
3
|
+
|
|
4
|
+
describe("shouldShowChatbot", () => {
|
|
5
|
+
it("is off when the flag is disabled, even with an agent URL", () => {
|
|
6
|
+
expect(shouldShowChatbot(false, "https://agent.example")).toBe(false);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("is off when the agent URL is missing, even with the flag enabled", () => {
|
|
10
|
+
expect(shouldShowChatbot(true, undefined)).toBe(false);
|
|
11
|
+
expect(shouldShowChatbot(true, "")).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("is on only when the flag is enabled AND the agent URL is set", () => {
|
|
15
|
+
expect(shouldShowChatbot(true, "https://agent.example")).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("defaults off when the flag value is undefined (LD not yet resolved / offline)", () => {
|
|
19
|
+
expect(shouldShowChatbot(undefined, "https://agent.example")).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("exposes the p1-chatbot flag key", () => {
|
|
23
|
+
expect(CHATBOT_FLAG_KEY).toBe("p1-chatbot");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { resolve, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const appDir = resolve(__dirname, "..");
|
|
8
|
+
|
|
9
|
+
describe("editor-client gates the chatbot behind the p1-chatbot flag", () => {
|
|
10
|
+
const content = readFileSync(
|
|
11
|
+
resolve(appDir, "app/p1/[[...p1]]/editor-client.tsx"),
|
|
12
|
+
"utf-8",
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
it("reads LaunchDarkly flags via useFlags", () => {
|
|
16
|
+
expect(content).toContain("useFlags");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("gates the AI plugin through shouldShowChatbot", () => {
|
|
20
|
+
expect(content).toContain("shouldShowChatbot");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("wraps the editor in the chatbot flag provider", () => {
|
|
24
|
+
expect(content).toContain("ChatbotFlagProvider");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("imports the plugin from the published @pantheon-systems/p1-ai-chat package", () => {
|
|
28
|
+
expect(content).toContain("@pantheon-systems/p1-ai-chat");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("chatbot flag provider is a client-side LaunchDarkly gate", () => {
|
|
33
|
+
const content = readFileSync(
|
|
34
|
+
resolve(appDir, "components/ChatbotFlagProvider.tsx"),
|
|
35
|
+
"utf-8",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
it("initializes from the public client-side ID env var", () => {
|
|
39
|
+
expect(content).toContain("NEXT_PUBLIC_LD_CLIENT_ID");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("uses the launchdarkly-react-client-sdk", () => {
|
|
43
|
+
expect(content).toContain("launchdarkly-react-client-sdk");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -14,14 +14,19 @@ import {
|
|
|
14
14
|
editorPathHref,
|
|
15
15
|
} from "@pantheon-systems/puck-css";
|
|
16
16
|
import { P1NextRouterProvider } from "@pantheon-systems/p1-next-sdk";
|
|
17
|
+
import { createAIChatPlugin } from "@pantheon-systems/p1-ai-chat";
|
|
18
|
+
import { useFlags } from "launchdarkly-react-client-sdk";
|
|
17
19
|
import type { Checkpoint } from "@pantheon-systems/puck-css";
|
|
18
20
|
import type { ContentRole } from "@pantheon-systems/puck-css";
|
|
21
|
+
import { P1_ASSETS } from "../../../constants/assets";
|
|
19
22
|
|
|
20
23
|
import "@pantheon-systems/puck-css/styles.css";
|
|
21
24
|
import "@pantheon-systems/puck-css/pds/styles.css";
|
|
22
25
|
|
|
26
|
+
import { ChatbotFlagProvider } from "../../../components/ChatbotFlagProvider";
|
|
23
27
|
import { P1Lockup } from "../../../components/p1-lockup";
|
|
24
28
|
import config from "../../../puck.config";
|
|
29
|
+
import { shouldShowChatbot, CHATBOT_FLAG_KEY } from "../../../lib/chatbot-flag/feature-gate";
|
|
25
30
|
|
|
26
31
|
const DEFAULT_PAGE_DATA = {
|
|
27
32
|
root: { props: { title: "New page" } },
|
|
@@ -128,7 +133,9 @@ export function EditorClientWrapper({ path }: { path: string }) {
|
|
|
128
133
|
config={{ ...p1Config, userRole }}
|
|
129
134
|
loginFallback={<P1SignInPage />}
|
|
130
135
|
>
|
|
131
|
-
<
|
|
136
|
+
<ChatbotFlagProvider>
|
|
137
|
+
<EditorContent path={path} lastGoodStateRef={lastGoodStateRef} />
|
|
138
|
+
</ChatbotFlagProvider>
|
|
132
139
|
</P1App>
|
|
133
140
|
{process.env.NEXT_PUBLIC_ENABLE_ROLE_SWITCHER === 'true' && (
|
|
134
141
|
<RoleSwitcher currentRole={userRole} onRoleChange={setUserRole} />
|
|
@@ -198,6 +205,20 @@ function EditorContent({
|
|
|
198
205
|
const router = useRouter();
|
|
199
206
|
const { getToken } = useP1Auth();
|
|
200
207
|
const p1Plugins = useP1Plugins(path, config);
|
|
208
|
+
const flags = useFlags();
|
|
209
|
+
const agentUrl = process.env.NEXT_PUBLIC_AGENT_URL;
|
|
210
|
+
const chatbotEnabled = shouldShowChatbot(flags[CHATBOT_FLAG_KEY], agentUrl);
|
|
211
|
+
const aiPlugin = React.useMemo(
|
|
212
|
+
() =>
|
|
213
|
+
chatbotEnabled && agentUrl
|
|
214
|
+
? createAIChatPlugin({ agentUrl })
|
|
215
|
+
: null,
|
|
216
|
+
[chatbotEnabled, agentUrl],
|
|
217
|
+
);
|
|
218
|
+
const additionalPlugins = React.useMemo(
|
|
219
|
+
() => (aiPlugin ? [...p1Plugins, aiPlugin] : p1Plugins),
|
|
220
|
+
[p1Plugins, aiPlugin],
|
|
221
|
+
) as typeof p1Plugins;
|
|
201
222
|
|
|
202
223
|
const [redirecting, setRedirecting] = React.useState(false);
|
|
203
224
|
|
|
@@ -236,13 +257,14 @@ function EditorContent({
|
|
|
236
257
|
const { loading, error, puckKey, puckProps } = useP1Editor({
|
|
237
258
|
documentPath: path,
|
|
238
259
|
puckConfig: editorConfig,
|
|
239
|
-
additionalPlugins
|
|
260
|
+
additionalPlugins,
|
|
240
261
|
onDocumentNotFound: handleDocumentNotFound,
|
|
241
262
|
pluginOptions: {
|
|
242
263
|
onDocumentSelect: handleDocumentSelect,
|
|
243
264
|
selectedDocumentPath: path,
|
|
244
265
|
siteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
|
|
245
266
|
dashboardUrl: process.env.NEXT_PUBLIC_P1_ADMIN_DASHBOARD_URL,
|
|
267
|
+
logoUrl: P1_ASSETS.LOGO_URL,
|
|
246
268
|
},
|
|
247
269
|
overrideOptions: {
|
|
248
270
|
showDefaultPublish: false,
|
|
@@ -337,7 +359,7 @@ function EditorContent({
|
|
|
337
359
|
</div>
|
|
338
360
|
)}
|
|
339
361
|
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
340
|
-
<Puck key={displayState.puckKey} {...displayState.puckProps as any} _experimentalFullScreenCanvas={true} />
|
|
362
|
+
<Puck key={`${displayState.puckKey}-${chatbotEnabled ? "ai" : "no-ai"}`} {...displayState.puckProps as any} _experimentalFullScreenCanvas={true} />
|
|
341
363
|
</div>
|
|
342
364
|
);
|
|
343
365
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Optional: syncs this site's Puck component registry to the CSS backend
|
|
2
|
+
# headlessly on every push, without anyone needing to open the editor.
|
|
3
|
+
#
|
|
4
|
+
# This file is NOT active until you copy it into .github/workflows/ yourself
|
|
5
|
+
# — it deliberately lives outside that directory in the scaffolded project so
|
|
6
|
+
# it can never auto-run before you've provisioned secrets.
|
|
7
|
+
#
|
|
8
|
+
# Setup:
|
|
9
|
+
# 1. Create a sat_ site token scoped to write:registry ONLY (do not reuse
|
|
10
|
+
# your read-scoped P1_CSS_API_KEY / SSR token for this).
|
|
11
|
+
# 2. Add repo secrets: CSS_BASE_URL, CSS_SITE_ID, CSS_REGISTRY_API_KEY.
|
|
12
|
+
# 3. Copy this file to .github/workflows/sync-puck-registry.yml.
|
|
13
|
+
#
|
|
14
|
+
# Triggers on push to any branch that touches puck.config.tsx or
|
|
15
|
+
# components/puck/** — the sync script resolves the CSS branch by matching
|
|
16
|
+
# the pushed git branch's name (falling back to the site's main branch when
|
|
17
|
+
# no CSS_BRANCH_ID is given). A push on a branch with no matching CSS branch
|
|
18
|
+
# is not an error: the script logs a skip and exits 0.
|
|
19
|
+
name: Sync Puck Component Registry
|
|
20
|
+
|
|
21
|
+
on:
|
|
22
|
+
push:
|
|
23
|
+
branches:
|
|
24
|
+
- '**'
|
|
25
|
+
paths:
|
|
26
|
+
- 'puck.config.tsx'
|
|
27
|
+
- 'components/puck/**'
|
|
28
|
+
workflow_dispatch:
|
|
29
|
+
inputs:
|
|
30
|
+
branch_id:
|
|
31
|
+
description: 'CSS branch ID/name to sync against (blank = match the current git branch, else site main)'
|
|
32
|
+
required: false
|
|
33
|
+
default: ''
|
|
34
|
+
|
|
35
|
+
jobs:
|
|
36
|
+
sync-registry:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
- uses: actions/setup-node@v4
|
|
41
|
+
with:
|
|
42
|
+
node-version: '22'
|
|
43
|
+
cache: npm
|
|
44
|
+
- run: npm ci
|
|
45
|
+
- name: Sync component registry
|
|
46
|
+
run: npm run sync:registry
|
|
47
|
+
env:
|
|
48
|
+
CSS_BASE_URL: ${{ secrets.CSS_BASE_URL }}
|
|
49
|
+
CSS_SITE_ID: ${{ secrets.CSS_SITE_ID }}
|
|
50
|
+
CSS_REGISTRY_API_KEY: ${{ secrets.CSS_REGISTRY_API_KEY }}
|
|
51
|
+
CSS_BRANCH_ID: ${{ github.event.inputs.branch_id || github.ref_name }}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { LDProvider } from "launchdarkly-react-client-sdk";
|
|
5
|
+
import { useP1Auth } from "@pantheon-systems/puck-css";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Wraps the editor with a LaunchDarkly client-side provider so the `p1-chatbot`
|
|
9
|
+
* flag can be evaluated at runtime. The client-side ID is public by design.
|
|
10
|
+
*
|
|
11
|
+
* When NEXT_PUBLIC_LD_CLIENT_ID is unset (local dev / offline), LaunchDarkly is
|
|
12
|
+
* not initialized and children render without a provider — `useFlags()` then
|
|
13
|
+
* returns no flags, so the chatbot defaults to hidden.
|
|
14
|
+
*/
|
|
15
|
+
export function ChatbotFlagProvider({
|
|
16
|
+
children,
|
|
17
|
+
}: {
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
const clientSideID = process.env.NEXT_PUBLIC_LD_CLIENT_ID;
|
|
21
|
+
const { user } = useP1Auth();
|
|
22
|
+
|
|
23
|
+
if (!clientSideID) {
|
|
24
|
+
return <>{children}</>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// LaunchDarkly evaluates the flag for the context present at mount and does not
|
|
28
|
+
// re-identify on context change. This provider mounts inside <P1App> (after
|
|
29
|
+
// auth), so the authenticated user is available here; the anonymous fallback
|
|
30
|
+
// only applies if it ever renders pre-auth.
|
|
31
|
+
//
|
|
32
|
+
// Key on the always-present, stable user id — email is optional on AuthUser, so
|
|
33
|
+
// keying on it would silently drop emailless users into the anonymous branch and
|
|
34
|
+
// lose per-user rollout stickiness. (LaunchDarkly also favors a non-PII key.)
|
|
35
|
+
// Email is kept as a targeting attribute.
|
|
36
|
+
const context = user
|
|
37
|
+
? { kind: "user" as const, key: user.id, email: user.email }
|
|
38
|
+
: { kind: "user" as const, key: "anonymous", anonymous: true };
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<LDProvider
|
|
42
|
+
clientSideID={clientSideID}
|
|
43
|
+
context={context}
|
|
44
|
+
reactOptions={{ useCamelCaseFlagKeys: false }}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</LDProvider>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** LaunchDarkly flag that gates the AI chatbot. Short-lived — removed once the
|
|
2
|
+
* chatbot ships to everyone in the alpha. */
|
|
3
|
+
export const CHATBOT_FLAG_KEY = "p1-chatbot";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether the AI chatbot plugin should be mounted in the editor.
|
|
7
|
+
*
|
|
8
|
+
* Requires both the `p1-chatbot` LaunchDarkly flag to be enabled and an agent
|
|
9
|
+
* URL to be configured. Defaults off when the flag is `undefined` (LD not yet
|
|
10
|
+
* resolved, unset client ID, or offline), so the chatbot stays hidden by default.
|
|
11
|
+
*/
|
|
12
|
+
export function shouldShowChatbot(
|
|
13
|
+
flagEnabled: boolean | undefined,
|
|
14
|
+
agentUrl: string | undefined,
|
|
15
|
+
): boolean {
|
|
16
|
+
return Boolean(flagEnabled && agentUrl);
|
|
17
|
+
}
|
package/template/package.json
CHANGED
|
@@ -7,15 +7,20 @@
|
|
|
7
7
|
"build": "next build",
|
|
8
8
|
"start": "next start",
|
|
9
9
|
"test": "vitest run",
|
|
10
|
-
"lint": "eslint ."
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"sync:registry": "tsx scripts/sync-puck-registry.ts"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@pantheon-systems/cpub-react-sdk": "^5.2.1",
|
|
14
|
-
"@pantheon-systems/
|
|
15
|
-
"@pantheon-systems/
|
|
15
|
+
"@pantheon-systems/css-client": "^0.7.0",
|
|
16
|
+
"@pantheon-systems/p1-ai-chat": "^0.1.0",
|
|
17
|
+
"@pantheon-systems/p1-next-sdk": "^0.7.0",
|
|
18
|
+
"@pantheon-systems/pds-toolkit-react": "2.0.0-alpha.44",
|
|
19
|
+
"@pantheon-systems/puck-css": "^0.7.0",
|
|
16
20
|
"@puckeditor/core": "^0.21.1",
|
|
17
21
|
"@tailwindcss/postcss": "^4.2.2",
|
|
18
22
|
"classnames": "^2.5.1",
|
|
23
|
+
"launchdarkly-react-client-sdk": "^3.9.2",
|
|
19
24
|
"next": "^16.2.6",
|
|
20
25
|
"postcss": "^8.5.12",
|
|
21
26
|
"react": "^19.2.5",
|
|
@@ -28,6 +33,7 @@
|
|
|
28
33
|
"@types/react": "^19.2.14",
|
|
29
34
|
"@types/react-dom": "^19.2.3",
|
|
30
35
|
"eslint": "^9.27.0",
|
|
36
|
+
"tsx": "^4.23.1",
|
|
31
37
|
"typescript": "^5.9.3",
|
|
32
38
|
"vitest": "^4.1.5",
|
|
33
39
|
"@eslint/js": "^9.27.0",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg width="40" height="33" viewBox="0 0 40 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M1.47059 0L4.41354 7.08983H0.667969L1.8719 10.1666H9.49682L1.47059 0Z" fill="#FFDC28"/>
|
|
3
|
+
<path d="M11.4372 25.7508L10.1664 22.6741H8.42739L4.81559 13.9121H3.27723L6.88903 22.6741H2.47461L10.6346 32.8406L7.69166 25.7508H11.4372Z" fill="#FFDC28"/>
|
|
4
|
+
<path d="M12.4403 19.5305H7.69141L8.69468 21.9384H12.4403C12.5071 21.9384 12.7747 21.8046 12.7747 20.7345C12.7078 19.6643 12.5071 19.5305 12.4403 19.5305Z" fill="#23232D"/>
|
|
5
|
+
<path d="M12.9088 16.6543H6.55469L7.55797 19.0622H12.9088C12.9757 19.0622 13.2432 18.9284 13.2432 17.8582C13.1763 16.7881 12.9757 16.6543 12.9088 16.6543Z" fill="#23232D"/>
|
|
6
|
+
<path d="M12.4397 13.3102C12.5066 13.3102 12.7741 13.1764 12.7741 12.1063C12.7741 11.0361 12.5735 10.9023 12.4397 10.9023H7.22266L8.22593 13.3102H12.4397Z" fill="#23232D"/>
|
|
7
|
+
<path d="M9.36461 16.1862H12.8426C12.9095 16.1862 13.1771 16.0524 13.1771 14.9823C13.1771 13.9121 12.9764 13.7783 12.8426 13.7783H8.36133L9.36461 16.1862Z" fill="#23232D"/>
|
|
8
|
+
<path d="M12.4403 19.5305H7.69141L8.69468 21.9384H12.4403C12.5071 21.9384 12.7747 21.8046 12.7747 20.7345C12.7078 19.6643 12.5071 19.5305 12.4403 19.5305Z" fill="#23232D"/>
|
|
9
|
+
<path d="M12.9088 16.6545H6.55469L7.55797 19.0624H12.9088C12.9757 19.0624 13.2432 18.9286 13.2432 17.8585C13.1763 16.7883 12.9757 16.6545 12.9088 16.6545Z" fill="#23232D"/>
|
|
10
|
+
<path d="M3.6118 16.1863L2.47475 13.3102H5.08328L6.28721 16.1863H8.76196L6.55475 10.9023H1.13705C0.735737 10.9023 0.468196 10.9023 0.267541 11.5043C0.066885 12.24 0 13.6446 0 16.3869C0 19.1292 -2.59134e-07 20.5338 0.267541 21.2696C0.468196 21.8715 0.668852 21.8715 1.13705 21.8715H5.8859L3.6118 16.1863Z" fill="#23232D"/>
|
|
11
|
+
<path d="M21.0527 23.9409V9.39014H26.5117C27.6315 9.39014 28.569 9.59847 29.3242 10.0151C30.0859 10.4318 30.6621 11.0047 31.0527 11.7339C31.4434 12.4631 31.6387 13.2899 31.6387 14.2144C31.6387 15.1453 31.4401 15.9754 31.043 16.7046C30.6523 17.4272 30.0729 17.9969 29.3047 18.4136C28.5365 18.8237 27.5924 19.0288 26.4727 19.0288H22.8594V16.8608H26.1113C26.7689 16.8608 27.306 16.7502 27.7227 16.5288C28.1458 16.3009 28.4551 15.9884 28.6504 15.5913C28.8522 15.1877 28.9531 14.7287 28.9531 14.2144C28.9531 13.6935 28.8522 13.2378 28.6504 12.8472C28.4551 12.45 28.1458 12.144 27.7227 11.9292C27.306 11.7078 26.7656 11.5972 26.1016 11.5972H23.6895V23.9409H21.0527ZM38.7676 9.39014V23.9409H36.1504V11.9585H36.0625L32.6641 14.1362V11.7144L36.2773 9.39014H38.7676Z" fill="#23232D"/>
|
|
12
|
+
</svg>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { resolve, load } from "../asset-stub-hooks.mjs";
|
|
3
|
+
|
|
4
|
+
describe("resolve", () => {
|
|
5
|
+
it("short-circuits CSS imports to an asset-stub URL without calling nextResolve", async () => {
|
|
6
|
+
const nextResolve = vi.fn();
|
|
7
|
+
const result = await resolve("./styles.css", {}, nextResolve);
|
|
8
|
+
expect(result.shortCircuit).toBe(true);
|
|
9
|
+
expect(result.url.startsWith("asset-stub:")).toBe(true);
|
|
10
|
+
expect(nextResolve).not.toHaveBeenCalled();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it.each([
|
|
14
|
+
"./logo.png", "./photo.jpg", "./photo.jpeg", "./icon.svg", "./anim.gif",
|
|
15
|
+
"./banner.webp", "./favicon.ico", "./sprite.bmp", "./photo.avif",
|
|
16
|
+
"./font.woff", "./font.woff2", "./font.ttf", "./font.eot", "./font.otf",
|
|
17
|
+
"./clip.mp4", "./clip.webm", "./clip.mov", "./audio.mp3", "./audio.wav",
|
|
18
|
+
"./theme.scss", "./theme.sass", "./theme.less",
|
|
19
|
+
])("short-circuits %s", async (specifier) => {
|
|
20
|
+
const nextResolve = vi.fn();
|
|
21
|
+
const result = await resolve(specifier, {}, nextResolve);
|
|
22
|
+
expect(result.shortCircuit).toBe(true);
|
|
23
|
+
expect(nextResolve).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("passes .ts specifiers through to nextResolve unchanged", async () => {
|
|
27
|
+
const nextResolve = vi.fn().mockResolvedValue({ url: "file:///abs/path.ts", shortCircuit: true });
|
|
28
|
+
const result = await resolve("./puck.config.tsx", {}, nextResolve);
|
|
29
|
+
expect(nextResolve).toHaveBeenCalledWith("./puck.config.tsx", {});
|
|
30
|
+
expect(result.url).toBe("file:///abs/path.ts");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("passes bare package specifiers through to nextResolve unchanged", async () => {
|
|
34
|
+
const nextResolve = vi.fn().mockResolvedValue({ url: "file:///node_modules/react/index.js", shortCircuit: true });
|
|
35
|
+
await resolve("react", {}, nextResolve);
|
|
36
|
+
expect(nextResolve).toHaveBeenCalledWith("react", {});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("propagates a real module-resolution error from nextResolve instead of masking it", async () => {
|
|
40
|
+
const nextResolve = vi.fn().mockRejectedValue(new Error("Cannot find module"));
|
|
41
|
+
await expect(resolve("./missing-module", {}, nextResolve)).rejects.toThrow("Cannot find module");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("load", () => {
|
|
46
|
+
it("returns an empty default export for asset-stub URLs without calling nextLoad", async () => {
|
|
47
|
+
const nextLoad = vi.fn();
|
|
48
|
+
const result = await load("asset-stub:.%2Fstyles.css", {}, nextLoad);
|
|
49
|
+
expect(result).toEqual({ format: "module", source: "export default {};", shortCircuit: true });
|
|
50
|
+
expect(nextLoad).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("passes non-asset-stub URLs through to nextLoad unchanged", async () => {
|
|
54
|
+
const nextLoad = vi.fn().mockResolvedValue({ format: "module", source: "export default 1;", shortCircuit: true });
|
|
55
|
+
const result = await load("file:///abs/path.ts", {}, nextLoad);
|
|
56
|
+
expect(nextLoad).toHaveBeenCalledWith("file:///abs/path.ts", {});
|
|
57
|
+
expect(result.source).toBe("export default 1;");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("propagates a real load error from nextLoad instead of masking it", async () => {
|
|
61
|
+
const nextLoad = vi.fn().mockRejectedValue(new Error("Syntax error"));
|
|
62
|
+
await expect(load("file:///abs/broken.ts", {}, nextLoad)).rejects.toThrow("Syntax error");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { validateEnv, resolveConfigModule, resolveBranchId, NoBranchMatchError } from "../sync-puck-registry.js";
|
|
3
|
+
|
|
4
|
+
function baseEnv(overrides: Record<string, string | undefined> = {}): Record<string, string | undefined> {
|
|
5
|
+
return {
|
|
6
|
+
CSS_BASE_URL: "https://css.example.com",
|
|
7
|
+
CSS_SITE_ID: "site-123",
|
|
8
|
+
CSS_REGISTRY_API_KEY: "sat_registrytoken",
|
|
9
|
+
...overrides,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("validateEnv", () => {
|
|
14
|
+
it("accepts CSS_BASE_URL/CSS_SITE_ID/CSS_REGISTRY_API_KEY directly", () => {
|
|
15
|
+
const result = validateEnv(baseEnv());
|
|
16
|
+
expect(result.baseUrl).toBe("https://css.example.com");
|
|
17
|
+
expect(result.siteId).toBe("site-123");
|
|
18
|
+
expect(result.apiKey).toBe("sat_registrytoken");
|
|
19
|
+
expect(result.branchOverride).toBeUndefined();
|
|
20
|
+
expect(result.puckConfigPath).toBe("puck.config.tsx");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("falls back to NEXT_PUBLIC_CSS_BASE_URL and NEXT_PUBLIC_CSS_SITE_ID", () => {
|
|
24
|
+
const result = validateEnv(
|
|
25
|
+
baseEnv({
|
|
26
|
+
CSS_BASE_URL: undefined,
|
|
27
|
+
CSS_SITE_ID: undefined,
|
|
28
|
+
NEXT_PUBLIC_CSS_BASE_URL: "https://fallback.example.com",
|
|
29
|
+
NEXT_PUBLIC_CSS_SITE_ID: "site-fallback",
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
expect(result.baseUrl).toBe("https://fallback.example.com");
|
|
33
|
+
expect(result.siteId).toBe("site-fallback");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("falls back to NEXT_PUBLIC_CSS_BRANCH_ID for the branch override", () => {
|
|
37
|
+
const result = validateEnv(baseEnv({ NEXT_PUBLIC_CSS_BRANCH_ID: "staging" }));
|
|
38
|
+
expect(result.branchOverride).toBe("staging");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("prefers CSS_BRANCH_ID over the NEXT_PUBLIC_ fallback when both are set", () => {
|
|
42
|
+
const result = validateEnv(baseEnv({ CSS_BRANCH_ID: "explicit", NEXT_PUBLIC_CSS_BRANCH_ID: "staging" }));
|
|
43
|
+
expect(result.branchOverride).toBe("explicit");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("defaults PUCK_CONFIG_PATH to puck.config.tsx", () => {
|
|
47
|
+
const result = validateEnv(baseEnv());
|
|
48
|
+
expect(result.puckConfigPath).toBe("puck.config.tsx");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("honors an explicit PUCK_CONFIG_PATH", () => {
|
|
52
|
+
const result = validateEnv(baseEnv({ PUCK_CONFIG_PATH: "config/puck.config.tsx" }));
|
|
53
|
+
expect(result.puckConfigPath).toBe("config/puck.config.tsx");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("reports all missing required vars together, not just the first", () => {
|
|
57
|
+
expect(() => validateEnv({})).toThrowError(/CSS_BASE_URL[\s\S]*CSS_SITE_ID[\s\S]*CSS_REGISTRY_API_KEY/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("gives an explicit, actionable message when only P1_CSS_API_KEY is set (do not reuse the read-only token)", () => {
|
|
61
|
+
expect(() =>
|
|
62
|
+
validateEnv(baseEnv({ CSS_REGISTRY_API_KEY: undefined, P1_CSS_API_KEY: "sat_readonlytoken" })),
|
|
63
|
+
).toThrowError(/write:registry/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("does not accidentally accept P1_CSS_API_KEY as a substitute for CSS_REGISTRY_API_KEY", () => {
|
|
67
|
+
const result = () =>
|
|
68
|
+
validateEnv(baseEnv({ CSS_REGISTRY_API_KEY: undefined, P1_CSS_API_KEY: "sat_readonlytoken" }));
|
|
69
|
+
expect(result).toThrow();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("resolveConfigModule", () => {
|
|
74
|
+
it("prefers a default export", () => {
|
|
75
|
+
const mod = { default: { components: {} }, config: { wrong: true } };
|
|
76
|
+
expect(resolveConfigModule(mod)).toBe(mod.default);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("falls back to a named config export when there is no default", () => {
|
|
80
|
+
const mod = { config: { components: {} } };
|
|
81
|
+
expect(resolveConfigModule(mod)).toBe(mod.config);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("falls back to the module itself when neither default nor config is present", () => {
|
|
85
|
+
const mod = { components: {} };
|
|
86
|
+
expect(resolveConfigModule(mod)).toBe(mod);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("resolveBranchId", () => {
|
|
91
|
+
const branches = [
|
|
92
|
+
{ id: "b-1", siteId: "site-123", name: "main", isMain: true },
|
|
93
|
+
{ id: "b-2", siteId: "site-123", name: "staging", isMain: false },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
it("resolves to the main branch when no override is given", () => {
|
|
97
|
+
expect(resolveBranchId(branches as never, "site-123")).toBe("b-1");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("resolves an override by branch id", () => {
|
|
101
|
+
expect(resolveBranchId(branches as never, "site-123", "b-2")).toBe("b-2");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("resolves an override by branch name", () => {
|
|
105
|
+
expect(resolveBranchId(branches as never, "site-123", "staging")).toBe("b-2");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("throws NoBranchMatchError ('No main branch found') when there is no override and no isMain branch", () => {
|
|
109
|
+
const noMain = [{ id: "b-2", siteId: "site-123", name: "staging", isMain: false }];
|
|
110
|
+
expect(() => resolveBranchId(noMain as never, "site-123")).toThrowError(
|
|
111
|
+
"No main branch found for site site-123",
|
|
112
|
+
);
|
|
113
|
+
try {
|
|
114
|
+
resolveBranchId(noMain as never, "site-123");
|
|
115
|
+
expect.unreachable();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
expect(err).toBeInstanceOf(NoBranchMatchError);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("throws NoBranchMatchError when an explicit override matches no branch by id or name", () => {
|
|
122
|
+
expect(() => resolveBranchId(branches as never, "site-123", "nonexistent")).toThrow(NoBranchMatchError);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node module customization hooks that stub out non-JS asset imports
|
|
3
|
+
* (CSS, images, fonts, video) so a plain Node script can `import()` a
|
|
4
|
+
* Next.js app's puck.config.tsx without a bundler. Zero dependencies.
|
|
5
|
+
*
|
|
6
|
+
* Wire in with node:module's register() — not the --import flag, which
|
|
7
|
+
* does not auto-install a resolve/load-only hooks file.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const ASSET_EXTENSION_PATTERN =
|
|
11
|
+
/\.(css|scss|sass|less|png|jpe?g|gif|svg|webp|ico|bmp|avif|woff2?|ttf|eot|otf|mp4|webm|mov|mp3|wav)$/i;
|
|
12
|
+
|
|
13
|
+
const ASSET_STUB_PROTOCOL = 'asset-stub:';
|
|
14
|
+
|
|
15
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
16
|
+
if (ASSET_EXTENSION_PATTERN.test(specifier)) {
|
|
17
|
+
return {
|
|
18
|
+
url: `${ASSET_STUB_PROTOCOL}${encodeURIComponent(specifier)}`,
|
|
19
|
+
shortCircuit: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return nextResolve(specifier, context);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function load(url, context, nextLoad) {
|
|
26
|
+
if (url.startsWith(ASSET_STUB_PROTOCOL)) {
|
|
27
|
+
return {
|
|
28
|
+
format: 'module',
|
|
29
|
+
source: 'export default {};',
|
|
30
|
+
shortCircuit: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return nextLoad(url, context);
|
|
34
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syncs this site's Puck component registry (_registry/components/* and the
|
|
3
|
+
* registry index) headlessly, without opening the editor in a browser.
|
|
4
|
+
* Intended to run from CI on push to main or a branch whose name matches a
|
|
5
|
+
* CSS branch, whenever puck.config.tsx or components/puck/** change.
|
|
6
|
+
*
|
|
7
|
+
* Usage: tsx scripts/sync-puck-registry.ts [--dry-run]
|
|
8
|
+
*
|
|
9
|
+
* Required env vars (see validateEnv below for the full fallback contract):
|
|
10
|
+
* CSS_BASE_URL, CSS_SITE_ID, CSS_REGISTRY_API_KEY
|
|
11
|
+
*
|
|
12
|
+
* CSS_REGISTRY_API_KEY must be a sat_ site token scoped to write:registry
|
|
13
|
+
* only — do not reuse a read-scoped token (P1_CSS_API_KEY). Because that
|
|
14
|
+
* token has no read access at all, every run rewrites every component
|
|
15
|
+
* descriptor + the registry index unconditionally (no skip-if-unchanged) —
|
|
16
|
+
* see syncComponentRegistryWriteOnly.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { register } from "node:module";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { pathToFileURL } from "node:url";
|
|
22
|
+
import { P1Client } from "@pantheon-systems/css-client";
|
|
23
|
+
import type { Branch } from "@pantheon-systems/css-client";
|
|
24
|
+
import { extractDescriptors, syncComponentRegistryWriteOnly } from "@pantheon-systems/puck-css/registry-sync";
|
|
25
|
+
|
|
26
|
+
export interface ValidatedEnv {
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
siteId: string;
|
|
29
|
+
apiKey: string;
|
|
30
|
+
branchOverride?: string;
|
|
31
|
+
puckConfigPath: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function validateEnv(env: Record<string, string | undefined>): ValidatedEnv {
|
|
35
|
+
const baseUrl = env.CSS_BASE_URL ?? env.NEXT_PUBLIC_CSS_BASE_URL;
|
|
36
|
+
const siteId = env.CSS_SITE_ID ?? env.NEXT_PUBLIC_CSS_SITE_ID;
|
|
37
|
+
const apiKey = env.CSS_REGISTRY_API_KEY;
|
|
38
|
+
const branchOverride = env.CSS_BRANCH_ID ?? env.NEXT_PUBLIC_CSS_BRANCH_ID;
|
|
39
|
+
const puckConfigPath = env.PUCK_CONFIG_PATH ?? "puck.config.tsx";
|
|
40
|
+
|
|
41
|
+
const missing: string[] = [];
|
|
42
|
+
if (baseUrl === undefined || baseUrl === "") {
|
|
43
|
+
missing.push("CSS_BASE_URL (or NEXT_PUBLIC_CSS_BASE_URL)");
|
|
44
|
+
}
|
|
45
|
+
if (siteId === undefined || siteId === "") {
|
|
46
|
+
missing.push("CSS_SITE_ID (or NEXT_PUBLIC_CSS_SITE_ID)");
|
|
47
|
+
}
|
|
48
|
+
if (apiKey === undefined || apiKey === "") {
|
|
49
|
+
if (env.P1_CSS_API_KEY !== undefined && env.P1_CSS_API_KEY !== "") {
|
|
50
|
+
missing.push(
|
|
51
|
+
"CSS_REGISTRY_API_KEY — do not reuse P1_CSS_API_KEY (your read-scoped site token); " +
|
|
52
|
+
"create a separate sat_ token scoped to write:registry",
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
missing.push("CSS_REGISTRY_API_KEY");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (missing.length > 0) {
|
|
60
|
+
throw new Error(`Missing required environment variable(s):\n - ${missing.join("\n - ")}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
baseUrl: baseUrl as string,
|
|
65
|
+
siteId: siteId as string,
|
|
66
|
+
apiKey: apiKey as string,
|
|
67
|
+
branchOverride,
|
|
68
|
+
puckConfigPath,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolveConfigModule(mod: unknown): unknown {
|
|
73
|
+
const record = mod as Record<string, unknown>;
|
|
74
|
+
return record.default ?? record.config ?? mod;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Thrown by resolveBranchId when no CSS branch matches. Distinguished from
|
|
79
|
+
* other errors so callers (main(), a CI trigger firing on every git branch)
|
|
80
|
+
* can treat "this branch has no CSS counterpart" as a benign no-op rather
|
|
81
|
+
* than a real sync failure.
|
|
82
|
+
*/
|
|
83
|
+
export class NoBranchMatchError extends Error {}
|
|
84
|
+
|
|
85
|
+
export function resolveBranchId(branches: Branch[], siteId: string, override?: string): string {
|
|
86
|
+
if (override !== undefined && override !== "") {
|
|
87
|
+
const match = branches.find((b) => b.id === override || b.name === override);
|
|
88
|
+
if (match === undefined) {
|
|
89
|
+
throw new NoBranchMatchError(`No branch matching "${override}" found for site ${siteId}`);
|
|
90
|
+
}
|
|
91
|
+
return match.id;
|
|
92
|
+
}
|
|
93
|
+
const mainBranch = branches.find((b) => b.isMain);
|
|
94
|
+
if (mainBranch === undefined) {
|
|
95
|
+
throw new NoBranchMatchError("No main branch found for site " + siteId);
|
|
96
|
+
}
|
|
97
|
+
return mainBranch.id;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function main(): Promise<void> {
|
|
101
|
+
const dryRun = process.argv.includes("--dry-run");
|
|
102
|
+
|
|
103
|
+
// Registered here (not at module scope) so importing this file for tests
|
|
104
|
+
// never installs a process-wide loader hook.
|
|
105
|
+
register("./asset-stub-hooks.mjs", import.meta.url);
|
|
106
|
+
|
|
107
|
+
const { baseUrl, siteId, apiKey, branchOverride, puckConfigPath } = validateEnv(process.env);
|
|
108
|
+
|
|
109
|
+
const configUrl = pathToFileURL(path.resolve(process.cwd(), puckConfigPath)).href;
|
|
110
|
+
const mod: unknown = await import(configUrl);
|
|
111
|
+
const puckConfig = resolveConfigModule(mod);
|
|
112
|
+
|
|
113
|
+
const descriptors = extractDescriptors(puckConfig);
|
|
114
|
+
|
|
115
|
+
const client = new P1Client({ baseUrl, apiKey });
|
|
116
|
+
const branches = await client.branches.list(siteId);
|
|
117
|
+
const branchId = resolveBranchId(branches, siteId, branchOverride);
|
|
118
|
+
|
|
119
|
+
if (dryRun) {
|
|
120
|
+
console.log(
|
|
121
|
+
`[sync-puck-registry] Dry run: ${String(descriptors.length)} component descriptor(s) found for site ${siteId}, branch ${branchId}. No writes performed.`,
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const result = await syncComponentRegistryWriteOnly(client, siteId, branchId, descriptors);
|
|
127
|
+
console.log(
|
|
128
|
+
`[sync-puck-registry] Synced site ${siteId}, branch ${branchId}: ` +
|
|
129
|
+
`wrote ${String(result.total)} component descriptor(s) + registry index`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const isMainModule = import.meta.url === pathToFileURL(process.argv[1] ?? "").href;
|
|
134
|
+
if (isMainModule) {
|
|
135
|
+
main().catch((err: unknown) => {
|
|
136
|
+
// A CI trigger firing on every git branch push has no way to know ahead
|
|
137
|
+
// of time which branches have a matching CSS branch — that has to be
|
|
138
|
+
// discovered at runtime. Treat "no match" as a benign no-op, not a
|
|
139
|
+
// failure, so unrelated feature-branch pushes don't turn CI red.
|
|
140
|
+
if (err instanceof NoBranchMatchError) {
|
|
141
|
+
console.log(`[sync-puck-registry] Skipping: ${err.message}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
145
|
+
console.error("[sync-puck-registry] FAILED:", message);
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
});
|
|
148
|
+
}
|