@chr33s/solarflare 0.0.2
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 +52 -0
- package/readme.md +183 -0
- package/src/ast.ts +316 -0
- package/src/build.bundle-client.ts +404 -0
- package/src/build.bundle-server.ts +131 -0
- package/src/build.bundle.ts +48 -0
- package/src/build.emit-manifests.ts +25 -0
- package/src/build.hmr-entry.ts +88 -0
- package/src/build.scan.ts +182 -0
- package/src/build.ts +227 -0
- package/src/build.validate.ts +63 -0
- package/src/client.hmr.ts +78 -0
- package/src/client.styles.ts +68 -0
- package/src/client.ts +190 -0
- package/src/codemod.ts +688 -0
- package/src/console-forward.ts +254 -0
- package/src/critical-css.ts +103 -0
- package/src/devtools-json.ts +52 -0
- package/src/diff-dom-streaming.ts +406 -0
- package/src/early-flush.ts +125 -0
- package/src/early-hints.ts +83 -0
- package/src/fetch.ts +44 -0
- package/src/fs.ts +11 -0
- package/src/head.ts +876 -0
- package/src/hmr.ts +647 -0
- package/src/hydration.ts +238 -0
- package/src/manifest.runtime.ts +25 -0
- package/src/manifest.ts +23 -0
- package/src/paths.ts +96 -0
- package/src/render-priority.ts +69 -0
- package/src/route-cache.ts +163 -0
- package/src/router-deferred.ts +85 -0
- package/src/router-stream.ts +65 -0
- package/src/router.ts +535 -0
- package/src/runtime.ts +32 -0
- package/src/serialize.ts +38 -0
- package/src/server.hmr.ts +67 -0
- package/src/server.styles.ts +42 -0
- package/src/server.ts +480 -0
- package/src/solarflare.d.ts +101 -0
- package/src/speculation-rules.ts +171 -0
- package/src/store.ts +78 -0
- package/src/stream-assets.ts +135 -0
- package/src/stylesheets.ts +222 -0
- package/src/worker.config.ts +243 -0
- package/src/worker.ts +542 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, unlink, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { rolldown } from "rolldown";
|
|
6
|
+
import { replacePlugin } from "rolldown/plugins";
|
|
7
|
+
import { transform } from "lightningcss";
|
|
8
|
+
import { createProgram, getDefaultExportInfo } from "./ast.ts";
|
|
9
|
+
import { exists } from "./fs.ts";
|
|
10
|
+
import { assetUrlPrefixPlugin, type BuildArgs, moduleTypes } from "./build.bundle.ts";
|
|
11
|
+
import { parsePath } from "./paths.ts";
|
|
12
|
+
import { generateClientScript } from "./console-forward.ts";
|
|
13
|
+
import { createScanner } from "./build.scan.ts";
|
|
14
|
+
import { generateChunkedClientEntry, type ComponentMeta } from "./build.hmr-entry.ts";
|
|
15
|
+
import type { RoutesManifest, ChunkManifest } from "./manifest.ts";
|
|
16
|
+
|
|
17
|
+
export interface BuildClientOptions {
|
|
18
|
+
args: BuildArgs;
|
|
19
|
+
rootDir: string;
|
|
20
|
+
appDir: string;
|
|
21
|
+
distDir: string;
|
|
22
|
+
distClient: string;
|
|
23
|
+
publicDir: string;
|
|
24
|
+
chunksPath: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function remove(path: string) {
|
|
28
|
+
try {
|
|
29
|
+
await unlink(path);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function hash(content: string) {
|
|
38
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeAssetPath(path: string) {
|
|
42
|
+
return path.replace(/\//g, ".");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getChunkName(file: string, contentHash?: string) {
|
|
46
|
+
const base = file
|
|
47
|
+
.replace(/\.client\.tsx?$/, "")
|
|
48
|
+
.replace(/\//g, ".")
|
|
49
|
+
.replace(/\$/g, "")
|
|
50
|
+
.replace(/^index$/, "index");
|
|
51
|
+
|
|
52
|
+
return contentHash ? `${base}-${contentHash}.js` : `${base}.js`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractPropsFromProgram(program: ts.Program, filePath: string) {
|
|
56
|
+
const checker = program.getTypeChecker();
|
|
57
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
58
|
+
if (!sourceFile) return [];
|
|
59
|
+
|
|
60
|
+
const exportInfo = getDefaultExportInfo(checker, sourceFile);
|
|
61
|
+
if (!exportInfo || exportInfo.signatures.length === 0) return [];
|
|
62
|
+
|
|
63
|
+
const firstParam = exportInfo.signatures[0].getParameters()[0];
|
|
64
|
+
if (!firstParam) return [];
|
|
65
|
+
|
|
66
|
+
const paramType = checker.getTypeOfSymbolAtLocation(firstParam, sourceFile);
|
|
67
|
+
const properties = paramType.getProperties();
|
|
68
|
+
|
|
69
|
+
return properties.map((p) => p.getName());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getComponentMeta(program: ts.Program, appDir: string, file: string) {
|
|
73
|
+
const filePath = join(appDir, file);
|
|
74
|
+
const props = extractPropsFromProgram(program, filePath);
|
|
75
|
+
const parsed = parsePath(file);
|
|
76
|
+
|
|
77
|
+
const content = await readFile(filePath, "utf-8");
|
|
78
|
+
const contentHash = hash(content);
|
|
79
|
+
const chunk = getChunkName(file, contentHash);
|
|
80
|
+
|
|
81
|
+
return { file, tag: parsed.tag, props, parsed, chunk, hash: contentHash };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function buildClient(options: BuildClientOptions) {
|
|
85
|
+
const { args, rootDir, appDir, distDir, distClient, publicDir, chunksPath } = options;
|
|
86
|
+
const distClientAssets = join(distClient, "assets");
|
|
87
|
+
const scanner = createScanner({ rootDir, appDir });
|
|
88
|
+
|
|
89
|
+
const cssAssetCache = new Map<string, string>();
|
|
90
|
+
const cssOutputsByBase = new Map<string, string>();
|
|
91
|
+
|
|
92
|
+
async function emitCssAsset(cssSourcePath: string) {
|
|
93
|
+
if (!(await exists(cssSourcePath))) return null;
|
|
94
|
+
|
|
95
|
+
const cached = cssAssetCache.get(cssSourcePath);
|
|
96
|
+
if (cached) return cached;
|
|
97
|
+
|
|
98
|
+
const cssRelativePath = cssSourcePath.replace(appDir + "/", "");
|
|
99
|
+
const cssContent = transform({
|
|
100
|
+
code: Buffer.from(await readFile(cssSourcePath, "utf-8")),
|
|
101
|
+
filename: cssSourcePath,
|
|
102
|
+
minify: args.production,
|
|
103
|
+
}).code.toString();
|
|
104
|
+
|
|
105
|
+
const cssHash = hash(cssContent);
|
|
106
|
+
const cssBase = normalizeAssetPath(cssRelativePath.replace(/\.css$/, ""));
|
|
107
|
+
const cssOutputName = `${cssBase}-${cssHash}.css`;
|
|
108
|
+
|
|
109
|
+
cssOutputsByBase.set(cssBase, cssOutputName);
|
|
110
|
+
|
|
111
|
+
await mkdir(distClientAssets, { recursive: true });
|
|
112
|
+
const destPath = join(distClientAssets, cssOutputName);
|
|
113
|
+
await writeFile(destPath, cssContent);
|
|
114
|
+
|
|
115
|
+
const outputPath = `/assets/${cssOutputName}`;
|
|
116
|
+
cssAssetCache.set(cssSourcePath, outputPath);
|
|
117
|
+
return outputPath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function resolveCssOutputs(cssImports: string[], baseDir: string) {
|
|
121
|
+
if (cssImports.length === 0) return [];
|
|
122
|
+
|
|
123
|
+
const cssOutputPaths: string[] = [];
|
|
124
|
+
|
|
125
|
+
for (const cssImport of cssImports) {
|
|
126
|
+
const cssSourcePath = cssImport.startsWith("./")
|
|
127
|
+
? join(appDir, cssImport.replace("./", ""))
|
|
128
|
+
: join(appDir, baseDir, cssImport);
|
|
129
|
+
|
|
130
|
+
const outputPath = await emitCssAsset(cssSourcePath);
|
|
131
|
+
if (outputPath && !cssOutputPaths.includes(outputPath)) {
|
|
132
|
+
cssOutputPaths.push(outputPath);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return cssOutputPaths;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log("🔍 Scanning for client components...");
|
|
140
|
+
const clientFiles = await scanner.findClientComponents();
|
|
141
|
+
console.log(` Found ${clientFiles.length} client component(s)`);
|
|
142
|
+
|
|
143
|
+
const filePaths = clientFiles.map((f) => join(appDir, f));
|
|
144
|
+
const program = createProgram(filePaths);
|
|
145
|
+
|
|
146
|
+
const metas = await Promise.all(
|
|
147
|
+
clientFiles.map((file) => getComponentMeta(program, appDir, file)),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const layoutFiles = await scanner.findLayouts();
|
|
151
|
+
const layoutCssMap: Record<string, string[]> = {};
|
|
152
|
+
const componentCssMap: Record<string, string[]> = {};
|
|
153
|
+
|
|
154
|
+
for (const layoutFile of layoutFiles) {
|
|
155
|
+
const layoutPath = join(appDir, layoutFile);
|
|
156
|
+
const allCssImports = await scanner.extractAllCssImports(layoutPath);
|
|
157
|
+
|
|
158
|
+
if (allCssImports.length > 0) {
|
|
159
|
+
const layoutDir = layoutFile.split("/").slice(0, -1).join("/");
|
|
160
|
+
const cssOutputPaths = await resolveCssOutputs(allCssImports, layoutDir);
|
|
161
|
+
|
|
162
|
+
if (cssOutputPaths.length > 0) {
|
|
163
|
+
const layoutPattern = layoutDir ? `/${layoutDir}` : "/";
|
|
164
|
+
if (!layoutCssMap[layoutPattern]) {
|
|
165
|
+
layoutCssMap[layoutPattern] = [];
|
|
166
|
+
}
|
|
167
|
+
for (const path of cssOutputPaths) {
|
|
168
|
+
if (!layoutCssMap[layoutPattern].includes(path)) {
|
|
169
|
+
layoutCssMap[layoutPattern].push(path);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (await exists(publicDir)) {
|
|
177
|
+
const publicFiles = await scanner.scanFiles("**/*", publicDir);
|
|
178
|
+
for (const file of publicFiles) {
|
|
179
|
+
const src = join(publicDir, file);
|
|
180
|
+
const dest = join(distClient, file);
|
|
181
|
+
|
|
182
|
+
const destDir = dirname(dest);
|
|
183
|
+
await mkdir(destDir, { recursive: true });
|
|
184
|
+
|
|
185
|
+
const content = await readFile(src, "utf-8");
|
|
186
|
+
await writeFile(dest, content);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!args.production) {
|
|
191
|
+
await mkdir(distClientAssets, { recursive: true });
|
|
192
|
+
const consoleScript = generateClientScript();
|
|
193
|
+
const consoleScriptPath = join(distClientAssets, "console-forward.js");
|
|
194
|
+
await writeFile(consoleScriptPath, consoleScript);
|
|
195
|
+
console.log(" Generated console-forward.js (dev mode)");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const inlineRoutesManifest: RoutesManifest = {
|
|
199
|
+
routes: metas.map((meta) => ({
|
|
200
|
+
pattern: meta.parsed.pattern,
|
|
201
|
+
tag: meta.tag,
|
|
202
|
+
chunk: `/assets/${meta.chunk}`,
|
|
203
|
+
styles: undefined,
|
|
204
|
+
type: "client" as const,
|
|
205
|
+
params: meta.parsed.params,
|
|
206
|
+
})),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const entryPaths: string[] = [];
|
|
210
|
+
const entryToMeta: Record<string, ComponentMeta & { parsed: ReturnType<typeof parsePath> }> = {};
|
|
211
|
+
|
|
212
|
+
await mkdir(distDir, { recursive: true });
|
|
213
|
+
|
|
214
|
+
for (const meta of metas) {
|
|
215
|
+
const componentPath = join(appDir, meta.file);
|
|
216
|
+
const componentCssImports = await scanner.extractAllCssImports(componentPath);
|
|
217
|
+
|
|
218
|
+
const componentDir = meta.file.split("/").slice(0, -1).join("/");
|
|
219
|
+
const componentCssOutputPaths = await resolveCssOutputs(componentCssImports, componentDir);
|
|
220
|
+
|
|
221
|
+
if (componentCssOutputPaths.length > 0) {
|
|
222
|
+
const existing = componentCssMap[meta.parsed.pattern] ?? [];
|
|
223
|
+
componentCssMap[meta.parsed.pattern] = [
|
|
224
|
+
...existing,
|
|
225
|
+
...componentCssOutputPaths.filter((path) => !existing.includes(path)),
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const cssFiles: string[] = [];
|
|
230
|
+
for (const cssImport of componentCssImports) {
|
|
231
|
+
const cleanPath = cssImport.replace(/^\.\//, "");
|
|
232
|
+
cssFiles.push(`../src/${cleanPath}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const entryContent = generateChunkedClientEntry(meta, inlineRoutesManifest, cssFiles, args);
|
|
236
|
+
const entryPath = join(distDir, `.entry-${meta.chunk.replace(".js", "")}.generated.tsx`);
|
|
237
|
+
await writeFile(entryPath, entryContent);
|
|
238
|
+
entryPaths.push(entryPath);
|
|
239
|
+
entryToMeta[entryPath] = meta;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log("📦 Building client chunks...");
|
|
243
|
+
|
|
244
|
+
await mkdir(distClient, { recursive: true });
|
|
245
|
+
|
|
246
|
+
const input: Record<string, string> = {};
|
|
247
|
+
for (const entryPath of entryPaths) {
|
|
248
|
+
const meta = entryToMeta[entryPath];
|
|
249
|
+
input[meta.chunk.replace(/\.js$/, "")] = entryPath;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const packageImports = await scanner.getPackageImports();
|
|
253
|
+
|
|
254
|
+
const bundle = await rolldown({
|
|
255
|
+
input,
|
|
256
|
+
platform: "browser",
|
|
257
|
+
tsconfig: true,
|
|
258
|
+
moduleTypes,
|
|
259
|
+
plugins: [
|
|
260
|
+
replacePlugin({
|
|
261
|
+
"globalThis.__SF_DEV__": JSON.stringify(!args.production),
|
|
262
|
+
}),
|
|
263
|
+
{
|
|
264
|
+
name: "raw-css-loader",
|
|
265
|
+
resolveId(source: string, importer: string | undefined) {
|
|
266
|
+
if (source.endsWith("?raw") && source.includes(".css")) {
|
|
267
|
+
const realPath = source.replace(/\?raw$/, "");
|
|
268
|
+
if (importer) {
|
|
269
|
+
const importerDir = importer.split("/").slice(0, -1).join("/");
|
|
270
|
+
return {
|
|
271
|
+
id: join(importerDir, realPath) + "?raw",
|
|
272
|
+
external: false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return { id: realPath + "?raw", external: false };
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
},
|
|
279
|
+
async load(id: string) {
|
|
280
|
+
if (id.endsWith("?raw")) {
|
|
281
|
+
const realPath = id.replace(/\?raw$/, "");
|
|
282
|
+
try {
|
|
283
|
+
const content = await readFile(realPath, "utf-8");
|
|
284
|
+
return {
|
|
285
|
+
code: /* tsx */ `export default ${JSON.stringify(content)};`,
|
|
286
|
+
moduleType: "js",
|
|
287
|
+
};
|
|
288
|
+
} catch {
|
|
289
|
+
console.warn(`[raw-css-loader] Could not load: ${realPath}`);
|
|
290
|
+
return { code: `export default "";`, moduleType: "js" };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
assetUrlPrefixPlugin,
|
|
297
|
+
],
|
|
298
|
+
resolve: {
|
|
299
|
+
alias: packageImports,
|
|
300
|
+
},
|
|
301
|
+
transform: {
|
|
302
|
+
target: "es2020",
|
|
303
|
+
jsx: {
|
|
304
|
+
runtime: "automatic",
|
|
305
|
+
development: !args.production,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await bundle.write({
|
|
311
|
+
dir: distClientAssets,
|
|
312
|
+
format: "esm",
|
|
313
|
+
entryFileNames: "[name].js",
|
|
314
|
+
minify: args.production,
|
|
315
|
+
chunkFileNames: "[name]-[hash].js",
|
|
316
|
+
assetFileNames: "[name]-[hash][extname]",
|
|
317
|
+
codeSplitting: {
|
|
318
|
+
minSize: 20000,
|
|
319
|
+
groups: [
|
|
320
|
+
{
|
|
321
|
+
name: "vendor",
|
|
322
|
+
test: /node_modules/,
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
...(args.sourcemap && { sourcemap: true }),
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (cssOutputsByBase.size > 0) {
|
|
330
|
+
const emittedCss = new Set(cssOutputsByBase.values());
|
|
331
|
+
const cssFiles = await scanner.scanFiles("**/*.css", distClientAssets);
|
|
332
|
+
|
|
333
|
+
for (const cssFile of cssFiles) {
|
|
334
|
+
const withoutExt = cssFile.replace(/\.css$/, "");
|
|
335
|
+
const lastDot = withoutExt.lastIndexOf(".");
|
|
336
|
+
if (lastDot === -1) continue;
|
|
337
|
+
const base = withoutExt.slice(0, lastDot);
|
|
338
|
+
const expected = cssOutputsByBase.get(base);
|
|
339
|
+
if (expected && cssFile !== expected && !emittedCss.has(cssFile)) {
|
|
340
|
+
await remove(join(distClientAssets, cssFile));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await bundle.close();
|
|
346
|
+
|
|
347
|
+
if (args.production) {
|
|
348
|
+
const cssFiles = await scanner.scanFiles("?(.)*.css", distClientAssets);
|
|
349
|
+
for (const cssFile of cssFiles) {
|
|
350
|
+
const cssPath = join(distClientAssets, cssFile);
|
|
351
|
+
let cssContent = await readFile(cssPath, "utf-8");
|
|
352
|
+
const result = transform({
|
|
353
|
+
code: Buffer.from(cssContent),
|
|
354
|
+
filename: cssPath,
|
|
355
|
+
minify: true,
|
|
356
|
+
});
|
|
357
|
+
const minified = result.code.toString();
|
|
358
|
+
if (minified !== cssContent) {
|
|
359
|
+
await writeFile(cssPath, minified);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const manifest: ChunkManifest = {
|
|
365
|
+
chunks: {},
|
|
366
|
+
tags: {},
|
|
367
|
+
styles: {},
|
|
368
|
+
devScripts: args.production ? undefined : ["/assets/console-forward.js"],
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
for (const meta of metas) {
|
|
372
|
+
manifest.chunks[meta.parsed.pattern] = `/assets/${meta.chunk}`;
|
|
373
|
+
manifest.tags[meta.tag] = `/assets/${meta.chunk}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const meta of metas) {
|
|
377
|
+
const routeStyles: string[] = [];
|
|
378
|
+
|
|
379
|
+
for (const [layoutPattern, cssFiles] of Object.entries(layoutCssMap)) {
|
|
380
|
+
if (layoutPattern === "/" || meta.parsed.pattern.startsWith(layoutPattern)) {
|
|
381
|
+
routeStyles.push(...cssFiles);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const componentStyles = componentCssMap[meta.parsed.pattern];
|
|
386
|
+
if (componentStyles?.length) {
|
|
387
|
+
routeStyles.push(...componentStyles);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (routeStyles.length > 0) {
|
|
391
|
+
manifest.styles[meta.parsed.pattern] = [...new Set(routeStyles)];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
await writeFile(chunksPath, JSON.stringify(manifest, null, 2));
|
|
396
|
+
|
|
397
|
+
console.log(` Generated ${metas.length} chunk(s)`);
|
|
398
|
+
|
|
399
|
+
for (const entryPath of entryPaths) {
|
|
400
|
+
await remove(entryPath);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
console.log("✅ Client build complete");
|
|
404
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { mkdir, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { glob } from "node:fs/promises";
|
|
4
|
+
import { rolldown } from "rolldown";
|
|
5
|
+
import { createProgram } from "./ast.ts";
|
|
6
|
+
import { assetUrlPrefixPlugin, type BuildArgs, moduleTypes } from "./build.bundle.ts";
|
|
7
|
+
import { createScanner } from "./build.scan.ts";
|
|
8
|
+
import { validateRoutes, generateRoutesTypeFile } from "./build.validate.ts";
|
|
9
|
+
import { generateModulesFile } from "./build.emit-manifests.ts";
|
|
10
|
+
|
|
11
|
+
export interface BuildServerOptions {
|
|
12
|
+
args: BuildArgs;
|
|
13
|
+
rootDir: string;
|
|
14
|
+
appDir: string;
|
|
15
|
+
distServer: string;
|
|
16
|
+
modulesPath: string;
|
|
17
|
+
chunksPath: string;
|
|
18
|
+
routesTypePath: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function buildServer(options: BuildServerOptions) {
|
|
22
|
+
const { args, rootDir, appDir, distServer, modulesPath, chunksPath, routesTypePath } = options;
|
|
23
|
+
const scanner = createScanner({ rootDir, appDir });
|
|
24
|
+
|
|
25
|
+
console.log("🔍 Scanning for route modules...");
|
|
26
|
+
const routeFiles = await scanner.findRouteModules();
|
|
27
|
+
const layoutFiles = await scanner.findLayouts();
|
|
28
|
+
const errorFile = await scanner.findErrorFile();
|
|
29
|
+
console.log(
|
|
30
|
+
` Found ${routeFiles.length} route(s), ${layoutFiles.length} layout(s)${errorFile ? ", and error page" : ""}`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
console.log("🔎 Validating route types...");
|
|
34
|
+
const valid = await validateRoutes(routeFiles, layoutFiles, appDir);
|
|
35
|
+
if (!valid) {
|
|
36
|
+
console.error("❌ Route validation failed");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const routesTypeContent = generateRoutesTypeFile(routeFiles);
|
|
41
|
+
await writeFile(routesTypePath, routesTypeContent);
|
|
42
|
+
console.log(" Generated route types");
|
|
43
|
+
|
|
44
|
+
const allModuleFiles = [
|
|
45
|
+
...routeFiles.map((f) => join(appDir, f)),
|
|
46
|
+
...layoutFiles.map((f) => join(appDir, f)),
|
|
47
|
+
...(errorFile ? [join(appDir, errorFile)] : []),
|
|
48
|
+
];
|
|
49
|
+
const moduleProgram = createProgram(allModuleFiles);
|
|
50
|
+
|
|
51
|
+
console.log("🔬 Analyzing module exports via AST...");
|
|
52
|
+
const { content: modulesContent, errors: moduleErrors } = generateModulesFile(
|
|
53
|
+
moduleProgram,
|
|
54
|
+
routeFiles,
|
|
55
|
+
layoutFiles,
|
|
56
|
+
errorFile,
|
|
57
|
+
appDir,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
for (const error of moduleErrors) {
|
|
61
|
+
console.error(` ❌ ${error}`);
|
|
62
|
+
}
|
|
63
|
+
if (moduleErrors.length > 0) {
|
|
64
|
+
console.error("❌ Module analysis failed");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await writeFile(modulesPath, modulesContent);
|
|
69
|
+
|
|
70
|
+
console.log("📦 Building server bundle...");
|
|
71
|
+
|
|
72
|
+
await mkdir(distServer, { recursive: true });
|
|
73
|
+
|
|
74
|
+
const packageImports = await scanner.getPackageImports();
|
|
75
|
+
|
|
76
|
+
const bundle = await rolldown({
|
|
77
|
+
input: join(appDir, "index.ts"),
|
|
78
|
+
platform: "node",
|
|
79
|
+
tsconfig: true,
|
|
80
|
+
moduleTypes,
|
|
81
|
+
external: [
|
|
82
|
+
"cloudflare:workers",
|
|
83
|
+
"preact",
|
|
84
|
+
"preact/hooks",
|
|
85
|
+
"preact/compat",
|
|
86
|
+
"preact/jsx-runtime",
|
|
87
|
+
"preact/jsx-dev-runtime",
|
|
88
|
+
"preact/debug",
|
|
89
|
+
"@preact/signals",
|
|
90
|
+
"@preact/signals-core",
|
|
91
|
+
"@preact/signals-debug",
|
|
92
|
+
"preact-render-to-string",
|
|
93
|
+
"preact-render-to-string/stream",
|
|
94
|
+
"preact-custom-element",
|
|
95
|
+
],
|
|
96
|
+
resolve: {
|
|
97
|
+
alias: {
|
|
98
|
+
...packageImports,
|
|
99
|
+
".modules.generated": modulesPath,
|
|
100
|
+
".chunks.generated.json": chunksPath,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
plugins: [assetUrlPrefixPlugin],
|
|
104
|
+
transform: {
|
|
105
|
+
jsx: {
|
|
106
|
+
runtime: "automatic",
|
|
107
|
+
development: false,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await bundle.write({
|
|
113
|
+
dir: distServer,
|
|
114
|
+
format: "esm",
|
|
115
|
+
codeSplitting: false,
|
|
116
|
+
entryFileNames: "index.js",
|
|
117
|
+
assetFileNames: "[name]-[hash][extname]",
|
|
118
|
+
minify: args.production,
|
|
119
|
+
...(args.sourcemap && { sourcemap: true }),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await bundle.close();
|
|
123
|
+
|
|
124
|
+
// Remove emitted assets from server bundle - they're served from dist/client/assets
|
|
125
|
+
for await (const file of glob("*", { cwd: distServer, withFileTypes: false })) {
|
|
126
|
+
if (file === "index.js" || file === "index.js.map") continue;
|
|
127
|
+
await unlink(join(distServer, file as string));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log("✅ Server build complete");
|
|
131
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ModuleTypes, NormalizedOutputOptions, OutputBundle } from "rolldown";
|
|
2
|
+
|
|
3
|
+
export const assetUrlPrefixPlugin = {
|
|
4
|
+
name: "asset-url-prefix",
|
|
5
|
+
generateBundle(_options: NormalizedOutputOptions, bundle: OutputBundle) {
|
|
6
|
+
const assetFileNames = Object.values(bundle)
|
|
7
|
+
.filter((item) => item.type === "asset")
|
|
8
|
+
.map((asset) => asset.fileName);
|
|
9
|
+
|
|
10
|
+
if (assetFileNames.length === 0) return;
|
|
11
|
+
|
|
12
|
+
for (const item of Object.values(bundle)) {
|
|
13
|
+
if (item.type !== "chunk") continue;
|
|
14
|
+
let { code } = item;
|
|
15
|
+
|
|
16
|
+
for (const fileName of assetFileNames) {
|
|
17
|
+
const prefixed = `/assets/${fileName}`;
|
|
18
|
+
code = code
|
|
19
|
+
.replaceAll(`"${fileName}"`, `"${prefixed}"`)
|
|
20
|
+
.replaceAll(`'${fileName}'`, `'${prefixed}'`)
|
|
21
|
+
.replaceAll(`\`${fileName}\``, `\`${prefixed}\``);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
item.code = code;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface BuildArgs {
|
|
30
|
+
production: boolean;
|
|
31
|
+
sourcemap: boolean;
|
|
32
|
+
debug: boolean;
|
|
33
|
+
clean?: boolean;
|
|
34
|
+
serve?: boolean;
|
|
35
|
+
watch?: boolean;
|
|
36
|
+
codemod?: boolean;
|
|
37
|
+
dry?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const moduleTypes: ModuleTypes = {
|
|
41
|
+
".svg": "asset",
|
|
42
|
+
".png": "asset",
|
|
43
|
+
".jpg": "asset",
|
|
44
|
+
".jpeg": "asset",
|
|
45
|
+
".gif": "asset",
|
|
46
|
+
".webp": "asset",
|
|
47
|
+
".ico": "asset",
|
|
48
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { parsePath } from "./paths.ts";
|
|
3
|
+
import { generateTypedModulesFile, validateModule, type ModuleEntry } from "./ast.ts";
|
|
4
|
+
|
|
5
|
+
export function generateModulesFile(
|
|
6
|
+
program: ts.Program,
|
|
7
|
+
routeFiles: string[],
|
|
8
|
+
layoutFiles: string[],
|
|
9
|
+
errorFile: string | null,
|
|
10
|
+
appDir: string,
|
|
11
|
+
): { content: string; errors: string[] } {
|
|
12
|
+
const allFiles = [...layoutFiles, ...routeFiles];
|
|
13
|
+
|
|
14
|
+
if (errorFile) {
|
|
15
|
+
allFiles.push(errorFile);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const entries: ModuleEntry[] = allFiles.map((file) => ({
|
|
19
|
+
path: file,
|
|
20
|
+
parsed: parsePath(file),
|
|
21
|
+
validation: validateModule(program, file, appDir),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
return generateTypedModulesFile(entries);
|
|
25
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { RoutesManifest } from "./manifest.ts";
|
|
2
|
+
|
|
3
|
+
export interface HmrEntryArgs {
|
|
4
|
+
production: boolean;
|
|
5
|
+
debug: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ComponentMeta {
|
|
9
|
+
file: string;
|
|
10
|
+
tag: string;
|
|
11
|
+
props: string[];
|
|
12
|
+
chunk: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildDebugImports(args: HmrEntryArgs) {
|
|
16
|
+
if (!args.debug) return "";
|
|
17
|
+
return /* tsx */ `
|
|
18
|
+
import 'preact/debug'
|
|
19
|
+
import '@preact/signals-debug'
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildRouterInit(routesManifest: RoutesManifest) {
|
|
24
|
+
return `const routesManifest = ${JSON.stringify(routesManifest)};`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildStylesheetRegistration(meta: ComponentMeta, cssFiles: string[], args: HmrEntryArgs) {
|
|
28
|
+
if (!cssFiles.length || args.production) {
|
|
29
|
+
return { imports: "", setup: "" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cssImports = cssFiles.map((file, i) => `import css${i} from '${file}?raw';`).join("\n");
|
|
33
|
+
const inlineStyles = cssFiles.map((file, i) => `{ id: '${file}', css: css${i} }`).join(", ");
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
imports: /* tsx */ `
|
|
37
|
+
import { registerInlineStyles } from '@chr33s/solarflare/client';
|
|
38
|
+
${cssImports}
|
|
39
|
+
`,
|
|
40
|
+
setup: /* tsx */ `
|
|
41
|
+
registerInlineStyles('${meta.tag}', [${inlineStyles}]);
|
|
42
|
+
`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildEntryInit(meta: ComponentMeta, cssFiles: string[]) {
|
|
47
|
+
return /* tsx */ `
|
|
48
|
+
initHmrEntry({
|
|
49
|
+
tag: '${meta.tag}',
|
|
50
|
+
props: ${JSON.stringify(meta.props)},
|
|
51
|
+
routesManifest,
|
|
52
|
+
BaseComponent,
|
|
53
|
+
hmr,
|
|
54
|
+
cssFiles: ${JSON.stringify(cssFiles)},
|
|
55
|
+
onCssUpdate: reloadAllStylesheets,
|
|
56
|
+
});
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function generateChunkedClientEntry(
|
|
61
|
+
meta: ComponentMeta,
|
|
62
|
+
routesManifest: RoutesManifest,
|
|
63
|
+
cssFiles: string[] = [],
|
|
64
|
+
args: HmrEntryArgs,
|
|
65
|
+
) {
|
|
66
|
+
const debugImports = buildDebugImports(args);
|
|
67
|
+
const { imports: stylesheetImports, setup: stylesheetSetup } = buildStylesheetRegistration(
|
|
68
|
+
meta,
|
|
69
|
+
cssFiles,
|
|
70
|
+
args,
|
|
71
|
+
);
|
|
72
|
+
const routesManifestInit = buildRouterInit(routesManifest);
|
|
73
|
+
const entryInit = buildEntryInit(meta, cssFiles);
|
|
74
|
+
|
|
75
|
+
return /* tsx */ `
|
|
76
|
+
/** Auto-generated: ${meta.chunk} */
|
|
77
|
+
${debugImports}
|
|
78
|
+
import { initHmrEntry, hmr, reloadAllStylesheets } from '@chr33s/solarflare/client';
|
|
79
|
+
${stylesheetImports}
|
|
80
|
+
import BaseComponent from '../src/${meta.file}';
|
|
81
|
+
|
|
82
|
+
${stylesheetSetup}
|
|
83
|
+
|
|
84
|
+
${routesManifestInit}
|
|
85
|
+
|
|
86
|
+
${entryInit}
|
|
87
|
+
`;
|
|
88
|
+
}
|