@akanjs/devkit 2.1.1 → 2.1.2-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 +167 -32
- package/akanConfig/akanConfig.ts +130 -31
- package/artifact/implicitRootLayout.test.ts +2 -0
- package/artifact/implicitRootLayout.ts +2 -2
- package/cloud/cloudApi.ts +25 -48
- package/executors.test.ts +47 -0
- package/executors.ts +166 -577
- package/package.json +2 -2
|
@@ -7,7 +7,13 @@ import { AkanAppConfig, AkanLibConfig } from "./akanConfig";
|
|
|
7
7
|
import type { DeepPartial, LibConfigResult } from "./types";
|
|
8
8
|
|
|
9
9
|
const akanPackageJson = JSON.parse(
|
|
10
|
-
fs.readFileSync(
|
|
10
|
+
fs.readFileSync(
|
|
11
|
+
path.join(
|
|
12
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
13
|
+
"../../../akanjs/package.json",
|
|
14
|
+
),
|
|
15
|
+
"utf8",
|
|
16
|
+
),
|
|
11
17
|
) as PackageJson;
|
|
12
18
|
|
|
13
19
|
const packageJson: PackageJson = {
|
|
@@ -34,7 +40,13 @@ const baseDevEnv = {
|
|
|
34
40
|
|
|
35
41
|
describe("AkanAppConfig", () => {
|
|
36
42
|
test("applies defaults for route domains, i18n, image, mobile, and imports", () => {
|
|
37
|
-
const config = new AkanAppConfig(
|
|
43
|
+
const config = new AkanAppConfig(
|
|
44
|
+
app,
|
|
45
|
+
["shared"],
|
|
46
|
+
packageJson,
|
|
47
|
+
{},
|
|
48
|
+
baseDevEnv,
|
|
49
|
+
);
|
|
38
50
|
|
|
39
51
|
expect([...config.domains].sort()).toEqual([
|
|
40
52
|
"portal-debug.akanjs.com",
|
|
@@ -61,7 +73,11 @@ describe("AkanAppConfig", () => {
|
|
|
61
73
|
},
|
|
62
74
|
});
|
|
63
75
|
expect(config.barrelImports).toEqual(
|
|
64
|
-
expect.arrayContaining([
|
|
76
|
+
expect.arrayContaining([
|
|
77
|
+
"@apps/portal/ui",
|
|
78
|
+
"@libs/shared/server",
|
|
79
|
+
"akanjs/common",
|
|
80
|
+
]),
|
|
65
81
|
);
|
|
66
82
|
expect(config.docker.content).toContain("ENV AKAN_PUBLIC_APP_NAME=portal");
|
|
67
83
|
expect(process.env.AKAN_PUBLIC_DEFAULT_LOCALE).toBe("en");
|
|
@@ -75,10 +91,21 @@ describe("AkanAppConfig", () => {
|
|
|
75
91
|
{
|
|
76
92
|
routes: [
|
|
77
93
|
{ domains: { debug: ["Root.Local:8282"], qa: ["QA.Root.Local"] } },
|
|
78
|
-
{
|
|
94
|
+
{
|
|
95
|
+
basePath: "/admin/",
|
|
96
|
+
domains: {
|
|
97
|
+
debug: ["Admin.Local:8282"],
|
|
98
|
+
main: ["Admin.Main.Local"],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
79
101
|
],
|
|
80
102
|
i18n: { locales: ["ko", "en"], defaultLocale: "ko" },
|
|
81
|
-
mobile: {
|
|
103
|
+
mobile: {
|
|
104
|
+
appName: "Portal App",
|
|
105
|
+
appId: "com.portal.mobile",
|
|
106
|
+
version: "1.2.3",
|
|
107
|
+
buildNum: 7,
|
|
108
|
+
},
|
|
82
109
|
images: { qualities: [80, 90], dangerouslyAllowSVG: true },
|
|
83
110
|
docker: {
|
|
84
111
|
image: { amd64: "oven/bun:amd64", arm64: "oven/bun:arm64" },
|
|
@@ -102,7 +129,12 @@ describe("AkanAppConfig", () => {
|
|
|
102
129
|
"admin.local",
|
|
103
130
|
"admin.main.local",
|
|
104
131
|
]);
|
|
105
|
-
expect([...config.branches].sort()).toEqual([
|
|
132
|
+
expect([...config.branches].sort()).toEqual([
|
|
133
|
+
"debug",
|
|
134
|
+
"develop",
|
|
135
|
+
"main",
|
|
136
|
+
"qa",
|
|
137
|
+
]);
|
|
106
138
|
expect(config.i18n.defaultLocale).toBe("ko");
|
|
107
139
|
expect(config.images.qualities).toEqual([80, 90]);
|
|
108
140
|
expect(config.images.dangerouslyAllowSVG).toBe(true);
|
|
@@ -122,9 +154,17 @@ describe("AkanAppConfig", () => {
|
|
|
122
154
|
});
|
|
123
155
|
|
|
124
156
|
test("creates production package json and reports missing external versions", () => {
|
|
125
|
-
const config = new AkanAppConfig(
|
|
157
|
+
const config = new AkanAppConfig(
|
|
158
|
+
app,
|
|
159
|
+
[],
|
|
160
|
+
packageJson,
|
|
161
|
+
{ externalLibs: ["@external/runtime"] },
|
|
162
|
+
baseDevEnv,
|
|
163
|
+
);
|
|
126
164
|
|
|
127
|
-
expect(
|
|
165
|
+
expect(
|
|
166
|
+
config.getProductionPackageJson({ scripts: { start: "bun main.js" } }),
|
|
167
|
+
).toMatchObject({
|
|
128
168
|
name: "portal",
|
|
129
169
|
main: "./main.js",
|
|
130
170
|
scripts: { start: "bun main.js" },
|
|
@@ -145,11 +185,16 @@ describe("AkanAppConfig", () => {
|
|
|
145
185
|
{ externalLibs: ["missing-lib"] },
|
|
146
186
|
baseDevEnv,
|
|
147
187
|
);
|
|
148
|
-
expect(() => brokenConfig.getProductionPackageJson()).toThrow(
|
|
188
|
+
expect(() => brokenConfig.getProductionPackageJson()).toThrow(
|
|
189
|
+
"Dependency missing-lib not found",
|
|
190
|
+
);
|
|
149
191
|
});
|
|
150
192
|
|
|
151
193
|
test("falls back to akanjs package versions for built-in runtime dependencies", () => {
|
|
152
|
-
const runtimeDependencies = {
|
|
194
|
+
const runtimeDependencies = {
|
|
195
|
+
...akanPackageJson.dependencies,
|
|
196
|
+
...akanPackageJson.peerDependencies,
|
|
197
|
+
};
|
|
153
198
|
const config = new AkanAppConfig(
|
|
154
199
|
app,
|
|
155
200
|
[],
|
|
@@ -168,44 +213,130 @@ describe("AkanAppConfig", () => {
|
|
|
168
213
|
expect(config.getProductionPackageJson().dependencies).toEqual({
|
|
169
214
|
react: runtimeDependencies.react,
|
|
170
215
|
"react-dom": runtimeDependencies["react-dom"],
|
|
171
|
-
"react-server-dom-webpack":
|
|
216
|
+
"react-server-dom-webpack":
|
|
217
|
+
runtimeDependencies["react-server-dom-webpack"],
|
|
172
218
|
croner: runtimeDependencies.croner,
|
|
173
219
|
sharp: runtimeDependencies.sharp,
|
|
174
220
|
});
|
|
175
221
|
});
|
|
176
222
|
|
|
177
223
|
test("adds backend runtime packages by database mode", () => {
|
|
178
|
-
const runtimeDependencies = {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
224
|
+
const runtimeDependencies = {
|
|
225
|
+
...akanPackageJson.dependencies,
|
|
226
|
+
...akanPackageJson.peerDependencies,
|
|
227
|
+
};
|
|
228
|
+
const singleConfig = new AkanAppConfig(
|
|
229
|
+
app,
|
|
230
|
+
[],
|
|
231
|
+
packageJson,
|
|
232
|
+
{ defaultDatabaseMode: "single" },
|
|
233
|
+
baseDevEnv,
|
|
234
|
+
);
|
|
235
|
+
const multipleConfig = new AkanAppConfig(
|
|
236
|
+
app,
|
|
237
|
+
[],
|
|
238
|
+
packageJson,
|
|
239
|
+
{ defaultDatabaseMode: "multiple" },
|
|
240
|
+
baseDevEnv,
|
|
241
|
+
);
|
|
242
|
+
const clusterConfig = new AkanAppConfig(
|
|
243
|
+
app,
|
|
244
|
+
[],
|
|
245
|
+
packageJson,
|
|
246
|
+
{ defaultDatabaseMode: "cluster" },
|
|
247
|
+
baseDevEnv,
|
|
248
|
+
);
|
|
182
249
|
|
|
183
250
|
expect(singleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
184
251
|
croner: runtimeDependencies.croner,
|
|
185
252
|
});
|
|
186
|
-
expect(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
expect(
|
|
190
|
-
|
|
253
|
+
expect(
|
|
254
|
+
singleConfig.getProductionPackageJson().dependencies,
|
|
255
|
+
).not.toHaveProperty("ioredis");
|
|
256
|
+
expect(
|
|
257
|
+
singleConfig.getProductionPackageJson().dependencies,
|
|
258
|
+
).not.toHaveProperty("bullmq");
|
|
259
|
+
expect(
|
|
260
|
+
singleConfig.getProductionPackageJson().dependencies,
|
|
261
|
+
).not.toHaveProperty("@libsql/client");
|
|
262
|
+
expect(
|
|
263
|
+
singleConfig.getProductionPackageJson().dependencies,
|
|
264
|
+
).not.toHaveProperty("postgres");
|
|
265
|
+
expect(
|
|
266
|
+
singleConfig.getProductionPackageJson().dependencies,
|
|
267
|
+
).not.toHaveProperty("protobufjs");
|
|
191
268
|
|
|
192
|
-
expect(
|
|
269
|
+
expect(
|
|
270
|
+
multipleConfig.getProductionPackageJson().dependencies,
|
|
271
|
+
).toMatchObject({
|
|
193
272
|
"@libsql/client": runtimeDependencies["@libsql/client"],
|
|
194
273
|
bullmq: runtimeDependencies.bullmq,
|
|
195
274
|
croner: runtimeDependencies.croner,
|
|
196
275
|
ioredis: runtimeDependencies.ioredis,
|
|
197
276
|
protobufjs: runtimeDependencies.protobufjs,
|
|
198
277
|
});
|
|
199
|
-
expect(
|
|
278
|
+
expect(
|
|
279
|
+
multipleConfig.getProductionPackageJson().dependencies,
|
|
280
|
+
).not.toHaveProperty("postgres");
|
|
200
281
|
|
|
201
|
-
expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
282
|
+
expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject(
|
|
283
|
+
{
|
|
284
|
+
bullmq: runtimeDependencies.bullmq,
|
|
285
|
+
croner: runtimeDependencies.croner,
|
|
286
|
+
ioredis: runtimeDependencies.ioredis,
|
|
287
|
+
postgres: runtimeDependencies.postgres,
|
|
288
|
+
protobufjs: runtimeDependencies.protobufjs,
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
expect(
|
|
292
|
+
clusterConfig.getProductionPackageJson().dependencies,
|
|
293
|
+
).not.toHaveProperty("@libsql/client");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("resolves database mode runtime packages and missing install specs", () => {
|
|
297
|
+
const runtimeDependencies = {
|
|
298
|
+
...akanPackageJson.dependencies,
|
|
299
|
+
...akanPackageJson.peerDependencies,
|
|
300
|
+
};
|
|
301
|
+
const config = new AkanAppConfig(
|
|
302
|
+
app,
|
|
303
|
+
[],
|
|
304
|
+
{
|
|
305
|
+
name: "repo",
|
|
306
|
+
version: "1.0.0",
|
|
307
|
+
description: "repo",
|
|
308
|
+
dependencies: {
|
|
309
|
+
bullmq: "5.0.0",
|
|
310
|
+
},
|
|
311
|
+
devDependencies: {
|
|
312
|
+
ioredis: "5.0.0",
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{},
|
|
316
|
+
baseDevEnv,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(config.getDatabaseModeRuntimePackages("single")).toEqual([]);
|
|
320
|
+
expect(config.getDatabaseModeRuntimePackages("multiple")).toEqual([
|
|
321
|
+
"@libsql/client",
|
|
322
|
+
"bullmq",
|
|
323
|
+
"ioredis",
|
|
324
|
+
"protobufjs",
|
|
325
|
+
]);
|
|
326
|
+
expect(config.getDatabaseModeRuntimePackages("cluster")).toEqual([
|
|
327
|
+
"bullmq",
|
|
328
|
+
"ioredis",
|
|
329
|
+
"postgres",
|
|
330
|
+
"protobufjs",
|
|
331
|
+
]);
|
|
332
|
+
expect(config.getMissingDatabaseModeDependencySpecs("multiple")).toEqual([
|
|
333
|
+
`@libsql/client@${runtimeDependencies["@libsql/client"]}`,
|
|
334
|
+
`protobufjs@${runtimeDependencies.protobufjs}`,
|
|
335
|
+
]);
|
|
336
|
+
expect(config.getMissingDatabaseModeDependencySpecs("cluster")).toEqual([
|
|
337
|
+
`postgres@${runtimeDependencies.postgres}`,
|
|
338
|
+
`protobufjs@${runtimeDependencies.protobufjs}`,
|
|
339
|
+
]);
|
|
209
340
|
});
|
|
210
341
|
|
|
211
342
|
test("normalizes multiple mobile targets and validates base paths", () => {
|
|
@@ -266,7 +397,11 @@ describe("AkanLibConfig", () => {
|
|
|
266
397
|
const lib = { name: "shared" } as never;
|
|
267
398
|
expect(new AkanLibConfig(lib, {}).externalLibs).toEqual([]);
|
|
268
399
|
|
|
269
|
-
const config: DeepPartial<LibConfigResult> = {
|
|
270
|
-
|
|
400
|
+
const config: DeepPartial<LibConfigResult> = {
|
|
401
|
+
externalLibs: ["firebase-admin"],
|
|
402
|
+
};
|
|
403
|
+
expect(new AkanLibConfig(lib, config).externalLibs).toEqual([
|
|
404
|
+
"firebase-admin",
|
|
405
|
+
]);
|
|
271
406
|
});
|
|
272
407
|
});
|
package/akanConfig/akanConfig.ts
CHANGED
|
@@ -47,8 +47,18 @@ const DEFAULT_OPTIMIZE_IMPORTS = [
|
|
|
47
47
|
"mui-core",
|
|
48
48
|
"react-icons/*",
|
|
49
49
|
];
|
|
50
|
-
const WORKSPACE_BARREL_FACETS = [
|
|
51
|
-
|
|
50
|
+
const WORKSPACE_BARREL_FACETS = [
|
|
51
|
+
"ui",
|
|
52
|
+
"webkit",
|
|
53
|
+
"common",
|
|
54
|
+
"client",
|
|
55
|
+
"server",
|
|
56
|
+
] as const;
|
|
57
|
+
const SSR_RUNTIME_PACKAGES = [
|
|
58
|
+
"react",
|
|
59
|
+
"react-dom",
|
|
60
|
+
"react-server-dom-webpack",
|
|
61
|
+
] as const;
|
|
52
62
|
const NATIVE_RUNTIME_PACKAGES = ["sharp"] as const;
|
|
53
63
|
const DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"] as const;
|
|
54
64
|
const DATABASE_MODE_RUNTIME_PACKAGES = {
|
|
@@ -111,19 +121,31 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
111
121
|
this.barrelImports = [
|
|
112
122
|
...DEFAULT_BARREL_IMPORTS,
|
|
113
123
|
...WORKSPACE_BARREL_FACETS.map((facet) => `@apps/${app.name}/${facet}`),
|
|
114
|
-
...libs.flatMap((lib) =>
|
|
124
|
+
...libs.flatMap((lib) =>
|
|
125
|
+
WORKSPACE_BARREL_FACETS.map((facet) => `@libs/${lib}/${facet}`),
|
|
126
|
+
),
|
|
115
127
|
...(config?.barrelImports ?? []),
|
|
116
128
|
];
|
|
117
|
-
this.optimizeImports = [
|
|
118
|
-
|
|
129
|
+
this.optimizeImports = [
|
|
130
|
+
...new Set([
|
|
131
|
+
...DEFAULT_OPTIMIZE_IMPORTS,
|
|
132
|
+
...(config?.optimizeImports ?? []),
|
|
133
|
+
]),
|
|
134
|
+
];
|
|
135
|
+
this.images = mergeImageConfig(
|
|
136
|
+
config?.images as Partial<AkanImageConfig> | undefined,
|
|
137
|
+
);
|
|
119
138
|
this.i18n = resolveAkanI18nConfig(config?.i18n);
|
|
120
139
|
process.env.AKAN_PUBLIC_DEFAULT_LOCALE = this.i18n.defaultLocale;
|
|
121
140
|
process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
|
|
122
|
-
this.publicEnv =
|
|
141
|
+
this.publicEnv =
|
|
142
|
+
(config?.publicEnv as string[] | undefined) ?? ([] as string[]);
|
|
123
143
|
this.mobile = this.#resolveMobileConfig(config.mobile);
|
|
124
144
|
this.docker = this.#makeDockerContent(config?.docker ?? {});
|
|
125
145
|
}
|
|
126
|
-
#resolveMobileConfig(
|
|
146
|
+
#resolveMobileConfig(
|
|
147
|
+
mobile: DeepPartial<AkanMobileConfig> | undefined,
|
|
148
|
+
): AkanMobileConfig {
|
|
127
149
|
const { targets: rawTargets, ...rawMobile } = mobile ?? {};
|
|
128
150
|
const appName = rawMobile.appName ?? this.app.name;
|
|
129
151
|
const appId = rawMobile.appId ?? `com.${this.app.name}.app`;
|
|
@@ -138,8 +160,11 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
138
160
|
const targets = Object.fromEntries(
|
|
139
161
|
targetEntries.map(([name, rawTarget]) => {
|
|
140
162
|
const target = rawTarget as DeepPartial<AkanMobileTargetConfig>;
|
|
141
|
-
const fallbackBasePath =
|
|
142
|
-
|
|
163
|
+
const fallbackBasePath =
|
|
164
|
+
!rawTargets && this.basePaths.has(name) ? name : undefined;
|
|
165
|
+
const basePath =
|
|
166
|
+
(target.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") ||
|
|
167
|
+
undefined;
|
|
143
168
|
if (basePath && !this.basePaths.has(basePath)) {
|
|
144
169
|
throw new Error(
|
|
145
170
|
`Mobile target '${name}' uses unknown basePath '${basePath}' in apps/${this.app.name}/akan.config.ts`,
|
|
@@ -180,8 +205,11 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
180
205
|
plugins: rawMobile.plugins,
|
|
181
206
|
} as AkanMobileConfig;
|
|
182
207
|
}
|
|
183
|
-
#defaultMobileTargetName(
|
|
184
|
-
|
|
208
|
+
#defaultMobileTargetName(
|
|
209
|
+
rawTargets: DeepPartial<AkanMobileConfig>["targets"] | undefined,
|
|
210
|
+
) {
|
|
211
|
+
if (rawTargets && Object.keys(rawTargets).length > 0)
|
|
212
|
+
return Object.keys(rawTargets)[0] as string;
|
|
185
213
|
return this.basePaths.has(this.app.name) ? this.app.name : "default";
|
|
186
214
|
}
|
|
187
215
|
#applyRoutes(routes: AkanRouteConfig[] = []) {
|
|
@@ -190,28 +218,38 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
190
218
|
const basePath = route.basePath.replace(/^\/+|\/+$/g, "");
|
|
191
219
|
this.basePaths.add(basePath);
|
|
192
220
|
const domains = this.subRoutes.getOrInsert(basePath, new Set());
|
|
193
|
-
Object.keys(route.domains).forEach(
|
|
221
|
+
Object.keys(route.domains).forEach(
|
|
222
|
+
(branch) => void this.branches.add(branch),
|
|
223
|
+
);
|
|
194
224
|
Object.values(route.domains)
|
|
195
225
|
.flat()
|
|
196
226
|
.forEach((domain) => {
|
|
197
227
|
if (domain) domains.add(domain.toLowerCase().replace(/:\d+$/, ""));
|
|
198
228
|
});
|
|
199
229
|
} else {
|
|
200
|
-
Object.keys(route.domains).forEach(
|
|
230
|
+
Object.keys(route.domains).forEach(
|
|
231
|
+
(branch) => void this.branches.add(branch),
|
|
232
|
+
);
|
|
201
233
|
Object.values(route.domains)
|
|
202
234
|
.flat()
|
|
203
235
|
.forEach((domain) => {
|
|
204
|
-
if (domain)
|
|
236
|
+
if (domain)
|
|
237
|
+
this.domains.add(domain.toLowerCase().replace(/:\d+$/, ""));
|
|
205
238
|
});
|
|
206
239
|
}
|
|
207
240
|
}
|
|
208
241
|
const appName = this.app.name.toLowerCase();
|
|
209
242
|
const serveDomain = this.baseDevEnv.serveDomain.toLowerCase();
|
|
210
243
|
if (this.subRoutes.size === 0)
|
|
211
|
-
this.branches.forEach(
|
|
244
|
+
this.branches.forEach(
|
|
245
|
+
(branch) =>
|
|
246
|
+
void this.domains.add(`${appName}-${branch}.${serveDomain}`),
|
|
247
|
+
);
|
|
212
248
|
else
|
|
213
249
|
Array.from(this.subRoutes.entries()).forEach(([basePath, domains]) => {
|
|
214
|
-
this.branches.forEach(
|
|
250
|
+
this.branches.forEach(
|
|
251
|
+
(domain) => void domains.add(`${basePath}-${domain}.${serveDomain}`),
|
|
252
|
+
);
|
|
215
253
|
});
|
|
216
254
|
}
|
|
217
255
|
#getDockerRunScripts(runs: (string | { [key in Arch]?: string })[]) {
|
|
@@ -227,12 +265,25 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
227
265
|
.join("\n");
|
|
228
266
|
});
|
|
229
267
|
}
|
|
230
|
-
#getDockerImageScript(
|
|
268
|
+
#getDockerImageScript(
|
|
269
|
+
image: string | { [key in Arch]?: string },
|
|
270
|
+
defaultImage: string,
|
|
271
|
+
) {
|
|
231
272
|
if (typeof image === "string") return `FROM ${image}`;
|
|
232
|
-
else
|
|
273
|
+
else
|
|
274
|
+
return archs
|
|
275
|
+
.map((arch) => `FROM ${image[arch] ?? defaultImage} AS ${arch}`)
|
|
276
|
+
.join("\n");
|
|
233
277
|
}
|
|
234
278
|
#makeDockerContent(docker: DeepPartial<DockerConfig>): DockerConfig {
|
|
235
|
-
if (docker.content)
|
|
279
|
+
if (docker.content)
|
|
280
|
+
return {
|
|
281
|
+
content: docker.content,
|
|
282
|
+
image: {},
|
|
283
|
+
preRuns: [],
|
|
284
|
+
postRuns: [],
|
|
285
|
+
command: [],
|
|
286
|
+
};
|
|
236
287
|
const preRunScripts = this.#getDockerRunScripts(docker.preRuns ?? []);
|
|
237
288
|
const postRunScripts = this.#getDockerRunScripts(docker.postRuns ?? []);
|
|
238
289
|
|
|
@@ -264,12 +315,20 @@ ENV AKAN_PUBLIC_LOCALES=${this.i18n.locales.join(",")}
|
|
|
264
315
|
ENV AKAN_PUBLIC_OPERATION_MODE=cloud
|
|
265
316
|
|
|
266
317
|
CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
267
|
-
return {
|
|
318
|
+
return {
|
|
319
|
+
content,
|
|
320
|
+
image: imageScript,
|
|
321
|
+
preRuns: docker.preRuns ?? [],
|
|
322
|
+
postRuns: docker.postRuns ?? [],
|
|
323
|
+
command,
|
|
324
|
+
};
|
|
268
325
|
}
|
|
269
326
|
static async from(app: App) {
|
|
270
327
|
const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
|
|
271
328
|
import(`${app.cwdPath}/akan.config.ts`).then((mod) => mod.default),
|
|
272
|
-
WorkspaceExecutor.getBaseDevEnv(
|
|
329
|
+
WorkspaceExecutor.getBaseDevEnv(
|
|
330
|
+
path.join(app.workspace.workspaceRoot, ".env"),
|
|
331
|
+
),
|
|
273
332
|
app.workspace.getLibs(),
|
|
274
333
|
app.workspace.getPackageJson(),
|
|
275
334
|
]);
|
|
@@ -277,11 +336,16 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
277
336
|
return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv);
|
|
278
337
|
}
|
|
279
338
|
#resolveProductionDependencyVersion(lib: string) {
|
|
280
|
-
const rootVersion =
|
|
339
|
+
const rootVersion =
|
|
340
|
+
this.rootPackageJson.dependencies?.[lib] ??
|
|
341
|
+
this.rootPackageJson.devDependencies?.[lib];
|
|
281
342
|
if (rootVersion) return rootVersion;
|
|
282
343
|
const akanPackageJson = getAkanPackageJson();
|
|
283
344
|
if (AKAN_RUNTIME_PACKAGES.has(lib))
|
|
284
|
-
return
|
|
345
|
+
return (
|
|
346
|
+
akanPackageJson.dependencies?.[lib] ??
|
|
347
|
+
akanPackageJson.peerDependencies?.[lib]
|
|
348
|
+
);
|
|
285
349
|
}
|
|
286
350
|
#getProductionRuntimePackages() {
|
|
287
351
|
return [
|
|
@@ -289,9 +353,30 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
289
353
|
...SSR_RUNTIME_PACKAGES,
|
|
290
354
|
...NATIVE_RUNTIME_PACKAGES,
|
|
291
355
|
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
292
|
-
...
|
|
356
|
+
...this.getDatabaseModeRuntimePackages(),
|
|
293
357
|
];
|
|
294
358
|
}
|
|
359
|
+
getDatabaseModeRuntimePackages(
|
|
360
|
+
databaseMode: DatabaseMode = this.defaultDatabaseMode,
|
|
361
|
+
) {
|
|
362
|
+
return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
|
|
363
|
+
}
|
|
364
|
+
getMissingDatabaseModeDependencySpecs(
|
|
365
|
+
databaseMode: DatabaseMode = this.defaultDatabaseMode,
|
|
366
|
+
) {
|
|
367
|
+
const rootDependencies = {
|
|
368
|
+
...this.rootPackageJson.dependencies,
|
|
369
|
+
...this.rootPackageJson.devDependencies,
|
|
370
|
+
};
|
|
371
|
+
return this.getDatabaseModeRuntimePackages(databaseMode)
|
|
372
|
+
.filter((lib) => !rootDependencies[lib])
|
|
373
|
+
.map((lib) => {
|
|
374
|
+
const version = this.#resolveProductionDependencyVersion(lib);
|
|
375
|
+
if (!version)
|
|
376
|
+
throw new Error(`Dependency ${lib} not found in package.json`);
|
|
377
|
+
return `${lib}@${version}`;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
295
380
|
getProductionPackageJson(data: Partial<PackageJson> = {}): PackageJson {
|
|
296
381
|
return {
|
|
297
382
|
name: this.app.name,
|
|
@@ -301,7 +386,8 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
301
386
|
dependencies: Object.fromEntries(
|
|
302
387
|
[...new Set(this.#getProductionRuntimePackages())].map((lib) => {
|
|
303
388
|
const version = this.#resolveProductionDependencyVersion(lib);
|
|
304
|
-
if (!version)
|
|
389
|
+
if (!version)
|
|
390
|
+
throw new Error(`Dependency ${lib} not found in package.json`);
|
|
305
391
|
return [lib, version];
|
|
306
392
|
}),
|
|
307
393
|
),
|
|
@@ -327,17 +413,26 @@ function getAkanPackageJson() {
|
|
|
327
413
|
}
|
|
328
414
|
for (const packageJsonPath of packageJsonPaths) {
|
|
329
415
|
try {
|
|
330
|
-
akanPackageJson = JSON.parse(
|
|
416
|
+
akanPackageJson = JSON.parse(
|
|
417
|
+
fs.readFileSync(packageJsonPath, "utf8"),
|
|
418
|
+
) as PackageJson;
|
|
331
419
|
return akanPackageJson;
|
|
332
420
|
} catch {
|
|
333
421
|
// Try the next known layout: source package first, bundled CLI package second.
|
|
334
422
|
}
|
|
335
423
|
}
|
|
336
|
-
akanPackageJson = {
|
|
424
|
+
akanPackageJson = {
|
|
425
|
+
name: "akanjs",
|
|
426
|
+
version: "0.0.0",
|
|
427
|
+
description: "akanjs",
|
|
428
|
+
dependencies: {},
|
|
429
|
+
};
|
|
337
430
|
return akanPackageJson;
|
|
338
431
|
}
|
|
339
432
|
|
|
340
|
-
function mergeImageConfig(
|
|
433
|
+
function mergeImageConfig(
|
|
434
|
+
config: Partial<AkanImageConfig> = {},
|
|
435
|
+
): AkanImageConfig {
|
|
341
436
|
return {
|
|
342
437
|
...DEFAULT_AKAN_IMAGE_CONFIG,
|
|
343
438
|
...config,
|
|
@@ -345,8 +440,10 @@ function mergeImageConfig(config: Partial<AkanImageConfig> = {}): AkanImageConfi
|
|
|
345
440
|
imageSizes: config.imageSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.imageSizes,
|
|
346
441
|
formats: config.formats ?? DEFAULT_AKAN_IMAGE_CONFIG.formats,
|
|
347
442
|
qualities: config.qualities ?? DEFAULT_AKAN_IMAGE_CONFIG.qualities,
|
|
348
|
-
remotePatterns:
|
|
349
|
-
|
|
443
|
+
remotePatterns:
|
|
444
|
+
config.remotePatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.remotePatterns,
|
|
445
|
+
localPatterns:
|
|
446
|
+
config.localPatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.localPatterns,
|
|
350
447
|
};
|
|
351
448
|
}
|
|
352
449
|
|
|
@@ -358,7 +455,9 @@ export class AkanLibConfig implements LibConfigResult {
|
|
|
358
455
|
this.externalLibs = config?.externalLibs ?? [];
|
|
359
456
|
}
|
|
360
457
|
static async from(lib: Lib) {
|
|
361
|
-
const [configImp] = await Promise.all([
|
|
458
|
+
const [configImp] = await Promise.all([
|
|
459
|
+
import(`${lib.cwdPath}/akan.config.ts`).then((mod) => mod.default),
|
|
460
|
+
]);
|
|
362
461
|
const config = typeof configImp === "function" ? configImp(lib) : configImp;
|
|
363
462
|
return new AkanLibConfig(lib, config);
|
|
364
463
|
}
|
|
@@ -46,6 +46,8 @@ describe("resolveSsrPageEntries", () => {
|
|
|
46
46
|
const generatedSource = await Bun.file(groupedRoot?.moduleAbsPath ?? "").text();
|
|
47
47
|
expect(generatedSource).toContain('import * as inheritedLayout from "../../../page/_layout.tsx";');
|
|
48
48
|
expect(generatedSource).not.toContain("<System.Provider");
|
|
49
|
+
expect(generatedSource).toContain("export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;");
|
|
50
|
+
expect(generatedSource).toContain("export const Error = userLayout.Error ?? inheritedLayout.Error;");
|
|
49
51
|
expect(generatedSource).toContain(
|
|
50
52
|
"<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>",
|
|
51
53
|
);
|
|
@@ -135,8 +135,8 @@ async function writeGeneratedRootLayoutFile(opts: {
|
|
|
135
135
|
? `import UserLayout, * as userLayout from ${JSON.stringify(sourceSpecifier)};\n`
|
|
136
136
|
: "const UserLayout = ({ children }) => children;\nconst userLayout = {};\n";
|
|
137
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`;
|
|
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 const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;\nexport const Error = userLayout.Error ?? inheritedLayout.Error;\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 const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;\nexport const Error = userLayout.Error ?? inheritedLayout.Error;\n\nexport default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {\n return <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>;\n}\n`;
|
|
140
140
|
await Bun.write(absPath, source);
|
|
141
141
|
return absPath;
|
|
142
142
|
}
|