@akanjs/devkit 3.1.0-rc.0 → 3.1.0-rc.10
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/abstractDoc.ts +1 -1
- package/agentsIndex.test.ts +4 -1
- package/akanConfig/akanConfig.test.ts +51 -5
- package/akanConfig/akanConfig.ts +33 -14
- package/akanConfig/akanWorkspaceConfig.test.ts +6 -6
- package/akanConfig/akanWorkspaceConfig.ts +1 -1
- package/akanContext.ts +4 -4
- package/akanMcpContract.ts +1 -1
- package/applicationBuildRunner.test.ts +57 -2
- package/applicationBuildRunner.ts +15 -6
- package/artifact/implicitRootLayout.test.ts +2 -0
- package/artifact/implicitRootLayout.ts +1 -0
- package/biome.base.json +1 -11
- package/biomeBase.ts +1 -1
- package/cloud/cloudApi.ts +25 -13
- package/cloud/cloudHttpError.test.ts +32 -0
- package/cloud/cloudHttpError.ts +54 -0
- package/cloud/cloudUpdates.ts +138 -0
- package/cloud/configLock.ts +2 -1
- package/cloud/globalConfig.test.ts +17 -0
- package/cloud/globalConfig.ts +9 -2
- package/cloud/index.ts +3 -0
- package/cloud/pipelineSecrets.ts +114 -0
- package/cloud/updatePackUpload.test.ts +151 -41
- package/cloud/updatePackUpload.ts +60 -23
- package/codeAgent/agent/CodeAgentProxy.ts +2 -2
- package/codeAgent/agent/akanCodeModel.ts +1 -1
- package/codeAgent/agent/akanContextFiles.test.ts +3 -1
- package/codeAgent/tools/WebToolPack.ts +4 -4
- package/codeAgent/tools/akanCodePlugins.test.ts +20 -16
- package/commandDecorators/argMeta.ts +11 -2
- package/commandDecorators/command.ts +45 -10
- package/commandDecorators/commandBuilder.ts +6 -1
- package/commandDecorators/commandDecorators.test.ts +205 -4
- package/commandDecorators/exitCodeError.ts +15 -0
- package/commandDecorators/index.ts +1 -0
- package/createTunnel.test.ts +14 -0
- package/createTunnel.ts +3 -1
- package/dependencyScanner.ts +1 -1
- package/devkitUtils.test.ts +1 -1
- package/executors.test.ts +131 -3
- package/executors.ts +56 -36
- package/frontendBuild/autoImportSync.test.ts +3 -1
- package/frontendBuild/clientEntryDiscovery.ts +1 -1
- package/frontendBuild/csrArtifactBuilder.test.ts +30 -0
- package/frontendBuild/csrArtifactBuilder.ts +23 -4
- package/frontendBuild/csrDevPatcher.test.ts +4 -1
- package/frontendBuild/cssBlockParser.test.ts +123 -0
- package/frontendBuild/cssBlockParser.ts +132 -0
- package/frontendBuild/cssCompiler.test.ts +53 -0
- package/frontendBuild/cssCompiler.ts +8 -4
- package/frontendBuild/fontOptimizer.ts +2 -1
- package/frontendBuild/frontendBuild.test.ts +14 -0
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.test.ts +98 -0
- package/frontendBuild/pagesBundleBuilder.ts +64 -9
- package/frontendBuild/routeClientBuilder.ts +1 -1
- package/frontendBuild/sourceMtimeIndex.test.ts +1 -1
- package/frontendBuild/themeValidator.test.ts +83 -2
- package/frontendBuild/themeValidator.ts +37 -15
- package/getRelatedCnsts.ts +2 -1
- package/incrementalBuilder/buildBatch.proc.ts +12 -7
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +63 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +7 -5
- package/integration/devStabilityHarness.ts +5 -3
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +1 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +1 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +1 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +1 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +1 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +1 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +1 -0
- package/lint/__fixtures__/no-import-external-library-in-test/bad.tsx +1 -0
- package/lint/__fixtures__/no-import-external-library-in-test/good.tsx +1 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +1 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -1
- package/lint/__fixtures__/no-static-in-object-light-model/bad.ts +2 -2
- package/mobile/desktopServerStage.test.ts +13 -0
- package/mobile/desktopServerStage.ts +5 -7
- package/mobile/index.ts +2 -0
- package/mobile/mobileTarget.ts +1 -2
- package/mobile/nativeApi.test.ts +26 -0
- package/mobile/nativeApi.ts +5 -2
- package/mobile/nativeApp.test.ts +135 -0
- package/mobile/nativeApp.ts +109 -63
- package/mobile/nativeConfig.test.ts +3 -0
- package/mobile/nativeConfig.ts +5 -3
- package/mobile/nativePublicEnv.test.ts +131 -0
- package/mobile/nativePublicEnv.ts +116 -0
- package/mobile/nativeWebDir.ts +3 -2
- package/mobile/serverBundleSecrets.test.ts +79 -0
- package/mobile/serverBundleSecrets.ts +60 -16
- package/mobile/updatePackFolder.test.ts +98 -0
- package/mobile/updatePackFolder.ts +131 -0
- package/mobile/updateUploader.test.ts +115 -22
- package/mobile/updateUploader.ts +77 -10
- package/package.json +20 -20
- package/piDependencyPins.test.ts +18 -19
- package/pipeline/PipelineCompiler.test.ts +157 -1
- package/pipeline/PipelineCompiler.ts +80 -46
- package/pipeline/PipelineDefinition.test.ts +9 -0
- package/pipeline/PipelineDefinition.ts +4 -3
- package/pipeline/PipelineDryRun.test.ts +27 -0
- package/pipeline/PipelineDryRun.ts +2 -2
- package/pipeline/PipelineInputReader.ts +157 -253
- package/pipeline/PipelineRules.ts +92 -0
- package/pipeline/index.ts +1 -0
- package/platformTest/LinuxDockerTarget.ts +2 -1
- package/platformTest/PlatformTestTarget.ts +7 -5
- package/platformTest/WindowsSshTarget.ts +2 -1
- package/qualityScanner.test.ts +5 -3
- package/qualityScanner.ts +1 -1
- package/recipeScanner.test.ts +9 -3
- package/scanInfo.ts +1 -1
- package/slicePlanner.ts +5 -3
- package/subspace.test.ts +90 -19
- package/subspace.ts +52 -500
- package/subspaceClone.ts +362 -0
- package/subspaceConfig.ts +1 -2
- package/subspaceReport.ts +162 -0
- package/testHelpers.ts +2 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/browserDirnamePlugin.test.ts +88 -0
- package/transforms/browserDirnamePlugin.ts +25 -0
- package/transforms/rscUseClientTransform.ts +2 -2
- package/transforms/serverEnvPlugin.test.ts +76 -0
- package/transforms/serverEnvPlugin.ts +9 -25
- package/transforms/serverEnvProfile.test.ts +189 -0
- package/transforms/serverEnvProfile.ts +324 -60
- package/transforms/transforms.test.ts +18 -2
- package/transforms/tsconfigPackageResolver.test.ts +9 -0
- package/typecheck/projectTypecheck.ts +6 -5
- package/types.ts +1 -0
- package/workflow/artifacts.ts +1 -1
- package/workflow/rolloutGate.test.ts +5 -1
- package/workflow/source.ts +2 -2
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "./argMeta";
|
|
24
24
|
import { camelToKebabCase } from "./camelToKebabCase";
|
|
25
25
|
import { CommandContainer } from "./dependencyBuilder";
|
|
26
|
+
import { ExitCodeError } from "./exitCodeError";
|
|
26
27
|
import { formatCommandHelp, formatHelp } from "./helpFormatter";
|
|
27
28
|
import { type CommandCls, getTargetCommandNames, getTargetMetas } from "./targetMeta";
|
|
28
29
|
|
|
@@ -102,6 +103,9 @@ const confirm = ((config, context) => prompts().then((m) => m.confirm(config, co
|
|
|
102
103
|
const input = ((config, context) => prompts().then((m) => m.input(config, context))) as typeof inquirerInput;
|
|
103
104
|
const checkbox = ((config, context) => prompts().then((m) => m.checkbox(config, context))) as typeof inquirerCheckbox;
|
|
104
105
|
|
|
106
|
+
const argLabelOf = (argMeta: ArgMeta) =>
|
|
107
|
+
argMeta.type === "Option" ? `option '--${camelToKebabCase(argMeta.name)}'` : `argument '${argMeta.name}'`;
|
|
108
|
+
|
|
105
109
|
// A DynamicEnum is not checked: it resolves against a context the internal args have not populated yet.
|
|
106
110
|
// Compared as strings: the value is still commander's raw string while a numeric choice list holds numbers.
|
|
107
111
|
const assertEnumChoice = (argMeta: ArgMeta, value: unknown) => {
|
|
@@ -109,10 +113,19 @@ const assertEnumChoice = (argMeta: ArgMeta, value: unknown) => {
|
|
|
109
113
|
if (!enumChoices || typeof enumChoices === "function") return;
|
|
110
114
|
const choices = normalizeEnumChoices(enumChoices);
|
|
111
115
|
if (choices.some((choice) => String(choice.value) === String(value))) return;
|
|
112
|
-
const label =
|
|
113
|
-
argMeta.type === "Option" ? `option '--${camelToKebabCase(argMeta.name)}'` : `argument '${argMeta.name}'`;
|
|
114
116
|
throw new Error(
|
|
115
|
-
`${
|
|
117
|
+
`${argLabelOf(argMeta)} argument '${String(value)}' is invalid. Allowed choices are ${choices.map((choice) => choice.name).join(", ")}.`,
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// On a piped stdin a prompt takes the pipe as its answer and echoes it: a piped secret printed as the secret's name.
|
|
122
|
+
export const assertPromptable = (
|
|
123
|
+
problem: string,
|
|
124
|
+
{ choices = [], remedy = "pass it on the command line" }: { choices?: readonly unknown[]; remedy?: string } = {},
|
|
125
|
+
) => {
|
|
126
|
+
if (process.stdin.isTTY) return;
|
|
127
|
+
throw new Error(
|
|
128
|
+
`${problem}: stdin is not a terminal to prompt on, so ${remedy}${choices.length ? ` (choices: ${choices.join(", ")})` : ""}.`,
|
|
116
129
|
);
|
|
117
130
|
};
|
|
118
131
|
|
|
@@ -136,8 +149,11 @@ export const getOptionValue = async (argMeta: ArgMeta, opt: Record<string, unkno
|
|
|
136
149
|
(typeof enumChoices === "function" ? await enumChoices(context) : enumChoices) ?? [],
|
|
137
150
|
);
|
|
138
151
|
if (choices.length === 1) return choices[0]?.value;
|
|
152
|
+
assertPromptable(`Missing ${argLabelOf(argMeta)}`, { choices: choices.map((choice) => choice.value) });
|
|
139
153
|
return await select({ message: ask ?? desc ?? `Select the ${name} value`, choices });
|
|
140
|
-
}
|
|
154
|
+
}
|
|
155
|
+
assertPromptable(`Missing ${argLabelOf(argMeta)}`);
|
|
156
|
+
if (type === "boolean") {
|
|
141
157
|
const message = ask ?? desc ?? `Do you want to set ${name}? ${desc ? ` (${desc})` : ""}: `;
|
|
142
158
|
return await confirm({ message });
|
|
143
159
|
} else return convertArgValue(await input({ message: inputMessageOf(argMeta) }), type ?? "string");
|
|
@@ -150,6 +166,13 @@ export const getArgumentValue = async (argMeta: ArgMeta, value: string | undefin
|
|
|
150
166
|
return convertArgValue(value, type ?? "string");
|
|
151
167
|
} else if (defaultValue !== undefined) return defaultValue;
|
|
152
168
|
else if (nullable) return null;
|
|
169
|
+
const { enum: enumChoices } = argMeta.argsOption;
|
|
170
|
+
assertPromptable(`Missing ${argLabelOf(argMeta)}`, {
|
|
171
|
+
choices:
|
|
172
|
+
enumChoices && typeof enumChoices !== "function"
|
|
173
|
+
? normalizeEnumChoices(enumChoices).map(({ value }) => value)
|
|
174
|
+
: [],
|
|
175
|
+
});
|
|
153
176
|
return convertArgValue(await input({ message: inputMessageOf(argMeta) }), type ?? "string");
|
|
154
177
|
};
|
|
155
178
|
|
|
@@ -163,20 +186,21 @@ const assignCommandContext = (context: CommandContext, argMeta: ArgMeta | Intern
|
|
|
163
186
|
else context.values[argMeta.type.toLowerCase()] = value;
|
|
164
187
|
};
|
|
165
188
|
|
|
189
|
+
//? Not `.env`, which a fresh clone lacks; an app or a package folder has a package.json and tsconfig.json of its own.
|
|
166
190
|
const assertCurrentDirectoryIsWorkspaceRoot = async () => {
|
|
167
191
|
const cwd = process.cwd();
|
|
168
|
-
const [hasPackageJson, hasTsConfig,
|
|
192
|
+
const [hasPackageJson, hasTsConfig, ...hasMemberDirs] = await Promise.all([
|
|
169
193
|
FileSys.fileExists(`${cwd}/package.json`),
|
|
170
194
|
FileSys.fileExists(`${cwd}/tsconfig.json`),
|
|
171
|
-
FileSys.
|
|
195
|
+
...["apps", "libs", "pkgs"].map((dir) => FileSys.dirExists(`${cwd}/${dir}`)),
|
|
172
196
|
]);
|
|
173
|
-
if (hasPackageJson && hasTsConfig &&
|
|
197
|
+
if (hasPackageJson && hasTsConfig && hasMemberDirs.some(Boolean)) return;
|
|
174
198
|
|
|
175
199
|
throw new Error(
|
|
176
200
|
[
|
|
177
201
|
"Akan CLI commands must be run from the workspace root.",
|
|
178
202
|
`Current directory: ${cwd}`,
|
|
179
|
-
"Move to the directory that contains package.json, tsconfig.json, and
|
|
203
|
+
"Move to the directory that contains package.json, tsconfig.json, and an apps/, libs/ or pkgs/ folder, then run the command again.",
|
|
180
204
|
].join("\n"),
|
|
181
205
|
);
|
|
182
206
|
};
|
|
@@ -205,6 +229,7 @@ export const getAppsArgumentValue = async (
|
|
|
205
229
|
return [...new Set(requested)].map((name) => AppExecutor.from(workspace, name));
|
|
206
230
|
}
|
|
207
231
|
if (appNames.length === 1 && appNames[0]) return [AppExecutor.from(workspace, appNames[0])];
|
|
232
|
+
assertPromptable("Missing apps", { choices: [...appNames, "all"] });
|
|
208
233
|
const remembered = new Set(await AppSelectionMemory.read(workspace.workspaceRoot));
|
|
209
234
|
const picked = await checkbox<string>({
|
|
210
235
|
message: "Select the apps to run (space to toggle, enter to confirm)",
|
|
@@ -224,6 +249,7 @@ export const getInternalArgumentValue = async (
|
|
|
224
249
|
if (argMeta.type === "Workspace") return workspace;
|
|
225
250
|
const sysType = argMeta.type.toLowerCase();
|
|
226
251
|
const [appNames, libNames, pkgNames] = await workspace.getExecs();
|
|
252
|
+
const problemOf = (kind: string) => (value ? `Unknown ${kind} '${value}'` : `Missing ${kind}`);
|
|
227
253
|
if (sysType === "sys" || sysType === "exec") {
|
|
228
254
|
const pkgChoices = sysType === "exec" ? pkgNames : [];
|
|
229
255
|
const execOf = (name: string) => {
|
|
@@ -234,6 +260,11 @@ export const getInternalArgumentValue = async (
|
|
|
234
260
|
};
|
|
235
261
|
const found = value ? execOf(value) : null;
|
|
236
262
|
if (found) return found;
|
|
263
|
+
if (!value && argMeta.option?.onlyApp && appNames.length === 1 && appNames[0])
|
|
264
|
+
return AppExecutor.from(workspace, appNames[0]);
|
|
265
|
+
assertPromptable(problemOf(sysType === "exec" ? "app, lib or pkg" : "app or lib"), {
|
|
266
|
+
choices: [...appNames, ...libNames, ...pkgChoices],
|
|
267
|
+
});
|
|
237
268
|
const name = await select<string>({
|
|
238
269
|
message: sysType === "exec" ? `Select the App or Lib or Pkg name` : `Select the App or Lib name`,
|
|
239
270
|
choices: [...appNames, ...libNames, ...pkgChoices],
|
|
@@ -244,15 +275,18 @@ export const getInternalArgumentValue = async (
|
|
|
244
275
|
} else if (sysType === "app") {
|
|
245
276
|
if (value && appNames.includes(value)) return AppExecutor.from(workspace, value);
|
|
246
277
|
if (!value && appNames.length === 1 && appNames[0]) return AppExecutor.from(workspace, appNames[0]);
|
|
278
|
+
assertPromptable(problemOf("app"), { choices: appNames });
|
|
247
279
|
const appName = await select<string>({ message: `Select the ${sysType} name`, choices: appNames });
|
|
248
280
|
return AppExecutor.from(workspace, appName);
|
|
249
281
|
} else if (sysType === "lib") {
|
|
250
282
|
if (value && libNames.includes(value)) return LibExecutor.from(workspace, value);
|
|
283
|
+
assertPromptable(problemOf("lib"), { choices: libNames });
|
|
251
284
|
const libName = await select<string>({ message: `Select the ${sysType} name`, choices: libNames });
|
|
252
285
|
return LibExecutor.from(workspace, libName);
|
|
253
286
|
} else if (sysType === "pkg") {
|
|
254
287
|
const pkgs = await workspace.getPkgs();
|
|
255
288
|
if (value && pkgs.includes(value)) return PkgExecutor.from(workspace, value);
|
|
289
|
+
assertPromptable(problemOf("pkg"), { choices: pkgs });
|
|
256
290
|
const pkgName = await select<string>({ message: `Select the ${sysType} name`, choices: pkgs });
|
|
257
291
|
return PkgExecutor.from(workspace, pkgName);
|
|
258
292
|
} else if (sysType === "module") {
|
|
@@ -270,6 +304,7 @@ export const getInternalArgumentValue = async (
|
|
|
270
304
|
if (modules.includes(moduleName)) return ModuleExecutor.from(lib, moduleName);
|
|
271
305
|
} else throw new Error(`Invalid system name: ${sysName}`);
|
|
272
306
|
}
|
|
307
|
+
assertPromptable(problemOf("module"));
|
|
273
308
|
const { type, name } = await select<{ type: "app" | "lib"; name: string }>({
|
|
274
309
|
message: `select the App or Lib name`,
|
|
275
310
|
choices: [
|
|
@@ -291,7 +326,7 @@ export const runCommands = async (...commands: CommandCls[]) => {
|
|
|
291
326
|
let failureExitCode = 1;
|
|
292
327
|
process.on("unhandledRejection", (error) => {
|
|
293
328
|
printCliError(error);
|
|
294
|
-
process.exit(failureExitCode);
|
|
329
|
+
process.exit(ExitCodeError.codeOf(error) ?? failureExitCode);
|
|
295
330
|
});
|
|
296
331
|
const __dirname = getDirname(import.meta.url);
|
|
297
332
|
const packageJsonCandidates = [`${path.dirname(Bun.main)}/package.json`, `${__dirname}/../package.json`];
|
|
@@ -419,6 +454,6 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
419
454
|
// Handled here, not by the handler above: a rejection reaching the entry's top-level await prints Bun's own trace.
|
|
420
455
|
await program.parseAsync(process.argv).catch((error: unknown) => {
|
|
421
456
|
printCliError(error);
|
|
422
|
-
process.exit(failureExitCode);
|
|
457
|
+
process.exit(ExitCodeError.codeOf(error) ?? failureExitCode);
|
|
423
458
|
});
|
|
424
459
|
};
|
|
@@ -115,7 +115,12 @@ class TargetBuilder<Deps extends readonly DependencyCls[], Params extends unknow
|
|
|
115
115
|
|
|
116
116
|
with<const Tokens extends readonly InternalArgToken[]>(...tokens: Tokens) {
|
|
117
117
|
return this.#add<AddInternalArgs<Params, Tokens>, AddInternalContext<Context, Tokens>>(
|
|
118
|
-
...tokens.map((token, offset) => ({
|
|
118
|
+
...tokens.map((token, offset) => ({
|
|
119
|
+
key: "",
|
|
120
|
+
idx: this.#args.length + offset,
|
|
121
|
+
type: token.type,
|
|
122
|
+
...(token.option ? { option: token.option } : {}),
|
|
123
|
+
})),
|
|
119
124
|
);
|
|
120
125
|
}
|
|
121
126
|
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AppExecutor, LibExecutor, WorkspaceExecutor } from "../executors";
|
|
4
|
+
import { makeCliTempWorkspace, tempRoots, writeText } from "../testHelpers";
|
|
5
|
+
import {
|
|
6
|
+
App,
|
|
7
|
+
type ArgMeta,
|
|
8
|
+
type ArgsOption,
|
|
9
|
+
getArgMetas,
|
|
10
|
+
type InternalArgMeta,
|
|
11
|
+
Lib,
|
|
12
|
+
Module,
|
|
13
|
+
Pkg,
|
|
14
|
+
Sys,
|
|
15
|
+
SysOrOnlyApp,
|
|
16
|
+
Workspace,
|
|
17
|
+
} from "./argMeta";
|
|
18
|
+
import { getAppsArgumentValue, getArgumentValue, getInternalArgumentValue, getOptionValue } from "./command";
|
|
5
19
|
import { command } from "./commandBuilder";
|
|
6
20
|
import {
|
|
7
21
|
assertUniqueDependencies,
|
|
@@ -289,6 +303,89 @@ describe("enum arg choices", () => {
|
|
|
289
303
|
});
|
|
290
304
|
});
|
|
291
305
|
|
|
306
|
+
describe("prompts on a stdin that is not a terminal", () => {
|
|
307
|
+
const ownIsTTY = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
|
308
|
+
beforeEach(() => {
|
|
309
|
+
Object.defineProperty(process.stdin, "isTTY", { value: undefined, configurable: true });
|
|
310
|
+
});
|
|
311
|
+
afterEach(() => {
|
|
312
|
+
if (ownIsTTY) Object.defineProperty(process.stdin, "isTTY", ownIsTTY);
|
|
313
|
+
else Reflect.deleteProperty(process.stdin, "isTTY");
|
|
314
|
+
});
|
|
315
|
+
const metaOf = (type: "Argument" | "Option", name: string, argsOption: ArgsOption = {}): ArgMeta => ({
|
|
316
|
+
name,
|
|
317
|
+
argsOption: { type: "string", ...argsOption },
|
|
318
|
+
key: "run",
|
|
319
|
+
idx: 0,
|
|
320
|
+
type,
|
|
321
|
+
});
|
|
322
|
+
const context = { values: {} };
|
|
323
|
+
const workspaceWith = (appNames: string[]) => {
|
|
324
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: "/workspace", repoName: "repo" });
|
|
325
|
+
workspace.getExecs = async () => [appNames, ["util"], []];
|
|
326
|
+
workspace.getApps = async () => appNames;
|
|
327
|
+
return workspace;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
test("a missing positional is refused by name instead of read from the pipe", async () => {
|
|
331
|
+
await expect(getArgumentValue(metaOf("Argument", "name", { desc: "secret name" }), undefined)).rejects.toThrow(
|
|
332
|
+
"Missing argument 'name': stdin is not a terminal to prompt on, so pass it on the command line.",
|
|
333
|
+
);
|
|
334
|
+
await expect(getArgumentValue(metaOf("Argument", "action", { enum: ["scan", "ssr"] }), undefined)).rejects.toThrow(
|
|
335
|
+
"Missing argument 'action': stdin is not a terminal to prompt on, so pass it on the command line (choices: scan, ssr).",
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("a missing option names its flag and the values it takes", async () => {
|
|
340
|
+
await expect(
|
|
341
|
+
getOptionValue(metaOf("Option", "platform", { enum: ["ios", "android"] }), {}, context),
|
|
342
|
+
).rejects.toThrow(
|
|
343
|
+
"Missing option '--platform': stdin is not a terminal to prompt on, so pass it on the command line (choices: ios, android).",
|
|
344
|
+
);
|
|
345
|
+
await expect(
|
|
346
|
+
getOptionValue(
|
|
347
|
+
metaOf("Option", "format", { enum: [{ label: "JSON output", value: "json" }, "text"] }),
|
|
348
|
+
{},
|
|
349
|
+
context,
|
|
350
|
+
),
|
|
351
|
+
).rejects.toThrow("(choices: json, text)");
|
|
352
|
+
await expect(getOptionValue(metaOf("Option", "assembleType"), {}, context)).rejects.toThrow(
|
|
353
|
+
"Missing option '--assemble-type'",
|
|
354
|
+
);
|
|
355
|
+
await expect(getOptionValue(metaOf("Option", "force", { type: "boolean" }), {}, context)).rejects.toThrow(
|
|
356
|
+
"Missing option '--force'",
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("a value given, defaulted, nullable or the only choice still resolves without a prompt", async () => {
|
|
361
|
+
expect(await getArgumentValue(metaOf("Argument", "name"), "NPM_TOKEN")).toBe("NPM_TOKEN");
|
|
362
|
+
expect(await getOptionValue(metaOf("Option", "env", { default: "main" }), {}, context)).toBe("main");
|
|
363
|
+
expect(await getOptionValue(metaOf("Option", "host", { nullable: true }), {}, context)).toBe(null);
|
|
364
|
+
expect(await getOptionValue(metaOf("Option", "target", { enum: async () => ["default"] }), {}, context)).toBe(
|
|
365
|
+
"default",
|
|
366
|
+
);
|
|
367
|
+
const [app] = await getAppsArgumentValue(undefined, workspaceWith(["web"]));
|
|
368
|
+
expect(app?.name).toBe("web");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("an app that is missing or unknown is refused with the workspace's apps", async () => {
|
|
372
|
+
const workspace = workspaceWith(["web", "admin"]);
|
|
373
|
+
await expect(getInternalArgumentValue({ key: "", idx: 0, type: "App" }, undefined, workspace)).rejects.toThrow(
|
|
374
|
+
"Missing app: stdin is not a terminal to prompt on, so pass it on the command line (choices: web, admin).",
|
|
375
|
+
);
|
|
376
|
+
await expect(getInternalArgumentValue({ key: "", idx: 0, type: "App" }, "globex", workspace)).rejects.toThrow(
|
|
377
|
+
"Unknown app 'globex'",
|
|
378
|
+
);
|
|
379
|
+
await expect(getInternalArgumentValue({ key: "", idx: 0, type: "Sys" }, "globex", workspace)).rejects.toThrow(
|
|
380
|
+
"Unknown app or lib 'globex': stdin is not a terminal to prompt on, so pass it on the command line (choices: web, admin, util).",
|
|
381
|
+
);
|
|
382
|
+
await expect(getInternalArgumentValue({ key: "", idx: 0, type: "Module" }, undefined, workspace)).rejects.toThrow(
|
|
383
|
+
"Missing module",
|
|
384
|
+
);
|
|
385
|
+
await expect(getAppsArgumentValue(undefined, workspace)).rejects.toThrow("(choices: web, admin, all)");
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
292
389
|
describe("enum arg type inference", () => {
|
|
293
390
|
afterEach(() => {
|
|
294
391
|
CommandContainer.clear();
|
|
@@ -334,3 +431,107 @@ describe("enum arg type inference", () => {
|
|
|
334
431
|
]);
|
|
335
432
|
});
|
|
336
433
|
});
|
|
434
|
+
|
|
435
|
+
describe("a Sys that defaults to the only app", () => {
|
|
436
|
+
const ownIsTTY = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
|
437
|
+
const asked: unknown[] = [];
|
|
438
|
+
let answer = "";
|
|
439
|
+
const setTTY = (isTTY: boolean | undefined) =>
|
|
440
|
+
Object.defineProperty(process.stdin, "isTTY", { value: isTTY, configurable: true });
|
|
441
|
+
beforeEach(() => {
|
|
442
|
+
asked.length = 0;
|
|
443
|
+
mock.module("@inquirer/prompts", () => ({
|
|
444
|
+
select: async ({ choices }: { choices: unknown[] }) => {
|
|
445
|
+
asked.push(choices);
|
|
446
|
+
return answer;
|
|
447
|
+
},
|
|
448
|
+
}));
|
|
449
|
+
});
|
|
450
|
+
afterEach(() => {
|
|
451
|
+
CommandContainer.clear();
|
|
452
|
+
if (ownIsTTY) Object.defineProperty(process.stdin, "isTTY", ownIsTTY);
|
|
453
|
+
else Reflect.deleteProperty(process.stdin, "isTTY");
|
|
454
|
+
});
|
|
455
|
+
const workspaceWith = (appNames: string[]) => {
|
|
456
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: "/workspace", repoName: "repo" });
|
|
457
|
+
workspace.getExecs = async () => [appNames, ["util"], []];
|
|
458
|
+
return workspace;
|
|
459
|
+
};
|
|
460
|
+
const sysMetaOf = (key: "typecheck" | "sync") => {
|
|
461
|
+
class OnlyAppCommand extends command("onlyApp", ({ public: target }) => ({
|
|
462
|
+
typecheck: target({ desc: "opted in" })
|
|
463
|
+
.with(SysOrOnlyApp)
|
|
464
|
+
.exec((sys) => sys),
|
|
465
|
+
sync: target({ desc: "plain" })
|
|
466
|
+
.with(Sys)
|
|
467
|
+
.exec((sys) => sys),
|
|
468
|
+
})) {}
|
|
469
|
+
const meta = getArgMetas(OnlyAppCommand, key)[0].find((arg): arg is InternalArgMeta => arg.type === "Sys");
|
|
470
|
+
if (!meta) throw new Error(`no Sys meta on ${key}`);
|
|
471
|
+
return meta;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
test("the opted-in command takes a one-app workspace's app without asking, even on a piped stdin", async () => {
|
|
475
|
+
setTTY(undefined);
|
|
476
|
+
const meta = sysMetaOf("typecheck");
|
|
477
|
+
expect(meta.option).toEqual({ onlyApp: true });
|
|
478
|
+
|
|
479
|
+
const app = await getInternalArgumentValue(meta, undefined, workspaceWith(["web"]));
|
|
480
|
+
expect(app instanceof AppExecutor ? app.name : null).toBe("web");
|
|
481
|
+
expect(asked).toEqual([]);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("a plain Sys command still asks app or library in a one-app workspace", async () => {
|
|
485
|
+
setTTY(true);
|
|
486
|
+
answer = "util";
|
|
487
|
+
const meta = sysMetaOf("sync");
|
|
488
|
+
expect(meta.option).toBeUndefined();
|
|
489
|
+
|
|
490
|
+
const lib = await getInternalArgumentValue(meta, undefined, workspaceWith(["web"]));
|
|
491
|
+
expect(lib instanceof LibExecutor ? lib.name : null).toBe("util");
|
|
492
|
+
expect(asked).toEqual([["web", "util"]]);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("the opted-in command still asks when the workspace has two apps", async () => {
|
|
496
|
+
setTTY(true);
|
|
497
|
+
answer = "admin";
|
|
498
|
+
const app = await getInternalArgumentValue(sysMetaOf("typecheck"), undefined, workspaceWith(["web", "admin"]));
|
|
499
|
+
expect(app instanceof AppExecutor ? app.name : null).toBe("admin");
|
|
500
|
+
expect(asked).toEqual([["web", "admin", "util"]]);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe("the exit code of a command that failed", () => {
|
|
505
|
+
const track = tempRoots();
|
|
506
|
+
const run = async (failure: string) => {
|
|
507
|
+
const { root } = track(await makeCliTempWorkspace());
|
|
508
|
+
await writeText(path.join(root, "package.json"), '{ "name": "repo", "version": "1.0.0" }\n');
|
|
509
|
+
await writeText(path.join(root, "tsconfig.json"), "{}\n");
|
|
510
|
+
const entry = path.join(root, "cli.ts");
|
|
511
|
+
await writeText(
|
|
512
|
+
entry,
|
|
513
|
+
[
|
|
514
|
+
`import { command, ExitCodeError, runCommands, runner, script } from ${JSON.stringify(path.join(import.meta.dir, "index.ts"))};`,
|
|
515
|
+
'class VerdictRunner extends runner("verdict") {}',
|
|
516
|
+
'class VerdictScript extends script("verdict", [VerdictRunner]) {}',
|
|
517
|
+
'class VerdictCommand extends command("verdict", [VerdictScript], ({ public: target }) => ({',
|
|
518
|
+
' verdict: target({ desc: "fails" }).exec(async function () {',
|
|
519
|
+
` throw ${failure};`,
|
|
520
|
+
" }),",
|
|
521
|
+
"})) {}",
|
|
522
|
+
"await runCommands(VerdictCommand);",
|
|
523
|
+
"",
|
|
524
|
+
].join("\n"),
|
|
525
|
+
);
|
|
526
|
+
const proc = Bun.spawn(["bun", entry, "verdict"], { cwd: root, stdout: "pipe", stderr: "pipe" });
|
|
527
|
+
const [stdout, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
528
|
+
return { stdout, code };
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
test("is the one an ExitCodeError carries, and 1 for any other error", async () => {
|
|
532
|
+
const verdict = await run('new ExitCodeError("needs a new store build", 2)');
|
|
533
|
+
expect(verdict.stdout).toContain("needs a new store build");
|
|
534
|
+
expect(verdict.code).toBe(2);
|
|
535
|
+
expect((await run('new Error("broke")')).code).toBe(1);
|
|
536
|
+
}, 30_000);
|
|
537
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//* A failure that is an answer, not a breakage: the CLI exits with its code, so a CI tells the two apart.
|
|
2
|
+
export class ExitCodeError extends Error {
|
|
3
|
+
override readonly name = "ExitCodeError";
|
|
4
|
+
|
|
5
|
+
constructor(
|
|
6
|
+
message: string,
|
|
7
|
+
readonly exitCode: number,
|
|
8
|
+
) {
|
|
9
|
+
super(message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static codeOf(error: unknown): number | null {
|
|
13
|
+
return error instanceof ExitCodeError ? error.exitCode : null;
|
|
14
|
+
}
|
|
15
|
+
}
|
package/createTunnel.test.ts
CHANGED
|
@@ -48,4 +48,18 @@ describe("createTunnel", () => {
|
|
|
48
48
|
expect(failure.message).toContain(reason.message);
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
test("refuses to dial a cluster host built on the local default serve domain", async () => {
|
|
53
|
+
const { app } = track(await createTempApp("demo"));
|
|
54
|
+
delete process.env.AKAN_PUBLIC_SERVE_DOMAIN;
|
|
55
|
+
const connect = spyOn(Client.prototype, "connect");
|
|
56
|
+
try {
|
|
57
|
+
await expect(createTunnel("redis", { app, environment: "debug" })).rejects.toThrow(
|
|
58
|
+
'The redis tunnel for the "debug" environment needs AKAN_PUBLIC_SERVE_DOMAIN (would be "localhost") set',
|
|
59
|
+
);
|
|
60
|
+
expect(connect).not.toHaveBeenCalled();
|
|
61
|
+
} finally {
|
|
62
|
+
connect.mockRestore();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
51
65
|
});
|
package/createTunnel.ts
CHANGED
|
@@ -10,7 +10,9 @@ export const createTunnel = async (
|
|
|
10
10
|
service: "redis" | "postgres",
|
|
11
11
|
{ app, environment, port = service === "postgres" ? 5432 : 6379 }: TunnelOption,
|
|
12
12
|
) => {
|
|
13
|
-
const
|
|
13
|
+
const baseDevEnv = WorkspaceExecutor.getBaseDevEnv();
|
|
14
|
+
WorkspaceExecutor.assertDeployableEnv(baseDevEnv, environment, `The ${service} tunnel`);
|
|
15
|
+
const { serveDomain, repoName } = baseDevEnv;
|
|
14
16
|
const host = `${app.name}-${environment}.${serveDomain}`;
|
|
15
17
|
const sshPort = process.env.SSH_TUNNEL_PORT ? parseInt(process.env.SSH_TUNNEL_PORT) : 32767;
|
|
16
18
|
const dstHost = `${service}-0.${service}-svc.${app.name}-${environment}.svc.cluster.local`;
|
package/dependencyScanner.ts
CHANGED
|
@@ -107,7 +107,7 @@ export class TypeScriptDependencyScanner {
|
|
|
107
107
|
return {
|
|
108
108
|
npmDeps: [...runtimeDeps].sort(),
|
|
109
109
|
npmDevDeps: [...devDeps].sort(),
|
|
110
|
-
missingDeps: missingDeps.sort(),
|
|
110
|
+
missingDeps: missingDeps.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
|
package/devkitUtils.test.ts
CHANGED
|
@@ -76,7 +76,7 @@ describe("TypeScriptDependencyScanner", () => {
|
|
|
76
76
|
});
|
|
77
77
|
expect(deps.pkgDeps).toEqual(["akanjs/tool"]);
|
|
78
78
|
expect(deps.libDeps).toEqual(["shared"]);
|
|
79
|
-
expect(deps.npmDeps.sort()).toEqual(["lodash", "react"]);
|
|
79
|
+
expect(deps.npmDeps.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))).toEqual(["lodash", "react"]);
|
|
80
80
|
expect(deps.npmDevDeps).toEqual(["typescript"]);
|
|
81
81
|
|
|
82
82
|
const graph = scanner.generateDependencyGraph();
|
package/executors.test.ts
CHANGED
|
@@ -99,6 +99,29 @@ describe("Executor filesystem helpers", () => {
|
|
|
99
99
|
expect(entries.dirs).toContain("nested");
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
test("lists a folder in name order, whatever order the filesystem keeps it in", async () => {
|
|
103
|
+
const root = await makeTempRoot();
|
|
104
|
+
const exec = new Executor("fixture", root);
|
|
105
|
+
for (const name of ["summary", "file", "banner", "_noti", "admin"]) await exec.mkdir(`lib/${name}`);
|
|
106
|
+
for (const name of ["st.ts", "cnst.ts", "dict.ts"]) await exec.writeFile(`lib/${name}`, "");
|
|
107
|
+
|
|
108
|
+
expect(await exec.readdir("lib")).toEqual([
|
|
109
|
+
"_noti",
|
|
110
|
+
"admin",
|
|
111
|
+
"banner",
|
|
112
|
+
"cnst.ts",
|
|
113
|
+
"dict.ts",
|
|
114
|
+
"file",
|
|
115
|
+
"st.ts",
|
|
116
|
+
"summary",
|
|
117
|
+
]);
|
|
118
|
+
expect(await exec.getFilesAndDirs("lib")).toEqual({
|
|
119
|
+
files: ["cnst.ts", "dict.ts", "st.ts"],
|
|
120
|
+
dirs: ["_noti", "admin", "banner", "file", "summary"],
|
|
121
|
+
});
|
|
122
|
+
expect(await exec.getAllFiles("**/*", { cwd: exec.getPath("lib") })).toEqual(["cnst.ts", "dict.ts", "st.ts"]);
|
|
123
|
+
});
|
|
124
|
+
|
|
102
125
|
test("a refreshed tsconfig re-reads the config it extends", async () => {
|
|
103
126
|
const root = await makeTempRoot();
|
|
104
127
|
const appDir = path.join(root, "apps/demo");
|
|
@@ -188,6 +211,35 @@ describe("Executor filesystem helpers", () => {
|
|
|
188
211
|
expect(formatted).toEqual([path.join(root, "apps/demo/page/task/_index.tsx")]);
|
|
189
212
|
});
|
|
190
213
|
|
|
214
|
+
test("hands the formatter only the files a template wrote, never one overwrite: false kept", async () => {
|
|
215
|
+
const { root, app } = trackRoot(await createTempApp("demo"));
|
|
216
|
+
const formatted: string[] = [];
|
|
217
|
+
app.getLinter = () =>
|
|
218
|
+
({
|
|
219
|
+
fixFiles: async (filePaths: string[]) => {
|
|
220
|
+
formatted.push(...filePaths);
|
|
221
|
+
return { fixed: [] };
|
|
222
|
+
},
|
|
223
|
+
}) as unknown as ReturnType<Executor["getLinter"]>;
|
|
224
|
+
const scalarDir = path.join(root, "apps/demo/lib/__scalar/money");
|
|
225
|
+
const own = "export class Money {}\n";
|
|
226
|
+
await writeText(path.join(scalarDir, "money.constant.ts"), own);
|
|
227
|
+
|
|
228
|
+
const applied = await app.applyTemplate({
|
|
229
|
+
basePath: "./lib/__scalar",
|
|
230
|
+
template: "__scalar",
|
|
231
|
+
dict: { model: "money", models: "moneys", sysName: "demo" },
|
|
232
|
+
overwrite: false,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(applied.map((fileContent) => fileContent.filePath)).toContain(path.join(scalarDir, "money.constant.ts"));
|
|
236
|
+
expect(formatted.sort((a, b) => a.localeCompare(b))).toEqual([
|
|
237
|
+
path.join(scalarDir, "money.dictionary.ts"),
|
|
238
|
+
path.join(scalarDir, "money.document.ts"),
|
|
239
|
+
]);
|
|
240
|
+
expect(await readFile(path.join(scalarDir, "money.constant.ts"), "utf8")).toBe(own);
|
|
241
|
+
});
|
|
242
|
+
|
|
191
243
|
test("a formatter that cannot run does not fail the scaffold", async () => {
|
|
192
244
|
// create-akan-workspace scaffolds before `bun install`, so Biome may be absent; a lint fix beats a failed scaffold.
|
|
193
245
|
const root = await makeTempRoot();
|
|
@@ -263,10 +315,27 @@ describe("Executor filesystem helpers", () => {
|
|
|
263
315
|
await readFile(path.join(templateRoot, "favicon.ico")),
|
|
264
316
|
);
|
|
265
317
|
});
|
|
318
|
+
|
|
319
|
+
test("keeps a static file already there when overwrite is false, and reports it unwritten", async () => {
|
|
320
|
+
const root = await makeTempRoot();
|
|
321
|
+
const exec = new Executor("fixture", root);
|
|
322
|
+
await writeText(path.join(root, "app/public/logo.png"), "the app's own logo");
|
|
323
|
+
|
|
324
|
+
const applied = await exec.applyTemplate({
|
|
325
|
+
basePath: "app/public",
|
|
326
|
+
template: "app/public/logo.png",
|
|
327
|
+
overwrite: false,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(await readFile(path.join(root, "app/public/logo.png"), "utf8")).toBe("the app's own logo");
|
|
331
|
+
expect(applied).toEqual([
|
|
332
|
+
expect.objectContaining({ filePath: path.join(root, "app/public/logo.png"), written: false }),
|
|
333
|
+
]);
|
|
334
|
+
});
|
|
266
335
|
});
|
|
267
336
|
|
|
268
337
|
describe("Workspace and app executor environment contracts", () => {
|
|
269
|
-
test("reads base development environment
|
|
338
|
+
test("reads base development environment", () => {
|
|
270
339
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
271
340
|
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
272
341
|
process.env.AKAN_PUBLIC_ENV = "local";
|
|
@@ -282,10 +351,53 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
282
351
|
env: "local",
|
|
283
352
|
portOffset: 10,
|
|
284
353
|
workspaceId: undefined,
|
|
354
|
+
defaulted: [],
|
|
285
355
|
});
|
|
356
|
+
});
|
|
286
357
|
|
|
287
|
-
|
|
288
|
-
|
|
358
|
+
test("unset, the repo name is the origin remote's and the serve domain localhost, both marked defaulted", async () => {
|
|
359
|
+
const root = await makeTempRoot();
|
|
360
|
+
for (const args of [
|
|
361
|
+
["init", "-q"],
|
|
362
|
+
["remote", "add", "origin", "git@github.com:akan-team/fixture-repo.git"],
|
|
363
|
+
])
|
|
364
|
+
expect(Bun.spawnSync(["git", ...args], { cwd: root }).exitCode).toBe(0);
|
|
365
|
+
for (const key of ["AKAN_PUBLIC_REPO_NAME", "AKAN_PUBLIC_SERVE_DOMAIN", "AKAN_PUBLIC_ENV", "AKAN_WORKSPACE_ROOT"])
|
|
366
|
+
delete process.env[key];
|
|
367
|
+
|
|
368
|
+
expect(WorkspaceExecutor.getBaseDevEnv(path.join(root, ".env"))).toMatchObject({
|
|
369
|
+
repoName: "fixture-repo",
|
|
370
|
+
serveDomain: "localhost",
|
|
371
|
+
env: "debug",
|
|
372
|
+
defaulted: ["AKAN_PUBLIC_REPO_NAME", "AKAN_PUBLIC_SERVE_DOMAIN"],
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
376
|
+
expect(WorkspaceExecutor.getBaseDevEnv(path.join(root, ".env"))).toMatchObject({
|
|
377
|
+
repoName: "fixture-repo",
|
|
378
|
+
serveDomain: "example.com",
|
|
379
|
+
defaulted: ["AKAN_PUBLIC_REPO_NAME"],
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("a build or tunnel for a real environment refuses a defaulted value, and a local one takes it", () => {
|
|
384
|
+
const baseDevEnv = {
|
|
385
|
+
repoName: "fixture-repo",
|
|
386
|
+
serveDomain: "localhost",
|
|
387
|
+
env: "debug" as const,
|
|
388
|
+
portOffset: 0,
|
|
389
|
+
workspaceRoot: undefined,
|
|
390
|
+
};
|
|
391
|
+
const defaulted = { ...baseDevEnv, defaulted: ["AKAN_PUBLIC_SERVE_DOMAIN" as const] };
|
|
392
|
+
|
|
393
|
+
expect(() => WorkspaceExecutor.assertDeployableEnv(defaulted, "main", "akan build")).toThrow(
|
|
394
|
+
'akan build for the "main" environment needs AKAN_PUBLIC_SERVE_DOMAIN (would be "localhost") set',
|
|
395
|
+
);
|
|
396
|
+
expect(() => WorkspaceExecutor.assertDeployableEnv(defaulted, "local", "akan build")).not.toThrow();
|
|
397
|
+
expect(() =>
|
|
398
|
+
WorkspaceExecutor.assertDeployableEnv({ ...baseDevEnv, defaulted: [] }, "main", "akan build"),
|
|
399
|
+
).not.toThrow();
|
|
400
|
+
expect(() => WorkspaceExecutor.assertDeployableEnv(baseDevEnv, "main", "akan build")).not.toThrow();
|
|
289
401
|
});
|
|
290
402
|
|
|
291
403
|
test("reads base development environment from an explicit env file", async () => {
|
|
@@ -313,6 +425,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
313
425
|
env: "develop",
|
|
314
426
|
portOffset: 7,
|
|
315
427
|
workspaceId: undefined,
|
|
428
|
+
defaulted: [],
|
|
316
429
|
});
|
|
317
430
|
});
|
|
318
431
|
|
|
@@ -371,6 +484,21 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
371
484
|
expect((await stat(path.join(root, "dist/apps/demo/public"))).isDirectory()).toBe(true);
|
|
372
485
|
});
|
|
373
486
|
|
|
487
|
+
test("the app command env hands the dev server the local defaults that stood in for unset values", async () => {
|
|
488
|
+
const root = await makeTempRoot();
|
|
489
|
+
delete process.env.AKAN_PUBLIC_REPO_NAME;
|
|
490
|
+
delete process.env.AKAN_PUBLIC_SERVE_DOMAIN;
|
|
491
|
+
process.env.AKAN_WORKSPACE_ROOT = root;
|
|
492
|
+
const app = AppExecutor.from(new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" }), "defaults");
|
|
493
|
+
|
|
494
|
+
const env = app.getCommandEnv();
|
|
495
|
+
expect(env.AKAN_PUBLIC_SERVE_DOMAIN).toBe("localhost");
|
|
496
|
+
expect(env.AKAN_PUBLIC_REPO_NAME).toBe(WorkspaceExecutor.getBaseDevEnv().repoName);
|
|
497
|
+
|
|
498
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
499
|
+
expect(app.getCommandEnv().AKAN_PUBLIC_SERVE_DOMAIN).toBe("example.com");
|
|
500
|
+
});
|
|
501
|
+
|
|
374
502
|
test("akan start clears the dev output and keeps the bin downloads and a native folder from before dist/native", async () => {
|
|
375
503
|
const root = await makeTempRoot();
|
|
376
504
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|