zylaris 1.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/LICENSE +21 -0
- package/README.md +558 -0
- package/Zylaris.js.png +0 -0
- package/examples/default/index.html +13 -0
- package/examples/default/package.json +23 -0
- package/examples/default/src/app/about/page.tsx +18 -0
- package/examples/default/src/app/counter/page.tsx +22 -0
- package/examples/default/src/app/global.css +225 -0
- package/examples/default/src/app/layout.tsx +33 -0
- package/examples/default/src/app/page.tsx +14 -0
- package/examples/default/src/entry-client.tsx +87 -0
- package/examples/default/src/entry-server.tsx +52 -0
- package/examples/default/src/router.ts +60 -0
- package/examples/default/tsconfig.json +28 -0
- package/examples/default/zylaris.config.ts +24 -0
- package/package.json +34 -0
- package/packages/adapter/package.json +59 -0
- package/packages/adapter/src/adapters/bun.ts +215 -0
- package/packages/adapter/src/adapters/cloudflare.ts +278 -0
- package/packages/adapter/src/adapters/deno.ts +219 -0
- package/packages/adapter/src/adapters/netlify.ts +274 -0
- package/packages/adapter/src/adapters/node.ts +155 -0
- package/packages/adapter/src/adapters/static.ts +134 -0
- package/packages/adapter/src/adapters/vercel.ts +239 -0
- package/packages/adapter/src/index.ts +115 -0
- package/packages/adapter/src/lib/builder.ts +361 -0
- package/packages/adapter/src/types.ts +191 -0
- package/packages/adapter/tsconfig.json +8 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/bin.ts +107 -0
- package/packages/cli/src/commands/build.ts +197 -0
- package/packages/cli/src/commands/create.ts +222 -0
- package/packages/cli/src/commands/deploy.ts +90 -0
- package/packages/cli/src/commands/dev.ts +108 -0
- package/packages/cli/src/index.ts +6 -0
- package/packages/cli/tsconfig.json +9 -0
- package/packages/compiler/package.json +39 -0
- package/packages/compiler/src/index.ts +210 -0
- package/packages/compiler/src/jit.ts +187 -0
- package/packages/compiler/tsconfig.json +9 -0
- package/packages/core/package.json +55 -0
- package/packages/core/src/components.test.ts +125 -0
- package/packages/core/src/components.ts +181 -0
- package/packages/core/src/config.ts +204 -0
- package/packages/core/src/hooks.ts +142 -0
- package/packages/core/src/index.ts +59 -0
- package/packages/core/src/jsx-runtime.ts +46 -0
- package/packages/core/tsconfig.json +16 -0
- package/packages/dev-server/package.json +51 -0
- package/packages/dev-server/src/index.ts +306 -0
- package/packages/dev-server/src/jit-middleware.ts +78 -0
- package/packages/dev-server/tsconfig.json +9 -0
- package/packages/plugins/package.json +44 -0
- package/packages/plugins/src/cdn/loader.ts +275 -0
- package/packages/plugins/src/index.ts +238 -0
- package/packages/plugins/src/loaders/auto-import.ts +219 -0
- package/packages/plugins/src/loaders/external.ts +332 -0
- package/packages/plugins/src/transforms/index.ts +407 -0
- package/packages/plugins/src/types.ts +296 -0
- package/packages/plugins/tsconfig.json +8 -0
- package/packages/reactivity/package.json +36 -0
- package/packages/reactivity/src/computed.d.ts +3 -0
- package/packages/reactivity/src/computed.d.ts.map +1 -0
- package/packages/reactivity/src/computed.js +64 -0
- package/packages/reactivity/src/computed.js.map +1 -0
- package/packages/reactivity/src/computed.test.ts +83 -0
- package/packages/reactivity/src/computed.ts +69 -0
- package/packages/reactivity/src/index.d.ts +6 -0
- package/packages/reactivity/src/index.d.ts.map +1 -0
- package/packages/reactivity/src/index.js +7 -0
- package/packages/reactivity/src/index.js.map +1 -0
- package/packages/reactivity/src/index.ts +18 -0
- package/packages/reactivity/src/resource.d.ts +6 -0
- package/packages/reactivity/src/resource.d.ts.map +1 -0
- package/packages/reactivity/src/resource.js +43 -0
- package/packages/reactivity/src/resource.js.map +1 -0
- package/packages/reactivity/src/resource.test.ts +70 -0
- package/packages/reactivity/src/resource.ts +59 -0
- package/packages/reactivity/src/signal.d.ts +7 -0
- package/packages/reactivity/src/signal.d.ts.map +1 -0
- package/packages/reactivity/src/signal.js +145 -0
- package/packages/reactivity/src/signal.js.map +1 -0
- package/packages/reactivity/src/signal.test.ts +130 -0
- package/packages/reactivity/src/signal.ts +207 -0
- package/packages/reactivity/src/store.d.ts +4 -0
- package/packages/reactivity/src/store.d.ts.map +1 -0
- package/packages/reactivity/src/store.js +62 -0
- package/packages/reactivity/src/store.js.map +1 -0
- package/packages/reactivity/src/store.test.ts +38 -0
- package/packages/reactivity/src/store.ts +111 -0
- package/packages/reactivity/src/types.d.ts +43 -0
- package/packages/reactivity/src/types.d.ts.map +1 -0
- package/packages/reactivity/src/types.js +3 -0
- package/packages/reactivity/src/types.js.map +1 -0
- package/packages/reactivity/src/types.ts +43 -0
- package/packages/reactivity/tsconfig.json +9 -0
- package/packages/router/package.json +44 -0
- package/packages/router/src/components.tsx +150 -0
- package/packages/router/src/fs-router.ts +163 -0
- package/packages/router/src/index.ts +22 -0
- package/packages/router/src/router.test.ts +111 -0
- package/packages/router/src/router.ts +112 -0
- package/packages/router/src/types.ts +69 -0
- package/packages/router/tsconfig.json +10 -0
- package/packages/server/package.json +41 -0
- package/packages/server/src/action.test.ts +102 -0
- package/packages/server/src/action.ts +201 -0
- package/packages/server/src/api.ts +143 -0
- package/packages/server/src/index.ts +18 -0
- package/packages/server/src/types.ts +72 -0
- package/packages/server/tsconfig.json +9 -0
- package/pnpm-workspace.yaml +4 -0
- package/scripts/publish.ps1 +138 -0
- package/scripts/publish.sh +142 -0
- package/tsconfig.json +28 -0
- package/turbo.json +24 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel Adapter (Serverless + Edge)
|
|
3
|
+
* Supports: Vercel Serverless Functions & Edge Functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import type { Adapter, AdapterConfig, BuildResult, RouteManifest } from '../types.js';
|
|
9
|
+
import {
|
|
10
|
+
buildClient,
|
|
11
|
+
generateManifest,
|
|
12
|
+
copyPublic,
|
|
13
|
+
analyzeBundle
|
|
14
|
+
} from '../lib/builder.js';
|
|
15
|
+
|
|
16
|
+
export const vercelAdapter: Adapter = {
|
|
17
|
+
name: 'vercel',
|
|
18
|
+
target: 'vercel',
|
|
19
|
+
|
|
20
|
+
async build(config: AdapterConfig): Promise<BuildResult> {
|
|
21
|
+
const outDir = path.resolve(config.outDir || '.vercel/output');;
|
|
22
|
+
const isEdge = config.function?.edge ?? false;
|
|
23
|
+
const warnings: string[] = [];
|
|
24
|
+
const errors: string[] = [];
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Clean output directory
|
|
28
|
+
await fs.rm(outDir, { recursive: true, force: true });
|
|
29
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
// Vercel output structure
|
|
32
|
+
const staticDir = path.join(outDir, 'static');
|
|
33
|
+
const functionsDir = path.join(outDir, 'functions');
|
|
34
|
+
|
|
35
|
+
await fs.mkdir(staticDir, { recursive: true });
|
|
36
|
+
await fs.mkdir(functionsDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
// Build client bundle
|
|
39
|
+
const { files: clientFiles } = await buildClient(config, staticDir);
|
|
40
|
+
|
|
41
|
+
// Copy public assets
|
|
42
|
+
await copyPublic(staticDir);
|
|
43
|
+
|
|
44
|
+
// Build server function
|
|
45
|
+
if (isEdge) {
|
|
46
|
+
await buildEdgeFunction(config, functionsDir);
|
|
47
|
+
} else {
|
|
48
|
+
await buildServerlessFunction(config, functionsDir);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Generate config files
|
|
52
|
+
await generateVercelConfig(config, outDir, isEdge);
|
|
53
|
+
|
|
54
|
+
// Generate manifest
|
|
55
|
+
const routes: RouteManifest[] = (config.static?.routes || ['/']).map(route => ({
|
|
56
|
+
path: route,
|
|
57
|
+
file: route === '/' ? 'index.html' : `${route}.html`,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const assets: Record<string, string> = {};
|
|
61
|
+
for (const file of clientFiles) {
|
|
62
|
+
const basename = path.basename(file);
|
|
63
|
+
assets[basename] = `/${basename}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await generateManifest(config, outDir, routes, assets);
|
|
67
|
+
|
|
68
|
+
// Analyze bundle
|
|
69
|
+
const analysis = await analyzeBundle(outDir);
|
|
70
|
+
|
|
71
|
+
// List all output files
|
|
72
|
+
const allFiles = await listFiles(outDir);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
outDir,
|
|
77
|
+
files: allFiles,
|
|
78
|
+
warnings,
|
|
79
|
+
errors,
|
|
80
|
+
analysis,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
} catch (error) {
|
|
84
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
outDir,
|
|
88
|
+
files: [],
|
|
89
|
+
warnings,
|
|
90
|
+
errors,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async preview(_config: AdapterConfig, port = 3000): Promise<void> {
|
|
96
|
+
const { execSync } = await import('child_process');
|
|
97
|
+
|
|
98
|
+
console.log('Starting Vercel dev server...');
|
|
99
|
+
execSync(`npx vercel dev --port ${port}`, { stdio: 'inherit' });
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async deploy(_config: AdapterConfig): Promise<void> {
|
|
103
|
+
const { execSync } = await import('child_process');
|
|
104
|
+
|
|
105
|
+
console.log('Deploying to Vercel...');
|
|
106
|
+
execSync('npx vercel --prod', { stdio: 'inherit' });
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Build Vercel Edge Function */
|
|
111
|
+
async function buildEdgeFunction(config: AdapterConfig, functionsDir: string): Promise<void> {
|
|
112
|
+
const functionDir = path.join(functionsDir, 'api');
|
|
113
|
+
await fs.mkdir(functionDir, { recursive: true });
|
|
114
|
+
|
|
115
|
+
const entryContent = `
|
|
116
|
+
import { handleRequest } from '@zylaris/server/edge';
|
|
117
|
+
|
|
118
|
+
export const config = {
|
|
119
|
+
runtime: 'edge',
|
|
120
|
+
regions: ${JSON.stringify(config.function?.regions || ['iad1'])},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default async function handler(request) {
|
|
124
|
+
return handleRequest(request, {
|
|
125
|
+
// App configuration
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
// Build edge function
|
|
131
|
+
const { build } = await import('esbuild');
|
|
132
|
+
await build({
|
|
133
|
+
stdin: {
|
|
134
|
+
contents: entryContent,
|
|
135
|
+
resolveDir: process.cwd(),
|
|
136
|
+
},
|
|
137
|
+
bundle: true,
|
|
138
|
+
format: 'esm',
|
|
139
|
+
platform: 'neutral',
|
|
140
|
+
target: 'es2022',
|
|
141
|
+
outfile: path.join(functionDir, 'index.js'),
|
|
142
|
+
external: ['node:*'],
|
|
143
|
+
minify: true,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Build Vercel Serverless Function */
|
|
148
|
+
async function buildServerlessFunction(config: AdapterConfig, functionsDir: string): Promise<void> {
|
|
149
|
+
const functionDir = path.join(functionsDir, 'api');
|
|
150
|
+
await fs.mkdir(functionDir, { recursive: true });
|
|
151
|
+
|
|
152
|
+
// Build server bundle for Node.js
|
|
153
|
+
const { build } = await import('esbuild');
|
|
154
|
+
|
|
155
|
+
const possibleEntries = ['./src/entry-server.ts', './src/entry-server.tsx'];
|
|
156
|
+
const entryPoints: string[] = [];
|
|
157
|
+
for (const e of possibleEntries) {
|
|
158
|
+
try {
|
|
159
|
+
await fs.access(e);
|
|
160
|
+
entryPoints.push(e);
|
|
161
|
+
} catch {
|
|
162
|
+
// skip
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await build({
|
|
167
|
+
entryPoints,
|
|
168
|
+
bundle: true,
|
|
169
|
+
format: 'cjs', // Serverless uses CommonJS
|
|
170
|
+
platform: 'node',
|
|
171
|
+
target: 'node18',
|
|
172
|
+
outfile: path.join(functionDir, 'index.js'),
|
|
173
|
+
external: ['@zylaris/*'],
|
|
174
|
+
minify: true,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Create function config
|
|
178
|
+
const funcConfig = {
|
|
179
|
+
maxDuration: config.function?.timeout || 10,
|
|
180
|
+
memory: config.function?.memory || 1024,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
await fs.writeFile(path.join(functionDir, '.vc-config.json'), JSON.stringify(funcConfig, null, 2));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Generate Vercel config files */
|
|
187
|
+
async function generateVercelConfig(
|
|
188
|
+
_config: AdapterConfig,
|
|
189
|
+
outDir: string,
|
|
190
|
+
isEdge: boolean
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
// config.json
|
|
193
|
+
const vercelConfig = {
|
|
194
|
+
version: 3,
|
|
195
|
+
routes: [
|
|
196
|
+
// API routes
|
|
197
|
+
{ src: '/api/(.*)', dest: '/api' },
|
|
198
|
+
// Static assets
|
|
199
|
+
{ src: '/_assets/(.*)', headers: { 'cache-control': 'public, max-age=31536000, immutable' } },
|
|
200
|
+
// SPA fallback
|
|
201
|
+
{ handle: 'filesystem' },
|
|
202
|
+
{ src: '/(.*)', dest: isEdge ? '/api' : '/index.html' },
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
await fs.writeFile(path.join(outDir, 'config.json'), JSON.stringify(vercelConfig, null, 2));
|
|
207
|
+
|
|
208
|
+
// Project config (for CLI)
|
|
209
|
+
const projectConfig = {
|
|
210
|
+
buildCommand: null,
|
|
211
|
+
outputDirectory: '.vercel/output',
|
|
212
|
+
framework: null,
|
|
213
|
+
installCommand: null,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
await fs.mkdir('.vercel', { recursive: true });
|
|
217
|
+
await fs.writeFile('.vercel/project.json', JSON.stringify(projectConfig, null, 2));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** List all files recursively */
|
|
221
|
+
async function listFiles(dir: string, prefix = ''): Promise<string[]> {
|
|
222
|
+
const files: string[] = [];
|
|
223
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
224
|
+
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
const fullPath = path.join(dir, entry.name);
|
|
227
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
228
|
+
|
|
229
|
+
if ((entry as { isDirectory(): boolean }).isDirectory()) {
|
|
230
|
+
files.push(...await listFiles(fullPath, relativePath));
|
|
231
|
+
} else {
|
|
232
|
+
files.push(relativePath);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return files;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export default vercelAdapter;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zylaris Adapter - Universal deployment for every hosting platform
|
|
3
|
+
*
|
|
4
|
+
* Supported Platforms:
|
|
5
|
+
* - Static: GitHub Pages, Surge.sh, AWS S3, etc.
|
|
6
|
+
* - Node.js: VPS, Docker, PM2, Railway, Render
|
|
7
|
+
* - Vercel: Serverless + Edge Functions
|
|
8
|
+
* - Netlify: Functions + Edge Functions
|
|
9
|
+
* - Cloudflare: Pages + Workers
|
|
10
|
+
* - Deno: Deno Deploy
|
|
11
|
+
* - Bun: Bun runtime
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
Adapter,
|
|
16
|
+
AdapterConfig,
|
|
17
|
+
BuildResult,
|
|
18
|
+
DeploymentTarget,
|
|
19
|
+
DeploymentManifest,
|
|
20
|
+
RouteManifest,
|
|
21
|
+
FunctionConfig,
|
|
22
|
+
StaticOptions,
|
|
23
|
+
RequestHandler,
|
|
24
|
+
EdgeContext,
|
|
25
|
+
PlatformConfig,
|
|
26
|
+
} from './types.js';
|
|
27
|
+
|
|
28
|
+
// Adapters
|
|
29
|
+
export { staticAdapter } from './adapters/static.js';
|
|
30
|
+
export { nodeAdapter } from './adapters/node.js';
|
|
31
|
+
export { vercelAdapter } from './adapters/vercel.js';
|
|
32
|
+
export { netlifyAdapter } from './adapters/netlify.js';
|
|
33
|
+
export { cloudflareAdapter } from './adapters/cloudflare.js';
|
|
34
|
+
export { denoAdapter } from './adapters/deno.js';
|
|
35
|
+
export { bunAdapter } from './adapters/bun.js';
|
|
36
|
+
|
|
37
|
+
import type { Adapter, DeploymentTarget } from './types.js';
|
|
38
|
+
import { staticAdapter } from './adapters/static.js';
|
|
39
|
+
import { nodeAdapter } from './adapters/node.js';
|
|
40
|
+
import { vercelAdapter } from './adapters/vercel.js';
|
|
41
|
+
import { netlifyAdapter } from './adapters/netlify.js';
|
|
42
|
+
import { cloudflareAdapter } from './adapters/cloudflare.js';
|
|
43
|
+
import { denoAdapter } from './adapters/deno.js';
|
|
44
|
+
import { bunAdapter } from './adapters/bun.js';
|
|
45
|
+
|
|
46
|
+
/** Map of all adapters */
|
|
47
|
+
export const adapters: Record<DeploymentTarget, Adapter> = {
|
|
48
|
+
static: staticAdapter,
|
|
49
|
+
node: nodeAdapter,
|
|
50
|
+
vercel: vercelAdapter,
|
|
51
|
+
netlify: netlifyAdapter,
|
|
52
|
+
cloudflare: cloudflareAdapter,
|
|
53
|
+
deno: denoAdapter,
|
|
54
|
+
bun: bunAdapter,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Get adapter by target */
|
|
58
|
+
export function getAdapter(target: DeploymentTarget): Adapter {
|
|
59
|
+
const adapter = adapters[target];
|
|
60
|
+
if (!adapter) {
|
|
61
|
+
throw new Error(`Unknown adapter target: ${target}. Available: ${Object.keys(adapters).join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
return adapter;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** List all available adapters */
|
|
67
|
+
export function listAdapters(): Array<{ target: DeploymentTarget; name: string; description: string }> {
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
target: 'static',
|
|
71
|
+
name: 'Static',
|
|
72
|
+
description: 'GitHub Pages, Surge.sh, AWS S3, Cloudflare Pages Static, Netlify Static'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
target: 'node',
|
|
76
|
+
name: 'Node.js',
|
|
77
|
+
description: 'VPS, Docker, PM2, Railway, Render, traditional Node.js hosting'
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
target: 'vercel',
|
|
81
|
+
name: 'Vercel',
|
|
82
|
+
description: 'Vercel Serverless Functions & Edge Functions'
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
target: 'netlify',
|
|
86
|
+
name: 'Netlify',
|
|
87
|
+
description: 'Netlify Functions & Edge Functions'
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
target: 'cloudflare',
|
|
91
|
+
name: 'Cloudflare',
|
|
92
|
+
description: 'Cloudflare Pages & Workers'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
target: 'deno',
|
|
96
|
+
name: 'Deno',
|
|
97
|
+
description: 'Deno Deploy edge runtime'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
target: 'bun',
|
|
101
|
+
name: 'Bun',
|
|
102
|
+
description: 'Bun runtime (experimental)'
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Check if adapter supports SSR */
|
|
108
|
+
export function supportsSSR(target: DeploymentTarget): boolean {
|
|
109
|
+
return target !== 'static';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Check if adapter supports Edge runtime */
|
|
113
|
+
export function supportsEdge(target: DeploymentTarget): boolean {
|
|
114
|
+
return ['vercel', 'netlify', 'cloudflare', 'deno'].includes(target);
|
|
115
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal build utilities for all adapters
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { build } from 'esbuild';
|
|
6
|
+
import * as fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
|
|
10
|
+
/** Check if path exists */
|
|
11
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
await fs.access(path);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
AdapterConfig,
|
|
22
|
+
BuildResult,
|
|
23
|
+
DeploymentManifest,
|
|
24
|
+
RouteManifest
|
|
25
|
+
} from '../types.js';
|
|
26
|
+
|
|
27
|
+
/** Recursively copy directory */
|
|
28
|
+
async function copyDir(src: string, dest: string): Promise<void> {
|
|
29
|
+
await fs.mkdir(dest, { recursive: true });
|
|
30
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const srcPath = path.join(src, entry.name);
|
|
33
|
+
const destPath = path.join(dest, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
await copyDir(srcPath, destPath);
|
|
36
|
+
} else {
|
|
37
|
+
await fs.copyFile(srcPath, destPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Default build options */
|
|
43
|
+
const defaultBuildOptions = {
|
|
44
|
+
bundle: true,
|
|
45
|
+
splitting: true,
|
|
46
|
+
format: 'esm' as const,
|
|
47
|
+
target: 'es2022',
|
|
48
|
+
minify: true,
|
|
49
|
+
sourcemap: true,
|
|
50
|
+
treeShaking: true,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Build client bundle */
|
|
54
|
+
export async function buildClient(
|
|
55
|
+
config: AdapterConfig,
|
|
56
|
+
outDir: string
|
|
57
|
+
): Promise<{ files: string[]; size: number }> {
|
|
58
|
+
const clientDir = path.join(outDir, 'client');
|
|
59
|
+
await fs.mkdir(clientDir, { recursive: true });
|
|
60
|
+
|
|
61
|
+
const possibleEntries = ['./src/entry-client.tsx', './src/entry-client.ts', './src/main.tsx', './src/main.ts'];
|
|
62
|
+
const existingEntries: string[] = [];
|
|
63
|
+
for (const e of possibleEntries) {
|
|
64
|
+
try {
|
|
65
|
+
await fs.access(e);
|
|
66
|
+
existingEntries.push(e);
|
|
67
|
+
} catch {
|
|
68
|
+
// skip
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const entryPoints = config.entry
|
|
72
|
+
? [config.entry]
|
|
73
|
+
: existingEntries;
|
|
74
|
+
|
|
75
|
+
if (entryPoints.length === 0) {
|
|
76
|
+
throw new Error('No client entry point found');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = await build({
|
|
80
|
+
...defaultBuildOptions,
|
|
81
|
+
...config.esbuild,
|
|
82
|
+
entryPoints,
|
|
83
|
+
outdir: clientDir,
|
|
84
|
+
platform: 'browser',
|
|
85
|
+
splitting: true,
|
|
86
|
+
format: 'esm',
|
|
87
|
+
metafile: true,
|
|
88
|
+
define: {
|
|
89
|
+
'process.env.NODE_ENV': '"production"',
|
|
90
|
+
'import.meta.env.SSR': 'false',
|
|
91
|
+
...config.esbuild?.define,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const files = Object.keys(result.metafile?.outputs || {});
|
|
96
|
+
const totalSize = files.reduce((sum, f) => sum + (result.metafile?.outputs[f]?.bytes || 0), 0);
|
|
97
|
+
|
|
98
|
+
return { files, size: totalSize };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Build server bundle */
|
|
102
|
+
export async function buildServer(
|
|
103
|
+
config: AdapterConfig,
|
|
104
|
+
outDir: string,
|
|
105
|
+
platform: 'node' | 'neutral' = 'node'
|
|
106
|
+
): Promise<{ files: string[]; size: number; entry: string }> {
|
|
107
|
+
const serverDir = path.join(outDir, 'server');
|
|
108
|
+
await fs.mkdir(serverDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
const possibleEntries = ['./src/entry-server.tsx', './src/entry-server.ts'];
|
|
111
|
+
const entryPoints: string[] = [];
|
|
112
|
+
for (const e of possibleEntries) {
|
|
113
|
+
try {
|
|
114
|
+
await fs.access(e);
|
|
115
|
+
entryPoints.push(e);
|
|
116
|
+
} catch {
|
|
117
|
+
// skip
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (entryPoints.length === 0) {
|
|
122
|
+
throw new Error('No server entry point found');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const result = await build({
|
|
126
|
+
...defaultBuildOptions,
|
|
127
|
+
...config.esbuild,
|
|
128
|
+
entryPoints,
|
|
129
|
+
outdir: serverDir,
|
|
130
|
+
platform,
|
|
131
|
+
splitting: false,
|
|
132
|
+
format: 'esm',
|
|
133
|
+
metafile: true,
|
|
134
|
+
external: ['node:*', ...Object.keys((await getPackageJson()).dependencies || {})],
|
|
135
|
+
define: {
|
|
136
|
+
'process.env.NODE_ENV': '"production"',
|
|
137
|
+
'import.meta.env.SSR': 'true',
|
|
138
|
+
...config.esbuild?.define,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const files = Object.keys(result.metafile?.outputs || {});
|
|
143
|
+
const entry = files.find(f => !f.endsWith('.map')) || files[0];
|
|
144
|
+
const totalSize = files.reduce((sum, f) => sum + (result.metafile?.outputs[f]?.bytes || 0), 0);
|
|
145
|
+
|
|
146
|
+
return { files, size: totalSize, entry };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Generate static pages */
|
|
150
|
+
export async function generateStaticPages(
|
|
151
|
+
config: AdapterConfig,
|
|
152
|
+
outDir: string,
|
|
153
|
+
clientFiles: string[]
|
|
154
|
+
): Promise<string[]> {
|
|
155
|
+
const staticDir = path.join(outDir, 'static');
|
|
156
|
+
await fs.mkdir(staticDir, { recursive: true });
|
|
157
|
+
|
|
158
|
+
const routes = config.static?.routes || ['/'];
|
|
159
|
+
const generated: string[] = [];
|
|
160
|
+
|
|
161
|
+
// Generate HTML for each route
|
|
162
|
+
for (const route of routes) {
|
|
163
|
+
const html = await renderRoute(route, clientFiles, config);
|
|
164
|
+
const filePath = routeToFilePath(route, config);
|
|
165
|
+
const fullPath = path.join(staticDir, filePath);
|
|
166
|
+
|
|
167
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
168
|
+
await fs.writeFile(fullPath, html);
|
|
169
|
+
generated.push(filePath);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Copy client assets
|
|
173
|
+
const clientDir = path.join(outDir, 'client');
|
|
174
|
+
if (await pathExists(clientDir)) {
|
|
175
|
+
await copyDir(clientDir, path.join(staticDir, '_assets'));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return generated;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Render a route to HTML */
|
|
182
|
+
async function renderRoute(
|
|
183
|
+
_route: string,
|
|
184
|
+
clientFiles: string[],
|
|
185
|
+
config: AdapterConfig
|
|
186
|
+
): Promise<string> {
|
|
187
|
+
const jsFiles = clientFiles.filter(f => f.endsWith('.js'));
|
|
188
|
+
const cssFiles = clientFiles.filter(f => f.endsWith('.css'));
|
|
189
|
+
const base = config.static?.base || '';
|
|
190
|
+
|
|
191
|
+
const jsTags = jsFiles
|
|
192
|
+
.map(f => `<script type="module" src="${base}/_assets/${path.basename(f)}"></script>`)
|
|
193
|
+
.join('\n ');
|
|
194
|
+
|
|
195
|
+
const cssTags = cssFiles
|
|
196
|
+
.map(f => `<link rel="stylesheet" href="${base}/_assets/${path.basename(f)}">`)
|
|
197
|
+
.join('\n ');
|
|
198
|
+
|
|
199
|
+
return `<!DOCTYPE html>
|
|
200
|
+
<html lang="en">
|
|
201
|
+
<head>
|
|
202
|
+
<meta charset="UTF-8">
|
|
203
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
204
|
+
<title>Zylaris App</title>
|
|
205
|
+
${cssTags}
|
|
206
|
+
</head>
|
|
207
|
+
<body>
|
|
208
|
+
<div id="app"></div>
|
|
209
|
+
${jsTags}
|
|
210
|
+
</body>
|
|
211
|
+
</html>`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Convert route to file path */
|
|
215
|
+
function routeToFilePath(route: string, config: AdapterConfig): string {
|
|
216
|
+
const cleanUrls = config.cleanUrls ?? true;
|
|
217
|
+
const trailingSlash = config.trailingSlash ?? 'ignore';
|
|
218
|
+
|
|
219
|
+
let path = route === '/' ? '/index' : route;
|
|
220
|
+
|
|
221
|
+
if (trailingSlash === 'always' && !path.endsWith('/')) {
|
|
222
|
+
path += '/';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (cleanUrls) {
|
|
226
|
+
return path + (path.endsWith('/') ? 'index.html' : '.html');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return path + '.html';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Generate deployment manifest */
|
|
233
|
+
export async function generateManifest(
|
|
234
|
+
_config: AdapterConfig,
|
|
235
|
+
outDir: string,
|
|
236
|
+
routes: RouteManifest[],
|
|
237
|
+
assets: Record<string, string>
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const manifest: DeploymentManifest = {
|
|
240
|
+
version: '0.1.0',
|
|
241
|
+
timestamp: new Date().toISOString(),
|
|
242
|
+
routes,
|
|
243
|
+
assets,
|
|
244
|
+
entry: {
|
|
245
|
+
client: '/_assets/entry-client.js',
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
await fs.writeFile(
|
|
250
|
+
path.join(outDir, 'manifest.json'),
|
|
251
|
+
JSON.stringify(manifest, null, 2)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Copy public directory */
|
|
256
|
+
export async function copyPublic(outDir: string): Promise<void> {
|
|
257
|
+
if (await pathExists('public')) {
|
|
258
|
+
await copyDir('public', outDir);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Generate HTTP headers file */
|
|
263
|
+
export async function generateHeaders(
|
|
264
|
+
config: AdapterConfig,
|
|
265
|
+
outDir: string
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
if (!config.headers) return;
|
|
268
|
+
|
|
269
|
+
const headersFile = Object.entries(config.headers)
|
|
270
|
+
.map(([pattern, header]) => `${pattern}\n ${header}`)
|
|
271
|
+
.join('\n');
|
|
272
|
+
|
|
273
|
+
await fs.writeFile(path.join(outDir, '_headers'), headersFile);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Generate redirects file */
|
|
277
|
+
export async function generateRedirects(
|
|
278
|
+
config: AdapterConfig,
|
|
279
|
+
outDir: string
|
|
280
|
+
): Promise<void> {
|
|
281
|
+
if (!config.redirects?.length) return;
|
|
282
|
+
|
|
283
|
+
const redirects = config.redirects
|
|
284
|
+
.map(r => `${r.from} ${r.to} ${r.status || 301}`)
|
|
285
|
+
.join('\n');
|
|
286
|
+
|
|
287
|
+
await fs.writeFile(path.join(outDir, '_redirects'), redirects);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Get package.json */
|
|
291
|
+
async function getPackageJson(): Promise<{ dependencies?: Record<string, string> }> {
|
|
292
|
+
try {
|
|
293
|
+
const content = await fs.readFile('package.json', 'utf-8');
|
|
294
|
+
return JSON.parse(content);
|
|
295
|
+
} catch {
|
|
296
|
+
return {};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Create hash for cache busting */
|
|
301
|
+
export function createContentHash(content: string, length = 8): string {
|
|
302
|
+
return createHash('md5').update(content).digest('hex').slice(0, length);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Calculate gzip size */
|
|
306
|
+
export async function calculateGzipSize(filePath: string): Promise<number> {
|
|
307
|
+
const { gzipSync } = await import('zlib');
|
|
308
|
+
const content = await fs.readFile(filePath);
|
|
309
|
+
return gzipSync(content).length;
|
|
310
|
+
return gzipSync(content).length;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Analyze bundle output */
|
|
314
|
+
export async function analyzeBundle(
|
|
315
|
+
outDir: string
|
|
316
|
+
): Promise<BuildResult['analysis']> {
|
|
317
|
+
const clientDir = path.join(outDir, 'client');
|
|
318
|
+
const serverDir = path.join(outDir, 'server');
|
|
319
|
+
|
|
320
|
+
let totalSize = 0;
|
|
321
|
+
let clientSize = 0;
|
|
322
|
+
let serverSize = 0;
|
|
323
|
+
const assets: Array<{ file: string; size: number; gzip: number }> = [];
|
|
324
|
+
|
|
325
|
+
// Analyze client assets
|
|
326
|
+
if (await pathExists(clientDir)) {
|
|
327
|
+
const files = await fs.readdir(clientDir, { recursive: true }) as string[];
|
|
328
|
+
for (const file of files.filter((f: string) => !f.endsWith('.map'))) {
|
|
329
|
+
const filePath = path.join(clientDir, file);
|
|
330
|
+
const stats = await fs.stat(filePath);
|
|
331
|
+
if (stats.isFile()) {
|
|
332
|
+
const size = stats.size;
|
|
333
|
+
const gzip = await calculateGzipSize(filePath);
|
|
334
|
+
totalSize += size;
|
|
335
|
+
clientSize += size;
|
|
336
|
+
assets.push({ file: `client/${file}`, size, gzip });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Analyze server assets
|
|
342
|
+
if (await pathExists(serverDir)) {
|
|
343
|
+
const files = await fs.readdir(serverDir, { recursive: true }) as string[];
|
|
344
|
+
for (const file of files.filter((f: string) => !f.endsWith('.map'))) {
|
|
345
|
+
const filePath = path.join(serverDir, file);
|
|
346
|
+
const stats = await fs.stat(filePath);
|
|
347
|
+
if (stats.isFile()) {
|
|
348
|
+
const size = stats.size;
|
|
349
|
+
serverSize += size;
|
|
350
|
+
totalSize += size;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
totalSize,
|
|
357
|
+
clientSize,
|
|
358
|
+
serverSize,
|
|
359
|
+
assets: assets.sort((a, b) => b.size - a.size),
|
|
360
|
+
};
|
|
361
|
+
}
|