@decocms/blocks-cli 8.0.0 → 8.1.0-next.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 +5 -3
- package/scripts/analyze-traces.mjs +1 -0
- package/scripts/cdn-rules.test.ts +105 -0
- package/scripts/cdn-rules.ts +192 -0
- package/scripts/deco-migrate-cli.ts +6 -7
- package/scripts/fast-deploy-kv.test.ts +172 -0
- package/scripts/generate-blocks.test.ts +11 -0
- package/scripts/generate-blocks.ts +0 -10
- package/scripts/generate-invoke.test.ts +2 -2
- package/scripts/generate-invoke.ts +9 -21
- package/scripts/generate-loaders.test.ts +2 -2
- package/scripts/generate-loaders.ts +0 -10
- package/scripts/generate-schema-loader-aliases.test.ts +186 -0
- package/scripts/generate-schema-workspaces.test.ts +180 -0
- package/scripts/generate-schema.test.ts +68 -29
- package/scripts/generate-schema.ts +101 -89
- package/scripts/generate-sections.test.ts +104 -35
- package/scripts/generate-sections.ts +57 -11
- package/scripts/generate-workspaces.test.ts +179 -0
- package/scripts/generate.test.ts +1 -2
- package/scripts/generate.ts +43 -45
- package/scripts/lib/blocks-dedupe.test.ts +1 -1
- package/scripts/lib/cf-kv-rest.ts +49 -1
- package/scripts/lib/installedPackages.ts +42 -0
- package/scripts/lib/invokeSource.ts +18 -0
- package/scripts/lib/kv-snapshot.ts +44 -0
- package/scripts/lib/read-decofile.ts +13 -2
- package/scripts/lib/upgrade-6-to-7.ts +1 -1
- package/scripts/lib/wrangler-config.test.ts +49 -0
- package/scripts/lib/wrangler-config.ts +31 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +6 -6
- package/scripts/migrate/analyzers/tailwind-config.ts +95 -1
- package/scripts/migrate/config.ts +2 -2
- package/scripts/migrate/delete-sets.ts +75 -0
- package/scripts/migrate/fast-deploy-scaffold.test.ts +130 -0
- package/scripts/migrate/icon-casing.test.ts +63 -0
- package/scripts/migrate/phase-analyze.test.ts +66 -4
- package/scripts/migrate/phase-analyze.ts +79 -60
- package/scripts/migrate/phase-cleanup.test.ts +3 -1
- package/scripts/migrate/phase-cleanup.ts +1 -1
- package/scripts/migrate/phase-compile.test.ts +3 -1
- package/scripts/migrate/phase-report.test.ts +2 -0
- package/scripts/migrate/phase-report.ts +3 -4
- package/scripts/migrate/phase-scaffold.ts +150 -45
- package/scripts/migrate/phase-transform.ts +107 -2
- package/scripts/migrate/phase-verify-legacy-specifiers.test.ts +13 -0
- package/scripts/migrate/phase-verify.test.ts +3 -1
- package/scripts/migrate/phase-verify.ts +87 -33
- package/scripts/migrate/post-cleanup/rules.ts +29 -11
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +1 -1
- package/scripts/migrate/post-cleanup/shim-classify.ts +2 -2
- package/scripts/migrate/source-layout.test.ts +43 -0
- package/scripts/migrate/source-layout.ts +32 -5
- package/scripts/migrate/templates/app-css.test.ts +24 -0
- package/scripts/migrate/templates/app-css.ts +27 -0
- package/scripts/migrate/templates/ci-workflows.test.ts +179 -0
- package/scripts/migrate/templates/ci-yml.ts +182 -0
- package/scripts/migrate/templates/cursor-rules.test.ts +3 -3
- package/scripts/migrate/templates/hooks.test.ts +3 -1
- package/scripts/migrate/templates/lib-utils.ts +3 -3
- package/scripts/migrate/templates/main-push-guard-yml.ts +45 -0
- package/scripts/migrate/templates/package-json.ts +10 -0
- package/scripts/migrate/templates/parity-yml.ts +136 -0
- package/scripts/migrate/templates/perf-yml.ts +449 -0
- package/scripts/migrate/templates/playwright-yml.ts +127 -0
- package/scripts/migrate/templates/react-doctor-yml.ts +46 -0
- package/scripts/migrate/templates/routes.test.ts +31 -0
- package/scripts/migrate/templates/routes.ts +19 -9
- package/scripts/migrate/templates/section-loaders.ts +3 -3
- package/scripts/migrate/templates/server-entry-vtex-invoke.test.ts +19 -0
- package/scripts/migrate/templates/server-entry.ts +19 -14
- package/scripts/migrate/templates/setup.ts +11 -3
- package/scripts/migrate/templates/sync-blocks-bot-yml.ts +201 -0
- package/scripts/migrate/templates/ui-components.ts +3 -0
- package/scripts/migrate/transforms/ctx-compat.test.ts +37 -0
- package/scripts/migrate/transforms/ctx-compat.ts +93 -1
- package/scripts/migrate/transforms/htmx-on-events.test.ts +1 -1
- package/scripts/migrate/transforms/htmx-on-events.ts +2 -2
- package/scripts/migrate/transforms/jsx-class-type.test.ts +28 -0
- package/scripts/migrate/transforms/jsx.ts +10 -0
- package/scripts/migrate/transforms/picture.test.ts +56 -0
- package/scripts/migrate/transforms/picture.ts +95 -0
- package/scripts/migrate/transforms/tailwind-renames.test.ts +29 -0
- package/scripts/migrate/transforms/tailwind-renames.ts +34 -0
- package/scripts/migrate/transforms/timer-types.test.ts +30 -0
- package/scripts/migrate/transforms/timer-types.ts +48 -0
- package/scripts/migrate/transforms/use-script-handlers.test.ts +37 -0
- package/scripts/migrate/transforms/use-script-handlers.ts +36 -0
- package/scripts/migrate/types.ts +13 -1
- package/scripts/migrate-blocks-to-kv.ts +16 -2
- package/scripts/migrate-to-cf-observability.test.ts +5 -5
- package/scripts/migrate-to-cf-observability.ts +1 -1
- package/scripts/migrate.ts +244 -6
- package/scripts/reconcile.test.ts +162 -0
- package/scripts/reconcile.ts +303 -0
- package/scripts/sync-blocks-bot.test.ts +276 -0
- package/scripts/sync-blocks-bot.ts +567 -0
- package/scripts/sync-blocks-to-kv.ts +20 -2
- package/scripts/upgrade-6-to-7.ts +0 -0
- package/scripts/lib/legacyArtifact.ts +0 -20
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffolds the E2E harness and its config/smoke companions:
|
|
3
|
+
* .github/workflows/playwright.yml — GitHub Actions workflow (chromium + webkit)
|
|
4
|
+
* playwright.config.ts — self-contained config (no support helpers)
|
|
5
|
+
* tests/e2e/smoke.spec.ts — harness smoke, no app boot, no network
|
|
6
|
+
*
|
|
7
|
+
* Design rationale:
|
|
8
|
+
* - The smoke proves the Playwright harness runs in CI (chromium + webkit)
|
|
9
|
+
* without the app booting or hitting the network — it renders inline HTML
|
|
10
|
+
* via page.setContent. Real storefront specs (page.goto("/")) need a VCR
|
|
11
|
+
* layer so CI does not hit the commerce API; add a `webServer` block to the
|
|
12
|
+
* config and specs that navigate the app once that harness exists.
|
|
13
|
+
* - webkit is included because the storefront audience is majority iOS Safari.
|
|
14
|
+
* - The workflow needs `@playwright/test` + the `test:e2e` script, both in the
|
|
15
|
+
* scaffolded package.json.
|
|
16
|
+
*
|
|
17
|
+
* @param bunVersion Bun version for setup-bun (= CANONICAL_BUN_VERSION).
|
|
18
|
+
*/
|
|
19
|
+
export function generatePlaywrightFiles(bunVersion: string): Record<string, string> {
|
|
20
|
+
const bun = bunVersion.replace(/^bun@/, "");
|
|
21
|
+
return {
|
|
22
|
+
".github/workflows/playwright.yml": generatePlaywrightYml(bun),
|
|
23
|
+
"playwright.config.ts": PLAYWRIGHT_CONFIG,
|
|
24
|
+
"tests/e2e/smoke.spec.ts": SMOKE_SPEC,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function generatePlaywrightYml(bunVersion: string): string {
|
|
29
|
+
return `name: Playwright
|
|
30
|
+
|
|
31
|
+
# Functional E2E (chromium + webkit). Today only a harness smoke that renders
|
|
32
|
+
# inline HTML — no app boot, no network — so it runs on a plain ubuntu runner.
|
|
33
|
+
# When specs navigate the real app (via VCR), pin the container and add a
|
|
34
|
+
# webServer block to playwright.config.ts.
|
|
35
|
+
|
|
36
|
+
on:
|
|
37
|
+
pull_request:
|
|
38
|
+
push:
|
|
39
|
+
branches: [main]
|
|
40
|
+
|
|
41
|
+
permissions:
|
|
42
|
+
contents: read
|
|
43
|
+
|
|
44
|
+
concurrency:
|
|
45
|
+
group: playwright-\${{ github.workflow }}-\${{ github.ref }}
|
|
46
|
+
cancel-in-progress: true
|
|
47
|
+
|
|
48
|
+
jobs:
|
|
49
|
+
e2e:
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v4
|
|
53
|
+
|
|
54
|
+
- uses: oven-sh/setup-bun@v2
|
|
55
|
+
with:
|
|
56
|
+
bun-version: "${bunVersion}"
|
|
57
|
+
|
|
58
|
+
- name: bun install --frozen-lockfile
|
|
59
|
+
run: bun install --frozen-lockfile
|
|
60
|
+
|
|
61
|
+
- name: Install browsers (chromium + webkit)
|
|
62
|
+
run: bunx playwright install --with-deps chromium webkit
|
|
63
|
+
|
|
64
|
+
- name: E2E
|
|
65
|
+
run: bun run test:e2e
|
|
66
|
+
|
|
67
|
+
- name: Upload HTML report
|
|
68
|
+
if: \${{ !cancelled() }}
|
|
69
|
+
uses: actions/upload-artifact@v4
|
|
70
|
+
with:
|
|
71
|
+
name: playwright-report
|
|
72
|
+
path: playwright-report/
|
|
73
|
+
retention-days: 14
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const PLAYWRIGHT_CONFIG = `/**
|
|
78
|
+
* Playwright config — E2E base.
|
|
79
|
+
*
|
|
80
|
+
* Today only a harness smoke (tests/e2e/smoke.spec.ts) that renders inline HTML,
|
|
81
|
+
* proving the harness runs in CI without the app booting or hitting the network.
|
|
82
|
+
* Real app E2E (page.goto("/")) needs a VCR layer so CI does not call the
|
|
83
|
+
* commerce API — add a \`webServer\` block (e.g. { command: "bun run build && bun
|
|
84
|
+
* run preview", url: "http://localhost:4173" }) and the navigating specs then.
|
|
85
|
+
*
|
|
86
|
+
* chromium + webkit (webkit because the storefront audience is majority iOS
|
|
87
|
+
* Safari). 1 worker, 0 retries on purpose: retry masks flake instead of exposing it.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
91
|
+
|
|
92
|
+
export default defineConfig({
|
|
93
|
+
testDir: "./tests/e2e",
|
|
94
|
+
fullyParallel: true,
|
|
95
|
+
forbidOnly: !!process.env.CI,
|
|
96
|
+
retries: 0,
|
|
97
|
+
workers: 1,
|
|
98
|
+
reporter: [["html", { open: "never" }], ["list"]],
|
|
99
|
+
use: {
|
|
100
|
+
trace: "retain-on-failure",
|
|
101
|
+
screenshot: "only-on-failure",
|
|
102
|
+
},
|
|
103
|
+
projects: [
|
|
104
|
+
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
|
105
|
+
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const SMOKE_SPEC = `/**
|
|
111
|
+
* Smoke E2E — proves the Playwright harness runs (chromium + webkit) in CI
|
|
112
|
+
* without the app booting or hitting the network. Does not test the real app;
|
|
113
|
+
* the storefront E2E (page.goto("/")) comes with the VCR + cassettes harness.
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
import { expect, test } from "@playwright/test";
|
|
117
|
+
|
|
118
|
+
test("harness renders inline content", async ({ page }) => {
|
|
119
|
+
await page.setContent(
|
|
120
|
+
'<title>e2e smoke</title><h1>ok</h1><div data-testid="marker">ready</div>',
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await expect(page).toHaveTitle("e2e smoke");
|
|
124
|
+
await expect(page.getByRole("heading", { name: "ok" })).toBeVisible();
|
|
125
|
+
await expect(page.getByTestId("marker")).toHaveText("ready");
|
|
126
|
+
});
|
|
127
|
+
`;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffolds `.github/workflows/react-doctor.yml` — flags React
|
|
3
|
+
* security/perf/correctness/a11y/bundle-size/architecture issues on PRs.
|
|
4
|
+
*
|
|
5
|
+
* Advisory by construction (the action's default: comments, never fails the
|
|
6
|
+
* build). Separate workflow from ci.yml on purpose — zero coupling with the
|
|
7
|
+
* blocking gates. `fetch-depth: 0` gives the merge-base so it reports only what
|
|
8
|
+
* the PR introduces.
|
|
9
|
+
*
|
|
10
|
+
* Fully generic — no per-site parameters. Docs: https://www.react.doctor/ci
|
|
11
|
+
*/
|
|
12
|
+
export function generateReactDoctorYml(): string {
|
|
13
|
+
return `name: React Doctor
|
|
14
|
+
|
|
15
|
+
# Flags React security/perf/correctness/a11y/bundle-size/architecture issues.
|
|
16
|
+
# Advisory by construction (comments, never fails the check). To make it a hard
|
|
17
|
+
# gate, add \`with: { blocking: error }\` to the action step below.
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
pull_request:
|
|
21
|
+
types: [opened, synchronize, reopened, ready_for_review]
|
|
22
|
+
push:
|
|
23
|
+
branches: [main]
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
pull-requests: write
|
|
28
|
+
issues: write
|
|
29
|
+
statuses: write
|
|
30
|
+
|
|
31
|
+
concurrency:
|
|
32
|
+
group: react-doctor-\${{ github.event.pull_request.number || github.ref }}
|
|
33
|
+
cancel-in-progress: true
|
|
34
|
+
|
|
35
|
+
jobs:
|
|
36
|
+
react-doctor:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
# fetch-depth: 0 gives the merge-base so it reports only PR-introduced findings.
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
with:
|
|
42
|
+
fetch-depth: 0
|
|
43
|
+
|
|
44
|
+
- uses: millionco/react-doctor@v2
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
@@ -57,3 +57,34 @@ describe("scaffolded deco admin routes use HMR-safe factories", () => {
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Regression guard: the page renderers must wire `loadDeferredSectionFn` to the
|
|
63
|
+
* `deferredSectionLoader` shim, never the raw `loadDeferredSection` server fn.
|
|
64
|
+
* `loadDeferredSection` is a createServerFn with an inputValidator, so it must
|
|
65
|
+
* be called as `fn({ data })`; DeferredSectionWrapper calls `loadFn(bareArgs)`,
|
|
66
|
+
* so passing the raw fn leaves `ctx.data` undefined and the deferred section
|
|
67
|
+
* renders blank. The shim wraps the `{ data }` envelope.
|
|
68
|
+
*/
|
|
69
|
+
describe("scaffolded page routes wire the deferred-loader shim, not the raw server fn", () => {
|
|
70
|
+
for (const platform of ["vtex", "custom"] as const) {
|
|
71
|
+
describe(`platform: ${platform}`, () => {
|
|
72
|
+
const files = generateRoutes(makeCtx(platform));
|
|
73
|
+
|
|
74
|
+
for (const file of ["src/routes/index.tsx", "src/routes/$.tsx"] as const) {
|
|
75
|
+
it(`${file} passes deferredSectionLoader from the sdk subpath`, () => {
|
|
76
|
+
const content = files[file];
|
|
77
|
+
expect(content, `${file} must be emitted`).toBeTypeOf("string");
|
|
78
|
+
|
|
79
|
+
expect(content).toContain(
|
|
80
|
+
'import { deferredSectionLoader } from "@decocms/tanstack/sdk/deferredSectionLoader"',
|
|
81
|
+
);
|
|
82
|
+
expect(content).toContain("loadDeferredSectionFn={deferredSectionLoader}");
|
|
83
|
+
|
|
84
|
+
// The broken form: the raw server fn passed directly.
|
|
85
|
+
expect(content).not.toContain("loadDeferredSectionFn={loadDeferredSection}");
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
@@ -46,22 +46,31 @@ function generateRoot(ctx: MigrationContext, siteTitle: string, vtexAccount: str
|
|
|
46
46
|
|
|
47
47
|
// Build preconnect list based on platform
|
|
48
48
|
const preconnects: string[] = [];
|
|
49
|
-
preconnects.push(` { rel: "preconnect", href: "https://
|
|
49
|
+
preconnects.push(` { rel: "preconnect", href: "https://assets.decocms.com", crossOrigin: "anonymous" as const },`);
|
|
50
50
|
if (isVtex) {
|
|
51
51
|
preconnects.push(` { rel: "preconnect", href: "https://${vtexAccount}.vtexassets.com", crossOrigin: "anonymous" as const },`);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
const googleFontLinks = [
|
|
55
|
+
...ctx.googleFonts.preconnects.map((href) =>
|
|
56
|
+
` { rel: "preconnect", href: "${href}"${href.includes("gstatic") ? ', crossOrigin: "anonymous" as const' : ""} },`
|
|
57
|
+
),
|
|
58
|
+
...ctx.googleFonts.stylesheets.map((href) =>
|
|
59
|
+
` { rel: "stylesheet", href: "${href}" },`
|
|
60
|
+
),
|
|
61
|
+
];
|
|
62
|
+
|
|
55
63
|
const fontPreloads = fonts.map((f) =>
|
|
56
64
|
` { rel: "preload", href: "${f}", as: "font", type: "font/woff2", crossOrigin: "anonymous" as const },`
|
|
57
65
|
);
|
|
58
66
|
|
|
59
|
-
// DNS prefetch for common third-party services
|
|
60
67
|
const dnsPrefetch: string[] = [];
|
|
61
68
|
if (isVtex) {
|
|
62
69
|
dnsPrefetch.push(` { rel: "dns-prefetch", href: "https://sp.vtex.com" },`);
|
|
63
70
|
}
|
|
64
71
|
|
|
72
|
+
const linkLines = [...preconnects, ...googleFontLinks, ...fontPreloads].filter(Boolean);
|
|
73
|
+
|
|
65
74
|
return `import { createRootRoute } from "@tanstack/react-router";
|
|
66
75
|
import { DecoRootLayout } from "@decocms/tanstack";
|
|
67
76
|
// @ts-ignore Vite ?url import
|
|
@@ -83,8 +92,7 @@ export const Route = createRootRoute({
|
|
|
83
92
|
{ property: "og:locale", content: "pt_BR" },
|
|
84
93
|
],
|
|
85
94
|
links: [
|
|
86
|
-
${
|
|
87
|
-
${fontPreloads.join("\n")}
|
|
95
|
+
${linkLines.join("\n")}
|
|
88
96
|
{ rel: "stylesheet", href: appCss },
|
|
89
97
|
{ rel: "icon", href: "/favicon.ico" },
|
|
90
98
|
${dnsPrefetch.join("\n")}
|
|
@@ -107,7 +115,8 @@ function RootLayout() {
|
|
|
107
115
|
|
|
108
116
|
function generateIndex(ctx: MigrationContext, siteTitle: string): string {
|
|
109
117
|
return `import { createFileRoute } from "@tanstack/react-router";
|
|
110
|
-
import { cmsHomeRouteConfig, DecoPageRenderer
|
|
118
|
+
import { cmsHomeRouteConfig, DecoPageRenderer } from "@decocms/tanstack";
|
|
119
|
+
import { deferredSectionLoader } from "@decocms/tanstack/sdk/deferredSectionLoader";
|
|
111
120
|
|
|
112
121
|
// MIGRATION TODO: customize defaultTitle / defaultDescription / fallback
|
|
113
122
|
// copy below for ${siteTitle}. CMS \`Site.seo\` overrides these once block
|
|
@@ -143,7 +152,7 @@ function HomePage() {
|
|
|
143
152
|
deferredPromises={data.deferredPromises}
|
|
144
153
|
pagePath={data.pagePath}
|
|
145
154
|
pageUrl={data.pageUrl}
|
|
146
|
-
loadDeferredSectionFn={
|
|
155
|
+
loadDeferredSectionFn={deferredSectionLoader}
|
|
147
156
|
/>
|
|
148
157
|
);
|
|
149
158
|
}
|
|
@@ -152,7 +161,8 @@ function HomePage() {
|
|
|
152
161
|
|
|
153
162
|
function generateCatchAll(ctx: MigrationContext, siteTitle: string): string {
|
|
154
163
|
return `import { createFileRoute } from "@tanstack/react-router";
|
|
155
|
-
import { cmsRouteConfig, DecoPageRenderer
|
|
164
|
+
import { cmsRouteConfig, DecoPageRenderer } from "@decocms/tanstack";
|
|
165
|
+
import { deferredSectionLoader } from "@decocms/tanstack/sdk/deferredSectionLoader";
|
|
156
166
|
|
|
157
167
|
// MIGRATION TODO: customize defaultTitle / defaultDescription for ${siteTitle}
|
|
158
168
|
// (CMS \`Site.seo\` overrides these per-page once block resolution kicks in).
|
|
@@ -180,7 +190,7 @@ function CmsPage() {
|
|
|
180
190
|
deferredPromises={data.deferredPromises}
|
|
181
191
|
pagePath={data.pagePath}
|
|
182
192
|
pageUrl={data.pageUrl}
|
|
183
|
-
loadDeferredSectionFn={
|
|
193
|
+
loadDeferredSectionFn={deferredSectionLoader}
|
|
184
194
|
/>
|
|
185
195
|
);
|
|
186
196
|
}
|
|
@@ -139,8 +139,8 @@ export function generateSectionLoaders(ctx: MigrationContext): string {
|
|
|
139
139
|
// returned.
|
|
140
140
|
//
|
|
141
141
|
// The previous template chose mixins XOR own-loader and silently dropped
|
|
142
|
-
// the section's loader when both were present — see
|
|
143
|
-
//
|
|
142
|
+
// the section's loader when both were present — see a production
|
|
143
|
+
// storefront's SearchContainer SSR regression.
|
|
144
144
|
for (const meta of ctx.sectionMetas) {
|
|
145
145
|
if (meta.isHeader || meta.isAccountSection || meta.isStatusOnly) continue;
|
|
146
146
|
// Skip sections with no loader AND no device needs
|
|
@@ -418,7 +418,7 @@ export function generateSectionLoaders(ctx: MigrationContext): string {
|
|
|
418
418
|
// ---------- Privacy cookie check ----------
|
|
419
419
|
if (isVtex && hasPrivacyPolice) {
|
|
420
420
|
entries.push(``);
|
|
421
|
-
const vtexAccount = ctx.vtexAccount || "
|
|
421
|
+
const vtexAccount = ctx.vtexAccount || "acme";
|
|
422
422
|
entries.push(` "site/sections/Account/PrivacyPolice.tsx": (props: any, req) => {`);
|
|
423
423
|
entries.push(` const cookies = req.headers.get("cookie") ?? "";`);
|
|
424
424
|
entries.push(` const logged = cookies.includes("VtexIdclientAutCookie_${vtexAccount}");`);
|
|
@@ -49,3 +49,22 @@ describe("VTEX invoke scaffold wires vtex.loaders + wishlist actions (#368)", ()
|
|
|
49
49
|
expect(invokeSrc).toContain("parseVtexAuthJwt");
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
describe("scaffolds ?renderJson / ?asJson OFF by default (opt-in)", () => {
|
|
54
|
+
const workerKey = "src/worker-entry.ts";
|
|
55
|
+
|
|
56
|
+
it("VTEX worker-entry disables both page-as-JSON endpoints", () => {
|
|
57
|
+
const worker = generateServerEntry(makeVtexCtx())[workerKey];
|
|
58
|
+
expect(worker).toContain("renderJson: false");
|
|
59
|
+
expect(worker).toContain("asJson: false");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("non-VTEX worker-entry disables both too", () => {
|
|
63
|
+
const ctx = createContext("/tmp/custom-scaffold-fixture-site");
|
|
64
|
+
ctx.siteName = "acme-custom";
|
|
65
|
+
ctx.platform = "custom";
|
|
66
|
+
const worker = generateServerEntry(ctx)[workerKey];
|
|
67
|
+
expect(worker).toContain("renderJson: false");
|
|
68
|
+
expect(worker).toContain("asJson: false");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -86,6 +86,11 @@ ${isCommerce ? `
|
|
|
86
86
|
const serverEntry = createServerEntry({ fetch: handler.fetch });
|
|
87
87
|
|
|
88
88
|
const decoWorker = createDecoWorkerEntry(serverEntry, {
|
|
89
|
+
// Page-as-JSON endpoints are OFF by default — opt in when you build a mobile
|
|
90
|
+
// app that consumes the storefront (see the renderJson docs). ?asJson is the
|
|
91
|
+
// legacy raw path; ?renderJson is the lean per-section one.
|
|
92
|
+
renderJson: false,
|
|
93
|
+
asJson: false,
|
|
89
94
|
admin: {
|
|
90
95
|
handleMeta,
|
|
91
96
|
handleDecofileRead,
|
|
@@ -93,14 +98,10 @@ const decoWorker = createDecoWorkerEntry(serverEntry, {
|
|
|
93
98
|
handleRender,
|
|
94
99
|
corsHeaders,
|
|
95
100
|
},
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
const cf = (request as unknown as { cf?: { regionCode?: string } }).cf;
|
|
101
|
-
const regionCode = request.headers.get("cf-region-code") ?? cf?.regionCode ?? "";
|
|
102
|
-
return regionCode ? { regionId: regionCode } : {};
|
|
103
|
-
},
|
|
101
|
+
// No geo in buildSegment: createDecoWorkerEntry's geoCacheKey: "auto" already
|
|
102
|
+
// backfills the region into the segment — but only when the decofile has a
|
|
103
|
+
// website/matchers/location.ts block. Setting it here unconditionally split
|
|
104
|
+
// every page's cache per state (~27 copies per URL in Brazil) for nothing.
|
|
104
105
|
});
|
|
105
106
|
|
|
106
107
|
export default instrumentWorker(decoWorker, {
|
|
@@ -161,19 +162,23 @@ const serverEntry = createServerEntry({ fetch: handler.fetch });
|
|
|
161
162
|
const MOBILE_RE = /mobile|android|iphone/i;
|
|
162
163
|
|
|
163
164
|
const decoWorker = createDecoWorkerEntry(serverEntry, {
|
|
165
|
+
// Page-as-JSON endpoints are OFF by default — opt in when you build a mobile
|
|
166
|
+
// app that consumes the storefront (see the renderJson docs). ?asJson is the
|
|
167
|
+
// legacy raw path; ?renderJson is the lean per-section one.
|
|
168
|
+
renderJson: false,
|
|
169
|
+
asJson: false,
|
|
164
170
|
csp: CSP_DIRECTIVES,
|
|
165
171
|
buildSegment: (request) => {
|
|
166
172
|
const vtx = extractVtexContext(request);
|
|
167
|
-
const cf = (request as unknown as { cf?: { regionCode?: string } }).cf;
|
|
168
|
-
const geoRegion = request.headers.get("cf-region-code") ?? cf?.regionCode ?? "";
|
|
169
173
|
return {
|
|
170
174
|
device: MOBILE_RE.test(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop",
|
|
171
175
|
loggedIn: vtx.isLoggedIn,
|
|
172
176
|
salesChannel: vtx.salesChannel,
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
+
// VTEX regionalization only. Don't fall back to Cloudflare geo here:
|
|
178
|
+
// geoCacheKey: "auto" adds the region only when a
|
|
179
|
+
// website/matchers/location.ts block exists; doing it unconditionally
|
|
180
|
+
// splits every page's cache per state.
|
|
181
|
+
regionId: (vtx as any).regionId,
|
|
177
182
|
};
|
|
178
183
|
},
|
|
179
184
|
admin: {
|
|
@@ -11,7 +11,7 @@ function discoverFonts(ctx: MigrationContext): string[] {
|
|
|
11
11
|
const staticFonts = path.join(ctx.sourceDir, "static", "fonts");
|
|
12
12
|
if (fs.existsSync(staticFonts)) return scanFontDir(staticFonts);
|
|
13
13
|
|
|
14
|
-
// Check static-*/fonts/ (multi-brand sites
|
|
14
|
+
// Check static-*/fonts/ (multi-brand sites)
|
|
15
15
|
try {
|
|
16
16
|
const entries = fs.readdirSync(ctx.sourceDir, { withFileTypes: true });
|
|
17
17
|
for (const e of entries) {
|
|
@@ -103,8 +103,8 @@ import { createInstrumentedFetch } from "@decocms/blocks/sdk/instrumentedFetch";
|
|
|
103
103
|
import { initVtexFromBlocks, setVtexFetch } from "@decocms/apps-vtex";` : ""}${hasLocationMatcher ? `
|
|
104
104
|
import { registerLocationMatcher } from "./matchers/location";` : ""}
|
|
105
105
|
import { blocks as generatedBlocks } from "../.deco/blocks.gen";
|
|
106
|
-
import { sectionMeta, syncComponents, loadingFallbacks } from "../.deco/sections.gen";
|
|
107
|
-
import { PreviewProviders } from "@decocms/tanstack";
|
|
106
|
+
import { sectionMeta, syncComponents, loadingFallbacks, renderJsons } from "../.deco/sections.gen";
|
|
107
|
+
import { PreviewProviders, setupTanstackFastDeploy } from "@decocms/tanstack";
|
|
108
108
|
// @ts-ignore Vite ?url import
|
|
109
109
|
import appCss from "./styles/app.css?url";
|
|
110
110
|
|
|
@@ -131,6 +131,13 @@ createSiteSetup({
|
|
|
131
131
|
return null;
|
|
132
132
|
},
|
|
133
133
|
});
|
|
134
|
+
|
|
135
|
+
// -- Fast Deploy --
|
|
136
|
+
// Hands the KV binding resolver to @decocms/blocks-admin, which cannot import
|
|
137
|
+
// @decocms/tanstack itself (wrong direction in the package graph). Without this
|
|
138
|
+
// call a Studio publish silently no-ops instead of writing through to KV.
|
|
139
|
+
// Inert unless the worker has DECO_FAST_DEPLOY=1 + a DECO_KV binding.
|
|
140
|
+
setupTanstackFastDeploy();
|
|
134
141
|
${isVtex ? `
|
|
135
142
|
// -- VTEX wiring --
|
|
136
143
|
setVtexFetch(createInstrumentedFetch("vtex"));
|
|
@@ -140,6 +147,7 @@ applySectionConventions({
|
|
|
140
147
|
meta: sectionMeta,
|
|
141
148
|
syncComponents,
|
|
142
149
|
loadingFallbacks,
|
|
150
|
+
renderJsons,
|
|
143
151
|
sectionGlob: import.meta.glob("./sections/**/*.tsx") as Record<string, () => Promise<any>>,
|
|
144
152
|
});
|
|
145
153
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffolds `.github/workflows/sync-blocks-bot.yml` — the secure content channel
|
|
3
|
+
* from the still-live Fresh/Deno storefront into the migrated repo.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the legacy push-based sync (a workflow in the *legacy* repo holding a
|
|
6
|
+
* cross-repo PAT, `rsync --delete` + `git push` straight into this repo's main).
|
|
7
|
+
* That token is a code-write door, not a content channel: whoever controls the
|
|
8
|
+
* legacy repo or the token can land arbitrary files here, unreviewed.
|
|
9
|
+
*
|
|
10
|
+
* Direction is inverted — this repo pulls, on its own schedule, with its own
|
|
11
|
+
* `GITHUB_TOKEN`, and gates the result:
|
|
12
|
+
* - `sync-blocks-bot.ts` fetches `<origin>/.decofile` (public, unauthenticated)
|
|
13
|
+
* and writes `.deco/blocks/`, denying the `Site` block and anything holding
|
|
14
|
+
* encrypted credentials, and aborting on a plaintext credential;
|
|
15
|
+
* - a path guard fails the run if anything outside `.deco/blocks/` changed;
|
|
16
|
+
* - `bun run generate && bun run build` runs IN THIS JOB, before the PR. It
|
|
17
|
+
* has to be here: a PR opened with `GITHUB_TOKEN` does not trigger
|
|
18
|
+
* `pull_request` workflows, so gating on the PR's own CI would need a PAT
|
|
19
|
+
* and would never fire on its own.
|
|
20
|
+
*
|
|
21
|
+
* Inert until the operator sets the repo variable `SYNC_BLOCKS_ORIGIN` (same
|
|
22
|
+
* one-knob pattern as parity.yml's `PARITY_PROD_URL`) — the job skips cleanly.
|
|
23
|
+
*
|
|
24
|
+
* Docs: docs/sync-blocks-bot.md
|
|
25
|
+
*
|
|
26
|
+
* @param bunVersion Pinned bun version, in lockstep with package.json.
|
|
27
|
+
* @param cliVersion Optional `@decocms/blocks-cli` version to run the pull
|
|
28
|
+
* with. Omit for a freshly scaffolded site: the site's own installed copy is
|
|
29
|
+
* by definition in-version, so the local file path is used. Pass a version
|
|
30
|
+
* for a site still on an older `@decocms/*` — pinning only the sync step gets
|
|
31
|
+
* the script without bumping the runtime the site builds against (blocks-cli
|
|
32
|
+
* pins `@decocms/blocks` exactly, so bumping the devDep drags a second
|
|
33
|
+
* runtime version into the tree). Drop it when the site bumps.
|
|
34
|
+
*
|
|
35
|
+
* GOTCHA: the pinned form uses `npx --package=<pkg> <bin>`, NOT
|
|
36
|
+
* `bunx <pkg> <bin>`. `bunx pkg@ver some-bin` treats `some-bin` as an
|
|
37
|
+
* *argument* and runs the package's first bin instead — in this package that
|
|
38
|
+
* is `deco-migrate`, which rewrites the whole site and exits 0 in dry-run, so
|
|
39
|
+
* the job goes green having run the wrong tool. Observed on a real run. The
|
|
40
|
+
* `--json`-report assertion below is the second line of defence.
|
|
41
|
+
*/
|
|
42
|
+
export function generateSyncBlocksBotYml(bunVersion: string, cliVersion?: string): string {
|
|
43
|
+
const bun = bunVersion.replace(/^bun@/, "");
|
|
44
|
+
const pullCmd = cliVersion
|
|
45
|
+
? `npx --yes --package=@decocms/blocks-cli@${cliVersion} deco-sync-blocks-bot`
|
|
46
|
+
: "bunx tsx node_modules/@decocms/blocks-cli/scripts/sync-blocks-bot.ts";
|
|
47
|
+
return `name: sync-blocks-bot
|
|
48
|
+
|
|
49
|
+
# Puxa o conteúdo publicado na loja de produção (Fresh/Deno) para \`.deco/blocks\`
|
|
50
|
+
# e abre um PR. Sem token cross-repo: o repo legado não tem permissão nenhuma
|
|
51
|
+
# aqui — este repo busca sozinho, valida e só então mergeia.
|
|
52
|
+
#
|
|
53
|
+
# Configuração (uma vez): variável de repo \`SYNC_BLOCKS_ORIGIN\` = origin da
|
|
54
|
+
# loja de produção (ex.: https://www.minhaloja.com.br). Sem ela o job pula limpo.
|
|
55
|
+
# Para revisão humana em vez de merge automático, mude \`AUTO_MERGE\` para "false".
|
|
56
|
+
#
|
|
57
|
+
# Depois de ligar isto, APAGUE o workflow de push no repo legado e REVOGUE o
|
|
58
|
+
# token cross-repo — o pull não fecha aquela porta sozinho. Ver docs/sync-blocks-bot.md.
|
|
59
|
+
|
|
60
|
+
on:
|
|
61
|
+
schedule:
|
|
62
|
+
# 06:00 UTC = 03:00 BRT, fora do horário de publicação do CMS.
|
|
63
|
+
- cron: "0 6 * * *"
|
|
64
|
+
workflow_dispatch:
|
|
65
|
+
inputs:
|
|
66
|
+
origin:
|
|
67
|
+
description: "Origin da loja de produção (sobrepõe SYNC_BLOCKS_ORIGIN)"
|
|
68
|
+
required: false
|
|
69
|
+
prune:
|
|
70
|
+
description: "Apagar blocos que não existem mais em produção"
|
|
71
|
+
type: boolean
|
|
72
|
+
default: true
|
|
73
|
+
dry_run:
|
|
74
|
+
description: "Só relatório, não escreve nem abre PR"
|
|
75
|
+
type: boolean
|
|
76
|
+
default: false
|
|
77
|
+
|
|
78
|
+
permissions:
|
|
79
|
+
contents: write
|
|
80
|
+
pull-requests: write
|
|
81
|
+
|
|
82
|
+
# Publicações do CMS acontecem em rajada; uma sync por vez, sem cancelar a que
|
|
83
|
+
# já está no gate de build.
|
|
84
|
+
concurrency:
|
|
85
|
+
group: sync-blocks-bot
|
|
86
|
+
cancel-in-progress: false
|
|
87
|
+
|
|
88
|
+
env:
|
|
89
|
+
BUN_VERSION: "${bun}"
|
|
90
|
+
AUTO_MERGE: "true"
|
|
91
|
+
# Chaves de bloco que a sync NUNCA sobrescreve. Blocos com secret encriptado
|
|
92
|
+
# já são protegidos por shape pelo script (as credenciais deste repo vivem no
|
|
93
|
+
# bloco de app dele, que não existe nesse layout em produção).
|
|
94
|
+
DENY_KEYS: "Site,site"
|
|
95
|
+
|
|
96
|
+
jobs:
|
|
97
|
+
sync:
|
|
98
|
+
runs-on: ubuntu-latest
|
|
99
|
+
steps:
|
|
100
|
+
- name: Resolve origin
|
|
101
|
+
id: cfg
|
|
102
|
+
env:
|
|
103
|
+
INPUT_ORIGIN: \${{ github.event.inputs.origin }}
|
|
104
|
+
VAR_ORIGIN: \${{ vars.SYNC_BLOCKS_ORIGIN }}
|
|
105
|
+
run: |
|
|
106
|
+
origin="\${INPUT_ORIGIN:-$VAR_ORIGIN}"
|
|
107
|
+
if [ -z "$origin" ]; then
|
|
108
|
+
echo "::notice::variável de repo SYNC_BLOCKS_ORIGIN não configurada — sync-blocks-bot inerte."
|
|
109
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
110
|
+
else
|
|
111
|
+
echo "origin=$origin" >> "$GITHUB_OUTPUT"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
- uses: actions/checkout@v4
|
|
115
|
+
if: steps.cfg.outputs.skip != 'true'
|
|
116
|
+
|
|
117
|
+
- uses: oven-sh/setup-bun@v2
|
|
118
|
+
if: steps.cfg.outputs.skip != 'true'
|
|
119
|
+
with:
|
|
120
|
+
bun-version: \${{ env.BUN_VERSION }}
|
|
121
|
+
|
|
122
|
+
- name: Install
|
|
123
|
+
if: steps.cfg.outputs.skip != 'true'
|
|
124
|
+
run: bun install --frozen-lockfile
|
|
125
|
+
|
|
126
|
+
- name: Pull decofile de produção
|
|
127
|
+
if: steps.cfg.outputs.skip != 'true'
|
|
128
|
+
env:
|
|
129
|
+
ORIGIN: \${{ steps.cfg.outputs.origin }}
|
|
130
|
+
PRUNE: \${{ github.event.inputs.prune == 'false' && ' ' || '--prune' }}
|
|
131
|
+
DRY_RUN: \${{ github.event.inputs.dry_run == 'true' && '--dry-run' || ' ' }}
|
|
132
|
+
run: |
|
|
133
|
+
# pipefail é obrigatório: sem ele o \`| tee\` mascara o exit 1 do gate
|
|
134
|
+
# de plaintext secret e o job passa mesmo tendo abortado o pull.
|
|
135
|
+
set -o pipefail
|
|
136
|
+
${pullCmd} \\
|
|
137
|
+
--origin "$ORIGIN" \\
|
|
138
|
+
--out .deco/blocks \\
|
|
139
|
+
--deny "$DENY_KEYS" \\
|
|
140
|
+
--fail-on-plaintext-secret \\
|
|
141
|
+
--json --github $PRUNE $DRY_RUN | tee /tmp/sync-blocks-report.json
|
|
142
|
+
# Prova de que foi ESTE script que rodou, e que ele viu conteúdo. Sem
|
|
143
|
+
# isso, uma invocação errada que resolva para outro bin do pacote
|
|
144
|
+
# termina 0 e o job fica verde tendo rodado a ferramenta errada.
|
|
145
|
+
grep -q '"remoteBlocks"' /tmp/sync-blocks-report.json || {
|
|
146
|
+
echo "::error::o passo de pull não produziu o relatório esperado — comando errado?"
|
|
147
|
+
exit 1
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
- name: Guard — só .deco/blocks pode mudar
|
|
151
|
+
id: guard
|
|
152
|
+
if: steps.cfg.outputs.skip != 'true' && github.event.inputs.dry_run != 'true'
|
|
153
|
+
run: |
|
|
154
|
+
{
|
|
155
|
+
git -c core.quotepath=false diff --name-only HEAD
|
|
156
|
+
git -c core.quotepath=false ls-files --others --exclude-standard
|
|
157
|
+
} | sort -u > /tmp/sync-blocks-changed.txt
|
|
158
|
+
offending="$(grep -v '^\\.deco/blocks/' /tmp/sync-blocks-changed.txt || true)"
|
|
159
|
+
if [ -n "$offending" ]; then
|
|
160
|
+
echo "::error::a sync mexeu fora de .deco/blocks — abortando:"
|
|
161
|
+
echo "$offending"
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
if [ ! -s /tmp/sync-blocks-changed.txt ]; then
|
|
165
|
+
echo "::notice::conteúdo já está em dia, nada a sincronizar."
|
|
166
|
+
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
167
|
+
else
|
|
168
|
+
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
169
|
+
echo "$(wc -l < /tmp/sync-blocks-changed.txt) arquivo(s) de bloco alterado(s)"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# Gate de verdade. Roda AQUI porque PR aberto com GITHUB_TOKEN não dispara
|
|
173
|
+
# o workflow de \`pull_request\` — gatear no CI do PR exigiria um PAT.
|
|
174
|
+
- name: Validar (generate + build)
|
|
175
|
+
if: steps.guard.outputs.changed == 'true'
|
|
176
|
+
run: bun run generate && bun run build
|
|
177
|
+
|
|
178
|
+
- name: Abrir PR (e mergear se AUTO_MERGE)
|
|
179
|
+
if: steps.guard.outputs.changed == 'true'
|
|
180
|
+
env:
|
|
181
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
182
|
+
ORIGIN: \${{ steps.cfg.outputs.origin }}
|
|
183
|
+
run: |
|
|
184
|
+
branch="sync-blocks/$(date -u +%Y-%m-%dT%H%M%SZ)"
|
|
185
|
+
git config user.name "github-actions[bot]"
|
|
186
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
187
|
+
git checkout -b "$branch"
|
|
188
|
+
git add .deco/blocks
|
|
189
|
+
git commit -m "chore(content): sync .deco/blocks de $ORIGIN"
|
|
190
|
+
git push origin "$branch"
|
|
191
|
+
url="$(gh pr create \\
|
|
192
|
+
--title "chore(content): sync .deco/blocks de produção" \\
|
|
193
|
+
--body "Conteúdo puxado de \\\`$ORIGIN/.decofile\\\` pelo workflow \\\`sync-blocks-bot\\\`. Só \\\`.deco/blocks/**\\\` mudou (guard) e \\\`generate + build\\\` passou antes deste PR existir." \\
|
|
194
|
+
--head "$branch")"
|
|
195
|
+
echo "PR: $url"
|
|
196
|
+
if [ "$AUTO_MERGE" = "true" ]; then
|
|
197
|
+
gh pr merge "$url" --squash --delete-branch || \\
|
|
198
|
+
echo "::notice::merge automático bloqueado (branch protection?) — PR aberto para revisão: $url"
|
|
199
|
+
fi
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
@@ -10,9 +10,12 @@ export function generateUiComponents(_ctx: MigrationContext): Record<string, str
|
|
|
10
10
|
getSrcSet,
|
|
11
11
|
registerImageCdnDomain,
|
|
12
12
|
getImageCdnDomain,
|
|
13
|
+
registerImageQuality,
|
|
14
|
+
getImageQuality,
|
|
13
15
|
FACTORS,
|
|
14
16
|
type ImageProps,
|
|
15
17
|
type FitOptions,
|
|
18
|
+
type ImageQuality,
|
|
16
19
|
} from "@decocms/blocks/hooks";
|
|
17
20
|
`;
|
|
18
21
|
|