@akanjs/devkit 3.0.0-alpha.64 → 3.0.0-alpha.65
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.
|
@@ -133,6 +133,45 @@ describe("AkanAppConfig", () => {
|
|
|
133
133
|
expect(config.docker.content).toContain('RUN if [ "$TARGETARCH" = "arm64"');
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
test("defaults both web surfaces on and keeps the image free of web env overrides", () => {
|
|
137
|
+
const config = new AkanAppConfig(app, [], packageJson, {}, baseDevEnv);
|
|
138
|
+
|
|
139
|
+
expect(config.web).toEqual({ ssr: true, csr: true });
|
|
140
|
+
expect(config.docker.content).not.toContain("AKAN_SSR");
|
|
141
|
+
expect(config.docker.content).not.toContain("AKAN_CSR");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("bakes the disabled surface into the image env so the default matches what was built", () => {
|
|
145
|
+
const ssrOnly = new AkanAppConfig(app, [], packageJson, { web: { csr: false } }, baseDevEnv);
|
|
146
|
+
expect(ssrOnly.web).toEqual({ ssr: true, csr: false });
|
|
147
|
+
expect(ssrOnly.docker.content).toContain("ENV AKAN_CSR=false");
|
|
148
|
+
expect(ssrOnly.docker.content).not.toContain("ENV AKAN_SSR=false");
|
|
149
|
+
|
|
150
|
+
const apiOnly = new AkanAppConfig(app, [], packageJson, { web: false }, baseDevEnv);
|
|
151
|
+
expect(apiOnly.web).toEqual({ ssr: false, csr: false });
|
|
152
|
+
expect(apiOnly.docker.content).toContain("ENV AKAN_SSR=false");
|
|
153
|
+
expect(apiOnly.docker.content).toContain("ENV AKAN_CSR=false");
|
|
154
|
+
|
|
155
|
+
expect(new AkanAppConfig(app, [], packageJson, { web: true }, baseDevEnv).web).toEqual({ ssr: true, csr: true });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("refuses a csr-less build that ships a mobile app", () => {
|
|
159
|
+
expect(
|
|
160
|
+
() => new AkanAppConfig(app, [], packageJson, { web: { csr: false }, mobile: { appName: "portal" } }, baseDevEnv),
|
|
161
|
+
).toThrow("the Capacitor build ships that bundle");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("installs only ca-certificates and tzdata in the default image", () => {
|
|
165
|
+
const config = new AkanAppConfig(app, [], packageJson, {}, baseDevEnv);
|
|
166
|
+
|
|
167
|
+
expect(config.docker.content).toContain(
|
|
168
|
+
"RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends ca-certificates tzdata && rm -rf /var/lib/apt/lists/*",
|
|
169
|
+
);
|
|
170
|
+
// The Chromium/ffmpeg toolchain moved to per-app `preRuns`; keeping it here paid for it in every image.
|
|
171
|
+
for (const dropped of ["libnss3", "ffmpeg", "build-essential", "redis", "xdg-utils"])
|
|
172
|
+
expect(config.docker.content).not.toContain(dropped);
|
|
173
|
+
});
|
|
174
|
+
|
|
136
175
|
test("creates production package json and reports missing external versions", () => {
|
|
137
176
|
const config = new AkanAppConfig(app, [], packageJson, { externalLibs: ["@external/runtime"] }, baseDevEnv);
|
|
138
177
|
|
package/akanConfig/akanConfig.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
type AkanMobileConfig,
|
|
12
12
|
type AkanMobileTargetConfig,
|
|
13
13
|
type AkanRouteConfig,
|
|
14
|
+
type AkanWebConfig,
|
|
15
|
+
type AkanWebOption,
|
|
14
16
|
type AppConfigResult,
|
|
15
17
|
type Arch,
|
|
16
18
|
archs,
|
|
@@ -163,11 +165,15 @@ const normalizeDeepLinks = (deepLinks: DeepPartial<AkanMobileTargetConfig["deepL
|
|
|
163
165
|
} satisfies AkanMobileTargetConfig["deepLinks"];
|
|
164
166
|
};
|
|
165
167
|
|
|
168
|
+
/** What `akan.config.ts` may write: the resolved shape made partial, with `web` in its authoring union. */
|
|
169
|
+
type AppConfigDeclaration = Omit<DeepPartial<AppConfigResult>, "web"> & { web?: AkanWebOption };
|
|
170
|
+
|
|
166
171
|
export class AkanAppConfig implements AppConfigResult {
|
|
167
172
|
app: App;
|
|
168
173
|
rootPackageJson: PackageJson;
|
|
169
174
|
docker: DockerConfig;
|
|
170
175
|
defaultDatabaseMode: DatabaseMode;
|
|
176
|
+
web: AkanWebConfig;
|
|
171
177
|
externalLibs: string[];
|
|
172
178
|
barrelImports: string[];
|
|
173
179
|
optimizeImports: string[];
|
|
@@ -192,7 +198,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
192
198
|
app: App,
|
|
193
199
|
libs: string[],
|
|
194
200
|
rootPackageJson: PackageJson,
|
|
195
|
-
config:
|
|
201
|
+
config: AppConfigDeclaration,
|
|
196
202
|
baseDevEnv: BaseDevEnv,
|
|
197
203
|
plugins: AkanPlugin[] = [],
|
|
198
204
|
libExternalLibs: readonly string[] = [],
|
|
@@ -221,8 +227,18 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
221
227
|
this.syncPageLibs = (config?.syncPageLibs as string[] | boolean | undefined) ?? false;
|
|
222
228
|
this.hasMobileConfig = Boolean(config.mobile);
|
|
223
229
|
this.mobile = this.#resolveMobileConfig(config.mobile);
|
|
230
|
+
this.web = this.#resolveWebConfig(config.web);
|
|
224
231
|
this.docker = this.#makeDockerContent(config?.docker ?? {});
|
|
225
232
|
}
|
|
233
|
+
#resolveWebConfig(web: AkanWebOption | undefined): AkanWebConfig {
|
|
234
|
+
const resolved = typeof web === "object" ? { ssr: true, csr: web.csr } : { ssr: web ?? true, csr: web ?? true };
|
|
235
|
+
// `akan build-ios` / `build-android` copy `dist/apps/<app>/csr/<target>.html` into the native project.
|
|
236
|
+
if (!resolved.csr && this.hasMobileConfig)
|
|
237
|
+
throw new Error(
|
|
238
|
+
`apps/${this.app.name}/akan.config.ts turns the CSR bundle off but declares mobile targets; the Capacitor build ships that bundle. Drop the mobile section or leave CSR on.`,
|
|
239
|
+
);
|
|
240
|
+
return resolved;
|
|
241
|
+
}
|
|
226
242
|
#resolveMobileConfig(mobile: DeepPartial<AkanMobileConfig> | undefined): AkanMobileConfig {
|
|
227
243
|
const {
|
|
228
244
|
targets: rawTargets,
|
|
@@ -355,10 +371,15 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
355
371
|
? this.#getDockerImageScript(docker.image, "oven/bun:1-slim")
|
|
356
372
|
: "FROM oven/bun:1-slim";
|
|
357
373
|
const command = docker.command ?? ["bun", "main.js"];
|
|
374
|
+
// The image default matches what the build actually produced; a deployment narrows it further with its
|
|
375
|
+
// own env, and can never widen it past the artifacts that are in the image.
|
|
376
|
+
const webEnvLines = [
|
|
377
|
+
...(this.web.ssr ? [] : ["ENV AKAN_SSR=false"]),
|
|
378
|
+
...(this.web.csr ? [] : ["ENV AKAN_CSR=false"]),
|
|
379
|
+
].join("\n");
|
|
358
380
|
const content = `${imageScript}
|
|
381
|
+
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends ca-certificates tzdata && rm -rf /var/lib/apt/lists/*
|
|
359
382
|
RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
|
|
360
|
-
RUN apt-get update && apt-get upgrade -y
|
|
361
|
-
RUN apt-get install -y --no-install-recommends git redis build-essential python3 ca-certificates fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils udev ffmpeg
|
|
362
383
|
ARG TARGETARCH
|
|
363
384
|
${preRunScripts.join("\n")}
|
|
364
385
|
RUN mkdir -p /workspace
|
|
@@ -377,7 +398,7 @@ ${this.basePaths.size ? `ENV AKAN_PUBLIC_BASE_PATHS=${[...this.basePaths].join("
|
|
|
377
398
|
ENV AKAN_PUBLIC_DEFAULT_LOCALE=${this.i18n.defaultLocale}
|
|
378
399
|
ENV AKAN_PUBLIC_LOCALES=${this.i18n.locales.join(",")}
|
|
379
400
|
ENV AKAN_PUBLIC_OPERATION_MODE=cloud
|
|
380
|
-
|
|
401
|
+
${webEnvLines}
|
|
381
402
|
CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
382
403
|
return {
|
|
383
404
|
content,
|
|
@@ -412,7 +433,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
412
433
|
app.workspace.getPackageJson(),
|
|
413
434
|
]);
|
|
414
435
|
const resolved = typeof configImp === "function" ? configImp(app) : configImp;
|
|
415
|
-
const { plugins, ...config } = (resolved ?? {}) as
|
|
436
|
+
const { plugins, ...config } = (resolved ?? {}) as AppConfigDeclaration & { plugins?: AkanPlugin[] };
|
|
416
437
|
const libExternalLibs = await AkanAppConfig.#collectLibExternalLibs(app, libs, bustImportCache);
|
|
417
438
|
return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [], libExternalLibs);
|
|
418
439
|
}
|
package/akanConfig/types.ts
CHANGED
|
@@ -77,6 +77,7 @@ export class ApplicationBuildRunner {
|
|
|
77
77
|
async build({ spinner = false }: BuildOptions = {}): Promise<BuildResult> {
|
|
78
78
|
// serial build is needed because of Bun.build is unstable for parallel build
|
|
79
79
|
const phaseOptions = { spinner };
|
|
80
|
+
const { web } = await this.#app.getConfig();
|
|
80
81
|
await this.#runPhase("prepare", "Preparing output directory", () => this.#app.prepareCommand("build"), undefined, {
|
|
81
82
|
spinner,
|
|
82
83
|
});
|
|
@@ -91,18 +92,20 @@ export class ApplicationBuildRunner {
|
|
|
91
92
|
await this.#runPhase(
|
|
92
93
|
"ssr",
|
|
93
94
|
"Building SSR route artifacts",
|
|
94
|
-
() => this.#buildSsr(),
|
|
95
|
+
async () => (web.ssr ? await this.#buildSsr() : null),
|
|
95
96
|
(result) =>
|
|
96
97
|
result
|
|
97
98
|
? `${result.allRoutes.manifest.routeIds.length} routes, ${result.allRoutes.manifest.knownEntries.length} entries`
|
|
98
|
-
:
|
|
99
|
+
: web.ssr
|
|
100
|
+
? "skipped"
|
|
101
|
+
: "disabled by akan.config.ts web.ssr",
|
|
99
102
|
phaseOptions,
|
|
100
103
|
);
|
|
101
104
|
await this.#runPhase(
|
|
102
105
|
"csr",
|
|
103
106
|
"Building CSR assets",
|
|
104
|
-
() => this.#buildCsr(),
|
|
105
|
-
(result) => result?.outputDir ?? "skipped",
|
|
107
|
+
async () => (web.csr ? await this.#buildCsr() : null),
|
|
108
|
+
(result) => result?.outputDir ?? (web.csr ? "skipped" : "disabled by akan.config.ts web.csr"),
|
|
106
109
|
phaseOptions,
|
|
107
110
|
);
|
|
108
111
|
await this.#runPhase(
|
|
@@ -174,6 +177,7 @@ export class ApplicationBuildRunner {
|
|
|
174
177
|
const backendExternals = [
|
|
175
178
|
...new Set([...akanConfig.externalLibs, ...SSR_RENDER_EXTERNALS, ...AKAN_OPTIONAL_BACKEND_EXTERNALS]),
|
|
176
179
|
];
|
|
180
|
+
const { web } = akanConfig;
|
|
177
181
|
const backendEntryPoints = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
|
|
178
182
|
for (const entrypoint of backendEntryPoints) {
|
|
179
183
|
if (!(await Bun.file(entrypoint).exists())) throw new Error(`Backend entrypoint not found: ${entrypoint}`);
|
|
@@ -187,17 +191,20 @@ export class ApplicationBuildRunner {
|
|
|
187
191
|
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
188
192
|
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
189
193
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
// Nothing spawns the RSC worker without SSR, so an api-only image does not carry it.
|
|
195
|
+
const rscWorkerResult = web.ssr
|
|
196
|
+
? await this.#buildOrThrow("rsc-worker", {
|
|
197
|
+
entrypoints: [this.#resolveRscWorkerBuildEntry()],
|
|
198
|
+
outdir: this.#app.dist.cwdPath,
|
|
199
|
+
target: "bun",
|
|
200
|
+
minify: true,
|
|
201
|
+
naming: { entry: "[name].[ext]", chunk: "chunk-[hash].[ext]" },
|
|
202
|
+
conditions: ["react-server"],
|
|
203
|
+
// `akan build` must embed production react-server-dom regardless of the shell's NODE_ENV.
|
|
204
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
205
|
+
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
206
|
+
})
|
|
207
|
+
: null;
|
|
201
208
|
const consoleRuntimeResult = await this.#buildOrThrow("console-runtime", {
|
|
202
209
|
entrypoints: [this.#resolveConsoleRuntimeBuildEntry()],
|
|
203
210
|
outdir: this.#app.dist.cwdPath,
|
|
@@ -208,8 +215,9 @@ export class ApplicationBuildRunner {
|
|
|
208
215
|
});
|
|
209
216
|
await this.#writeConsoleShim();
|
|
210
217
|
return {
|
|
211
|
-
entrypoints: backendEntryPoints.length + 2,
|
|
212
|
-
outputs:
|
|
218
|
+
entrypoints: backendEntryPoints.length + (rscWorkerResult ? 2 : 1),
|
|
219
|
+
outputs:
|
|
220
|
+
backendResult.outputs.length + (rscWorkerResult?.outputs.length ?? 0) + consoleRuntimeResult.outputs.length + 1,
|
|
213
221
|
};
|
|
214
222
|
}
|
|
215
223
|
|
package/executors.ts
CHANGED
|
@@ -1371,11 +1371,20 @@ export class AppExecutor extends SysExecutor {
|
|
|
1371
1371
|
await Promise.all([this.dist.mkdir("private"), this.dist.mkdir("public")]);
|
|
1372
1372
|
//* Lib assets are symlinks in the app dir (see syncAssets). dist is the docker build context and the
|
|
1373
1373
|
//* release tarball root, neither of which follows a link out of itself, so materialize them here.
|
|
1374
|
+
//* `public/` has exactly one reader — the web router's catch-all — so an api-only build leaves it out.
|
|
1374
1375
|
await Promise.all([
|
|
1375
1376
|
this.cp("private", `${this.dist.cwdPath}/private`, { dereference: true }),
|
|
1376
|
-
this.cp("public", `${this.dist.cwdPath}/public`, { dereference: true }),
|
|
1377
|
+
...(akanConfig.web.ssr ? [this.cp("public", `${this.dist.cwdPath}/public`, { dereference: true })] : []),
|
|
1377
1378
|
]);
|
|
1378
|
-
} else
|
|
1379
|
+
} else {
|
|
1380
|
+
await this.removeDir(".akan");
|
|
1381
|
+
//? `web` is a build declaration: `akan start` keeps the full dev surface because the incremental
|
|
1382
|
+
//? builder is also the file watcher, so switching it off would take server-code HMR with it.
|
|
1383
|
+
if (!akanConfig.web.ssr || !akanConfig.web.csr)
|
|
1384
|
+
this.logger.warn(
|
|
1385
|
+
`akan.config.ts disables web.${!akanConfig.web.ssr ? "ssr" : "csr"}; \`akan start\` still serves it, \`akan build\` will not`,
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1379
1388
|
//? `akan start` is the dev server, and it must say so rather than inherit an answer. Bun auto-loads the
|
|
1380
1389
|
//? workspace `.env`, so NODE_ENV=production can reach this process without ever being exported in a shell —
|
|
1381
1390
|
//? and a dev server that believes it is production serves from the production route cache, whose every
|
|
@@ -77,6 +77,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
77
77
|
branches: [...akanConfig.branches],
|
|
78
78
|
i18n: akanConfig.i18n,
|
|
79
79
|
imageConfig: akanConfig.images,
|
|
80
|
+
web: akanConfig.web,
|
|
80
81
|
deepLinkAssociations: Object.values(akanConfig.mobile.targets)
|
|
81
82
|
.filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0)
|
|
82
83
|
.map((target) => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.65",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@langchain/openai": "^1.4.6",
|
|
46
46
|
"@tailwindcss/node": "^4.3.0",
|
|
47
47
|
"@trapezedev/project": "^7.1.4",
|
|
48
|
-
"akanjs": "3.0.0-alpha.
|
|
48
|
+
"akanjs": "3.0.0-alpha.65",
|
|
49
49
|
"chalk": "^5.6.2",
|
|
50
50
|
"commander": "^14.0.3",
|
|
51
51
|
"dayjs": "^1.11.20",
|