@akanjs/devkit 3.0.0-alpha.9 → 3.0.0-alpha.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/DEV_RUNTIME_KNOBS.md +27 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +21 -1
- package/aiEditor.ts +1 -1
- package/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +113 -578
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +442 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +143 -47
- package/akanConfig/types.ts +9 -0
- package/akanContext.ts +30 -14
- package/akanMcpContract.ts +25 -4
- package/appSelectionMemory.ts +28 -0
- package/applicationBuildRunner.test.ts +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 +86 -47
- package/biomeBase.ts +6 -5
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/codegenLock.test.ts +137 -0
- package/codegenLock.ts +136 -0
- package/commandDecorators/argMeta.ts +9 -1
- package/commandDecorators/command.ts +94 -7
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/commandDecorators/helpFormatter.ts +4 -0
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +129 -4
- package/executors.ts +105 -24
- package/fileEditor.ts +19 -19
- package/fileSys.ts +19 -1
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +7 -2
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +123 -12
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/devGeneratedIndexSync.ts +3 -2
- 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/sourceMtimeIndex.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/frontendBuild/watchRootResolver.test.ts +67 -0
- package/frontendBuild/watchRootResolver.ts +13 -1
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +51 -7
- package/incrementalBuilder/incrementalBuilder.proc.ts +17 -6
- package/index.ts +0 -7
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +112 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +99 -25
- package/package.json +5 -8
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- 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/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +48 -2
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
- package/ui/MultiScrollList.tsx +0 -242
- package/ui/ScrollList.tsx +0 -107
- package/ui/index.ts +0 -2
- package/useStdoutDimensions.ts +0 -20
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
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* 규칙을 추가할 때는 이 파일만 고치고, 루트 AGENTS.md 의 목록도 같이 갱신한다.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
export type SysType = "app" | "lib";
|
|
11
|
+
|
|
10
12
|
export const appRootAllowedFiles = new Set([
|
|
11
13
|
// 스코프 에이전트 가이드 — scan(write) 이 유지하는 색인 + 마커 밖 hand-written 내용 (agentsIndex.ts)
|
|
12
14
|
"AGENTS.md",
|
|
@@ -41,6 +43,43 @@ export const appRootAllowedDirs = new Set([
|
|
|
41
43
|
"webkit",
|
|
42
44
|
]);
|
|
43
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
|
+
|
|
44
83
|
export const libFacetRootAllowedFiles = new Set([
|
|
45
84
|
"cnst.ts",
|
|
46
85
|
"db.ts",
|
|
@@ -53,8 +92,15 @@ export const libFacetRootAllowedFiles = new Set([
|
|
|
53
92
|
"useServer.ts",
|
|
54
93
|
]);
|
|
55
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
|
+
|
|
56
101
|
/**
|
|
57
|
-
* scanSync 는
|
|
102
|
+
* scanSync 는 루트를 `Bun.Glob("*")` 로 읽어 dotfile 을 아예 보지 못한다. 디렉터리를 직접 읽는
|
|
58
103
|
* doctor 가 그 차이만큼 `.DS_Store` 같은 툴 산출물을 에러로 올리므로 같은 기준으로 걸러낸다.
|
|
59
104
|
*/
|
|
60
|
-
export const
|
|
105
|
+
export const isScannedRootEntry = (type: SysType, name: string) =>
|
|
106
|
+
!name.startsWith(".") || rootAllowedDirs[type].has(name);
|
package/builder.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import type { Loader } from "bun";
|
|
5
|
-
import type { Executor } from "./executors";
|
|
6
|
-
import { FileSys } from "./fileSys";
|
|
7
|
-
import type { PackageJson } from "./types";
|
|
8
|
-
|
|
9
|
-
/** Relative paths under these directories are not used as Bun.build entrypoints (Bun does not expand `**` in entrypoint strings; we scan with {@link Bun.Glob}). */
|
|
10
|
-
const SKIP_ENTRY_DIR_SET = new Set(["node_modules", "dist", "build", ".git", ".next"]);
|
|
11
|
-
|
|
12
|
-
const assetExtensions = [".css", ".md", ".js", ".png", ".ico", ".svg", ".json", ".template"];
|
|
13
|
-
const assetLoader = Object.fromEntries(assetExtensions.map((ext) => [ext, "file" as const])) satisfies Record<
|
|
14
|
-
string,
|
|
15
|
-
Loader
|
|
16
|
-
>;
|
|
17
|
-
|
|
18
|
-
type BunBuildConfig = Parameters<typeof Bun.build>[0] & { bundle?: boolean };
|
|
19
|
-
type ExportValue = string | string[] | { [condition: string]: ExportValue };
|
|
20
|
-
|
|
21
|
-
interface BuildOptions {
|
|
22
|
-
bundle?: boolean;
|
|
23
|
-
additionalEntryPoints?: string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface BuilderOptions {
|
|
27
|
-
executor: Executor;
|
|
28
|
-
distExecutor: Executor;
|
|
29
|
-
pkgJson: PackageJson;
|
|
30
|
-
rootPackageJson: PackageJson;
|
|
31
|
-
}
|
|
32
|
-
export class Builder {
|
|
33
|
-
#executor: Executor;
|
|
34
|
-
#distExecutor: Executor;
|
|
35
|
-
#pkgJson: PackageJson;
|
|
36
|
-
|
|
37
|
-
constructor({ executor, distExecutor, pkgJson }: BuilderOptions) {
|
|
38
|
-
this.#executor = executor;
|
|
39
|
-
this.#distExecutor = distExecutor;
|
|
40
|
-
this.#pkgJson = pkgJson;
|
|
41
|
-
}
|
|
42
|
-
#globEntrypoints(cwd: string, pattern: string): string[] {
|
|
43
|
-
const glob = new Bun.Glob(pattern);
|
|
44
|
-
return Array.from(glob.scanSync({ cwd, onlyFiles: true }))
|
|
45
|
-
.filter((relativePath) => {
|
|
46
|
-
const segments = relativePath.split(path.sep);
|
|
47
|
-
return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
|
|
48
|
-
})
|
|
49
|
-
.map((rel) => path.join(cwd, rel));
|
|
50
|
-
}
|
|
51
|
-
#globFiles(cwd: string, pattern = "**/*.*"): string[] {
|
|
52
|
-
const glob = new Bun.Glob(pattern);
|
|
53
|
-
return Array.from(glob.scanSync({ cwd, onlyFiles: true })).filter((relativePath) => {
|
|
54
|
-
const segments = relativePath.split(path.sep);
|
|
55
|
-
return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
#resolveAdditionalEntrypoints(cwd: string, additionalEntryPoints: string[]): string[] {
|
|
60
|
-
const out: string[] = [];
|
|
61
|
-
for (const p of additionalEntryPoints) {
|
|
62
|
-
if (p.includes("*")) {
|
|
63
|
-
const rel = p.startsWith(`${cwd}/`) || p.startsWith(`${cwd}${path.sep}`) ? p.slice(cwd.length + 1) : p;
|
|
64
|
-
out.push(...this.#globEntrypoints(cwd, rel));
|
|
65
|
-
} else out.push(path.isAbsolute(p) ? p : path.join(cwd, p));
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
#getBuildOptions({ bundle = false, additionalEntryPoints = [] }: BuildOptions = {}): BunBuildConfig {
|
|
70
|
-
const cwd = this.#executor.cwdPath;
|
|
71
|
-
const entrypoints = [
|
|
72
|
-
...(bundle ? [path.join(cwd, "index.ts")] : this.#globEntrypoints(cwd, "**/*.{ts,tsx}")),
|
|
73
|
-
...this.#resolveAdditionalEntrypoints(cwd, additionalEntryPoints),
|
|
74
|
-
];
|
|
75
|
-
return {
|
|
76
|
-
root: cwd,
|
|
77
|
-
entrypoints,
|
|
78
|
-
bundle,
|
|
79
|
-
packages: "external",
|
|
80
|
-
splitting: false,
|
|
81
|
-
target: this.#pkgJson.bun?.platform,
|
|
82
|
-
format: "esm",
|
|
83
|
-
outdir: this.#distExecutor.cwdPath,
|
|
84
|
-
external: ["react", "react-dom"],
|
|
85
|
-
loader: assetLoader,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
async #copySourceFiles() {
|
|
89
|
-
const cwd = this.#executor.cwdPath;
|
|
90
|
-
for (const relativePath of this.#globFiles(cwd)) {
|
|
91
|
-
if (relativePath === "package.json") continue;
|
|
92
|
-
const sourcePath = path.join(cwd, relativePath);
|
|
93
|
-
const targetPath = path.join(this.#distExecutor.cwdPath, relativePath);
|
|
94
|
-
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
95
|
-
await Bun.write(targetPath, Bun.file(sourcePath));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
#toPublishedPath(publishedPath: string, { useSource }: { useSource: boolean }) {
|
|
99
|
-
const hasDotSlash = publishedPath.startsWith("./");
|
|
100
|
-
const withoutFormatDir = publishedPath.replace(/^(?:\.\/)?(?:esm|cjs)\//, hasDotSlash ? "./" : "");
|
|
101
|
-
if (!useSource) return withoutFormatDir;
|
|
102
|
-
if (!hasDotSlash && withoutFormatDir === publishedPath) return publishedPath;
|
|
103
|
-
const parsed = path.posix.parse(withoutFormatDir);
|
|
104
|
-
if (![".js", ".mjs", ".cjs"].includes(parsed.ext)) return withoutFormatDir;
|
|
105
|
-
const withoutExt = path.posix.join(parsed.dir, parsed.name);
|
|
106
|
-
const sourcePath = withoutExt.startsWith("./") ? withoutExt.slice(2) : withoutExt;
|
|
107
|
-
const sourceCandidates = [`${sourcePath}.ts`, `${sourcePath}.tsx`];
|
|
108
|
-
const matchedSource = sourceCandidates.find((candidate) =>
|
|
109
|
-
existsSync(path.join(this.#executor.cwdPath, candidate)),
|
|
110
|
-
);
|
|
111
|
-
if (!matchedSource) return withoutFormatDir;
|
|
112
|
-
return hasDotSlash ? `./${matchedSource}` : matchedSource;
|
|
113
|
-
}
|
|
114
|
-
#normalizeExports(
|
|
115
|
-
exportsValue: ExportValue | undefined,
|
|
116
|
-
{ useSource }: { useSource: boolean },
|
|
117
|
-
): ExportValue | undefined {
|
|
118
|
-
if (!exportsValue) return exportsValue;
|
|
119
|
-
if (typeof exportsValue === "string") return this.#toPublishedPath(exportsValue, { useSource });
|
|
120
|
-
if (Array.isArray(exportsValue))
|
|
121
|
-
return exportsValue.map((value) => this.#normalizeExports(value, { useSource }) as string);
|
|
122
|
-
if (typeof exportsValue !== "object") return exportsValue;
|
|
123
|
-
|
|
124
|
-
return Object.fromEntries(
|
|
125
|
-
Object.entries(exportsValue)
|
|
126
|
-
.filter(([condition]) => condition !== "require")
|
|
127
|
-
.map(([condition, value]) => [condition, this.#normalizeExports(value, { useSource })])
|
|
128
|
-
.filter((entry): entry is [string, ExportValue] => entry[1] !== undefined),
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
#getPackageJson({ bundle = false }: BuildOptions = {}): PackageJson {
|
|
132
|
-
const rootEntry = bundle ? "./index.js" : "./index.ts";
|
|
133
|
-
const normalizedExports = this.#normalizeExports(this.#pkgJson.exports as ExportValue | undefined, {
|
|
134
|
-
useSource: !bundle,
|
|
135
|
-
});
|
|
136
|
-
return {
|
|
137
|
-
...this.#pkgJson,
|
|
138
|
-
type: "module",
|
|
139
|
-
main: rootEntry,
|
|
140
|
-
bin: this.#normalizeExports(this.#pkgJson.bin as ExportValue | undefined, { useSource: !bundle }),
|
|
141
|
-
exports: {
|
|
142
|
-
...((typeof normalizedExports === "object" && !Array.isArray(normalizedExports)
|
|
143
|
-
? normalizedExports
|
|
144
|
-
: {}) as Record<string, ExportValue>),
|
|
145
|
-
".": {
|
|
146
|
-
import: rootEntry,
|
|
147
|
-
types: bundle ? "./index.d.ts" : "./index.ts",
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
async build(options: BuildOptions = {}) {
|
|
153
|
-
if (await FileSys.dirExists(this.#distExecutor.cwdPath))
|
|
154
|
-
await this.#distExecutor.exec(`rm -rf ${this.#distExecutor.cwdPath}`);
|
|
155
|
-
|
|
156
|
-
if (options.bundle) {
|
|
157
|
-
const buildResult = await Bun.build({ ...this.#getBuildOptions(options) });
|
|
158
|
-
if (!buildResult.success) throw new AggregateError(buildResult.logs, "Bundle failed");
|
|
159
|
-
} else await this.#copySourceFiles();
|
|
160
|
-
|
|
161
|
-
const pkgPackageJson = this.#getPackageJson(options);
|
|
162
|
-
await this.#distExecutor.setPackageJson(pkgPackageJson);
|
|
163
|
-
}
|
|
164
|
-
}
|
package/extractDeps.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import type { PackageJson } from "./types";
|
|
2
|
-
|
|
3
|
-
const NODE_NATIVE_MODULE_SET = new Set([
|
|
4
|
-
"assert",
|
|
5
|
-
"async_hooks",
|
|
6
|
-
"buffer",
|
|
7
|
-
"child_process",
|
|
8
|
-
"cluster",
|
|
9
|
-
"console",
|
|
10
|
-
"constants",
|
|
11
|
-
"crypto",
|
|
12
|
-
"dgram",
|
|
13
|
-
"dns",
|
|
14
|
-
"domain",
|
|
15
|
-
"events",
|
|
16
|
-
"fs",
|
|
17
|
-
"http",
|
|
18
|
-
"http2",
|
|
19
|
-
"https",
|
|
20
|
-
"inspector",
|
|
21
|
-
"module",
|
|
22
|
-
"net",
|
|
23
|
-
"os",
|
|
24
|
-
"path",
|
|
25
|
-
"perf_hooks",
|
|
26
|
-
"process",
|
|
27
|
-
"punycode",
|
|
28
|
-
"querystring",
|
|
29
|
-
"readline",
|
|
30
|
-
"repl",
|
|
31
|
-
"stream",
|
|
32
|
-
"string_decoder",
|
|
33
|
-
"timers",
|
|
34
|
-
"tls",
|
|
35
|
-
"trace_events",
|
|
36
|
-
"tty",
|
|
37
|
-
"url",
|
|
38
|
-
"util",
|
|
39
|
-
"v8",
|
|
40
|
-
"vm",
|
|
41
|
-
"wasi",
|
|
42
|
-
"worker_threads",
|
|
43
|
-
"zlib",
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
export const extractDependencies = (
|
|
47
|
-
filepaths: { path: string; text: string }[],
|
|
48
|
-
pacakgeJson: PackageJson,
|
|
49
|
-
defaultDependencies: string[] = [],
|
|
50
|
-
) => {
|
|
51
|
-
if (!pacakgeJson.dependencies) throw new Error("No dependencies found in package.json");
|
|
52
|
-
const dependencies = new Set<string>(defaultDependencies);
|
|
53
|
-
|
|
54
|
-
const existingDependencies = new Set<string>([
|
|
55
|
-
...Object.keys(pacakgeJson.dependencies ?? {}),
|
|
56
|
-
...Object.keys(pacakgeJson.devDependencies ?? {}),
|
|
57
|
-
]);
|
|
58
|
-
const versionObj = {
|
|
59
|
-
...(pacakgeJson.dependencies ?? {}),
|
|
60
|
-
...(pacakgeJson.devDependencies ?? {}),
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// Look for require statements: require('package-name') or import 'package-name'
|
|
64
|
-
const requireRegex = /(?:require\s*\(|import\s*(?:[\w\s{},*]*\s+from\s*)?|import\s*\()\s*['"`]([^'"`]+)['"`]/g;
|
|
65
|
-
for (const { text } of filepaths.filter(({ path }) => path.endsWith(".js") || path.endsWith(".ts"))) {
|
|
66
|
-
let requireMatch = requireRegex.exec(text);
|
|
67
|
-
while (requireMatch !== null) {
|
|
68
|
-
const moduleName = requireMatch[1];
|
|
69
|
-
if (!moduleName) throw new Error(`No module name found in ${text}`);
|
|
70
|
-
const moduleNameParts = moduleName.split("/");
|
|
71
|
-
const subModuleLength = moduleNameParts.length;
|
|
72
|
-
for (let i = 0; i < subModuleLength; i++) {
|
|
73
|
-
const libName = moduleNameParts.slice(0, i + 1).join("/");
|
|
74
|
-
if (!NODE_NATIVE_MODULE_SET.has(libName) && existingDependencies.has(libName)) dependencies.add(libName);
|
|
75
|
-
}
|
|
76
|
-
requireMatch = requireRegex.exec(text);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return Object.fromEntries(
|
|
80
|
-
[...dependencies].sort().map((dep) => {
|
|
81
|
-
const version = versionObj[dep];
|
|
82
|
-
if (!version) throw new Error(`No version found for ${dep}`);
|
|
83
|
-
return [dep, version];
|
|
84
|
-
}),
|
|
85
|
-
);
|
|
86
|
-
};
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { StyleGuard, type StyleGuardRule } from "./styleGuard";
|
|
3
|
-
|
|
4
|
-
const guard = new StyleGuard();
|
|
5
|
-
const scan = (content: string, path = "Demo.tsx") => guard.run([{ path, content }]);
|
|
6
|
-
const rules = (content: string): StyleGuardRule[] => scan(content).map((v) => v.rule);
|
|
7
|
-
|
|
8
|
-
describe("StyleGuard raw-palette", () => {
|
|
9
|
-
test("flags raw Tailwind palette utilities", () => {
|
|
10
|
-
expect(rules('<div className="bg-blue-500 text-gray-700" />')).toEqual(["raw-palette", "raw-palette"]);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test("flags palette with variant prefix and opacity", () => {
|
|
14
|
-
expect(rules('<div className="hover:bg-red-500/50" />')).toContain("raw-palette");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("flags numeric neutral but allows bare semantic neutral", () => {
|
|
18
|
-
expect(rules('<div className="bg-neutral-500" />')).toEqual(["raw-palette"]);
|
|
19
|
-
expect(scan('<div className="bg-neutral text-neutral-foreground" />')).toHaveLength(0);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("does not flag semantic tokens or black/white", () => {
|
|
23
|
-
expect(scan('<div className="bg-primary text-muted-foreground border-border" />')).toHaveLength(0);
|
|
24
|
-
expect(scan('<div className="bg-black text-white bg-white/30 bg-black/50" />')).toHaveLength(0);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("does not flag non-color numeric utilities", () => {
|
|
28
|
-
expect(scan('<div className="gap-4 mt-2 grid-cols-3 w-500" />')).toHaveLength(0);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("StyleGuard arbitrary-color", () => {
|
|
33
|
-
test("flags hex and color-function arbitrary values", () => {
|
|
34
|
-
expect(rules('<div className="bg-[#3b82f6]" />')).toEqual(["arbitrary-color"]);
|
|
35
|
-
expect(rules('<div className="text-[rgb(0,0,0)]" />')).toEqual(["arbitrary-color"]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("allows arbitrary CSS variable references", () => {
|
|
39
|
-
expect(scan('<div className="bg-[--brand] text-[var(--fg)]" />')).toHaveLength(0);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe("StyleGuard inline-color", () => {
|
|
44
|
-
test("flags hardcoded color in style object", () => {
|
|
45
|
-
expect(rules("<div style={{ color: '#fff', background: 'rgb(0,0,0)' }} />")).toEqual([
|
|
46
|
-
"inline-color",
|
|
47
|
-
"inline-color",
|
|
48
|
-
]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("flags color literal inside <style> tag", () => {
|
|
52
|
-
expect(rules("<style>{`.x { color: #abcdef; }`}</style>")).toEqual(["inline-color"]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("allows var() references in style object", () => {
|
|
56
|
-
expect(scan("<div style={{ color: 'var(--primary)', width: '100%' }} />")).toHaveLength(0);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe("StyleGuard daisyui-legacy", () => {
|
|
61
|
-
test("flags high-signal daisyUI compound classes", () => {
|
|
62
|
-
expect(rules('<button className="btn-primary" />')).toEqual(["daisyui-legacy"]);
|
|
63
|
-
expect(rules('<span className="badge-success" />')).toEqual(["daisyui-legacy"]);
|
|
64
|
-
expect(rules('<div className="modal-box card-body" />')).toEqual(["daisyui-legacy", "daisyui-legacy"]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("does not flag bare ambiguous names that collide with Tailwind", () => {
|
|
68
|
-
expect(scan('<div className="card input badge btn" />')).toHaveLength(0);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// The fixtures below must contain a literal `${`. Writing it inside a plain string trips biome's
|
|
73
|
-
// noTemplateCurlyInString, so the placeholder is assembled from `D` — that keeps the rule on for real code
|
|
74
|
-
// instead of scattering suppressions through the fixtures.
|
|
75
|
-
const D = "$";
|
|
76
|
-
const INTERPOLATED = {
|
|
77
|
-
size: `<div className={\`min-h-[${D}{minHeight}px] flex\`} />`,
|
|
78
|
-
color: `<div className={\`bg-[${D}{color}] w-full\`} />`,
|
|
79
|
-
brokenBracket: `<div className={\`min-h-[ w-full${D}{minHeight}px] flex\`} />`,
|
|
80
|
-
outsideBrackets: `<div className={\`flex gap-2 ${D}{isOpen ? "opacity-50" : ""}\`} />`,
|
|
81
|
-
styleProp: `<div style={{ minHeight }} className={\`flex ${D}{extra}\`} />`,
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
describe("StyleGuard interpolated-arbitrary", () => {
|
|
85
|
-
test("flags an arbitrary value assembled from a runtime expression", () => {
|
|
86
|
-
expect(rules(INTERPOLATED.size)).toEqual(["interpolated-arbitrary"]);
|
|
87
|
-
expect(rules(INTERPOLATED.color)).toEqual(["interpolated-arbitrary"]);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("flags the broken-bracket typo that swallows the next class", () => {
|
|
91
|
-
expect(rules(INTERPOLATED.brokenBracket)).toEqual(["interpolated-arbitrary"]);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("allows a literal arbitrary value, and interpolation outside brackets", () => {
|
|
95
|
-
expect(scan('<div className="min-h-[300px] flex" />')).toHaveLength(0);
|
|
96
|
-
expect(scan(INTERPOLATED.outsideBrackets)).toHaveLength(0);
|
|
97
|
-
expect(scan(INTERPOLATED.styleProp)).toHaveLength(0);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe("StyleGuard violation shape", () => {
|
|
102
|
-
test("reports 1-based line and trimmed snippet with a suggestion", () => {
|
|
103
|
-
const content = ['<div className="ok" />', ' <div className="bg-blue-500" />'].join("\n");
|
|
104
|
-
const [v] = scan(content);
|
|
105
|
-
expect(v.line).toBe(2);
|
|
106
|
-
expect(v.snippet).toBe('<div className="bg-blue-500" />');
|
|
107
|
-
expect(v.severity).toBe("error");
|
|
108
|
-
expect(v.suggestion.length).toBeGreaterThan(0);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("StyleGuard comment handling", () => {
|
|
113
|
-
test("does not flag class names inside line or block comments", () => {
|
|
114
|
-
expect(scan('// iconClassName="btn-primary bg-blue-500"')).toHaveLength(0);
|
|
115
|
-
expect(scan("/** legacy: toggle-accent / bg-red-500 */")).toHaveLength(0);
|
|
116
|
-
expect(scan("{/* <div className='bg-blue-500' /> */}")).toHaveLength(0);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("still flags real code on a line that also contains a string with //", () => {
|
|
120
|
-
expect(rules('<a href="https://x.io" className="bg-blue-500" />')).toEqual(["raw-palette"]);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe("StyleGuard escape hatch", () => {
|
|
125
|
-
test("styleguard-disable-next-line suppresses the following line only", () => {
|
|
126
|
-
const content = [
|
|
127
|
-
"// styleguard-disable-next-line raw-palette",
|
|
128
|
-
'<div className="bg-blue-500" />',
|
|
129
|
-
'<div className="bg-red-500" />',
|
|
130
|
-
].join("\n");
|
|
131
|
-
const found = scan(content);
|
|
132
|
-
expect(found).toHaveLength(1);
|
|
133
|
-
expect(found[0].line).toBe(3);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("file-level styleguard-disable suppresses the named rule everywhere", () => {
|
|
137
|
-
const content = ["// styleguard-disable raw-palette", '<div className="bg-blue-500 bg-[#fff]" />'].join("\n");
|
|
138
|
-
// raw-palette suppressed, arbitrary-color still reported.
|
|
139
|
-
expect(rules(content)).toEqual(["arbitrary-color"]);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("bare styleguard-disable suppresses all rules in the file", () => {
|
|
143
|
-
const content = ["/* styleguard-disable */", '<div className="bg-blue-500 btn-primary bg-[#fff]" />'].join("\n");
|
|
144
|
-
expect(scan(content)).toHaveLength(0);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe("StyleGuard.countNamedComponentClasses", () => {
|
|
149
|
-
test("counts named classes declared inside @layer components", () => {
|
|
150
|
-
const css = `
|
|
151
|
-
@layer components {
|
|
152
|
-
.foo { color: var(--primary); }
|
|
153
|
-
.bar-baz { padding: 1rem; }
|
|
154
|
-
}
|
|
155
|
-
.outside { color: red; }
|
|
156
|
-
`;
|
|
157
|
-
const metric = StyleGuard.countNamedComponentClasses(css);
|
|
158
|
-
expect(metric.count).toBe(2);
|
|
159
|
-
expect(metric.names).toEqual(["bar-baz", "foo"]);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("returns zero when no component layer exists", () => {
|
|
163
|
-
expect(StyleGuard.countNamedComponentClasses(".a { color: red; }").count).toBe(0);
|
|
164
|
-
});
|
|
165
|
-
});
|