@akanjs/devkit 2.1.0-rc.9 → 2.1.1-rc.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/akanConfig/akanConfig.test.ts +45 -9
- package/akanConfig/akanConfig.ts +22 -2
- package/applicationBuildRunner.test.ts +10 -0
- package/applicationBuildRunner.ts +15 -3
- package/applicationTestPreload.ts +40 -0
- package/artifact/implicitRootLayout.test.ts +75 -0
- package/artifact/implicitRootLayout.ts +41 -3
- package/artifact/routeSeedIndex.test.ts +8 -8
- package/cloud/cloudApi.ts +101 -0
- package/cloud/index.ts +1 -0
- package/constants.ts +10 -3
- package/devkitUtils.test.ts +21 -0
- package/executors.ts +0 -2
- package/frontendBuild/buildRouteClient.test.ts +5 -5
- package/frontendBuild/frontendBuild.test.ts +27 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +10 -3
- package/index.ts +2 -0
- package/package.json +3 -2
|
@@ -25,8 +25,8 @@ const packageJson: PackageJson = {
|
|
|
25
25
|
|
|
26
26
|
const app = { name: "portal" } as never;
|
|
27
27
|
const baseDevEnv = {
|
|
28
|
-
repoName: "
|
|
29
|
-
serveDomain: "
|
|
28
|
+
repoName: "akanjs",
|
|
29
|
+
serveDomain: "akanjs.com",
|
|
30
30
|
env: "debug" as const,
|
|
31
31
|
portOffset: 0,
|
|
32
32
|
workspaceRoot: "/workspace",
|
|
@@ -37,9 +37,9 @@ describe("AkanAppConfig", () => {
|
|
|
37
37
|
const config = new AkanAppConfig(app, ["shared"], packageJson, {}, baseDevEnv);
|
|
38
38
|
|
|
39
39
|
expect([...config.domains].sort()).toEqual([
|
|
40
|
-
"portal-debug.
|
|
41
|
-
"portal-develop.
|
|
42
|
-
"portal-main.
|
|
40
|
+
"portal-debug.akanjs.com",
|
|
41
|
+
"portal-develop.akanjs.com",
|
|
42
|
+
"portal-main.akanjs.com",
|
|
43
43
|
]);
|
|
44
44
|
expect(config.basePaths.size).toBe(0);
|
|
45
45
|
expect(config.i18n.defaultLocale).toBe("en");
|
|
@@ -95,10 +95,10 @@ describe("AkanAppConfig", () => {
|
|
|
95
95
|
expect([...config.domains].sort()).toEqual(["qa.root.local", "root.local"]);
|
|
96
96
|
expect([...config.basePaths]).toEqual(["admin"]);
|
|
97
97
|
expect([...(config.subRoutes.get("admin") ?? [])].sort()).toEqual([
|
|
98
|
-
"admin-debug.
|
|
99
|
-
"admin-develop.
|
|
100
|
-
"admin-main.
|
|
101
|
-
"admin-qa.
|
|
98
|
+
"admin-debug.akanjs.com",
|
|
99
|
+
"admin-develop.akanjs.com",
|
|
100
|
+
"admin-main.akanjs.com",
|
|
101
|
+
"admin-qa.akanjs.com",
|
|
102
102
|
"admin.local",
|
|
103
103
|
"admin.main.local",
|
|
104
104
|
]);
|
|
@@ -132,6 +132,7 @@ describe("AkanAppConfig", () => {
|
|
|
132
132
|
react: "19.0.0",
|
|
133
133
|
"react-dom": "19.0.0",
|
|
134
134
|
"react-server-dom-webpack": "19.0.0",
|
|
135
|
+
croner: akanPackageJson.peerDependencies?.croner,
|
|
135
136
|
sharp: "1.0.0",
|
|
136
137
|
"@external/runtime": "2.0.0",
|
|
137
138
|
},
|
|
@@ -168,10 +169,45 @@ describe("AkanAppConfig", () => {
|
|
|
168
169
|
react: runtimeDependencies.react,
|
|
169
170
|
"react-dom": runtimeDependencies["react-dom"],
|
|
170
171
|
"react-server-dom-webpack": runtimeDependencies["react-server-dom-webpack"],
|
|
172
|
+
croner: runtimeDependencies.croner,
|
|
171
173
|
sharp: runtimeDependencies.sharp,
|
|
172
174
|
});
|
|
173
175
|
});
|
|
174
176
|
|
|
177
|
+
test("adds backend runtime packages by database mode", () => {
|
|
178
|
+
const runtimeDependencies = { ...akanPackageJson.dependencies, ...akanPackageJson.peerDependencies };
|
|
179
|
+
const singleConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "single" }, baseDevEnv);
|
|
180
|
+
const multipleConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "multiple" }, baseDevEnv);
|
|
181
|
+
const clusterConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "cluster" }, baseDevEnv);
|
|
182
|
+
|
|
183
|
+
expect(singleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
184
|
+
croner: runtimeDependencies.croner,
|
|
185
|
+
});
|
|
186
|
+
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("ioredis");
|
|
187
|
+
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("bullmq");
|
|
188
|
+
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("@libsql/client");
|
|
189
|
+
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("postgres");
|
|
190
|
+
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("protobufjs");
|
|
191
|
+
|
|
192
|
+
expect(multipleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
193
|
+
"@libsql/client": runtimeDependencies["@libsql/client"],
|
|
194
|
+
bullmq: runtimeDependencies.bullmq,
|
|
195
|
+
croner: runtimeDependencies.croner,
|
|
196
|
+
ioredis: runtimeDependencies.ioredis,
|
|
197
|
+
protobufjs: runtimeDependencies.protobufjs,
|
|
198
|
+
});
|
|
199
|
+
expect(multipleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("postgres");
|
|
200
|
+
|
|
201
|
+
expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
202
|
+
bullmq: runtimeDependencies.bullmq,
|
|
203
|
+
croner: runtimeDependencies.croner,
|
|
204
|
+
ioredis: runtimeDependencies.ioredis,
|
|
205
|
+
postgres: runtimeDependencies.postgres,
|
|
206
|
+
protobufjs: runtimeDependencies.protobufjs,
|
|
207
|
+
});
|
|
208
|
+
expect(clusterConfig.getProductionPackageJson().dependencies).not.toHaveProperty("@libsql/client");
|
|
209
|
+
});
|
|
210
|
+
|
|
175
211
|
test("normalizes multiple mobile targets and validates base paths", () => {
|
|
176
212
|
const config = new AkanAppConfig(
|
|
177
213
|
app,
|
package/akanConfig/akanConfig.ts
CHANGED
|
@@ -50,7 +50,18 @@ const DEFAULT_OPTIMIZE_IMPORTS = [
|
|
|
50
50
|
const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] as const;
|
|
51
51
|
const SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"] as const;
|
|
52
52
|
const NATIVE_RUNTIME_PACKAGES = ["sharp"] as const;
|
|
53
|
-
const
|
|
53
|
+
const DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"] as const;
|
|
54
|
+
const DATABASE_MODE_RUNTIME_PACKAGES = {
|
|
55
|
+
single: [],
|
|
56
|
+
multiple: ["@libsql/client", "bullmq", "ioredis", "protobufjs"],
|
|
57
|
+
cluster: ["bullmq", "ioredis", "postgres", "protobufjs"],
|
|
58
|
+
} satisfies Record<DatabaseMode, readonly string[]>;
|
|
59
|
+
const AKAN_RUNTIME_PACKAGES = new Set<string>([
|
|
60
|
+
...SSR_RUNTIME_PACKAGES,
|
|
61
|
+
...NATIVE_RUNTIME_PACKAGES,
|
|
62
|
+
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
63
|
+
...Object.values(DATABASE_MODE_RUNTIME_PACKAGES).flat(),
|
|
64
|
+
]);
|
|
54
65
|
const DEFAULT_AKAN_IMAGE_CONFIG: AkanImageConfig = {
|
|
55
66
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
|
56
67
|
imageSizes: [32, 48, 64, 96, 128, 256, 384],
|
|
@@ -272,6 +283,15 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
272
283
|
if (AKAN_RUNTIME_PACKAGES.has(lib))
|
|
273
284
|
return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
|
|
274
285
|
}
|
|
286
|
+
#getProductionRuntimePackages() {
|
|
287
|
+
return [
|
|
288
|
+
...this.externalLibs,
|
|
289
|
+
...SSR_RUNTIME_PACKAGES,
|
|
290
|
+
...NATIVE_RUNTIME_PACKAGES,
|
|
291
|
+
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
292
|
+
...DATABASE_MODE_RUNTIME_PACKAGES[this.defaultDatabaseMode],
|
|
293
|
+
];
|
|
294
|
+
}
|
|
275
295
|
getProductionPackageJson(data: Partial<PackageJson> = {}): PackageJson {
|
|
276
296
|
return {
|
|
277
297
|
name: this.app.name,
|
|
@@ -279,7 +299,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
279
299
|
version: "1.0.0",
|
|
280
300
|
main: "./main.js",
|
|
281
301
|
dependencies: Object.fromEntries(
|
|
282
|
-
[...new Set(
|
|
302
|
+
[...new Set(this.#getProductionRuntimePackages())].map((lib) => {
|
|
283
303
|
const version = this.#resolveProductionDependencyVersion(lib);
|
|
284
304
|
if (!version) throw new Error(`Dependency ${lib} not found in package.json`);
|
|
285
305
|
return [lib, version];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { AKAN_OPTIONAL_BACKEND_EXTERNALS } from "./applicationBuildRunner";
|
|
3
|
+
|
|
4
|
+
describe("ApplicationBuildRunner", () => {
|
|
5
|
+
test("externalizes Akan optional backend dependencies", () => {
|
|
6
|
+
expect(AKAN_OPTIONAL_BACKEND_EXTERNALS).toEqual(
|
|
7
|
+
expect.arrayContaining(["@libsql/client", "bullmq", "croner", "ioredis", "postgres", "protobufjs"]),
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -47,10 +47,21 @@ const SSR_RENDER_EXTERNALS = [
|
|
|
47
47
|
"react/jsx-dev-runtime",
|
|
48
48
|
"react-dom",
|
|
49
49
|
"react-dom/server.browser",
|
|
50
|
+
"react-server-dom-webpack",
|
|
51
|
+
"react-server-dom-webpack/server.node",
|
|
50
52
|
"react-server-dom-webpack/client.node",
|
|
51
53
|
"react-server-dom-webpack/client.browser",
|
|
52
54
|
] as const;
|
|
53
55
|
|
|
56
|
+
export const AKAN_OPTIONAL_BACKEND_EXTERNALS = [
|
|
57
|
+
"@libsql/client",
|
|
58
|
+
"bullmq",
|
|
59
|
+
"croner",
|
|
60
|
+
"ioredis",
|
|
61
|
+
"postgres",
|
|
62
|
+
"protobufjs",
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
54
65
|
export class ApplicationBuildRunner {
|
|
55
66
|
#app: App;
|
|
56
67
|
#fast: boolean;
|
|
@@ -161,7 +172,9 @@ export class ApplicationBuildRunner {
|
|
|
161
172
|
|
|
162
173
|
async #buildBackend() {
|
|
163
174
|
const akanConfig = await this.#app.getConfig();
|
|
164
|
-
const backendExternals = [
|
|
175
|
+
const backendExternals = [
|
|
176
|
+
...new Set([...akanConfig.externalLibs, ...SSR_RENDER_EXTERNALS, ...AKAN_OPTIONAL_BACKEND_EXTERNALS]),
|
|
177
|
+
];
|
|
165
178
|
const backendEntryPoints = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
|
|
166
179
|
for (const entrypoint of backendEntryPoints) {
|
|
167
180
|
if (!(await Bun.file(entrypoint).exists())) throw new Error(`Backend entrypoint not found: ${entrypoint}`);
|
|
@@ -183,8 +196,7 @@ export class ApplicationBuildRunner {
|
|
|
183
196
|
conditions: ["react-server"],
|
|
184
197
|
// `akan build` must embed production react-server-dom regardless of the shell's NODE_ENV.
|
|
185
198
|
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
186
|
-
plugins:
|
|
187
|
-
akanConfig.externalLibs.length > 0 ? [this.#createExternalSpecifiersPlugin(akanConfig.externalLibs)] : [],
|
|
199
|
+
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
188
200
|
});
|
|
189
201
|
return {
|
|
190
202
|
entrypoints: backendEntryPoints.length + 1,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export const SIGNAL_TEST_PRELOAD_PATH = "test/signalTest.preload.ts";
|
|
4
|
+
|
|
5
|
+
export interface SignalTestPreloadTarget {
|
|
6
|
+
cwdPath: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function resolveSignalTestPreloadPath(target: SignalTestPreloadTarget) {
|
|
10
|
+
const candidates: string[] = [];
|
|
11
|
+
const addResolvedPackageCandidate = (basePath: string) => {
|
|
12
|
+
try {
|
|
13
|
+
candidates.push(
|
|
14
|
+
path.join(path.dirname(Bun.resolveSync("akanjs/package.json", basePath)), SIGNAL_TEST_PRELOAD_PATH),
|
|
15
|
+
);
|
|
16
|
+
} catch {
|
|
17
|
+
// Source workspaces and published installs can resolve Akan packages from different roots.
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
addResolvedPackageCandidate(target.cwdPath);
|
|
22
|
+
addResolvedPackageCandidate(process.cwd());
|
|
23
|
+
addResolvedPackageCandidate(path.dirname(Bun.main));
|
|
24
|
+
addResolvedPackageCandidate(import.meta.dir);
|
|
25
|
+
|
|
26
|
+
candidates.push(
|
|
27
|
+
path.join(target.cwdPath, "../../node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
28
|
+
path.join(target.cwdPath, "../../pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
29
|
+
path.join(process.cwd(), "node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
30
|
+
path.join(process.cwd(), "pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
31
|
+
path.join(path.dirname(Bun.main), "../../akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
32
|
+
path.resolve(import.meta.dir, "../../akanjs", SIGNAL_TEST_PRELOAD_PATH),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
36
|
+
if (await Bun.file(candidate).exists()) return candidate;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}`);
|
|
40
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { resolveSsrPageEntries } from "./implicitRootLayout";
|
|
6
|
+
|
|
7
|
+
const tempRoots: string[] = [];
|
|
8
|
+
|
|
9
|
+
const makeTempRoot = async () => {
|
|
10
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "akan-implicit-root-layout-"));
|
|
11
|
+
tempRoots.push(root);
|
|
12
|
+
return root;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const write = async (filePath: string, content: string) => {
|
|
16
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
17
|
+
await writeFile(filePath, content);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("resolveSsrPageEntries", () => {
|
|
25
|
+
test("inherits root layout static exports for grouped root boundaries", async () => {
|
|
26
|
+
const appRoot = await makeTempRoot();
|
|
27
|
+
const pageRoot = path.join(appRoot, "page");
|
|
28
|
+
const rootLayoutPath = path.join(pageRoot, "_layout.tsx");
|
|
29
|
+
const groupedLayoutPath = path.join(pageRoot, "(home)", "_layout.tsx");
|
|
30
|
+
|
|
31
|
+
await write(path.join(appRoot, "env", "env.client.ts"), "export const env = {};\n");
|
|
32
|
+
await write(rootLayoutPath, 'export const theme = "dark";\nexport const fonts = [{ name: "pretendard" }];\n');
|
|
33
|
+
await write(groupedLayoutPath, "export default function Layout({ children }) { return children; }\n");
|
|
34
|
+
|
|
35
|
+
const entries = await resolveSsrPageEntries({
|
|
36
|
+
appCwdPath: appRoot,
|
|
37
|
+
appName: "demo",
|
|
38
|
+
pageKeys: ["./_layout.tsx", "./(home)/_layout.tsx", "./(home)/_index.tsx"],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const groupedRoot = entries.find((entry) => entry.key === "./(home)/__root_layout.tsx");
|
|
42
|
+
expect(groupedRoot).toBeDefined();
|
|
43
|
+
expect(groupedRoot?.seedAbsPaths).toContain(rootLayoutPath);
|
|
44
|
+
expect(groupedRoot?.seedAbsPaths).toContain(groupedLayoutPath);
|
|
45
|
+
|
|
46
|
+
const generatedSource = await Bun.file(groupedRoot?.moduleAbsPath ?? "").text();
|
|
47
|
+
expect(generatedSource).toContain('import * as inheritedLayout from "../../../page/_layout.tsx";');
|
|
48
|
+
expect(generatedSource).not.toContain("<System.Provider");
|
|
49
|
+
expect(generatedSource).toContain(
|
|
50
|
+
"<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("keeps system provider for grouped root boundaries without an ancestor root", async () => {
|
|
55
|
+
const appRoot = await makeTempRoot();
|
|
56
|
+
const pageRoot = path.join(appRoot, "page");
|
|
57
|
+
const groupedLayoutPath = path.join(pageRoot, "(home)", "_layout.tsx");
|
|
58
|
+
|
|
59
|
+
await write(path.join(appRoot, "env", "env.client.ts"), "export const env = {};\n");
|
|
60
|
+
await write(groupedLayoutPath, 'export const theme = "dark";\n');
|
|
61
|
+
|
|
62
|
+
const entries = await resolveSsrPageEntries({
|
|
63
|
+
appCwdPath: appRoot,
|
|
64
|
+
appName: "demo",
|
|
65
|
+
pageKeys: ["./(home)/_layout.tsx", "./(home)/_index.tsx"],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const groupedRoot = entries.find((entry) => entry.key === "./(home)/__root_layout.tsx");
|
|
69
|
+
expect(groupedRoot).toBeDefined();
|
|
70
|
+
|
|
71
|
+
const generatedSource = await Bun.file(groupedRoot?.moduleAbsPath ?? "").text();
|
|
72
|
+
expect(generatedSource).toContain("<System.Provider");
|
|
73
|
+
expect(generatedSource).toContain("theme={userLayout.theme ?? inheritedLayout.theme}");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -68,6 +68,23 @@ function findRootBoundaries(pageKeys: string[], appCwdPath: string, basePaths: I
|
|
|
68
68
|
return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function hasAncestorRootBoundary(boundary: RootBoundary, boundaries: RootBoundary[]): boolean {
|
|
72
|
+
return boundaries.some(
|
|
73
|
+
(candidate) =>
|
|
74
|
+
candidate !== boundary &&
|
|
75
|
+
candidate.segments.length < boundary.segments.length &&
|
|
76
|
+
candidate.segments.every((segment, index) => boundary.segments[index] === segment),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findExplicitRootLayoutAbsPath(pageKeys: string[], appCwdPath: string): string | null {
|
|
81
|
+
const rootLayoutKey = pageKeys.find((key) => {
|
|
82
|
+
const segments = getRootBoundarySegments(key);
|
|
83
|
+
return segments !== null && segments.length === 0;
|
|
84
|
+
});
|
|
85
|
+
return rootLayoutKey ? path.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
function routePrefixForSegments(segments: string[]): string | null {
|
|
72
89
|
const visible = segments.filter((segment) => !/^\(.+\)$/.test(segment));
|
|
73
90
|
return visible[0] ?? null;
|
|
@@ -86,7 +103,9 @@ async function writeGeneratedRootLayoutFile(opts: {
|
|
|
86
103
|
appCwdPath: string;
|
|
87
104
|
appName: string;
|
|
88
105
|
boundary: RootBoundary;
|
|
106
|
+
rootSourceAbsPath: string | null;
|
|
89
107
|
includeStInit: boolean;
|
|
108
|
+
includeSystemProvider: boolean;
|
|
90
109
|
}): Promise<string> {
|
|
91
110
|
await assertEnvClientConvention(opts.appCwdPath, opts.appName);
|
|
92
111
|
const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
|
|
@@ -95,14 +114,29 @@ async function writeGeneratedRootLayoutFile(opts: {
|
|
|
95
114
|
? path.relative(path.dirname(absPath), opts.boundary.sourceAbsPath).split(path.sep).join("/")
|
|
96
115
|
: null;
|
|
97
116
|
const sourceSpecifier = sourceRel ? (sourceRel.startsWith(".") ? sourceRel : `./${sourceRel}`) : null;
|
|
117
|
+
const inheritedSourceAbsPath =
|
|
118
|
+
opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
|
|
119
|
+
const inheritedSourceRel = inheritedSourceAbsPath
|
|
120
|
+
? path.relative(path.dirname(absPath), inheritedSourceAbsPath).split(path.sep).join("/")
|
|
121
|
+
: null;
|
|
122
|
+
const inheritedSourceSpecifier = inheritedSourceRel
|
|
123
|
+
? inheritedSourceRel.startsWith(".")
|
|
124
|
+
? inheritedSourceRel
|
|
125
|
+
: `./${inheritedSourceRel}`
|
|
126
|
+
: null;
|
|
98
127
|
const clientImport = opts.includeStInit
|
|
99
128
|
? `import { st } from "@apps/${opts.appName}/client";\nvoid st;\n`
|
|
100
129
|
: `import "@apps/${opts.appName}/client";\n`;
|
|
130
|
+
const inheritedImport = inheritedSourceSpecifier
|
|
131
|
+
? `import * as inheritedLayout from ${JSON.stringify(inheritedSourceSpecifier)};\n`
|
|
132
|
+
: "const inheritedLayout = {};\n";
|
|
101
133
|
const prefix = routePrefixForSegments(opts.boundary.segments);
|
|
102
134
|
const userImport = sourceSpecifier
|
|
103
135
|
? `import UserLayout, * as userLayout from ${JSON.stringify(sourceSpecifier)};\n`
|
|
104
136
|
: "const UserLayout = ({ children }) => children;\nconst userLayout = {};\n";
|
|
105
|
-
const source =
|
|
137
|
+
const source = opts.includeSystemProvider
|
|
138
|
+
? `import type { LayoutProps, PageProps } from "akanjs/client";\nimport { loadFonts } from "akanjs/client";\nimport { System } from "akanjs/ui";\nimport { env } from "@apps/${opts.appName}/env/env.client";\n${clientImport}${inheritedImport}${userImport}\nconst userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];\nconst defaultFonts = userFonts.filter((font) => font.default);\nif (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");\nconst defaultFont = defaultFonts[0];\nconst defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;\n\nexport async function generateHead(props: PageProps) {\n if (userLayout.generateHead) return userLayout.generateHead(props);\n if (userLayout.head !== undefined) return userLayout.head;\n if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);\n return inheritedLayout.head;\n}\n\nexport default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {\n return (\n <System.Provider\n of={GeneratedLayout as never}\n appName=${JSON.stringify(opts.appName)}\n ${prefix ? `prefix=${JSON.stringify(prefix)}\n ` : ""}params={params}\n manifest={userLayout.manifest ?? inheritedLayout.manifest}\n env={env}\n theme={userLayout.theme ?? inheritedLayout.theme}\n fonts={loadFonts(userFonts)}\n className={defaultFontClassName}\n gaTrackingId={userLayout.gaTrackingId ?? inheritedLayout.gaTrackingId}\n layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}\n reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}\n >\n <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>\n </System.Provider>\n );\n}\n`
|
|
139
|
+
: `import type { LayoutProps, PageProps } from "akanjs/client";\n${inheritedImport}${userImport}\nexport async function generateHead(props: PageProps) {\n if (userLayout.generateHead) return userLayout.generateHead(props);\n if (userLayout.head !== undefined) return userLayout.head;\n if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);\n return inheritedLayout.head;\n}\n\nexport default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {\n return <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>;\n}\n`;
|
|
106
140
|
await Bun.write(absPath, source);
|
|
107
141
|
return absPath;
|
|
108
142
|
}
|
|
@@ -120,6 +154,8 @@ export async function resolveSsrPageEntries(opts: {
|
|
|
120
154
|
const absPageDir = path.resolve(opts.appCwdPath, "page");
|
|
121
155
|
const hasSt = await appHasStModule(opts.appCwdPath);
|
|
122
156
|
const basePaths = opts.basePaths ?? [];
|
|
157
|
+
const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
|
|
158
|
+
const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
|
|
123
159
|
const rootLayoutKeys = new Set(
|
|
124
160
|
opts.pageKeys.filter((key) => {
|
|
125
161
|
const segments = getRootBoundarySegments(key);
|
|
@@ -133,15 +169,17 @@ export async function resolveSsrPageEntries(opts: {
|
|
|
133
169
|
moduleAbsPath: path.resolve(absPageDir, key),
|
|
134
170
|
}));
|
|
135
171
|
const generated = await Promise.all(
|
|
136
|
-
|
|
172
|
+
rootBoundaries.map(async (boundary) => ({
|
|
137
173
|
key: implicitRootLayoutKey(boundary.segments),
|
|
138
174
|
moduleAbsPath: await writeGeneratedRootLayoutFile({
|
|
139
175
|
appCwdPath: opts.appCwdPath,
|
|
140
176
|
appName: opts.appName,
|
|
141
177
|
boundary,
|
|
178
|
+
rootSourceAbsPath,
|
|
142
179
|
includeStInit: hasSt && boundary.segments.length === 0,
|
|
180
|
+
includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries),
|
|
143
181
|
}),
|
|
144
|
-
seedAbsPaths:
|
|
182
|
+
seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))],
|
|
145
183
|
})),
|
|
146
184
|
);
|
|
147
185
|
const entries = [...base, ...generated];
|
|
@@ -46,7 +46,7 @@ describe("computeRouteSeedIndex", () => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
test("serializes seed paths relative to the artifact directory", () => {
|
|
49
|
-
const artifactDir = "/repo/dist/apps/
|
|
49
|
+
const artifactDir = "/repo/dist/apps/akan/.akan/artifact";
|
|
50
50
|
const serialized = serializeRouteSeedIndexForArtifact(
|
|
51
51
|
{
|
|
52
52
|
entries: [
|
|
@@ -54,36 +54,36 @@ describe("computeRouteSeedIndex", () => {
|
|
|
54
54
|
routeId: "/profile",
|
|
55
55
|
pattern: "/profile",
|
|
56
56
|
seeds: [
|
|
57
|
-
"/repo/dist/apps/
|
|
58
|
-
"/repo/apps/
|
|
57
|
+
"/repo/dist/apps/akan/.akan/generated/implicit-root-layout.tsx",
|
|
58
|
+
"/repo/apps/akan/page/profile.tsx",
|
|
59
59
|
],
|
|
60
60
|
},
|
|
61
61
|
],
|
|
62
|
-
globalLayoutFiles: ["/repo/dist/apps/
|
|
62
|
+
globalLayoutFiles: ["/repo/dist/apps/akan/.akan/generated/implicit-root-layout.tsx"],
|
|
63
63
|
},
|
|
64
64
|
artifactDir,
|
|
65
65
|
);
|
|
66
66
|
|
|
67
67
|
expect(serialized.entries[0]?.seeds).toEqual([
|
|
68
68
|
"../generated/implicit-root-layout.tsx",
|
|
69
|
-
"../../../../../apps/
|
|
69
|
+
"../../../../../apps/akan/page/profile.tsx",
|
|
70
70
|
]);
|
|
71
71
|
expect(serialized.globalLayoutFiles).toEqual(["../generated/implicit-root-layout.tsx"]);
|
|
72
72
|
expect(JSON.stringify(serialized)).not.toContain("/repo/");
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
test("omits source seed metadata for production artifacts", () => {
|
|
76
|
-
const artifactDir = "/repo/dist/apps/
|
|
76
|
+
const artifactDir = "/repo/dist/apps/akan/.akan/artifact";
|
|
77
77
|
const serialized = serializeRouteSeedIndexForArtifact(
|
|
78
78
|
{
|
|
79
79
|
entries: [
|
|
80
80
|
{
|
|
81
81
|
routeId: "/:lang/profile",
|
|
82
82
|
pattern: "/:lang/profile",
|
|
83
|
-
seeds: ["/repo/apps/
|
|
83
|
+
seeds: ["/repo/apps/akan/page/profile.tsx"],
|
|
84
84
|
},
|
|
85
85
|
],
|
|
86
|
-
globalLayoutFiles: ["/repo/apps/
|
|
86
|
+
globalLayoutFiles: ["/repo/apps/akan/.akan/generated/implicit-root-layout.tsx"],
|
|
87
87
|
},
|
|
88
88
|
artifactDir,
|
|
89
89
|
{ production: true },
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
interface AccessToken {
|
|
2
|
+
jwt: string;
|
|
3
|
+
refreshToken: string;
|
|
4
|
+
expiresAt: Date;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
class HttpClient {
|
|
8
|
+
readonly baseUrl: string;
|
|
9
|
+
constructor(baseUrl: string) {
|
|
10
|
+
this.baseUrl = baseUrl;
|
|
11
|
+
}
|
|
12
|
+
async get<T>(
|
|
13
|
+
url: string,
|
|
14
|
+
{ headers }: { headers?: Record<string, string> } = {},
|
|
15
|
+
): Promise<T> {
|
|
16
|
+
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
17
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
18
|
+
});
|
|
19
|
+
return response.json();
|
|
20
|
+
}
|
|
21
|
+
async post<T>(
|
|
22
|
+
url: string,
|
|
23
|
+
data: unknown,
|
|
24
|
+
{ headers }: { headers?: Record<string, string> } = {},
|
|
25
|
+
): Promise<T> {
|
|
26
|
+
const isFormData = data instanceof FormData;
|
|
27
|
+
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
body: isFormData ? data : JSON.stringify(data),
|
|
30
|
+
headers: isFormData
|
|
31
|
+
? headers
|
|
32
|
+
: { "Content-Type": "application/json", ...headers },
|
|
33
|
+
});
|
|
34
|
+
return response.json();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class CloudApi {
|
|
39
|
+
readonly api: HttpClient;
|
|
40
|
+
#accessToken: AccessToken | null = null;
|
|
41
|
+
constructor(
|
|
42
|
+
host: string,
|
|
43
|
+
{ accessToken }: { accessToken?: AccessToken } = {},
|
|
44
|
+
) {
|
|
45
|
+
this.api = new HttpClient(`${host}/api`);
|
|
46
|
+
this.#accessToken = accessToken ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async uploadEnv(
|
|
50
|
+
devProjectId: string,
|
|
51
|
+
fileStream: ReadableStream,
|
|
52
|
+
): Promise<boolean> {
|
|
53
|
+
const formData = new FormData();
|
|
54
|
+
formData.append("devProjectId", devProjectId);
|
|
55
|
+
formData.append("fileStream", await new Response(fileStream).blob());
|
|
56
|
+
const response = await this.api.post<{ success: boolean }>(
|
|
57
|
+
`/uploadEnv/${devProjectId}`,
|
|
58
|
+
formData,
|
|
59
|
+
);
|
|
60
|
+
return response.success;
|
|
61
|
+
}
|
|
62
|
+
async downloadEnv(devProjectId: string): Promise<boolean> {
|
|
63
|
+
const response = await this.api.get<{ success: boolean }>(
|
|
64
|
+
`/downloadEnv/${devProjectId}`,
|
|
65
|
+
);
|
|
66
|
+
return response.success;
|
|
67
|
+
}
|
|
68
|
+
async getRemoteAuthToken(remoteId: string): Promise<AccessToken> {
|
|
69
|
+
if (this.#needRefreshToken()) return await this.refreshAuthToken();
|
|
70
|
+
else if (this.#accessToken) return this.#accessToken;
|
|
71
|
+
const accessToken = await this.api.get<AccessToken>(
|
|
72
|
+
`/getRemoteAuthToken/${remoteId}`,
|
|
73
|
+
);
|
|
74
|
+
this.#accessToken = {
|
|
75
|
+
jwt: accessToken.jwt,
|
|
76
|
+
refreshToken: accessToken.refreshToken,
|
|
77
|
+
expiresAt: new Date(accessToken.expiresAt),
|
|
78
|
+
};
|
|
79
|
+
return accessToken;
|
|
80
|
+
}
|
|
81
|
+
async refreshAuthToken(): Promise<AccessToken> {
|
|
82
|
+
const response = await this.api.post<AccessToken>(
|
|
83
|
+
`/refreshRemoteAuthToken`,
|
|
84
|
+
{
|
|
85
|
+
refreshToken: this.#accessToken?.refreshToken,
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
this.#accessToken = {
|
|
89
|
+
jwt: response.jwt,
|
|
90
|
+
refreshToken: response.refreshToken,
|
|
91
|
+
expiresAt: new Date(response.expiresAt),
|
|
92
|
+
};
|
|
93
|
+
return response;
|
|
94
|
+
}
|
|
95
|
+
#needRefreshToken(): boolean {
|
|
96
|
+
return !!(
|
|
97
|
+
this.#accessToken?.expiresAt &&
|
|
98
|
+
this.#accessToken.expiresAt.getTime() < Date.now() - 1000 * 60 * 60
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
package/cloud/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./cloudApi";
|
package/constants.ts
CHANGED
|
@@ -3,8 +3,12 @@ import type { SupportedLlmModel } from "./aiEditor";
|
|
|
3
3
|
export const basePath = `${Bun.env.HOME ?? Bun.env.USERPROFILE}/.akan`;
|
|
4
4
|
export const configPath = `${basePath}/config.json`;
|
|
5
5
|
export const akanCloudHost =
|
|
6
|
-
process.env.AKAN_PUBLIC_OPERATION_MODE === "local"
|
|
7
|
-
|
|
6
|
+
process.env.AKAN_PUBLIC_OPERATION_MODE === "local"
|
|
7
|
+
? "http://localhost"
|
|
8
|
+
: "https://cloud.akanjs.com";
|
|
9
|
+
export const akanCloudUrl = `${akanCloudHost}${
|
|
10
|
+
process.env.AKAN_PUBLIC_OPERATION_MODE === "local" ? ":8282" : ""
|
|
11
|
+
}/api`;
|
|
8
12
|
|
|
9
13
|
export interface HostConfig {
|
|
10
14
|
auth?: {
|
|
@@ -22,4 +26,7 @@ export interface AkanGlobalConfig {
|
|
|
22
26
|
apiKey: string;
|
|
23
27
|
} | null;
|
|
24
28
|
}
|
|
25
|
-
export const defaultAkanGlobalConfig: AkanGlobalConfig = {
|
|
29
|
+
export const defaultAkanGlobalConfig: AkanGlobalConfig = {
|
|
30
|
+
cloudHost: {},
|
|
31
|
+
llm: null,
|
|
32
|
+
};
|
package/devkitUtils.test.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { ApplicationBuildReporter } from "./applicationBuildReporter";
|
|
6
|
+
import { resolveSignalTestPreloadPath } from "./applicationTestPreload";
|
|
6
7
|
import { TypeScriptDependencyScanner } from "./dependencyScanner";
|
|
7
8
|
import { extractDependencies } from "./extractDeps";
|
|
8
9
|
import { getModelFileData } from "./getModelFileData";
|
|
@@ -84,6 +85,26 @@ describe("extractDependencies", () => {
|
|
|
84
85
|
});
|
|
85
86
|
});
|
|
86
87
|
|
|
88
|
+
describe("resolveSignalTestPreloadPath", () => {
|
|
89
|
+
test("resolves the preload file from an installed akanjs package", async () => {
|
|
90
|
+
const root = await makeTempRoot();
|
|
91
|
+
const libDir = path.join(root, "libs/shared");
|
|
92
|
+
await write(
|
|
93
|
+
path.join(root, "node_modules/akanjs/package.json"),
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
name: "akanjs",
|
|
96
|
+
version: "0.0.0",
|
|
97
|
+
exports: { "./package.json": "./package.json" },
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
await write(path.join(root, "node_modules/akanjs/test/signalTest.preload.ts"), "export {};\n");
|
|
101
|
+
|
|
102
|
+
await expect(resolveSignalTestPreloadPath({ cwdPath: libDir })).resolves.toContain(
|
|
103
|
+
"node_modules/akanjs/test/signalTest.preload.ts",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
87
108
|
describe("TypeScriptDependencyScanner", () => {
|
|
88
109
|
test("separates monorepo package, lib, runtime, and type-only dependencies", async () => {
|
|
89
110
|
const root = await makeTempRoot();
|
package/executors.ts
CHANGED
|
@@ -239,8 +239,6 @@ export class Executor {
|
|
|
239
239
|
this.logger = new Logger(name);
|
|
240
240
|
this.logs = [] as string[];
|
|
241
241
|
this.cwdPath = cwdPath;
|
|
242
|
-
//! TODO: 테스트 확인 필요
|
|
243
|
-
// if (!(await Bun.file(cwdPath).exists())) await mkdir(cwdPath, { recursive: true });
|
|
244
242
|
}
|
|
245
243
|
#stdout(data: Buffer) {
|
|
246
244
|
if (Executor.verbose) Logger.raw(chalk.dim(data.toString()));
|
|
@@ -29,14 +29,14 @@ describe("route client store bootstrap", () => {
|
|
|
29
29
|
test("wraps client entries with app client bootstrap before re-exporting components", () => {
|
|
30
30
|
const original = "/repo/pkgs/akanjs/ui/Model/NewWrapper_Client.tsx";
|
|
31
31
|
const source = RouteClientBuilder.createStoreBootstrapEntrySource({
|
|
32
|
-
appName: "
|
|
32
|
+
appName: "akan",
|
|
33
33
|
originalEntry: original,
|
|
34
34
|
exportNames: ["NewWrapper_Client", "default"],
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
expect(source).toBe(
|
|
38
38
|
[
|
|
39
|
-
'import "@apps/
|
|
39
|
+
'import "@apps/akan/client";',
|
|
40
40
|
`export { NewWrapper_Client } from ${JSON.stringify(original)};`,
|
|
41
41
|
`export { default } from ${JSON.stringify(original)};`,
|
|
42
42
|
"",
|
|
@@ -45,12 +45,12 @@ describe("route client store bootstrap", () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test("remaps wrapper manifest keys back to original client entry paths", () => {
|
|
48
|
-
const wrapper = "/repo/apps/
|
|
48
|
+
const wrapper = "/repo/apps/akan/.akan/generated/client-entry-bootstrap/NewWrapper_Client.tsx";
|
|
49
49
|
const original = "/repo/pkgs/akanjs/ui/Model/NewWrapper_Client.tsx";
|
|
50
50
|
const remapped = RouteClientBuilder.resolveOriginalManifestEntry(
|
|
51
|
-
"apps/
|
|
51
|
+
"apps/akan/.akan/generated/client-entry-bootstrap/NewWrapper_Client.tsx#NewWrapper_Client",
|
|
52
52
|
new Map([[wrapper, original]]),
|
|
53
|
-
new Map([[wrapper, "apps/
|
|
53
|
+
new Map([[wrapper, "apps/akan/.akan/generated/client-entry-bootstrap/NewWrapper_Client.tsx"]]),
|
|
54
54
|
"/repo",
|
|
55
55
|
);
|
|
56
56
|
|
|
@@ -10,6 +10,7 @@ import { CssImportResolver } from "./cssImportResolver";
|
|
|
10
10
|
import { HmrChangeClassifier } from "./hmrChangeClassifier";
|
|
11
11
|
import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
|
|
12
12
|
import { RoutesManifestArtifactSerializer } from "./routesManifestArtifactSerializer";
|
|
13
|
+
import { prepareCssAsset } from "./ssrBaseArtifactBuilder";
|
|
13
14
|
|
|
14
15
|
const tempRoots: string[] = [];
|
|
15
16
|
|
|
@@ -97,6 +98,32 @@ describe("CsrArtifactBuilder", () => {
|
|
|
97
98
|
});
|
|
98
99
|
});
|
|
99
100
|
|
|
101
|
+
describe("SsrBaseArtifactBuilder", () => {
|
|
102
|
+
test("minifies CSS assets only for production builds", async () => {
|
|
103
|
+
const css = [
|
|
104
|
+
".card {",
|
|
105
|
+
" color: red;",
|
|
106
|
+
" padding: 1rem;",
|
|
107
|
+
"}",
|
|
108
|
+
"",
|
|
109
|
+
"@media (width >= 768px) {",
|
|
110
|
+
" .card {",
|
|
111
|
+
" color: blue;",
|
|
112
|
+
" }",
|
|
113
|
+
"}",
|
|
114
|
+
"",
|
|
115
|
+
].join("\n");
|
|
116
|
+
|
|
117
|
+
const development = await prepareCssAsset("start", "", css);
|
|
118
|
+
const production = await prepareCssAsset("build", "", css);
|
|
119
|
+
|
|
120
|
+
expect(development).toBe(css);
|
|
121
|
+
expect(production.length).toBeLessThan(css.length);
|
|
122
|
+
expect(production).toContain(".card{");
|
|
123
|
+
expect(production).not.toContain("\n ");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
100
127
|
describe("RoutesManifestArtifactSerializer", () => {
|
|
101
128
|
test("serializes absolute artifact paths relative to artifact directory", () => {
|
|
102
129
|
const manifest = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { optimize } from "@tailwindcss/node";
|
|
2
3
|
import type { BaseBuildArtifact } from "akanjs/server";
|
|
3
4
|
import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
|
|
4
5
|
import { computeRouteSeedIndex, type RouteSeedIndex, saveRouteSeedIndex } from "../artifact/routeSeedIndex";
|
|
@@ -16,6 +17,11 @@ export interface BuildSsrBaseArtifactResult {
|
|
|
16
17
|
optimizedFonts: Awaited<ReturnType<FontOptimizer["optimize"]>>;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
export function prepareCssAsset(command: "build" | "start", basePath: string, cssText: string): string {
|
|
21
|
+
if (command !== "build") return cssText;
|
|
22
|
+
return optimize(cssText, { file: `${basePath || "root"}.css`, minify: true }).code;
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
export class SsrBaseArtifactBuilder {
|
|
20
26
|
#app: App;
|
|
21
27
|
#command: "build" | "start";
|
|
@@ -143,13 +149,14 @@ export class SsrBaseArtifactBuilder {
|
|
|
143
149
|
|
|
144
150
|
async #writeCssAsset(basePath: string, cssText: string) {
|
|
145
151
|
const cssAssetName = basePath || "root";
|
|
146
|
-
const
|
|
152
|
+
const preparedCssText = await prepareCssAsset(this.#command, basePath, cssText);
|
|
153
|
+
const cssHash = Bun.hash(`${basePath}\n${preparedCssText}`).toString(36);
|
|
147
154
|
const [cssRelPath, cssUrl] = [
|
|
148
155
|
`styles/${cssAssetName}-${cssHash}.css`,
|
|
149
156
|
`/_akan/styles/${cssAssetName}-${cssHash}.css`,
|
|
150
157
|
];
|
|
151
|
-
await Bun.write(path.join(this.#absArtifactDir, cssRelPath),
|
|
152
|
-
this.#app.verbose(`[base-artifact] wrote ${
|
|
158
|
+
await Bun.write(path.join(this.#absArtifactDir, cssRelPath), preparedCssText);
|
|
159
|
+
this.#app.verbose(`[base-artifact] wrote ${preparedCssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
|
|
153
160
|
return [basePath, { cssUrl, cssRelPath }] as const;
|
|
154
161
|
}
|
|
155
162
|
}
|
package/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./akanConfig";
|
|
|
4
4
|
export * from "./applicationBuildReporter";
|
|
5
5
|
export * from "./applicationBuildRunner";
|
|
6
6
|
export * from "./applicationReleasePackager";
|
|
7
|
+
export * from "./applicationTestPreload";
|
|
7
8
|
export * from "./artifact";
|
|
8
9
|
export * from "./auth";
|
|
9
10
|
export * from "./builder";
|
|
@@ -34,3 +35,4 @@ export * from "./types";
|
|
|
34
35
|
export * from "./ui";
|
|
35
36
|
export * from "./uploadRelease";
|
|
36
37
|
export * from "./useStdoutDimensions";
|
|
38
|
+
export * from "./cloud";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1-rc.0",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
"@langchain/core": "^1.1.47",
|
|
31
31
|
"@langchain/deepseek": "^1.0.26",
|
|
32
32
|
"@langchain/openai": "^1.4.6",
|
|
33
|
+
"@tailwindcss/node": "^4.3.0",
|
|
33
34
|
"@trapezedev/project": "^7.1.4",
|
|
34
|
-
"akanjs": "2.1.
|
|
35
|
+
"akanjs": "2.1.1-rc.0",
|
|
35
36
|
"chalk": "^5.6.2",
|
|
36
37
|
"commander": "^14.0.3",
|
|
37
38
|
"daisyui": "^5.5.20",
|