@akanjs/devkit 3.0.0-beta.19 → 3.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +1 -4
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +1 -3
- package/akanApp/akanApp.host.ts +64 -250
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +30 -39
- package/akanConfig/akanConfig.ts +40 -136
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.ts +25 -59
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +3 -19
- package/artifact/implicitRootLayout.ts +34 -91
- package/artifact/routeSeedIndex.ts +11 -44
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +4 -14
- package/capacitorApp.ts +86 -235
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +33 -57
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +3 -2
- package/cloud/globalConfig.ts +64 -31
- package/codeAgent/agent/AkanCodeServices.ts +15 -57
- package/codeAgent/agent/AkanContextFiles.ts +5 -28
- package/codeAgent/agent/CodeAgent.ts +33 -108
- package/codeAgent/agent/CodeAgentAsks.ts +7 -30
- package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
- package/codeAgent/agent/CodeAgentGate.ts +8 -34
- package/codeAgent/agent/CodeAgentProxy.ts +4 -13
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
- package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
- package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
- package/codeAgent/agent/CodeAgentUi.ts +2 -7
- package/codeAgent/agent/CodeMailbox.ts +7 -34
- package/codeAgent/agent/CodeSessionFork.ts +4 -19
- package/codeAgent/agent/CodeSessionIndex.ts +5 -31
- package/codeAgent/agent/SubagentPool.ts +48 -58
- package/codeAgent/agent/akanCodeModel.ts +5 -39
- package/codeAgent/agent/akanCodePaths.ts +4 -24
- package/codeAgent/agent/akanContextFiles.test.ts +0 -2
- package/codeAgent/agent/akanSystemPrompt.ts +1 -9
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +5 -28
- package/codeAgent/agent/codeMailbox.test.ts +0 -4
- package/codeAgent/agent/codeSessionFork.test.ts +0 -2
- package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
- package/codeAgent/agent/consoleToStderr.ts +2 -12
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +3 -18
- package/codeAgent/feedback/PreviewChrome.ts +5 -29
- package/codeAgent/feedback/PreviewFeedback.ts +2 -9
- package/codeAgent/feedback/PreviewView.ts +3 -15
- package/codeAgent/feedback/TurnFeedback.ts +2 -17
- package/codeAgent/feedback/VerifyFeedback.ts +1 -7
- package/codeAgent/feedback/previewChrome.test.ts +1 -1
- package/codeAgent/index.ts +2 -10
- package/codeAgent/skills/akan-code/SKILL.md +12 -4
- package/codeAgent/tools/AkanCli.ts +4 -16
- package/codeAgent/tools/AkanCodePlugins.ts +21 -43
- package/codeAgent/tools/AkanEditScope.ts +2 -12
- package/codeAgent/tools/AkanEnvKeys.ts +3 -11
- package/codeAgent/tools/AkanToolPack.ts +6 -28
- package/codeAgent/tools/AkanVerifier.ts +1 -8
- package/codeAgent/tools/AskToolPack.ts +3 -15
- package/codeAgent/tools/MailToolPack.ts +0 -9
- package/codeAgent/tools/McpClient.ts +3 -24
- package/codeAgent/tools/McpOAuth.ts +6 -41
- package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
- package/codeAgent/tools/McpServerConfig.ts +7 -26
- package/codeAgent/tools/McpSignIn.ts +46 -49
- package/codeAgent/tools/McpTokenStore.ts +6 -17
- package/codeAgent/tools/McpToolPack.ts +107 -35
- package/codeAgent/tools/SessionToolPack.ts +1 -11
- package/codeAgent/tools/WebToolPack.ts +1 -7
- package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
- package/codeAgent/tools/askToolPack.test.ts +0 -3
- package/codeAgent/tools/mcpOAuth.test.ts +315 -27
- package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +44 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +1 -9
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +66 -144
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +27 -78
- package/devkitUtils.test.ts +47 -19
- package/executors.test.ts +114 -60
- package/executors.ts +165 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +1 -1
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +14 -38
- package/frontendBuild/autoImportSync.ts +15 -67
- package/frontendBuild/buildRouteClient.test.ts +10 -36
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- package/frontendBuild/cssImportResolver.ts +5 -18
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +5 -13
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +27 -52
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +7 -31
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +7 -47
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/devWatchBatch.test.ts +14 -18
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
- package/index.ts +3 -15
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +9 -29
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/gritRules.test.ts +6 -23
- package/linter.test.ts +1 -2
- package/linter.ts +22 -115
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +2 -2
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +3 -14
- package/platformTest/IdleWatchedProcess.test.ts +5 -13
- package/platformTest/IdleWatchedProcess.ts +1 -5
- package/platformTest/LinuxDockerTarget.ts +20 -35
- package/platformTest/PlatformTestRun.test.ts +5 -9
- package/platformTest/PlatformTestTarget.ts +24 -2
- package/platformTest/TestSnapshot.test.ts +6 -14
- package/platformTest/TestSnapshot.ts +1 -5
- package/platformTest/WindowsSshTarget.ts +6 -22
- package/prompter.ts +3 -6
- package/qualityScanner.test.ts +6 -24
- package/qualityScanner.ts +196 -284
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.ts +7 -30
- package/scanInfo.ts +82 -214
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/subspace.test.ts +6 -25
- package/subspace.ts +31 -109
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +49 -34
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +85 -146
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/source.ts +33 -56
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
package/fileEditor.ts
CHANGED
|
@@ -17,57 +17,29 @@ export class FileEditor {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
find(pattern: string | RegExp): number {
|
|
20
|
-
const lines = this.#content.split("\n");
|
|
21
20
|
const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
|
|
22
|
-
|
|
23
|
-
for (let i = 0; i < lines.length; i++) {
|
|
24
|
-
const line = lines[i];
|
|
25
|
-
if (!line) continue;
|
|
26
|
-
if (regex.test(line)) return i;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return -1;
|
|
21
|
+
return this.#content.split("\n").findIndex((line) => !!line && regex.test(line));
|
|
30
22
|
}
|
|
31
23
|
|
|
32
24
|
findAll(pattern: string | RegExp): number[] {
|
|
33
|
-
const lines = this.#content.split("\n");
|
|
34
25
|
const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (let i = 0; i < lines.length; i++) {
|
|
38
|
-
const line = lines[i];
|
|
39
|
-
if (!line) continue;
|
|
40
|
-
if (regex.test(line)) matches.push(i);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return matches;
|
|
26
|
+
return this.#content.split("\n").flatMap((line, index) => (line && regex.test(line) ? [index] : []));
|
|
44
27
|
}
|
|
45
28
|
|
|
46
29
|
insertAfter(pattern: string | RegExp, data: string): this {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (lineIndex === -1) {
|
|
50
|
-
throw new Error(`Pattern not found: ${pattern}`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const lines = this.#content.split("\n");
|
|
54
|
-
lines.splice(lineIndex + 1, 0, data);
|
|
55
|
-
this.#content = lines.join("\n");
|
|
56
|
-
|
|
57
|
-
return this;
|
|
30
|
+
return this.#insertAt(pattern, data, 1);
|
|
58
31
|
}
|
|
59
32
|
|
|
60
33
|
insertBefore(pattern: string | RegExp, data: string): this {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (lineIndex === -1) {
|
|
64
|
-
throw new Error(`Pattern not found: ${pattern}`);
|
|
65
|
-
}
|
|
34
|
+
return this.#insertAt(pattern, data, 0);
|
|
35
|
+
}
|
|
66
36
|
|
|
37
|
+
#insertAt(pattern: string | RegExp, data: string, offset: number): this {
|
|
38
|
+
const lineIndex = this.find(pattern);
|
|
39
|
+
if (lineIndex === -1) throw new Error(`Pattern not found: ${pattern}`);
|
|
67
40
|
const lines = this.#content.split("\n");
|
|
68
|
-
lines.splice(lineIndex, 0, data);
|
|
41
|
+
lines.splice(lineIndex + offset, 0, data);
|
|
69
42
|
this.#content = lines.join("\n");
|
|
70
|
-
|
|
71
43
|
return this;
|
|
72
44
|
}
|
|
73
45
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { getDirname } from "./
|
|
4
|
+
import { getDirname } from "./fileSys";
|
|
5
5
|
|
|
6
6
|
describe("getDirname", () => {
|
|
7
7
|
test("converts file URLs to filesystem paths", () => {
|
package/fileSys.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { lstat, rename, rm, stat } from "node:fs/promises";
|
|
2
|
+
import nodePath from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
import { Logger } from "akanjs/common";
|
|
3
5
|
|
|
6
|
+
export const getDirname = (url: string) => nodePath.dirname(fileURLToPath(url));
|
|
7
|
+
|
|
4
8
|
export class FileSys {
|
|
5
9
|
static logger = new Logger("FileSys");
|
|
6
10
|
static async fileExists(path: string) {
|
|
@@ -39,14 +43,8 @@ export class FileSys {
|
|
|
39
43
|
static async writeText(path: string, content: string) {
|
|
40
44
|
return await Bun.file(path).write(content);
|
|
41
45
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* Generated barrels are written by every dev server watching the same `libs/` tree while those same
|
|
45
|
-
* servers' watchers are reading them, and a half-written barrel reads back as a user edit.
|
|
46
|
-
*
|
|
47
|
-
* The temp file is a sibling because `rename` is only atomic within a filesystem, and it lands in a
|
|
48
|
-
* watched directory — the `.tmp` suffix is what `HmrChangeClassifier` ignores it by.
|
|
49
|
-
*/
|
|
46
|
+
// One `rename`, so a watcher never reads a half-written barrel as a user edit. The temp is a sibling (rename is atomic
|
|
47
|
+
// only within a filesystem), and its `.tmp` suffix is what `HmrChangeClassifier` ignores it by.
|
|
50
48
|
static async writeTextAtomic(filePath: string, content: string) {
|
|
51
49
|
const temp = `${filePath}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
52
50
|
try {
|
package/formSetterScanner.ts
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import type { QualityWarning, SourceFileInfo } from "./qualityScanner";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* A control handed `onChange={st.do.setTitleOnTask}` **by reference** names the field it writes, so it emits
|
|
8
|
-
* `data-akan-action` and `useFieldTool` publishes the field to the in-page agent. Any wrapper around that setter is
|
|
9
|
-
* an anonymous closure carrying neither, and the field goes quiet — for the agent, for an E2E selector, and for the
|
|
10
|
-
* accessibility tree.
|
|
11
|
-
*
|
|
12
|
-
* `no-unpublished-form-setter.grit` is the per-line enforcement, and it fires only on a pure forwarding wrapper,
|
|
13
|
-
* because every other shape has a legitimate reading and a lint error would be wrong. This is the other half: the
|
|
14
|
-
* per-file count of fields that ended up unreachable whatever the reason, which is the number worth watching. A
|
|
15
|
-
* warning rather than an error for the same reason — the remedy depends on why the wrapper is there.
|
|
16
|
-
*
|
|
17
|
-
* Only a handler that takes a parameter is counted. A zero-parameter handler (`onClick={() => st.do.setStatusOnUser
|
|
18
|
-
* ("active")}`) is a button setting a constant, not a form control, and its remedy is an `st.tool` beside it.
|
|
19
|
-
*/
|
|
4
|
+
// Only a setter passed by reference publishes its field (`data-akan-action`, agent tool). The grit rule flags only pure
|
|
5
|
+
// forwarding wrappers; this counts every field left unpublished whatever the reason. A zero-parameter handler is a
|
|
6
|
+
// button setting a constant, not a form control, so it is not counted.
|
|
20
7
|
export class FormSetterScanner {
|
|
21
8
|
static #fieldSetter = /^set[A-Za-z0-9_$]*On[A-Za-z0-9_$]*$/;
|
|
22
9
|
|
|
@@ -14,11 +14,6 @@ export interface BuildAllRoutesResult {
|
|
|
14
14
|
seedIndex: RouteSeedIndex;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Walk every route in `pages` and produce its client bundle up-front.
|
|
19
|
-
* Intended to run under `akan build` (production) so the serve path
|
|
20
|
-
* never needs to compile.
|
|
21
|
-
*/
|
|
22
17
|
export class AllRoutesBuilder {
|
|
23
18
|
#app: App;
|
|
24
19
|
#artifact: BaseBuildArtifact;
|
|
@@ -47,23 +42,16 @@ export class AllRoutesBuilder {
|
|
|
47
42
|
this.#app.verbose(`[build-all] discovered ${seedIndex.entries.length} routes`);
|
|
48
43
|
this.#discovery = await GraphClientEntryDiscovery.create(this.#app);
|
|
49
44
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
// Discovery is cached and does no bundling, so collecting every entry up front costs almost nothing.
|
|
53
|
-
const allEntries: string[] = [];
|
|
54
|
-
const seen = new Set<string>();
|
|
45
|
+
// Every route's entries go into one Bun.build: chunk splitting only dedupes within a single build.
|
|
46
|
+
const allEntries = new Set<string>();
|
|
55
47
|
for (const entry of seedIndex.entries) {
|
|
56
48
|
const seeds = Array.from(new Set([...seedIndex.globalLayoutFiles, ...entry.seeds]));
|
|
57
|
-
for (const discovered of await this.#discovery.discover(seeds))
|
|
58
|
-
if (seen.has(discovered)) continue;
|
|
59
|
-
seen.add(discovered);
|
|
60
|
-
allEntries.push(discovered);
|
|
61
|
-
}
|
|
49
|
+
for (const discovered of await this.#discovery.discover(seeds)) allEntries.add(discovered);
|
|
62
50
|
this.#routeIds.push(entry.routeId);
|
|
63
51
|
}
|
|
64
|
-
this.#app.verbose(`[build-all] ${allEntries.
|
|
52
|
+
this.#app.verbose(`[build-all] ${allEntries.size} client entries across ${this.#routeIds.length} routes`);
|
|
65
53
|
|
|
66
|
-
const delta = await this.#buildEntries(allEntries);
|
|
54
|
+
const delta = await this.#buildEntries([...allEntries]);
|
|
67
55
|
this.#mergeDelta(delta);
|
|
68
56
|
this.#merged.knownEntries = Array.from(this.#knownSet);
|
|
69
57
|
|
|
@@ -113,10 +101,8 @@ export class AllRoutesBuilder {
|
|
|
113
101
|
}
|
|
114
102
|
|
|
115
103
|
#mergeDelta(delta: BuildRouteClientResult): void {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.#merged.ssrManifest.moduleMap[url] = byName;
|
|
119
|
-
}
|
|
104
|
+
Object.assign(this.#merged.clientManifest, delta.manifestDelta);
|
|
105
|
+
Object.assign(this.#merged.ssrManifest.moduleMap, delta.ssrManifestDelta.moduleMap);
|
|
120
106
|
for (const abs of delta.newEntries) this.#knownSet.add(abs);
|
|
121
107
|
}
|
|
122
108
|
}
|
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { tempDirs } from "../testHelpers";
|
|
5
5
|
import { AutoImportSync, transformSource } from "./autoImportSync";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const makeTempRoot = tempDirs("akan-auto-import-");
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
const seed = async (root: string, rel: string, content: string) => {
|
|
10
|
+
const abs = path.join(root, rel);
|
|
11
|
+
await mkdir(path.dirname(abs), { recursive: true });
|
|
12
|
+
await writeFile(abs, content);
|
|
13
|
+
return abs;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
afterEach(async () => {
|
|
16
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
17
|
-
});
|
|
18
|
-
|
|
19
16
|
const constantCtx = { role: "constant", scope: "libs", project: "shared" } as const;
|
|
20
17
|
const documentCtx = { role: "document", scope: "libs", project: "shared" } as const;
|
|
21
18
|
const serviceCtx = { role: "service", scope: "libs", project: "shared" } as const;
|
|
@@ -184,13 +181,6 @@ describe("transformSource — client role (package client entry)", () => {
|
|
|
184
181
|
});
|
|
185
182
|
|
|
186
183
|
describe("AutoImportSync.syncForBatch", () => {
|
|
187
|
-
const seed = async (root: string, rel: string, content: string) => {
|
|
188
|
-
const abs = path.join(root, rel);
|
|
189
|
-
await mkdir(path.dirname(abs), { recursive: true });
|
|
190
|
-
await writeFile(abs, content);
|
|
191
|
-
return abs;
|
|
192
|
-
};
|
|
193
|
-
|
|
194
184
|
test("writes the fix once, then is idempotent", async () => {
|
|
195
185
|
const root = await makeTempRoot();
|
|
196
186
|
const abs = await seed(
|
|
@@ -309,9 +299,7 @@ describe("AutoImportSync.syncForBatch", () => {
|
|
|
309
299
|
|
|
310
300
|
test("leaves JS globals and ambiguous domain names alone", async () => {
|
|
311
301
|
const root = await makeTempRoot();
|
|
312
|
-
// `Map` is a real model here but must not shadow the JS global.
|
|
313
302
|
await seed(root, "libs/x/lib/map/map.constant.ts", `export class Map extends via {}\n`);
|
|
314
|
-
// `Dup` is exported by two files → ambiguous → skipped.
|
|
315
303
|
await seed(root, "libs/x/lib/a/a.constant.ts", `export class Dup {}\n`);
|
|
316
304
|
await seed(root, "libs/x/lib/b/b.constant.ts", `export class Dup {}\n`);
|
|
317
305
|
const consumer = await seed(
|
|
@@ -348,7 +336,7 @@ describe("AutoImportSync.syncForBatch", () => {
|
|
|
348
336
|
|
|
349
337
|
test("srvkit: does not import a barrel whose file is absent", async () => {
|
|
350
338
|
const root = await makeTempRoot();
|
|
351
|
-
await seed(root, "libs/x/lib/dict.ts", `export const x = 1;\n`);
|
|
339
|
+
await seed(root, "libs/x/lib/dict.ts", `export const x = 1;\n`);
|
|
352
340
|
const file = await seed(root, "libs/x/srvkit/api.ts", `export const f = () => Err.of(srv.user);\n`);
|
|
353
341
|
const sync = new AutoImportSync({ workspaceRoot: root });
|
|
354
342
|
|
|
@@ -370,20 +358,11 @@ describe("AutoImportSync.syncForBatch", () => {
|
|
|
370
358
|
});
|
|
371
359
|
|
|
372
360
|
describe("AutoImportSync — scope", () => {
|
|
373
|
-
const writeAt = async (root: string, rel: string, source: string) => {
|
|
374
|
-
const abs = path.join(root, rel);
|
|
375
|
-
await mkdir(path.dirname(abs), { recursive: true });
|
|
376
|
-
await writeFile(abs, source);
|
|
377
|
-
return abs;
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
// `lib/__lib/` is generated and gitignored: `akan sync` rewrites it, which is also what makes the
|
|
381
|
-
// watcher hand it to the syncer, so an edit there is churn that the next sync discards.
|
|
382
361
|
test("leaves the generated lib/__lib stubs alone and still syncs lib/__scalar", async () => {
|
|
383
362
|
const root = await makeTempRoot();
|
|
384
363
|
const needsImport = "export class A {\n count = field(Int);\n}\n";
|
|
385
|
-
const generated = await
|
|
386
|
-
const scalar = await
|
|
364
|
+
const generated = await seed(root, "libs/shared/lib/__lib/lib.constant.ts", needsImport);
|
|
365
|
+
const scalar = await seed(root, "libs/shared/lib/__scalar/money/money.constant.ts", needsImport);
|
|
387
366
|
|
|
388
367
|
const result = await new AutoImportSync({ workspaceRoot: root }).syncForBatch([generated, scalar]);
|
|
389
368
|
|
|
@@ -393,11 +372,9 @@ describe("AutoImportSync — scope", () => {
|
|
|
393
372
|
expect(await readFile(scalar, "utf8")).toContain('import { Int } from "akanjs/base";');
|
|
394
373
|
});
|
|
395
374
|
|
|
396
|
-
// The transform is pure and the write is conditional on a difference, so a second pass over its own
|
|
397
|
-
// output writes nothing — without that the watcher would re-fire on the syncer's own edit.
|
|
398
375
|
test("a second pass over its own output changes nothing", async () => {
|
|
399
376
|
const root = await makeTempRoot();
|
|
400
|
-
const file = await
|
|
377
|
+
const file = await seed(
|
|
401
378
|
root,
|
|
402
379
|
"libs/shared/lib/task/task.constant.ts",
|
|
403
380
|
"export class A {\n count = field(Int);\n}\n",
|
|
@@ -410,11 +387,10 @@ describe("AutoImportSync — scope", () => {
|
|
|
410
387
|
expect(await readFile(file, "utf8")).toBe(afterFirst);
|
|
411
388
|
});
|
|
412
389
|
|
|
413
|
-
// One try/catch per file: a file that cannot be read is reported and the rest of the batch still runs.
|
|
414
390
|
test("a failing file is reported without abandoning the batch", async () => {
|
|
415
391
|
const root = await makeTempRoot();
|
|
416
392
|
const missing = path.join(root, "libs/shared/lib/task/gone.constant.ts");
|
|
417
|
-
const good = await
|
|
393
|
+
const good = await seed(
|
|
418
394
|
root,
|
|
419
395
|
"libs/shared/lib/task/task.constant.ts",
|
|
420
396
|
"export class A {\n count = field(Int);\n}\n",
|
|
@@ -3,11 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import ts from "typescript";
|
|
4
4
|
import { FileSys } from "../fileSys";
|
|
5
5
|
|
|
6
|
-
//*
|
|
7
|
-
//* framework symbols that are used but not imported (e.g. `Int` in a *.constant.ts, `fetch` in a *.store.ts)
|
|
8
|
-
//* are inserted automatically. Detection is a closed-registry scan: we only look for a fixed, framework-owned
|
|
9
|
-
//* set of identifiers per file role, so no type information is required. Writes only happen on a real diff,
|
|
10
|
-
//* which keeps the watcher from looping on its own edits.
|
|
6
|
+
//* Closed-registry scan (no type information); writes only on a real diff, or the watcher loops on its own edits.
|
|
11
7
|
|
|
12
8
|
export interface AutoImportSyncResult {
|
|
13
9
|
changedFiles: string[];
|
|
@@ -39,13 +35,9 @@ interface FileContext {
|
|
|
39
35
|
interface ImportTarget {
|
|
40
36
|
specifier: string;
|
|
41
37
|
kind: ImportKind;
|
|
42
|
-
//* When true, emitted as a type-only import (`import type { X }`, or inline `type X` when mixed with
|
|
43
|
-
//* value names from the same specifier). Only meaningful for `kind: "named"`.
|
|
44
38
|
typeOnly?: boolean;
|
|
45
39
|
}
|
|
46
40
|
|
|
47
|
-
//* A registry rule: the identifiers `names` should be imported from `specifier` with the given `kind`.
|
|
48
|
-
//* `specifier` may depend on the file's package (e.g. the client entrypoint).
|
|
49
41
|
interface ImportRule {
|
|
50
42
|
names: string[];
|
|
51
43
|
specifier: string | ((ctx: FileContext) => string);
|
|
@@ -55,23 +47,19 @@ interface ImportRule {
|
|
|
55
47
|
|
|
56
48
|
const TEST_FILE_RE = /\.(test|spec)\.(ts|tsx)$/;
|
|
57
49
|
|
|
58
|
-
//*
|
|
59
|
-
//* relative barrels `../cnst`, `../db`, `../srv`, `../dict` are always correct for them.
|
|
50
|
+
//* Domain files sit at a fixed depth (`lib/<model>/`), so the relative barrels below are always correct for them.
|
|
60
51
|
const AKAN_BASE: ImportRule = {
|
|
61
52
|
names: ["Int", "Float", "ID", "Any", "Upload", "enumOf", "dayjs"],
|
|
62
53
|
specifier: "akanjs/base",
|
|
63
54
|
kind: "named",
|
|
64
55
|
};
|
|
65
|
-
//* Type-only exports of akanjs/base — emitted as `type` imports (e.g. `import { type Dayjs, dayjs }`).
|
|
66
56
|
const AKAN_BASE_TYPES: ImportRule = { names: ["Dayjs"], specifier: "akanjs/base", kind: "named", typeOnly: true };
|
|
67
57
|
const CNST_NS: ImportRule = { names: ["cnst"], specifier: "../cnst", kind: "namespace" };
|
|
68
58
|
const DB_NS: ImportRule = { names: ["db"], specifier: "../db", kind: "namespace" };
|
|
69
59
|
const SRV_NS: ImportRule = { names: ["srv"], specifier: "../srv", kind: "namespace" };
|
|
70
60
|
const ERR_DICT: ImportRule = { names: ["Err"], specifier: "../dict", kind: "named" };
|
|
71
61
|
|
|
72
|
-
//*
|
|
73
|
-
//* only high-confidence, framework-owned identifiers are listed (domain model/scalar refs are resolved
|
|
74
|
-
//* separately). Order matters only when the same name could appear twice — keep names unique per role.
|
|
62
|
+
//* Keep names unique per role: targetFor takes the first rule that lists a name.
|
|
75
63
|
const RULES: Record<FileRole, ImportRule[]> = {
|
|
76
64
|
constant: [AKAN_BASE, AKAN_BASE_TYPES, { names: ["via"], specifier: "akanjs/constant", kind: "named" }],
|
|
77
65
|
document: [
|
|
@@ -143,8 +131,6 @@ const RULES: Record<FileRole, ImportRule[]> = {
|
|
|
143
131
|
],
|
|
144
132
|
};
|
|
145
133
|
|
|
146
|
-
//* Domain imports resolve open-ended model/scalar class refs against a per-package index, rather than
|
|
147
|
-
//* a fixed registry. Only these roles opt in, and each may pull from the listed sibling file kinds.
|
|
148
134
|
type DomainKind = "constant" | "document" | "signal";
|
|
149
135
|
type DomainIndex = Map<string, { file: string; kind: DomainKind }[]>;
|
|
150
136
|
const PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
|
|
@@ -153,21 +139,17 @@ const DOMAIN_ROLE_KINDS: Partial<Record<FileRole, DomainKind[]>> = {
|
|
|
153
139
|
dictionary: ["constant", "document", "signal"],
|
|
154
140
|
common: ["constant"],
|
|
155
141
|
};
|
|
156
|
-
//*
|
|
157
|
-
//* Their specifier depth is per-file (see `#libBarrelResolver`), so they can't live in the RULES table.
|
|
142
|
+
//* srvkit/common import the lib barrels from a per-file depth, so they cannot live in the RULES table.
|
|
158
143
|
const LIB_BARRELS: Record<string, { barrel: string; kind: ImportKind }> = {
|
|
159
144
|
Err: { barrel: "dict", kind: "named" },
|
|
160
145
|
db: { barrel: "db", kind: "namespace" },
|
|
161
146
|
cnst: { barrel: "cnst", kind: "namespace" },
|
|
162
147
|
srv: { barrel: "srv", kind: "namespace" },
|
|
163
148
|
};
|
|
164
|
-
//*
|
|
165
|
-
//*
|
|
166
|
-
//* (`fetch.viewX()`); one bare use leaves the file alone.
|
|
149
|
+
//* Importing Akan's `fetch` over the platform global silently rebinds `fetch(url)`, so such a name is claimed
|
|
150
|
+
//* only when every reference is member-shaped (`fetch.viewX()`).
|
|
167
151
|
const GLOBAL_NAMES = new Set(["fetch"]);
|
|
168
|
-
//*
|
|
169
|
-
//* e.g. `Map` cannot shadow the JS `Map` used in `new Map()`. Domain-y globals like `File` are allowed
|
|
170
|
-
//* on purpose (they are real models here).
|
|
152
|
+
//* Structural globals a package model must not shadow (`new Map()`); `File` is left out on purpose — it is a real model.
|
|
171
153
|
const DOMAIN_DENYLIST = new Set([
|
|
172
154
|
"Map",
|
|
173
155
|
"Set",
|
|
@@ -208,7 +190,6 @@ const DOMAIN_DENYLIST = new Set([
|
|
|
208
190
|
|
|
209
191
|
export class AutoImportSync {
|
|
210
192
|
readonly #workspaceRoot: string;
|
|
211
|
-
//* Per-package domain index (symbol -> source file), invalidated when a package's model file changes.
|
|
212
193
|
readonly #domainCache = new Map<string, DomainIndex>();
|
|
213
194
|
|
|
214
195
|
constructor({ workspaceRoot }: AutoImportSyncOptions) {
|
|
@@ -220,8 +201,6 @@ export class AutoImportSync {
|
|
|
220
201
|
const errors: string[] = [];
|
|
221
202
|
const seen = new Set<string>();
|
|
222
203
|
|
|
223
|
-
// Drop cached domain indexes for any package whose model files changed in this batch, so a newly
|
|
224
|
-
// added/renamed model is visible to references resolved later in the same batch.
|
|
225
204
|
for (const file of files) {
|
|
226
205
|
const pkgRoot = this.#domainPkgRootOf(file);
|
|
227
206
|
if (pkgRoot) this.#domainCache.delete(pkgRoot);
|
|
@@ -244,8 +223,6 @@ export class AutoImportSync {
|
|
|
244
223
|
return { changedFiles, errors };
|
|
245
224
|
}
|
|
246
225
|
|
|
247
|
-
//* Resolve the role + package location for a file, or null when the file is out of scope
|
|
248
|
-
//* (outside apps/libs, a generated/declaration/test file, or a facet+extension we do not handle).
|
|
249
226
|
#contextFor(abs: string): FileContext | null {
|
|
250
227
|
const rel = path.relative(this.#workspaceRoot, abs);
|
|
251
228
|
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
@@ -255,9 +232,7 @@ export class AutoImportSync {
|
|
|
255
232
|
const parts = rel.split(path.sep).filter(Boolean);
|
|
256
233
|
const [scope, project, facet] = parts;
|
|
257
234
|
if ((scope !== "apps" && scope !== "libs") || !project || !facet) return null;
|
|
258
|
-
//* `lib/__lib/`
|
|
259
|
-
//* `akan sync`, which is also what makes the watcher report them. An edit there is churn at best and
|
|
260
|
-
//* silently discarded at worst. `lib/__scalar/` is real source and stays in scope.
|
|
235
|
+
//* `lib/__lib/` is rewritten by every `akan sync`, so an edit there is discarded; `lib/__scalar/` is real source.
|
|
261
236
|
if (facet === "lib" && parts[3] === "__lib") return null;
|
|
262
237
|
const role = roleFor(facet, base);
|
|
263
238
|
if (!role) return null;
|
|
@@ -276,10 +251,6 @@ export class AutoImportSync {
|
|
|
276
251
|
return true;
|
|
277
252
|
}
|
|
278
253
|
|
|
279
|
-
//* Build the dynamic resolver for identifiers the static registry does not cover, or undefined when
|
|
280
|
-
//* the role has none. `srvkit`/`common` resolve the package's generated lib barrels (variable depth);
|
|
281
|
-
//* the domain roles resolve open-ended model/scalar refs against the per-package index. `common`
|
|
282
|
-
//* uses both — barrels first, then domain (the two symbol sets are disjoint).
|
|
283
254
|
async #extraResolverFor(abs: string, ctx: FileContext) {
|
|
284
255
|
const resolvers: ((symbol: string) => ImportTarget | null)[] = [];
|
|
285
256
|
if (ctx.role === "srvkit" || ctx.role === "common") resolvers.push(await this.#libBarrelResolver(abs, ctx));
|
|
@@ -295,22 +266,17 @@ export class AutoImportSync {
|
|
|
295
266
|
};
|
|
296
267
|
}
|
|
297
268
|
|
|
298
|
-
//* Resolve an unbound PascalCase identifier to a relative import of the package model/scalar that
|
|
299
|
-
//* exports it (only when exactly one package file of an allowed kind exports that name).
|
|
300
269
|
async #domainResolver(abs: string, ctx: FileContext, kinds: DomainKind[]) {
|
|
301
270
|
const index = await this.#domainIndex(path.join(this.#workspaceRoot, ctx.scope, ctx.project));
|
|
302
271
|
const fileDir = path.dirname(abs);
|
|
303
272
|
return (symbol: string): ImportTarget | null => {
|
|
304
273
|
if (!PASCAL_CASE_RE.test(symbol) || DOMAIN_DENYLIST.has(symbol)) return null;
|
|
305
274
|
const entries = (index.get(symbol) ?? []).filter((entry) => kinds.includes(entry.kind));
|
|
306
|
-
if (entries.length !== 1) return null;
|
|
275
|
+
if (entries.length !== 1) return null;
|
|
307
276
|
return { specifier: relativeSpecifier(fileDir, entries[0].file), kind: "named" };
|
|
308
277
|
};
|
|
309
278
|
}
|
|
310
279
|
|
|
311
|
-
//* srvkit/common files import the package's generated lib barrels from a variable depth
|
|
312
|
-
//* (`../lib/dict`, `../../lib/dict`, …). Resolve each barrel name to a path relative to this file,
|
|
313
|
-
//* but only for barrels that actually exist (so an absent `lib/srv.ts` never yields a broken import).
|
|
314
280
|
async #libBarrelResolver(abs: string, ctx: FileContext) {
|
|
315
281
|
const fileDir = path.dirname(abs);
|
|
316
282
|
const libDir = path.join(this.#workspaceRoot, ctx.scope, ctx.project, "lib");
|
|
@@ -330,7 +296,6 @@ export class AutoImportSync {
|
|
|
330
296
|
return index;
|
|
331
297
|
}
|
|
332
298
|
|
|
333
|
-
//* The apps/libs package root for a model file (`*.constant/document/signal.ts` under `lib/`), else null.
|
|
334
299
|
#domainPkgRootOf(file: string): string | null {
|
|
335
300
|
const rel = path.relative(this.#workspaceRoot, path.resolve(file));
|
|
336
301
|
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
@@ -341,9 +306,6 @@ export class AutoImportSync {
|
|
|
341
306
|
}
|
|
342
307
|
}
|
|
343
308
|
|
|
344
|
-
//* Which facet+extension combinations get auto-imports, and the role that drives their registry.
|
|
345
|
-
//* `client` covers every file that pulls domain symbols from the package client entrypoint:
|
|
346
|
-
//* lib UI (.tsx), the `ui`/`page` facets (.tsx), and the `webkit` facet (.ts/.tsx).
|
|
347
309
|
const roleFor = (facet: string, base: string): FileRole | null => {
|
|
348
310
|
if (facet === "lib") {
|
|
349
311
|
if (base.endsWith(".constant.ts")) return "constant";
|
|
@@ -371,8 +333,7 @@ const targetFor = (symbol: string, ctx: FileContext): ImportTarget | null => {
|
|
|
371
333
|
return null;
|
|
372
334
|
};
|
|
373
335
|
|
|
374
|
-
|
|
375
|
-
//* handles identifiers not matched by the framework registry — domain model refs and srvkit barrels.
|
|
336
|
+
/** The rewritten source, or null when nothing changes. */
|
|
376
337
|
export const transformSource = (
|
|
377
338
|
source: string,
|
|
378
339
|
fileName: string,
|
|
@@ -384,8 +345,6 @@ export const transformSource = (
|
|
|
384
345
|
const used = collectUsedReferences(sf);
|
|
385
346
|
const bareGlobals = collectBareGlobalReferences(sf);
|
|
386
347
|
|
|
387
|
-
// Group the needed symbols by their import target: framework registry first, then dynamic resolver.
|
|
388
|
-
// Named groups map each name to whether it is type-only, so we can emit `import type`/inline `type`.
|
|
389
348
|
const namedBySpecifier = new Map<string, Map<string, boolean>>();
|
|
390
349
|
const namespaceImports: { name: string; specifier: string }[] = [];
|
|
391
350
|
for (const name of used) {
|
|
@@ -423,8 +382,7 @@ export const transformSource = (
|
|
|
423
382
|
for (const ns of namespaceImports) newStatements.push(`import * as ${ns.name} from "${ns.specifier}";`);
|
|
424
383
|
|
|
425
384
|
if (newStatements.length > 0) {
|
|
426
|
-
//
|
|
427
|
-
// zero-width insertion sharing a boundary with the merge replacement.
|
|
385
|
+
// A zero-width insertion at the anchor's boundary would collide with the anchor's own merge edit.
|
|
428
386
|
const anchorEdit = anchor ? edits.find((edit) => edit.start === anchor.getStart(sf)) : undefined;
|
|
429
387
|
if (anchorEdit) anchorEdit.text = `${anchorEdit.text}\n${newStatements.join("\n")}`;
|
|
430
388
|
else if (anchor)
|
|
@@ -443,8 +401,7 @@ export const transformSource = (
|
|
|
443
401
|
return out === source ? null : out;
|
|
444
402
|
};
|
|
445
403
|
|
|
446
|
-
//*
|
|
447
|
-
//* suppresses an insertion (safe); under-collecting would risk a duplicate import (unsafe).
|
|
404
|
+
//* Over-collecting only suppresses an insertion; under-collecting would add a duplicate import.
|
|
448
405
|
const collectBoundNames = (sf: ts.SourceFile): Set<string> => {
|
|
449
406
|
const names = new Set<string>();
|
|
450
407
|
const visit = (node: ts.Node) => {
|
|
@@ -496,8 +453,6 @@ const collectBareGlobalReferences = (sf: ts.SourceFile): Set<string> => {
|
|
|
496
453
|
return bare;
|
|
497
454
|
};
|
|
498
455
|
|
|
499
|
-
//* True when the identifier reads a binding, as opposed to being a member name, property key, or
|
|
500
|
-
//* declaration name. e.g. in `cnst.Coordinate`, `cnst` is a reference but `Coordinate` is not.
|
|
501
456
|
const isReferencePosition = (node: ts.Identifier): boolean => {
|
|
502
457
|
const parent = node.parent;
|
|
503
458
|
if (!parent) return true;
|
|
@@ -512,7 +467,7 @@ const isReferencePosition = (node: ts.Identifier): boolean => {
|
|
|
512
467
|
|
|
513
468
|
interface ExistingNamedImport {
|
|
514
469
|
decl: ts.ImportDeclaration;
|
|
515
|
-
names: Map<string, boolean>;
|
|
470
|
+
names: Map<string, boolean>;
|
|
516
471
|
}
|
|
517
472
|
|
|
518
473
|
const collectExistingNamedImports = (importDecls: ts.ImportDeclaration[]): Map<string, ExistingNamedImport> => {
|
|
@@ -523,7 +478,7 @@ const collectExistingNamedImports = (importDecls: ts.ImportDeclaration[]): Map<s
|
|
|
523
478
|
if (!clause || !bindings || !ts.isNamedImports(bindings)) continue;
|
|
524
479
|
if (!ts.isStringLiteral(decl.moduleSpecifier)) continue;
|
|
525
480
|
const specifier = decl.moduleSpecifier.text;
|
|
526
|
-
if (map.has(specifier)) continue;
|
|
481
|
+
if (map.has(specifier)) continue;
|
|
527
482
|
const names = new Map<string, boolean>();
|
|
528
483
|
for (const el of bindings.elements) names.set(el.name.text, clause.isTypeOnly || el.isTypeOnly);
|
|
529
484
|
map.set(specifier, { decl, names });
|
|
@@ -531,8 +486,6 @@ const collectExistingNamedImports = (importDecls: ts.ImportDeclaration[]): Map<s
|
|
|
531
486
|
return map;
|
|
532
487
|
};
|
|
533
488
|
|
|
534
|
-
//* Render a named import, hoisting to `import type { … }` when every name is type-only and otherwise
|
|
535
|
-
//* prefixing each type-only name with inline `type`. Names sorted case-insensitively (Biome order).
|
|
536
489
|
const formatNamedImport = (specifier: string, names: Map<string, boolean>): string => {
|
|
537
490
|
const entries = [...names.entries()].sort((a, b) => compareNames(a[0], b[0]));
|
|
538
491
|
if (entries.every(([, isType]) => isType))
|
|
@@ -552,8 +505,7 @@ const directivePrologueEnd = (sf: ts.SourceFile): number => {
|
|
|
552
505
|
|
|
553
506
|
const scriptKindFor = (fileName: string) => (fileName.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
554
507
|
|
|
555
|
-
|
|
556
|
-
//* import specifier sort — so e.g. `Dayjs` precedes `dayjs`.
|
|
508
|
+
// Matches Biome's import-specifier sort: case-insensitive, uppercase first on a tie (`Dayjs` before `dayjs`).
|
|
557
509
|
const compareNames = (a: string, b: string): number => {
|
|
558
510
|
const [la, lb] = [a.toLowerCase(), b.toLowerCase()];
|
|
559
511
|
if (la !== lb) return la < lb ? -1 : 1;
|
|
@@ -567,8 +519,6 @@ const fileExists = async (file: string) =>
|
|
|
567
519
|
.then((s) => s.isFile())
|
|
568
520
|
.catch(() => false);
|
|
569
521
|
|
|
570
|
-
// ---- Domain index ---------------------------------------------------------------------------------
|
|
571
|
-
|
|
572
522
|
const domainKindOf = (base: string): DomainKind | null => {
|
|
573
523
|
if (base.endsWith(".constant.ts")) return "constant";
|
|
574
524
|
if (base.endsWith(".document.ts")) return "document";
|
|
@@ -576,7 +526,6 @@ const domainKindOf = (base: string): DomainKind | null => {
|
|
|
576
526
|
return null;
|
|
577
527
|
};
|
|
578
528
|
|
|
579
|
-
//* Scan a package `lib/` for model files and index their exported PascalCase declarations.
|
|
580
529
|
const buildDomainIndex = async (libDir: string): Promise<DomainIndex> => {
|
|
581
530
|
const index: DomainIndex = new Map();
|
|
582
531
|
for (const file of await collectDomainFiles(libDir)) {
|
|
@@ -622,7 +571,6 @@ const exportedPascalNames = (source: string, fileName: string): string[] => {
|
|
|
622
571
|
return names.filter((name) => PASCAL_CASE_RE.test(name));
|
|
623
572
|
};
|
|
624
573
|
|
|
625
|
-
//* Relative module specifier from a file's directory to a target file, extension stripped, `./`-anchored.
|
|
626
574
|
const relativeSpecifier = (fromDir: string, toFile: string): string => {
|
|
627
575
|
const rel = path
|
|
628
576
|
.relative(fromDir, toFile)
|
|
@@ -1,29 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
4
2
|
import path from "node:path";
|
|
3
|
+
import { tempDirs, writeText as write } from "../testHelpers";
|
|
5
4
|
import { createTsconfigPackageResolver } from "../transforms/barrelImportsPlugin";
|
|
6
5
|
import { CLIENT_BUNDLE_NAMING } from "./clientBuildTypes";
|
|
7
6
|
import { ClientEntriesBundler } from "./clientEntriesBundler";
|
|
8
7
|
import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
|
|
9
8
|
import { RouteClientBuilder } from "./routeClientBuilder";
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const makeTempRoot = async () => {
|
|
14
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "akan-client-entry-"));
|
|
15
|
-
tempRoots.push(root);
|
|
16
|
-
return root;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const write = async (filePath: string, content: string) => {
|
|
20
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
21
|
-
await writeFile(filePath, content);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
afterEach(async () => {
|
|
25
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
26
|
-
});
|
|
10
|
+
const makeTempRoot = tempDirs("akan-client-entry-");
|
|
27
11
|
|
|
28
12
|
describe("route client store bootstrap", () => {
|
|
29
13
|
test("wraps client entries with app client bootstrap before re-exporting components", () => {
|
|
@@ -210,24 +194,14 @@ describe("route client store bootstrap", () => {
|
|
|
210
194
|
await write(uiEntry, 'export { ClientPathWrapper } from "./System/Client";\n');
|
|
211
195
|
await write(clientEntry, '"use client";\nexport const ClientPathWrapper = () => null;\n');
|
|
212
196
|
|
|
197
|
+
const entryFiles = new Map([
|
|
198
|
+
["akanjs/ui", uiEntry],
|
|
199
|
+
["akanjs/ui/System/Client.tsx", clientEntry],
|
|
200
|
+
]);
|
|
213
201
|
const discovery = new GraphClientEntryDiscovery({ barrelImports: ["akanjs/ui"] }, async (specifier) => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
entryFile: uiEntry,
|
|
218
|
-
pkgDir: path.dirname(uiEntry),
|
|
219
|
-
preserveFilePath: true,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
if (specifier === "akanjs/ui/System/Client.tsx") {
|
|
223
|
-
return {
|
|
224
|
-
pkgName: "akanjs/ui/System/Client.tsx",
|
|
225
|
-
entryFile: clientEntry,
|
|
226
|
-
pkgDir: path.dirname(clientEntry),
|
|
227
|
-
preserveFilePath: true,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
return null;
|
|
202
|
+
const entryFile = entryFiles.get(specifier);
|
|
203
|
+
if (!entryFile) return null;
|
|
204
|
+
return { pkgName: specifier, entryFile, pkgDir: path.dirname(entryFile), preserveFilePath: true };
|
|
231
205
|
});
|
|
232
206
|
|
|
233
207
|
expect(await discovery.discover([seed])).toEqual([clientEntry]);
|