@akanjs/devkit 3.0.0-alpha.8 → 3.0.0-alpha.81
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/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +47 -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 +65 -562
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +412 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +132 -47
- package/akanConfig/types.ts +8 -0
- package/akanContext.ts +70 -14
- package/akanMcpContract.ts +25 -4
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +45 -21
- package/artifact/implicitRootLayout.test.ts +67 -0
- package/artifact/implicitRootLayout.ts +27 -7
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +340 -0
- package/biomeBase.ts +9 -0
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/commandDecorators/command.ts +30 -6
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +132 -5
- package/executors.ts +89 -14
- package/fileEditor.ts +19 -19
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +4 -0
- 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 +123 -12
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/fontOptimizer.ts +37 -18
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +178 -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/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +8 -4
- package/index.ts +0 -5
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +109 -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-import-client-in-server.grit +48 -0
- package/lint/no-import-server-in-client.grit +45 -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 +99 -25
- package/package.json +6 -6
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- package/repoIdentity.ts +42 -0
- 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 +103 -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/ui/ScrollList.tsx +6 -8
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +49 -4
- 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/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { SubspaceConfigInput, SubspaceDeclaration } from "akanjs";
|
|
3
|
+
import type { WorkspaceExecutor } from "./executors";
|
|
4
|
+
import { FileSys } from "./fileSys";
|
|
5
|
+
|
|
6
|
+
export type { SubspaceConfigInput, SubspaceDeclaration };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `akan.subspace.ts` at the workspace root: which customer repo each app is mirrored to.
|
|
10
|
+
*
|
|
11
|
+
* There is no branch field. The branch is whichever one the workspace is on, so the same declaration
|
|
12
|
+
* serves `develop` and `main`, and every subspace on one branch holds the same akanjs version and the
|
|
13
|
+
* same library source.
|
|
14
|
+
*/
|
|
15
|
+
export class SubspaceConfig {
|
|
16
|
+
static readonly fileName = "akan.subspace.ts";
|
|
17
|
+
static readonly defaultPushableBranches = ["main", "develop", "debug"];
|
|
18
|
+
|
|
19
|
+
static async from(workspace: WorkspaceExecutor): Promise<SubspaceConfig | null> {
|
|
20
|
+
const configPath = path.join(workspace.workspaceRoot, SubspaceConfig.fileName);
|
|
21
|
+
if (!(await FileSys.fileExists(configPath))) return null;
|
|
22
|
+
const input = await import(configPath).then((mod: { default: SubspaceConfigInput }) => mod.default);
|
|
23
|
+
return new SubspaceConfig(input);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
readonly pushableBranches: string[];
|
|
27
|
+
readonly exclude: string[];
|
|
28
|
+
readonly subspaces: SubspaceDeclaration[];
|
|
29
|
+
|
|
30
|
+
constructor(input: SubspaceConfigInput) {
|
|
31
|
+
this.pushableBranches = input.pushableBranches ?? SubspaceConfig.defaultPushableBranches;
|
|
32
|
+
this.exclude = input.exclude ?? [];
|
|
33
|
+
this.subspaces = input.subspaces ?? [];
|
|
34
|
+
this.#assertValid();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#assertValid() {
|
|
38
|
+
const seenNames = new Set<string>();
|
|
39
|
+
const appOwners = new Map<string, string>();
|
|
40
|
+
for (const subspace of this.subspaces) {
|
|
41
|
+
if (!subspace.name || !subspace.repo)
|
|
42
|
+
throw new Error(`${SubspaceConfig.fileName}: every subspace needs a name and a repo`);
|
|
43
|
+
if (seenNames.has(subspace.name))
|
|
44
|
+
throw new Error(`${SubspaceConfig.fileName}: duplicate subspace "${subspace.name}"`);
|
|
45
|
+
seenNames.add(subspace.name);
|
|
46
|
+
if (!subspace.apps?.length)
|
|
47
|
+
throw new Error(`${SubspaceConfig.fileName}: subspace "${subspace.name}" declares no apps`);
|
|
48
|
+
for (const app of subspace.apps) {
|
|
49
|
+
const owner = appOwners.get(app);
|
|
50
|
+
//* One app never goes to two subspaces: the two would need per-subspace domains and ids, which
|
|
51
|
+
//* this declaration cannot express. Share code through a lib instead.
|
|
52
|
+
if (owner)
|
|
53
|
+
throw new Error(
|
|
54
|
+
`${SubspaceConfig.fileName}: app "${app}" is claimed by both "${owner}" and "${subspace.name}"`,
|
|
55
|
+
);
|
|
56
|
+
appOwners.set(app, subspace.name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
select(names: string[]) {
|
|
62
|
+
if (!names.length) return this.subspaces;
|
|
63
|
+
return names.map((name) => {
|
|
64
|
+
const subspace = this.subspaces.find((candidate) => candidate.name === name);
|
|
65
|
+
if (!subspace) throw new Error(`${SubspaceConfig.fileName}: unknown subspace "${name}"`);
|
|
66
|
+
return subspace;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
assertPushable(branch: string) {
|
|
71
|
+
if (this.pushableBranches.includes(branch)) return;
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Branch "${branch}" is not pushable. ${SubspaceConfig.fileName} allows: ${this.pushableBranches.join(", ")}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type ts from "typescript";
|
|
4
|
+
|
|
5
|
+
type TypeScript = typeof ts;
|
|
6
|
+
|
|
7
|
+
export class AsyncDefaultExportDetector {
|
|
8
|
+
static #typescriptLoad: Promise<TypeScript> | undefined;
|
|
9
|
+
|
|
10
|
+
// `typescript` costs ~70 MB resident and this detector is reached from the cli entry through the root
|
|
11
|
+
// layout generator, so the compiler loads on first use rather than at import (`entryModuleGraph.test.ts`).
|
|
12
|
+
static #loadTypescript(): Promise<TypeScript> {
|
|
13
|
+
AsyncDefaultExportDetector.#typescriptLoad ??= import("typescript").then(
|
|
14
|
+
(mod) => (mod.default ?? mod) as TypeScript,
|
|
15
|
+
);
|
|
16
|
+
return AsyncDefaultExportDetector.#typescriptLoad;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static async detect(moduleAbsPath: string): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
const typescript = await AsyncDefaultExportDetector.#loadTypescript();
|
|
22
|
+
const source = fs.readFileSync(path.resolve(moduleAbsPath), "utf8");
|
|
23
|
+
const sourceFile = typescript.createSourceFile(
|
|
24
|
+
moduleAbsPath,
|
|
25
|
+
source,
|
|
26
|
+
typescript.ScriptTarget.Latest,
|
|
27
|
+
true,
|
|
28
|
+
AsyncDefaultExportDetector.#scriptKind(typescript, moduleAbsPath),
|
|
29
|
+
);
|
|
30
|
+
return new AsyncDefaultExportDetector(typescript).detectInSourceFile(sourceFile);
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#ts: TypeScript;
|
|
37
|
+
|
|
38
|
+
constructor(typescript: TypeScript) {
|
|
39
|
+
this.#ts = typescript;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
detectInSourceFile(sourceFile: ts.SourceFile): boolean {
|
|
43
|
+
const ts = this.#ts;
|
|
44
|
+
const asyncBindings = new Map<string, boolean>();
|
|
45
|
+
let defaultIdentifier: string | null = null;
|
|
46
|
+
|
|
47
|
+
for (const statement of sourceFile.statements) {
|
|
48
|
+
if (ts.isFunctionDeclaration(statement)) {
|
|
49
|
+
if (this.#hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
|
|
50
|
+
return this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword);
|
|
51
|
+
}
|
|
52
|
+
if (statement.name) {
|
|
53
|
+
asyncBindings.set(statement.name.text, this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword));
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (ts.isVariableStatement(statement)) {
|
|
59
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
60
|
+
if (!ts.isIdentifier(declaration.name)) continue;
|
|
61
|
+
asyncBindings.set(declaration.name.text, this.#isAsyncFunctionExpression(declaration.initializer));
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (ts.isExportAssignment(statement)) {
|
|
67
|
+
if (this.#isAsyncFunctionExpression(statement.expression)) return true;
|
|
68
|
+
if (ts.isIdentifier(statement.expression)) defaultIdentifier = statement.expression.text;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
73
|
+
const exportClause = statement.exportClause;
|
|
74
|
+
for (const specifier of exportClause.elements) {
|
|
75
|
+
if (specifier.name.text !== "default") continue;
|
|
76
|
+
defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
|
|
85
|
+
const ts = this.#ts;
|
|
86
|
+
return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#isAsyncFunctionExpression(node?: ts.Expression): boolean {
|
|
90
|
+
const ts = this.#ts;
|
|
91
|
+
return Boolean(
|
|
92
|
+
node &&
|
|
93
|
+
(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
|
|
94
|
+
this.#hasModifier(node, ts.SyntaxKind.AsyncKeyword),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static #scriptKind(typescript: TypeScript, moduleAbsPath: string): ts.ScriptKind {
|
|
99
|
+
return moduleAbsPath.endsWith(".tsx") || moduleAbsPath.endsWith(".jsx")
|
|
100
|
+
? typescript.ScriptKind.TSX
|
|
101
|
+
: typescript.ScriptKind.TS;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -223,7 +223,7 @@ const resolveNodePackageExport = async (workspaceRoot: string, specifier: string
|
|
|
223
223
|
const subpath = specifier === packageName ? "." : `.${specifier.slice(packageName.length)}`;
|
|
224
224
|
const exported = resolvePackageExport(pkgJson.exports, subpath);
|
|
225
225
|
const rel = exported ?? (subpath === "." ? (pkgJson.module ?? pkgJson.main ?? "index.js") : null);
|
|
226
|
-
if (!rel
|
|
226
|
+
if (!rel?.startsWith(".")) return null;
|
|
227
227
|
const entryFile = await resolveFileCandidate(path.resolve(pkgDir, rel));
|
|
228
228
|
if (!entryFile) return null;
|
|
229
229
|
const pkgEntryName = specifier;
|
|
@@ -104,7 +104,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
104
104
|
["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
|
|
105
105
|
["Bee", { subpath: "@scope/pkg/leaf", originalName: "B" }],
|
|
106
106
|
]),
|
|
107
|
-
} as BarrelAnalyzer;
|
|
107
|
+
} as unknown as BarrelAnalyzer;
|
|
108
108
|
|
|
109
109
|
const rewritten = await rewriteBarrelImports(
|
|
110
110
|
'import DefaultExport, { A, Bee as LocalBee, type Shape, Missing } from "@scope/pkg";\nconsole.log(A);',
|
|
@@ -128,7 +128,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
128
128
|
test("preserves generated client barrel side effects when flattening app client imports", async () => {
|
|
129
129
|
const analyzer = {
|
|
130
130
|
analyze: async () => new Map([["st", { subpath: "@apps/demo/lib/st", originalName: "st" }]]),
|
|
131
|
-
} as BarrelAnalyzer;
|
|
131
|
+
} as unknown as BarrelAnalyzer;
|
|
132
132
|
|
|
133
133
|
const rewritten = await rewriteBarrelImports(
|
|
134
134
|
'import { st } from "@apps/demo/client";\nvoid st;\n',
|
|
@@ -144,7 +144,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
144
144
|
test("does not rewrite import-looking code inside template literals", async () => {
|
|
145
145
|
const analyzer = {
|
|
146
146
|
analyze: async () => new Map([["AkanApp", { subpath: "akanjs/server/akanApp", originalName: "AkanApp" }]]),
|
|
147
|
-
} as BarrelAnalyzer;
|
|
147
|
+
} as unknown as BarrelAnalyzer;
|
|
148
148
|
|
|
149
149
|
const source = [
|
|
150
150
|
'import { Code } from "@apps/docs/ui";',
|
|
@@ -170,7 +170,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
170
170
|
["AkanOption", { subpath: "akanjs/server/akanOption", originalName: "AkanOption" }],
|
|
171
171
|
["Try", { subpath: "akanjs/server/decorators", originalName: "Try" }],
|
|
172
172
|
]),
|
|
173
|
-
} as BarrelAnalyzer;
|
|
173
|
+
} as unknown as BarrelAnalyzer;
|
|
174
174
|
|
|
175
175
|
const rewritten = await rewriteBarrelImports(
|
|
176
176
|
'import { AkanOption, Try } from "akanjs/server";\nexport const option = new AkanOption();\n',
|
|
@@ -191,7 +191,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
191
191
|
const analyzer = {
|
|
192
192
|
analyze: async () =>
|
|
193
193
|
new Map([["BottomInset", { subpath: "akanjs/ui/Layout/BottomInset", originalName: "BottomInset" }]]),
|
|
194
|
-
} as BarrelAnalyzer;
|
|
194
|
+
} as unknown as BarrelAnalyzer;
|
|
195
195
|
|
|
196
196
|
const rewritten = await rewriteBarrelImports('import { BottomInset } from "akanjs/ui";\n', ["akanjs/ui"], analyzer);
|
|
197
197
|
|
|
@@ -0,0 +1,230 @@
|
|
|
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 { createTsconfigPackageResolver } from "./barrelImportsPlugin";
|
|
6
|
+
|
|
7
|
+
const tempRoots: string[] = [];
|
|
8
|
+
|
|
9
|
+
const makeTempRoot = async () => {
|
|
10
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-resolver-"));
|
|
11
|
+
tempRoots.push(root);
|
|
12
|
+
return root;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const write = async (root: string, relPath: string, content = "export const value = 1;\n") => {
|
|
16
|
+
const filePath = path.join(root, relPath);
|
|
17
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
18
|
+
await writeFile(filePath, content);
|
|
19
|
+
return filePath;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const resolverFor = async (root: string, paths: Record<string, string[]>) =>
|
|
23
|
+
await createTsconfigPackageResolver({
|
|
24
|
+
workspace: { workspaceRoot: root },
|
|
25
|
+
getTsConfig: async () => ({ compilerOptions: { paths } }),
|
|
26
|
+
} as never);
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("createTsconfigPackageResolver — exact tsconfig mapping", () => {
|
|
33
|
+
test("resolves a package barrel and keeps the specifier", async () => {
|
|
34
|
+
const root = await makeTempRoot();
|
|
35
|
+
const entryFile = await write(root, "libs/util/index.ts");
|
|
36
|
+
const resolve = await resolverFor(root, { "@libs/util": ["./libs/util/index.ts"] });
|
|
37
|
+
|
|
38
|
+
expect(await resolve("@libs/util")).toEqual({
|
|
39
|
+
pkgName: "@libs/util",
|
|
40
|
+
entryFile,
|
|
41
|
+
pkgDir: path.join(root, "libs/util"),
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("collapses a facet specifier to its parent package", async () => {
|
|
46
|
+
// `@libs/util/server` maps to the sibling file `libs/util/server.ts`, not to a package directory, so a
|
|
47
|
+
// leaf rewritten under the raw specifier would become `@libs/util/server/lib/sig` — a path that does
|
|
48
|
+
// not exist. The parent specifier is what the `@libs/*` wildcard can actually resolve.
|
|
49
|
+
const root = await makeTempRoot();
|
|
50
|
+
const entryFile = await write(root, "libs/util/server.ts");
|
|
51
|
+
const resolve = await resolverFor(root, { "@libs/util/server": ["./libs/util/server.ts"] });
|
|
52
|
+
|
|
53
|
+
expect(await resolve("@libs/util/server")).toEqual({
|
|
54
|
+
pkgName: "@libs/util",
|
|
55
|
+
entryFile,
|
|
56
|
+
pkgDir: path.join(root, "libs/util"),
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("keeps the specifier when the entry file name does not match the facet", async () => {
|
|
61
|
+
const root = await makeTempRoot();
|
|
62
|
+
const entryFile = await write(root, "libs/util/clientEntry.ts");
|
|
63
|
+
const resolve = await resolverFor(root, { "@libs/util/client": ["./libs/util/clientEntry.ts"] });
|
|
64
|
+
|
|
65
|
+
expect(await resolve("@libs/util/client")).toEqual({
|
|
66
|
+
pkgName: "@libs/util/client",
|
|
67
|
+
entryFile,
|
|
68
|
+
pkgDir: path.join(root, "libs/util"),
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("returns null when the mapped file is missing", async () => {
|
|
73
|
+
const root = await makeTempRoot();
|
|
74
|
+
const resolve = await resolverFor(root, { "@libs/util": ["./libs/util/index.ts"] });
|
|
75
|
+
expect(await resolve("@libs/util")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("returns null for an empty mapping", async () => {
|
|
79
|
+
const root = await makeTempRoot();
|
|
80
|
+
await write(root, "libs/util/index.ts");
|
|
81
|
+
const resolve = await resolverFor(root, { "@libs/util": [""] });
|
|
82
|
+
expect(await resolve("@libs/util")).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("createTsconfigPackageResolver — wildcard tsconfig mapping", () => {
|
|
87
|
+
test("prefers the longer prefix", async () => {
|
|
88
|
+
const root = await makeTempRoot();
|
|
89
|
+
const entryFile = await write(root, "custom/ui/index.ts");
|
|
90
|
+
await write(root, "libs/util/ui/index.ts");
|
|
91
|
+
const resolve = await resolverFor(root, {
|
|
92
|
+
"@libs/*": ["./libs/*"],
|
|
93
|
+
"@libs/util/*": ["./custom/*"],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(await resolve("@libs/util/ui")).toEqual({
|
|
97
|
+
pkgName: "@libs/util/ui",
|
|
98
|
+
entryFile,
|
|
99
|
+
pkgDir: path.join(root, "custom/ui"),
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("collapses a sibling facet file to its parent package", async () => {
|
|
104
|
+
const root = await makeTempRoot();
|
|
105
|
+
const entryFile = await write(root, "apps/minimal/client.ts");
|
|
106
|
+
const resolve = await resolverFor(root, { "@apps/*": ["./apps/*"] });
|
|
107
|
+
|
|
108
|
+
expect(await resolve("@apps/minimal/client")).toEqual({
|
|
109
|
+
pkgName: "@apps/minimal",
|
|
110
|
+
entryFile,
|
|
111
|
+
pkgDir: path.join(root, "apps/minimal"),
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("resolves a directory barrel and keeps the specifier", async () => {
|
|
116
|
+
const root = await makeTempRoot();
|
|
117
|
+
const entryFile = await write(root, "apps/minimal/index.tsx");
|
|
118
|
+
const resolve = await resolverFor(root, { "@apps/*": ["./apps/*"] });
|
|
119
|
+
|
|
120
|
+
expect(await resolve("@apps/minimal")).toEqual({
|
|
121
|
+
pkgName: "@apps/minimal",
|
|
122
|
+
entryFile,
|
|
123
|
+
pkgDir: path.join(root, "apps/minimal"),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("prefers a sibling facet file over a same-named directory barrel", async () => {
|
|
128
|
+
const root = await makeTempRoot();
|
|
129
|
+
const entryFile = await write(root, "apps/minimal/client.ts");
|
|
130
|
+
await write(root, "apps/minimal/client/index.ts");
|
|
131
|
+
const resolve = await resolverFor(root, { "@apps/*": ["./apps/*"] });
|
|
132
|
+
|
|
133
|
+
expect((await resolve("@apps/minimal/client"))?.entryFile).toBe(entryFile);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("stops at the matched prefix instead of falling through to a shorter one", async () => {
|
|
137
|
+
// Falling through would resolve `@libs/util/ui` against `./libs/*`, silently loading an unrelated
|
|
138
|
+
// directory that happens to sit at the same subpath under a different root.
|
|
139
|
+
const root = await makeTempRoot();
|
|
140
|
+
await write(root, "libs/util/ui/index.ts");
|
|
141
|
+
const resolve = await resolverFor(root, {
|
|
142
|
+
"@libs/*": ["./libs/*"],
|
|
143
|
+
"@libs/util/*": ["./custom/*"],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(await resolve("@libs/util/ui")).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("tries every replacement of a matched prefix", async () => {
|
|
150
|
+
const root = await makeTempRoot();
|
|
151
|
+
const entryFile = await write(root, "second/util/index.ts");
|
|
152
|
+
const resolve = await resolverFor(root, { "@libs/*": ["./first/*", "./second/*"] });
|
|
153
|
+
|
|
154
|
+
expect((await resolve("@libs/util"))?.entryFile).toBe(entryFile);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("createTsconfigPackageResolver — node_modules fallback", () => {
|
|
159
|
+
const writePackageJson = async (root: string, pkgName: string, pkgJson: unknown) =>
|
|
160
|
+
await write(root, `node_modules/${pkgName}/package.json`, JSON.stringify(pkgJson));
|
|
161
|
+
|
|
162
|
+
test("resolves an exports subpath and preserves the file path", async () => {
|
|
163
|
+
const root = await makeTempRoot();
|
|
164
|
+
await writePackageJson(root, "akanjs", { exports: { "./ui": { source: "./ui/index.ts" } } });
|
|
165
|
+
const entryFile = await write(root, "node_modules/akanjs/ui/index.ts");
|
|
166
|
+
const resolve = await resolverFor(root, {});
|
|
167
|
+
|
|
168
|
+
expect(await resolve("akanjs/ui")).toEqual({
|
|
169
|
+
pkgName: "akanjs/ui",
|
|
170
|
+
entryFile,
|
|
171
|
+
pkgDir: path.join(root, "node_modules/akanjs/ui"),
|
|
172
|
+
preserveFilePath: true,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("resolves an exports wildcard subpath", async () => {
|
|
177
|
+
const root = await makeTempRoot();
|
|
178
|
+
await writePackageJson(root, "akanjs", { exports: { "./*": "./dist/*.js" } });
|
|
179
|
+
const entryFile = await write(root, "node_modules/akanjs/dist/signal.js");
|
|
180
|
+
const resolve = await resolverFor(root, {});
|
|
181
|
+
|
|
182
|
+
expect((await resolve("akanjs/signal"))?.entryFile).toBe(entryFile);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("resolves a scoped package subpath", async () => {
|
|
186
|
+
// A scoped name is two path segments, so the package boundary cannot be found by splitting on the
|
|
187
|
+
// first slash. Kept vendor-neutral on purpose: `packageExports.test.ts` reads every
|
|
188
|
+
// `"@akanjs/devkit/<x>"` literal under this package as a subpath the monorepo imports.
|
|
189
|
+
const root = await makeTempRoot();
|
|
190
|
+
await writePackageJson(root, "@vendor/kit", { exports: { "./lint": "./lint/index.ts" } });
|
|
191
|
+
const entryFile = await write(root, "node_modules/@vendor/kit/lint/index.ts");
|
|
192
|
+
const resolve = await resolverFor(root, {});
|
|
193
|
+
|
|
194
|
+
expect((await resolve("@vendor/kit/lint"))?.entryFile).toBe(entryFile);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("falls back to module/main when exports resolution declines the specifier", async () => {
|
|
198
|
+
// `main: "index.js"` has no leading `./`, which the exports resolver rejects — the last fallback
|
|
199
|
+
// resolves it against the package directory, and it is the one path that does not preserve the file.
|
|
200
|
+
const root = await makeTempRoot();
|
|
201
|
+
await writePackageJson(root, "plainpkg", { main: "index.js" });
|
|
202
|
+
const entryFile = await write(root, "node_modules/plainpkg/index.js");
|
|
203
|
+
const resolve = await resolverFor(root, {});
|
|
204
|
+
|
|
205
|
+
expect(await resolve("plainpkg")).toEqual({
|
|
206
|
+
pkgName: "plainpkg",
|
|
207
|
+
entryFile,
|
|
208
|
+
pkgDir: path.join(root, "node_modules/plainpkg"),
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("returns null when the package is not installed", async () => {
|
|
213
|
+
const root = await makeTempRoot();
|
|
214
|
+
expect(await resolverFor(root, {}).then((resolve) => resolve("missingpkg"))).toBeNull();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("returns null when the package.json is unparseable", async () => {
|
|
218
|
+
const root = await makeTempRoot();
|
|
219
|
+
await write(root, "node_modules/brokenpkg/package.json", "{ not json");
|
|
220
|
+
const resolve = await resolverFor(root, {});
|
|
221
|
+
expect(await resolve("brokenpkg")).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("returns null when the resolved entry file is missing", async () => {
|
|
225
|
+
const root = await makeTempRoot();
|
|
226
|
+
await writePackageJson(root, "ghostpkg", { module: "./dist/index.js" });
|
|
227
|
+
const resolve = await resolverFor(root, {});
|
|
228
|
+
expect(await resolve("ghostpkg")).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
});
|
package/tsconfig.json
CHANGED
package/typeChecker.ts
CHANGED
package/types.ts
CHANGED
package/ui/ScrollList.tsx
CHANGED
|
@@ -83,14 +83,12 @@ export const ScrollList = ({ list, ...props }: ScrollListProps) => {
|
|
|
83
83
|
<Text backgroundColor="green">scrolling... + {scrollPos}</Text>
|
|
84
84
|
</>
|
|
85
85
|
) : (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
))}
|
|
93
|
-
</>
|
|
86
|
+
renderLogs.map((log, index) => (
|
|
87
|
+
<>
|
|
88
|
+
<Text key={index}>{log}</Text>
|
|
89
|
+
<Newline />
|
|
90
|
+
</>
|
|
91
|
+
))
|
|
94
92
|
)}
|
|
95
93
|
</Text>
|
|
96
94
|
</Box>
|
package/uploadRelease.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Logger, RestClient } from "akanjs/common";
|
|
2
2
|
|
|
3
3
|
import { Spinner } from "./spinner";
|
|
4
4
|
|
|
@@ -26,7 +26,7 @@ export const uploadRelease = async (
|
|
|
26
26
|
) => {
|
|
27
27
|
const logger = new Logger("uploadRelease");
|
|
28
28
|
const basePath = local ? "http://localhost:8282/backend" : "https://cloud.akanjs.com/backend";
|
|
29
|
-
const httpClient = new
|
|
29
|
+
const httpClient = new RestClient(basePath);
|
|
30
30
|
const buildPath = `${workspaceRoot}/releases/builds/${appName}-release.tar.gz`;
|
|
31
31
|
const appBuildPath = `${workspaceRoot}/releases/builds/${appName}-appBuild.zip`;
|
|
32
32
|
const sourcePath = `${workspaceRoot}/releases/sources/${appName}-source.tar.gz`;
|