@akanjs/devkit 3.1.2-rc.4 → 3.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +353 -0
- package/abstractDoc.ts +1 -1
- package/agentAssets.ts +2 -2
- package/akanConfig/akanConfig.ts +2 -2
- package/akanMcpContract.ts +1 -1
- package/apiReference.test.ts +37 -0
- package/apiReference.ts +240 -0
- package/applicationBuildReporter.test.ts +25 -3
- package/applicationBuildReporter.ts +8 -1
- package/applicationBuildRunner.test.ts +34 -14
- package/applicationBuildRunner.ts +76 -14
- package/artifact/implicitRootLayout.test.ts +42 -0
- package/artifact/implicitRootLayout.ts +5 -6
- package/artifact/storeBootstrap.ts +24 -0
- package/cloud/cloudApi.test.ts +70 -7
- package/cloud/cloudApi.ts +10 -4
- package/cloud/constants.ts +3 -0
- package/cloud/globalConfig.test.ts +36 -0
- package/cloud/globalConfig.ts +10 -1
- package/codeAgent/skills/akan-debug/SKILL.md +2 -1
- package/codeAgent/skills/akan-page/SKILL.md +16 -7
- package/codeAgent/skills/akan-service/SKILL.md +48 -19
- package/commandDecorators/command.ts +55 -9
- package/commandDecorators/commandDecorators.test.ts +131 -2
- package/createTunnel.test.ts +12 -2
- package/declarationReader.ts +211 -0
- package/executors.test.ts +65 -8
- package/executors.ts +5 -3
- package/frameworkGraph.test.ts +121 -0
- package/frameworkGraph.ts +165 -0
- package/frontendBuild/allRoutesBuilder.test.ts +84 -0
- package/frontendBuild/allRoutesBuilder.ts +32 -12
- package/frontendBuild/buildRouteClient.test.ts +192 -2
- package/frontendBuild/bundleDefine.ts +4 -0
- package/frontendBuild/clientBuildTypes.ts +7 -1
- package/frontendBuild/clientEntriesBundler.test.ts +82 -0
- package/frontendBuild/clientEntriesBundler.ts +55 -4
- package/frontendBuild/csrArtifactBuilder.test.ts +23 -0
- package/frontendBuild/csrArtifactBuilder.ts +2 -1
- package/frontendBuild/fontOptimizer.test.ts +54 -0
- package/frontendBuild/fontOptimizer.ts +5 -4
- package/frontendBuild/index.ts +3 -0
- package/frontendBuild/routeClientBuilder.ts +6 -2
- package/frontendBuild/routeModulePreloads.test.ts +59 -0
- package/frontendBuild/routeModulePreloads.ts +52 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +35 -10
- package/frontendBuild/ssrDevBundler.test.ts +26 -0
- package/frontendBuild/ssrDevBundler.ts +2 -1
- package/frontendBuild/vendorImportScanner.test.ts +71 -0
- package/frontendBuild/vendorImportScanner.ts +80 -0
- package/frontendBuild/vendorNarrowing.test.ts +103 -0
- package/frontendBuild/vendorNarrowing.ts +64 -0
- package/frontendBuild/vendorSpecifiers.ts +13 -0
- package/gitRevision.ts +6 -2
- package/incrementalBuilder/builderGraphPayloads.ts +60 -0
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +65 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +5 -36
- package/integration/devStability.integration.test.ts +1 -1
- package/integration/devStabilityHarness.test.ts +1 -1
- package/lint/no-deep-internal-import.grit +1 -1
- package/maintainabilityBaseline.ts +7 -2
- package/maintainabilityContract.test.ts +183 -0
- package/maintainabilityContract.ts +145 -30
- package/maintainabilityExceptionScanner.test.ts +13 -2
- package/maintainabilityExceptionScanner.ts +16 -6
- package/maintainabilityLimits.ts +12 -0
- package/maintainabilityReport.test.ts +35 -0
- package/maintainabilityReport.ts +29 -8
- package/mobile/nativePublicEnv.test.ts +5 -1
- package/package.json +3 -3
- package/preview/PreviewChrome.ts +3 -3
- package/preview/PreviewView.ts +26 -8
- package/preview/previewView.test.ts +87 -3
- package/routeJsBudget.test.ts +177 -0
- package/routeJsBudget.ts +222 -0
- package/structureScanner.test.ts +167 -0
- package/structureScanner.ts +164 -0
- package/transforms/barrelAnalyzer.ts +10 -3
- package/transforms/barrelImportsPlugin.ts +13 -7
- package/transforms/serverEnvPlugin.test.ts +1 -1
- package/transforms/serverEnvPlugin.ts +1 -1
- package/transforms/transforms.test.ts +64 -12
- package/transforms/tsconfigPackageResolver.test.ts +12 -0
- package/workflow/plan.ts +79 -17
- package/workflow/types.ts +1 -0
- package/workspaceGraph.test.ts +58 -0
- package/workspaceGraph.ts +32 -3
- package/workspaceLayout.ts +1 -0
- /package/integration/{devStabilityHarness.ts → devStabilityHarness.fixture.ts} +0 -0
|
@@ -229,6 +229,28 @@ const assertCurrentDirectoryIsWorkspaceRoot = async () => {
|
|
|
229
229
|
);
|
|
230
230
|
};
|
|
231
231
|
|
|
232
|
+
export type RerunOf = (target: string) => string;
|
|
233
|
+
|
|
234
|
+
const shellWordOf = (word: string) => (/^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`);
|
|
235
|
+
|
|
236
|
+
//? Commander fills operands left to right, so every operand before an empty slot was given.
|
|
237
|
+
export const rerunCommandOf =
|
|
238
|
+
(argv: readonly string[], operands: readonly unknown[], slot: number): RerunOf =>
|
|
239
|
+
(target) => {
|
|
240
|
+
const [commandName = "", ...words] = argv;
|
|
241
|
+
let at = 0;
|
|
242
|
+
for (const operand of operands.slice(0, slot)) if (typeof operand === "string") at = words.indexOf(operand, at) + 1;
|
|
243
|
+
const given = operands[slot];
|
|
244
|
+
const givenAt = typeof given === "string" ? words.indexOf(given, at) : -1;
|
|
245
|
+
const rerun = words.map(shellWordOf);
|
|
246
|
+
if (givenAt >= 0) rerun.splice(givenAt, 1, target);
|
|
247
|
+
else rerun.splice(at, 0, target);
|
|
248
|
+
return ["akan", commandName, ...rerun].join(" ");
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const rerunRemedyOf = (rerun: RerunOf | undefined, placeholder: string) =>
|
|
252
|
+
rerun ? { remedy: `rerun it as \`${rerun(placeholder)}\`` } : {};
|
|
253
|
+
|
|
232
254
|
const parseAppNameList = (value: string | string[] | undefined): string[] =>
|
|
233
255
|
(Array.isArray(value) ? value : value === undefined ? [] : [value])
|
|
234
256
|
.flatMap((entry) => entry.split(","))
|
|
@@ -239,6 +261,7 @@ const parseAppNameList = (value: string | string[] | undefined): string[] =>
|
|
|
239
261
|
export const getAppsArgumentValue = async (
|
|
240
262
|
value: string | string[] | undefined,
|
|
241
263
|
workspace: WorkspaceExecutor,
|
|
264
|
+
rerun?: RerunOf,
|
|
242
265
|
): Promise<AppExecutor[]> => {
|
|
243
266
|
const appNames = await workspace.getApps();
|
|
244
267
|
if (appNames.length === 0) throw new Error("No apps found in this workspace (apps/<appName>/akan.config.ts)");
|
|
@@ -253,7 +276,7 @@ export const getAppsArgumentValue = async (
|
|
|
253
276
|
return [...new Set(requested)].map((name) => AppExecutor.from(workspace, name));
|
|
254
277
|
}
|
|
255
278
|
if (appNames.length === 1 && appNames[0]) return [AppExecutor.from(workspace, appNames[0])];
|
|
256
|
-
assertPromptable("Missing apps", { choices: [...appNames, "all"] });
|
|
279
|
+
assertPromptable("Missing apps", { choices: [...appNames, "all"], ...rerunRemedyOf(rerun, "<apps...>") });
|
|
257
280
|
const remembered = new Set(await AppSelectionMemory.read(workspace.workspaceRoot));
|
|
258
281
|
const picked = await checkbox<string>({
|
|
259
282
|
message: "Select the apps to run (space to toggle, enter to confirm)",
|
|
@@ -269,6 +292,7 @@ export const getInternalArgumentValue = async (
|
|
|
269
292
|
argMeta: InternalArgMeta,
|
|
270
293
|
value: string | undefined,
|
|
271
294
|
workspace: WorkspaceExecutor,
|
|
295
|
+
rerun?: RerunOf,
|
|
272
296
|
) => {
|
|
273
297
|
if (argMeta.type === "Workspace") return workspace;
|
|
274
298
|
const sysType = argMeta.type.toLowerCase();
|
|
@@ -286,12 +310,24 @@ export const getInternalArgumentValue = async (
|
|
|
286
310
|
if (found) return found;
|
|
287
311
|
if (!value && argMeta.option?.onlyApp && appNames.length === 1 && appNames[0])
|
|
288
312
|
return AppExecutor.from(workspace, appNames[0]);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
313
|
+
const [kind, placeholder] =
|
|
314
|
+
sysType === "exec" ? ["app, lib or pkg", "<app-lib-or-pkg>"] : ["app or lib", "<app-or-lib>"];
|
|
315
|
+
const choices = [...appNames, ...libNames, ...pkgChoices];
|
|
316
|
+
const only = choices.length === 1 ? execOf(choices[0] ?? "") : null;
|
|
317
|
+
//? Not for Lib, Pkg or Module, which name what a command acts on: `akan remove-library` would delete the only one.
|
|
318
|
+
if (!value && only && !process.stdin.isTTY) {
|
|
319
|
+
const how = rerun ? `: ${rerun(only.name)}` : "";
|
|
320
|
+
Logger.rawLog(
|
|
321
|
+
`No ${kind} named, so this runs on '${only.name}', the only one in this workspace${how}`,
|
|
322
|
+
undefined,
|
|
323
|
+
"error",
|
|
324
|
+
);
|
|
325
|
+
return only;
|
|
326
|
+
}
|
|
327
|
+
assertPromptable(problemOf(kind), { choices, ...rerunRemedyOf(rerun, placeholder) });
|
|
292
328
|
const name = await select<string>({
|
|
293
329
|
message: sysType === "exec" ? `Select the App or Lib or Pkg name` : `Select the App or Lib name`,
|
|
294
|
-
choices
|
|
330
|
+
choices,
|
|
295
331
|
});
|
|
296
332
|
const picked = execOf(name);
|
|
297
333
|
if (!picked) throw new Error(`Invalid system name: ${name}`);
|
|
@@ -299,18 +335,18 @@ export const getInternalArgumentValue = async (
|
|
|
299
335
|
} else if (sysType === "app") {
|
|
300
336
|
if (value && appNames.includes(value)) return AppExecutor.from(workspace, value);
|
|
301
337
|
if (!value && appNames.length === 1 && appNames[0]) return AppExecutor.from(workspace, appNames[0]);
|
|
302
|
-
assertPromptable(problemOf("app"), { choices: appNames });
|
|
338
|
+
assertPromptable(problemOf("app"), { choices: appNames, ...rerunRemedyOf(rerun, "<app>") });
|
|
303
339
|
const appName = await select<string>({ message: `Select the ${sysType} name`, choices: appNames });
|
|
304
340
|
return AppExecutor.from(workspace, appName);
|
|
305
341
|
} else if (sysType === "lib") {
|
|
306
342
|
if (value && libNames.includes(value)) return LibExecutor.from(workspace, value);
|
|
307
|
-
assertPromptable(problemOf("lib"), { choices: libNames });
|
|
343
|
+
assertPromptable(problemOf("lib"), { choices: libNames, ...rerunRemedyOf(rerun, "<lib>") });
|
|
308
344
|
const libName = await select<string>({ message: `Select the ${sysType} name`, choices: libNames });
|
|
309
345
|
return LibExecutor.from(workspace, libName);
|
|
310
346
|
} else if (sysType === "pkg") {
|
|
311
347
|
const pkgs = await workspace.getPkgs();
|
|
312
348
|
if (value && pkgs.includes(value)) return PkgExecutor.from(workspace, value);
|
|
313
|
-
assertPromptable(problemOf("pkg"), { choices: pkgs });
|
|
349
|
+
assertPromptable(problemOf("pkg"), { choices: pkgs, ...rerunRemedyOf(rerun, "<pkg>") });
|
|
314
350
|
const pkgName = await select<string>({ message: `Select the ${sysType} name`, choices: pkgs });
|
|
315
351
|
return PkgExecutor.from(workspace, pkgName);
|
|
316
352
|
} else if (sysType === "module") {
|
|
@@ -328,7 +364,14 @@ export const getInternalArgumentValue = async (
|
|
|
328
364
|
if (modules.includes(moduleName)) return ModuleExecutor.from(lib, moduleName);
|
|
329
365
|
} else throw new Error(`Invalid system name: ${sysName}`);
|
|
330
366
|
}
|
|
331
|
-
|
|
367
|
+
const systems = [
|
|
368
|
+
...appNames.map((name) => AppExecutor.from(workspace, name)),
|
|
369
|
+
...libNames.map((name) => LibExecutor.from(workspace, name)),
|
|
370
|
+
];
|
|
371
|
+
const choices = process.stdin.isTTY
|
|
372
|
+
? []
|
|
373
|
+
: (await Promise.all(systems.map(async (sys) => (await sys.getModules()).map((m) => `${sys.name}:${m}`)))).flat();
|
|
374
|
+
assertPromptable(problemOf("module"), { choices, ...rerunRemedyOf(rerun, "<sys:module>") });
|
|
332
375
|
const { type, name } = await select<{ type: "app" | "lib"; name: string }>({
|
|
333
376
|
message: `select the App or Lib name`,
|
|
334
377
|
choices: [
|
|
@@ -443,6 +486,7 @@ export const runCommands = async (...commands: CommandCls[]) => {
|
|
|
443
486
|
const workspace = WorkspaceExecutor.fromRoot();
|
|
444
487
|
const commandContext: CommandContext = { values: {} };
|
|
445
488
|
for (const argMeta of allArgMetas) {
|
|
489
|
+
const rerun = rerunCommandOf(process.argv.slice(2), cmdArgs, argMeta.idx);
|
|
446
490
|
if (argMeta.type === "Option")
|
|
447
491
|
commandArgs[argMeta.idx] = await getOptionValue(argMeta, opt, commandContext);
|
|
448
492
|
else if (argMeta.type === "Argument")
|
|
@@ -451,12 +495,14 @@ export const runCommands = async (...commands: CommandCls[]) => {
|
|
|
451
495
|
commandArgs[argMeta.idx] = await getAppsArgumentValue(
|
|
452
496
|
cmdArgs[argMeta.idx] as string | string[] | undefined,
|
|
453
497
|
workspace,
|
|
498
|
+
rerun,
|
|
454
499
|
);
|
|
455
500
|
else
|
|
456
501
|
commandArgs[argMeta.idx] = await getInternalArgumentValue(
|
|
457
502
|
argMeta as InternalArgMeta,
|
|
458
503
|
cmdArgs[argMeta.idx] as string,
|
|
459
504
|
workspace,
|
|
505
|
+
rerun,
|
|
460
506
|
);
|
|
461
507
|
if (commandArgs[argMeta.idx] instanceof AppExecutor)
|
|
462
508
|
process.env.AKAN_PUBLIC_APP_NAME = (commandArgs[argMeta.idx] as AppExecutor).name;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { AppExecutor, LibExecutor, WorkspaceExecutor } from "../executors";
|
|
3
|
+
import { AppExecutor, LibExecutor, PkgExecutor, WorkspaceExecutor } from "../executors";
|
|
4
4
|
import { makeCliTempWorkspace, tempRoots, writeText } from "../testHelpers";
|
|
5
5
|
import {
|
|
6
6
|
App,
|
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
SysOrOnlyApp,
|
|
16
16
|
Workspace,
|
|
17
17
|
} from "./argMeta";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
getAppsArgumentValue,
|
|
20
|
+
getArgumentValue,
|
|
21
|
+
getInternalArgumentValue,
|
|
22
|
+
getOptionValue,
|
|
23
|
+
rerunCommandOf,
|
|
24
|
+
} from "./command";
|
|
19
25
|
import { command } from "./commandBuilder";
|
|
20
26
|
import {
|
|
21
27
|
assertUniqueDependencies,
|
|
@@ -499,6 +505,129 @@ describe("a Sys that defaults to the only app", () => {
|
|
|
499
505
|
expect(app instanceof AppExecutor ? app.name : null).toBe("admin");
|
|
500
506
|
expect(asked).toEqual([["web", "admin", "util"]]);
|
|
501
507
|
});
|
|
508
|
+
|
|
509
|
+
test("a terminal is still asked when the workspace holds a single app or lib", async () => {
|
|
510
|
+
setTTY(true);
|
|
511
|
+
answer = "web";
|
|
512
|
+
const workspace = workspaceWith([]);
|
|
513
|
+
workspace.getExecs = async () => [["web"], [], []];
|
|
514
|
+
const app = await getInternalArgumentValue(sysMetaOf("sync"), undefined, workspace);
|
|
515
|
+
expect(app instanceof AppExecutor ? app.name : null).toBe("web");
|
|
516
|
+
expect(asked).toEqual([["web"]]);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("without a terminal, an Exec takes the workspace's only app, lib or pkg", async () => {
|
|
520
|
+
setTTY(undefined);
|
|
521
|
+
const workspace = workspaceWith([]);
|
|
522
|
+
workspace.getExecs = async () => [[], [], ["tools"]];
|
|
523
|
+
const pkg = await getInternalArgumentValue({ key: "", idx: 0, type: "Exec" }, undefined, workspace);
|
|
524
|
+
expect(pkg instanceof PkgExecutor ? pkg.name : null).toBe("tools");
|
|
525
|
+
expect(asked).toEqual([]);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test("without a terminal, the only lib, pkg or module is not taken, as a command may be removing it", async () => {
|
|
529
|
+
setTTY(undefined);
|
|
530
|
+
const workspace = workspaceWith([]);
|
|
531
|
+
workspace.getExecs = async () => [[], ["util"], []];
|
|
532
|
+
workspace.getPkgs = async () => ["tools"];
|
|
533
|
+
const rerunOf = (name: string) => rerunCommandOf([name], [undefined], 0);
|
|
534
|
+
await expect(
|
|
535
|
+
getInternalArgumentValue({ key: "", idx: 0, type: "Lib" }, undefined, workspace, rerunOf("remove-library")),
|
|
536
|
+
).rejects.toThrow("so rerun it as `akan remove-library <lib>` (choices: util).");
|
|
537
|
+
await expect(
|
|
538
|
+
getInternalArgumentValue({ key: "", idx: 0, type: "Pkg" }, undefined, workspace, rerunOf("remove-package")),
|
|
539
|
+
).rejects.toThrow("so rerun it as `akan remove-package <pkg>` (choices: tools).");
|
|
540
|
+
expect(asked).toEqual([]);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("without a terminal, a named target the workspace lacks is refused, not swapped for the only one", async () => {
|
|
544
|
+
setTTY(undefined);
|
|
545
|
+
const workspace = workspaceWith([]);
|
|
546
|
+
workspace.getExecs = async () => [["web"], [], []];
|
|
547
|
+
await expect(
|
|
548
|
+
getInternalArgumentValue(sysMetaOf("sync"), "webb", workspace, rerunCommandOf(["sync", "webb"], ["webb"], 0)),
|
|
549
|
+
).rejects.toThrow(
|
|
550
|
+
"Unknown app or lib 'webb': stdin is not a terminal to prompt on, so rerun it as `akan sync <app-or-lib>` (choices: web).",
|
|
551
|
+
);
|
|
552
|
+
expect(asked).toEqual([]);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
describe("the line a refused target is rerun with", () => {
|
|
557
|
+
test("puts the target in its operand slot, after the operands before it, keeping every option", () => {
|
|
558
|
+
expect(rerunCommandOf(["test", "--write", "false"], [undefined, undefined], 0)("<t>")).toBe(
|
|
559
|
+
"akan test <t> --write false",
|
|
560
|
+
);
|
|
561
|
+
expect(rerunCommandOf(["create-module", "--page", "true", "photo"], ["photo", undefined], 1)("web")).toBe(
|
|
562
|
+
"akan create-module --page true photo web",
|
|
563
|
+
);
|
|
564
|
+
expect(rerunCommandOf(["build", "--env", "it's main"], [undefined], 0)("web")).toBe(
|
|
565
|
+
"akan build web --env 'it'\\''s main'",
|
|
566
|
+
);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test("takes the place of a target that was given but is not in the workspace", () => {
|
|
570
|
+
expect(rerunCommandOf(["sync", "nope"], ["nope"], 0)("<app-or-lib>")).toBe("akan sync <app-or-lib>");
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
describe("a target left out where no terminal can ask for it", () => {
|
|
575
|
+
const track = tempRoots();
|
|
576
|
+
const run = async (apps: string[]) => {
|
|
577
|
+
const { root } = track(await makeCliTempWorkspace());
|
|
578
|
+
await writeText(path.join(root, "package.json"), '{ "name": "repo", "version": "1.0.0" }\n');
|
|
579
|
+
await writeText(path.join(root, "tsconfig.json"), "{}\n");
|
|
580
|
+
for (const app of apps) await writeText(path.join(root, "apps", app, "akan.config.ts"), "export default {};\n");
|
|
581
|
+
const entry = path.join(root, "cli.ts");
|
|
582
|
+
await writeText(
|
|
583
|
+
entry,
|
|
584
|
+
[
|
|
585
|
+
`import { command, runCommands, Sys } from ${JSON.stringify(path.join(import.meta.dir, "index.ts"))};`,
|
|
586
|
+
'class ThingCommand extends command("thing", ({ public: target }) => ({',
|
|
587
|
+
' createThing: target({ desc: "creates a thing" })',
|
|
588
|
+
' .arg("thing", String, { desc: "name of the thing" })',
|
|
589
|
+
" .with(Sys)",
|
|
590
|
+
' .option("force", Boolean, { default: false })',
|
|
591
|
+
" .exec(async function (thing, sys, force) {",
|
|
592
|
+
' process.stdout.write(thing + " in " + sys.name + ", force " + force + "\\n");',
|
|
593
|
+
" }),",
|
|
594
|
+
"})) {}",
|
|
595
|
+
"await runCommands(ThingCommand);",
|
|
596
|
+
"",
|
|
597
|
+
].join("\n"),
|
|
598
|
+
);
|
|
599
|
+
const proc = Bun.spawn(["bun", entry, "create-thing", "photo", "--force"], {
|
|
600
|
+
cwd: root,
|
|
601
|
+
stdin: "ignore",
|
|
602
|
+
stdout: "pipe",
|
|
603
|
+
stderr: "pipe",
|
|
604
|
+
});
|
|
605
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
606
|
+
new Response(proc.stdout).text(),
|
|
607
|
+
new Response(proc.stderr).text(),
|
|
608
|
+
proc.exited,
|
|
609
|
+
]);
|
|
610
|
+
return { stdout, stderr, code };
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
test("runs on the workspace's only app or lib, naming it on stderr", async () => {
|
|
614
|
+
const ran = await run(["web"]);
|
|
615
|
+
expect(ran.code).toBe(0);
|
|
616
|
+
expect(ran.stdout).toContain("photo in web, force true\n");
|
|
617
|
+
expect(ran.stderr).toContain(
|
|
618
|
+
"No app or lib named, so this runs on 'web', the only one in this workspace: akan create-thing photo web --force",
|
|
619
|
+
);
|
|
620
|
+
}, 30_000);
|
|
621
|
+
|
|
622
|
+
test("exits at once with the choices and the line to rerun when there are several", async () => {
|
|
623
|
+
const refused = await run(["web", "admin"]);
|
|
624
|
+
expect(refused.code).toBe(1);
|
|
625
|
+
expect(refused.stdout).toContain(
|
|
626
|
+
"Missing app or lib: stdin is not a terminal to prompt on, so rerun it as `akan create-thing photo <app-or-lib> --force`",
|
|
627
|
+
);
|
|
628
|
+
expect(refused.stdout).toMatch(/\(choices: (web, admin|admin, web)\)\./);
|
|
629
|
+
expect(refused.stdout).not.toContain("photo in");
|
|
630
|
+
}, 30_000);
|
|
502
631
|
});
|
|
503
632
|
|
|
504
633
|
describe("the exit code of a command that failed", () => {
|
package/createTunnel.test.ts
CHANGED
|
@@ -20,10 +20,20 @@ const publicBlobOf = (publicKey: string) => {
|
|
|
20
20
|
return parsed.getPublicSSH();
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
//? ssh2 1.17's keygen strips a leading zero byte off an ed25519 public key, so 1 pair in 256 cannot be read back.
|
|
24
|
+
const ed25519Pair = () => {
|
|
25
|
+
for (let tries = 1; ; tries++) {
|
|
26
|
+
const pair = utils.generateKeyPairSync("ed25519");
|
|
27
|
+
const parsed = utils.parseKey(pair.private);
|
|
28
|
+
if (!(parsed instanceof Error)) return pair;
|
|
29
|
+
if (tries === 8) throw parsed;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
23
33
|
const sshServers: { server: Server; clients: Set<Connection> }[] = [];
|
|
24
34
|
|
|
25
35
|
const startSshServer = async () => {
|
|
26
|
-
const hostKey =
|
|
36
|
+
const hostKey = ed25519Pair();
|
|
27
37
|
const methods: string[] = [];
|
|
28
38
|
const clients = new Set<Connection>();
|
|
29
39
|
const server = new Server({ hostKeys: [hostKey.private] }, (client) => {
|
|
@@ -136,7 +146,7 @@ describe("createTunnel", () => {
|
|
|
136
146
|
const ssh = await startSshServer();
|
|
137
147
|
const knownHosts = path.join(root, "known_hosts.json");
|
|
138
148
|
const keyPath = path.join(root, "id_tunnel");
|
|
139
|
-
await writeFile(keyPath,
|
|
149
|
+
await writeFile(keyPath, ed25519Pair().private);
|
|
140
150
|
Object.assign(process.env, {
|
|
141
151
|
SSH_TUNNEL_PORT: String(ssh.port),
|
|
142
152
|
SSH_TUNNEL_KNOWN_HOSTS: knownHosts,
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
|
|
4
|
+
export interface MemberFilter {
|
|
5
|
+
only?: string[];
|
|
6
|
+
omit?: string[];
|
|
7
|
+
prefix?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const nodeFlags =
|
|
11
|
+
ts.NodeBuilderFlags.NoTruncation |
|
|
12
|
+
ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope |
|
|
13
|
+
ts.NodeBuilderFlags.OmitParameterModifiers;
|
|
14
|
+
const typeFlags = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope;
|
|
15
|
+
|
|
16
|
+
export class DeclarationReader {
|
|
17
|
+
static readonly #printer = ts.createPrinter({ removeComments: true, omitTrailingSemicolon: true });
|
|
18
|
+
readonly #dir: string;
|
|
19
|
+
readonly #program: ts.Program;
|
|
20
|
+
readonly #checker: ts.TypeChecker;
|
|
21
|
+
readonly #entries: Map<string, string>;
|
|
22
|
+
readonly #scratch: ts.SourceFile;
|
|
23
|
+
|
|
24
|
+
constructor(akanjsDir: string) {
|
|
25
|
+
this.#dir = akanjsDir;
|
|
26
|
+
const configPath = path.join(akanjsDir, "tsconfig.json");
|
|
27
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
28
|
+
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, akanjsDir, undefined, configPath);
|
|
29
|
+
this.#entries = DeclarationReader.#entryFiles(akanjsDir);
|
|
30
|
+
this.#program = ts.createProgram({
|
|
31
|
+
rootNames: [...new Set(this.#entries.values())],
|
|
32
|
+
options: { ...parsed.options, noEmit: true },
|
|
33
|
+
});
|
|
34
|
+
this.#checker = this.#program.getTypeChecker();
|
|
35
|
+
this.#scratch = ts.createSourceFile("reference.ts", "", ts.ScriptTarget.Latest);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static #entryFiles(akanjsDir: string) {
|
|
39
|
+
const manifest = JSON.parse(ts.sys.readFile(path.join(akanjsDir, "package.json")) ?? "{}") as {
|
|
40
|
+
exports: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
const entries = new Map<string, string>();
|
|
43
|
+
for (const [key, value] of Object.entries(manifest.exports)) {
|
|
44
|
+
const target = typeof value === "string" ? value : (value as { types?: string; import?: string }).types;
|
|
45
|
+
if (key.includes("*") || typeof target !== "string" || !/\.tsx?$/.test(target)) continue;
|
|
46
|
+
entries.set(key === "." ? "akanjs" : `akanjs/${key.slice(2)}`, path.join(akanjsDir, target));
|
|
47
|
+
}
|
|
48
|
+
return entries;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
exported(from: string, name: string): ts.Symbol {
|
|
52
|
+
const file = this.#entries.get(from);
|
|
53
|
+
const source = file ? this.#program.getSourceFile(file) : undefined;
|
|
54
|
+
const module = source ? this.#checker.getSymbolAtLocation(source) : undefined;
|
|
55
|
+
const symbol = module ? this.#checker.getExportsOfModule(module).find((each) => each.name === name) : undefined;
|
|
56
|
+
if (!symbol) throw new Error(`${name} is not exported from ${from}: update the API reference manifest`);
|
|
57
|
+
return symbol.flags & ts.SymbolFlags.Alias ? this.#checker.getAliasedSymbol(symbol) : symbol;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
declared(file: string, name: string): ts.Symbol {
|
|
61
|
+
const source = this.#program.getSourceFile(path.join(this.#dir, file));
|
|
62
|
+
const statement = source?.statements.find(
|
|
63
|
+
(each) =>
|
|
64
|
+
(ts.isClassDeclaration(each) || ts.isInterfaceDeclaration(each) || ts.isTypeAliasDeclaration(each)) &&
|
|
65
|
+
each.name?.text === name,
|
|
66
|
+
) as ts.ClassDeclaration | ts.InterfaceDeclaration | ts.TypeAliasDeclaration | undefined;
|
|
67
|
+
const symbol = statement?.name ? this.#checker.getSymbolAtLocation(statement.name) : undefined;
|
|
68
|
+
if (!symbol) throw new Error(`${name} is not declared in ${file}: update the API reference manifest`);
|
|
69
|
+
return symbol;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
declaredType(symbol: ts.Symbol): ts.Type {
|
|
73
|
+
return this.#checker.getDeclaredTypeOfSymbol(symbol);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
instanceType(symbol: ts.Symbol): ts.Type {
|
|
77
|
+
if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface | ts.SymbolFlags.TypeAlias))
|
|
78
|
+
return this.declaredType(symbol);
|
|
79
|
+
const type = this.#checker.getTypeOfSymbol(symbol);
|
|
80
|
+
const [signature] = type.getCallSignatures();
|
|
81
|
+
return signature ? signature.getReturnType() : type;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
declarationText(symbol: ts.Symbol): string {
|
|
85
|
+
const [declaration] = symbol.declarations ?? [];
|
|
86
|
+
if (!declaration) throw new Error(`${symbol.name} has no declaration`);
|
|
87
|
+
return DeclarationReader.#print(declaration, declaration.getSourceFile()).replace(/^export /, "");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
signatures(symbol: ts.Symbol, name: string): string[] {
|
|
91
|
+
const type = this.#checker.getTypeOfSymbol(symbol);
|
|
92
|
+
const signatures = type.getCallSignatures();
|
|
93
|
+
if (signatures.length === 0) throw new Error(`${name} is not callable`);
|
|
94
|
+
return signatures.map((signature) => `${name}${this.#checker.signatureToString(signature, undefined, typeFlags)}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
props(type: ts.Type, omit: string[] = []): string[] {
|
|
98
|
+
const lines: string[] = [];
|
|
99
|
+
const properties = this.#checker.getPropertiesOfType(type);
|
|
100
|
+
DeclarationReader.#assertNamed(properties, omit);
|
|
101
|
+
for (const property of properties) {
|
|
102
|
+
if (omit.includes(property.name) || DeclarationReader.#isInternalName(property.name)) continue;
|
|
103
|
+
const [declaration] = property.declarations ?? [];
|
|
104
|
+
if (declaration && !this.#isOwn(declaration)) continue;
|
|
105
|
+
const optional = property.flags & ts.SymbolFlags.Optional ? "?" : "";
|
|
106
|
+
const propertyType = declaration
|
|
107
|
+
? this.#checker.getTypeOfSymbolAtLocation(property, declaration)
|
|
108
|
+
: this.#checker.getTypeOfSymbol(property);
|
|
109
|
+
const shown = this.#checker.typeToString(
|
|
110
|
+
optional ? this.#checker.getNonNullableType(propertyType) : propertyType,
|
|
111
|
+
undefined,
|
|
112
|
+
typeFlags,
|
|
113
|
+
);
|
|
114
|
+
const doc = this.#firstSentence(property);
|
|
115
|
+
if (doc) lines.push(`/** ${doc} */`);
|
|
116
|
+
lines.push(`${property.name}${optional}: ${shown};`);
|
|
117
|
+
}
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
members(type: ts.Type, { only, omit = [], prefix = "." }: MemberFilter): [string, string][] {
|
|
122
|
+
const properties = this.#checker.getPropertiesOfType(type);
|
|
123
|
+
DeclarationReader.#assertNamed(properties, [...(only ?? []), ...omit]);
|
|
124
|
+
const found: [string, string][] = [];
|
|
125
|
+
for (const property of properties) {
|
|
126
|
+
const name = property.name;
|
|
127
|
+
if ((only && !only.includes(name)) || omit.includes(name) || DeclarationReader.#isInternalName(name)) continue;
|
|
128
|
+
const [declaration] = property.declarations ?? [];
|
|
129
|
+
if (!declaration || DeclarationReader.#isHidden(declaration)) continue;
|
|
130
|
+
const signatures = this.#checker.getTypeOfSymbolAtLocation(property, declaration).getCallSignatures();
|
|
131
|
+
for (const signature of signatures)
|
|
132
|
+
found.push([name, `${prefix}${name}${this.#withoutReturn(signature, declaration)}`]);
|
|
133
|
+
}
|
|
134
|
+
return found;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static #assertNamed(properties: ts.Symbol[], names: string[]) {
|
|
138
|
+
for (const name of names)
|
|
139
|
+
if (!properties.some((property) => property.name === name))
|
|
140
|
+
throw new Error(`${name} is not a member any more: update the API reference manifest`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static #isInternalName(name: string) {
|
|
144
|
+
return name.startsWith("_") || name.startsWith("#");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#isOwn(declaration: ts.Declaration) {
|
|
148
|
+
return !path.relative(this.#dir, declaration.getSourceFile().fileName).startsWith("..");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
static #isHidden(declaration: ts.Declaration) {
|
|
152
|
+
const flags = ts.getCombinedModifierFlags(declaration);
|
|
153
|
+
return (
|
|
154
|
+
(flags & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected | ts.ModifierFlags.Static)) !== 0 ||
|
|
155
|
+
(ts.isPropertyDeclaration(declaration) && !declaration.type && !declaration.initializer) ||
|
|
156
|
+
ts.isConstructorDeclaration(declaration)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#withoutReturn(signature: ts.Signature, enclosing: ts.Declaration) {
|
|
161
|
+
const node = this.#checker.signatureToSignatureDeclaration(
|
|
162
|
+
signature,
|
|
163
|
+
ts.SyntaxKind.MethodSignature,
|
|
164
|
+
enclosing,
|
|
165
|
+
nodeFlags,
|
|
166
|
+
) as ts.MethodSignature | undefined;
|
|
167
|
+
if (!node) throw new Error(`${this.#checker.signatureToString(signature)} cannot be printed`);
|
|
168
|
+
const typeParameters = node.typeParameters?.filter((each) => !each.name.text.startsWith("_"));
|
|
169
|
+
const parameters = node.parameters.map((parameter) =>
|
|
170
|
+
parameter.questionToken && parameter.type && ts.isUnionTypeNode(parameter.type)
|
|
171
|
+
? ts.factory.updateParameterDeclaration(
|
|
172
|
+
parameter,
|
|
173
|
+
parameter.modifiers,
|
|
174
|
+
parameter.dotDotDotToken,
|
|
175
|
+
parameter.name,
|
|
176
|
+
parameter.questionToken,
|
|
177
|
+
DeclarationReader.#withoutUndefined(parameter.type),
|
|
178
|
+
parameter.initializer,
|
|
179
|
+
)
|
|
180
|
+
: parameter,
|
|
181
|
+
);
|
|
182
|
+
const stripped = ts.factory.createMethodSignature(
|
|
183
|
+
undefined,
|
|
184
|
+
"x",
|
|
185
|
+
undefined,
|
|
186
|
+
typeParameters?.length ? typeParameters : undefined,
|
|
187
|
+
parameters,
|
|
188
|
+
undefined,
|
|
189
|
+
);
|
|
190
|
+
return DeclarationReader.#print(stripped, this.#scratch)
|
|
191
|
+
.replace(/^x/, "")
|
|
192
|
+
.replace(/;$/, "")
|
|
193
|
+
.replaceAll(" & Record<never, never>", "");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
static #print(node: ts.Node, source: ts.SourceFile) {
|
|
197
|
+
return DeclarationReader.#printer.printNode(ts.EmitHint.Unspecified, node, source).replace(/\s+/g, " ");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static #withoutUndefined(type: ts.UnionTypeNode): ts.TypeNode {
|
|
201
|
+
const kept = type.types.filter((each) => each.kind !== ts.SyntaxKind.UndefinedKeyword);
|
|
202
|
+
return kept.length === 1 ? kept[0] : ts.factory.createUnionTypeNode(kept);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#firstSentence(symbol: ts.Symbol) {
|
|
206
|
+
const text = ts.displayPartsToString(symbol.getDocumentationComment(this.#checker)).replace(/\s+/g, " ").trim();
|
|
207
|
+
if (!text) return "";
|
|
208
|
+
const end = text.search(/\.(\s|$)/);
|
|
209
|
+
return (end === -1 ? text : text.slice(0, end + 1)).replace(/\*\//g, "*\\/");
|
|
210
|
+
}
|
|
211
|
+
}
|
package/executors.test.ts
CHANGED
|
@@ -304,8 +304,8 @@ describe("Executor filesystem helpers", () => {
|
|
|
304
304
|
|
|
305
305
|
await exec.applyTemplate({ basePath: "app", template: "app", dict: { appName: "demo" }, options: { libs: [] } });
|
|
306
306
|
const templateRoot = path.resolve(import.meta.dir, "../cli/templates/app/public");
|
|
307
|
-
await expect(readFile(path.join(root, "app/public/
|
|
308
|
-
await readFile(path.join(templateRoot, "
|
|
307
|
+
await expect(readFile(path.join(root, "app/public/apple-touch-icon.png"))).resolves.toEqual(
|
|
308
|
+
await readFile(path.join(templateRoot, "apple-touch-icon.png")),
|
|
309
309
|
);
|
|
310
310
|
await expect(readFile(path.join(root, "app/public/favicon.ico"))).resolves.toEqual(
|
|
311
311
|
await readFile(path.join(templateRoot, "favicon.ico")),
|
|
@@ -315,22 +315,32 @@ describe("Executor filesystem helpers", () => {
|
|
|
315
315
|
test("keeps a static file already there when overwrite is false, and reports it unwritten", async () => {
|
|
316
316
|
const root = await makeTempRoot();
|
|
317
317
|
const exec = new Executor("fixture", root);
|
|
318
|
-
await writeText(path.join(root, "app/public/
|
|
318
|
+
await writeText(path.join(root, "app/public/favicon.ico"), "the app's own icon");
|
|
319
319
|
|
|
320
320
|
const applied = await exec.applyTemplate({
|
|
321
321
|
basePath: "app/public",
|
|
322
|
-
template: "app/public/
|
|
322
|
+
template: "app/public/favicon.ico",
|
|
323
323
|
overwrite: false,
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
-
expect(await readFile(path.join(root, "app/public/
|
|
326
|
+
expect(await readFile(path.join(root, "app/public/favicon.ico"), "utf8")).toBe("the app's own icon");
|
|
327
327
|
expect(applied).toEqual([
|
|
328
|
-
expect.objectContaining({ filePath: path.join(root, "app/public/
|
|
328
|
+
expect.objectContaining({ filePath: path.join(root, "app/public/favicon.ico"), written: false }),
|
|
329
329
|
]);
|
|
330
330
|
});
|
|
331
331
|
});
|
|
332
332
|
|
|
333
333
|
describe("Workspace and app executor environment contracts", () => {
|
|
334
|
+
const baseDevEnvKeys = [
|
|
335
|
+
"AKAN_PUBLIC_APP_NAME",
|
|
336
|
+
"AKAN_WORKSPACE_ROOT",
|
|
337
|
+
"AKAN_WORKSPACE_ID",
|
|
338
|
+
"AKAN_PUBLIC_REPO_NAME",
|
|
339
|
+
"AKAN_PUBLIC_SERVE_DOMAIN",
|
|
340
|
+
"AKAN_PUBLIC_ENV",
|
|
341
|
+
"PORT_OFFSET",
|
|
342
|
+
];
|
|
343
|
+
|
|
334
344
|
test("reads base development environment", () => {
|
|
335
345
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
336
346
|
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
@@ -427,8 +437,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
427
437
|
"",
|
|
428
438
|
].join("\n"),
|
|
429
439
|
);
|
|
430
|
-
delete process.env
|
|
431
|
-
delete process.env.AKAN_PUBLIC_SERVE_DOMAIN;
|
|
440
|
+
for (const key of baseDevEnvKeys) delete process.env[key];
|
|
432
441
|
|
|
433
442
|
expect(WorkspaceExecutor.getBaseDevEnv(path.join(root, ".env"))).toEqual({
|
|
434
443
|
appName: "file-app",
|
|
@@ -458,6 +467,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
458
467
|
"",
|
|
459
468
|
].join("\r\n"),
|
|
460
469
|
);
|
|
470
|
+
for (const key of baseDevEnvKeys) delete process.env[key];
|
|
461
471
|
|
|
462
472
|
expect(WorkspaceExecutor.getBaseDevEnv(path.join(root, ".env"))).toEqual({
|
|
463
473
|
appName: 'say \\"hi\\"\nagain',
|
|
@@ -471,6 +481,53 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
471
481
|
});
|
|
472
482
|
});
|
|
473
483
|
|
|
484
|
+
test("the shell outranks the workspace .env key by key, and a default stands in for what neither sets", async () => {
|
|
485
|
+
const root = await makeTempRoot();
|
|
486
|
+
await writeFile(
|
|
487
|
+
path.join(root, ".env"),
|
|
488
|
+
[
|
|
489
|
+
"AKAN_PUBLIC_ENV=local",
|
|
490
|
+
"AKAN_PUBLIC_REPO_NAME=file-repo",
|
|
491
|
+
"AKAN_PUBLIC_SERVE_DOMAIN=file.example.com",
|
|
492
|
+
"",
|
|
493
|
+
].join("\n"),
|
|
494
|
+
);
|
|
495
|
+
for (const key of baseDevEnvKeys) delete process.env[key];
|
|
496
|
+
Object.assign(process.env, { AKAN_PUBLIC_ENV: "main", AKAN_PUBLIC_SERVE_DOMAIN: "shell.example.com" });
|
|
497
|
+
|
|
498
|
+
expect(WorkspaceExecutor.getBaseDevEnv(path.join(root, ".env"))).toMatchObject({
|
|
499
|
+
env: "main",
|
|
500
|
+
repoName: "file-repo",
|
|
501
|
+
serveDomain: "shell.example.com",
|
|
502
|
+
defaulted: [],
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
delete process.env.AKAN_PUBLIC_ENV;
|
|
506
|
+
await writeFile(path.join(root, ".env"), "AKAN_PUBLIC_ENV=local\n");
|
|
507
|
+
expect(WorkspaceExecutor.getBaseDevEnv(path.join(root, ".env"))).toMatchObject({
|
|
508
|
+
env: "local",
|
|
509
|
+
serveDomain: "shell.example.com",
|
|
510
|
+
defaulted: ["AKAN_PUBLIC_REPO_NAME"],
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("an empty export reads as unset, so the .env's value stands", async () => {
|
|
515
|
+
const root = await makeTempRoot();
|
|
516
|
+
await writeFile(
|
|
517
|
+
path.join(root, ".env"),
|
|
518
|
+
"AKAN_PUBLIC_ENV=main\nAKAN_PUBLIC_REPO_NAME=file-repo\nAKAN_PUBLIC_SERVE_DOMAIN=file.example.com\n",
|
|
519
|
+
);
|
|
520
|
+
for (const key of baseDevEnvKeys) delete process.env[key];
|
|
521
|
+
Object.assign(process.env, { AKAN_PUBLIC_ENV: "", AKAN_PUBLIC_REPO_NAME: "", AKAN_PUBLIC_SERVE_DOMAIN: "" });
|
|
522
|
+
|
|
523
|
+
expect(WorkspaceExecutor.getBaseDevEnv(path.join(root, ".env"))).toMatchObject({
|
|
524
|
+
env: "main",
|
|
525
|
+
repoName: "file-repo",
|
|
526
|
+
serveDomain: "file.example.com",
|
|
527
|
+
defaulted: [],
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
474
531
|
test("builds app command environment and prepareCommand artifacts", async () => {
|
|
475
532
|
const root = await makeTempRoot();
|
|
476
533
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|