@akanjs/devkit 2.1.0 → 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.
@@ -25,8 +25,8 @@ const packageJson: PackageJson = {
25
25
 
26
26
  const app = { name: "portal" } as never;
27
27
  const baseDevEnv = {
28
- repoName: "akansoft",
29
- serveDomain: "akamir.com",
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.akamir.com",
41
- "portal-develop.akamir.com",
42
- "portal-main.akamir.com",
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.akamir.com",
99
- "admin-develop.akamir.com",
100
- "admin-main.akamir.com",
101
- "admin-qa.akamir.com",
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
  ]);
@@ -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 = `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}${userImport}\nconst userFonts = userLayout.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 return userLayout.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}\n env={env}\n theme={userLayout.theme}\n fonts={loadFonts(userFonts)}\n className={defaultFontClassName}\n gaTrackingId={userLayout.gaTrackingId}\n layoutStyle={userLayout.layoutStyle}\n reconnect={userLayout.reconnect ?? false}\n >\n <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>\n </System.Provider>\n );\n}\n`;
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
- findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths).map(async (boundary) => ({
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: boundary.sourceAbsPath ? [boundary.sourceAbsPath] : [],
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/angelo/.akan/artifact";
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/angelo/.akan/generated/implicit-root-layout.tsx",
58
- "/repo/apps/angelo/page/profile.tsx",
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/angelo/.akan/generated/implicit-root-layout.tsx"],
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/angelo/page/profile.tsx",
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/angelo/.akan/artifact";
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/angelo/page/profile.tsx"],
83
+ seeds: ["/repo/apps/akan/page/profile.tsx"],
84
84
  },
85
85
  ],
86
- globalLayoutFiles: ["/repo/apps/angelo/.akan/generated/implicit-root-layout.tsx"],
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" ? "http://localhost" : "https://cloud.akanjs.com";
7
- export const akanCloudUrl = `${akanCloudHost}${process.env.AKAN_PUBLIC_OPERATION_MODE === "local" ? ":8282" : ""}/메ㅑ`;
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 = { cloudHost: {}, llm: null };
29
+ export const defaultAkanGlobalConfig: AkanGlobalConfig = {
30
+ cloudHost: {},
31
+ llm: null,
32
+ };
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: "angelo",
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/angelo/client";',
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/angelo/.akan/generated/client-entry-bootstrap/NewWrapper_Client.tsx";
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/angelo/.akan/generated/client-entry-bootstrap/NewWrapper_Client.tsx#NewWrapper_Client",
51
+ "apps/akan/.akan/generated/client-entry-bootstrap/NewWrapper_Client.tsx#NewWrapper_Client",
52
52
  new Map([[wrapper, original]]),
53
- new Map([[wrapper, "apps/angelo/.akan/generated/client-entry-bootstrap/NewWrapper_Client.tsx"]]),
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 cssHash = Bun.hash(`${basePath}\n${cssText}`).toString(36);
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), cssText);
152
- this.#app.verbose(`[base-artifact] wrote ${cssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
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
@@ -35,3 +35,4 @@ export * from "./types";
35
35
  export * from "./ui";
36
36
  export * from "./uploadRelease";
37
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.0",
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.0",
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",