@akanjs/devkit 2.1.0 → 2.1.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/akanConfig/akanConfig.test.ts +9 -9
- package/artifact/implicitRootLayout.test.ts +75 -0
- package/artifact/implicitRootLayout.ts +41 -3
- package/artifact/routeSeedIndex.test.ts +8 -8
- package/cloud/cloudApi.ts +102 -0
- package/cloud/index.ts +1 -0
- package/constants.ts +10 -3
- 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 +1 -0
- package/package.json +4 -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
|
]);
|
|
@@ -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,102 @@
|
|
|
1
|
+
import dayjs, { type Dayjs } from "dayjs";
|
|
2
|
+
|
|
3
|
+
interface AccessTokenDto {
|
|
4
|
+
jwt: string;
|
|
5
|
+
refreshToken: string | null;
|
|
6
|
+
expiresAt: string | null;
|
|
7
|
+
}
|
|
8
|
+
interface AccessToken {
|
|
9
|
+
jwt: string;
|
|
10
|
+
refreshToken: string | null;
|
|
11
|
+
expiresAt: Dayjs | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class HttpClient {
|
|
15
|
+
readonly baseUrl: string;
|
|
16
|
+
constructor(baseUrl: string) {
|
|
17
|
+
this.baseUrl = baseUrl;
|
|
18
|
+
}
|
|
19
|
+
async get<T>(
|
|
20
|
+
url: string,
|
|
21
|
+
{ headers }: { headers?: Record<string, string> } = {},
|
|
22
|
+
): Promise<T> {
|
|
23
|
+
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
24
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
25
|
+
});
|
|
26
|
+
return response.json();
|
|
27
|
+
}
|
|
28
|
+
async post<T>(
|
|
29
|
+
url: string,
|
|
30
|
+
data: unknown,
|
|
31
|
+
{ headers }: { headers?: Record<string, string> } = {},
|
|
32
|
+
): Promise<T> {
|
|
33
|
+
const isFormData = data instanceof FormData;
|
|
34
|
+
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
body: isFormData ? data : JSON.stringify(data),
|
|
37
|
+
headers: isFormData
|
|
38
|
+
? headers
|
|
39
|
+
: { "Content-Type": "application/json", ...headers },
|
|
40
|
+
});
|
|
41
|
+
return response.json();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class CloudApi {
|
|
46
|
+
readonly api: HttpClient;
|
|
47
|
+
#accessToken: AccessToken | null = null;
|
|
48
|
+
constructor(
|
|
49
|
+
host: string,
|
|
50
|
+
{ accessToken }: { accessToken?: AccessToken } = {},
|
|
51
|
+
) {
|
|
52
|
+
this.api = new HttpClient(`${host}/api`);
|
|
53
|
+
this.#accessToken = accessToken ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async uploadEnv(devProjectId: string, file: File): Promise<boolean> {
|
|
57
|
+
const formData = new FormData();
|
|
58
|
+
formData.append("devProjectId", devProjectId);
|
|
59
|
+
formData.append("file", file);
|
|
60
|
+
const response = await this.api.post<{ success: boolean }>(
|
|
61
|
+
`/uploadEnv/${devProjectId}`,
|
|
62
|
+
formData,
|
|
63
|
+
);
|
|
64
|
+
return response.success;
|
|
65
|
+
}
|
|
66
|
+
async downloadEnv(devProjectId: string): Promise<boolean> {
|
|
67
|
+
const response = await this.api.get<{ success: boolean }>(
|
|
68
|
+
`/downloadEnv/${devProjectId}`,
|
|
69
|
+
);
|
|
70
|
+
return response.success;
|
|
71
|
+
}
|
|
72
|
+
async getRemoteAuthToken(remoteId: string): Promise<AccessToken> {
|
|
73
|
+
if (this.#needRefreshToken()) return await this.refreshAuthToken();
|
|
74
|
+
else if (this.#accessToken) return this.#accessToken;
|
|
75
|
+
const accessToken = await this.api.get<AccessTokenDto>(
|
|
76
|
+
`/getRemoteAuthToken/${remoteId}`,
|
|
77
|
+
);
|
|
78
|
+
this.#accessToken = {
|
|
79
|
+
jwt: accessToken.jwt,
|
|
80
|
+
refreshToken: accessToken.refreshToken,
|
|
81
|
+
expiresAt: accessToken.expiresAt ? dayjs(accessToken.expiresAt) : null,
|
|
82
|
+
};
|
|
83
|
+
return this.#accessToken;
|
|
84
|
+
}
|
|
85
|
+
async refreshAuthToken(): Promise<AccessToken> {
|
|
86
|
+
const response = await this.api.post<AccessTokenDto>(
|
|
87
|
+
`/refreshRemoteAuthToken`,
|
|
88
|
+
{
|
|
89
|
+
refreshToken: this.#accessToken?.refreshToken,
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
this.#accessToken = {
|
|
93
|
+
jwt: response.jwt,
|
|
94
|
+
refreshToken: response.refreshToken,
|
|
95
|
+
expiresAt: response.expiresAt ? dayjs(response.expiresAt) : null,
|
|
96
|
+
};
|
|
97
|
+
return this.#accessToken;
|
|
98
|
+
}
|
|
99
|
+
#needRefreshToken(): boolean {
|
|
100
|
+
return !!this.#accessToken?.expiresAt?.isBefore(dayjs().add(1, "hour"));
|
|
101
|
+
}
|
|
102
|
+
}
|
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/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
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.1",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -30,11 +30,13 @@
|
|
|
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.1",
|
|
35
36
|
"chalk": "^5.6.2",
|
|
36
37
|
"commander": "^14.0.3",
|
|
37
38
|
"daisyui": "^5.5.20",
|
|
39
|
+
"dayjs": "^1.11.20",
|
|
38
40
|
"fontaine": "^0.8.0",
|
|
39
41
|
"fonteditor-core": "^2.6.3",
|
|
40
42
|
"ignore": "^7.0.5",
|