rari 0.1.3
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/dist/chunk-BLXvPPr8.js +30 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +193 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/platform-CvSUcmnc.js +102 -0
- package/dist/platform-DIsErRFA.js +3 -0
- package/dist/railway-XPhB0Ls4.js +216 -0
- package/dist/render-BtA14L2P.js +218 -0
- package/dist/runtime-client-BEWMJWMx.d.ts +283 -0
- package/dist/runtime-client-CC4YQweh.js +707 -0
- package/dist/server-BvGV8m79.js +7084 -0
- package/dist/server-MY0-nRif.d.ts +69 -0
- package/dist/server-build-Cp6_RdeA.js +3 -0
- package/dist/server-build-DaBgiV55.js +618 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +5 -0
- package/package.json +96 -0
- package/src/cli.ts +236 -0
- package/src/client.ts +61 -0
- package/src/deployment/railway.ts +244 -0
- package/src/deployment/render.ts +240 -0
- package/src/index.ts +1 -0
- package/src/platform.ts +163 -0
- package/src/router/file-routes.ts +453 -0
- package/src/router/index.ts +40 -0
- package/src/router/navigation.tsx +535 -0
- package/src/router/router.tsx +504 -0
- package/src/router/types.ts +168 -0
- package/src/router/utils.ts +473 -0
- package/src/router/vite-plugin.ts +324 -0
- package/src/runtime-client.ts +415 -0
- package/src/server.ts +79 -0
- package/src/vite/index.ts +1920 -0
- package/src/vite/server-build.ts +951 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
import type { Plugin } from 'rolldown-vite'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import { build } from 'esbuild'
|
|
6
|
+
|
|
7
|
+
export interface ServerComponentManifest {
|
|
8
|
+
components: Record<
|
|
9
|
+
string,
|
|
10
|
+
{
|
|
11
|
+
id: string
|
|
12
|
+
filePath: string
|
|
13
|
+
relativePath: string
|
|
14
|
+
bundlePath: string
|
|
15
|
+
dependencies: string[]
|
|
16
|
+
hasNodeImports: boolean
|
|
17
|
+
}
|
|
18
|
+
>
|
|
19
|
+
version: string
|
|
20
|
+
buildTime: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ServerBuildOptions {
|
|
24
|
+
outDir?: string
|
|
25
|
+
serverDir?: string
|
|
26
|
+
manifestPath?: string
|
|
27
|
+
minify?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ServerComponentBuilder {
|
|
31
|
+
private serverComponents = new Map<
|
|
32
|
+
string,
|
|
33
|
+
{
|
|
34
|
+
filePath: string
|
|
35
|
+
originalCode: string
|
|
36
|
+
dependencies: string[]
|
|
37
|
+
hasNodeImports: boolean
|
|
38
|
+
}
|
|
39
|
+
>()
|
|
40
|
+
|
|
41
|
+
private options: Required<ServerBuildOptions>
|
|
42
|
+
private projectRoot: string
|
|
43
|
+
|
|
44
|
+
getComponentCount(): number {
|
|
45
|
+
return this.serverComponents.size
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
constructor(projectRoot: string, options: ServerBuildOptions = {}) {
|
|
49
|
+
this.projectRoot = projectRoot
|
|
50
|
+
this.options = {
|
|
51
|
+
outDir: options.outDir || path.join(projectRoot, 'dist'),
|
|
52
|
+
serverDir: options.serverDir || 'server',
|
|
53
|
+
manifestPath: options.manifestPath || 'server-manifest.json',
|
|
54
|
+
minify: options.minify ?? process.env.NODE_ENV === 'production',
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
isServerComponent(filePath: string): boolean {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(filePath)) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
const code = fs.readFileSync(filePath, 'utf-8')
|
|
64
|
+
|
|
65
|
+
const serverDirectives = [
|
|
66
|
+
'\'use server\'',
|
|
67
|
+
'"use server"',
|
|
68
|
+
'/* @server */',
|
|
69
|
+
'// @server',
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
const trimmedCode = code.trim()
|
|
73
|
+
|
|
74
|
+
const hasServerDirective = serverDirectives.some(
|
|
75
|
+
directive =>
|
|
76
|
+
trimmedCode.startsWith(directive) || code.includes(directive),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const isInFunctionsDir
|
|
80
|
+
= filePath.includes('/functions/') || filePath.includes('\\functions\\')
|
|
81
|
+
const hasServerFunctionSignature
|
|
82
|
+
= (code.includes('export async function')
|
|
83
|
+
|| code.includes('export function'))
|
|
84
|
+
&& code.includes('\'use server\'')
|
|
85
|
+
|
|
86
|
+
const hasNodeImports
|
|
87
|
+
= code.includes('from \'node:')
|
|
88
|
+
|| code.includes('from "node:')
|
|
89
|
+
|| code.includes('from \'fs\'')
|
|
90
|
+
|| code.includes('from "fs"')
|
|
91
|
+
|| code.includes('from \'path\'')
|
|
92
|
+
|| code.includes('from "path"')
|
|
93
|
+
|
|
94
|
+
const hasAsyncDefaultExport = /export\s+default\s+async\s+function/.test(
|
|
95
|
+
code,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const isServer
|
|
99
|
+
= hasServerDirective
|
|
100
|
+
|| (isInFunctionsDir && hasServerFunctionSignature)
|
|
101
|
+
|| (hasNodeImports && hasAsyncDefaultExport)
|
|
102
|
+
|
|
103
|
+
return isServer
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private isClientComponent(filePath: string): boolean {
|
|
111
|
+
try {
|
|
112
|
+
if (!fs.existsSync(filePath)) {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
const code = fs.readFileSync(filePath, 'utf-8')
|
|
116
|
+
|
|
117
|
+
const clientDirectives = [
|
|
118
|
+
'\'use client\'',
|
|
119
|
+
'"use client"',
|
|
120
|
+
'/* @client */',
|
|
121
|
+
'// @client',
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
const trimmedCode = code.trim()
|
|
125
|
+
|
|
126
|
+
const hasClientDirective = clientDirectives.some(
|
|
127
|
+
directive =>
|
|
128
|
+
trimmedCode.startsWith(directive) || code.includes(directive),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return hasClientDirective
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
addServerComponent(filePath: string) {
|
|
139
|
+
if (!this.isServerComponent(filePath)) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const code = fs.readFileSync(filePath, 'utf-8')
|
|
144
|
+
const dependencies = this.extractDependencies(code)
|
|
145
|
+
const hasNodeImports = this.hasNodeImports(code)
|
|
146
|
+
|
|
147
|
+
this.serverComponents.set(filePath, {
|
|
148
|
+
filePath,
|
|
149
|
+
originalCode: code,
|
|
150
|
+
dependencies,
|
|
151
|
+
hasNodeImports,
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private extractDependencies(code: string): string[] {
|
|
156
|
+
const dependencies: string[] = []
|
|
157
|
+
const importRegex
|
|
158
|
+
= /import(?:\s+(?:\w+|\{[^}]*\}|\*\s+as\s+\w+)(?:\s*,\s*(?:\w+|\{[^}]*\}|\*\s+as\s+\w+))*\s+from\s+)?['"]([^'"]+)['"]/g
|
|
159
|
+
let match
|
|
160
|
+
|
|
161
|
+
while (true) {
|
|
162
|
+
match = importRegex.exec(code)
|
|
163
|
+
if (match === null)
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
const importPath = match[1]
|
|
167
|
+
if (
|
|
168
|
+
!importPath.startsWith('.')
|
|
169
|
+
&& !importPath.startsWith('/')
|
|
170
|
+
&& !importPath.startsWith('node:')
|
|
171
|
+
&& !this.isNodeBuiltin(importPath)
|
|
172
|
+
) {
|
|
173
|
+
dependencies.push(importPath)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Array.from(new Set(dependencies))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private isNodeBuiltin(moduleName: string): boolean {
|
|
181
|
+
const nodeBuiltins = [
|
|
182
|
+
'fs',
|
|
183
|
+
'path',
|
|
184
|
+
'os',
|
|
185
|
+
'crypto',
|
|
186
|
+
'util',
|
|
187
|
+
'stream',
|
|
188
|
+
'events',
|
|
189
|
+
'process',
|
|
190
|
+
'buffer',
|
|
191
|
+
'url',
|
|
192
|
+
'querystring',
|
|
193
|
+
'zlib',
|
|
194
|
+
'http',
|
|
195
|
+
'https',
|
|
196
|
+
'net',
|
|
197
|
+
'tls',
|
|
198
|
+
'child_process',
|
|
199
|
+
'cluster',
|
|
200
|
+
'worker_threads',
|
|
201
|
+
]
|
|
202
|
+
return nodeBuiltins.includes(moduleName)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private hasNodeImports(code: string): boolean {
|
|
206
|
+
return (
|
|
207
|
+
code.includes('from \'node:')
|
|
208
|
+
|| code.includes('from "node:')
|
|
209
|
+
|| code.includes('from \'fs\'')
|
|
210
|
+
|| code.includes('from "fs"')
|
|
211
|
+
|| code.includes('from \'path\'')
|
|
212
|
+
|| code.includes('from "path"')
|
|
213
|
+
|| code.includes('from \'os\'')
|
|
214
|
+
|| code.includes('from "os"')
|
|
215
|
+
|| code.includes('from \'crypto\'')
|
|
216
|
+
|| code.includes('from "crypto"')
|
|
217
|
+
|| code.includes('from \'util\'')
|
|
218
|
+
|| code.includes('from "util"')
|
|
219
|
+
|| code.includes('from \'stream\'')
|
|
220
|
+
|| code.includes('from "stream"')
|
|
221
|
+
|| code.includes('from \'events\'')
|
|
222
|
+
|| code.includes('from "events"')
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async getTransformedComponentsForDevelopment(): Promise<Array<{ id: string, code: string }>> {
|
|
227
|
+
const components: Array<{ id: string, code: string }> = []
|
|
228
|
+
|
|
229
|
+
for (const [filePath, component] of this.serverComponents) {
|
|
230
|
+
const relativePath = path.relative(this.projectRoot, filePath)
|
|
231
|
+
const componentId = this.getComponentId(relativePath)
|
|
232
|
+
|
|
233
|
+
const transformedCode = await this.buildComponentCodeOnly(filePath, componentId, component)
|
|
234
|
+
|
|
235
|
+
components.push({
|
|
236
|
+
id: componentId,
|
|
237
|
+
code: transformedCode,
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return components
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private async buildComponentCodeOnly(
|
|
245
|
+
inputPath: string,
|
|
246
|
+
componentId: string,
|
|
247
|
+
_component: { dependencies: string[], hasNodeImports: boolean },
|
|
248
|
+
): Promise<string> {
|
|
249
|
+
const originalCode = await fs.promises.readFile(inputPath, 'utf-8')
|
|
250
|
+
const clientTransformedCode = this.transformClientImports(
|
|
251
|
+
originalCode,
|
|
252
|
+
inputPath,
|
|
253
|
+
)
|
|
254
|
+
const transformedCode = this.transformNodeImports(clientTransformedCode)
|
|
255
|
+
|
|
256
|
+
const ext = path.extname(inputPath)
|
|
257
|
+
let loader: string
|
|
258
|
+
if (ext === '.tsx') {
|
|
259
|
+
loader = 'tsx'
|
|
260
|
+
}
|
|
261
|
+
else if (ext === '.ts') {
|
|
262
|
+
loader = 'ts'
|
|
263
|
+
}
|
|
264
|
+
else if (ext === '.jsx') {
|
|
265
|
+
loader = 'jsx'
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
loader = 'js'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const result = await build({
|
|
273
|
+
stdin: {
|
|
274
|
+
contents: transformedCode,
|
|
275
|
+
resolveDir: path.dirname(inputPath),
|
|
276
|
+
sourcefile: inputPath,
|
|
277
|
+
loader: loader as any,
|
|
278
|
+
},
|
|
279
|
+
bundle: true,
|
|
280
|
+
platform: 'neutral',
|
|
281
|
+
target: 'es2022',
|
|
282
|
+
format: 'esm',
|
|
283
|
+
external: [],
|
|
284
|
+
mainFields: ['module', 'main'],
|
|
285
|
+
conditions: ['import', 'module', 'default'],
|
|
286
|
+
define: {
|
|
287
|
+
'global': 'globalThis',
|
|
288
|
+
'process.env.NODE_ENV': '"production"',
|
|
289
|
+
},
|
|
290
|
+
loader: {
|
|
291
|
+
'.ts': 'ts',
|
|
292
|
+
'.tsx': 'tsx',
|
|
293
|
+
'.js': 'js',
|
|
294
|
+
'.jsx': 'jsx',
|
|
295
|
+
},
|
|
296
|
+
resolveExtensions: ['.ts', '.tsx', '.js', '.jsx'],
|
|
297
|
+
minify: false,
|
|
298
|
+
minifyIdentifiers: false,
|
|
299
|
+
sourcemap: false,
|
|
300
|
+
metafile: false,
|
|
301
|
+
write: false,
|
|
302
|
+
plugins: [
|
|
303
|
+
{
|
|
304
|
+
name: 'hmr-auto-external',
|
|
305
|
+
setup(build) {
|
|
306
|
+
build.onResolve({ filter: /^[^./]/ }, async (args) => {
|
|
307
|
+
return { path: args.path, external: true }
|
|
308
|
+
})
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: 'resolve-server-functions',
|
|
313
|
+
setup(build) {
|
|
314
|
+
build.onResolve(
|
|
315
|
+
{ filter: /^\.\.?\/.*functions/ },
|
|
316
|
+
async (args) => {
|
|
317
|
+
const resolvedPath = path.resolve(
|
|
318
|
+
path.dirname(args.importer),
|
|
319
|
+
args.path,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
const possibleExtensions = [
|
|
323
|
+
'.ts',
|
|
324
|
+
'.js',
|
|
325
|
+
'.tsx',
|
|
326
|
+
'.jsx',
|
|
327
|
+
'/index.ts',
|
|
328
|
+
'/index.js',
|
|
329
|
+
]
|
|
330
|
+
for (const ext of possibleExtensions) {
|
|
331
|
+
const fullPath = resolvedPath + ext
|
|
332
|
+
if (fs.existsSync(fullPath)) {
|
|
333
|
+
return { path: fullPath }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return null
|
|
338
|
+
},
|
|
339
|
+
)
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
banner: {
|
|
344
|
+
js: `// Rari Server Component Bundle
|
|
345
|
+
// Generated at: ${new Date().toISOString()}
|
|
346
|
+
// Original file: ${path.relative(this.projectRoot, inputPath)}
|
|
347
|
+
`,
|
|
348
|
+
},
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
if (result.outputFiles && result.outputFiles.length > 0) {
|
|
352
|
+
const outputFile = result.outputFiles[0]
|
|
353
|
+
|
|
354
|
+
const finalTransformedCode = this.createSelfRegisteringModule(
|
|
355
|
+
outputFile.text,
|
|
356
|
+
componentId,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return finalTransformedCode
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (result.errors.length > 0) {
|
|
363
|
+
console.error('ESBuild errors:', result.errors)
|
|
364
|
+
throw new Error(
|
|
365
|
+
`ESBuild compilation failed with ${result.errors.length} errors`,
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
throw new Error('No output generated from ESBuild')
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
console.error(`ESBuild failed for ${inputPath}:`, error)
|
|
373
|
+
throw error
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async buildServerComponents(): Promise<ServerComponentManifest> {
|
|
378
|
+
const serverOutDir = path.join(this.options.outDir, this.options.serverDir)
|
|
379
|
+
|
|
380
|
+
await fs.promises.mkdir(serverOutDir, { recursive: true })
|
|
381
|
+
|
|
382
|
+
const manifest: ServerComponentManifest = {
|
|
383
|
+
components: {},
|
|
384
|
+
version: '1.0.0',
|
|
385
|
+
buildTime: new Date().toISOString(),
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (const [filePath, component] of this.serverComponents) {
|
|
389
|
+
const relativePath = path.relative(this.projectRoot, filePath)
|
|
390
|
+
const componentId = this.getComponentId(relativePath)
|
|
391
|
+
const bundlePath = path.join(this.options.serverDir, `${componentId}.js`)
|
|
392
|
+
const fullBundlePath = path.join(this.options.outDir, bundlePath)
|
|
393
|
+
|
|
394
|
+
const bundleDir = path.dirname(fullBundlePath)
|
|
395
|
+
await fs.promises.mkdir(bundleDir, { recursive: true })
|
|
396
|
+
|
|
397
|
+
await this.buildSingleComponent(filePath, fullBundlePath, component)
|
|
398
|
+
|
|
399
|
+
manifest.components[componentId] = {
|
|
400
|
+
id: componentId,
|
|
401
|
+
filePath,
|
|
402
|
+
relativePath,
|
|
403
|
+
bundlePath,
|
|
404
|
+
dependencies: component.dependencies,
|
|
405
|
+
hasNodeImports: component.hasNodeImports,
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const manifestPath = path.join(
|
|
410
|
+
this.options.outDir,
|
|
411
|
+
this.options.manifestPath,
|
|
412
|
+
)
|
|
413
|
+
await fs.promises.writeFile(
|
|
414
|
+
manifestPath,
|
|
415
|
+
JSON.stringify(manifest, null, 2),
|
|
416
|
+
'utf-8',
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return manifest
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async buildSingleComponent(
|
|
423
|
+
inputPath: string,
|
|
424
|
+
outputPath: string,
|
|
425
|
+
_component: { dependencies: string[], hasNodeImports: boolean },
|
|
426
|
+
): Promise<void> {
|
|
427
|
+
const componentId = this.getComponentId(
|
|
428
|
+
path.relative(this.projectRoot, inputPath),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
const originalCode = await fs.promises.readFile(inputPath, 'utf-8')
|
|
432
|
+
const clientTransformedCode = this.transformClientImports(
|
|
433
|
+
originalCode,
|
|
434
|
+
inputPath,
|
|
435
|
+
)
|
|
436
|
+
const transformedCode = this.transformNodeImports(clientTransformedCode)
|
|
437
|
+
|
|
438
|
+
const ext = path.extname(inputPath)
|
|
439
|
+
let loader: string
|
|
440
|
+
if (ext === '.tsx') {
|
|
441
|
+
loader = 'tsx'
|
|
442
|
+
}
|
|
443
|
+
else if (ext === '.ts') {
|
|
444
|
+
loader = 'ts'
|
|
445
|
+
}
|
|
446
|
+
else if (ext === '.jsx') {
|
|
447
|
+
loader = 'jsx'
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
loader = 'js'
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
const result = await build({
|
|
455
|
+
stdin: {
|
|
456
|
+
contents: transformedCode,
|
|
457
|
+
resolveDir: path.dirname(inputPath),
|
|
458
|
+
sourcefile: inputPath,
|
|
459
|
+
loader: loader as any,
|
|
460
|
+
},
|
|
461
|
+
bundle: true,
|
|
462
|
+
platform: 'neutral',
|
|
463
|
+
target: 'es2022',
|
|
464
|
+
format: 'esm',
|
|
465
|
+
outfile: outputPath,
|
|
466
|
+
external: [],
|
|
467
|
+
mainFields: ['module', 'main'],
|
|
468
|
+
conditions: ['import', 'module', 'default'],
|
|
469
|
+
define: {
|
|
470
|
+
'global': 'globalThis',
|
|
471
|
+
'process.env.NODE_ENV': '"production"',
|
|
472
|
+
},
|
|
473
|
+
loader: {
|
|
474
|
+
'.ts': 'ts',
|
|
475
|
+
'.tsx': 'tsx',
|
|
476
|
+
'.js': 'js',
|
|
477
|
+
'.jsx': 'jsx',
|
|
478
|
+
},
|
|
479
|
+
resolveExtensions: ['.ts', '.tsx', '.js', '.jsx'],
|
|
480
|
+
minify: false,
|
|
481
|
+
minifyIdentifiers: false,
|
|
482
|
+
sourcemap: false,
|
|
483
|
+
metafile: false,
|
|
484
|
+
write: false,
|
|
485
|
+
plugins: [
|
|
486
|
+
{
|
|
487
|
+
name: 'auto-external',
|
|
488
|
+
setup(build) {
|
|
489
|
+
build.onResolve({ filter: /^[^./]/ }, async (args) => {
|
|
490
|
+
return { path: args.path, external: true }
|
|
491
|
+
})
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: 'resolve-server-functions',
|
|
496
|
+
setup(build) {
|
|
497
|
+
build.onResolve(
|
|
498
|
+
{ filter: /^\.\.?\/.*functions/ },
|
|
499
|
+
async (args) => {
|
|
500
|
+
const resolvedPath = path.resolve(
|
|
501
|
+
path.dirname(args.importer),
|
|
502
|
+
args.path,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
const possibleExtensions = [
|
|
506
|
+
'.ts',
|
|
507
|
+
'.js',
|
|
508
|
+
'.tsx',
|
|
509
|
+
'.jsx',
|
|
510
|
+
'/index.ts',
|
|
511
|
+
'/index.js',
|
|
512
|
+
]
|
|
513
|
+
for (const ext of possibleExtensions) {
|
|
514
|
+
const fullPath = resolvedPath + ext
|
|
515
|
+
if (fs.existsSync(fullPath)) {
|
|
516
|
+
return { path: fullPath }
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return null
|
|
521
|
+
},
|
|
522
|
+
)
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
banner: {
|
|
527
|
+
js: `// Rari Server Component Bundle
|
|
528
|
+
// Generated at: ${new Date().toISOString()}
|
|
529
|
+
// Original file: ${path.relative(this.projectRoot, inputPath)}
|
|
530
|
+
`,
|
|
531
|
+
},
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
if (result.outputFiles && result.outputFiles.length > 0) {
|
|
535
|
+
const outputFile = result.outputFiles[0]
|
|
536
|
+
|
|
537
|
+
const finalTransformedCode = this.createSelfRegisteringModule(
|
|
538
|
+
outputFile.text,
|
|
539
|
+
componentId,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
await fs.promises.writeFile(outputPath, finalTransformedCode, 'utf-8')
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (result.errors.length > 0) {
|
|
546
|
+
console.error('ESBuild errors:', result.errors)
|
|
547
|
+
throw new Error(
|
|
548
|
+
`ESBuild compilation failed with ${result.errors.length} errors`,
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (result.warnings.length > 0) {
|
|
553
|
+
console.warn('ESBuild warnings:', result.warnings)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
console.error(`ESBuild failed for ${inputPath}:`, error)
|
|
558
|
+
throw error
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private createSelfRegisteringModule(
|
|
563
|
+
code: string,
|
|
564
|
+
componentId: string,
|
|
565
|
+
): string {
|
|
566
|
+
if (code.includes('Self-registering Production Component')) {
|
|
567
|
+
return code
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let transformedCode = code
|
|
571
|
+
|
|
572
|
+
let defaultExportName: string | null = null
|
|
573
|
+
const namedExports: string[] = []
|
|
574
|
+
|
|
575
|
+
transformedCode = transformedCode.replace(
|
|
576
|
+
/^export\s+default\s+function\s+(\w+)/gm,
|
|
577
|
+
(match, name) => {
|
|
578
|
+
defaultExportName = name
|
|
579
|
+
return `function ${name}`
|
|
580
|
+
},
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
transformedCode = transformedCode.replace(
|
|
584
|
+
/^export\s+default\s+async\s+function\s+(\w+)/gm,
|
|
585
|
+
(match, name) => {
|
|
586
|
+
defaultExportName = name
|
|
587
|
+
return `async function ${name}`
|
|
588
|
+
},
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
transformedCode = transformedCode.replace(
|
|
592
|
+
/^export\s+default\s+(\w+);?\s*$/gm,
|
|
593
|
+
(match, name) => {
|
|
594
|
+
defaultExportName = name
|
|
595
|
+
return `// Default export: ${name}`
|
|
596
|
+
},
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
transformedCode = transformedCode.replace(
|
|
600
|
+
/^export\s*\{\s*(\w+)\s+as\s+default\s*\};?\s*$/gm,
|
|
601
|
+
(match, name) => {
|
|
602
|
+
defaultExportName = name
|
|
603
|
+
return `// Default export: ${name}`
|
|
604
|
+
},
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
transformedCode = transformedCode.replace(
|
|
608
|
+
/^export\s*\{([^}]+)\};?\s*$/gm,
|
|
609
|
+
(match, exports) => {
|
|
610
|
+
const exportList = exports.split(',').map((exp: string) => exp.trim())
|
|
611
|
+
exportList.forEach((exp: string) => {
|
|
612
|
+
if (exp.includes('as default')) {
|
|
613
|
+
const actualName = exp.replace('as default', '').trim()
|
|
614
|
+
defaultExportName = actualName
|
|
615
|
+
}
|
|
616
|
+
else if (exp === 'default') {
|
|
617
|
+
const possibleDefault = `${componentId}_default`
|
|
618
|
+
if (transformedCode.includes(`var ${possibleDefault}`)) {
|
|
619
|
+
defaultExportName = possibleDefault
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
namedExports.push(exp)
|
|
624
|
+
}
|
|
625
|
+
})
|
|
626
|
+
return `// Exports: ${exports}`
|
|
627
|
+
},
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
transformedCode = transformedCode.replace(
|
|
631
|
+
/^export\s+(?:async\s+)?function\s+(\w+)/gm,
|
|
632
|
+
(match, name) => {
|
|
633
|
+
namedExports.push(name)
|
|
634
|
+
return match.replace('export ', '')
|
|
635
|
+
},
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
transformedCode = transformedCode.replace(
|
|
639
|
+
/^export\s+(const|let|var)\s+(\w+)/gm,
|
|
640
|
+
(match, keyword, name) => {
|
|
641
|
+
namedExports.push(name)
|
|
642
|
+
return `${keyword} ${name}`
|
|
643
|
+
},
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
if (!defaultExportName) {
|
|
647
|
+
const possibleDefault = `${componentId}_default`
|
|
648
|
+
if (transformedCode.includes(`var ${possibleDefault}`)) {
|
|
649
|
+
defaultExportName = possibleDefault
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const mainExport = defaultExportName || componentId
|
|
654
|
+
|
|
655
|
+
const selfRegisteringCode = `// Self-registering Production Component: ${componentId}
|
|
656
|
+
"use module";
|
|
657
|
+
|
|
658
|
+
// Original component code with exports removed for self-registration
|
|
659
|
+
${transformedCode}
|
|
660
|
+
|
|
661
|
+
// Self-registration logic
|
|
662
|
+
(function() {
|
|
663
|
+
try {
|
|
664
|
+
const moduleKey = "${componentId}";
|
|
665
|
+
const registrationKey = "Component_${Math.random().toString(36).substr(2, 8)}";
|
|
666
|
+
let mainExport = null;
|
|
667
|
+
let exportedFunctions = {};
|
|
668
|
+
|
|
669
|
+
globalThis.__rsc_functions = globalThis.__rsc_functions || {};
|
|
670
|
+
|
|
671
|
+
// Register named exports
|
|
672
|
+
${namedExports
|
|
673
|
+
.map(
|
|
674
|
+
name => `
|
|
675
|
+
if (typeof ${name} !== 'undefined') {
|
|
676
|
+
globalThis.${name} = ${name};
|
|
677
|
+
globalThis.__rsc_functions['${name}'] = ${name};
|
|
678
|
+
exportedFunctions['${name}'] = ${name};
|
|
679
|
+
}`,
|
|
680
|
+
)
|
|
681
|
+
.join('')}
|
|
682
|
+
|
|
683
|
+
// Set main export
|
|
684
|
+
if (typeof ${mainExport} !== 'undefined') {
|
|
685
|
+
mainExport = ${mainExport};
|
|
686
|
+
} else {
|
|
687
|
+
const potentialExports = {};
|
|
688
|
+
${namedExports.map(name => `if (typeof ${name} !== 'undefined') potentialExports.${name} = ${name};`).join('\n ')}
|
|
689
|
+
|
|
690
|
+
if (Object.keys(potentialExports).length > 0) {
|
|
691
|
+
if (Object.keys(potentialExports).length === 1) {
|
|
692
|
+
mainExport = potentialExports[Object.keys(potentialExports)[0]];
|
|
693
|
+
} else {
|
|
694
|
+
mainExport = potentialExports;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (mainExport !== null) {
|
|
700
|
+
if (!globalThis[moduleKey]) {
|
|
701
|
+
globalThis[moduleKey] = mainExport;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!globalThis[registrationKey]) {
|
|
705
|
+
globalThis[registrationKey] = mainExport;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (typeof globalThis.RscModuleManager !== 'undefined' && globalThis.RscModuleManager.register) {
|
|
709
|
+
globalThis.RscModuleManager.register(moduleKey, mainExport, exportedFunctions);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
} catch (error) {
|
|
713
|
+
console.error('Error in self-registration for ${componentId}:', error);
|
|
714
|
+
}
|
|
715
|
+
})();`
|
|
716
|
+
|
|
717
|
+
return selfRegisteringCode
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private transformClientImports(code: string, inputPath: string): string {
|
|
721
|
+
let transformedCode = code
|
|
722
|
+
|
|
723
|
+
const importRegex
|
|
724
|
+
= /import\s+(\w+)(?:\s*,\s*\{[^}]*\})?\s+from\s+['"]([./][^'"]+)['"];?\s*$/gm
|
|
725
|
+
let match
|
|
726
|
+
|
|
727
|
+
const replacements: Array<{ original: string, replacement: string }> = []
|
|
728
|
+
let hasClientComponents = false
|
|
729
|
+
|
|
730
|
+
while (true) {
|
|
731
|
+
match = importRegex.exec(code)
|
|
732
|
+
if (match === null)
|
|
733
|
+
break
|
|
734
|
+
|
|
735
|
+
const [fullMatch, defaultImport, importPath] = match
|
|
736
|
+
|
|
737
|
+
const resolvedPath = this.resolveImportPath(importPath, inputPath)
|
|
738
|
+
|
|
739
|
+
if (this.isClientComponent(resolvedPath)) {
|
|
740
|
+
hasClientComponents = true
|
|
741
|
+
const componentName = defaultImport || 'default'
|
|
742
|
+
|
|
743
|
+
const replacement = `const ${componentName} = registerClientReference(
|
|
744
|
+
null,
|
|
745
|
+
${JSON.stringify(path.relative(this.projectRoot, resolvedPath))},
|
|
746
|
+
"default"
|
|
747
|
+
);`
|
|
748
|
+
|
|
749
|
+
replacements.push({ original: fullMatch, replacement })
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (hasClientComponents) {
|
|
754
|
+
const functionDefinition = `
|
|
755
|
+
// registerClientReference function for client components
|
|
756
|
+
function registerClientReference(clientReference, id, exportName) {
|
|
757
|
+
const key = id + '#' + exportName;
|
|
758
|
+
|
|
759
|
+
const clientProxy = {};
|
|
760
|
+
|
|
761
|
+
Object.defineProperty(clientProxy, '$$typeof', {
|
|
762
|
+
value: Symbol.for('react.client.reference'),
|
|
763
|
+
enumerable: false
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
Object.defineProperty(clientProxy, '$$id', {
|
|
767
|
+
value: key,
|
|
768
|
+
enumerable: false
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
Object.defineProperty(clientProxy, '$$async', {
|
|
772
|
+
value: false,
|
|
773
|
+
enumerable: false
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
if (typeof globalThis.__rari_bridge !== 'undefined' &&
|
|
778
|
+
typeof globalThis.__rari_bridge.registerClientReference === 'function') {
|
|
779
|
+
globalThis.__rari_bridge.registerClientReference(key, id, exportName);
|
|
780
|
+
}
|
|
781
|
+
} catch (error) {
|
|
782
|
+
console.error('Failed to register client reference with Rust bridge:', error);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return clientProxy;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
`
|
|
789
|
+
transformedCode = functionDefinition + transformedCode
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
for (const { original, replacement } of replacements) {
|
|
793
|
+
transformedCode = transformedCode.replace(original, replacement)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return transformedCode
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private resolveImportPath(importPath: string, importerPath: string): string {
|
|
800
|
+
const resolvedPath = path.resolve(path.dirname(importerPath), importPath)
|
|
801
|
+
|
|
802
|
+
const extensions = ['.tsx', '.jsx', '.ts', '.js']
|
|
803
|
+
for (const ext of extensions) {
|
|
804
|
+
const pathWithExt = `${resolvedPath}${ext}`
|
|
805
|
+
if (fs.existsSync(pathWithExt)) {
|
|
806
|
+
return pathWithExt
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (fs.existsSync(resolvedPath)) {
|
|
811
|
+
for (const ext of extensions) {
|
|
812
|
+
const indexPath = path.join(resolvedPath, `index${ext}`)
|
|
813
|
+
if (fs.existsSync(indexPath)) {
|
|
814
|
+
return indexPath
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return `${resolvedPath}.tsx`
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
private transformNodeImports(code: string): string {
|
|
823
|
+
let transformedCode = code
|
|
824
|
+
|
|
825
|
+
transformedCode = transformedCode.replace(
|
|
826
|
+
/import\s+(\w+)\s+from\s+['"]node:process['"];?\s*$/gm,
|
|
827
|
+
(match, importName) => {
|
|
828
|
+
return `const ${importName} = globalThis.process;`
|
|
829
|
+
},
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
transformedCode = transformedCode.replace(
|
|
833
|
+
/import\s+\{([^}]+)\}\s+from\s+['"]node:fs['"];?\s*$/gm,
|
|
834
|
+
(match, imports) => {
|
|
835
|
+
const importList = imports.split(',').map((imp: string) => imp.trim())
|
|
836
|
+
return importList
|
|
837
|
+
.map((imp: string) => {
|
|
838
|
+
const cleanImp = imp.replace(/\s+as\s+\w+/, '')
|
|
839
|
+
if (cleanImp === 'existsSync') {
|
|
840
|
+
return `const ${cleanImp} = (path) => { try { if (globalThis.Deno?.statSync) { globalThis.Deno.statSync(path); return true; } return false; } catch (error) { return false; } };`
|
|
841
|
+
}
|
|
842
|
+
if (cleanImp === 'readFileSync') {
|
|
843
|
+
return `const ${cleanImp} = (path, encoding = 'utf8') => globalThis.Deno.readTextFileSync(path);`
|
|
844
|
+
}
|
|
845
|
+
return `const ${cleanImp} = globalThis.Deno?.${cleanImp} || (() => { throw new Error('${cleanImp} not available'); });`
|
|
846
|
+
})
|
|
847
|
+
.join('\n')
|
|
848
|
+
},
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
transformedCode = transformedCode.replace(
|
|
852
|
+
/import\s+\{([^}]+)\}\s+from\s+['"]node:path['"];?\s*$/gm,
|
|
853
|
+
(match, imports) => {
|
|
854
|
+
const importList = imports.split(',').map((imp: string) => imp.trim())
|
|
855
|
+
return importList
|
|
856
|
+
.map((imp: string) => {
|
|
857
|
+
const cleanImp = imp.replace(/\s+as\s+\w+/, '')
|
|
858
|
+
if (cleanImp === 'join') {
|
|
859
|
+
return `const ${cleanImp} = (...paths) => paths.filter(Boolean).join('/').replace(/\\/+/g, '/');`
|
|
860
|
+
}
|
|
861
|
+
return `const ${cleanImp} = globalThis.path?.${cleanImp} || (() => { throw new Error('${cleanImp} not available'); });`
|
|
862
|
+
})
|
|
863
|
+
.join('\n')
|
|
864
|
+
},
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
transformedCode = transformedCode.replace(
|
|
868
|
+
/import\s+\{([^}]+)\}\s+from\s+['"]node:process['"];?\s*$/gm,
|
|
869
|
+
(match, imports) => {
|
|
870
|
+
const importList = imports.split(',').map((imp: string) => imp.trim())
|
|
871
|
+
return importList
|
|
872
|
+
.map((imp: string) => {
|
|
873
|
+
const cleanImp = imp.replace(/\s+as\s+\w+/, '')
|
|
874
|
+
if (cleanImp === 'cwd') {
|
|
875
|
+
return `const ${cleanImp} = () => globalThis.Deno?.cwd?.() || '.';`
|
|
876
|
+
}
|
|
877
|
+
return `const ${cleanImp} = globalThis.process?.${cleanImp} || (() => { throw new Error('${cleanImp} not available'); });`
|
|
878
|
+
})
|
|
879
|
+
.join('\n')
|
|
880
|
+
},
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
return transformedCode
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
private getComponentId(relativePath: string): string {
|
|
887
|
+
return relativePath
|
|
888
|
+
.replace(/\\/g, '/')
|
|
889
|
+
.replace(/\.(tsx?|jsx?)$/, '')
|
|
890
|
+
.replace(/[^\w/-]/g, '_')
|
|
891
|
+
.replace(/^src\//, '')
|
|
892
|
+
.replace(/^components\//, '')
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export function scanDirectory(dir: string, builder: ServerComponentBuilder) {
|
|
897
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
898
|
+
|
|
899
|
+
for (const entry of entries) {
|
|
900
|
+
const fullPath = path.join(dir, entry.name)
|
|
901
|
+
|
|
902
|
+
if (entry.isDirectory()) {
|
|
903
|
+
scanDirectory(fullPath, builder)
|
|
904
|
+
}
|
|
905
|
+
else if (entry.isFile() && /\.(?:tsx?|jsx?)$/.test(entry.name)) {
|
|
906
|
+
try {
|
|
907
|
+
if (builder.isServerComponent(fullPath)) {
|
|
908
|
+
builder.addServerComponent(fullPath)
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
console.warn(
|
|
913
|
+
`[server-build] Error checking ${fullPath}:`,
|
|
914
|
+
error instanceof Error ? error.message : error,
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
export function createServerBuildPlugin(
|
|
922
|
+
options: ServerBuildOptions = {},
|
|
923
|
+
): Plugin {
|
|
924
|
+
let builder: ServerComponentBuilder | null = null
|
|
925
|
+
let projectRoot: string
|
|
926
|
+
|
|
927
|
+
return {
|
|
928
|
+
name: 'rari-server-build',
|
|
929
|
+
|
|
930
|
+
configResolved(config) {
|
|
931
|
+
projectRoot = config.root
|
|
932
|
+
builder = new ServerComponentBuilder(projectRoot, options)
|
|
933
|
+
},
|
|
934
|
+
|
|
935
|
+
buildStart() {
|
|
936
|
+
if (!builder)
|
|
937
|
+
return
|
|
938
|
+
|
|
939
|
+
const srcDir = path.join(projectRoot, 'src')
|
|
940
|
+
if (fs.existsSync(srcDir)) {
|
|
941
|
+
scanDirectory(srcDir, builder)
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
|
|
945
|
+
async closeBundle() {
|
|
946
|
+
if (builder) {
|
|
947
|
+
await builder.buildServerComponents()
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
}
|
|
951
|
+
}
|