@fluojs/cli 2.0.1 → 3.0.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/README.ko.md +169 -21
- package/README.md +172 -21
- package/dist/cli.d.ts +5 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +127 -35
- package/dist/commands/diagnostics.d.ts +35 -0
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +60 -0
- package/dist/commands/inspect.d.ts +5 -1
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +84 -17
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +27 -15
- package/dist/commands/migration-transform-tokens.d.ts +15 -0
- package/dist/commands/migration-transform-tokens.d.ts.map +1 -0
- package/dist/commands/migration-transform-tokens.js +41 -0
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +29 -9
- package/dist/commands/output-path-safety.d.ts +14 -0
- package/dist/commands/output-path-safety.d.ts.map +1 -0
- package/dist/commands/output-path-safety.js +34 -0
- package/dist/commands/scripts.js +2 -2
- package/dist/commands/typegen-artifact.d.ts +54 -0
- package/dist/commands/typegen-artifact.d.ts.map +1 -0
- package/dist/commands/typegen-artifact.js +117 -0
- package/dist/commands/typegen-generation-child.d.ts +2 -0
- package/dist/commands/typegen-generation-child.d.ts.map +1 -0
- package/dist/commands/typegen-generation-child.js +59 -0
- package/dist/commands/typegen-generation-process.d.ts +46 -0
- package/dist/commands/typegen-generation-process.d.ts.map +1 -0
- package/dist/commands/typegen-generation-process.js +131 -0
- package/dist/commands/typegen-generation-protocol.d.ts +16 -0
- package/dist/commands/typegen-generation-protocol.d.ts.map +1 -0
- package/dist/commands/typegen-generation-protocol.js +35 -0
- package/dist/commands/typegen-isolated-source.d.ts +10 -0
- package/dist/commands/typegen-isolated-source.d.ts.map +1 -0
- package/dist/commands/typegen-isolated-source.js +75 -0
- package/dist/commands/typegen-options.d.ts +20 -0
- package/dist/commands/typegen-options.d.ts.map +1 -0
- package/dist/commands/typegen-options.js +71 -0
- package/dist/commands/typegen-source.d.ts +59 -0
- package/dist/commands/typegen-source.d.ts.map +1 -0
- package/dist/commands/typegen-source.js +183 -0
- package/dist/commands/typegen-watch.d.ts +46 -0
- package/dist/commands/typegen-watch.d.ts.map +1 -0
- package/dist/commands/typegen-watch.js +212 -0
- package/dist/commands/typegen.d.ts +29 -0
- package/dist/commands/typegen.d.ts.map +1 -0
- package/dist/commands/typegen.js +119 -0
- package/dist/dev-runner/node-restart-runner.d.ts.map +1 -1
- package/dist/dev-runner/node-restart-runner.js +54 -7
- package/dist/fixtures/inspect-react-app.module.d.ts +4 -0
- package/dist/fixtures/inspect-react-app.module.d.ts.map +1 -0
- package/dist/fixtures/inspect-react-app.module.js +32 -0
- package/dist/fixtures/typegen-react-app.module.d.ts +4 -0
- package/dist/fixtures/typegen-react-app.module.d.ts.map +1 -0
- package/dist/fixtures/typegen-react-app.module.js +33 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/new/install.js +1 -1
- package/dist/new/package-spec-resolver.d.ts.map +1 -1
- package/dist/new/package-spec-resolver.js +50 -59
- package/dist/new/prompt.d.ts +1 -0
- package/dist/new/prompt.d.ts.map +1 -1
- package/dist/new/prompt.js +40 -12
- package/dist/new/published-internal-dependencies.d.ts +20 -0
- package/dist/new/published-internal-dependencies.d.ts.map +1 -0
- package/dist/new/published-internal-dependencies.js +20 -0
- package/dist/new/react-vite-ssr-scaffold.d.ts +19 -0
- package/dist/new/react-vite-ssr-scaffold.d.ts.map +1 -0
- package/dist/new/react-vite-ssr-scaffold.js +108 -0
- package/dist/new/resolver.d.ts +2 -2
- package/dist/new/resolver.d.ts.map +1 -1
- package/dist/new/resolver.js +5 -1
- package/dist/new/scaffold.d.ts.map +1 -1
- package/dist/new/scaffold.js +62 -31
- package/dist/new/starter-profiles.d.ts +8 -4
- package/dist/new/starter-profiles.d.ts.map +1 -1
- package/dist/new/starter-profiles.js +52 -18
- package/dist/new/templates/react-vite-ssr/README.md.ejs +74 -0
- package/dist/new/templates/react-vite-ssr/playwright.config.ts.ejs +26 -0
- package/dist/new/templates/react-vite-ssr/src/app.test.ts.ejs +82 -0
- package/dist/new/templates/react-vite-ssr/src/app.ts.ejs +91 -0
- package/dist/new/templates/react-vite-ssr/src/entry-client.tsx.ejs +29 -0
- package/dist/new/templates/react-vite-ssr/src/entry-server.tsx.ejs +52 -0
- package/dist/new/templates/react-vite-ssr/src/load-manifest.test.ts.ejs +55 -0
- package/dist/new/templates/react-vite-ssr/src/load-manifest.ts.ejs +53 -0
- package/dist/new/templates/react-vite-ssr/src/main.ts.ejs +18 -0
- package/dist/new/templates/react-vite-ssr/src/page.tsx.ejs +43 -0
- package/dist/new/templates/react-vite-ssr/src/react-app.test.tsx.ejs +67 -0
- package/dist/new/templates/react-vite-ssr/src/react-app.tsx.ejs +46 -0
- package/dist/new/templates/react-vite-ssr/src/styles.css.ejs +25 -0
- package/dist/new/templates/react-vite-ssr/src/styles.d.ts.ejs +1 -0
- package/dist/new/templates/react-vite-ssr/tests/production-hydration.spec.ts.ejs +54 -0
- package/dist/new/templates/react-vite-ssr/tsconfig.json.ejs +16 -0
- package/dist/new/templates/react-vite-ssr/vite.client.config.ts.ejs +24 -0
- package/dist/new/templates/react-vite-ssr/vite.server.config.ts.ejs +17 -0
- package/dist/new/templates/react-vite-ssr/vitest.config.ts.ejs +10 -0
- package/dist/new/types.d.ts +4 -0
- package/dist/new/types.d.ts.map +1 -1
- package/dist/public-typegen.d.ts +14 -0
- package/dist/public-typegen.d.ts.map +1 -0
- package/dist/public-typegen.js +17 -0
- package/dist/run-cli.d.ts +2 -1
- package/dist/run-cli.d.ts.map +1 -1
- package/dist/studio/sidecar.d.ts.map +1 -1
- package/dist/studio/sidecar.js +35 -9
- package/dist/transforms/nestjs-migrate.d.ts +4 -1
- package/dist/transforms/nestjs-migrate.d.ts.map +1 -1
- package/dist/transforms/nestjs-migrate.js +625 -65
- package/dist/typegen-contract.d.ts +10 -0
- package/dist/typegen-contract.d.ts.map +1 -0
- package/dist/typegen-contract.js +9 -0
- package/dist/usage.d.ts +6 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +47 -0
- package/package.json +19 -10
|
@@ -51,7 +51,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
51
51
|
id: 'application-bun-bun-http',
|
|
52
52
|
platformPromptLabel: 'Bun native HTTP',
|
|
53
53
|
promptLabel: 'Application (HTTP starter)',
|
|
54
|
-
schema: createSchema('application', 'bun', 'bun', 'http')
|
|
54
|
+
schema: createSchema('application', 'bun', 'bun', 'http'),
|
|
55
|
+
starter: 'standard'
|
|
55
56
|
}, {
|
|
56
57
|
dependencies: {
|
|
57
58
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/validation', '@fluojs/di', '@fluojs/http', '@fluojs/platform-deno', '@fluojs/runtime'],
|
|
@@ -67,7 +68,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
67
68
|
id: 'application-deno-deno-http',
|
|
68
69
|
platformPromptLabel: 'Deno native HTTP',
|
|
69
70
|
promptLabel: 'Application (HTTP starter)',
|
|
70
|
-
schema: createSchema('application', 'deno', 'deno', 'http')
|
|
71
|
+
schema: createSchema('application', 'deno', 'deno', 'http'),
|
|
72
|
+
starter: 'standard'
|
|
71
73
|
}, {
|
|
72
74
|
dependencies: {
|
|
73
75
|
dependencies: ['@fluojs/core', '@fluojs/validation', '@fluojs/di', '@fluojs/http', '@fluojs/platform-cloudflare-workers', '@fluojs/runtime'],
|
|
@@ -83,7 +85,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
83
85
|
id: 'application-cloudflare-workers-cloudflare-workers-http',
|
|
84
86
|
platformPromptLabel: 'Cloudflare Workers',
|
|
85
87
|
promptLabel: 'Application (HTTP starter)',
|
|
86
|
-
schema: createSchema('application', 'cloudflare-workers', 'cloudflare-workers', 'http')
|
|
88
|
+
schema: createSchema('application', 'cloudflare-workers', 'cloudflare-workers', 'http'),
|
|
89
|
+
starter: 'standard'
|
|
87
90
|
}, {
|
|
88
91
|
dependencies: {
|
|
89
92
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/validation', '@fluojs/di', '@fluojs/http', '@fluojs/platform-fastify', '@fluojs/runtime'],
|
|
@@ -99,7 +102,25 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
99
102
|
id: 'application-node-fastify-http',
|
|
100
103
|
platformPromptLabel: 'Fastify',
|
|
101
104
|
promptLabel: 'Application (HTTP starter)',
|
|
102
|
-
schema: createSchema('application', 'node', 'fastify', 'http')
|
|
105
|
+
schema: createSchema('application', 'node', 'fastify', 'http'),
|
|
106
|
+
starter: 'standard'
|
|
107
|
+
}, {
|
|
108
|
+
dependencies: {
|
|
109
|
+
dependencies: ['@fluojs/core', '@fluojs/http', '@fluojs/platform-fastify', '@fluojs/react', '@fluojs/runtime', '@fluojs/validation', 'react', 'react-dom'],
|
|
110
|
+
devDependencies: ['@fluojs/cli', '@fluojs/testing', '@fluojs/vite', '@playwright/test', '@types/react', '@types/react-dom', 'happy-dom']
|
|
111
|
+
},
|
|
112
|
+
emitter: {
|
|
113
|
+
platform: 'fastify',
|
|
114
|
+
preset: 'standard',
|
|
115
|
+
runtime: 'node',
|
|
116
|
+
transport: 'http',
|
|
117
|
+
type: 'react-vite-ssr'
|
|
118
|
+
},
|
|
119
|
+
id: 'application-node-fastify-react-vite-ssr',
|
|
120
|
+
platformPromptLabel: 'Fastify',
|
|
121
|
+
promptLabel: 'React SSR + Vite',
|
|
122
|
+
schema: createSchema('application', 'node', 'fastify', 'http'),
|
|
123
|
+
starter: 'react-vite-ssr'
|
|
103
124
|
}, {
|
|
104
125
|
dependencies: {
|
|
105
126
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/validation', '@fluojs/di', '@fluojs/http', '@fluojs/platform-express', '@fluojs/runtime'],
|
|
@@ -115,7 +136,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
115
136
|
id: 'application-node-express-http',
|
|
116
137
|
platformPromptLabel: 'Express',
|
|
117
138
|
promptLabel: 'Application (HTTP starter)',
|
|
118
|
-
schema: createSchema('application', 'node', 'express', 'http')
|
|
139
|
+
schema: createSchema('application', 'node', 'express', 'http'),
|
|
140
|
+
starter: 'standard'
|
|
119
141
|
}, {
|
|
120
142
|
dependencies: {
|
|
121
143
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/validation', '@fluojs/di', '@fluojs/http', '@fluojs/platform-nodejs', '@fluojs/runtime'],
|
|
@@ -131,7 +153,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
131
153
|
id: 'application-node-nodejs-http',
|
|
132
154
|
platformPromptLabel: 'Raw Node.js HTTP',
|
|
133
155
|
promptLabel: 'Application (HTTP starter)',
|
|
134
|
-
schema: createSchema('application', 'node', 'nodejs', 'http')
|
|
156
|
+
schema: createSchema('application', 'node', 'nodejs', 'http'),
|
|
157
|
+
starter: 'standard'
|
|
135
158
|
}, {
|
|
136
159
|
dependencies: {
|
|
137
160
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/di', '@fluojs/microservices', '@fluojs/runtime'],
|
|
@@ -146,7 +169,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
146
169
|
},
|
|
147
170
|
id: 'microservice-node-none-tcp',
|
|
148
171
|
promptLabel: 'Microservice (transport-first starter)',
|
|
149
|
-
schema: createSchema('microservice', 'node', 'none', 'tcp')
|
|
172
|
+
schema: createSchema('microservice', 'node', 'none', 'tcp'),
|
|
173
|
+
starter: 'standard'
|
|
150
174
|
}, {
|
|
151
175
|
dependencies: {
|
|
152
176
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/di', '@fluojs/microservices', '@fluojs/runtime', 'ioredis'],
|
|
@@ -161,7 +185,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
161
185
|
},
|
|
162
186
|
id: 'microservice-node-none-redis-streams',
|
|
163
187
|
promptLabel: 'Microservice (Redis Streams starter)',
|
|
164
|
-
schema: createSchema('microservice', 'node', 'none', 'redis-streams')
|
|
188
|
+
schema: createSchema('microservice', 'node', 'none', 'redis-streams'),
|
|
189
|
+
starter: 'standard'
|
|
165
190
|
}, {
|
|
166
191
|
dependencies: {
|
|
167
192
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/di', '@fluojs/microservices', '@fluojs/runtime', 'mqtt'],
|
|
@@ -176,7 +201,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
176
201
|
},
|
|
177
202
|
id: 'microservice-node-none-mqtt',
|
|
178
203
|
promptLabel: 'Microservice (MQTT starter)',
|
|
179
|
-
schema: createSchema('microservice', 'node', 'none', 'mqtt')
|
|
204
|
+
schema: createSchema('microservice', 'node', 'none', 'mqtt'),
|
|
205
|
+
starter: 'standard'
|
|
180
206
|
}, {
|
|
181
207
|
dependencies: {
|
|
182
208
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/di', '@fluojs/microservices', '@fluojs/runtime', '@grpc/grpc-js', '@grpc/proto-loader'],
|
|
@@ -191,7 +217,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
191
217
|
},
|
|
192
218
|
id: 'microservice-node-none-grpc',
|
|
193
219
|
promptLabel: 'Microservice (gRPC starter)',
|
|
194
|
-
schema: createSchema('microservice', 'node', 'none', 'grpc')
|
|
220
|
+
schema: createSchema('microservice', 'node', 'none', 'grpc'),
|
|
221
|
+
starter: 'standard'
|
|
195
222
|
}, {
|
|
196
223
|
dependencies: {
|
|
197
224
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/di', '@fluojs/microservices', '@fluojs/runtime', 'nats'],
|
|
@@ -206,7 +233,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
206
233
|
},
|
|
207
234
|
id: 'microservice-node-none-nats',
|
|
208
235
|
promptLabel: 'Microservice (NATS starter)',
|
|
209
|
-
schema: createSchema('microservice', 'node', 'none', 'nats')
|
|
236
|
+
schema: createSchema('microservice', 'node', 'none', 'nats'),
|
|
237
|
+
starter: 'standard'
|
|
210
238
|
}, {
|
|
211
239
|
dependencies: {
|
|
212
240
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/di', '@fluojs/microservices', '@fluojs/runtime', 'kafkajs'],
|
|
@@ -221,7 +249,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
221
249
|
},
|
|
222
250
|
id: 'microservice-node-none-kafka',
|
|
223
251
|
promptLabel: 'Microservice (Kafka starter)',
|
|
224
|
-
schema: createSchema('microservice', 'node', 'none', 'kafka')
|
|
252
|
+
schema: createSchema('microservice', 'node', 'none', 'kafka'),
|
|
253
|
+
starter: 'standard'
|
|
225
254
|
}, {
|
|
226
255
|
dependencies: {
|
|
227
256
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/di', '@fluojs/microservices', '@fluojs/runtime', 'amqplib'],
|
|
@@ -236,7 +265,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
236
265
|
},
|
|
237
266
|
id: 'microservice-node-none-rabbitmq',
|
|
238
267
|
promptLabel: 'Microservice (RabbitMQ starter)',
|
|
239
|
-
schema: createSchema('microservice', 'node', 'none', 'rabbitmq')
|
|
268
|
+
schema: createSchema('microservice', 'node', 'none', 'rabbitmq'),
|
|
269
|
+
starter: 'standard'
|
|
240
270
|
}, {
|
|
241
271
|
dependencies: {
|
|
242
272
|
dependencies: ['@fluojs/config', '@fluojs/core', '@fluojs/validation', '@fluojs/di', '@fluojs/http', '@fluojs/microservices', '@fluojs/platform-fastify', '@fluojs/runtime'],
|
|
@@ -251,7 +281,8 @@ export const STARTER_PROFILE_REGISTRY = [{
|
|
|
251
281
|
},
|
|
252
282
|
id: 'mixed-node-fastify-tcp',
|
|
253
283
|
promptLabel: 'Mixed (HTTP API + microservice starter)',
|
|
254
|
-
schema: createSchema('mixed', 'node', 'fastify', 'tcp')
|
|
284
|
+
schema: createSchema('mixed', 'node', 'fastify', 'tcp'),
|
|
285
|
+
starter: 'standard'
|
|
255
286
|
}];
|
|
256
287
|
|
|
257
288
|
/** Supported scaffold shapes accepted by the current starter matrix. */
|
|
@@ -266,6 +297,8 @@ export const SUPPORTED_BOOTSTRAP_TRANSPORTS = ['http', ...SUPPORTED_MICROSERVICE
|
|
|
266
297
|
export const SUPPORTED_BOOTSTRAP_TOOLING_PRESETS = ['standard'];
|
|
267
298
|
/** Supported topology modes accepted by the current starter matrix. */
|
|
268
299
|
export const SUPPORTED_BOOTSTRAP_TOPOLOGY_MODES = ['single-package'];
|
|
300
|
+
/** Named starter variants accepted by `fluo new`. */
|
|
301
|
+
export const SUPPORTED_BOOTSTRAP_STARTERS = ['standard', 'react-vite-ssr'];
|
|
269
302
|
|
|
270
303
|
/** Default starter profile used for shape-less `fluo new` invocations. */
|
|
271
304
|
export const DEFAULT_BOOTSTRAP_PROFILE = STARTER_PROFILE_REGISTRY.find(profile => profile.id === 'application-node-fastify-http');
|
|
@@ -279,7 +312,7 @@ export const DEFAULT_BOOTSTRAP_PROFILE = STARTER_PROFILE_REGISTRY.find(profile =
|
|
|
279
312
|
*/
|
|
280
313
|
export function getStarterProfileForShape(shape, runtime) {
|
|
281
314
|
if (runtime !== undefined) {
|
|
282
|
-
return STARTER_PROFILE_REGISTRY.find(profile => profile.schema.shape === shape && profile.schema.runtime === runtime) ?? DEFAULT_BOOTSTRAP_PROFILE;
|
|
315
|
+
return STARTER_PROFILE_REGISTRY.find(profile => profile.starter === 'standard' && profile.schema.shape === shape && profile.schema.runtime === runtime) ?? DEFAULT_BOOTSTRAP_PROFILE;
|
|
283
316
|
}
|
|
284
317
|
if (shape === 'application') {
|
|
285
318
|
return DEFAULT_BOOTSTRAP_PROFILE;
|
|
@@ -300,7 +333,7 @@ export function getStarterProfileForShape(shape, runtime) {
|
|
|
300
333
|
* @returns Application starter profiles for the selected runtime, or all application starters when omitted.
|
|
301
334
|
*/
|
|
302
335
|
export function getApplicationStarterProfiles(runtime) {
|
|
303
|
-
return STARTER_PROFILE_REGISTRY.filter(profile => profile.schema.shape === 'application' && (runtime === undefined || profile.schema.runtime === runtime));
|
|
336
|
+
return STARTER_PROFILE_REGISTRY.filter(profile => profile.starter === 'standard' && profile.schema.shape === 'application' && (runtime === undefined || profile.schema.runtime === runtime));
|
|
304
337
|
}
|
|
305
338
|
|
|
306
339
|
/**
|
|
@@ -327,10 +360,11 @@ export function getDefaultBootstrapSchema() {
|
|
|
327
360
|
* Finds the starter profile that exactly matches a resolved bootstrap schema.
|
|
328
361
|
*
|
|
329
362
|
* @param schema Fully resolved bootstrap schema.
|
|
363
|
+
* @param starter Named starter variant selected by the caller.
|
|
330
364
|
* @returns The matching starter profile, or `undefined` when the schema is validation-only.
|
|
331
365
|
*/
|
|
332
|
-
export function getStarterProfileFromSchema(schema) {
|
|
333
|
-
return STARTER_PROFILE_REGISTRY.find(profile => profile.schema.shape === schema.shape && profile.schema.runtime === schema.runtime && profile.schema.platform === schema.platform && profile.schema.transport === schema.transport && profile.schema.tooling === schema.tooling && profile.schema.topology.deferred === schema.topology.deferred && profile.schema.topology.mode === schema.topology.mode);
|
|
366
|
+
export function getStarterProfileFromSchema(schema, starter = 'standard') {
|
|
367
|
+
return STARTER_PROFILE_REGISTRY.find(profile => profile.starter === starter && profile.schema.shape === schema.shape && profile.schema.runtime === schema.runtime && profile.schema.platform === schema.platform && profile.schema.transport === schema.transport && profile.schema.tooling === schema.tooling && profile.schema.topology.deferred === schema.topology.deferred && profile.schema.topology.mode === schema.topology.mode);
|
|
334
368
|
}
|
|
335
369
|
|
|
336
370
|
/**
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# <%= projectName %>
|
|
2
|
+
|
|
3
|
+
HTTP-first React SSR starter generated by `fluo new --starter react-vite-ssr`.
|
|
4
|
+
|
|
5
|
+
Requires Node.js `>=24.0.0 <27`. Use Node 24 LTS for local development, CI, and container images. The server build targets `node24` and uses `@types/node@^24.0.0`.
|
|
6
|
+
|
|
7
|
+
## First page
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
<%= installCommand %>
|
|
11
|
+
<%= devCommand %>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Open `http://127.0.0.1:3000/products/sku-42?preview=true`, then edit
|
|
15
|
+
`src/page.tsx`. The page component contains only page UI and hydrated interaction; the generated
|
|
16
|
+
application wiring keeps the server/client render tree aligned for you.
|
|
17
|
+
|
|
18
|
+
The decorated route remains explicit in `src/app.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
@Router('/products')
|
|
22
|
+
class ProductPageRouter {
|
|
23
|
+
@Path('/:sku')
|
|
24
|
+
@RequestDto(ProductPageRequest)
|
|
25
|
+
show(_input: ProductPageRequest): ReactElement {
|
|
26
|
+
return createElement(ProductPage);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This handler still runs through `@fluojs/http` matching, DTO binding, validation, middleware,
|
|
32
|
+
guards, interceptors, request scopes, and not-found behavior before the application renderer sees
|
|
33
|
+
the returned `ReactElement`. Decorator-bearing application declarations stay in `.ts` files so the
|
|
34
|
+
Vite transform handles them; JSX rendering stays in `.tsx` modules such as `src/page.tsx`.
|
|
35
|
+
|
|
36
|
+
## Production verification
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
<%= typecheckCommand %>
|
|
40
|
+
<%= testCommand %>
|
|
41
|
+
<%= buildCommand %>
|
|
42
|
+
<%= startCommand %>
|
|
43
|
+
<%= testBrowserCommand %>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`<%= testBrowserCommand %>` starts the built Fastify server and verifies the first HTTP response, emitted
|
|
47
|
+
assets, hydration, interaction, and full-document navigation without console warnings or errors.
|
|
48
|
+
Install the Playwright Chrome browser first when your environment does not already provide it.
|
|
49
|
+
|
|
50
|
+
## Application-owned wiring
|
|
51
|
+
|
|
52
|
+
Read these files only when you need to change the default composition:
|
|
53
|
+
|
|
54
|
+
- `src/app.ts` owns explicit HTTP routes and registers the application renderer.
|
|
55
|
+
- `src/entry-server.tsx` is the application-owned `ReactPageRenderer`. It turns the already-loaded
|
|
56
|
+
Vite manifest into hydration assets and returns `ReactServerEntry` through
|
|
57
|
+
`createReactServerEntry(...)`. Replace this renderer when the application needs another document
|
|
58
|
+
or explicit asset policy.
|
|
59
|
+
- `src/react-app.tsx` is the shared server/client document. It owns
|
|
60
|
+
`ReactClientRouterProvider`, the request route snapshot, and stylesheet placement.
|
|
61
|
+
- `src/entry-client.tsx` hydrates that same document and reports a mismatch with the exact entry and
|
|
62
|
+
shared-composition files to compare.
|
|
63
|
+
- `src/main.ts` loads `dist/client/.vite/manifest.json` through `src/load-manifest.ts`, then boots the
|
|
64
|
+
normal Fastify HTTP application. Missing or malformed build output points back to `<%= buildCommand %>` and
|
|
65
|
+
`vite.client.config.ts`.
|
|
66
|
+
|
|
67
|
+
`@fluojs/react/vite` only parses the manifest value loaded by the application. It does not discover
|
|
68
|
+
files, run Vite, serve assets, or move route ownership out of `@fluojs/http`. The Fastify asset
|
|
69
|
+
controller is a minimal local starter boundary; production deployments should serve `dist/client`
|
|
70
|
+
through their normal static-file or CDN infrastructure.
|
|
71
|
+
|
|
72
|
+
`Link` remains a real anchor and `router.push(...)` performs full-document navigation through the
|
|
73
|
+
server-owned route. This starter intentionally excludes RSC, Server Functions, file routing, a
|
|
74
|
+
client route table, SPA document swapping, prefetch, and a data cache.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const BROWSER_TEST_PORT = 43_006;
|
|
4
|
+
const serverCommand = process.env.FLUO_REACT_STARTER_SERVER_COMMAND === 'dev'
|
|
5
|
+
? <%= devCommandJson %>
|
|
6
|
+
: <%= startCommandJson %>;
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
fullyParallel: false,
|
|
10
|
+
outputDir: 'node_modules/.cache/playwright-results',
|
|
11
|
+
testDir: './tests',
|
|
12
|
+
timeout: 30_000,
|
|
13
|
+
use: {
|
|
14
|
+
baseURL: `http://127.0.0.1:${BROWSER_TEST_PORT}`,
|
|
15
|
+
channel: 'chrome',
|
|
16
|
+
headless: true,
|
|
17
|
+
},
|
|
18
|
+
webServer: {
|
|
19
|
+
command: serverCommand,
|
|
20
|
+
env: { PORT: String(BROWSER_TEST_PORT) },
|
|
21
|
+
reuseExistingServer: false,
|
|
22
|
+
timeout: 30_000,
|
|
23
|
+
url: `http://127.0.0.1:${BROWSER_TEST_PORT}/assets/entry-client.js`,
|
|
24
|
+
},
|
|
25
|
+
workers: 1,
|
|
26
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createTestApp } from '@fluojs/testing';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { createAppModule } from './app';
|
|
5
|
+
import { ReactStarterEntryError, createReactPageRenderer } from './entry-server';
|
|
6
|
+
|
|
7
|
+
const VITE_MANIFEST = {
|
|
8
|
+
'src/entry-client.tsx': {
|
|
9
|
+
css: ['styles.css'],
|
|
10
|
+
file: 'entry-client.js',
|
|
11
|
+
isEntry: true,
|
|
12
|
+
src: 'src/entry-client.tsx',
|
|
13
|
+
},
|
|
14
|
+
'src/entry-server.tsx': {
|
|
15
|
+
file: 'entry-server.js',
|
|
16
|
+
isEntry: true,
|
|
17
|
+
src: 'src/entry-server.tsx',
|
|
18
|
+
},
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
function readHtml(body: unknown): string {
|
|
22
|
+
if (body instanceof Uint8Array) {
|
|
23
|
+
return new TextDecoder().decode(body);
|
|
24
|
+
}
|
|
25
|
+
return typeof body === 'string' ? body : JSON.stringify(body);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('React Vite SSR starter', () => {
|
|
29
|
+
it('streams an HTTP-owned page with manifest-derived hydration assets', async () => {
|
|
30
|
+
const AppModule = createAppModule({
|
|
31
|
+
clientDirectory: new URL('../dist/client/', import.meta.url),
|
|
32
|
+
renderPage: createReactPageRenderer(VITE_MANIFEST),
|
|
33
|
+
});
|
|
34
|
+
const app = await createTestApp({ rootModule: AppModule });
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await app.request('GET', '/products/sku-42').query('preview', 'true').send();
|
|
38
|
+
const html = readHtml(response.body);
|
|
39
|
+
|
|
40
|
+
expect(response.status).toBe(200);
|
|
41
|
+
expect(response.headers['Content-Type']).toBe('text/html; charset=utf-8');
|
|
42
|
+
expect(html).toContain('Catalog item sku-42');
|
|
43
|
+
expect(html).toContain('Preview mode');
|
|
44
|
+
expect(html).toContain('src="/assets/entry-client.js"');
|
|
45
|
+
expect(html).toContain('href="/assets/styles.css"');
|
|
46
|
+
expect(html).toContain('href="/products/sku-84?preview=false"');
|
|
47
|
+
} finally {
|
|
48
|
+
await app.close();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('keeps route validation on the server DTO boundary', async () => {
|
|
53
|
+
const AppModule = createAppModule({
|
|
54
|
+
clientDirectory: new URL('../dist/client/', import.meta.url),
|
|
55
|
+
renderPage: createReactPageRenderer(VITE_MANIFEST),
|
|
56
|
+
});
|
|
57
|
+
const app = await createTestApp({ rootModule: AppModule });
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await app.request('GET', '/products/x').query('preview', 'maybe').send();
|
|
61
|
+
expect(response.status).toBe(400);
|
|
62
|
+
} finally {
|
|
63
|
+
await app.close();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('identifies the application entry files when the Vite manifest is incompatible', () => {
|
|
68
|
+
try {
|
|
69
|
+
createReactPageRenderer({});
|
|
70
|
+
throw new TypeError('Expected createReactPageRenderer(...) to reject an incompatible manifest.');
|
|
71
|
+
} catch (error) {
|
|
72
|
+
expect(error).toBeInstanceOf(ReactStarterEntryError);
|
|
73
|
+
if (error instanceof ReactStarterEntryError) {
|
|
74
|
+
expect(error.code).toBe('react-starter-entry-incompatible');
|
|
75
|
+
expect(error.message).toContain('src/entry-server.tsx');
|
|
76
|
+
expect(error.message).toContain('src/entry-client.tsx');
|
|
77
|
+
expect(error.message).toContain('vite.client.config.ts');
|
|
78
|
+
expect(error.message).toContain('generated build script');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import { Module } from '@fluojs/core';
|
|
4
|
+
import {
|
|
5
|
+
Controller,
|
|
6
|
+
FromPath,
|
|
7
|
+
FromQuery,
|
|
8
|
+
Get,
|
|
9
|
+
NotFoundException,
|
|
10
|
+
Optional,
|
|
11
|
+
RequestDto,
|
|
12
|
+
type RequestContext,
|
|
13
|
+
} from '@fluojs/http';
|
|
14
|
+
import {
|
|
15
|
+
Path,
|
|
16
|
+
ReactModule,
|
|
17
|
+
Router,
|
|
18
|
+
type ReactPageRenderer,
|
|
19
|
+
} from '@fluojs/react';
|
|
20
|
+
import { IsIn, IsString, MinLength } from '@fluojs/validation';
|
|
21
|
+
import { createElement, type ReactElement } from 'react';
|
|
22
|
+
|
|
23
|
+
import { ProductPage } from './page';
|
|
24
|
+
|
|
25
|
+
const ASSET_FILE_PATTERN = /^[a-zA-Z0-9._-]+\.(?:css|js)$/u;
|
|
26
|
+
|
|
27
|
+
export type ReactViteAppModuleOptions = {
|
|
28
|
+
readonly clientDirectory: URL;
|
|
29
|
+
readonly renderPage: ReactPageRenderer;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
class ProductPageRequest {
|
|
33
|
+
@MinLength(3)
|
|
34
|
+
@IsString()
|
|
35
|
+
@FromPath('sku')
|
|
36
|
+
sku = '';
|
|
37
|
+
|
|
38
|
+
@IsIn(['true', 'false'])
|
|
39
|
+
@Optional()
|
|
40
|
+
@FromQuery('preview')
|
|
41
|
+
preview?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class AssetRequest {
|
|
45
|
+
@FromPath('file')
|
|
46
|
+
file = '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createAppModule(options: ReactViteAppModuleOptions) {
|
|
50
|
+
@Router('/products')
|
|
51
|
+
class ProductPageRouter {
|
|
52
|
+
@Path('/:sku')
|
|
53
|
+
@RequestDto(ProductPageRequest)
|
|
54
|
+
show(_input: ProductPageRequest): ReactElement {
|
|
55
|
+
return createElement(ProductPage);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Controller('/assets')
|
|
60
|
+
class ViteAssetController {
|
|
61
|
+
@Get('/:file')
|
|
62
|
+
@RequestDto(AssetRequest)
|
|
63
|
+
async serve(input: AssetRequest, context: RequestContext): Promise<Uint8Array> {
|
|
64
|
+
if (!ASSET_FILE_PATTERN.test(input.file)) {
|
|
65
|
+
throw new NotFoundException('Vite asset not found.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const body = await readFile(new URL(input.file, options.clientDirectory));
|
|
70
|
+
context.response.setHeader(
|
|
71
|
+
'Content-Type',
|
|
72
|
+
input.file.endsWith('.css') ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8',
|
|
73
|
+
);
|
|
74
|
+
return body;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
77
|
+
throw new NotFoundException('Vite asset not found.', { cause: error });
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Module({
|
|
85
|
+
controllers: [ViteAssetController],
|
|
86
|
+
imports: [ReactModule.forRoot({ controllers: [ProductPageRouter], renderPage: options.renderPage })],
|
|
87
|
+
})
|
|
88
|
+
class AppModule {}
|
|
89
|
+
|
|
90
|
+
return AppModule;
|
|
91
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
2
|
+
|
|
3
|
+
import { ProductPage } from './page';
|
|
4
|
+
import {
|
|
5
|
+
ReactAppDocument,
|
|
6
|
+
REACT_IDENTIFIER_PREFIX,
|
|
7
|
+
reportReactHydrationMismatch,
|
|
8
|
+
} from './react-app';
|
|
9
|
+
import './styles.css';
|
|
10
|
+
|
|
11
|
+
const stylesheets = [...document.querySelectorAll<HTMLLinkElement>('link[data-vite-style]')]
|
|
12
|
+
.map((link) => link.getAttribute('href'))
|
|
13
|
+
.filter((href): href is string => href !== null);
|
|
14
|
+
|
|
15
|
+
hydrateRoot(
|
|
16
|
+
document,
|
|
17
|
+
<ReactAppDocument
|
|
18
|
+
page={<ProductPage />}
|
|
19
|
+
route={{
|
|
20
|
+
params: { sku: document.documentElement.dataset.routeSku ?? '' },
|
|
21
|
+
url: `${window.location.pathname}${window.location.search}`,
|
|
22
|
+
}}
|
|
23
|
+
stylesheets={stylesheets}
|
|
24
|
+
/>,
|
|
25
|
+
{
|
|
26
|
+
identifierPrefix: REACT_IDENTIFIER_PREFIX,
|
|
27
|
+
onRecoverableError: reportReactHydrationMismatch,
|
|
28
|
+
},
|
|
29
|
+
);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createReactServerEntry,
|
|
3
|
+
type ReactPageRenderer,
|
|
4
|
+
} from '@fluojs/react';
|
|
5
|
+
import { createReactViteAssetManifest } from '@fluojs/react/vite';
|
|
6
|
+
|
|
7
|
+
import { ReactAppDocument, REACT_IDENTIFIER_PREFIX } from './react-app';
|
|
8
|
+
|
|
9
|
+
export class ReactStarterEntryError extends Error {
|
|
10
|
+
readonly code = 'react-starter-entry-incompatible' as const;
|
|
11
|
+
|
|
12
|
+
constructor(readonly diagnostics: readonly string[]) {
|
|
13
|
+
super(
|
|
14
|
+
[
|
|
15
|
+
'The React starter could not match its server and client entries.',
|
|
16
|
+
'Check src/entry-server.tsx, src/entry-client.tsx, and vite.client.config.ts, then run the generated build script again.',
|
|
17
|
+
...diagnostics,
|
|
18
|
+
].join('\n'),
|
|
19
|
+
);
|
|
20
|
+
this.name = 'ReactStarterEntryError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createReactPageRenderer(manifest: unknown): ReactPageRenderer {
|
|
25
|
+
const result = createReactViteAssetManifest({
|
|
26
|
+
base: '/assets/',
|
|
27
|
+
entries: {
|
|
28
|
+
client: 'src/entry-client.tsx',
|
|
29
|
+
server: 'src/entry-server.tsx',
|
|
30
|
+
},
|
|
31
|
+
identifierPrefix: REACT_IDENTIFIER_PREFIX,
|
|
32
|
+
manifest,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!result.ok) {
|
|
36
|
+
throw new ReactStarterEntryError(
|
|
37
|
+
result.diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const assets = result.manifest;
|
|
42
|
+
const renderPage: ReactPageRenderer = (page, context) => createReactServerEntry(
|
|
43
|
+
<ReactAppDocument
|
|
44
|
+
page={page}
|
|
45
|
+
route={{ params: context.request.params, url: context.request.url }}
|
|
46
|
+
stylesheets={assets.css}
|
|
47
|
+
/>,
|
|
48
|
+
assets.hydrationOptions,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return renderPage;
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { ReactStarterManifestError, loadReactViteManifest } from './load-manifest';
|
|
9
|
+
|
|
10
|
+
const temporaryDirectories: string[] = [];
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await Promise.all(
|
|
14
|
+
temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })),
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('React Vite manifest loading', () => {
|
|
19
|
+
it('identifies the build command when the client manifest is missing', async () => {
|
|
20
|
+
const directory = await mkdtemp(join(tmpdir(), 'fluo-react-manifest-missing-'));
|
|
21
|
+
temporaryDirectories.push(directory);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await loadReactViteManifest(pathToFileURL(join(directory, 'manifest.json')));
|
|
25
|
+
throw new TypeError('Expected loadReactViteManifest(...) to reject a missing manifest.');
|
|
26
|
+
} catch (error) {
|
|
27
|
+
expect(error).toBeInstanceOf(ReactStarterManifestError);
|
|
28
|
+
if (error instanceof ReactStarterManifestError) {
|
|
29
|
+
expect(error.code).toBe('react-starter-manifest-missing');
|
|
30
|
+
expect(error.message).toContain('generated build script');
|
|
31
|
+
expect(error.message).toContain('generated start script');
|
|
32
|
+
expect(error.message).toContain('vite.client.config.ts');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('identifies generated build output when the client manifest is malformed', async () => {
|
|
38
|
+
const directory = await mkdtemp(join(tmpdir(), 'fluo-react-manifest-malformed-'));
|
|
39
|
+
temporaryDirectories.push(directory);
|
|
40
|
+
const manifestPath = join(directory, 'manifest.json');
|
|
41
|
+
await writeFile(manifestPath, '{');
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await loadReactViteManifest(pathToFileURL(manifestPath));
|
|
45
|
+
throw new TypeError('Expected loadReactViteManifest(...) to reject malformed JSON.');
|
|
46
|
+
} catch (error) {
|
|
47
|
+
expect(error).toBeInstanceOf(ReactStarterManifestError);
|
|
48
|
+
if (error instanceof ReactStarterManifestError) {
|
|
49
|
+
expect(error.code).toBe('react-starter-manifest-malformed');
|
|
50
|
+
expect(error.message).toContain('dist/client/.vite/manifest.json');
|
|
51
|
+
expect(error.message).toContain('generated build script');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export type ReactStarterManifestErrorCode =
|
|
4
|
+
| 'react-starter-manifest-malformed'
|
|
5
|
+
| 'react-starter-manifest-missing'
|
|
6
|
+
| 'react-starter-manifest-read-failed';
|
|
7
|
+
|
|
8
|
+
export class ReactStarterManifestError extends Error {
|
|
9
|
+
constructor(
|
|
10
|
+
readonly code: ReactStarterManifestErrorCode,
|
|
11
|
+
message: string,
|
|
12
|
+
options: ErrorOptions,
|
|
13
|
+
) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.name = 'ReactStarterManifestError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function loadReactViteManifest(manifestUrl: URL): Promise<unknown> {
|
|
20
|
+
let source: string;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
source = await readFile(manifestUrl, 'utf8');
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
26
|
+
throw new ReactStarterManifestError(
|
|
27
|
+
'react-starter-manifest-missing',
|
|
28
|
+
'The Vite client manifest is missing. Run the generated build script before the generated start script; if it remains missing, check build.manifest and build.outDir in vite.client.config.ts.',
|
|
29
|
+
{ cause: error },
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new ReactStarterManifestError(
|
|
34
|
+
'react-starter-manifest-read-failed',
|
|
35
|
+
'The Vite client manifest could not be read. Check the generated dist/client directory and file permissions, then run the generated build script again.',
|
|
36
|
+
{ cause: error },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const manifest: unknown = JSON.parse(source);
|
|
42
|
+
return manifest;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof SyntaxError) {
|
|
45
|
+
throw new ReactStarterManifestError(
|
|
46
|
+
'react-starter-manifest-malformed',
|
|
47
|
+
'The Vite client manifest is not valid JSON. Remove dist, run the generated build script again, and do not edit dist/client/.vite/manifest.json by hand.',
|
|
48
|
+
{ cause: error },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|