@akanjs/devkit 3.0.0-alpha.7 → 3.0.0-alpha.70
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/akanConfig/akanConfig.test.ts +181 -14
- package/akanConfig/akanConfig.ts +128 -47
- package/akanConfig/types.ts +8 -0
- package/akanContext.ts +53 -11
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +45 -21
- package/artifact/implicitRootLayout.ts +2 -2
- package/biome.base.json +336 -0
- package/biomeBase.ts +9 -0
- package/executors.test.ts +87 -5
- package/executors.ts +40 -9
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/buildRouteClient.test.ts +28 -2
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/cssCompiler.ts +122 -11
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +88 -1
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -0
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleGuard.test.ts +15 -0
- package/frontendBuild/styleGuard.ts +17 -0
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/getCredentials.ts +1 -3
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +2 -2
- package/integration/devStabilityHarness.ts +2 -10
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-daisyui-legacy-class.grit +26 -9
- 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-model-type-in-util-zone.grit +58 -0
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/linter.ts +17 -12
- package/package.json +5 -5
- package/qualityScanner.test.ts +52 -0
- package/qualityScanner.ts +46 -18
- package/repoIdentity.ts +42 -0
- package/scanInfo.ts +29 -23
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/tsconfig.json +1 -1
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +49 -4
package/qualityScanner.test.ts
CHANGED
|
@@ -184,6 +184,15 @@ describe("AkanQualityScanner ssr rules", () => {
|
|
|
184
184
|
});
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
+
const signalOf = (entries: string) =>
|
|
188
|
+
[
|
|
189
|
+
`import { endpoint, slice } from "akanjs/signal";`,
|
|
190
|
+
`export class PostSlice extends slice(srv.post, { guards: {}, mcp: { get: true } }, (init) => ({`,
|
|
191
|
+
entries,
|
|
192
|
+
`})) {}`,
|
|
193
|
+
"",
|
|
194
|
+
].join("\n");
|
|
195
|
+
|
|
187
196
|
describe("AkanQualityScanner layout rules", () => {
|
|
188
197
|
test("flags an unknown app root file but not a facet entrypoint", async () => {
|
|
189
198
|
const root = await makeWorkspace({
|
|
@@ -196,4 +205,47 @@ describe("AkanQualityScanner layout rules", () => {
|
|
|
196
205
|
expect(warnings).toHaveLength(1);
|
|
197
206
|
expect(warnings[0]?.file).toBe("apps/demo/helper.ts");
|
|
198
207
|
});
|
|
208
|
+
|
|
209
|
+
test("flags an unknown lib root file but not a facet entrypoint", async () => {
|
|
210
|
+
const root = await makeWorkspace({
|
|
211
|
+
"libs/demo/client.ts": "export const client = 1;\n",
|
|
212
|
+
"libs/demo/helper.ts": "export const helper = 1;\n",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const warnings = rulesOf(await new AkanQualityScanner().scan(root), "akan.layout.lib-root-file");
|
|
216
|
+
|
|
217
|
+
expect(warnings).toHaveLength(1);
|
|
218
|
+
expect(warnings[0]?.file).toBe("libs/demo/helper.ts");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("flags a root folder no facet owns, on both sides", async () => {
|
|
222
|
+
const root = await makeWorkspace({
|
|
223
|
+
"apps/demo/base/helper.ts": "export const helper = 1;\n",
|
|
224
|
+
"apps/demo/common/helper.ts": "export const helper = 1;\n",
|
|
225
|
+
"libs/demo/base/helper.ts": "export const helper = 1;\n",
|
|
226
|
+
"libs/demo/common/helper.ts": "export const helper = 1;\n",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const result = await new AkanQualityScanner().scan(root);
|
|
230
|
+
|
|
231
|
+
expect(rulesOf(result, "akan.layout.app-root-folder").map((warning) => warning.file)).toEqual([
|
|
232
|
+
"apps/demo/base/helper.ts",
|
|
233
|
+
]);
|
|
234
|
+
expect(rulesOf(result, "akan.layout.lib-root-folder").map((warning) => warning.file)).toEqual([
|
|
235
|
+
"libs/demo/base/helper.ts",
|
|
236
|
+
]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("keeps a root signal test out of the lib facet rule", async () => {
|
|
240
|
+
const root = await makeWorkspace({
|
|
241
|
+
"libs/demo/lib/cnst.ts": "export const cnst = 1;\n",
|
|
242
|
+
"libs/demo/lib/user.signal.test.ts": "export const test = 1;\n",
|
|
243
|
+
"libs/demo/lib/helper.ts": "export const helper = 1;\n",
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const warnings = rulesOf(await new AkanQualityScanner().scan(root), "akan.layout.lib-facet-file");
|
|
247
|
+
|
|
248
|
+
expect(warnings).toHaveLength(1);
|
|
249
|
+
expect(warnings[0]?.file).toBe("libs/demo/lib/helper.ts");
|
|
250
|
+
});
|
|
199
251
|
});
|
package/qualityScanner.ts
CHANGED
|
@@ -5,11 +5,12 @@ import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
|
|
|
5
5
|
import ignore from "ignore";
|
|
6
6
|
import ts from "typescript";
|
|
7
7
|
import { AbstractDoc } from "./abstractDoc";
|
|
8
|
+
import { FormSetterScanner } from "./formSetterScanner";
|
|
8
9
|
import { formatSsrBalance, type SsrBalanceEntry, SsrScanner } from "./ssrScanner";
|
|
9
|
-
import {
|
|
10
|
+
import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
|
|
10
11
|
|
|
11
12
|
type QualitySeverity = "warning";
|
|
12
|
-
type QualityScope = "global" | "file" | "convention" | "layout" | "ssr";
|
|
13
|
+
type QualityScope = "global" | "file" | "convention" | "layout" | "ssr" | "agent";
|
|
13
14
|
|
|
14
15
|
export interface QualityWarning {
|
|
15
16
|
rule: string;
|
|
@@ -88,7 +89,8 @@ const SUGGESTED_RULES = [
|
|
|
88
89
|
"Keep apps/*/lib and libs/*/lib root files limited to generated support facets such as cnst.ts, db.ts, dict.ts, sig.ts, srv.ts, st.ts, useClient.ts, and useServer.ts.",
|
|
89
90
|
"Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
|
|
90
91
|
"Keep module UI filenames predictable: database modules use <Model>.Template/Unit/Util/View/Zone.tsx, service modules use <Service>.Util/Zone.tsx, and scalar modules use <Scalar>.Template/Unit.tsx.",
|
|
91
|
-
"
|
|
92
|
+
"Hand a form control its store setter by reference so the field publishes an agent tool; put normalization in the control's transform prop and a multi-write in a _postSet<Field> hook on the store.",
|
|
93
|
+
"Move shared app and lib utilities to common/ instead of creating apps/*/base or libs/*/base.",
|
|
92
94
|
"Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline.",
|
|
93
95
|
];
|
|
94
96
|
|
|
@@ -122,14 +124,22 @@ const RULE_FIXES: Record<string, string> = {
|
|
|
122
124
|
"Move the Window augmentation into an approved browser integration file and keep it isolated.",
|
|
123
125
|
"akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
|
|
124
126
|
"akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
|
|
127
|
+
"akan.agent.unpublished-form-setter":
|
|
128
|
+
"Pass the setter by reference where the wrapper only forwards (onChange={st.do.setTitleOnTask}); move normalization into the control's own transform prop; move a multi-write into a _postSet<Field> hook on the store, keeping the generated setter on the control.",
|
|
125
129
|
"akan.file.component-internal-declaration":
|
|
126
130
|
"Move the type or helper to a type/util file in ui/, webkit/, or common/ by purpose. If it is the component's props, declare it as `interface <Component>Props`.",
|
|
127
131
|
"akan.file.component-export":
|
|
128
132
|
"Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix — only PascalCase components and their `<Component>Props` interface belong here.",
|
|
129
133
|
"akan.layout.app-root-file":
|
|
130
134
|
"Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
|
|
135
|
+
"akan.layout.app-root-folder":
|
|
136
|
+
"Move the folder's contents into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit) and delete it.",
|
|
131
137
|
"akan.layout.lib-root-file":
|
|
132
|
-
"Move the file into a
|
|
138
|
+
"Move the file into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit).",
|
|
139
|
+
"akan.layout.lib-root-folder":
|
|
140
|
+
"Move the folder's contents into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit) and delete it.",
|
|
141
|
+
"akan.layout.lib-facet-file":
|
|
142
|
+
"Move the file into a domain module folder under lib/; keep the lib facet root limited to generated support facets.",
|
|
133
143
|
"akan.layout.module-ui-file":
|
|
134
144
|
"Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component.",
|
|
135
145
|
"akan.ssr.unnecessary-use-client":
|
|
@@ -173,6 +183,7 @@ export class AkanQualityScanner {
|
|
|
173
183
|
...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
|
|
174
184
|
...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
|
|
175
185
|
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
|
|
186
|
+
...new FormSetterScanner().scan(sourceFiles),
|
|
176
187
|
...ssr.warnings,
|
|
177
188
|
];
|
|
178
189
|
|
|
@@ -408,26 +419,31 @@ export class AkanQualityScanner {
|
|
|
408
419
|
}
|
|
409
420
|
|
|
410
421
|
#scanLayoutQuality(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
411
|
-
const segments = sourceFile.file.split("/");
|
|
412
422
|
const warnings: QualityWarning[] = [];
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
423
|
+
const rootEntry = getSysRootEntry(sourceFile.file);
|
|
424
|
+
if (rootEntry) {
|
|
425
|
+
const { type, name, isDir } = rootEntry;
|
|
426
|
+
const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
|
|
427
|
+
const kind = isDir ? "folder" : "file";
|
|
428
|
+
if (!allowed) {
|
|
429
|
+
warnings.push({
|
|
430
|
+
rule: `akan.layout.${type}-root-${kind}`,
|
|
431
|
+
scope: "layout",
|
|
432
|
+
severity: "warning",
|
|
433
|
+
file: sourceFile.file,
|
|
434
|
+
message: `Unexpected ${type} root ${kind} "${name}". Keep ${type} code in conventional ${type} folders.`,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
421
437
|
}
|
|
422
438
|
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
439
|
+
const libFacetFile = getLibFacetRootFile(sourceFile.file);
|
|
440
|
+
if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile)) {
|
|
425
441
|
warnings.push({
|
|
426
|
-
rule: "akan.layout.lib-
|
|
442
|
+
rule: "akan.layout.lib-facet-file",
|
|
427
443
|
scope: "layout",
|
|
428
444
|
severity: "warning",
|
|
429
445
|
file: sourceFile.file,
|
|
430
|
-
message: `Unexpected lib root file "${
|
|
446
|
+
message: `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`,
|
|
431
447
|
});
|
|
432
448
|
}
|
|
433
449
|
|
|
@@ -821,7 +837,19 @@ function getConventionDescription(suffix: (typeof CONVENTION_SUFFIXES)[number],
|
|
|
821
837
|
return `${modelName}Store`;
|
|
822
838
|
}
|
|
823
839
|
|
|
824
|
-
|
|
840
|
+
const sysRootTypes = { apps: "app", libs: "lib" } as const;
|
|
841
|
+
|
|
842
|
+
function getSysRootEntry(file: string) {
|
|
843
|
+
const segments = file.split("/");
|
|
844
|
+
const root = segments[0];
|
|
845
|
+
const name = segments[2];
|
|
846
|
+
if (!root || !name || segments.length < 3) return null;
|
|
847
|
+
const type = sysRootTypes[root as keyof typeof sysRootTypes];
|
|
848
|
+
if (!type) return null;
|
|
849
|
+
return { type, name, isDir: segments.length > 3 };
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function getLibFacetRootFile(file: string) {
|
|
825
853
|
const segments = file.split("/");
|
|
826
854
|
if ((segments[0] === "libs" || segments[0] === "apps") && segments.length === 4 && segments[2] === "lib")
|
|
827
855
|
return segments[3];
|
package/repoIdentity.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// Resolved once per root: every CLI command builds a WorkspaceExecutor, and this would otherwise fork git on each.
|
|
5
|
+
const resolved = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
const readRemoteName = (workspaceRoot: string): string | null => {
|
|
8
|
+
try {
|
|
9
|
+
const url = execFileSync("git", ["config", "--get", "remote.origin.url"], {
|
|
10
|
+
cwd: workspaceRoot,
|
|
11
|
+
encoding: "utf-8",
|
|
12
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
13
|
+
}).trim();
|
|
14
|
+
// Both remote spellings end in the repository: git@host:owner/name.git and https://host/owner/name.git.
|
|
15
|
+
return (
|
|
16
|
+
url
|
|
17
|
+
.replace(/\.git$/, "")
|
|
18
|
+
.split(/[/:]/)
|
|
19
|
+
.pop() || null
|
|
20
|
+
);
|
|
21
|
+
} catch {
|
|
22
|
+
// No git, no origin, or no git binary — a fresh `akan workspace` before its first commit lands here.
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The repository's own name.
|
|
29
|
+
*
|
|
30
|
+
* Deriving it from the working directory made every generated file that names the repo — the AGENTS.md title, its
|
|
31
|
+
* `- Repo:` line — depend on what each person happened to call the folder they cloned into, so one commit rendered
|
|
32
|
+
* a different guide per developer and the diff never settled. The origin remote is the one identity every clone
|
|
33
|
+
* shares. `AKAN_PUBLIC_REPO_NAME` is deliberately not consulted: that is a deployment namespace (queue prefixes,
|
|
34
|
+
* cache keys, secret paths) which a monorepo hosting several products legitimately points somewhere else.
|
|
35
|
+
*/
|
|
36
|
+
export const resolveRepoName = (workspaceRoot: string): string => {
|
|
37
|
+
const cached = resolved.get(workspaceRoot);
|
|
38
|
+
if (cached) return cached;
|
|
39
|
+
const repoName = readRemoteName(workspaceRoot) ?? path.basename(workspaceRoot);
|
|
40
|
+
resolved.set(workspaceRoot, repoName);
|
|
41
|
+
return repoName;
|
|
42
|
+
};
|
package/scanInfo.ts
CHANGED
|
@@ -9,9 +9,9 @@ import type {
|
|
|
9
9
|
PkgScanResult,
|
|
10
10
|
ScanResult,
|
|
11
11
|
} from "./akanConfig";
|
|
12
|
-
|
|
12
|
+
import { AkanAppConfig } from "./akanConfig";
|
|
13
13
|
import { AppExecutor, LibExecutor, PkgExecutor, WorkspaceExecutor } from "./executors";
|
|
14
|
-
import {
|
|
14
|
+
import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
|
|
15
15
|
|
|
16
16
|
const scalarFileTypes = ["constant", "dictionary", "document", "template", "unit", "util", "view", "zone"] as const;
|
|
17
17
|
type ScalarFileType = (typeof scalarFileTypes)[number];
|
|
@@ -57,7 +57,6 @@ const moduleUiFileTypes = {
|
|
|
57
57
|
scalar: new Set(["Template", "Unit"]),
|
|
58
58
|
} satisfies Record<ModuleKind, Set<string>>;
|
|
59
59
|
const testFilePattern = /\.(test|spec)\.(ts|tsx)$/;
|
|
60
|
-
const rootSignalTestFilePattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(ts|tsx)$/;
|
|
61
60
|
|
|
62
61
|
// The dependency scanner needs `typescript` (~65MB resident). Only the scan/sync commands run it, so
|
|
63
62
|
// load it on demand rather than through the module graph of every process that imports scan info.
|
|
@@ -65,8 +64,6 @@ const createDependencyScanner = async (exec: AppExecutor | LibExecutor | PkgExec
|
|
|
65
64
|
(await import("./dependencyScanner")).TypeScriptDependencyScanner.from(exec);
|
|
66
65
|
|
|
67
66
|
const isAllowedTestFile = (filename: string) => testFilePattern.test(filename);
|
|
68
|
-
const isAllowedLibRootFile = (filename: string) =>
|
|
69
|
-
libFacetRootAllowedFiles.has(filename) || rootSignalTestFilePattern.test(filename);
|
|
70
67
|
const getScanPath = (exec: AppExecutor | LibExecutor, relativePath: string) =>
|
|
71
68
|
path.posix.join(`${exec.type}s`, exec.name, relativePath.split(path.sep).join("/"));
|
|
72
69
|
async function clearGeneratedRootCapacitorConfigs(exec: AppExecutor | LibExecutor) {
|
|
@@ -85,24 +82,29 @@ async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: {
|
|
|
85
82
|
violations.push(`${getScanPath(exec, relativePath)}: ${reason}`);
|
|
86
83
|
};
|
|
87
84
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
-
|
|
85
|
+
const allowedRootFiles: ReadonlySet<string> = rootAllowedFiles[exec.type];
|
|
86
|
+
const allowedRootDirs: ReadonlySet<string> = rootAllowedDirs[exec.type];
|
|
87
|
+
const { files, dirs } = await exec.getFilesAndDirs(".");
|
|
88
|
+
files
|
|
89
|
+
.filter((filename) => !allowedRootFiles.has(filename))
|
|
90
|
+
.forEach((filename) => {
|
|
91
|
+
addViolation(filename, `unsupported ${exec.type} root file`);
|
|
92
|
+
});
|
|
93
|
+
dirs
|
|
94
|
+
.filter((dirname) => !allowedRootDirs.has(dirname))
|
|
95
|
+
.forEach((dirname) => {
|
|
96
|
+
addViolation(dirname, `unsupported ${exec.type} root folder`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
//* An app validates its page tree in `getPageKeys`, which a lib has no equivalent of — so a lib's own
|
|
100
|
+
//* routes went unchecked until whichever app opted into them with `syncPageLibs` blew up on the sync.
|
|
101
|
+
if (exec.type === "lib")
|
|
102
|
+
for (const { relativePath, reason } of await exec.getPageConventionViolations()) addViolation(relativePath, reason);
|
|
101
103
|
|
|
102
104
|
libRoot.files
|
|
103
|
-
.filter((filename) => !
|
|
105
|
+
.filter((filename) => !isAllowedLibFacetRootFile(filename))
|
|
104
106
|
.forEach((filename) => {
|
|
105
|
-
addViolation(path.join("lib", filename), "unsupported lib root file");
|
|
107
|
+
addViolation(path.join("lib", filename), "unsupported lib facet root file");
|
|
106
108
|
});
|
|
107
109
|
|
|
108
110
|
libRoot.dirs
|
|
@@ -284,12 +286,11 @@ class ScanInfo {
|
|
|
284
286
|
}),
|
|
285
287
|
]);
|
|
286
288
|
const routes = exec.type === "lib" ? [] : await (exec as AppExecutor).getPageKeys();
|
|
287
|
-
const
|
|
289
|
+
const common = {
|
|
288
290
|
name: exec.name,
|
|
289
291
|
type: exec.type,
|
|
290
292
|
repoName: exec.workspace.repoName,
|
|
291
293
|
serveDomain: WorkspaceExecutor.getBaseDevEnv(path.join(exec.workspace.workspaceRoot, ".env")).serveDomain,
|
|
292
|
-
akanConfig,
|
|
293
294
|
files,
|
|
294
295
|
libDeps,
|
|
295
296
|
pkgDeps,
|
|
@@ -297,6 +298,11 @@ class ScanInfo {
|
|
|
297
298
|
devDependencies: npmDevDeps.filter((dep) => !isAkanFrameworkDependency(dep)),
|
|
298
299
|
routes,
|
|
299
300
|
};
|
|
301
|
+
//? `exec.type` and the resolved config class are correlated, which the union alone cannot express.
|
|
302
|
+
const scanResult: AppScanResult | LibScanResult =
|
|
303
|
+
akanConfig instanceof AkanAppConfig
|
|
304
|
+
? ({ ...common, akanConfig } satisfies AppScanResult)
|
|
305
|
+
: ({ ...common, akanConfig } satisfies LibScanResult);
|
|
300
306
|
return scanResult;
|
|
301
307
|
}
|
|
302
308
|
|
|
@@ -461,7 +467,7 @@ export class LibInfo extends ScanInfo {
|
|
|
461
467
|
LibInfo.libInfos.set(libName, await LibInfo.fromExecutor(libExecutor));
|
|
462
468
|
}),
|
|
463
469
|
);
|
|
464
|
-
const libInfo = new LibInfo(exec, scanResult);
|
|
470
|
+
const libInfo = new LibInfo(exec, scanResult as LibScanResult);
|
|
465
471
|
LibInfo.libInfos.set(exec.name, libInfo);
|
|
466
472
|
LibInfo.#sortedLibIndices = null;
|
|
467
473
|
return libInfo;
|
package/tsconfig.json
CHANGED
package/workspaceLayout.test.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appRootAllowedDirs,
|
|
4
|
+
appRootAllowedFiles,
|
|
5
|
+
isAllowedLibFacetRootFile,
|
|
6
|
+
isScannedRootEntry,
|
|
7
|
+
libRootAllowedDirs,
|
|
8
|
+
libRootAllowedFiles,
|
|
9
|
+
} from "./workspaceLayout";
|
|
3
10
|
|
|
4
11
|
describe("app root layout allowlist", () => {
|
|
5
12
|
test("admits the scoped agent guides sync writes into every app", () => {
|
|
@@ -19,8 +26,53 @@ describe("app root layout allowlist", () => {
|
|
|
19
26
|
});
|
|
20
27
|
|
|
21
28
|
test("skips dotfile artifacts the sync glob never sees, but keeps .akan", () => {
|
|
22
|
-
expect(
|
|
23
|
-
expect(
|
|
24
|
-
expect(
|
|
29
|
+
expect(isScannedRootEntry("app", ".DS_Store")).toBe(false);
|
|
30
|
+
expect(isScannedRootEntry("app", ".akan")).toBe(true);
|
|
31
|
+
expect(isScannedRootEntry("app", "lib")).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("lib root layout allowlist", () => {
|
|
36
|
+
test("admits what the libRoot template and scan write", () => {
|
|
37
|
+
for (const filename of ["AGENTS.md", "akan.config.ts", "akan.lib.json", "client.ts", "index.ts", "server.ts"]) {
|
|
38
|
+
expect(libRootAllowedFiles.has(filename)).toBe(true);
|
|
39
|
+
}
|
|
40
|
+
for (const dirname of ["common", "env", "lib", "page", "plugin", "private", "public", "srvkit", "ui", "webkit"]) {
|
|
41
|
+
expect(libRootAllowedDirs.has(dirname)).toBe(true);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("rejects a lib root entry no facet owns", () => {
|
|
46
|
+
expect(libRootAllowedFiles.has("helper.ts")).toBe(false);
|
|
47
|
+
expect(libRootAllowedDirs.has("base")).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("rejects the app-only run and mobile entries", () => {
|
|
51
|
+
for (const filename of ["main.ts", "capacitor.config.ts", "akan.app.json"]) {
|
|
52
|
+
expect(libRootAllowedFiles.has(filename)).toBe(false);
|
|
53
|
+
}
|
|
54
|
+
for (const dirname of [".akan", "android", "ios", "mobile", "script", "secrets"]) {
|
|
55
|
+
expect(libRootAllowedDirs.has(dirname)).toBe(false);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("skips dotfile artifacts in a lib root too", () => {
|
|
60
|
+
expect(isScannedRootEntry("lib", ".gitignore")).toBe(false);
|
|
61
|
+
expect(isScannedRootEntry("lib", ".akan")).toBe(false);
|
|
62
|
+
expect(isScannedRootEntry("lib", "ui")).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("lib facet root allowlist", () => {
|
|
67
|
+
test("admits the generated support facets and a root signal test", () => {
|
|
68
|
+
expect(isAllowedLibFacetRootFile("cnst.ts")).toBe(true);
|
|
69
|
+
expect(isAllowedLibFacetRootFile("option.ts")).toBe(true);
|
|
70
|
+
expect(isAllowedLibFacetRootFile("user.signal.test.ts")).toBe(true);
|
|
71
|
+
expect(isAllowedLibFacetRootFile("user.signal.spec.ts")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("rejects a hand-written file beside the barrels", () => {
|
|
75
|
+
expect(isAllowedLibFacetRootFile("helper.ts")).toBe(false);
|
|
76
|
+
expect(isAllowedLibFacetRootFile("user.test.ts")).toBe(false);
|
|
25
77
|
});
|
|
26
78
|
});
|
package/workspaceLayout.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* 같은 규칙을 scanInfo(`akan sync`, hard error) · akanContext(`akan doctor`, diagnostic) ·
|
|
5
5
|
* qualityScanner(`akan quality scan`, warning) 세 곳이 각자 복사해 두면서 실제로 어긋났다
|
|
6
6
|
* (스코프 AGENTS.md/CLAUDE.md 는 sync 만 허용, `plugin` 은 문서에만, `secrets` 는 doctor 만 거부).
|
|
7
|
-
* 규칙을 추가할 때는 이 파일만 고치고, 루트 AGENTS.md
|
|
8
|
-
* 의 목록도 같이 갱신한다.
|
|
7
|
+
* 규칙을 추가할 때는 이 파일만 고치고, 루트 AGENTS.md 의 목록도 같이 갱신한다.
|
|
9
8
|
*/
|
|
10
9
|
|
|
10
|
+
export type SysType = "app" | "lib";
|
|
11
|
+
|
|
11
12
|
export const appRootAllowedFiles = new Set([
|
|
12
13
|
// 스코프 에이전트 가이드 — scan(write) 이 유지하는 색인 + 마커 밖 hand-written 내용 (agentsIndex.ts)
|
|
13
14
|
"AGENTS.md",
|
|
@@ -42,6 +43,43 @@ export const appRootAllowedDirs = new Set([
|
|
|
42
43
|
"webkit",
|
|
43
44
|
]);
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 라이브러리 루트는 앱 루트에서 실행/모바일 전용 항목(`main.ts`, `capacitor.config.ts`, `.akan`,
|
|
48
|
+
* `android`, `ios`, `mobile`, `script`, `secrets`)을 뺀 집합이다 — 라이브러리는 부팅되지도, 패키징되지도
|
|
49
|
+
* 않는다. `index.ts` 는 lib 에만 생성되고, `README.md` / `tsconfig.spec.json` 은 라이브러리가 패키지로
|
|
50
|
+
* 배포되기 때문에 남는다. `public` / `private` 은 syncAssets 가 앱으로 심링크하므로 라이브러리도 가진다.
|
|
51
|
+
*/
|
|
52
|
+
export const libRootAllowedFiles = new Set([
|
|
53
|
+
"AGENTS.md",
|
|
54
|
+
"CLAUDE.md",
|
|
55
|
+
"README.md",
|
|
56
|
+
"akan.config.ts",
|
|
57
|
+
"akan.lib.json",
|
|
58
|
+
"client.ts",
|
|
59
|
+
"index.ts",
|
|
60
|
+
"package.json",
|
|
61
|
+
"server.ts",
|
|
62
|
+
"tsconfig.json",
|
|
63
|
+
"tsconfig.spec.json",
|
|
64
|
+
"tsconfig.tsbuildinfo",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
export const libRootAllowedDirs = new Set([
|
|
68
|
+
"common",
|
|
69
|
+
"env",
|
|
70
|
+
"lib",
|
|
71
|
+
"page",
|
|
72
|
+
"plugin",
|
|
73
|
+
"private",
|
|
74
|
+
"public",
|
|
75
|
+
"srvkit",
|
|
76
|
+
"ui",
|
|
77
|
+
"webkit",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
export const rootAllowedFiles = { app: appRootAllowedFiles, lib: libRootAllowedFiles } as const;
|
|
81
|
+
export const rootAllowedDirs = { app: appRootAllowedDirs, lib: libRootAllowedDirs } as const;
|
|
82
|
+
|
|
45
83
|
export const libFacetRootAllowedFiles = new Set([
|
|
46
84
|
"cnst.ts",
|
|
47
85
|
"db.ts",
|
|
@@ -54,8 +92,15 @@ export const libFacetRootAllowedFiles = new Set([
|
|
|
54
92
|
"useServer.ts",
|
|
55
93
|
]);
|
|
56
94
|
|
|
95
|
+
/** `<model>.signal.test.ts` — lib 파싯 루트에 놓이는 유일한 비생성 파일 (테스트가 배럴 전체를 부팅한다). */
|
|
96
|
+
const libFacetRootTestPattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(ts|tsx)$/;
|
|
97
|
+
|
|
98
|
+
export const isAllowedLibFacetRootFile = (filename: string) =>
|
|
99
|
+
libFacetRootAllowedFiles.has(filename) || libFacetRootTestPattern.test(filename);
|
|
100
|
+
|
|
57
101
|
/**
|
|
58
|
-
* scanSync 는
|
|
102
|
+
* scanSync 는 루트를 `Bun.Glob("*")` 로 읽어 dotfile 을 아예 보지 못한다. 디렉터리를 직접 읽는
|
|
59
103
|
* doctor 가 그 차이만큼 `.DS_Store` 같은 툴 산출물을 에러로 올리므로 같은 기준으로 걸러낸다.
|
|
60
104
|
*/
|
|
61
|
-
export const
|
|
105
|
+
export const isScannedRootEntry = (type: SysType, name: string) =>
|
|
106
|
+
!name.startsWith(".") || rootAllowedDirs[type].has(name);
|