Skip to content

Commit f4220b5

Browse files
Fix create worker manifest loading (#477)
fix create worker manifest loading
1 parent 471a254 commit f4220b5

14 files changed

Lines changed: 1198 additions & 84 deletions

.changeset/fuzzy-workers-build.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/create": patch
3+
---
4+
5+
Fix Worker usage by adding a provider-based `@tanstack/create/worker` entry that avoids importing the full generated create manifest at startup.

docs/cli-reference.md

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,31 +44,76 @@ tanstack create --list-add-ons --framework React --json
4444
tanstack create --addon-details drizzle --framework React --json
4545
```
4646

47-
### Worker-safe programmatic generation
47+
### Programmatic generation
4848

49-
Use `@tanstack/create/edge` in Cloudflare Workers and other runtimes that do not expose a Node package filesystem. The default `@tanstack/create` export is still the Node/CLI path and scans framework templates from disk.
49+
Use `@tanstack/create/worker` in Cloudflare Workers and other edge SSR runtimes. It does not import the generated template manifest at module startup. Instead, provide a loader for the framework and add-on chunks your Worker supports.
50+
51+
The default `@tanstack/create` export is still the Node/CLI path and scans framework templates from disk. `@tanstack/create/edge` remains the bundled in-memory manifest path; it is Worker-compatible at runtime, but it imports the full generated manifest and is not appropriate for size-constrained Worker bundles.
5052

5153
```ts
5254
import {
53-
createApp,
5455
createMemoryEnvironment,
55-
finalizeAddOns,
56-
getFrameworkById,
57-
populateAddOnOptionsDefaults,
58-
} from '@tanstack/create/edge'
56+
createWorkerCreate,
57+
createWorkerManifestLoader,
58+
} from '@tanstack/create/worker'
59+
import { manifestCatalog } from '@tanstack/create/worker-manifest/catalog'
60+
61+
import type {
62+
WorkerAddOnManifestModule,
63+
WorkerFrameworkManifestModule,
64+
} from '@tanstack/create/worker'
65+
66+
const frameworkLoaders: Record<
67+
string,
68+
() => Promise<WorkerFrameworkManifestModule>
69+
> = {
70+
react: () => import('@tanstack/create/worker-manifest/frameworks/react'),
71+
}
72+
73+
const addOnLoaders: Record<
74+
string,
75+
Record<string, () => Promise<WorkerAddOnManifestModule>>
76+
> = {
77+
react: {
78+
'tanstack-query': () =>
79+
import(
80+
'@tanstack/create/worker-manifest/frameworks/react/add-ons/tanstack-query'
81+
),
82+
cloudflare: () =>
83+
import(
84+
'@tanstack/create/worker-manifest/frameworks/react/add-ons/cloudflare'
85+
),
86+
},
87+
}
5988

60-
const framework = getFrameworkById('react')!
61-
const chosenAddOns = await finalizeAddOns(framework, 'file-router', [
89+
const create = createWorkerCreate(
90+
createWorkerManifestLoader({
91+
loadCatalog: async () => manifestCatalog,
92+
async loadFramework(frameworkId) {
93+
const load = frameworkLoaders[frameworkId]
94+
if (!load) throw new Error(`Unsupported framework: ${frameworkId}`)
95+
return load()
96+
},
97+
async loadAddOn(frameworkId, addOnId) {
98+
const load = addOnLoaders[frameworkId]?.[addOnId]
99+
if (!load) throw new Error(`Unsupported add-on: ${addOnId}`)
100+
return load()
101+
},
102+
}),
103+
)
104+
105+
const framework = await create.getFrameworkById('react')
106+
const chosenAddOns = await create.finalizeAddOns(framework!, 'file-router', [
62107
'tanstack-query',
63108
'cloudflare',
64109
])
65-
const addOnOptions = populateAddOnOptionsDefaults(chosenAddOns)
110+
const addOnOptions = create.populateAddOnOptionsDefaults(chosenAddOns)
66111
const { environment, output } = createMemoryEnvironment('/app')
67112

68-
await createApp(environment, {
113+
await create.createApp(environment, {
69114
projectName: 'app',
70115
targetDir: '/app',
71-
framework,
116+
framework: framework!,
72117
mode: 'file-router',
73118
typescript: true,
74119
tailwind: true,

packages/create/package.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@
1616
"import": "./dist/edge.js",
1717
"default": "./dist/edge.js"
1818
},
19+
"./worker": {
20+
"types": "./dist/types/worker.d.ts",
21+
"import": "./dist/worker.js",
22+
"default": "./dist/worker.js"
23+
},
24+
"./worker/bundled": {
25+
"types": "./dist/types/worker-bundled.d.ts",
26+
"import": "./dist/worker-bundled.js",
27+
"default": "./dist/worker-bundled.js"
28+
},
29+
"./worker-manifest/*": {
30+
"types": "./dist/types/generated/worker/*.d.ts",
31+
"import": "./dist/generated/worker/*.js",
32+
"default": "./dist/generated/worker/*.js"
33+
},
1934
"./manifest": {
2035
"types": "./dist/types/manifest.d.ts",
2136
"import": "./dist/manifest.js",
@@ -31,7 +46,7 @@
3146
"dev": "npm run generate-manifest && tsc --watch",
3247
"test": "npm run generate-manifest && eslint ./src && vitest run",
3348
"test:watch": "npm run generate-manifest && vitest",
34-
"test:coverage": "vitest run --coverage"
49+
"test:coverage": "npm run generate-manifest && vitest run --coverage"
3550
},
3651
"repository": {
3752
"type": "git",

packages/create/scripts/generate-manifest.mjs

Lines changed: 167 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
mkdirSync,
44
readFileSync,
55
readdirSync,
6+
rmSync,
67
writeFileSync,
78
} from 'node:fs'
89
import { dirname, extname, join, relative, resolve } from 'node:path'
@@ -11,6 +12,7 @@ import { fileURLToPath } from 'node:url'
1112
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
1213
const frameworksDir = resolve(packageDir, 'src/frameworks')
1314
const outputFile = resolve(packageDir, 'src/generated/create-manifest.ts')
15+
const workerOutputDir = resolve(packageDir, 'src/generated/worker')
1416

1517
const binaryExtensions = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico'])
1618
const templateRenderers = new Map()
@@ -250,8 +252,8 @@ function compileTemplate(template) {
250252
return lines.join('\n')
251253
}
252254

253-
function createTemplateRendererSource() {
254-
const entries = Array.from(templateRenderers.entries()).sort(([a], [b]) =>
255+
function createTemplateRendererSource(renderers = templateRenderers) {
256+
const entries = Array.from(renderers.entries()).sort(([a], [b]) =>
255257
a.localeCompare(b),
256258
)
257259

@@ -331,7 +333,7 @@ type TemplateRenderContext = {
331333
getPackageManagerExecuteScript: (...args: Array<any>) => string
332334
relativePath: (...args: Array<any>) => string
333335
integrationImportContent: (...args: Array<any>) => string
334-
integrationImportCode: (...args: Array<any>) => string
336+
integrationImportCode: (...args: Array<any>) => string | undefined
335337
renderTemplate: (content: string) => string
336338
ignoreFile: () => never
337339
}
@@ -376,6 +378,10 @@ const templateRenderers: Record<string, TemplateRenderer> = {
376378
${mapEntries}
377379
}
378380
381+
export function hasManifestTemplate(template: string) {
382+
return getManifestTemplateKey(template) in templateRenderers
383+
}
384+
379385
export function renderManifestTemplate(
380386
template: string,
381387
context: TemplateRenderContext,
@@ -390,6 +396,162 @@ export function renderManifestTemplate(
390396
`
391397
}
392398

399+
function createTemplateRenderersForFrameworkBase(framework) {
400+
const renderers = new Map()
401+
402+
for (const [file, contents] of Object.entries(framework.base)) {
403+
if (file.endsWith('.ejs')) {
404+
renderers.set(getTemplateKey(contents), compileTemplate(contents))
405+
}
406+
}
407+
408+
return renderers
409+
}
410+
411+
function createTemplateRenderersForAddOn(addOn) {
412+
const renderers = new Map()
413+
414+
for (const [file, contents] of Object.entries(addOn.files)) {
415+
if (file.endsWith('.ejs')) {
416+
renderers.set(getTemplateKey(contents), compileTemplate(contents))
417+
}
418+
}
419+
420+
if (addOn.packageTemplate) {
421+
renderers.set(
422+
getTemplateKey(addOn.packageTemplate),
423+
compileTemplate(addOn.packageTemplate),
424+
)
425+
}
426+
427+
if (addOn.readmeIsEjs && addOn.readme) {
428+
renderers.set(getTemplateKey(addOn.readme), compileTemplate(addOn.readme))
429+
}
430+
431+
return renderers
432+
}
433+
434+
function stripAddOnForCatalog(addOn) {
435+
const {
436+
files: _files,
437+
deletedFiles: _deletedFiles,
438+
packageTemplate: _packageTemplate,
439+
readme: _readme,
440+
readmeIsEjs: _readmeIsEjs,
441+
...metadata
442+
} = addOn
443+
444+
return metadata
445+
}
446+
447+
function stripFrameworkForCatalog(framework) {
448+
const { base: _base, addOns, ...metadata } = framework
449+
450+
return {
451+
...metadata,
452+
addOns: addOns.map(stripAddOnForCatalog),
453+
}
454+
}
455+
456+
function toModuleSegment(value) {
457+
return value.toLowerCase().replace(/[^a-z0-9_-]+/g, '-')
458+
}
459+
460+
function writeGeneratedModule(file, source) {
461+
mkdirSync(dirname(file), { recursive: true })
462+
writeFileSync(
463+
file,
464+
`// Generated by scripts/generate-manifest.mjs. Do not edit by hand.\n${source}`,
465+
)
466+
}
467+
468+
function writeWorkerManifest(manifest) {
469+
rmSync(workerOutputDir, { recursive: true, force: true })
470+
471+
writeGeneratedModule(
472+
join(workerOutputDir, 'catalog.ts'),
473+
`export const manifestCatalog = ${JSON.stringify(
474+
{
475+
frameworks: manifest.map(stripFrameworkForCatalog),
476+
},
477+
null,
478+
2,
479+
)}\n`,
480+
)
481+
482+
const frameworkLoaders = []
483+
const addOnLoaders = []
484+
485+
for (const framework of manifest) {
486+
const frameworkSegment = toModuleSegment(framework.id)
487+
const frameworkModule = `./frameworks/${frameworkSegment}.js`
488+
frameworkLoaders.push([framework.id, frameworkModule])
489+
490+
writeGeneratedModule(
491+
join(workerOutputDir, 'frameworks', `${frameworkSegment}.ts`),
492+
`${createTemplateRendererSource(
493+
createTemplateRenderersForFrameworkBase(framework),
494+
)}\n\nexport const framework = ${JSON.stringify(
495+
{
496+
id: framework.id,
497+
base: framework.base,
498+
},
499+
null,
500+
2,
501+
)}\n`,
502+
)
503+
504+
for (const addOn of framework.addOns) {
505+
const addOnSegment = toModuleSegment(addOn.id)
506+
const addOnModule = `./frameworks/${frameworkSegment}/add-ons/${addOnSegment}.js`
507+
addOnLoaders.push([framework.id, addOn.id, addOnModule])
508+
509+
writeGeneratedModule(
510+
join(
511+
workerOutputDir,
512+
'frameworks',
513+
frameworkSegment,
514+
'add-ons',
515+
`${addOnSegment}.ts`,
516+
),
517+
`${createTemplateRendererSource(
518+
createTemplateRenderersForAddOn(addOn),
519+
)}\n\nexport const addOn = ${JSON.stringify(addOn, null, 2)}\n`,
520+
)
521+
}
522+
}
523+
524+
const frameworkLoaderSource = frameworkLoaders
525+
.map(
526+
([frameworkId, modulePath]) =>
527+
` ${JSON.stringify(frameworkId)}: () => import(${JSON.stringify(
528+
modulePath,
529+
)}),`,
530+
)
531+
.join('\n')
532+
533+
const addOnLoaderGroups = manifest
534+
.map((framework) => {
535+
const entries = addOnLoaders
536+
.filter(([frameworkId]) => frameworkId === framework.id)
537+
.map(
538+
([, addOnId, modulePath]) =>
539+
` ${JSON.stringify(addOnId)}: () => import(${JSON.stringify(
540+
modulePath,
541+
)}),`,
542+
)
543+
.join('\n')
544+
545+
return ` ${JSON.stringify(framework.id)}: {\n${entries}\n },`
546+
})
547+
.join('\n')
548+
549+
writeGeneratedModule(
550+
join(workerOutputDir, 'bundled-loader.ts'),
551+
`const frameworkLoaders = {\n${frameworkLoaderSource}\n}\n\nconst addOnLoaders = {\n${addOnLoaderGroups}\n}\n\nexport function createBundledWorkerManifestLoader() {\n return {\n async loadCatalog() {\n const module = await import('./catalog.js')\n return module.manifestCatalog\n },\n async loadFramework(frameworkId: string) {\n const load = frameworkLoaders[frameworkId as keyof typeof frameworkLoaders]\n if (!load) {\n throw new Error(\`Framework \${frameworkId} not found in bundled worker manifest\`)\n }\n const module = await load()\n return {\n ...module.framework,\n renderManifestTemplate: module.renderManifestTemplate,\n hasManifestTemplate: module.hasManifestTemplate,\n }\n },\n async loadAddOn(frameworkId: string, addOnId: string) {\n const frameworkAddOnLoaders = addOnLoaders[frameworkId as keyof typeof addOnLoaders]\n const load = frameworkAddOnLoaders?.[addOnId as keyof typeof frameworkAddOnLoaders]\n if (!load) {\n throw new Error(\`Add-on \${addOnId} not found in bundled worker manifest for framework \${frameworkId}\`)\n }\n const module = await load()\n return {\n ...module.addOn,\n renderManifestTemplate: module.renderManifestTemplate,\n hasManifestTemplate: module.hasManifestTemplate,\n }\n },\n }\n}\n`,
552+
)
553+
}
554+
393555
const manifest = [createFramework('react'), createFramework('solid')]
394556

395557
mkdirSync(dirname(outputFile), { recursive: true })
@@ -405,3 +567,5 @@ writeFileSync(
405567
2,
406568
)}\n`,
407569
)
570+
571+
writeWorkerManifest(manifest)

packages/create/src/edge-frameworks.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { createManifestFrameworks } from './generated/create-manifest.js'
1+
import {
2+
createManifestFrameworks,
3+
renderManifestTemplate,
4+
} from './generated/create-manifest.js'
5+
import {
6+
registerTemplateRenderer,
7+
setDefaultTemplateRenderer,
8+
} from './edge-render.js'
29

310
import type {
411
AddOn,
@@ -33,7 +40,13 @@ export function createFrameworkFromManifest(
3340
}
3441
}
3542

36-
const frameworks = createManifestFrameworks().map(createFrameworkFromManifest)
43+
setDefaultTemplateRenderer(renderManifestTemplate)
44+
45+
const frameworks = createManifestFrameworks().map((frameworkDefinition) => {
46+
const framework = createFrameworkFromManifest(frameworkDefinition)
47+
registerTemplateRenderer(framework, renderManifestTemplate)
48+
return framework
49+
})
3750

3851
export function getFrameworkById(id: string) {
3952
if (id === 'react-cra') {

packages/create/src/edge-package-json.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render } from './edge-render.js'
1+
import { renderForOptions } from './edge-render.js'
22
import { formatCommand, sortObject } from './utils.js'
33
import { getPackageManagerExecuteCommand } from './package-manager.js'
44

@@ -151,7 +151,9 @@ export function createPackageJSON(options: Options) {
151151
}
152152

153153
try {
154-
addOnPackageJSON = JSON.parse(render(addOn.packageTemplate, templateValues))
154+
addOnPackageJSON = JSON.parse(
155+
renderForOptions(options, addOn.packageTemplate, templateValues),
156+
)
155157
} catch (error) {
156158
console.error(
157159
`Error processing package.json.ejs for add-on ${addOn.id}:`,

0 commit comments

Comments
 (0)