@tschk/moonshine-deploy-vercel 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +8 -8
  2. package/src/index.ts +108 -60
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tschk/moonshine-deploy-vercel",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -15,16 +15,16 @@
15
15
  "test": "bun test"
16
16
  },
17
17
  "dependencies": {
18
- "@tschk/moonshine-adapter-conformance": "^0.3.5",
19
- "@tschk/moonshine-compiler": "^0.3.5",
20
- "@tschk/moonshine-framework": "^0.3.5",
21
- "@tschk/moonshine-react": "^0.3.5",
22
- "@tschk/moonshine-router": "^0.3.5",
23
- "@tschk/moonshine-server": "^0.3.5"
18
+ "@tschk/moonshine-compiler": "^0.3.6",
19
+ "@tschk/moonshine-framework": "^0.3.6",
20
+ "@tschk/moonshine-react": "^0.3.6",
21
+ "@tschk/moonshine-router": "^0.3.6",
22
+ "@tschk/moonshine-server": "^0.3.6"
24
23
  },
25
24
  "devDependencies": {
26
25
  "@types/bun": "1.3.14",
27
- "typescript": "7.0.2"
26
+ "typescript": "7.0.2",
27
+ "@tschk/moonshine-adapter-conformance": "^0.3.6"
28
28
  },
29
29
  "files": [
30
30
  "src"
package/src/index.ts CHANGED
@@ -6,11 +6,9 @@ import type {
6
6
  DeploymentAdapter,
7
7
  MoonshineManifest,
8
8
  RouteArtifact,
9
- } from "@tschk/moonshine-framework";
10
- import type {
11
9
  Harness,
12
10
  HarnessFactory,
13
- } from "@tschk/moonshine-adapter-conformance";
11
+ } from "@tschk/moonshine-framework";
14
12
 
15
13
  type VercelRoute = {
16
14
  src: string;
@@ -63,19 +61,21 @@ async function copyAssets(
63
61
  assets: MoonshineManifest["assets"],
64
62
  outDir: string,
65
63
  ): Promise<void> {
66
- for (const asset of assets) {
67
- const source = isAbsolute(asset.file)
68
- ? asset.file
69
- : resolve(outDir, asset.file);
70
- const target = resolve(
71
- outDir,
72
- (asset.path ?? asset.file).replace(/^\/+/, ""),
73
- );
74
- await mkdir(dirname(target), { recursive: true });
75
- if (source !== target) {
76
- await copyFile(source, target);
77
- }
78
- }
64
+ await Promise.all(
65
+ assets.map(async (asset) => {
66
+ const source = isAbsolute(asset.file)
67
+ ? asset.file
68
+ : resolve(outDir, asset.file);
69
+ const target = resolve(
70
+ outDir,
71
+ (asset.path ?? asset.file).replace(/^\/+/, ""),
72
+ );
73
+ await mkdir(dirname(target), { recursive: true });
74
+ if (source !== target) {
75
+ await copyFile(source, target);
76
+ }
77
+ }),
78
+ );
79
79
  }
80
80
 
81
81
  function validateRuntime(
@@ -114,9 +114,45 @@ function routeToRegex(path: string): string {
114
114
  .replace(/:([^/]+)/g, "(?<$1>[^/]+)")}$`;
115
115
  }
116
116
 
117
- function staticSlug(path: string): string {
117
+ function staticSlug(path: string, usedSlugs: Set<string>): string {
118
118
  if (path === "/") return "index";
119
- return path.replace(/^\/+/, "").replace(/\//g, "-");
119
+ let slug = path.replace(/^\/+/, "").replace(/\//g, "-");
120
+ if (!usedSlugs.has(slug)) {
121
+ usedSlugs.add(slug);
122
+ return slug;
123
+ }
124
+ let i = 1;
125
+ while (usedSlugs.has(`${slug}-${i}`)) i++;
126
+ usedSlugs.add(`${slug}-${i}`);
127
+ return `${slug}-${i}`;
128
+ }
129
+
130
+ async function mapConcurrent<T, R>(
131
+ items: T[],
132
+ fn: (item: T) => Promise<R>,
133
+ concurrency: number,
134
+ ): Promise<R[]> {
135
+ const results = new Array<R>(items.length);
136
+ const errors: unknown[] = [];
137
+ let index = 0;
138
+
139
+ const worker = async () => {
140
+ while (index < items.length) {
141
+ const i = index++;
142
+ try {
143
+ results[i] = await fn(items[i]);
144
+ } catch (e) {
145
+ errors.push(e);
146
+ }
147
+ }
148
+ };
149
+
150
+ await Promise.all(
151
+ Array.from({ length: Math.min(concurrency, items.length) }, worker),
152
+ );
153
+
154
+ if (errors.length > 0) throw errors[0];
155
+ return results;
120
156
  }
121
157
 
122
158
  async function prerenderRoute(route: RouteArtifact): Promise<string> {
@@ -155,31 +191,38 @@ export const vercelAdapter: DeploymentAdapter = {
155
191
  await mkdir(functionsDir, { recursive: true });
156
192
 
157
193
  const config: VercelConfig = { version: 3, routes: [] };
194
+ const usedSlugs = new Set<string>();
158
195
 
159
- for (const route of resolvedManifest.routes) {
160
- if (route.mode === "static" && !route.path.includes("*")) {
161
- const html = await prerenderRoute(route);
162
- const slug = staticSlug(route.path);
163
- await writeFile(resolve(staticDir, `${slug}.html`), html);
164
- config.routes.push({
165
- src: routeToRegex(route.path),
166
- dest: `/${slug}.html`,
167
- });
168
- } else {
169
- const funcDir = resolve(functionsDir, `${route.id}.func`);
170
- await mkdir(funcDir, { recursive: true });
196
+ const routesConfig = await mapConcurrent(
197
+ resolvedManifest.routes,
198
+ async (route) => {
199
+ if (route.mode === "static" && !route.path.includes("*")) {
200
+ const html = await prerenderRoute(route);
201
+ const slug = staticSlug(route.path, usedSlugs);
202
+ await writeFile(resolve(staticDir, `${slug}.html`), html);
203
+ return {
204
+ src: routeToRegex(route.path),
205
+ dest: `/${slug}.html`,
206
+ };
207
+ } else {
208
+ const funcDir = resolve(functionsDir, `${route.id}.func`);
209
+ await mkdir(funcDir, { recursive: true });
171
210
 
172
- const runtime = route.runtime === "vercel-edge" ? "edge" : "nodejs20.x";
173
- const vcConfig = { runtime, handler: "index.ts" };
174
- if (runtime !== "edge") {
175
- (vcConfig as Record<string, string>).launcherType = "Nodejs";
176
- }
177
- await writeFile(
178
- resolve(funcDir, ".vc-config.json"),
179
- JSON.stringify(vcConfig, null, 2) + "\n",
180
- );
211
+ const runtime =
212
+ route.runtime === "vercel-edge" ? "edge" : "nodejs20.x";
213
+ const vcConfig: Record<string, string> = {
214
+ runtime,
215
+ handler: "index.ts",
216
+ };
217
+ if (runtime !== "edge") {
218
+ vcConfig.launcherType = "Nodejs";
219
+ }
220
+ await writeFile(
221
+ resolve(funcDir, ".vc-config.json"),
222
+ JSON.stringify(vcConfig, null, 2) + "\n",
223
+ );
181
224
 
182
- const functionEntry = `import { createRequestHandler } from "@tschk/moonshine-server";
225
+ const functionEntry = `import { createRequestHandler } from "@tschk/moonshine-server";
183
226
  import { reactRenderer } from "@tschk/moonshine-react";
184
227
  import manifest from "../../manifest.json" with { type: "json" };
185
228
 
@@ -187,28 +230,33 @@ const fetch = createRequestHandler({ manifest, renderer: reactRenderer });
187
230
 
188
231
  export default { fetch };
189
232
  `;
190
- await writeFile(resolve(funcDir, "index.ts"), functionEntry);
233
+ await writeFile(resolve(funcDir, "index.ts"), functionEntry);
191
234
 
192
- config.routes.push({
193
- src: routeToRegex(route.path),
194
- dest: `/${route.id}`,
195
- });
196
- }
197
- }
235
+ return {
236
+ src: routeToRegex(route.path),
237
+ dest: `/${route.id}`,
238
+ };
239
+ }
240
+ },
241
+ 20,
242
+ );
243
+ config.routes.push(...routesConfig);
198
244
 
199
- for (const asset of resolvedManifest.assets) {
200
- const source = isAbsolute(asset.file)
201
- ? asset.file
202
- : resolve(outDir, asset.file);
203
- const target = resolve(
204
- staticDir,
205
- (asset.path ?? asset.file).replace(/^\/+/, ""),
206
- );
207
- await mkdir(dirname(target), { recursive: true });
208
- if (source !== target) {
209
- await copyFile(source, target);
210
- }
211
- }
245
+ await Promise.all(
246
+ resolvedManifest.assets.map(async (asset) => {
247
+ const source = isAbsolute(asset.file)
248
+ ? asset.file
249
+ : resolve(outDir, asset.file);
250
+ const target = resolve(
251
+ staticDir,
252
+ (asset.path ?? asset.file).replace(/^\/+/, ""),
253
+ );
254
+ await mkdir(dirname(target), { recursive: true });
255
+ if (source !== target) {
256
+ await copyFile(source, target);
257
+ }
258
+ }),
259
+ );
212
260
 
213
261
  await writeFile(
214
262
  resolve(vercelOutput, "manifest.json"),