@unieojs/unio-evjs-adapter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/emit.ts ADDED
@@ -0,0 +1,384 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import type {
5
+ AdapterBuildResult,
6
+ CapabilityStatus,
7
+ ClientManifest,
8
+ EmitUboaOptions,
9
+ PageManifestEntry,
10
+ ServerManifest,
11
+ UboaArtifact,
12
+ UboaCapabilities,
13
+ UboaMiddleware,
14
+ UboaRoute,
15
+ UboaServerFunctionDescriptor,
16
+ } from "./types.js";
17
+ import {
18
+ assertClientManifest,
19
+ assertServerManifest,
20
+ } from "./manifest.js";
21
+ import {
22
+ isRecord,
23
+ normalizeRoutePath,
24
+ pathExists,
25
+ routeId,
26
+ toPosix,
27
+ writeJson,
28
+ } from "./utils.js";
29
+
30
+ const ADAPTER_PACKAGE = "@unieojs/unio-evjs-adapter";
31
+ const UBOA_VERSION = "1.0.0";
32
+ const DEFAULT_SERVER_FUNCTION = "evjs";
33
+ const DEFAULT_SERVER_ENDPOINT = "/api/fn";
34
+
35
+ interface ResolvedManifests {
36
+ clientManifest: ClientManifest;
37
+ serverManifest?: ServerManifest;
38
+ clientRoot: string;
39
+ serverRoot: string;
40
+ }
41
+
42
+ type ServerManifestWithEntry = ServerManifest & { entry: string };
43
+
44
+ function outputRoot(cwd: string, outputDir?: string): string {
45
+ return path.resolve(cwd, outputDir ?? ".unio/output");
46
+ }
47
+
48
+ async function resolveBuildInputs(
49
+ options: EmitUboaOptions,
50
+ ): Promise<ResolvedManifests> {
51
+ const cwd = path.resolve(options.cwd ?? process.cwd());
52
+ const clientManifest = options.clientManifest;
53
+ const serverManifest = options.serverManifest;
54
+
55
+ if (!clientManifest) {
56
+ throw new Error(
57
+ "EVJS client manifest must be provided from @evjs/ev buildEnd metadata.",
58
+ );
59
+ }
60
+ assertClientManifest(clientManifest);
61
+ if (serverManifest) {
62
+ assertServerManifest(serverManifest);
63
+ }
64
+
65
+ return {
66
+ clientManifest,
67
+ serverManifest,
68
+ clientRoot: await resolveClientRoot(cwd, clientManifest, serverManifest),
69
+ serverRoot: path.join(cwd, "dist/server"),
70
+ };
71
+ }
72
+
73
+ async function resolveClientRoot(
74
+ cwd: string,
75
+ clientManifest: ClientManifest,
76
+ serverManifest: ServerManifest | undefined,
77
+ ): Promise<string> {
78
+ const fullstackClientRoot = path.join(cwd, "dist/client");
79
+ const flatClientRoot = path.join(cwd, "dist");
80
+
81
+ if (
82
+ serverManifest ||
83
+ (
84
+ clientManifest.pages &&
85
+ Object.keys(clientManifest.pages).length > 0
86
+ )
87
+ ) {
88
+ return fullstackClientRoot;
89
+ }
90
+
91
+ if (await pathExists(path.join(flatClientRoot, "index.html"))) {
92
+ return flatClientRoot;
93
+ }
94
+
95
+ return await pathExists(fullstackClientRoot)
96
+ ? fullstackClientRoot
97
+ : flatClientRoot;
98
+ }
99
+
100
+ async function copyIfExists(source: string, dest: string): Promise<boolean> {
101
+ if (!(await pathExists(source))) return false;
102
+ await fs.mkdir(path.dirname(dest), { recursive: true });
103
+ await fs.cp(source, dest, { recursive: true });
104
+ return true;
105
+ }
106
+
107
+ function buildContentVersion(): string {
108
+ const parts = [
109
+ process.env.SPRINT_ID ? `SPRINT=${process.env.SPRINT_ID}` : undefined,
110
+ process.env.COMMIT_ID ? `COMMIT=${process.env.COMMIT_ID}` : undefined,
111
+ ].filter(Boolean);
112
+
113
+ return parts.length > 0 ? parts.join(", ") : "local";
114
+ }
115
+
116
+ function hasServerEntry(
117
+ manifest: ServerManifest | undefined,
118
+ ): manifest is ServerManifestWithEntry {
119
+ return Boolean(manifest?.entry);
120
+ }
121
+
122
+ function hasRscMetadata(manifest: ServerManifest | undefined): boolean {
123
+ if (!isRecord(manifest) || !isRecord(manifest.rsc)) return false;
124
+ return Object.keys(manifest.rsc).length > 0;
125
+ }
126
+
127
+ function buildCapabilities(
128
+ clientManifest: ClientManifest,
129
+ serverManifest: ServerManifest | undefined,
130
+ ): UboaCapabilities {
131
+ const serverEnabled = hasServerEntry(serverManifest);
132
+ const hasFns = serverEnabled && Object.keys(serverManifest.fns).length > 0;
133
+
134
+ let serverFunctionIntrospection: CapabilityStatus = "unsupported";
135
+ if (serverEnabled) {
136
+ serverFunctionIntrospection = hasFns ? "supported" : "degraded";
137
+ }
138
+
139
+ const rsc: CapabilityStatus = hasRscMetadata(serverManifest)
140
+ ? "partial"
141
+ : "unsupported";
142
+
143
+ return {
144
+ static: clientManifest ? "supported" : "unsupported",
145
+ routes: "supported",
146
+ serverlessFunction: serverEnabled ? "supported" : "unsupported",
147
+ serverFunctionIntrospection,
148
+ restRouteIntrospection: serverEnabled ? "degraded" : "unsupported",
149
+ edgeMiddleware: "unsupported",
150
+ rsc,
151
+ imageOptimization: "unsupported",
152
+ };
153
+ }
154
+
155
+ function staticRoutesForRoutes(
156
+ routes: Array<{ path: string }> | undefined,
157
+ entry: string,
158
+ ): UboaRoute[] {
159
+ const sourceRoutes =
160
+ routes && routes.length > 0 ? routes : [{ path: "/" }, { path: "*" }];
161
+ const seen = new Set<string>();
162
+ const result: UboaRoute[] = [];
163
+
164
+ for (const route of sourceRoutes) {
165
+ const match = normalizeRoutePath(route.path);
166
+ const key = `${match}:${entry}`;
167
+ if (seen.has(key)) continue;
168
+ seen.add(key);
169
+ result.push({
170
+ id: routeId("static", match),
171
+ match,
172
+ type: "static",
173
+ entry,
174
+ });
175
+ }
176
+
177
+ return result;
178
+ }
179
+
180
+ function pageEntry(pageName: string): string {
181
+ return `static/${pageName}.html`;
182
+ }
183
+
184
+ function pageRoutes(
185
+ pages: Record<string, PageManifestEntry>,
186
+ ): UboaRoute[] {
187
+ const routes: UboaRoute[] = [];
188
+
189
+ for (const pageName of Object.keys(pages)) {
190
+ const match = pageName === "index" ? "/" : normalizeRoutePath(pageName);
191
+ routes.push({
192
+ id: routeId("static", match),
193
+ match,
194
+ type: "static",
195
+ entry: pageEntry(pageName),
196
+ });
197
+ }
198
+
199
+ return routes;
200
+ }
201
+
202
+ function buildStaticRoutes(clientManifest: ClientManifest): UboaRoute[] {
203
+ if (clientManifest.pages && Object.keys(clientManifest.pages).length > 0) {
204
+ return pageRoutes(clientManifest.pages);
205
+ }
206
+
207
+ return staticRoutesForRoutes(clientManifest.routes, "static/index.html");
208
+ }
209
+
210
+ function serverRoutes(serverFunctionName: string, endpoint: string): UboaRoute[] {
211
+ return [
212
+ {
213
+ id: "server-evjs-rpc",
214
+ match: endpoint,
215
+ type: "serverFunction",
216
+ serverFunction: serverFunctionName,
217
+ methods: ["POST"],
218
+ },
219
+ {
220
+ id: "server-evjs-api-fallback",
221
+ match: "/api/:path*",
222
+ type: "serverFunction",
223
+ serverFunction: serverFunctionName,
224
+ },
225
+ ];
226
+ }
227
+
228
+ async function emitServerFunction(
229
+ root: string,
230
+ serverRoot: string,
231
+ serverManifest: ServerManifestWithEntry,
232
+ serverFunctionName: string,
233
+ endpoint: string,
234
+ extendDescriptor: EmitUboaOptions["extendServerFunctionDescriptor"],
235
+ ): Promise<void> {
236
+ const functionDir = path.join(root, "server-functions", serverFunctionName);
237
+ const sourceEntry = path.join(serverRoot, serverManifest.entry);
238
+
239
+ if (!(await pathExists(sourceEntry))) {
240
+ throw new Error(`EVJS server entry not found: ${toPosix(sourceEntry)}`);
241
+ }
242
+
243
+ await fs.mkdir(path.dirname(functionDir), { recursive: true });
244
+ await fs.cp(serverRoot, functionDir, { recursive: true });
245
+
246
+ const descriptor: UboaServerFunctionDescriptor = {
247
+ name: serverFunctionName,
248
+ runtime: "nodejs22",
249
+ entry: toPosix(serverManifest.entry),
250
+ handler: "default",
251
+ handlerProtocol: "fetch",
252
+ memory: 512,
253
+ maxDuration: 10,
254
+ environment: {},
255
+ bindings: {},
256
+ framework: {
257
+ name: "evjs",
258
+ serverFunctionEndpoint: endpoint,
259
+ serverManifest: "dist/server/manifest.json",
260
+ serverFunctions: Object.keys(serverManifest.fns),
261
+ },
262
+ source: {
263
+ entry: `dist/server/${serverManifest.entry}`,
264
+ },
265
+ };
266
+ const extendedDescriptor = extendDescriptor
267
+ ? await extendDescriptor({ descriptor, serverFunctionName })
268
+ : descriptor;
269
+
270
+ if (!isRecord(extendedDescriptor)) {
271
+ throw new Error("extendServerFunctionDescriptor must return an object.");
272
+ }
273
+
274
+ await writeJson(
275
+ path.join(functionDir, "serverFunction.json"),
276
+ extendedDescriptor,
277
+ );
278
+ }
279
+
280
+ export async function emitUboa(
281
+ options: EmitUboaOptions,
282
+ ): Promise<AdapterBuildResult> {
283
+ const cwd = path.resolve(options.cwd ?? process.cwd());
284
+ const root = outputRoot(cwd, options.outputDir);
285
+ const serverFunctionName = options.serverFunctionName ?? DEFAULT_SERVER_FUNCTION;
286
+ const serverEndpoint =
287
+ options.serverFunctionEndpoint ?? DEFAULT_SERVER_ENDPOINT;
288
+ const { clientManifest, serverManifest, clientRoot, serverRoot } =
289
+ await resolveBuildInputs({ ...options, cwd });
290
+ const warnings: string[] = [];
291
+
292
+ await fs.rm(root, { recursive: true, force: true });
293
+ await fs.mkdir(root, { recursive: true });
294
+
295
+ await copyIfExists(clientRoot, path.join(root, "static"));
296
+
297
+ const routes = buildStaticRoutes(clientManifest);
298
+ const middleware: UboaMiddleware[] = [];
299
+ let artifacts: UboaArtifact[] = [
300
+ { id: "static", resourceKind: "assets", path: "static" },
301
+ ];
302
+
303
+ if (hasServerEntry(serverManifest)) {
304
+ await emitServerFunction(
305
+ root,
306
+ serverRoot,
307
+ serverManifest,
308
+ serverFunctionName,
309
+ serverEndpoint,
310
+ options.extendServerFunctionDescriptor,
311
+ );
312
+ routes.push(...serverRoutes(serverFunctionName, serverEndpoint));
313
+ artifacts.push({
314
+ id: serverFunctionName,
315
+ resourceKind: "serverFunction",
316
+ path: `server-functions/${serverFunctionName}`,
317
+ });
318
+
319
+ if (Object.keys(serverManifest.fns).length === 0) {
320
+ warnings.push(
321
+ "EVJS serverManifest.fns is empty; server function introspection is degraded.",
322
+ );
323
+ }
324
+ }
325
+
326
+ if (options.extendArtifacts) {
327
+ const extendedArtifacts = await options.extendArtifacts({
328
+ artifacts,
329
+ serverFunctionName,
330
+ });
331
+
332
+ if (!Array.isArray(extendedArtifacts)) {
333
+ throw new Error("extendArtifacts must return an array.");
334
+ }
335
+
336
+ artifacts = extendedArtifacts;
337
+ }
338
+
339
+ const capabilities = buildCapabilities(clientManifest, serverManifest);
340
+
341
+ await writeJson(path.join(root, "unio.json"), {
342
+ version: UBOA_VERSION,
343
+ framework: {
344
+ name: "evjs",
345
+ version: "unknown",
346
+ adapter: ADAPTER_PACKAGE,
347
+ },
348
+ build: {
349
+ contentVersion: buildContentVersion(),
350
+ buildId: process.env.BUILD_ID || process.env.TASK_ID || "local",
351
+ },
352
+ capabilities,
353
+ warnings,
354
+ });
355
+ await writeJson(path.join(root, "routes.json"), routes);
356
+ await writeJson(path.join(root, "middleware.json"), middleware);
357
+ await writeJson(path.join(root, "artifacts.json"), { artifacts });
358
+ await writeJson(path.join(root, "features.json"), {
359
+ features: [],
360
+ evjs: {
361
+ serverFunctions: serverManifest ? Object.keys(serverManifest.fns) : [],
362
+ routeHandlers: hasServerEntry(serverManifest) ? "degraded" : "unsupported",
363
+ },
364
+ });
365
+ await writeJson(path.join(root, "observability.json"), {
366
+ adapter: ADAPTER_PACKAGE,
367
+ framework: "evjs",
368
+ buildId: process.env.BUILD_ID || process.env.TASK_ID || "local",
369
+ });
370
+
371
+ await options.afterEmit?.({
372
+ outputDir: root,
373
+ serverFunctionName,
374
+ });
375
+
376
+ return {
377
+ outputDir: root,
378
+ routes,
379
+ middleware,
380
+ artifacts,
381
+ capabilities,
382
+ warnings,
383
+ };
384
+ }
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ export { build } from "./build.js";
2
+ export { detect } from "./detect.js";
3
+ export { emitUboa } from "./emit.js";
4
+ export { unio } from "./plugin.js";
5
+ export { validate } from "./validate.js";
6
+ export type {
7
+ AdapterBuildResult,
8
+ AfterEmitContext,
9
+ BuildOptions,
10
+ CapabilityStatus,
11
+ ClientManifest,
12
+ DetectResult,
13
+ EmitUboaOptions,
14
+ EvBuildResult,
15
+ EvPlugin,
16
+ EvPluginContext,
17
+ EvPluginHooks,
18
+ ExtendArtifactsContext,
19
+ ExtendServerFunctionDescriptorContext,
20
+ PageManifestEntry,
21
+ RouteEntry,
22
+ ServerFnEntry,
23
+ ServerManifest,
24
+ UboaApiSchemaArtifact,
25
+ UboaApiSchemaFormat,
26
+ UboaArtifact,
27
+ UboaArtifactReference,
28
+ UboaArtifactResourceKind,
29
+ UboaAssetsArtifact,
30
+ UboaCapabilities,
31
+ UboaEdgeBundleArtifact,
32
+ UboaEdgeFunctionRoute,
33
+ UboaEdgeMiddleware,
34
+ UboaMiddleware,
35
+ UboaRoute,
36
+ UboaServerFunctionDescriptor,
37
+ UboaServerFunctionArtifact,
38
+ UboaServerFunctionRoute,
39
+ UboaServerlessMiddleware,
40
+ UboaStaticRoute,
41
+ UnioEvjsAdapterOptions,
42
+ ValidationResult,
43
+ } from "./types.js";
44
+
45
+ export { unio as default } from "./plugin.js";
@@ -0,0 +1,108 @@
1
+ import type {
2
+ ClientManifest,
3
+ PageManifestEntry,
4
+ RouteEntry,
5
+ ServerFnEntry,
6
+ ServerManifest,
7
+ } from "./types.js";
8
+
9
+ import { isRecord, isStringArray } from "./utils.js";
10
+
11
+ function invalidManifest(kind: "client" | "server", reason: string): Error {
12
+ return new Error(`Invalid EVJS ${kind} manifest: ${reason}`);
13
+ }
14
+
15
+ function assertRouteEntry(value: unknown, label: string): asserts value is RouteEntry {
16
+ if (!isRecord(value) || typeof value.path !== "string") {
17
+ throw invalidManifest("client", `${label}.path must be a string`);
18
+ }
19
+ }
20
+
21
+ function assertRoutes(value: unknown, label: string): asserts value is RouteEntry[] {
22
+ if (!Array.isArray(value)) {
23
+ throw invalidManifest("client", `${label} must be an array`);
24
+ }
25
+
26
+ value.forEach((route, index) => {
27
+ assertRouteEntry(route, `${label}[${index}]`);
28
+ });
29
+ }
30
+
31
+ function assertPageManifestEntry(
32
+ value: unknown,
33
+ label: string,
34
+ ): asserts value is PageManifestEntry {
35
+ if (!isRecord(value)) {
36
+ throw invalidManifest("client", `${label} must be an object`);
37
+ }
38
+ assertAssets(value.assets, `${label}.assets`);
39
+ if (value.routes !== undefined) {
40
+ assertRoutes(value.routes, `${label}.routes`);
41
+ }
42
+ }
43
+
44
+ function assertAssets(
45
+ value: unknown,
46
+ label: string,
47
+ ): asserts value is { js: string[]; css: string[] } {
48
+ if (!isRecord(value)) {
49
+ throw invalidManifest("client", `${label} must be an object`);
50
+ }
51
+ if (!isStringArray(value.js)) {
52
+ throw invalidManifest("client", `${label}.js must be an array`);
53
+ }
54
+ if (!isStringArray(value.css)) {
55
+ throw invalidManifest("client", `${label}.css must be an array`);
56
+ }
57
+ }
58
+
59
+ export function assertClientManifest(value: unknown): asserts value is ClientManifest {
60
+ if (!isRecord(value)) {
61
+ throw invalidManifest("client", "manifest must be an object");
62
+ }
63
+ if (value.version !== 1) {
64
+ throw invalidManifest("client", "version must be 1");
65
+ }
66
+
67
+ assertAssets(value.assets, "assets");
68
+
69
+ if (value.routes !== undefined) {
70
+ assertRoutes(value.routes, "routes");
71
+ }
72
+
73
+ if (value.pages !== undefined) {
74
+ if (!isRecord(value.pages)) {
75
+ throw invalidManifest("client", "pages must be an object");
76
+ }
77
+ for (const [pageName, page] of Object.entries(value.pages)) {
78
+ assertPageManifestEntry(page, `pages.${pageName}`);
79
+ }
80
+ }
81
+ }
82
+
83
+ function assertServerFnEntry(
84
+ value: unknown,
85
+ label: string,
86
+ ): asserts value is ServerFnEntry {
87
+ if (!isRecord(value)) {
88
+ throw invalidManifest("server", `${label} must be an object`);
89
+ }
90
+ }
91
+
92
+ export function assertServerManifest(value: unknown): asserts value is ServerManifest {
93
+ if (!isRecord(value)) {
94
+ throw invalidManifest("server", "manifest must be an object");
95
+ }
96
+ if (value.version !== 1) {
97
+ throw invalidManifest("server", "version must be 1");
98
+ }
99
+ if (value.entry !== undefined && typeof value.entry !== "string") {
100
+ throw invalidManifest("server", "entry must be a string when present");
101
+ }
102
+ if (!isRecord(value.fns)) {
103
+ throw invalidManifest("server", "fns must be an object");
104
+ }
105
+ for (const [fnId, fnEntry] of Object.entries(value.fns)) {
106
+ assertServerFnEntry(fnEntry, `fns.${fnId}`);
107
+ }
108
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type {
2
+ AdapterBuildResult,
3
+ EvPlugin,
4
+ UnioEvjsAdapterOptions,
5
+ } from "./types.js";
6
+ import { emitUboa } from "./emit.js";
7
+
8
+ export const UNIO_PLUGIN_NAME = "@unieojs/unio-evjs-adapter";
9
+
10
+ export function createUnioPlugin(
11
+ options: UnioEvjsAdapterOptions = {},
12
+ onResult?: (result: AdapterBuildResult) => void,
13
+ ): EvPlugin {
14
+ return {
15
+ name: UNIO_PLUGIN_NAME,
16
+ setup(ctx) {
17
+ const cwd = options.cwd ?? ctx.cwd;
18
+ const isProductionBuild = ctx.mode === "production";
19
+
20
+ return {
21
+ async buildEnd(result) {
22
+ if (!isProductionBuild) return;
23
+
24
+ const adapterResult = await emitUboa({
25
+ ...options,
26
+ cwd,
27
+ clientManifest: result.clientManifest,
28
+ serverManifest: result.serverManifest,
29
+ });
30
+ onResult?.(adapterResult);
31
+ },
32
+ };
33
+ },
34
+ };
35
+ }
36
+
37
+ export function unio(options: UnioEvjsAdapterOptions = {}): EvPlugin {
38
+ return createUnioPlugin(options);
39
+ }