@decocms/start 0.31.1 → 0.32.1
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 +3 -1
- package/scripts/migrate/colors.ts +46 -0
- package/scripts/migrate/phase-analyze.ts +402 -0
- package/scripts/migrate/phase-cleanup.ts +212 -0
- package/scripts/migrate/phase-report.ts +171 -0
- package/scripts/migrate/phase-scaffold.ts +133 -0
- package/scripts/migrate/phase-transform.ts +102 -0
- package/scripts/migrate/phase-verify.ts +308 -0
- package/scripts/migrate/templates/knip-config.ts +27 -0
- package/scripts/migrate/templates/package-json.ts +98 -0
- package/scripts/migrate/templates/routes.ts +280 -0
- package/scripts/migrate/templates/server-entry.ts +163 -0
- package/scripts/migrate/templates/setup.ts +30 -0
- package/scripts/migrate/templates/tsconfig.ts +21 -0
- package/scripts/migrate/templates/vite-config.ts +108 -0
- package/scripts/migrate/templates/wrangler.ts +25 -0
- package/scripts/migrate/transforms/deno-isms.ts +59 -0
- package/scripts/migrate/transforms/fresh-apis.ts +218 -0
- package/scripts/migrate/transforms/imports.ts +217 -0
- package/scripts/migrate/transforms/jsx.ts +184 -0
- package/scripts/migrate/transforms/tailwind.ts +409 -0
- package/scripts/migrate/types.ts +141 -0
- package/scripts/migrate.ts +135 -0
- package/scripts/tailwind-lint.ts +518 -0
- package/src/types/widgets.ts +1 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export function generateRoutes(
|
|
4
|
+
ctx: MigrationContext,
|
|
5
|
+
): Record<string, string> {
|
|
6
|
+
const siteName = ctx.siteName;
|
|
7
|
+
const siteTitle = siteName.charAt(0).toUpperCase() + siteName.slice(1);
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
"src/routes/__root.tsx": generateRoot(ctx, siteTitle),
|
|
11
|
+
"src/routes/index.tsx": generateIndex(siteTitle),
|
|
12
|
+
"src/routes/$.tsx": generateCatchAll(siteTitle),
|
|
13
|
+
"src/routes/deco/meta.ts": generateDecoMeta(),
|
|
14
|
+
"src/routes/deco/invoke.$.ts": generateDecoInvoke(),
|
|
15
|
+
"src/routes/deco/render.ts": generateDecoRender(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function generateRoot(ctx: MigrationContext, siteTitle: string): string {
|
|
20
|
+
const gtmScript = ctx.gtmId
|
|
21
|
+
? `
|
|
22
|
+
// Google Tag Manager
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (typeof window === "undefined") return;
|
|
25
|
+
const script = document.createElement("script");
|
|
26
|
+
script.async = true;
|
|
27
|
+
script.src = "https://www.googletagmanager.com/gtm.js?id=${ctx.gtmId}";
|
|
28
|
+
document.head.appendChild(script);
|
|
29
|
+
window.dataLayer = window.dataLayer || [];
|
|
30
|
+
window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
|
|
31
|
+
}, []);`
|
|
32
|
+
: "";
|
|
33
|
+
|
|
34
|
+
return `import { useState, useEffect, useRef } from "react";
|
|
35
|
+
import {
|
|
36
|
+
createRootRoute,
|
|
37
|
+
HeadContent,
|
|
38
|
+
Outlet,
|
|
39
|
+
Scripts,
|
|
40
|
+
useRouterState,
|
|
41
|
+
} from "@tanstack/react-router";
|
|
42
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
43
|
+
import { LiveControls } from "@decocms/start/hooks";
|
|
44
|
+
import { ANALYTICS_SCRIPT } from "@decocms/start/sdk/analytics";
|
|
45
|
+
// @ts-ignore Vite ?url import
|
|
46
|
+
import appCss from "../styles/app.css?url";
|
|
47
|
+
|
|
48
|
+
declare global {
|
|
49
|
+
interface Window {
|
|
50
|
+
__deco_ready?: boolean;
|
|
51
|
+
dataLayer: unknown[];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const PROGRESS_CSS = \`
|
|
56
|
+
@keyframes progressSlide { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
|
|
57
|
+
.nav-progress-bar { animation: progressSlide 1s ease-in-out infinite; }
|
|
58
|
+
\`;
|
|
59
|
+
|
|
60
|
+
function NavigationProgress() {
|
|
61
|
+
const isLoading = useRouterState({ select: (s) => s.isLoading });
|
|
62
|
+
if (!isLoading) return null;
|
|
63
|
+
return (
|
|
64
|
+
<div className="fixed top-0 left-0 right-0 z-[9999] h-1 bg-primary/20 overflow-hidden">
|
|
65
|
+
<style dangerouslySetInnerHTML={{ __html: PROGRESS_CSS }} />
|
|
66
|
+
<div className="nav-progress-bar h-full w-1/3 bg-primary rounded-full" />
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function StableOutlet() {
|
|
72
|
+
const isLoading = useRouterState({ select: (s) => s.isLoading });
|
|
73
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
74
|
+
const savedHeight = useRef<number | undefined>(undefined);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (isLoading && ref.current) {
|
|
78
|
+
savedHeight.current = ref.current.offsetHeight;
|
|
79
|
+
}
|
|
80
|
+
if (!isLoading) {
|
|
81
|
+
savedHeight.current = undefined;
|
|
82
|
+
}
|
|
83
|
+
}, [isLoading]);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div ref={ref} style={savedHeight.current ? { minHeight: savedHeight.current } : undefined}>
|
|
87
|
+
<Outlet />
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const DECO_EVENTS_BOOTSTRAP = \`
|
|
93
|
+
window.DECO = window.DECO || {};
|
|
94
|
+
window.DECO.events = window.DECO.events || {
|
|
95
|
+
_q: [],
|
|
96
|
+
_subs: [],
|
|
97
|
+
dispatch: function(e) {
|
|
98
|
+
this._q.push(e);
|
|
99
|
+
for (var i = 0; i < this._subs.length; i++) {
|
|
100
|
+
try { this._subs[i](e); } catch(err) { console.error('[DECO.events]', err); }
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
subscribe: function(fn) {
|
|
104
|
+
this._subs.push(fn);
|
|
105
|
+
for (var i = 0; i < this._q.length; i++) {
|
|
106
|
+
try { fn(this._q[i]); } catch(err) {}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
window.dataLayer = window.dataLayer || [];
|
|
111
|
+
\`;
|
|
112
|
+
|
|
113
|
+
export const Route = createRootRoute({
|
|
114
|
+
head: () => ({
|
|
115
|
+
meta: [
|
|
116
|
+
{ charSet: "utf-8" },
|
|
117
|
+
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
118
|
+
{ title: "${siteTitle}" },
|
|
119
|
+
],
|
|
120
|
+
links: [
|
|
121
|
+
{ rel: "stylesheet", href: appCss },
|
|
122
|
+
{ rel: "icon", href: "/favicon.ico" },
|
|
123
|
+
],
|
|
124
|
+
}),
|
|
125
|
+
component: RootLayout,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
function RootLayout() {
|
|
129
|
+
const [queryClient] = useState(
|
|
130
|
+
() =>
|
|
131
|
+
new QueryClient({
|
|
132
|
+
defaultOptions: {
|
|
133
|
+
queries: { staleTime: 30_000 },
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
${gtmScript}
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
const id = setTimeout(() => {
|
|
141
|
+
window.__deco_ready = true;
|
|
142
|
+
document.dispatchEvent(new Event("deco:ready"));
|
|
143
|
+
}, 500);
|
|
144
|
+
return () => clearTimeout(id);
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<html lang="pt-BR" data-theme="light" suppressHydrationWarning>
|
|
149
|
+
<head>
|
|
150
|
+
<HeadContent />
|
|
151
|
+
</head>
|
|
152
|
+
<body className="bg-base-200 text-base-content" suppressHydrationWarning>
|
|
153
|
+
<script dangerouslySetInnerHTML={{ __html: DECO_EVENTS_BOOTSTRAP }} />
|
|
154
|
+
<QueryClientProvider client={queryClient}>
|
|
155
|
+
<NavigationProgress />
|
|
156
|
+
<main>
|
|
157
|
+
<StableOutlet />
|
|
158
|
+
</main>
|
|
159
|
+
</QueryClientProvider>
|
|
160
|
+
<LiveControls site="${ctx.siteName}" />
|
|
161
|
+
<script type="module" dangerouslySetInnerHTML={{ __html: ANALYTICS_SCRIPT }} />
|
|
162
|
+
<Scripts />
|
|
163
|
+
</body>
|
|
164
|
+
</html>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function generateIndex(siteTitle: string): string {
|
|
171
|
+
return `import { createFileRoute } from "@tanstack/react-router";
|
|
172
|
+
import { cmsHomeRouteConfig, deferredSectionLoader } from "@decocms/start/routes";
|
|
173
|
+
import { DecoPageRenderer } from "@decocms/start/hooks";
|
|
174
|
+
|
|
175
|
+
export const Route = createFileRoute("/")({
|
|
176
|
+
...cmsHomeRouteConfig({
|
|
177
|
+
defaultTitle: "${siteTitle}",
|
|
178
|
+
siteName: "${siteTitle}",
|
|
179
|
+
}),
|
|
180
|
+
component: HomePage,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
function HomePage() {
|
|
184
|
+
const data = Route.useLoaderData() as Record<string, any> | null;
|
|
185
|
+
|
|
186
|
+
if (!data) {
|
|
187
|
+
return (
|
|
188
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
189
|
+
<div className="text-center">
|
|
190
|
+
<h1 className="text-4xl font-bold mb-4">${siteTitle}</h1>
|
|
191
|
+
<p className="text-sm text-base-content/40 mt-2">Nenhuma pagina CMS encontrada para /</p>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<DecoPageRenderer
|
|
199
|
+
sections={data.resolvedSections ?? []}
|
|
200
|
+
deferredSections={data.deferredSections ?? []}
|
|
201
|
+
deferredPromises={data.deferredPromises}
|
|
202
|
+
pagePath={data.pagePath}
|
|
203
|
+
pageUrl={data.pageUrl}
|
|
204
|
+
loadDeferredSectionFn={deferredSectionLoader}
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function generateCatchAll(siteTitle: string): string {
|
|
212
|
+
return `import { createFileRoute } from "@tanstack/react-router";
|
|
213
|
+
import { cmsRouteConfig, deferredSectionLoader } from "@decocms/start/routes";
|
|
214
|
+
import { DecoPageRenderer } from "@decocms/start/hooks";
|
|
215
|
+
|
|
216
|
+
const routeConfig = cmsRouteConfig({
|
|
217
|
+
siteName: "${siteTitle}",
|
|
218
|
+
defaultTitle: "${siteTitle}",
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
export const Route = createFileRoute("/$")({
|
|
222
|
+
...routeConfig,
|
|
223
|
+
component: CmsPage,
|
|
224
|
+
notFoundComponent: NotFoundPage,
|
|
225
|
+
staleTime: 30_000,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
function CmsPage() {
|
|
229
|
+
const data = Route.useLoaderData() as Record<string, any> | null;
|
|
230
|
+
if (!data) return <NotFoundPage />;
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<DecoPageRenderer
|
|
234
|
+
sections={data.resolvedSections ?? []}
|
|
235
|
+
deferredSections={data.deferredSections ?? []}
|
|
236
|
+
deferredPromises={data.deferredPromises}
|
|
237
|
+
pagePath={data.pagePath}
|
|
238
|
+
pageUrl={data.pageUrl}
|
|
239
|
+
loadDeferredSectionFn={deferredSectionLoader}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function NotFoundPage() {
|
|
245
|
+
return (
|
|
246
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
247
|
+
<div className="text-center">
|
|
248
|
+
<h1 className="text-6xl font-bold text-base-content/20 mb-4">404</h1>
|
|
249
|
+
<h2 className="text-2xl font-bold mb-2">Pagina nao encontrada</h2>
|
|
250
|
+
<a href="/" className="btn btn-primary">Voltar para Home</a>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function generateDecoMeta(): string {
|
|
259
|
+
return `import { createFileRoute } from "@tanstack/react-router";
|
|
260
|
+
import { decoMetaRoute } from "@decocms/start/routes";
|
|
261
|
+
|
|
262
|
+
export const Route = createFileRoute("/deco/meta")(decoMetaRoute);
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function generateDecoInvoke(): string {
|
|
267
|
+
return `import { createFileRoute } from "@tanstack/react-router";
|
|
268
|
+
import { decoInvokeRoute } from "@decocms/start/routes";
|
|
269
|
+
|
|
270
|
+
export const Route = createFileRoute("/deco/invoke/$")(decoInvokeRoute);
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function generateDecoRender(): string {
|
|
275
|
+
return `import { createFileRoute } from "@tanstack/react-router";
|
|
276
|
+
import { decoRenderRoute } from "@decocms/start/routes";
|
|
277
|
+
|
|
278
|
+
export const Route = createFileRoute("/deco/render")(decoRenderRoute);
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export function generateServerEntry(
|
|
4
|
+
ctx: MigrationContext,
|
|
5
|
+
): Record<string, string> {
|
|
6
|
+
return {
|
|
7
|
+
"src/server.ts": generateServer(),
|
|
8
|
+
"src/worker-entry.ts": generateWorkerEntry(ctx),
|
|
9
|
+
"src/router.tsx": generateRouter(),
|
|
10
|
+
"src/runtime.ts": generateRuntime(),
|
|
11
|
+
"src/context.ts": generateContext(ctx),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function generateServer(): string {
|
|
16
|
+
return `import {
|
|
17
|
+
createStartHandler,
|
|
18
|
+
defaultStreamHandler,
|
|
19
|
+
} from "@tanstack/react-start/server";
|
|
20
|
+
|
|
21
|
+
export default createStartHandler(defaultStreamHandler);
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function generateWorkerEntry(_ctx: MigrationContext): string {
|
|
26
|
+
return `/**
|
|
27
|
+
* Cloudflare Worker entry point.
|
|
28
|
+
*
|
|
29
|
+
* For a simple site without VTEX proxy or A/B testing, this is a thin wrapper
|
|
30
|
+
* around the TanStack Start handler. Add proxy logic, security headers, or
|
|
31
|
+
* A/B testing as needed.
|
|
32
|
+
*/
|
|
33
|
+
import "./setup";
|
|
34
|
+
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
|
|
35
|
+
import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
|
|
36
|
+
import {
|
|
37
|
+
handleMeta,
|
|
38
|
+
handleDecofileRead,
|
|
39
|
+
handleDecofileReload,
|
|
40
|
+
handleRender,
|
|
41
|
+
corsHeaders,
|
|
42
|
+
} from "@decocms/start/admin";
|
|
43
|
+
|
|
44
|
+
const serverEntry = createServerEntry({ fetch: handler.fetch });
|
|
45
|
+
|
|
46
|
+
export default createDecoWorkerEntry(serverEntry, {
|
|
47
|
+
admin: {
|
|
48
|
+
handleMeta,
|
|
49
|
+
handleDecofileRead,
|
|
50
|
+
handleDecofileReload,
|
|
51
|
+
handleRender,
|
|
52
|
+
corsHeaders,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function generateRouter(): string {
|
|
59
|
+
return `import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
|
60
|
+
import type { SearchSerializer, SearchParser } from "@tanstack/react-router";
|
|
61
|
+
import { routeTree } from "./routeTree.gen";
|
|
62
|
+
import "./setup";
|
|
63
|
+
|
|
64
|
+
const parseSearch: SearchParser = (searchStr) => {
|
|
65
|
+
const str = searchStr.startsWith("?") ? searchStr.slice(1) : searchStr;
|
|
66
|
+
if (!str) return {};
|
|
67
|
+
const params = new URLSearchParams(str);
|
|
68
|
+
const result: Record<string, string | string[]> = {};
|
|
69
|
+
for (const key of new Set(params.keys())) {
|
|
70
|
+
const values = params.getAll(key);
|
|
71
|
+
result[key] = values.length === 1 ? values[0] : values;
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const stringifySearch: SearchSerializer = (search) => {
|
|
77
|
+
const params = new URLSearchParams();
|
|
78
|
+
for (const [key, value] of Object.entries(search)) {
|
|
79
|
+
if (value === undefined || value === null || value === "") continue;
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
for (const v of value) params.append(key, String(v));
|
|
82
|
+
} else {
|
|
83
|
+
params.append(key, String(value));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const str = params.toString();
|
|
87
|
+
return str ? \`?\${str}\` : "";
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function getRouter() {
|
|
91
|
+
const router = createTanStackRouter({
|
|
92
|
+
routeTree,
|
|
93
|
+
scrollRestoration: true,
|
|
94
|
+
defaultPreload: "intent",
|
|
95
|
+
parseSearch,
|
|
96
|
+
stringifySearch,
|
|
97
|
+
});
|
|
98
|
+
return router;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
declare module "@tanstack/react-router" {
|
|
102
|
+
interface Register {
|
|
103
|
+
router: ReturnType<typeof getRouter>;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function generateRuntime(): string {
|
|
110
|
+
return `/**
|
|
111
|
+
* Runtime invoke proxy — turns nested property access into typed RPC calls.
|
|
112
|
+
*
|
|
113
|
+
* invoke.vtex.loaders.productList(props)
|
|
114
|
+
* → POST /deco/invoke/vtex/loaders/productList
|
|
115
|
+
*/
|
|
116
|
+
function createNestedInvokeProxy(path: string[] = []): any {
|
|
117
|
+
return new Proxy(
|
|
118
|
+
Object.assign(async (props: any) => {
|
|
119
|
+
const key = path.join("/");
|
|
120
|
+
for (const k of [key, \`\${key}.ts\`]) {
|
|
121
|
+
const response = await fetch(\`/deco/invoke/\${k}\`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/json" },
|
|
124
|
+
body: JSON.stringify(props ?? {}),
|
|
125
|
+
});
|
|
126
|
+
if (response.status === 404) continue;
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error(\`invoke(\${k}) failed: \${response.status}\`);
|
|
129
|
+
}
|
|
130
|
+
return response.json();
|
|
131
|
+
}
|
|
132
|
+
throw new Error(\`invoke(\${key}) failed: handler not found\`);
|
|
133
|
+
}, {}),
|
|
134
|
+
{
|
|
135
|
+
get(_target: any, prop: string) {
|
|
136
|
+
if (prop === "then" || prop === "catch" || prop === "finally") {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
return createNestedInvokeProxy([...path, prop]);
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const invoke = createNestedInvokeProxy() as any;
|
|
146
|
+
export const Runtime = { invoke };
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function generateContext(ctx: MigrationContext): string {
|
|
151
|
+
return `import { createContext } from "react";
|
|
152
|
+
|
|
153
|
+
export interface AccountContextValue {
|
|
154
|
+
name: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const Account = createContext<AccountContextValue>({
|
|
158
|
+
name: "${ctx.siteName}",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export default Account;
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export function generateSetup(_ctx: MigrationContext): string {
|
|
4
|
+
return `/**
|
|
5
|
+
* Site setup — registers all sections, loaders and matchers with the CMS.
|
|
6
|
+
*
|
|
7
|
+
* This file is imported by router.tsx at startup.
|
|
8
|
+
* It uses import.meta.glob to lazily discover all section components.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
registerSections,
|
|
12
|
+
loadBlocks,
|
|
13
|
+
setBlocks,
|
|
14
|
+
} from "@decocms/start/cms";
|
|
15
|
+
import { registerBuiltinMatchers } from "@decocms/start/matchers/builtins";
|
|
16
|
+
|
|
17
|
+
// -- CMS Blocks --
|
|
18
|
+
// Load generated blocks at module level so they're available for resolution.
|
|
19
|
+
const blocks = loadBlocks();
|
|
20
|
+
setBlocks(blocks);
|
|
21
|
+
|
|
22
|
+
// -- Section Registry --
|
|
23
|
+
// Discovers all .tsx files under src/sections/ and registers them as CMS blocks.
|
|
24
|
+
const sectionModules = import.meta.glob("./sections/**/*.tsx");
|
|
25
|
+
registerSections(sectionModules);
|
|
26
|
+
|
|
27
|
+
// -- Matchers --
|
|
28
|
+
registerBuiltinMatchers();
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function generateTsconfig(): string {
|
|
2
|
+
const config = {
|
|
3
|
+
compilerOptions: {
|
|
4
|
+
jsx: "react-jsx",
|
|
5
|
+
moduleResolution: "bundler",
|
|
6
|
+
module: "ESNext",
|
|
7
|
+
target: "ES2022",
|
|
8
|
+
skipLibCheck: true,
|
|
9
|
+
strictNullChecks: true,
|
|
10
|
+
forceConsistentCasingInFileNames: true,
|
|
11
|
+
types: ["vite/client"],
|
|
12
|
+
baseUrl: ".",
|
|
13
|
+
paths: {
|
|
14
|
+
"~/*": ["./src/*"],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
include: ["src/**/*", "vite.config.ts"],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
21
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export function generateViteConfig(ctx: MigrationContext): string {
|
|
4
|
+
return `import { cloudflare } from "@cloudflare/vite-plugin";
|
|
5
|
+
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
6
|
+
import { decoVitePlugin } from "@decocms/start/vite";
|
|
7
|
+
import react from "@vitejs/plugin-react";
|
|
8
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
9
|
+
import { defineConfig } from "vite";
|
|
10
|
+
import path from "path";
|
|
11
|
+
|
|
12
|
+
const srcDir = path.resolve(__dirname, "src");
|
|
13
|
+
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
server: {
|
|
16
|
+
allowedHosts: [".decocdn.com"],
|
|
17
|
+
},
|
|
18
|
+
plugins: [
|
|
19
|
+
cloudflare({ viteEnvironment: { name: "ssr" } }),
|
|
20
|
+
tanstackStart({ server: { entry: "server" } }),
|
|
21
|
+
react({
|
|
22
|
+
babel: {
|
|
23
|
+
plugins: [
|
|
24
|
+
["babel-plugin-react-compiler", { target: "19" }],
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
tailwindcss(),
|
|
29
|
+
decoVitePlugin(),
|
|
30
|
+
{
|
|
31
|
+
name: "site-manual-chunks",
|
|
32
|
+
config(_cfg, { command }) {
|
|
33
|
+
if (command !== "build") return;
|
|
34
|
+
return {
|
|
35
|
+
build: {
|
|
36
|
+
rollupOptions: {
|
|
37
|
+
output: {
|
|
38
|
+
manualChunks(id: string) {
|
|
39
|
+
if (id.includes("node_modules/react-dom") || id.includes("node_modules/react/"))
|
|
40
|
+
return "vendor-react";
|
|
41
|
+
if (id.includes("@tanstack/react-router") || id.includes("@tanstack/start"))
|
|
42
|
+
return "vendor-router";
|
|
43
|
+
if (id.includes("@tanstack/react-query")) return "vendor-query";
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "deco-stub-meta-gen",
|
|
53
|
+
enforce: "pre" as const,
|
|
54
|
+
resolveId(id, importer, options) {
|
|
55
|
+
if (!options?.ssr && importer && id.includes("meta.gen")) {
|
|
56
|
+
return "\\0stub:meta-gen";
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
load(id) {
|
|
60
|
+
if (id === "\\0stub:meta-gen") {
|
|
61
|
+
return "export default {};";
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
build: {
|
|
67
|
+
sourcemap: "hidden",
|
|
68
|
+
rollupOptions: {
|
|
69
|
+
onLog(level, log, handler) {
|
|
70
|
+
if (
|
|
71
|
+
log.code === "PLUGIN_WARNING" &&
|
|
72
|
+
log.plugin === "vite:reporter" &&
|
|
73
|
+
log.message?.includes("dynamic import will not move module")
|
|
74
|
+
) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
handler(level, log);
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
define: {
|
|
82
|
+
"process.env.DECO_SITE_NAME": JSON.stringify(
|
|
83
|
+
process.env.DECO_SITE_NAME || "${ctx.siteName}"
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
esbuild: {
|
|
87
|
+
jsx: "automatic",
|
|
88
|
+
jsxImportSource: "react",
|
|
89
|
+
},
|
|
90
|
+
resolve: {
|
|
91
|
+
dedupe: [
|
|
92
|
+
"@tanstack/react-start",
|
|
93
|
+
"@tanstack/react-router",
|
|
94
|
+
"@tanstack/react-start-server",
|
|
95
|
+
"@tanstack/start-server-core",
|
|
96
|
+
"@tanstack/start-client-core",
|
|
97
|
+
"@tanstack/start-plugin-core",
|
|
98
|
+
"@tanstack/start-storage-context",
|
|
99
|
+
"react",
|
|
100
|
+
"react-dom",
|
|
101
|
+
],
|
|
102
|
+
alias: {
|
|
103
|
+
"~": srcDir,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export function generateWrangler(ctx: MigrationContext): string {
|
|
4
|
+
// Sanitize site name for worker name (lowercase, hyphens only)
|
|
5
|
+
const workerName = ctx.siteName
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
8
|
+
.replace(/-+/g, "-");
|
|
9
|
+
|
|
10
|
+
return `{
|
|
11
|
+
"name": "${workerName}-tanstack",
|
|
12
|
+
"compatibility_date": "2026-02-14",
|
|
13
|
+
"compatibility_flags": ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
|
|
14
|
+
"main": "./src/worker-entry.ts",
|
|
15
|
+
"workers_dev": true,
|
|
16
|
+
"preview_urls": true,
|
|
17
|
+
"observability": {
|
|
18
|
+
"logs": {
|
|
19
|
+
"enabled": true,
|
|
20
|
+
"invocation_logs": true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { TransformResult } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Removes Deno-specific patterns:
|
|
5
|
+
*
|
|
6
|
+
* - // deno-lint-ignore ... comments
|
|
7
|
+
* - // deno-lint-ignore-file comments
|
|
8
|
+
* - npm: prefix from import specifiers (already in imports.ts but this catches stragglers)
|
|
9
|
+
* - Strip .ts/.tsx extensions from local import paths
|
|
10
|
+
*/
|
|
11
|
+
export function transformDenoIsms(content: string): TransformResult {
|
|
12
|
+
const notes: string[] = [];
|
|
13
|
+
let changed = false;
|
|
14
|
+
let result = content;
|
|
15
|
+
|
|
16
|
+
// Remove deno-lint-ignore comments (single line and file-level)
|
|
17
|
+
// Also handle JSX comment form: {/* deno-lint-ignore ... */}
|
|
18
|
+
if (/deno-lint-ignore/.test(result)) {
|
|
19
|
+
result = result.replace(/^\s*\/\/\s*deno-lint-ignore[^\n]*\n?/gm, "");
|
|
20
|
+
result = result.replace(/\s*\{\/\*\s*deno-lint-ignore[^*]*\*\/\}\s*/g, " ");
|
|
21
|
+
changed = true;
|
|
22
|
+
notes.push("Removed deno-lint-ignore comments");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Remove npm: prefix in import specifiers that weren't caught by imports transform
|
|
26
|
+
if (/from\s+["']npm:/.test(result)) {
|
|
27
|
+
// npm:pkg@version → pkg (strip version)
|
|
28
|
+
result = result.replace(
|
|
29
|
+
/(from\s+["'])npm:(@?[^@"']+)@[^"']*(["'])/g,
|
|
30
|
+
"$1$2$3",
|
|
31
|
+
);
|
|
32
|
+
// npm:pkg → pkg (no version)
|
|
33
|
+
result = result.replace(
|
|
34
|
+
/(from\s+["'])npm:([^"'@][^"']*)(["'])/g,
|
|
35
|
+
"$1$2$3",
|
|
36
|
+
);
|
|
37
|
+
changed = true;
|
|
38
|
+
notes.push("Removed npm: prefix from imports");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Remove Deno.* API usages — flag for manual review
|
|
42
|
+
if (result.includes("Deno.")) {
|
|
43
|
+
notes.push("MANUAL: Deno.* API usage found — needs Node.js equivalent");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Remove /// <reference ... /> directives for Deno
|
|
47
|
+
const refDirectiveRegex =
|
|
48
|
+
/^\/\/\/\s*<reference\s+(?:lib|path|types)\s*=\s*"[^"]*deno[^"]*"\s*\/>\s*\n?/gm;
|
|
49
|
+
if (refDirectiveRegex.test(result)) {
|
|
50
|
+
result = result.replace(refDirectiveRegex, "");
|
|
51
|
+
changed = true;
|
|
52
|
+
notes.push("Removed Deno reference directives");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Clean up blank lines
|
|
56
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
57
|
+
|
|
58
|
+
return { content: result, changed, notes };
|
|
59
|
+
}
|