@akanjs/devkit 2.3.0 → 2.3.1-rc.1
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/commandDecorators/command.ts +2 -2
- package/commandDecorators/targetMeta.ts +1 -0
- package/executors.test.ts +92 -0
- package/executors.ts +33 -2
- package/frontendBuild/frontendBuild.test.ts +27 -0
- package/frontendBuild/pagesBundleBuilder.ts +13 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +48 -4
- package/package.json +2 -2
|
@@ -336,7 +336,7 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
336
336
|
};
|
|
337
337
|
|
|
338
338
|
programCommand.action(async (...args: unknown[]) => {
|
|
339
|
-
Logger.rawLog();
|
|
339
|
+
if (!targetMeta.targetOption.stdio) Logger.rawLog();
|
|
340
340
|
const cmdArgs = args.slice(0, args.length - 2);
|
|
341
341
|
const opt = args[args.length - 2] as Record<string, unknown>;
|
|
342
342
|
const commandArgs = [] as unknown[];
|
|
@@ -364,7 +364,7 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
364
364
|
|
|
365
365
|
try {
|
|
366
366
|
await targetMeta.handler.call(cmd, ...commandArgs);
|
|
367
|
-
Logger.rawLog();
|
|
367
|
+
if (!targetMeta.targetOption.stdio) Logger.rawLog();
|
|
368
368
|
} catch (e) {
|
|
369
369
|
printCliError(e);
|
|
370
370
|
throw e;
|
package/executors.test.ts
CHANGED
|
@@ -256,6 +256,98 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
256
256
|
expect((await stat(path.join(root, "dist/apps/demo/public"))).isDirectory()).toBe(true);
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
+
test("accepts metadata route exports during page key discovery", async () => {
|
|
260
|
+
const root = await makeTempRoot();
|
|
261
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
262
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
263
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
264
|
+
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
265
|
+
await mkdir(path.join(root, "apps/demo/page/docs"), { recursive: true });
|
|
266
|
+
await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
|
|
267
|
+
await writeFile(
|
|
268
|
+
path.join(root, "apps/demo/page/_layout.tsx"),
|
|
269
|
+
[
|
|
270
|
+
"export const head = null;",
|
|
271
|
+
"export const metadata = { title: 'Root' };",
|
|
272
|
+
"export default function Layout({ children }) { return children; }",
|
|
273
|
+
"",
|
|
274
|
+
].join("\n"),
|
|
275
|
+
);
|
|
276
|
+
await writeFile(
|
|
277
|
+
path.join(root, "apps/demo/page/docs/_layout.tsx"),
|
|
278
|
+
[
|
|
279
|
+
"export async function generateMetadata() { return { title: 'Docs' }; }",
|
|
280
|
+
"export default function Layout({ children }) { return children; }",
|
|
281
|
+
"",
|
|
282
|
+
].join("\n"),
|
|
283
|
+
);
|
|
284
|
+
await writeFile(
|
|
285
|
+
path.join(root, "apps/demo/page/docs/intro.tsx"),
|
|
286
|
+
["export const metadata = { title: 'Intro' };", "export default function Page() { return null; }", ""].join("\n"),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
290
|
+
const app = AppExecutor.from(workspace, "demo");
|
|
291
|
+
|
|
292
|
+
await expect(app.getPageKeys({ refresh: true })).resolves.toEqual([
|
|
293
|
+
"./_layout.tsx",
|
|
294
|
+
"./docs/_layout.tsx",
|
|
295
|
+
"./docs/intro.tsx",
|
|
296
|
+
]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("rejects conflicting metadata route exports during page key discovery", async () => {
|
|
300
|
+
const root = await makeTempRoot();
|
|
301
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
302
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
303
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
304
|
+
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
305
|
+
await mkdir(path.join(root, "apps/demo/page"), { recursive: true });
|
|
306
|
+
await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
|
|
307
|
+
await writeFile(
|
|
308
|
+
path.join(root, "apps/demo/page/conflict.tsx"),
|
|
309
|
+
[
|
|
310
|
+
"export const metadata = { title: 'Conflict' };",
|
|
311
|
+
"export function generateMetadata() { return { title: 'Conflict' }; }",
|
|
312
|
+
"export default function Page() { return null; }",
|
|
313
|
+
"",
|
|
314
|
+
].join("\n"),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
318
|
+
const app = AppExecutor.from(workspace, "demo");
|
|
319
|
+
|
|
320
|
+
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(
|
|
321
|
+
"metadata and generateMetadata cannot both be exported",
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("rejects mixed head and metadata route export channels during page key discovery", async () => {
|
|
326
|
+
const root = await makeTempRoot();
|
|
327
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
328
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
329
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
330
|
+
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
331
|
+
await mkdir(path.join(root, "apps/demo/page"), { recursive: true });
|
|
332
|
+
await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
|
|
333
|
+
await writeFile(
|
|
334
|
+
path.join(root, "apps/demo/page/mixed.tsx"),
|
|
335
|
+
[
|
|
336
|
+
"export const head = null;",
|
|
337
|
+
"export const metadata = { title: 'Mixed' };",
|
|
338
|
+
"export default function Page() { return null; }",
|
|
339
|
+
"",
|
|
340
|
+
].join("\n"),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
344
|
+
const app = AppExecutor.from(workspace, "demo");
|
|
345
|
+
|
|
346
|
+
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(
|
|
347
|
+
"head/generateHead and metadata/generateMetadata cannot both be exported",
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
259
351
|
test("assigns start command ports from sorted app order", async () => {
|
|
260
352
|
const root = await makeTempRoot();
|
|
261
353
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
package/executors.ts
CHANGED
|
@@ -151,11 +151,21 @@ const parseEnvFile = (envPath: string): Record<string, string> => {
|
|
|
151
151
|
return env;
|
|
152
152
|
};
|
|
153
153
|
|
|
154
|
-
const PAGE_ROUTE_EXPORTS = new Set([
|
|
154
|
+
const PAGE_ROUTE_EXPORTS = new Set([
|
|
155
|
+
"default",
|
|
156
|
+
"pageConfig",
|
|
157
|
+
"head",
|
|
158
|
+
"metadata",
|
|
159
|
+
"generateHead",
|
|
160
|
+
"generateMetadata",
|
|
161
|
+
"Loading",
|
|
162
|
+
]);
|
|
155
163
|
const ROOT_LAYOUT_EXPORTS = new Set([
|
|
156
164
|
"default",
|
|
157
165
|
"head",
|
|
166
|
+
"metadata",
|
|
158
167
|
"generateHead",
|
|
168
|
+
"generateMetadata",
|
|
159
169
|
"fonts",
|
|
160
170
|
"manifest",
|
|
161
171
|
"theme",
|
|
@@ -166,7 +176,16 @@ const ROOT_LAYOUT_EXPORTS = new Set([
|
|
|
166
176
|
"NotFound",
|
|
167
177
|
"Error",
|
|
168
178
|
]);
|
|
169
|
-
const LAYOUT_ROUTE_EXPORTS = new Set([
|
|
179
|
+
const LAYOUT_ROUTE_EXPORTS = new Set([
|
|
180
|
+
"default",
|
|
181
|
+
"head",
|
|
182
|
+
"metadata",
|
|
183
|
+
"generateHead",
|
|
184
|
+
"generateMetadata",
|
|
185
|
+
"Loading",
|
|
186
|
+
"NotFound",
|
|
187
|
+
"Error",
|
|
188
|
+
]);
|
|
170
189
|
|
|
171
190
|
function validateRouteSourceExports(
|
|
172
191
|
source: string,
|
|
@@ -221,6 +240,18 @@ function validateRouteSourceExports(
|
|
|
221
240
|
if (exported.has("head") && exported.has("generateHead")) {
|
|
222
241
|
throw new Error(`[route-convention] head and generateHead cannot both be exported in ${filePath}`);
|
|
223
242
|
}
|
|
243
|
+
if (
|
|
244
|
+
!options.rootLayout &&
|
|
245
|
+
(exported.has("head") || exported.has("generateHead")) &&
|
|
246
|
+
(exported.has("metadata") || exported.has("generateMetadata"))
|
|
247
|
+
) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`[route-convention] head/generateHead and metadata/generateMetadata cannot both be exported in ${filePath}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (exported.has("metadata") && exported.has("generateMetadata")) {
|
|
253
|
+
throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
|
|
254
|
+
}
|
|
224
255
|
}
|
|
225
256
|
|
|
226
257
|
export class Executor {
|
|
@@ -8,6 +8,7 @@ import { CsrArtifactBuilder } from "./csrArtifactBuilder";
|
|
|
8
8
|
import { CssCompiler, isIgnoredNodeModuleSource } from "./cssCompiler";
|
|
9
9
|
import { CssImportResolver } from "./cssImportResolver";
|
|
10
10
|
import { HmrChangeClassifier } from "./hmrChangeClassifier";
|
|
11
|
+
import { PagesBundleBuilder } from "./pagesBundleBuilder";
|
|
11
12
|
import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
12
13
|
import { RoutesManifestArtifactSerializer } from "./routesManifestArtifactSerializer";
|
|
13
14
|
import { prepareCssAsset } from "./ssrBaseArtifactBuilder";
|
|
@@ -95,6 +96,32 @@ describe("PagesEntrySourceGenerator", () => {
|
|
|
95
96
|
});
|
|
96
97
|
});
|
|
97
98
|
|
|
99
|
+
describe("PagesBundleBuilder", () => {
|
|
100
|
+
test("stubs CSS imports in the server pages bundle", async () => {
|
|
101
|
+
const root = await makeTempRoot();
|
|
102
|
+
const entry = path.join(root, "entry.tsx");
|
|
103
|
+
const css = path.join(root, "styles.css");
|
|
104
|
+
const outdir = path.join(root, "out");
|
|
105
|
+
await write(entry, ['import "./styles.css";', "export const marker = 1;", ""].join("\n"));
|
|
106
|
+
await write(
|
|
107
|
+
css,
|
|
108
|
+
['@plugin "daisyui" {', " themes: false;", "}", "@theme {", " --color-primary: red;", "}", ""].join("\n"),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const result = await Bun.build({
|
|
112
|
+
entrypoints: [entry],
|
|
113
|
+
outdir,
|
|
114
|
+
target: "bun",
|
|
115
|
+
format: "esm",
|
|
116
|
+
plugins: [PagesBundleBuilder.createServerCssStubPlugin()],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(result.success).toBe(true);
|
|
120
|
+
expect(result.logs).toEqual([]);
|
|
121
|
+
expect(result.outputs.some((output) => output.kind === "entry-point")).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
98
125
|
describe("CsrArtifactBuilder", () => {
|
|
99
126
|
test("replaces module script src with inline script", async () => {
|
|
100
127
|
const html = [
|
|
@@ -64,6 +64,7 @@ export class PagesBundleBuilder {
|
|
|
64
64
|
define: this.#define(),
|
|
65
65
|
plugins: [
|
|
66
66
|
PagesBundleBuilder.createPagesEntryPlugin(entrySource),
|
|
67
|
+
PagesBundleBuilder.createServerCssStubPlugin(),
|
|
67
68
|
await createExternalizeFrameworkPlugin({ app: this.#app, extra: akanConfig.externalLibs }),
|
|
68
69
|
akanConfig.barrelImports.length > 0
|
|
69
70
|
? await createBarrelImportsPlugin(this.#app, {
|
|
@@ -134,4 +135,16 @@ export class PagesBundleBuilder {
|
|
|
134
135
|
},
|
|
135
136
|
};
|
|
136
137
|
}
|
|
138
|
+
|
|
139
|
+
static createServerCssStubPlugin(): BunPlugin {
|
|
140
|
+
return {
|
|
141
|
+
name: "akan-server-css-stub",
|
|
142
|
+
setup(build) {
|
|
143
|
+
build.onLoad({ filter: /\.css$/ }, () => ({
|
|
144
|
+
contents: "",
|
|
145
|
+
loader: "js",
|
|
146
|
+
}));
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
137
150
|
}
|
|
@@ -8,6 +8,7 @@ import { ClientEntriesBundler } from "./clientEntriesBundler";
|
|
|
8
8
|
import { CssCompiler } from "./cssCompiler";
|
|
9
9
|
import { FontOptimizer } from "./fontOptimizer";
|
|
10
10
|
import { PagesBundleBuilder } from "./pagesBundleBuilder";
|
|
11
|
+
import { RouteClientBuilder } from "./routeClientBuilder";
|
|
11
12
|
import { VENDOR_SPECIFIERS, type VendorSpecifier } from "./vendorSpecifiers";
|
|
12
13
|
|
|
13
14
|
export interface BuildSsrBaseArtifactResult {
|
|
@@ -37,7 +38,8 @@ export class SsrBaseArtifactBuilder {
|
|
|
37
38
|
|
|
38
39
|
async build(): Promise<BuildSsrBaseArtifactResult> {
|
|
39
40
|
const akanConfig = await this.#app.getConfig();
|
|
40
|
-
const { rscClientUrl, vendorMap } =
|
|
41
|
+
const { rscClientUrl, rscRuntimeClientManifest, rscRuntimeSsrManifest, vendorMap } =
|
|
42
|
+
await this.#buildRuntimeClientEntries();
|
|
41
43
|
const pageKeys = await this.#app.getPageKeys();
|
|
42
44
|
this.#app.verbose(`[base-artifact] discovered ${pageKeys.length} route files under ${this.#app.cwdPath}/page`);
|
|
43
45
|
|
|
@@ -58,6 +60,8 @@ export class SsrBaseArtifactBuilder {
|
|
|
58
60
|
|
|
59
61
|
const artifact: BaseBuildArtifact = {
|
|
60
62
|
rscClientUrl,
|
|
63
|
+
rscRuntimeClientManifest,
|
|
64
|
+
rscRuntimeSsrManifest,
|
|
61
65
|
vendorMap,
|
|
62
66
|
cssAssets,
|
|
63
67
|
pagesBundlePath:
|
|
@@ -80,21 +84,61 @@ export class SsrBaseArtifactBuilder {
|
|
|
80
84
|
return { artifact, seedIndex, cssCompiler, optimizedFonts };
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
async #buildRuntimeClientEntries(): Promise<
|
|
87
|
+
async #buildRuntimeClientEntries(): Promise<
|
|
88
|
+
Pick<BaseBuildArtifact, "rscClientUrl" | "rscRuntimeClientManifest" | "rscRuntimeSsrManifest"> & {
|
|
89
|
+
vendorMap: Record<VendorSpecifier, string>;
|
|
90
|
+
}
|
|
91
|
+
> {
|
|
84
92
|
const akanServerPath = await this.#resolveAkanServerPath();
|
|
85
93
|
const rscClientEntry = `${akanServerPath}/rscClient.tsx`;
|
|
94
|
+
const rscSegmentOutletEntry = `${akanServerPath}/rscSegmentOutlet.tsx`;
|
|
86
95
|
const vendorEntries = VENDOR_SPECIFIERS.map((specifier) => ({
|
|
87
96
|
specifier,
|
|
88
97
|
absPath: `${akanServerPath}/vendor/${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`,
|
|
89
98
|
}));
|
|
90
|
-
const entries = [rscClientEntry, ...vendorEntries.map((v) => v.absPath)];
|
|
99
|
+
const entries = [rscClientEntry, rscSegmentOutletEntry, ...vendorEntries.map((v) => v.absPath)];
|
|
91
100
|
const clientBundle = await new ClientEntriesBundler({ app: this.#app, entries, command: this.#command }).bundle();
|
|
101
|
+
const ssrBundle = await new ClientEntriesBundler({
|
|
102
|
+
app: this.#app,
|
|
103
|
+
entries: [rscSegmentOutletEntry],
|
|
104
|
+
...RouteClientBuilder.resolveSsrClientExternalOptions(this.#command),
|
|
105
|
+
outputSubdir: "client-ssr",
|
|
106
|
+
command: this.#command,
|
|
107
|
+
}).bundle();
|
|
92
108
|
const rscClientUrl = clientBundle.entryUrlsByAbsPath.get(rscClientEntry) ?? "";
|
|
109
|
+
const rscRuntimeSsrManifest = {
|
|
110
|
+
moduleLoading: null,
|
|
111
|
+
moduleMap: Object.fromEntries(
|
|
112
|
+
Object.entries(clientBundle.manifest)
|
|
113
|
+
.map(([key, row]) => {
|
|
114
|
+
const ssrOutput = ssrBundle.entryOutputAbsByAbsPath.get(rscSegmentOutletEntry);
|
|
115
|
+
if (
|
|
116
|
+
!ssrOutput ||
|
|
117
|
+
key !== `${clientBundle.clientReferenceIdByAbsPath.get(rscSegmentOutletEntry)}#${row.name}`
|
|
118
|
+
) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return [
|
|
122
|
+
row.id,
|
|
123
|
+
{ [row.name]: { id: ssrOutput, chunks: [ssrOutput, ssrOutput], name: row.name, async: true } },
|
|
124
|
+
];
|
|
125
|
+
})
|
|
126
|
+
.filter(
|
|
127
|
+
(entry): entry is [string, Record<string, { id: string; chunks: string[]; name: string; async: true }>] =>
|
|
128
|
+
Boolean(entry),
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
};
|
|
93
132
|
const vendorMap = Object.fromEntries(
|
|
94
133
|
vendorEntries.map(({ specifier, absPath }) => [specifier, clientBundle.entryUrlsByAbsPath.get(absPath) ?? ""]),
|
|
95
134
|
) as Record<VendorSpecifier, string>;
|
|
96
135
|
this.#app.verbose(`[base-artifact] rscClientUrl=${rscClientUrl} vendors=${Object.keys(vendorMap).length}`);
|
|
97
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
rscClientUrl,
|
|
138
|
+
rscRuntimeClientManifest: clientBundle.manifest,
|
|
139
|
+
rscRuntimeSsrManifest,
|
|
140
|
+
vendorMap,
|
|
141
|
+
};
|
|
98
142
|
}
|
|
99
143
|
async #resolveAkanServerPath() {
|
|
100
144
|
const candidates: string[] = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1-rc.1",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@langchain/openai": "^1.4.6",
|
|
33
33
|
"@tailwindcss/node": "^4.3.0",
|
|
34
34
|
"@trapezedev/project": "^7.1.4",
|
|
35
|
-
"akanjs": "2.3.
|
|
35
|
+
"akanjs": "2.3.1-rc.1",
|
|
36
36
|
"chalk": "^5.6.2",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"daisyui": "^5.5.20",
|