@akanjs/devkit 3.0.1 → 3.0.2
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 -2
- package/akanApp/akanApp.host.test.ts +62 -0
- package/akanApp/akanApp.host.ts +13 -2
- package/akanApp/devHostPolicy.test.ts +14 -0
- package/akanApp/devHostPolicy.ts +11 -0
- package/akanConfig/akanConfig.test.ts +4 -1
- package/akanConfig/akanConfig.ts +3 -1
- package/akanContext.ts +18 -1
- package/biome.base.json +8 -1
- package/commandDecorators/command.ts +3 -2
- package/commandDecorators/commandDecorators.test.ts +7 -0
- package/createTunnel.test.ts +51 -0
- package/createTunnel.ts +32 -14
- package/csrE2e/csrPageStack.e2e.test.ts +12 -3
- package/executors.test.ts +329 -4
- package/executors.ts +49 -47
- package/frontendBuild/csrArtifactBuilder.test.ts +34 -0
- package/frontendBuild/csrArtifactBuilder.ts +14 -8
- package/frontendBuild/csrDevModuleCompiler.ts +0 -1
- package/frontendBuild/csrDevResolver.ts +0 -2
- package/frontendBuild/devGeneratedIndexSync.test.ts +13 -1
- package/frontendBuild/devGeneratedIndexSync.ts +21 -4
- package/frontendBuild/frontendBuild.test.ts +1 -1
- package/jsonFormatter.test.ts +64 -0
- package/jsonFormatter.ts +78 -0
- package/lint/__fixtures__/README.md +12 -3
- package/lint/__fixtures__/no-double-brace-placeholder/bad.ts +22 -0
- package/lint/__fixtures__/no-double-brace-placeholder/fixture.json +1 -0
- package/lint/__fixtures__/no-double-brace-placeholder/good.ts +15 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +17 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +13 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +8 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +8 -1
- package/lint/__fixtures__/no-unguarded-endpoint/bad.ts +20 -0
- package/lint/__fixtures__/no-unguarded-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-unguarded-endpoint/good.ts +28 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -7
- package/lint/gritRules.test.ts +23 -15
- package/lint/no-double-brace-placeholder.grit +15 -0
- package/lint/no-inline-color.grit +21 -5
- package/lint/no-throw-raw-error.grit +18 -33
- package/lint/no-unguarded-endpoint.grit +73 -0
- package/manifestDependencies.ts +65 -0
- package/mobile/desktopBin.test.ts +1 -1
- package/mobile/desktopServerStage.test.ts +7 -4
- package/mobile/nativeApp.test.ts +57 -8
- package/mobile/nativeApp.ts +27 -9
- package/mobile/nativeConfig.test.ts +26 -4
- package/mobile/nativeConfig.ts +19 -1
- package/package.json +2 -2
- package/platformTest/LinuxDockerTarget.ts +1 -1
- package/platformTest/PlatformTestTarget.ts +3 -1
- package/platformTest/WindowsSshTarget.ts +1 -1
- package/qualityScanner.test.ts +51 -0
- package/qualityScanner.ts +22 -1
- package/scanInfo.ts +14 -2
- package/testHelpers.ts +17 -0
- package/transforms/transforms.test.ts +13 -0
- package/tsconfig.json +4 -15
- package/workspaceLayout.test.ts +78 -1
- package/workspaceLayout.ts +19 -2
package/scanInfo.ts
CHANGED
|
@@ -10,7 +10,14 @@ import type {
|
|
|
10
10
|
} from "./akanConfig";
|
|
11
11
|
import { AkanAppConfig } from "./akanConfig";
|
|
12
12
|
import { AppExecutor, LibExecutor, PkgExecutor, WorkspaceExecutor } from "./executors";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
barrelFacetDirs,
|
|
15
|
+
facetIndexShadowOf,
|
|
16
|
+
isAllowedLibFacetRootFile,
|
|
17
|
+
rootAllowedDirs,
|
|
18
|
+
rootAllowedFiles,
|
|
19
|
+
rootEntryHintOf,
|
|
20
|
+
} from "./workspaceLayout";
|
|
14
21
|
|
|
15
22
|
const scalarFileTypes = ["constant", "dictionary", "document", "template", "unit", "util", "view", "zone"] as const;
|
|
16
23
|
type ScalarFileType = (typeof scalarFileTypes)[number];
|
|
@@ -74,6 +81,11 @@ async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: {
|
|
|
74
81
|
for (const dirname of dirs)
|
|
75
82
|
if (!allowedRootDirs.has(dirname))
|
|
76
83
|
addViolation(dirname, rootEntryHintOf(exec.type, dirname) ?? `unsupported ${exec.type} root folder`);
|
|
84
|
+
for (const facet of barrelFacetDirs)
|
|
85
|
+
for (const filename of await exec.readdir(facet)) {
|
|
86
|
+
const shadow = facetIndexShadowOf(`${facet}/${filename}`);
|
|
87
|
+
if (shadow) addViolation(path.join(facet, filename), shadow);
|
|
88
|
+
}
|
|
77
89
|
|
|
78
90
|
//* A lib has no `getPageKeys`, so its own route files are validated here.
|
|
79
91
|
if (exec.type === "lib")
|
|
@@ -275,7 +287,7 @@ class ScanInfo {
|
|
|
275
287
|
}
|
|
276
288
|
}
|
|
277
289
|
|
|
278
|
-
const isAkanFrameworkDependency = (dep: string) => dep === "akanjs" || dep.startsWith("akanjs/");
|
|
290
|
+
export const isAkanFrameworkDependency = (dep: string) => dep === "akanjs" || dep.startsWith("akanjs/");
|
|
279
291
|
export class AppInfo extends ScanInfo {
|
|
280
292
|
readonly type = "app";
|
|
281
293
|
readonly exec: AppExecutor;
|
package/testHelpers.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach } from "bun:test";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
@@ -122,6 +123,22 @@ export const writeJson = async (filePath: string, value: object) => {
|
|
|
122
123
|
await writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
123
124
|
};
|
|
124
125
|
|
|
126
|
+
const repoRoot = path.resolve(import.meta.dir, "../../..");
|
|
127
|
+
const biomeBin = path.join(repoRoot, "node_modules", ".bin", process.platform === "win32" ? "biome.exe" : "biome");
|
|
128
|
+
export const hasBiome = existsSync(biomeBin);
|
|
129
|
+
|
|
130
|
+
//? Through stdin under a repo-relative name, so the repo's own Biome config and overrides judge content kept elsewhere.
|
|
131
|
+
export const formatWithBiome = async (content: string, repoPath: string) => {
|
|
132
|
+
const proc = Bun.spawn([biomeBin, "check", "--write", `--stdin-file-path=${repoPath}`], {
|
|
133
|
+
cwd: repoRoot,
|
|
134
|
+
stdin: new Blob([content]),
|
|
135
|
+
stdout: "pipe",
|
|
136
|
+
stderr: "pipe",
|
|
137
|
+
});
|
|
138
|
+
const [output] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
139
|
+
return output;
|
|
140
|
+
};
|
|
141
|
+
|
|
125
142
|
const tsconfigJson = { compilerOptions: { target: "ESNext", paths: {} } };
|
|
126
143
|
|
|
127
144
|
export const createTempApp = async (appName = "demo", parentDir?: string) => {
|
|
@@ -230,6 +230,19 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
230
230
|
expect(secondMap?.get("b2")).toEqual({ subpath: "second/b2", originalName: "b2" });
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
+
test("an empty facet barrel maps no name, so an import from it is left as written", async () => {
|
|
234
|
+
const root = await makeTempRoot();
|
|
235
|
+
const pkgDir = path.join(root, "common");
|
|
236
|
+
await write(path.join(pkgDir, "index.ts"), "export {};\n");
|
|
237
|
+
const analyzer = new BarrelAnalyzer({
|
|
238
|
+
resolvePackage: async () => ({ pkgName: "@libs/util/common", entryFile: path.join(pkgDir, "index.ts"), pkgDir }),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(await analyzer.analyze("@libs/util/common")).toEqual(new Map());
|
|
242
|
+
const source = 'import { pad } from "@libs/util/common";\n';
|
|
243
|
+
expect(await rewriteBarrelImports(source, ["@libs/util/common"], analyzer)).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
|
|
233
246
|
test("rewrites flattenable named imports and preserves default, type, and unknown imports", async () => {
|
|
234
247
|
const analyzer = fakeAnalyzer([
|
|
235
248
|
["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
|
package/tsconfig.json
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"lib": [
|
|
4
|
-
"ESNext",
|
|
5
|
-
"DOM"
|
|
6
|
-
],
|
|
3
|
+
"lib": ["ESNext", "DOM"],
|
|
7
4
|
"target": "ESNext",
|
|
8
5
|
"module": "esnext",
|
|
9
6
|
"moduleDetection": "force",
|
|
10
7
|
"jsx": "react-jsx",
|
|
11
8
|
"allowJs": true,
|
|
12
|
-
"types": [
|
|
13
|
-
"bun"
|
|
14
|
-
],
|
|
9
|
+
"types": ["bun"],
|
|
15
10
|
"moduleResolution": "bundler",
|
|
16
11
|
"allowImportingTsExtensions": true,
|
|
17
12
|
"verbatimModuleSyntax": true,
|
|
@@ -30,12 +25,6 @@
|
|
|
30
25
|
},
|
|
31
26
|
"extends": "../../../tsconfig.json",
|
|
32
27
|
"files": [],
|
|
33
|
-
"include": [
|
|
34
|
-
|
|
35
|
-
"**/*.tsx",
|
|
36
|
-
"../../akanjs/server/types/*.d.ts"
|
|
37
|
-
],
|
|
38
|
-
"exclude": [
|
|
39
|
-
"lint/__fixtures__"
|
|
40
|
-
]
|
|
28
|
+
"include": ["**/*.ts", "**/*.tsx", "../../akanjs/server/types/*.d.ts"],
|
|
29
|
+
"exclude": ["lint/__fixtures__"]
|
|
41
30
|
}
|
package/workspaceLayout.test.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import {
|
|
3
5
|
appRootAllowedDirs,
|
|
4
6
|
appRootAllowedFiles,
|
|
7
|
+
facetIndexShadowOf,
|
|
5
8
|
isAllowedLibFacetRootFile,
|
|
6
9
|
isScannedRootEntry,
|
|
7
10
|
libRootAllowedDirs,
|
|
@@ -34,7 +37,7 @@ describe("app root layout allowlist", () => {
|
|
|
34
37
|
test("names what a Capacitor app kept in its root as a leftover, not an unknown entry", () => {
|
|
35
38
|
for (const name of ["android", "ios", "mobile", "capacitor.config.ts", "capacitor.config.json"]) {
|
|
36
39
|
expect(appRootAllowedFiles.has(name) || appRootAllowedDirs.has(name)).toBe(false);
|
|
37
|
-
expect(rootEntryHintOf("app", name)).toContain("
|
|
40
|
+
expect(rootEntryHintOf("app", name)).toContain("dist/native/<app>/<target>");
|
|
38
41
|
}
|
|
39
42
|
expect(rootEntryHintOf("app", "base")).toBeNull();
|
|
40
43
|
expect(rootEntryHintOf("lib", "ios")).toBeNull();
|
|
@@ -78,6 +81,28 @@ describe("lib root layout allowlist", () => {
|
|
|
78
81
|
});
|
|
79
82
|
});
|
|
80
83
|
|
|
84
|
+
describe("hand-written facet index", () => {
|
|
85
|
+
test("flags every index Bun resolves ahead of the generated barrel, at the root of each barrel facet", () => {
|
|
86
|
+
for (const facet of ["ui", "webkit", "srvkit", "common", "plugin"])
|
|
87
|
+
for (const filename of ["index.tsx", "index.jsx", "index.mts"])
|
|
88
|
+
expect(facetIndexShadowOf(`${facet}/${filename}`)).toContain(`shadows the generated ${facet}/index.ts`);
|
|
89
|
+
expect(facetIndexShadowOf("ui/index.tsx")).toContain("Delete it");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("leaves the generated barrel, a folder namespace, and folders without a barrel alone", () => {
|
|
93
|
+
for (const relativePath of [
|
|
94
|
+
"ui/index.ts",
|
|
95
|
+
"ui/index.js",
|
|
96
|
+
"ui/Chat/index.tsx",
|
|
97
|
+
"ui/Chat/index_.tsx",
|
|
98
|
+
"page/index.tsx",
|
|
99
|
+
"lib/index.tsx",
|
|
100
|
+
"index.tsx",
|
|
101
|
+
])
|
|
102
|
+
expect(facetIndexShadowOf(relativePath)).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
81
106
|
describe("lib facet root allowlist", () => {
|
|
82
107
|
test("admits the generated support facets and a root signal test", () => {
|
|
83
108
|
expect(isAllowedLibFacetRootFile("cnst.ts")).toBe(true);
|
|
@@ -91,3 +116,55 @@ describe("lib facet root allowlist", () => {
|
|
|
91
116
|
expect(isAllowedLibFacetRootFile("user.test.ts")).toBe(false);
|
|
92
117
|
});
|
|
93
118
|
});
|
|
119
|
+
|
|
120
|
+
const templatesDir = path.join(import.meta.dir, "..", "cli", "templates");
|
|
121
|
+
|
|
122
|
+
type TsConfig = { references?: { path: string }[] };
|
|
123
|
+
|
|
124
|
+
const loadRootTemplate = async (dir: string): Promise<TsConfig> =>
|
|
125
|
+
JSON.parse(await Bun.file(path.join(templatesDir, dir, "tsconfig.json.template")).text());
|
|
126
|
+
|
|
127
|
+
const templateDirsWithRootTsconfig = async (): Promise<string[]> => {
|
|
128
|
+
const entries = await readdir(templatesDir, { withFileTypes: true });
|
|
129
|
+
const dirs: string[] = [];
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (!entry.isDirectory()) continue;
|
|
132
|
+
if (await Bun.file(path.join(templatesDir, entry.name, "tsconfig.json.template")).exists()) dirs.push(entry.name);
|
|
133
|
+
}
|
|
134
|
+
return dirs.sort((a, b) => a.localeCompare(b));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
describe("cli root tsconfig templates vs workspace scan allowlists", () => {
|
|
138
|
+
test("only the app and libRoot root templates may declare references", async () => {
|
|
139
|
+
const dirs = await templateDirsWithRootTsconfig();
|
|
140
|
+
for (const dir of ["app", "libRoot", "pkgRoot", "workspaceRoot"]) expect(dirs).toContain(dir);
|
|
141
|
+
for (const dir of dirs) {
|
|
142
|
+
const tsconfig = await loadRootTemplate(dir);
|
|
143
|
+
if (!tsconfig.references) continue;
|
|
144
|
+
expect(["app", "libRoot"], `${dir} template declares references`).toContain(dir);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("app template references stay inside appRootAllowedFiles", async () => {
|
|
149
|
+
const tsconfig = await loadRootTemplate("app");
|
|
150
|
+
for (const reference of tsconfig.references ?? []) {
|
|
151
|
+
const filename = path.basename(reference.path);
|
|
152
|
+
expect(appRootAllowedFiles.has(filename), `app template references "${reference.path}"`).toBe(true);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("libRoot template references stay inside libRootAllowedFiles", async () => {
|
|
157
|
+
const tsconfig = await loadRootTemplate("libRoot");
|
|
158
|
+
for (const reference of tsconfig.references ?? []) {
|
|
159
|
+
const filename = path.basename(reference.path);
|
|
160
|
+
expect(libRootAllowedFiles.has(filename), `libRoot template references "${reference.path}"`).toBe(true);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("pkgRoot and workspaceRoot templates declare no references", async () => {
|
|
165
|
+
for (const dir of ["pkgRoot", "workspaceRoot"]) {
|
|
166
|
+
const tsconfig = await loadRootTemplate(dir);
|
|
167
|
+
expect(tsconfig.references, `${dir} template must not declare references`).toBeUndefined();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
package/workspaceLayout.ts
CHANGED
|
@@ -84,7 +84,24 @@ const libFacetRootTestPattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(
|
|
|
84
84
|
export const isAllowedLibFacetRootFile = (filename: string) =>
|
|
85
85
|
libFacetRootAllowedFiles.has(filename) || libFacetRootTestPattern.test(filename);
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
export const barrelFacetDirs = ["ui", "webkit", "srvkit", "common", "plugin"] as const;
|
|
88
|
+
|
|
89
|
+
//* Bun resolves a folder import to `index.tsx`, `index.jsx` or `index.mts` ahead of `index.ts`, and TypeScript to
|
|
90
|
+
//* `index.ts` first, so one of these beside the generated barrel typechecks every name the barrel adds and serves none.
|
|
91
|
+
const barrelShadowingIndexFiles = new Set(["index.tsx", "index.jsx", "index.mts"]);
|
|
92
|
+
|
|
93
|
+
export const facetIndexShadowOf = (sysRelativePath: string) => {
|
|
94
|
+
const [facet, filename = "", ...nested] = sysRelativePath.split("/");
|
|
95
|
+
if (nested.length || !barrelShadowingIndexFiles.has(filename) || !barrelFacetDirs.some((dir) => dir === facet))
|
|
96
|
+
return null;
|
|
97
|
+
return (
|
|
98
|
+
`shadows the generated ${facet}/index.ts: Bun loads ${filename} and TypeScript checks index.ts, so a name only ` +
|
|
99
|
+
`the generated barrel exports typechecks and is undefined at runtime. Delete it, since the generated barrel ` +
|
|
100
|
+
`re-exports every file and folder in ${facet}/; a namespace it builds belongs in ${facet}/<Folder>/index.tsx`
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
//* What a Capacitor app kept in its root. The native runtime generates its projects under `dist/native/<app>/<target>`, so
|
|
88
105
|
//* these are named as leftovers instead of as unknown entries.
|
|
89
106
|
const retiredAppRootEntries = new Set([
|
|
90
107
|
"android",
|
|
@@ -97,7 +114,7 @@ const retiredAppRootEntries = new Set([
|
|
|
97
114
|
|
|
98
115
|
export const rootEntryHintOf = (type: SysType, name: string) =>
|
|
99
116
|
type === "app" && retiredAppRootEntries.has(name)
|
|
100
|
-
? "a Capacitor-era entry; the native runtime generates its projects under
|
|
117
|
+
? "a Capacitor-era entry; the native runtime generates its projects under dist/native/<app>/<target>, so delete it"
|
|
101
118
|
: null;
|
|
102
119
|
|
|
103
120
|
// scanSync reads roots through `Bun.Glob("*")`, which skips dotfiles, so doctor skips them the same way.
|