@akanjs/devkit 3.0.0-alpha.11 → 3.0.0-alpha.110
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 +15 -0
- package/DEV_RUNTIME_KNOBS.md +34 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +21 -1
- package/aiEditor.ts +1 -1
- package/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +113 -578
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +442 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +143 -47
- package/akanConfig/types.ts +9 -0
- package/akanContext.ts +30 -14
- package/akanMcpContract.ts +25 -4
- package/appSelectionMemory.ts +28 -0
- package/applicationBuildRunner.test.ts +37 -2
- package/applicationBuildRunner.ts +52 -23
- package/artifact/implicitRootLayout.test.ts +70 -1
- package/artifact/implicitRootLayout.ts +41 -14
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +28 -1
- package/biomeBase.ts +1 -1
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/codegenLock.test.ts +137 -0
- package/codegenLock.ts +136 -0
- package/commandDecorators/argMeta.ts +9 -1
- package/commandDecorators/command.ts +94 -7
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/commandDecorators/helpFormatter.ts +4 -0
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +135 -7
- package/executors.ts +115 -25
- package/fileEditor.ts +19 -19
- package/fileSys.ts +19 -1
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +7 -2
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +63 -10
- package/frontendBuild/devGeneratedIndexSync.ts +3 -2
- package/frontendBuild/fontOptimizer.test.ts +75 -2
- package/frontendBuild/fontOptimizer.ts +98 -27
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +124 -10
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.ts +3 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/sourceMtimeIndex.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/frontendBuild/watchRootResolver.test.ts +129 -0
- package/frontendBuild/watchRootResolver.ts +66 -1
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +51 -7
- package/incrementalBuilder/incrementalBuilder.proc.ts +17 -6
- package/index.ts +0 -7
- package/integration/devStabilityHarness.ts +13 -19
- package/libSource.test.ts +112 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +82 -13
- package/package.json +5 -8
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +80 -142
- package/qualityScanner.ts +89 -31
- package/recipeScanner.ts +4 -1
- package/routeSourceValidator.test.ts +63 -0
- package/routeSourceValidator.ts +123 -16
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +182 -0
- package/slicePlanner.ts +235 -0
- package/ssrScanner.test.ts +301 -0
- package/subspace.test.ts +418 -0
- package/subspace.ts +723 -0
- package/subspaceConfig.ts +76 -0
- package/transforms/asyncDefaultExportDetector.ts +105 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +6 -2
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +48 -2
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/mcpScanner.ts +0 -217
- package/src/capacitorApp.ts +0 -282
- package/storeScanner.ts +0 -173
- package/streamAi.ts +0 -45
- package/ui/MultiScrollList.tsx +0 -242
- package/ui/ScrollList.tsx +0 -107
- package/ui/index.ts +0 -2
- package/useStdoutDimensions.ts +0 -20
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { TypeScriptDependencyScanner } from "./dependencyScanner";
|
|
6
|
+
import type { PackageJson } from "./types";
|
|
7
|
+
|
|
8
|
+
const tempRoots: string[] = [];
|
|
9
|
+
|
|
10
|
+
const makeTempRoot = async () => {
|
|
11
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-depscan-"));
|
|
12
|
+
tempRoots.push(root);
|
|
13
|
+
return root;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const write = async (root: string, relPath: string, content: string) => {
|
|
17
|
+
const filePath = path.join(root, relPath);
|
|
18
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
19
|
+
await writeFile(filePath, content);
|
|
20
|
+
return filePath;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const scannerFor = async (root: string, dependencies: Record<string, string> = {}) =>
|
|
24
|
+
new TypeScriptDependencyScanner(path.join(root, "pkgs/@akanjs/devkit"), {
|
|
25
|
+
workspaceRoot: root,
|
|
26
|
+
tsconfig: { compilerOptions: { target: "esnext" } },
|
|
27
|
+
rootPackageJson: { dependencies } as unknown as PackageJson,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("TypeScriptDependencyScanner — getPackageBuildDependencies", () => {
|
|
35
|
+
test("collects imports from ordinary source files", async () => {
|
|
36
|
+
const root = await makeTempRoot();
|
|
37
|
+
await write(root, "pkgs/@akanjs/devkit/linter.ts", `import chalk from "chalk";\nexport const x = chalk;\n`);
|
|
38
|
+
const scanner = await scannerFor(root, { chalk: "^5.0.0" });
|
|
39
|
+
|
|
40
|
+
const { npmDeps, missingDeps } = await scanner.getPackageBuildDependencies("@akanjs/devkit");
|
|
41
|
+
|
|
42
|
+
expect(npmDeps).toEqual(["chalk"]);
|
|
43
|
+
expect(missingDeps).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("reports an import that root package.json does not version", async () => {
|
|
47
|
+
const root = await makeTempRoot();
|
|
48
|
+
await write(root, "pkgs/@akanjs/devkit/linter.ts", `import clsx from "clsx";\nexport const x = clsx;\n`);
|
|
49
|
+
const scanner = await scannerFor(root);
|
|
50
|
+
|
|
51
|
+
expect((await scanner.getPackageBuildDependencies("@akanjs/devkit")).missingDeps).toEqual(["clsx"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("skips __fixtures__, whose imports are lint violations rather than dependencies", async () => {
|
|
55
|
+
const root = await makeTempRoot();
|
|
56
|
+
await write(
|
|
57
|
+
root,
|
|
58
|
+
"pkgs/@akanjs/devkit/lint/__fixtures__/no-import-external-library/bad.tsx",
|
|
59
|
+
`import clsx from "clsx";\nimport { cnst } from "@libs/shared/client";\nexport const x = [clsx, cnst];\n`,
|
|
60
|
+
);
|
|
61
|
+
const scanner = await scannerFor(root);
|
|
62
|
+
|
|
63
|
+
const { npmDeps, npmDevDeps, missingDeps } = await scanner.getPackageBuildDependencies("@akanjs/devkit");
|
|
64
|
+
|
|
65
|
+
expect(missingDeps).toEqual([]);
|
|
66
|
+
expect(npmDeps).toEqual([]);
|
|
67
|
+
expect(npmDevDeps).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("skips a nested __fixtures__ directory at any depth", async () => {
|
|
71
|
+
const root = await makeTempRoot();
|
|
72
|
+
await write(root, "pkgs/@akanjs/devkit/__fixtures__/bad.ts", `import a from "pkg-a";\nexport const x = a;\n`);
|
|
73
|
+
await write(root, "pkgs/@akanjs/devkit/a/b/__fixtures__/c/bad.ts", `import b from "pkg-b";\nexport const x = b;\n`);
|
|
74
|
+
const scanner = await scannerFor(root);
|
|
75
|
+
|
|
76
|
+
expect((await scanner.getPackageBuildDependencies("@akanjs/devkit")).missingDeps).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("keeps a directory whose name merely contains the fixtures marker", async () => {
|
|
80
|
+
const root = await makeTempRoot();
|
|
81
|
+
await write(
|
|
82
|
+
root,
|
|
83
|
+
"pkgs/@akanjs/devkit/__fixtures__helper/keep.ts",
|
|
84
|
+
`import a from "pkg-a";\nexport const x = a;\n`,
|
|
85
|
+
);
|
|
86
|
+
const scanner = await scannerFor(root);
|
|
87
|
+
|
|
88
|
+
expect((await scanner.getPackageBuildDependencies("@akanjs/devkit")).missingDeps).toEqual(["pkg-a"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("skips test files but still reports their non-test siblings", async () => {
|
|
92
|
+
const root = await makeTempRoot();
|
|
93
|
+
await write(root, "pkgs/@akanjs/devkit/linter.test.ts", `import a from "pkg-a";\nexport const x = a;\n`);
|
|
94
|
+
await write(root, "pkgs/@akanjs/devkit/linter.ts", `import b from "pkg-b";\nexport const x = b;\n`);
|
|
95
|
+
const scanner = await scannerFor(root);
|
|
96
|
+
|
|
97
|
+
expect((await scanner.getPackageBuildDependencies("@akanjs/devkit")).missingDeps).toEqual(["pkg-b"]);
|
|
98
|
+
});
|
|
99
|
+
});
|
package/dependencyScanner.ts
CHANGED
|
@@ -7,6 +7,11 @@ import { FileSys } from "./fileSys";
|
|
|
7
7
|
import type { PackageJson, TsConfigJson } from "./types";
|
|
8
8
|
|
|
9
9
|
const testFileRegex = /\.(?:test|spec)\.[cm]?[tj]sx?$/;
|
|
10
|
+
// `__fixtures__` holds deliberately invalid sample sources for the lint-rule suites; tsconfig `exclude` and
|
|
11
|
+
// biome's `!` override already skip them, and their imports are violations rather than real dependencies.
|
|
12
|
+
const skipDirs = ["node_modules", "dist", "build", ".git", ".next", "public", "ios", "android", "__fixtures__"];
|
|
13
|
+
const isSkippedPath = (filePath: string) =>
|
|
14
|
+
skipDirs.some((dir) => filePath.includes(`/${dir}/`) || filePath.startsWith(`${dir}/`));
|
|
10
15
|
const builtinModuleSet = new Set([...builtinModules, ...builtinModules.map((mod) => `node:${mod}`)]);
|
|
11
16
|
const stripShebang = (source: string) => source.replace(/^#!.*(?:\r?\n|$)/, "");
|
|
12
17
|
|
|
@@ -17,10 +22,10 @@ export class TypeScriptDependencyScanner {
|
|
|
17
22
|
#visitedFiles = new Set<string>();
|
|
18
23
|
readonly #tsTranspiler = new Bun.Transpiler({ loader: "ts" });
|
|
19
24
|
readonly #tsxTranspiler = new Bun.Transpiler({ loader: "tsx" });
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
readonly #directory: string;
|
|
26
|
+
readonly #rootPackageJson: PackageJson;
|
|
27
|
+
readonly #ig: ReturnType<typeof ignore>;
|
|
28
|
+
readonly #workspaceRoot: string;
|
|
24
29
|
|
|
25
30
|
constructor(
|
|
26
31
|
directory: string,
|
|
@@ -30,10 +35,10 @@ export class TypeScriptDependencyScanner {
|
|
|
30
35
|
gitignorePatterns = [],
|
|
31
36
|
}: { workspaceRoot: string; tsconfig: TsConfigJson; rootPackageJson: PackageJson; gitignorePatterns?: string[] },
|
|
32
37
|
) {
|
|
33
|
-
this
|
|
34
|
-
this
|
|
35
|
-
this
|
|
36
|
-
this
|
|
38
|
+
this.#directory = directory;
|
|
39
|
+
this.#rootPackageJson = rootPackageJson;
|
|
40
|
+
this.#ig = ignore().add(gitignorePatterns);
|
|
41
|
+
this.#workspaceRoot = workspaceRoot;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
async getMonorepoDependencies(
|
|
@@ -41,7 +46,7 @@ export class TypeScriptDependencyScanner {
|
|
|
41
46
|
{ pkgs = [], libs = [] }: { pkgs?: string[]; libs?: string[] } = {},
|
|
42
47
|
): Promise<{ pkgDeps: string[]; libDeps: string[]; npmDeps: string[]; npmDevDeps: string[] }> {
|
|
43
48
|
const npmSet = new Set(
|
|
44
|
-
Object.keys({ ...this
|
|
49
|
+
Object.keys({ ...this.#rootPackageJson.dependencies, ...this.#rootPackageJson.devDependencies }),
|
|
45
50
|
);
|
|
46
51
|
const pkgPathSet = new Set(pkgs);
|
|
47
52
|
const libPathSet = new Set(libs.map((lib) => `@libs/${lib}`));
|
|
@@ -68,11 +73,11 @@ export class TypeScriptDependencyScanner {
|
|
|
68
73
|
): Promise<{ npmDeps: string[]; npmDevDeps: string[]; missingDeps: string[] }> {
|
|
69
74
|
const runtimeDeps = new Set<string>();
|
|
70
75
|
const devDeps = new Set<string>();
|
|
71
|
-
const sourceFiles = await this.#findTypeScriptFiles(this
|
|
76
|
+
const sourceFiles = await this.#findTypeScriptFiles(this.#directory, {
|
|
72
77
|
excludeBuildFiles: true,
|
|
73
78
|
excludeTestFiles: true,
|
|
74
79
|
});
|
|
75
|
-
const cssFiles = await this.#findCssFiles(this
|
|
80
|
+
const cssFiles = await this.#findCssFiles(this.#directory);
|
|
76
81
|
|
|
77
82
|
for (const filePath of sourceFiles) {
|
|
78
83
|
const fileContent = await FileSys.readText(filePath);
|
|
@@ -86,7 +91,7 @@ export class TypeScriptDependencyScanner {
|
|
|
86
91
|
this.#addNormalizedImports(runtimeDeps, this.#extractCssPluginImports(fileContent), projectName);
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
const buildFilePath = path.join(this
|
|
94
|
+
const buildFilePath = path.join(this.#directory, "build.ts");
|
|
90
95
|
if (await FileSys.fileExists(buildFilePath)) {
|
|
91
96
|
const fileContent = await FileSys.readText(buildFilePath);
|
|
92
97
|
const { imports, typeImports } = this.#extractImports(fileContent, buildFilePath);
|
|
@@ -95,7 +100,7 @@ export class TypeScriptDependencyScanner {
|
|
|
95
100
|
|
|
96
101
|
for (const dep of runtimeDeps) devDeps.delete(dep);
|
|
97
102
|
|
|
98
|
-
const rootDeps = { ...this
|
|
103
|
+
const rootDeps = { ...this.#rootPackageJson.dependencies, ...this.#rootPackageJson.devDependencies };
|
|
99
104
|
const missingDeps: string[] = [];
|
|
100
105
|
for (const dep of [...runtimeDeps, ...devDeps]) {
|
|
101
106
|
if (rootDeps[dep] || (await this.#hasWorkspacePackage(dep))) continue;
|
|
@@ -109,7 +114,7 @@ export class TypeScriptDependencyScanner {
|
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
async #hasWorkspacePackage(dep: string) {
|
|
112
|
-
const packageJsonPath = path.join(this
|
|
117
|
+
const packageJsonPath = path.join(this.#workspaceRoot, "pkgs", dep, "package.json");
|
|
113
118
|
if (!(await Bun.file(packageJsonPath).exists())) return false;
|
|
114
119
|
try {
|
|
115
120
|
const packageJson = await FileSys.readJson<PackageJson>(packageJsonPath);
|
|
@@ -156,9 +161,9 @@ export class TypeScriptDependencyScanner {
|
|
|
156
161
|
this.#fileTypeDependencies.clear();
|
|
157
162
|
this.#visitedFiles.clear();
|
|
158
163
|
|
|
159
|
-
const files = await this.#findTypeScriptFiles(this
|
|
164
|
+
const files = await this.#findTypeScriptFiles(this.#directory);
|
|
160
165
|
|
|
161
|
-
for (const file of files) await this.#analyzeFile(file, this
|
|
166
|
+
for (const file of files) await this.#analyzeFile(file, this.#directory);
|
|
162
167
|
|
|
163
168
|
return this.#fileDependencies;
|
|
164
169
|
}
|
|
@@ -171,17 +176,16 @@ export class TypeScriptDependencyScanner {
|
|
|
171
176
|
}: { excludeBuildFiles?: boolean; excludeTestFiles?: boolean } = {},
|
|
172
177
|
): Promise<string[]> {
|
|
173
178
|
const files: string[] = [];
|
|
174
|
-
const skipDirs = ["node_modules", "dist", "build", ".git", ".next", "public", "ios", "android"];
|
|
175
179
|
|
|
176
180
|
const glob = new Bun.Glob("**/*.{ts,tsx}");
|
|
177
181
|
for await (const filePath of glob.scan({ cwd: directory, onlyFiles: true })) {
|
|
178
|
-
if (
|
|
182
|
+
if (isSkippedPath(filePath)) continue;
|
|
179
183
|
if (excludeBuildFiles && filePath === "build.ts") continue;
|
|
180
184
|
if (excludeTestFiles && testFileRegex.test(filePath)) continue;
|
|
181
185
|
|
|
182
186
|
const fullPath = path.join(directory, filePath);
|
|
183
|
-
const relativePath = path.relative(this
|
|
184
|
-
if (this
|
|
187
|
+
const relativePath = path.relative(this.#workspaceRoot, fullPath);
|
|
188
|
+
if (this.#ig.ignores(relativePath)) continue;
|
|
185
189
|
|
|
186
190
|
files.push(fullPath);
|
|
187
191
|
}
|
|
@@ -190,14 +194,13 @@ export class TypeScriptDependencyScanner {
|
|
|
190
194
|
|
|
191
195
|
async #findCssFiles(directory: string): Promise<string[]> {
|
|
192
196
|
const files: string[] = [];
|
|
193
|
-
const skipDirs = ["node_modules", "dist", "build", ".git", ".next", "public", "ios", "android"];
|
|
194
197
|
const glob = new Bun.Glob("**/*.css");
|
|
195
198
|
for await (const filePath of glob.scan({ cwd: directory, onlyFiles: true })) {
|
|
196
|
-
if (
|
|
199
|
+
if (isSkippedPath(filePath)) continue;
|
|
197
200
|
|
|
198
201
|
const fullPath = path.join(directory, filePath);
|
|
199
|
-
const relativePath = path.relative(this
|
|
200
|
-
if (this
|
|
202
|
+
const relativePath = path.relative(this.#workspaceRoot, fullPath);
|
|
203
|
+
if (this.#ig.ignores(relativePath)) continue;
|
|
201
204
|
|
|
202
205
|
files.push(fullPath);
|
|
203
206
|
}
|
package/devkitUtils.test.ts
CHANGED
|
@@ -6,8 +6,6 @@ import { ApplicationBuildReporter } from "./applicationBuildReporter";
|
|
|
6
6
|
import { resolveSignalTestPreloadPath } from "./applicationTestPreload";
|
|
7
7
|
import { TypeScriptDependencyScanner } from "./dependencyScanner";
|
|
8
8
|
import { AppExecutor, WorkspaceExecutor } from "./executors";
|
|
9
|
-
import { extractDependencies } from "./extractDeps";
|
|
10
|
-
import { getModelFileData } from "./getModelFileData";
|
|
11
9
|
import type { PackageJson, TsConfigJson } from "./types";
|
|
12
10
|
|
|
13
11
|
const tempRoots: string[] = [];
|
|
@@ -27,65 +25,6 @@ afterEach(async () => {
|
|
|
27
25
|
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
28
26
|
});
|
|
29
27
|
|
|
30
|
-
describe("extractDependencies", () => {
|
|
31
|
-
const packageJson: PackageJson = {
|
|
32
|
-
name: "fixture",
|
|
33
|
-
version: "1.0.0",
|
|
34
|
-
description: "fixture",
|
|
35
|
-
dependencies: {
|
|
36
|
-
react: "19.0.0",
|
|
37
|
-
"@scope/pkg": "1.0.0",
|
|
38
|
-
lodash: "4.0.0",
|
|
39
|
-
},
|
|
40
|
-
devDependencies: {
|
|
41
|
-
typescript: "6.0.0",
|
|
42
|
-
vite: "5.0.0",
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
test("extracts runtime package versions from imports and requires", () => {
|
|
47
|
-
const deps = extractDependencies(
|
|
48
|
-
[
|
|
49
|
-
{
|
|
50
|
-
path: "index.ts",
|
|
51
|
-
text: [
|
|
52
|
-
'import React from "react";',
|
|
53
|
-
'import { value } from "@scope/pkg/subpath";',
|
|
54
|
-
'import type { Type } from "typescript";',
|
|
55
|
-
'const lodash = require("lodash/fp");',
|
|
56
|
-
'const fs = require("node:fs");',
|
|
57
|
-
'const path = require("path");',
|
|
58
|
-
"",
|
|
59
|
-
].join("\n"),
|
|
60
|
-
},
|
|
61
|
-
{ path: "style.css", text: '@import "vite";' },
|
|
62
|
-
],
|
|
63
|
-
packageJson,
|
|
64
|
-
["vite"],
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
expect(deps).toEqual({
|
|
68
|
-
"@scope/pkg": "1.0.0",
|
|
69
|
-
lodash: "4.0.0",
|
|
70
|
-
react: "19.0.0",
|
|
71
|
-
typescript: "6.0.0",
|
|
72
|
-
vite: "5.0.0",
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("reports missing dependency sections and missing versions", () => {
|
|
77
|
-
expect(() =>
|
|
78
|
-
extractDependencies([{ path: "index.ts", text: 'import React from "react";' }], {
|
|
79
|
-
name: "broken",
|
|
80
|
-
version: "1.0.0",
|
|
81
|
-
description: "broken",
|
|
82
|
-
}),
|
|
83
|
-
).toThrow("No dependencies found");
|
|
84
|
-
|
|
85
|
-
expect(() => extractDependencies([], packageJson, ["missing"])).toThrow("No version found for missing");
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
28
|
describe("resolveSignalTestPreloadPath", () => {
|
|
90
29
|
test("resolves the preload file from an installed akanjs package", async () => {
|
|
91
30
|
const root = await makeTempRoot();
|
|
@@ -252,49 +191,6 @@ describe("scan convention", () => {
|
|
|
252
191
|
});
|
|
253
192
|
});
|
|
254
193
|
|
|
255
|
-
describe("getModelFileData", () => {
|
|
256
|
-
test("reads model files and derives imported local, scalar, and lib models", async () => {
|
|
257
|
-
const root = await makeTempRoot();
|
|
258
|
-
const cwd = process.cwd();
|
|
259
|
-
process.chdir(root);
|
|
260
|
-
try {
|
|
261
|
-
await write(
|
|
262
|
-
path.join(root, "apps/demo/lib/post/post.constant.ts"),
|
|
263
|
-
[
|
|
264
|
-
'import { cnst as shared } from "@libs/shared";',
|
|
265
|
-
'import { User } from "../user/user.constant";',
|
|
266
|
-
'import { Money } from "../_money/money.constant";',
|
|
267
|
-
"export const Post = {};",
|
|
268
|
-
"",
|
|
269
|
-
].join("\n"),
|
|
270
|
-
);
|
|
271
|
-
await write(
|
|
272
|
-
path.join(root, "apps/demo/lib/post/Post.Unit.tsx"),
|
|
273
|
-
"export default function Unit() { return null; }\n",
|
|
274
|
-
);
|
|
275
|
-
await write(
|
|
276
|
-
path.join(root, "apps/demo/lib/post/Post.View.tsx"),
|
|
277
|
-
"export default function View() { return null; }\n",
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
const data = await getModelFileData("apps/demo", "post");
|
|
281
|
-
expect(data).toMatchObject({
|
|
282
|
-
moduleType: "app",
|
|
283
|
-
moduleName: "demo",
|
|
284
|
-
modelName: "post",
|
|
285
|
-
importModelNames: ["user"],
|
|
286
|
-
hasImportScalar: true,
|
|
287
|
-
importLibNames: ["shared"],
|
|
288
|
-
});
|
|
289
|
-
expect(data.constantFileStr).toContain("export const Post");
|
|
290
|
-
expect(data.unitFileStr).toContain("Unit");
|
|
291
|
-
expect(data.viewFileStr).toContain("View");
|
|
292
|
-
} finally {
|
|
293
|
-
process.chdir(cwd);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
194
|
describe("ApplicationBuildReporter", () => {
|
|
299
195
|
test("formats duration, phase lines, and nested errors", () => {
|
|
300
196
|
expect(ApplicationBuildReporter.formatDuration(999)).toBe("999ms");
|
package/executors.test.ts
CHANGED
|
@@ -31,7 +31,6 @@ const rootPackageJson = (extra: Partial<PackageJson> = {}): PackageJson => ({
|
|
|
31
31
|
react: "19.0.0",
|
|
32
32
|
"react-dom": "19.0.0",
|
|
33
33
|
"react-server-dom-webpack": "19.0.0",
|
|
34
|
-
sharp: "1.0.0",
|
|
35
34
|
lodash: "4.0.0",
|
|
36
35
|
},
|
|
37
36
|
devDependencies: {
|
|
@@ -128,6 +127,49 @@ describe("Executor filesystem helpers", () => {
|
|
|
128
127
|
expect(await readFile(path.join(root, "local/docker-compose.yaml"), "utf8")).toBe("custom");
|
|
129
128
|
});
|
|
130
129
|
|
|
130
|
+
test("hands every scaffolded TypeScript file to the formatter, and nothing else", async () => {
|
|
131
|
+
// A template emits identifiers it cannot sort — `import { fetch, Task, usePage }` is right for a model
|
|
132
|
+
// named Task and wrong for Zoo — and unsorted imports fail `biome check`. So the scaffold is formatted
|
|
133
|
+
// on the way out, or it is red for most model names whatever the template says.
|
|
134
|
+
const root = await makeTempRoot();
|
|
135
|
+
const exec = new Executor("fixture", root);
|
|
136
|
+
const formatted: string[] = [];
|
|
137
|
+
exec.getLinter = () =>
|
|
138
|
+
({
|
|
139
|
+
fixFiles: async (filePaths: string[]) => {
|
|
140
|
+
formatted.push(...filePaths);
|
|
141
|
+
return { fixed: [] };
|
|
142
|
+
},
|
|
143
|
+
}) as unknown as ReturnType<Executor["getLinter"]>;
|
|
144
|
+
|
|
145
|
+
await exec.applyTemplate({
|
|
146
|
+
basePath: "apps/demo/page/task",
|
|
147
|
+
template: "crudSinglePage",
|
|
148
|
+
dict: { model: "task", appName: "demo" },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(formatted).toEqual([path.join(root, "apps/demo/page/task/_index.tsx")]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("a formatter that cannot run does not fail the scaffold", async () => {
|
|
155
|
+
// `create-akan-workspace` scaffolds before `bun install`, so there is no local Biome binary and often
|
|
156
|
+
// no config above the target. An unformatted file is a lint fix; a failed scaffold is not.
|
|
157
|
+
const root = await makeTempRoot();
|
|
158
|
+
const exec = new Executor("fixture", root);
|
|
159
|
+
exec.getLinter = () => {
|
|
160
|
+
throw new Error("biome.json not found");
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const created = await exec.applyTemplate({
|
|
164
|
+
basePath: "apps/demo/page/task",
|
|
165
|
+
template: "crudSinglePage",
|
|
166
|
+
dict: { model: "task", appName: "demo" },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(created).toHaveLength(1);
|
|
170
|
+
expect(await readFile(path.join(root, "apps/demo/page/task/_index.tsx"), "utf8")).toContain("Task.Zone.Card");
|
|
171
|
+
});
|
|
172
|
+
|
|
131
173
|
test("applies hidden files and directories from CLI templates", async () => {
|
|
132
174
|
const root = await makeTempRoot();
|
|
133
175
|
const exec = new Executor("fixture", root);
|
|
@@ -204,6 +246,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
204
246
|
serveDomain: "example.com",
|
|
205
247
|
env: "local",
|
|
206
248
|
portOffset: 10,
|
|
249
|
+
workspaceId: undefined,
|
|
207
250
|
});
|
|
208
251
|
|
|
209
252
|
delete process.env.AKAN_PUBLIC_REPO_NAME;
|
|
@@ -234,6 +277,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
234
277
|
serveDomain: "file.example.com",
|
|
235
278
|
env: "develop",
|
|
236
279
|
portOffset: 7,
|
|
280
|
+
workspaceId: undefined,
|
|
237
281
|
});
|
|
238
282
|
});
|
|
239
283
|
|
|
@@ -271,6 +315,11 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
271
315
|
const prepared = await app.prepareCommand("build");
|
|
272
316
|
expect(prepared.env.AKAN_COMMAND_TYPE).toBe("build");
|
|
273
317
|
expect(prepared.env.AKAN_PUBLIC_BASE_PATHS).toBe("admin");
|
|
318
|
+
// Bundling reads `process.env` through getPublicEnv and `define`s every AKAN_PUBLIC_* into a literal, so a
|
|
319
|
+
// dev port published here would be baked into the artifact and outrank the PORT the container is run with.
|
|
320
|
+
expect(process.env.AKAN_PUBLIC_APP_NAME).toBe("demo");
|
|
321
|
+
expect(process.env.AKAN_PUBLIC_CLIENT_PORT).toBeUndefined();
|
|
322
|
+
expect(process.env.AKAN_PUBLIC_SERVER_PORT).toBeUndefined();
|
|
274
323
|
expect((await stat(path.join(root, "dist/apps/demo/private"))).isDirectory()).toBe(true);
|
|
275
324
|
expect((await stat(path.join(root, "dist/apps/demo/public"))).isDirectory()).toBe(true);
|
|
276
325
|
});
|
|
@@ -546,9 +595,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
546
595
|
"_index.tsx": `export const pageConfig = { devOnly: process.env.NODE_ENV !== "production" };\n${PAGE_SOURCE}`,
|
|
547
596
|
});
|
|
548
597
|
|
|
549
|
-
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(
|
|
550
|
-
"pageConfig.devOnly must be a literal true or false",
|
|
551
|
-
);
|
|
598
|
+
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow("devOnly must be a literal true or false");
|
|
552
599
|
});
|
|
553
600
|
|
|
554
601
|
test("reads devOnly through a satisfies annotation", async () => {
|
|
@@ -622,6 +669,64 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
622
669
|
});
|
|
623
670
|
});
|
|
624
671
|
|
|
672
|
+
describe("root layout source validation during page key discovery", () => {
|
|
673
|
+
const makeRouteValidationApp = async (appName: string, config: string, files: Record<string, string>) => {
|
|
674
|
+
const root = await makeTempRoot();
|
|
675
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
676
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
677
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
678
|
+
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
679
|
+
await mkdir(path.join(root, "apps", appName), { recursive: true });
|
|
680
|
+
await writeFile(path.join(root, `apps/${appName}/akan.config.ts`), config);
|
|
681
|
+
for (const [file, source] of Object.entries(files)) {
|
|
682
|
+
const filePath = path.join(root, `apps/${appName}/page`, file);
|
|
683
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
684
|
+
await writeFile(filePath, source);
|
|
685
|
+
}
|
|
686
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
687
|
+
return AppExecutor.from(workspace, appName);
|
|
688
|
+
};
|
|
689
|
+
const wsConnectLayout = [
|
|
690
|
+
"export const wsConnect = false;",
|
|
691
|
+
"export default function Layout({ children }) { return children; }",
|
|
692
|
+
"",
|
|
693
|
+
].join("\n");
|
|
694
|
+
|
|
695
|
+
test("accepts wsConnect on a configured base-path root layout", async () => {
|
|
696
|
+
process.env.AKAN_PUBLIC_BASE_PATHS = "web,admin";
|
|
697
|
+
const app = await makeRouteValidationApp(
|
|
698
|
+
"base-path-root-ws",
|
|
699
|
+
[
|
|
700
|
+
"export default {",
|
|
701
|
+
' routes: [{ basePath: "web", domains: {} }, { basePath: "admin", domains: {} }],',
|
|
702
|
+
"};",
|
|
703
|
+
"",
|
|
704
|
+
].join("\n"),
|
|
705
|
+
{ "admin/_layout.tsx": wsConnectLayout },
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
await expect(app.getPageKeys({ refresh: true })).resolves.toEqual(["./admin/_layout.tsx"]);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("rejects wsConnect on a nested layout", async () => {
|
|
712
|
+
const app = await makeRouteValidationApp(
|
|
713
|
+
"nested-layout-ws",
|
|
714
|
+
'export default { routes: [{ basePath: "admin", domains: {} }] };\n',
|
|
715
|
+
{ "admin/users/_layout.tsx": wsConnectLayout },
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(/unsupported export "wsConnect"/);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test("accepts wsConnect on a grouped root layout", async () => {
|
|
722
|
+
const app = await makeRouteValidationApp("grouped-root-ws", "export default {};\n", {
|
|
723
|
+
"(docs)/_layout.tsx": wsConnectLayout,
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
await expect(app.getPageKeys({ refresh: true })).resolves.toEqual(["./(docs)/_layout.tsx"]);
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
625
730
|
test("accepts metadata route exports during page key discovery", async () => {
|
|
626
731
|
const root = await makeTempRoot();
|
|
627
732
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
@@ -760,6 +865,29 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
760
865
|
expect(offsetMinimalStart.env.AKAN_PUBLIC_CLIENT_PORT).toBe("8286");
|
|
761
866
|
expect(offsetMinimalStart.env.AKAN_PUBLIC_SERVER_PORT).toBe("8286");
|
|
762
867
|
});
|
|
868
|
+
|
|
869
|
+
test("pins the start command to development so an ambient NODE_ENV cannot pick the production router", async () => {
|
|
870
|
+
const root = await makeTempRoot();
|
|
871
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
872
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
873
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
874
|
+
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
875
|
+
await mkdir(path.join(root, "apps/ambient"), { recursive: true });
|
|
876
|
+
await writeFile(path.join(root, "apps/ambient/akan.config.ts"), "export default {};\n");
|
|
877
|
+
|
|
878
|
+
const originalNodeEnv = process.env.NODE_ENV;
|
|
879
|
+
process.env.NODE_ENV = "production";
|
|
880
|
+
try {
|
|
881
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
882
|
+
const started = await AppExecutor.from(workspace, "ambient").prepareCommand("start");
|
|
883
|
+
expect(started.env.NODE_ENV).toBe("development");
|
|
884
|
+
// The builder runs in this process and bakes NODE_ENV into the dev bundles it emits.
|
|
885
|
+
expect(process.env.NODE_ENV).toBe("development");
|
|
886
|
+
} finally {
|
|
887
|
+
if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
|
|
888
|
+
else process.env.NODE_ENV = originalNodeEnv;
|
|
889
|
+
}
|
|
890
|
+
});
|
|
763
891
|
});
|
|
764
892
|
|
|
765
893
|
describe("PkgExecutor package generation", () => {
|
|
@@ -773,7 +901,7 @@ describe("PkgExecutor package generation", () => {
|
|
|
773
901
|
exports: { "./extra": { import: "./extra.ts" } },
|
|
774
902
|
peerDependencies: { react: "19.0.0" },
|
|
775
903
|
peerDependenciesMeta: { react: { optional: true } },
|
|
776
|
-
optionalDependencies: {
|
|
904
|
+
optionalDependencies: { "@sample/native": "1.0.0" },
|
|
777
905
|
});
|
|
778
906
|
|
|
779
907
|
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
@@ -783,12 +911,12 @@ describe("PkgExecutor package generation", () => {
|
|
|
783
911
|
expect(distPackageJson).toMatchObject({
|
|
784
912
|
name: "@sample/tool",
|
|
785
913
|
type: "module",
|
|
786
|
-
engines: { bun: ">=1.
|
|
914
|
+
engines: { bun: ">=1.4.0" },
|
|
787
915
|
dependencies: { lodash: "4.0.0" },
|
|
788
916
|
devDependencies: { typescript: "6.0.0" },
|
|
789
917
|
peerDependencies: { react: "19.0.0" },
|
|
790
918
|
peerDependenciesMeta: { react: { optional: true } },
|
|
791
|
-
optionalDependencies: {
|
|
919
|
+
optionalDependencies: { "@sample/native": "1.0.0" },
|
|
792
920
|
});
|
|
793
921
|
expect(distPackageJson.exports?.["."]).toEqual({
|
|
794
922
|
import: "./index.ts",
|