@unieojs/unio-nextjs-adapter 0.1.0 → 0.2.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/types.ts CHANGED
@@ -69,7 +69,11 @@ export interface BuildContext {
69
69
  distDir: string;
70
70
  /** Absolute project root */
71
71
  projectDir: string;
72
+ /** Absolute repository root, when Next.js provides it */
73
+ repoRoot?: string;
72
74
  config: NextConfigLike;
75
+ nextVersion?: string;
76
+ buildId?: string;
73
77
  }
74
78
 
75
79
  /**
@@ -91,3 +95,181 @@ export interface NextAdapter {
91
95
  ) => NextConfigLike | Promise<NextConfigLike>;
92
96
  onBuildComplete?: (ctx: BuildContext) => void | Promise<void>;
93
97
  }
98
+
99
+ export type CapabilityStatus =
100
+ | "supported"
101
+ | "partial"
102
+ | "degraded"
103
+ | "unsupported";
104
+
105
+ export interface UboaCapabilities {
106
+ static: CapabilityStatus;
107
+ routes: CapabilityStatus;
108
+ serverlessFunction: CapabilityStatus;
109
+ serverFunctionIntrospection: CapabilityStatus;
110
+ restRouteIntrospection: CapabilityStatus;
111
+ edgeMiddleware: CapabilityStatus;
112
+ isr: CapabilityStatus;
113
+ imageOptimization: CapabilityStatus;
114
+ }
115
+
116
+ interface UboaRouteBase {
117
+ id: string;
118
+ match: string;
119
+ }
120
+
121
+ export interface UboaAssetsRoute extends UboaRouteBase {
122
+ type: "assets";
123
+ entry: string;
124
+ serverFunction?: never;
125
+ edgeFunction?: never;
126
+ methods?: never;
127
+ }
128
+
129
+ export interface UboaServerFunctionRoute extends UboaRouteBase {
130
+ type: "serverFunction";
131
+ serverFunction: string;
132
+ methods?: string[];
133
+ entry?: never;
134
+ edgeFunction?: never;
135
+ }
136
+
137
+ export interface UboaEdgeFunctionRoute extends UboaRouteBase {
138
+ type: "edgeFunction";
139
+ edgeFunction: string;
140
+ entry?: never;
141
+ serverFunction?: never;
142
+ methods?: never;
143
+ }
144
+
145
+ export type UboaRoute =
146
+ | UboaAssetsRoute
147
+ | UboaServerFunctionRoute
148
+ | UboaEdgeFunctionRoute;
149
+
150
+ interface UboaMiddlewareBase {
151
+ name: string;
152
+ match: string[];
153
+ }
154
+
155
+ export interface UboaEdgeMiddleware extends UboaMiddlewareBase {
156
+ runtime: "edge";
157
+ entry: string;
158
+ serverFunction?: never;
159
+ }
160
+
161
+ export interface UboaNodejsMiddleware extends UboaMiddlewareBase {
162
+ runtime: "nodejs";
163
+ serverFunction: string;
164
+ entry?: never;
165
+ }
166
+
167
+ export type UboaMiddleware =
168
+ | UboaEdgeMiddleware
169
+ | UboaNodejsMiddleware;
170
+
171
+ export type UboaArtifactResourceKind =
172
+ | "assets"
173
+ | "serverFunction"
174
+ | "edgeBundle"
175
+ | "apiSchema";
176
+
177
+ export interface UboaAssetsArtifact {
178
+ id: string;
179
+ resourceKind: "assets";
180
+ path: string;
181
+ }
182
+
183
+ export interface UboaServerFunctionArtifact {
184
+ id: string;
185
+ resourceKind: "serverFunction";
186
+ path: string;
187
+ }
188
+
189
+ export interface UboaEdgeBundleArtifact {
190
+ id: string;
191
+ resourceKind: "edgeBundle";
192
+ path: string;
193
+ }
194
+
195
+ export interface UboaArtifactReference {
196
+ id: string;
197
+ resourceKind: UboaArtifactResourceKind;
198
+ }
199
+
200
+ export type UboaApiSchemaFormat = "oneapi";
201
+
202
+ export interface UboaApiSchemaArtifact {
203
+ id: string;
204
+ resourceKind: "apiSchema";
205
+ path: string;
206
+ schemaFormat: UboaApiSchemaFormat;
207
+ describes?: UboaArtifactReference;
208
+ }
209
+
210
+ export type UboaArtifact =
211
+ | UboaAssetsArtifact
212
+ | UboaServerFunctionArtifact
213
+ | UboaEdgeBundleArtifact
214
+ | UboaApiSchemaArtifact;
215
+
216
+ export interface UboaServerFunctionDescriptor {
217
+ name: string;
218
+ runtime: string;
219
+ entry: string;
220
+ handler: string;
221
+ memory: number;
222
+ maxDuration: number;
223
+ environment: Record<string, unknown>;
224
+ bindings: Record<string, unknown>;
225
+ framework: Record<string, unknown>;
226
+ source: Record<string, unknown>;
227
+ [key: string]: unknown;
228
+ }
229
+
230
+ export type NextRouteOutputGroup =
231
+ | "pages"
232
+ | "pagesApi"
233
+ | "appPages"
234
+ | "appRoutes"
235
+ | "middleware";
236
+
237
+ export interface GeneratedUboa {
238
+ outputDir: string;
239
+ routes: UboaRoute[];
240
+ middleware: UboaMiddleware[];
241
+ artifacts: UboaArtifact[];
242
+ capabilities: UboaCapabilities;
243
+ warnings: string[];
244
+ }
245
+
246
+ export interface ExtendServerFunctionDescriptorContext {
247
+ descriptor: UboaServerFunctionDescriptor;
248
+ name: string;
249
+ route: RouteOutput;
250
+ routeGroup: NextRouteOutputGroup;
251
+ ctx: BuildContext;
252
+ }
253
+
254
+ export interface ExtendArtifactsContext {
255
+ artifacts: UboaArtifact[];
256
+ routes: UboaRoute[];
257
+ middleware: UboaMiddleware[];
258
+ ctx: BuildContext;
259
+ }
260
+
261
+ export interface AfterEmitContext {
262
+ outputDir: string;
263
+ generated: GeneratedUboa;
264
+ ctx: BuildContext;
265
+ }
266
+
267
+ export interface UnioNextjsAdapterOptions {
268
+ extendServerFunctionDescriptor?: (
269
+ context: ExtendServerFunctionDescriptorContext,
270
+ ) => Record<string, unknown> | Promise<Record<string, unknown>>;
271
+ extendArtifacts?: (
272
+ context: ExtendArtifactsContext,
273
+ ) => UboaArtifact[] | Promise<UboaArtifact[]>;
274
+ afterEmit?: (context: AfterEmitContext) => void | Promise<void>;
275
+ }
package/src/uboa.ts CHANGED
@@ -3,45 +3,25 @@ import path from "node:path";
3
3
 
4
4
  import type {
5
5
  BuildContext,
6
+ GeneratedUboa,
7
+ NextRouteOutputGroup,
6
8
  PrerenderOutput,
7
9
  RouteOutput,
8
10
  StaticFileOutput,
11
+ UboaArtifact,
12
+ UboaCapabilities,
13
+ UboaMiddleware,
14
+ UboaRoute,
15
+ UboaServerFunctionDescriptor,
16
+ UnioNextjsAdapterOptions,
9
17
  } from "./types.js";
10
18
 
11
19
  const ADAPTER_PACKAGE = "@unieojs/unio-nextjs-adapter";
12
20
  const UBOA_VERSION = "1.0.0";
13
21
 
14
- type CapabilityStatus = "supported" | "partial" | "degraded" | "unsupported";
15
-
16
- interface UboaRoute {
17
- id: string;
18
- match: string;
19
- type: "static" | "serverFunction";
20
- entry?: string;
21
- serverFunction?: string;
22
- }
23
-
24
- interface UboaMiddleware {
25
- name: string;
26
- match: string[];
27
- runtime: "serverless";
28
- serverFunction?: string;
29
- }
30
-
31
- interface UboaArtifact {
32
- id: string;
33
- resourceKind: "assets" | "serverFunction";
34
- path: string;
35
- }
36
-
37
- interface GeneratedUboa {
38
- routes: UboaRoute[];
39
- middleware: UboaMiddleware[];
40
- artifacts: UboaArtifact[];
41
- }
42
-
43
22
  type StaticLikeOutput = StaticFileOutput | PrerenderOutput;
44
23
  type RouteOutputKey = "pages" | "pagesApi" | "appPages" | "appRoutes";
24
+ type DynamicServerOutput = Pick<GeneratedUboa, "routes" | "middleware" | "artifacts">;
45
25
 
46
26
  const ROUTE_GROUPS: Array<[string, RouteOutputKey]> = [
47
27
  ["pages", "pages"],
@@ -109,6 +89,21 @@ function isRecord(value: unknown): value is Record<string, unknown> {
109
89
  return typeof value === "object" && value !== null && !Array.isArray(value);
110
90
  }
111
91
 
92
+ function isStringRecord(value: unknown): value is Record<string, string> {
93
+ return isRecord(value) &&
94
+ Object.values(value).every((entry) => typeof entry === "string");
95
+ }
96
+
97
+ function isSafeRelativePath(value: string): boolean {
98
+ if (!value) return false;
99
+ if (path.isAbsolute(value) || path.win32.isAbsolute(value)) return false;
100
+ return !value.split(/[\\/]+/).includes("..");
101
+ }
102
+
103
+ function buildId(ctx: BuildContext): string {
104
+ return ctx.buildId || process.env.BUILD_ID || process.env.TASK_ID || "local";
105
+ }
106
+
112
107
  function routeConfig(route: RouteOutput): Record<string, unknown> {
113
108
  return isRecord(route.config) ? route.config : {};
114
109
  }
@@ -118,6 +113,14 @@ function routeMaxDuration(route: RouteOutput): number {
118
113
  return typeof maxDuration === "number" ? maxDuration : 10;
119
114
  }
120
115
 
116
+ function edgeRuntimeModulePath(route: RouteOutput): string {
117
+ const edgeRuntime = route.edgeRuntime;
118
+ if (isRecord(edgeRuntime) && typeof edgeRuntime.modulePath === "string") {
119
+ return edgeRuntime.modulePath;
120
+ }
121
+ return route.filePath;
122
+ }
123
+
121
124
  function matcherSource(matcher: unknown): string | undefined {
122
125
  if (typeof matcher === "string") return matcher;
123
126
  if (!isRecord(matcher)) return undefined;
@@ -149,27 +152,99 @@ async function copyEntryFile(source: string, dest: string): Promise<void> {
149
152
  await fs.copyFile(source, dest);
150
153
  }
151
154
 
155
+ function routeSourceRoot(ctx: BuildContext): string {
156
+ return ctx.repoRoot ?? ctx.projectDir;
157
+ }
158
+
159
+ function relativeRouteFile(ctx: BuildContext, filePath: string): string {
160
+ const relativePath = path.relative(routeSourceRoot(ctx), filePath);
161
+
162
+ if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
163
+ return path.basename(filePath);
164
+ }
165
+
166
+ return toPosix(relativePath);
167
+ }
168
+
169
+ function assetTargetPath(
170
+ ctx: BuildContext,
171
+ assetKey: string,
172
+ assetPath: string,
173
+ ): string {
174
+ return isSafeRelativePath(assetKey)
175
+ ? toPosix(assetKey)
176
+ : relativeRouteFile(ctx, assetPath);
177
+ }
178
+
179
+ async function copyRoutePayload(
180
+ functionDir: string,
181
+ ctx: BuildContext,
182
+ route: RouteOutput,
183
+ ): Promise<string> {
184
+ const entry = relativeRouteFile(ctx, route.filePath);
185
+ await copyEntryFile(route.filePath, path.join(functionDir, entry));
186
+
187
+ for (const assetMap of [route.assets, route.wasmAssets]) {
188
+ if (!isStringRecord(assetMap)) continue;
189
+
190
+ for (const [assetKey, assetPath] of Object.entries(assetMap)) {
191
+ await copyEntryFile(
192
+ assetPath,
193
+ path.join(functionDir, assetTargetPath(ctx, assetKey, assetPath)),
194
+ );
195
+ }
196
+ }
197
+
198
+ return entry;
199
+ }
200
+
152
201
  async function emitServerFunction(
153
202
  root: string,
203
+ ctx: BuildContext,
154
204
  name: string,
155
205
  route: RouteOutput,
206
+ routeGroup: NextRouteOutputGroup,
207
+ options: UnioNextjsAdapterOptions,
156
208
  ): Promise<void> {
157
209
  const functionDir = path.join(root, "server-functions", name);
158
- await copyEntryFile(route.filePath, path.join(functionDir, "index.js"));
159
- await writeJson(path.join(functionDir, "serverFunction.json"), {
210
+ const entry = await copyRoutePayload(functionDir, ctx, route);
211
+ const descriptor: UboaServerFunctionDescriptor = {
160
212
  name,
161
213
  runtime: "nodejs",
162
- entry: "index.js",
163
- handler: "default",
214
+ entry,
215
+ handler: "handler",
164
216
  memory: 512,
165
217
  maxDuration: routeMaxDuration(route),
166
218
  environment: {},
167
219
  bindings: {},
220
+ framework: {
221
+ name: "next",
222
+ version: ctx.nextVersion ?? "unknown",
223
+ routeGroup,
224
+ outputType: route.type,
225
+ },
168
226
  source: {
227
+ id: route.id,
169
228
  type: route.type,
170
229
  pathname: route.pathname,
230
+ filePath: relativeRouteFile(ctx, route.filePath),
171
231
  },
172
- });
232
+ };
233
+ const extendedDescriptor = options.extendServerFunctionDescriptor
234
+ ? await options.extendServerFunctionDescriptor({
235
+ descriptor,
236
+ name,
237
+ route,
238
+ routeGroup,
239
+ ctx,
240
+ })
241
+ : descriptor;
242
+
243
+ if (!isRecord(extendedDescriptor)) {
244
+ throw new Error("extendServerFunctionDescriptor must return an object.");
245
+ }
246
+
247
+ await writeJson(path.join(functionDir, "serverFunction.json"), extendedDescriptor);
173
248
  }
174
249
 
175
250
  function buildContentVersion(): string {
@@ -181,34 +256,48 @@ function buildContentVersion(): string {
181
256
  return parts.length > 0 ? parts.join(", ") : "local";
182
257
  }
183
258
 
184
- function buildCapabilities(ctx: BuildContext): Record<string, CapabilityStatus> {
259
+ function buildCapabilities(ctx: BuildContext): UboaCapabilities {
185
260
  const { outputs } = ctx;
186
261
  const nodeRoutes = ROUTE_GROUPS.some(([, key]) =>
187
262
  outputs[key].some((route) => route.runtime === "nodejs"),
188
263
  );
189
264
  const nodeMiddleware = outputs.middleware?.runtime === "nodejs";
265
+ const edgeMiddleware = outputs.middleware?.runtime === "edge";
266
+ const hasServerFunction = nodeRoutes || nodeMiddleware;
190
267
 
191
268
  return {
192
269
  static: outputs.staticFiles.length > 0 || outputs.prerenders.length > 0 ?
193
270
  "supported" : "unsupported",
194
271
  routes: "supported",
195
- serverlessFunction: nodeRoutes || nodeMiddleware ? "supported" : "unsupported",
196
- edgeMiddleware: "unsupported",
272
+ serverlessFunction: hasServerFunction ? "supported" : "unsupported",
273
+ serverFunctionIntrospection: hasServerFunction ? "supported" : "unsupported",
274
+ restRouteIntrospection: nodeRoutes ? "supported" : "unsupported",
275
+ edgeMiddleware: edgeMiddleware ? "supported" : "unsupported",
197
276
  isr: outputs.prerenders.length > 0 ? "degraded" : "unsupported",
198
277
  imageOptimization: "unsupported",
199
278
  };
200
279
  }
201
280
 
281
+ function buildWarnings(ctx: BuildContext): string[] {
282
+ return ctx.outputs.prerenders.length > 0
283
+ ? ["Next.js prerenders are emitted as static assets; ISR metadata is degraded."]
284
+ : [];
285
+ }
286
+
202
287
  function assertSupportedOutputs(ctx: BuildContext): void {
203
288
  const unsupported: string[] = [];
204
289
 
205
- if (ctx.outputs.middleware && ctx.outputs.middleware.runtime !== "nodejs") {
290
+ if (
291
+ ctx.outputs.middleware &&
292
+ ctx.outputs.middleware.runtime !== "nodejs" &&
293
+ ctx.outputs.middleware.runtime !== "edge"
294
+ ) {
206
295
  unsupported.push(`middleware (${ctx.outputs.middleware.runtime})`);
207
296
  }
208
297
 
209
298
  for (const [, key] of ROUTE_GROUPS) {
210
299
  for (const route of ctx.outputs[key]) {
211
- if (route.runtime !== "nodejs") {
300
+ if (route.runtime !== "nodejs" && route.runtime !== "edge") {
212
301
  unsupported.push(`${key}:${route.pathname || route.id} (${route.runtime})`);
213
302
  }
214
303
  }
@@ -219,26 +308,41 @@ function assertSupportedOutputs(ctx: BuildContext): void {
219
308
  throw new Error(
220
309
  "[unio-nextjs-adapter] Unsupported Next.js outputs: " +
221
310
  `${unsupported.join(", ")}. ` +
222
- "This adapter currently supports static assets and nodejs serverless functions only.",
311
+ "This adapter currently supports static assets, nodejs serverless functions, and edge functions only.",
223
312
  );
224
313
  }
225
314
 
226
315
  async function emitCoreManifest(
227
316
  root: string,
228
317
  ctx: BuildContext,
318
+ capabilities: UboaCapabilities,
319
+ warnings: string[],
229
320
  ): Promise<void> {
230
321
  await writeJson(path.join(root, "unio.json"), {
231
322
  version: UBOA_VERSION,
232
323
  framework: {
233
324
  name: "next",
234
- version: "unknown",
325
+ version: ctx.nextVersion ?? "unknown",
235
326
  adapter: ADAPTER_PACKAGE,
236
327
  },
237
328
  build: {
238
329
  contentVersion: buildContentVersion(),
239
- buildId: process.env.BUILD_ID || process.env.TASK_ID || "local",
330
+ buildId: buildId(ctx),
240
331
  },
241
- capabilities: buildCapabilities(ctx),
332
+ capabilities,
333
+ warnings,
334
+ });
335
+ }
336
+
337
+ async function emitFeaturesManifest(root: string): Promise<void> {
338
+ await writeJson(path.join(root, "features.json"), { features: [] });
339
+ }
340
+
341
+ async function emitObservabilityManifest(root: string, ctx: BuildContext): Promise<void> {
342
+ await writeJson(path.join(root, "observability.json"), {
343
+ adapter: ADAPTER_PACKAGE,
344
+ framework: "next",
345
+ buildId: buildId(ctx),
242
346
  });
243
347
  }
244
348
 
@@ -257,7 +361,7 @@ async function emitStaticFiles(
257
361
  routes.push({
258
362
  id: `static-${slugify(file.pathname || file.id, "index")}`,
259
363
  match: normalizeStaticMatch(file),
260
- type: "static",
364
+ type: "assets",
261
365
  entry: staticEntryFor(file),
262
366
  });
263
367
  }
@@ -265,9 +369,19 @@ async function emitStaticFiles(
265
369
  return routes;
266
370
  }
267
371
 
372
+ function sourceForRoute(ctx: BuildContext, route: RouteOutput): Record<string, unknown> {
373
+ return {
374
+ id: route.id,
375
+ type: route.type,
376
+ pathname: route.pathname,
377
+ filePath: relativeRouteFile(ctx, route.filePath),
378
+ };
379
+ }
380
+
268
381
  async function emitRouteOutputs(
269
382
  root: string,
270
383
  ctx: BuildContext,
384
+ options: UnioNextjsAdapterOptions,
271
385
  ): Promise<Pick<GeneratedUboa, "routes" | "artifacts">> {
272
386
  const routes: UboaRoute[] = [];
273
387
  const artifacts: UboaArtifact[] = [];
@@ -276,7 +390,23 @@ async function emitRouteOutputs(
276
390
  for (const route of ctx.outputs[key]) {
277
391
  const name = routeFunctionName(group, route);
278
392
 
279
- await emitServerFunction(root, name, route);
393
+ if (route.runtime === "edge") {
394
+ await emitEdgeBundle(root, ctx, name, route, key);
395
+ routes.push({
396
+ id: `edge-${name}`,
397
+ match: routeMatch(route),
398
+ type: "edgeFunction",
399
+ edgeFunction: name,
400
+ });
401
+ artifacts.push({
402
+ id: name,
403
+ resourceKind: "edgeBundle",
404
+ path: `edge/${name}`,
405
+ });
406
+ continue;
407
+ }
408
+
409
+ await emitServerFunction(root, ctx, name, route, key, options);
280
410
  routes.push({
281
411
  id: `server-${name}`,
282
412
  match: routeMatch(route),
@@ -294,21 +424,71 @@ async function emitRouteOutputs(
294
424
  return { routes, artifacts };
295
425
  }
296
426
 
427
+ async function emitEdgeBundle(
428
+ root: string,
429
+ ctx: BuildContext,
430
+ name: string,
431
+ route: RouteOutput,
432
+ routeGroup: NextRouteOutputGroup,
433
+ ): Promise<void> {
434
+ const entry = `edge/${name}/index.js`;
435
+ await copyEntryFile(edgeRuntimeModulePath(route), path.join(root, entry));
436
+ await writeJson(path.join(root, "edge", name, "edge.json"), {
437
+ name,
438
+ runtime: "edge",
439
+ entry: "index.js",
440
+ handlerProtocol: "fetch",
441
+ framework: {
442
+ name: "next",
443
+ version: ctx.nextVersion ?? "unknown",
444
+ routeGroup,
445
+ outputType: route.type,
446
+ },
447
+ source: sourceForRoute(ctx, route),
448
+ });
449
+ }
450
+
297
451
  async function emitMiddleware(
298
452
  root: string,
453
+ ctx: BuildContext,
299
454
  middleware: RouteOutput | undefined,
455
+ options: UnioNextjsAdapterOptions,
300
456
  ): Promise<Pick<GeneratedUboa, "middleware" | "artifacts">> {
301
457
  if (!middleware) return { middleware: [], artifacts: [] };
302
458
 
303
459
  const name = "middleware";
304
- await emitServerFunction(root, name, middleware);
460
+
461
+ if (middleware.runtime === "edge") {
462
+ const entry = `edge/${name}/index.js`;
463
+ await emitEdgeBundle(root, ctx, name, middleware, "middleware");
464
+
465
+ return {
466
+ middleware: [
467
+ {
468
+ name,
469
+ match: middlewareMatchers(middleware),
470
+ runtime: "edge",
471
+ entry,
472
+ },
473
+ ],
474
+ artifacts: [
475
+ {
476
+ id: name,
477
+ resourceKind: "edgeBundle",
478
+ path: `edge/${name}`,
479
+ },
480
+ ],
481
+ };
482
+ }
483
+
484
+ await emitServerFunction(root, ctx, name, middleware, "middleware", options);
305
485
 
306
486
  return {
307
487
  middleware: [
308
488
  {
309
489
  name,
310
490
  match: middlewareMatchers(middleware),
311
- runtime: "serverless",
491
+ runtime: "nodejs",
312
492
  serverFunction: name,
313
493
  },
314
494
  ],
@@ -322,7 +502,25 @@ async function emitMiddleware(
322
502
  };
323
503
  }
324
504
 
325
- export async function emitUboaOutput(ctx: BuildContext): Promise<GeneratedUboa> {
505
+ async function emitDynamicServerOutput(
506
+ root: string,
507
+ ctx: BuildContext,
508
+ options: UnioNextjsAdapterOptions,
509
+ ): Promise<DynamicServerOutput> {
510
+ const routeOutput = await emitRouteOutputs(root, ctx, options);
511
+ const middlewareOutput = await emitMiddleware(root, ctx, ctx.outputs.middleware, options);
512
+
513
+ return {
514
+ routes: routeOutput.routes,
515
+ middleware: middlewareOutput.middleware,
516
+ artifacts: [...routeOutput.artifacts, ...middlewareOutput.artifacts],
517
+ };
518
+ }
519
+
520
+ export async function emitUboaOutput(
521
+ ctx: BuildContext,
522
+ options: UnioNextjsAdapterOptions = {},
523
+ ): Promise<GeneratedUboa> {
326
524
  assertSupportedOutputs(ctx);
327
525
 
328
526
  const root = path.join(ctx.projectDir, ".unio", "output");
@@ -331,10 +529,9 @@ export async function emitUboaOutput(ctx: BuildContext): Promise<GeneratedUboa>
331
529
 
332
530
  const staticFiles = [...ctx.outputs.staticFiles, ...ctx.outputs.prerenders];
333
531
  const staticRoutes = await emitStaticFiles(root, staticFiles);
334
- const routeOutput = await emitRouteOutputs(root, ctx);
335
- const middlewareOutput = await emitMiddleware(root, ctx.outputs.middleware);
532
+ const dynamicOutput = await emitDynamicServerOutput(root, ctx, options);
336
533
 
337
- const artifacts: UboaArtifact[] = [];
534
+ let artifacts: UboaArtifact[] = [];
338
535
  if (staticFiles.length > 0) {
339
536
  artifacts.push({
340
537
  id: "static",
@@ -342,15 +539,49 @@ export async function emitUboaOutput(ctx: BuildContext): Promise<GeneratedUboa>
342
539
  path: "static",
343
540
  });
344
541
  }
345
- artifacts.push(...routeOutput.artifacts, ...middlewareOutput.artifacts);
542
+ artifacts.push(...dynamicOutput.artifacts);
543
+
544
+ const routes = [...staticRoutes, ...dynamicOutput.routes];
545
+ const middleware = dynamicOutput.middleware;
546
+ const capabilities = buildCapabilities(ctx);
547
+ const warnings = buildWarnings(ctx);
548
+
549
+ if (options.extendArtifacts) {
550
+ const extendedArtifacts = await options.extendArtifacts({
551
+ artifacts,
552
+ routes,
553
+ middleware,
554
+ ctx,
555
+ });
346
556
 
347
- const routes = [...staticRoutes, ...routeOutput.routes];
348
- const middleware = middlewareOutput.middleware;
557
+ if (!Array.isArray(extendedArtifacts)) {
558
+ throw new Error("extendArtifacts must return an array.");
559
+ }
349
560
 
350
- await emitCoreManifest(root, ctx);
561
+ artifacts = extendedArtifacts;
562
+ }
563
+
564
+ const generated: GeneratedUboa = {
565
+ outputDir: root,
566
+ routes,
567
+ middleware,
568
+ artifacts,
569
+ capabilities,
570
+ warnings,
571
+ };
572
+
573
+ await emitCoreManifest(root, ctx, capabilities, warnings);
351
574
  await writeJson(path.join(root, "routes.json"), routes);
352
575
  await writeJson(path.join(root, "middleware.json"), middleware);
353
576
  await writeJson(path.join(root, "artifacts.json"), { artifacts });
577
+ await emitFeaturesManifest(root);
578
+ await emitObservabilityManifest(root, ctx);
579
+
580
+ await options.afterEmit?.({
581
+ outputDir: root,
582
+ generated,
583
+ ctx,
584
+ });
354
585
 
355
- return { routes, middleware, artifacts };
586
+ return generated;
356
587
  }
@@ -1,5 +0,0 @@
1
- import type { Outputs } from "./types.js";
2
- /**
3
- * Validate that a Next.js output set is pure static export.
4
- */
5
- export declare function assertSsgOnly(outputs: Outputs): void;