@akanjs/devkit 2.1.1-rc.2 → 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/aiEditor.test.ts +68 -0
- package/aiEditor.ts +82 -28
- 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 +70 -3
- package/index.ts +1 -1
- package/linter.ts +308 -97
- package/package.json +2 -2
- package/prompter.ts +17 -4
- package/typecheck/typecheck.proc.ts +21 -0
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
|
}
|
package/cloud/cloudApi.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type AccessTokenDto,
|
|
4
|
-
akanCloudHost,
|
|
5
|
-
type HostConfig,
|
|
6
|
-
} from "./constants";
|
|
1
|
+
import type { Workspace } from "../commandDecorators";
|
|
2
|
+
import { type AccessToken, type AccessTokenDto, akanCloudHost, type HostConfig } from "./constants";
|
|
7
3
|
import { GlobalConfig } from "./globalConfig";
|
|
8
4
|
|
|
9
5
|
class HttpClient {
|
|
@@ -13,10 +9,7 @@ class HttpClient {
|
|
|
13
9
|
this.baseUrl = baseUrl;
|
|
14
10
|
this.headers = headers;
|
|
15
11
|
}
|
|
16
|
-
async get<T>(
|
|
17
|
-
url: string,
|
|
18
|
-
{ headers }: { headers?: Record<string, string> } = {},
|
|
19
|
-
): Promise<T> {
|
|
12
|
+
async get<T>(url: string, { headers }: { headers?: Record<string, string> } = {}): Promise<T> {
|
|
20
13
|
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
21
14
|
headers: {
|
|
22
15
|
"Content-Type": "application/json",
|
|
@@ -26,25 +19,14 @@ class HttpClient {
|
|
|
26
19
|
});
|
|
27
20
|
return response.json();
|
|
28
21
|
}
|
|
29
|
-
async getFile(
|
|
30
|
-
url: string,
|
|
31
|
-
localPath: string,
|
|
32
|
-
headers?: Record<string, string>,
|
|
33
|
-
): Promise<void> {
|
|
22
|
+
async getFile(url: string, localPath: string, headers?: Record<string, string>): Promise<void> {
|
|
34
23
|
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
35
24
|
headers: { ...this.headers, ...headers },
|
|
36
25
|
});
|
|
37
|
-
if (!response.ok)
|
|
38
|
-
throw new Error(
|
|
39
|
-
`Failed to download file: ${response.status} ${response.statusText}`,
|
|
40
|
-
);
|
|
26
|
+
if (!response.ok) throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);
|
|
41
27
|
await Bun.write(localPath, response);
|
|
42
28
|
}
|
|
43
|
-
async post<T>(
|
|
44
|
-
url: string,
|
|
45
|
-
data: unknown,
|
|
46
|
-
{ headers }: { headers?: Record<string, string> } = {},
|
|
47
|
-
): Promise<T> {
|
|
29
|
+
async post<T>(url: string, data: unknown, { headers }: { headers?: Record<string, string> } = {}): Promise<T> {
|
|
48
30
|
const isFormData = data instanceof FormData;
|
|
49
31
|
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
50
32
|
method: "POST",
|
|
@@ -64,12 +46,14 @@ class HttpClient {
|
|
|
64
46
|
export class CloudApi {
|
|
65
47
|
readonly #api: HttpClient;
|
|
66
48
|
#accessToken: AccessToken | null = null;
|
|
49
|
+
#workspace: Workspace;
|
|
67
50
|
|
|
68
|
-
static async fromHost(host?: string) {
|
|
51
|
+
static async fromHost(workspace: Workspace, host?: string) {
|
|
69
52
|
const hostConfig = await GlobalConfig.getHostConfig(host);
|
|
70
|
-
return new CloudApi(hostConfig);
|
|
53
|
+
return new CloudApi(workspace, hostConfig);
|
|
71
54
|
}
|
|
72
|
-
constructor(hostConfig: HostConfig) {
|
|
55
|
+
constructor(workspace: Workspace, hostConfig: HostConfig) {
|
|
56
|
+
this.#workspace = workspace;
|
|
73
57
|
const host = akanCloudHost;
|
|
74
58
|
this.#api = new HttpClient(`${host}/api`);
|
|
75
59
|
this.#accessToken = hostConfig.auth?.accessToken ?? null;
|
|
@@ -83,25 +67,20 @@ export class CloudApi {
|
|
|
83
67
|
const formData = new FormData();
|
|
84
68
|
formData.append("devProjectId", devProjectId);
|
|
85
69
|
formData.append("file", file);
|
|
86
|
-
const data = await this.#api.post<boolean>(
|
|
87
|
-
`/uploadEnv/${devProjectId}`,
|
|
88
|
-
formData,
|
|
89
|
-
);
|
|
70
|
+
const data = await this.#api.post<boolean>(`/uploadEnv/${devProjectId}`, formData);
|
|
90
71
|
return data;
|
|
91
72
|
}
|
|
92
|
-
async downloadEnv(devProjectId: string
|
|
73
|
+
async downloadEnv(devProjectId: string): Promise<void> {
|
|
74
|
+
const localPath = `${this.#workspace.workspaceRoot}/local/env.tar`;
|
|
93
75
|
await this.#api.getFile(`/downloadEnv/${devProjectId}`, localPath);
|
|
94
76
|
}
|
|
95
77
|
async getRemoteAuthToken(remoteId: string): Promise<AccessToken | null> {
|
|
96
78
|
try {
|
|
97
79
|
if (this.#accessToken) {
|
|
98
|
-
if (GlobalConfig.needRefreshToken(this.#accessToken))
|
|
99
|
-
|
|
100
|
-
else return await this.refreshAuthToken();
|
|
80
|
+
if (GlobalConfig.needRefreshToken(this.#accessToken)) return await this.#refreshAuthToken();
|
|
81
|
+
else return await this.#refreshAuthToken();
|
|
101
82
|
}
|
|
102
|
-
const accessToken = await this.#api.get<AccessTokenDto>(
|
|
103
|
-
`/getRemoteAuthToken/${remoteId}`,
|
|
104
|
-
);
|
|
83
|
+
const accessToken = await this.#api.get<AccessTokenDto>(`/getRemoteAuthToken/${remoteId}`);
|
|
105
84
|
this.#accessToken = GlobalConfig.toAccessToken(accessToken);
|
|
106
85
|
this.#api.setHeaders({
|
|
107
86
|
Authorization: `Bearer ${this.#accessToken.jwt}`,
|
|
@@ -111,22 +90,20 @@ export class CloudApi {
|
|
|
111
90
|
return null;
|
|
112
91
|
}
|
|
113
92
|
}
|
|
114
|
-
async refreshAuthToken(): Promise<AccessToken> {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
);
|
|
93
|
+
async #refreshAuthToken(): Promise<AccessToken> {
|
|
94
|
+
const refreshToken = this.#accessToken?.refreshToken;
|
|
95
|
+
if (!refreshToken) throw new Error("No refresh token");
|
|
96
|
+
return await this.refreshAuthToken(refreshToken);
|
|
97
|
+
}
|
|
98
|
+
async refreshAuthToken(refreshToken: string): Promise<AccessToken> {
|
|
99
|
+
const response = await this.#api.post<AccessTokenDto>(`/refreshRemoteAuthToken`, { refreshToken });
|
|
121
100
|
this.#accessToken = GlobalConfig.toAccessToken(response);
|
|
122
101
|
this.#api.setHeaders({ Authorization: `Bearer ${this.#accessToken.jwt}` });
|
|
123
102
|
return this.#accessToken;
|
|
124
103
|
}
|
|
125
104
|
async getRemoteSelf(): Promise<{ id: string; nickname: string } | null> {
|
|
126
105
|
try {
|
|
127
|
-
const data = await this.#api.get<{ id: string; nickname: string }>(
|
|
128
|
-
`/getRemoteSelf`,
|
|
129
|
-
);
|
|
106
|
+
const data = await this.#api.get<{ id: string; nickname: string }>(`/getRemoteSelf`);
|
|
130
107
|
return data;
|
|
131
108
|
} catch {
|
|
132
109
|
return null;
|
package/executors.test.ts
CHANGED
|
@@ -251,6 +251,53 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
251
251
|
expect((await stat(path.join(root, "dist/apps/demo/private"))).isDirectory()).toBe(true);
|
|
252
252
|
expect((await stat(path.join(root, "dist/apps/demo/public"))).isDirectory()).toBe(true);
|
|
253
253
|
});
|
|
254
|
+
|
|
255
|
+
test("assigns start command ports from sorted app order", async () => {
|
|
256
|
+
const root = await makeTempRoot();
|
|
257
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
258
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
259
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
260
|
+
|
|
261
|
+
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
262
|
+
for (const appName of ["minimal", "akan"]) {
|
|
263
|
+
await mkdir(path.join(root, `apps/${appName}`), { recursive: true });
|
|
264
|
+
await writeFile(
|
|
265
|
+
path.join(root, `apps/${appName}/akan.config.ts`),
|
|
266
|
+
[
|
|
267
|
+
"export default {",
|
|
268
|
+
` routes: [{ basePath: "${appName}", domains: { debug: ["${appName}.local:8282"] } }],`,
|
|
269
|
+
"};",
|
|
270
|
+
"",
|
|
271
|
+
].join("\n"),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
276
|
+
const akan = AppExecutor.from(workspace, "akan");
|
|
277
|
+
const minimal = AppExecutor.from(workspace, "minimal");
|
|
278
|
+
|
|
279
|
+
const akanStart = await akan.prepareCommand("start");
|
|
280
|
+
expect(akanStart.env.PORT).toBe("8282");
|
|
281
|
+
expect(akanStart.env.AKAN_PUBLIC_CLIENT_PORT).toBe("8282");
|
|
282
|
+
expect(akanStart.env.AKAN_PUBLIC_SERVER_PORT).toBe("8282");
|
|
283
|
+
|
|
284
|
+
const minimalStart = await minimal.prepareCommand("start");
|
|
285
|
+
expect(minimalStart.env.PORT).toBe("8283");
|
|
286
|
+
expect(minimalStart.env.AKAN_PUBLIC_CLIENT_PORT).toBe("8283");
|
|
287
|
+
expect(minimalStart.env.AKAN_PUBLIC_SERVER_PORT).toBe("8283");
|
|
288
|
+
|
|
289
|
+
process.env.PORT_OFFSET = "3";
|
|
290
|
+
|
|
291
|
+
const offsetAkanStart = await akan.prepareCommand("start");
|
|
292
|
+
expect(offsetAkanStart.env.PORT).toBe("8285");
|
|
293
|
+
expect(offsetAkanStart.env.AKAN_PUBLIC_CLIENT_PORT).toBe("8285");
|
|
294
|
+
expect(offsetAkanStart.env.AKAN_PUBLIC_SERVER_PORT).toBe("8285");
|
|
295
|
+
|
|
296
|
+
const offsetMinimalStart = await minimal.prepareCommand("start");
|
|
297
|
+
expect(offsetMinimalStart.env.PORT).toBe("8286");
|
|
298
|
+
expect(offsetMinimalStart.env.AKAN_PUBLIC_CLIENT_PORT).toBe("8286");
|
|
299
|
+
expect(offsetMinimalStart.env.AKAN_PUBLIC_SERVER_PORT).toBe("8286");
|
|
300
|
+
});
|
|
254
301
|
});
|
|
255
302
|
|
|
256
303
|
describe("PkgExecutor package generation", () => {
|
package/executors.ts
CHANGED
|
@@ -163,8 +163,10 @@ const ROOT_LAYOUT_EXPORTS = new Set([
|
|
|
163
163
|
"layoutStyle",
|
|
164
164
|
"gaTrackingId",
|
|
165
165
|
"Loading",
|
|
166
|
+
"NotFound",
|
|
167
|
+
"Error",
|
|
166
168
|
]);
|
|
167
|
-
const LAYOUT_ROUTE_EXPORTS = new Set(["default", "head", "generateHead", "Loading"]);
|
|
169
|
+
const LAYOUT_ROUTE_EXPORTS = new Set(["default", "head", "generateHead", "Loading", "NotFound", "Error"]);
|
|
168
170
|
|
|
169
171
|
function validateRouteSourceExports(
|
|
170
172
|
source: string,
|
|
@@ -739,6 +741,51 @@ export class Executor {
|
|
|
739
741
|
const message = typeChecker.formatDiagnostics(fileDiagnostics);
|
|
740
742
|
return { fileDiagnostics, fileErrors, fileWarnings, message };
|
|
741
743
|
}
|
|
744
|
+
async typeCheckAsync(filePath: string) {
|
|
745
|
+
const path = this.getPath(filePath);
|
|
746
|
+
const entry = await this.#resolveTypecheckWorkerEntry();
|
|
747
|
+
const proc = Bun.spawn([process.execPath, entry], {
|
|
748
|
+
cwd: this.cwdPath,
|
|
749
|
+
env: {
|
|
750
|
+
...process.env,
|
|
751
|
+
AKAN_TYPECHECK_CWD: this.cwdPath,
|
|
752
|
+
AKAN_TYPECHECK_FILE: path,
|
|
753
|
+
},
|
|
754
|
+
stdout: "pipe",
|
|
755
|
+
stderr: "pipe",
|
|
756
|
+
});
|
|
757
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
758
|
+
new Response(proc.stdout).text(),
|
|
759
|
+
new Response(proc.stderr).text(),
|
|
760
|
+
proc.exited,
|
|
761
|
+
]);
|
|
762
|
+
if (exitCode !== 0) throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
|
|
763
|
+
|
|
764
|
+
const result = JSON.parse(stdout) as {
|
|
765
|
+
fileDiagnosticsCount: number;
|
|
766
|
+
fileErrorsCount: number;
|
|
767
|
+
fileWarningsCount: number;
|
|
768
|
+
message: string;
|
|
769
|
+
};
|
|
770
|
+
return {
|
|
771
|
+
fileDiagnostics: Array.from({ length: result.fileDiagnosticsCount }),
|
|
772
|
+
fileErrors: Array.from({ length: result.fileErrorsCount }),
|
|
773
|
+
fileWarnings: Array.from({ length: result.fileWarningsCount }),
|
|
774
|
+
message: result.message,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
async #resolveTypecheckWorkerEntry() {
|
|
778
|
+
const dirname = getDirname(import.meta.url);
|
|
779
|
+
const candidates = [
|
|
780
|
+
path.join(process.cwd(), "pkgs/@akanjs/devkit/typecheck/typecheck.proc.ts"),
|
|
781
|
+
path.join(process.cwd(), "node_modules/@akanjs/devkit/typecheck/typecheck.proc.ts"),
|
|
782
|
+
path.join(dirname, "typecheck/typecheck.proc.ts"),
|
|
783
|
+
path.join(dirname, "typecheck.proc.js"),
|
|
784
|
+
path.join(dirname, "typecheck.proc.ts"),
|
|
785
|
+
];
|
|
786
|
+
for (const candidate of candidates) if (await Bun.file(candidate).exists()) return candidate;
|
|
787
|
+
throw new Error(`[devkit] typecheck worker entry not found; looked in: ${candidates.join(", ")}`);
|
|
788
|
+
}
|
|
742
789
|
getLinter() {
|
|
743
790
|
this.linter ??= new Linter(this.cwdPath);
|
|
744
791
|
return this.linter;
|
|
@@ -811,7 +858,15 @@ export class WorkspaceExecutor extends Executor {
|
|
|
811
858
|
| "local"
|
|
812
859
|
| undefined;
|
|
813
860
|
if (!env) throw new Error("AKAN_PUBLIC_ENV is not set");
|
|
814
|
-
return {
|
|
861
|
+
return {
|
|
862
|
+
...(appName ? { appName } : {}),
|
|
863
|
+
workspaceRoot,
|
|
864
|
+
repoName,
|
|
865
|
+
serveDomain,
|
|
866
|
+
env,
|
|
867
|
+
portOffset,
|
|
868
|
+
workspaceId,
|
|
869
|
+
};
|
|
815
870
|
}
|
|
816
871
|
getWorkspaceId<AllowEmpty extends boolean = false>({
|
|
817
872
|
allowEmpty,
|
|
@@ -1259,6 +1314,13 @@ export class AppExecutor extends SysExecutor {
|
|
|
1259
1314
|
getEnv() {
|
|
1260
1315
|
return WorkspaceExecutor.getBaseDevEnv().env;
|
|
1261
1316
|
}
|
|
1317
|
+
async getDevPort() {
|
|
1318
|
+
const basePort = 8282;
|
|
1319
|
+
const appNames = (await this.workspace.getApps()).sort((a, b) => a.localeCompare(b));
|
|
1320
|
+
const appIndex = Math.max(appNames.indexOf(this.name), 0);
|
|
1321
|
+
const portOffset = WorkspaceExecutor.getBaseDevEnv().portOffset;
|
|
1322
|
+
return basePort + appIndex + portOffset;
|
|
1323
|
+
}
|
|
1262
1324
|
getCommandEnv(env: Record<string, string> = {}): Record<string, string> {
|
|
1263
1325
|
const basePort = 8282;
|
|
1264
1326
|
const portOffset = WorkspaceExecutor.getBaseDevEnv().portOffset;
|
|
@@ -1290,7 +1352,12 @@ export class AppExecutor extends SysExecutor {
|
|
|
1290
1352
|
this.cp("public", `${this.dist.cwdPath}/public`),
|
|
1291
1353
|
]);
|
|
1292
1354
|
} else await this.removeDir(".akan");
|
|
1293
|
-
const
|
|
1355
|
+
const devPort = type === "start" ? (await this.getDevPort()).toString() : undefined;
|
|
1356
|
+
const env = this.getCommandEnv({
|
|
1357
|
+
AKAN_COMMAND_TYPE: type,
|
|
1358
|
+
...routeEnv,
|
|
1359
|
+
...(devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}),
|
|
1360
|
+
});
|
|
1294
1361
|
return { env };
|
|
1295
1362
|
}
|
|
1296
1363
|
#publicEnv: Record<string, string> | null = null;
|
package/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./artifact";
|
|
|
9
9
|
export * from "./builder";
|
|
10
10
|
export * from "./capacitorApp";
|
|
11
11
|
export * from "./cloud";
|
|
12
|
+
export * from "./cloud";
|
|
12
13
|
export * from "./commandDecorators";
|
|
13
14
|
export * from "./createTunnel";
|
|
14
15
|
export * from "./dependencyScanner";
|
|
@@ -34,4 +35,3 @@ export * from "./types";
|
|
|
34
35
|
export * from "./ui";
|
|
35
36
|
export * from "./uploadRelease";
|
|
36
37
|
export * from "./useStdoutDimensions";
|
|
37
|
-
export * from "./cloud";
|