@intentius/chant 0.0.4 → 0.0.8
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/README.md +10 -351
- package/bin/chant +20 -0
- package/package.json +18 -17
- package/src/bench.test.ts +3 -54
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +8 -23
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +22 -18
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +8 -23
- package/src/cli/commands/build.ts +1 -2
- package/src/cli/commands/import.test.ts +1 -1
- package/src/cli/commands/import.ts +2 -2
- package/src/cli/commands/init-lexicon.test.ts +0 -3
- package/src/cli/commands/init-lexicon.ts +31 -95
- package/src/cli/commands/init.test.ts +10 -14
- package/src/cli/commands/init.ts +16 -10
- package/src/cli/commands/lint.ts +9 -33
- package/src/cli/commands/list.ts +2 -2
- package/src/cli/commands/update.ts +5 -3
- package/src/cli/conflict-check.test.ts +0 -1
- package/src/cli/handlers/dev.ts +1 -9
- package/src/cli/main.ts +14 -4
- package/src/cli/mcp/server.test.ts +207 -4
- package/src/cli/mcp/server.ts +6 -0
- package/src/cli/mcp/tools/explain.ts +134 -0
- package/src/cli/mcp/tools/scaffold.ts +107 -0
- package/src/cli/mcp/tools/search.ts +98 -0
- package/src/codegen/docs-interpolation.test.ts +2 -2
- package/src/codegen/docs.ts +5 -4
- package/src/codegen/generate-registry.test.ts +2 -2
- package/src/codegen/generate-registry.ts +5 -6
- package/src/codegen/generate-typescript.test.ts +6 -6
- package/src/codegen/generate-typescript.ts +2 -6
- package/src/codegen/generate.ts +1 -12
- package/src/codegen/package.ts +28 -1
- package/src/codegen/typecheck.ts +6 -11
- package/src/codegen/validate.ts +16 -0
- package/src/config.ts +4 -0
- package/src/discovery/files.ts +6 -6
- package/src/discovery/import.ts +1 -1
- package/src/index.ts +1 -2
- package/src/lexicon-integrity.ts +5 -4
- package/src/lexicon.ts +2 -6
- package/src/lint/config.ts +8 -6
- package/src/lint/engine.ts +1 -5
- package/src/lint/rule.ts +0 -18
- package/src/lint/rules/evl009-composite-no-constant.test.ts +24 -8
- package/src/lint/rules/evl009-composite-no-constant.ts +50 -29
- package/src/lint/rules/index.ts +1 -22
- package/src/runtime-adapter.ts +158 -0
- package/src/serializer-walker.test.ts +0 -9
- package/src/serializer-walker.ts +1 -3
- package/src/stack-output.ts +3 -3
- package/src/barrel.test.ts +0 -157
- package/src/barrel.ts +0 -101
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
- package/src/codegen/case.test.ts +0 -30
- package/src/codegen/case.ts +0 -11
- package/src/codegen/rollback.test.ts +0 -92
- package/src/codegen/rollback.ts +0 -115
- package/src/lint/rules/barrel-import-style.test.ts +0 -80
- package/src/lint/rules/barrel-import-style.ts +0 -59
- package/src/lint/rules/enforce-barrel-import.test.ts +0 -169
- package/src/lint/rules/enforce-barrel-import.ts +0 -81
- package/src/lint/rules/enforce-barrel-ref.test.ts +0 -114
- package/src/lint/rules/enforce-barrel-ref.ts +0 -75
- package/src/lint/rules/evl006-barrel-usage.test.ts +0 -63
- package/src/lint/rules/evl006-barrel-usage.ts +0 -95
- package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +0 -118
- package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +0 -140
- package/src/lint/rules/prefer-namespace-import.test.ts +0 -102
- package/src/lint/rules/prefer-namespace-import.ts +0 -63
- package/src/lint/rules/stale-barrel-types.ts +0 -60
- package/src/project/scan.test.ts +0 -178
- package/src/project/scan.ts +0 -182
- package/src/project/sync.test.ts +0 -87
- package/src/project/sync.ts +0 -46
package/src/barrel.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { readdirSync, readFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
|
|
4
|
-
export function barrel(dir: string): Record<string, unknown> {
|
|
5
|
-
let allExports: Record<string, unknown> | null = null;
|
|
6
|
-
|
|
7
|
-
function load(): Record<string, unknown> {
|
|
8
|
-
if (allExports) return allExports;
|
|
9
|
-
allExports = {};
|
|
10
|
-
|
|
11
|
-
const files = readdirSync(dir)
|
|
12
|
-
.filter(
|
|
13
|
-
(f) =>
|
|
14
|
-
f.endsWith(".ts") &&
|
|
15
|
-
!f.startsWith("_") &&
|
|
16
|
-
!f.endsWith(".test.ts") &&
|
|
17
|
-
!f.endsWith(".spec.ts"),
|
|
18
|
-
)
|
|
19
|
-
.sort();
|
|
20
|
-
|
|
21
|
-
// Identify files that reference the barrel (.$. or .$[) — these
|
|
22
|
-
// may silently resolve cross-references to undefined if their
|
|
23
|
-
// dependency files haven't loaded yet
|
|
24
|
-
const barrelRefPattern = /\.\$[.\[]/;
|
|
25
|
-
const usesBarrel = new Set<string>();
|
|
26
|
-
for (const file of files) {
|
|
27
|
-
const src = readFileSync(join(dir, file), "utf-8");
|
|
28
|
-
if (barrelRefPattern.test(src)) {
|
|
29
|
-
usesBarrel.add(file);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function loadFile(file: string, overwrite = false): boolean {
|
|
34
|
-
const fullPath = join(dir, file);
|
|
35
|
-
try {
|
|
36
|
-
const mod = require(fullPath);
|
|
37
|
-
for (const [key, val] of Object.entries(mod)) {
|
|
38
|
-
if (val !== undefined && (overwrite || !(key in allExports!))) {
|
|
39
|
-
allExports![key] = val;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return true;
|
|
43
|
-
} catch {
|
|
44
|
-
// Clear require cache so retry re-executes the file
|
|
45
|
-
delete require.cache[require.resolve(fullPath)];
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// First pass — load all files in alphabetical order
|
|
51
|
-
const failed: string[] = [];
|
|
52
|
-
for (const file of files) {
|
|
53
|
-
if (!loadFile(file)) {
|
|
54
|
-
failed.push(file);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Retry files that threw — their dependencies are now available
|
|
59
|
-
for (const file of failed) {
|
|
60
|
-
loadFile(file);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Second pass — reload files that reference the barrel so
|
|
64
|
-
// cross-references that silently resolved to undefined now
|
|
65
|
-
// pick up the correct values. Files without barrel references
|
|
66
|
-
// keep their original instances to preserve the reference graph.
|
|
67
|
-
for (const file of files) {
|
|
68
|
-
if (!usesBarrel.has(file)) continue;
|
|
69
|
-
const fullPath = join(dir, file);
|
|
70
|
-
try { delete require.cache[require.resolve(fullPath)]; } catch {}
|
|
71
|
-
}
|
|
72
|
-
for (const file of files) {
|
|
73
|
-
if (!usesBarrel.has(file)) continue;
|
|
74
|
-
loadFile(file, true);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return allExports;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return new Proxy<Record<string, unknown>>({}, {
|
|
81
|
-
get(_, prop: string | symbol) {
|
|
82
|
-
if (typeof prop === 'symbol') return undefined;
|
|
83
|
-
return load()[prop];
|
|
84
|
-
},
|
|
85
|
-
has(_, prop: string | symbol) {
|
|
86
|
-
if (typeof prop === 'symbol') return false;
|
|
87
|
-
return prop in load();
|
|
88
|
-
},
|
|
89
|
-
ownKeys(_) {
|
|
90
|
-
return Object.keys(load());
|
|
91
|
-
},
|
|
92
|
-
getOwnPropertyDescriptor(_, prop: string | symbol) {
|
|
93
|
-
if (typeof prop === 'symbol') return undefined;
|
|
94
|
-
const exports = load();
|
|
95
|
-
if (prop in exports) {
|
|
96
|
-
return { configurable: true, enumerable: true, value: exports[prop] };
|
|
97
|
-
}
|
|
98
|
-
return undefined;
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, cpSync } from "fs";
|
|
2
|
-
import { join, basename } from "path";
|
|
3
|
-
|
|
4
|
-
export interface Snapshot {
|
|
5
|
-
timestamp: string;
|
|
6
|
-
resources: number;
|
|
7
|
-
path: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* List available generation snapshots.
|
|
12
|
-
*/
|
|
13
|
-
export function listSnapshots(snapshotsDir: string): Snapshot[] {
|
|
14
|
-
if (!existsSync(snapshotsDir)) return [];
|
|
15
|
-
|
|
16
|
-
return readdirSync(snapshotsDir)
|
|
17
|
-
.filter((d) => !d.startsWith("."))
|
|
18
|
-
.sort()
|
|
19
|
-
.reverse()
|
|
20
|
-
.map((dir) => {
|
|
21
|
-
const fullPath = join(snapshotsDir, dir);
|
|
22
|
-
const metaPath = join(fullPath, "meta.json");
|
|
23
|
-
let resources = 0;
|
|
24
|
-
if (existsSync(metaPath)) {
|
|
25
|
-
try {
|
|
26
|
-
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
27
|
-
resources = meta.resources ?? 0;
|
|
28
|
-
} catch {}
|
|
29
|
-
}
|
|
30
|
-
return { timestamp: dir, resources, path: fullPath };
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Restore a snapshot to the generated directory.
|
|
36
|
-
*/
|
|
37
|
-
export function restoreSnapshot(timestamp: string, generatedDir: string): void {
|
|
38
|
-
const snapshotsDir = join(generatedDir, "..", "..", ".snapshots");
|
|
39
|
-
const snapshotDir = join(snapshotsDir, timestamp);
|
|
40
|
-
if (!existsSync(snapshotDir)) {
|
|
41
|
-
throw new Error(`Snapshot not found: ${timestamp}`);
|
|
42
|
-
}
|
|
43
|
-
mkdirSync(generatedDir, { recursive: true });
|
|
44
|
-
cpSync(snapshotDir, generatedDir, { recursive: true });
|
|
45
|
-
}
|
package/src/codegen/case.test.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { toCamelCase, toPascalCase } from "./case";
|
|
3
|
-
|
|
4
|
-
describe("toCamelCase", () => {
|
|
5
|
-
test("lowercases first character", () => {
|
|
6
|
-
expect(toCamelCase("BucketName")).toBe("bucketName");
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
test("preserves already-camelCase", () => {
|
|
10
|
-
expect(toCamelCase("already")).toBe("already");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test("handles single character", () => {
|
|
14
|
-
expect(toCamelCase("A")).toBe("a");
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("toPascalCase", () => {
|
|
19
|
-
test("uppercases first character", () => {
|
|
20
|
-
expect(toPascalCase("bucketName")).toBe("BucketName");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("preserves already-PascalCase", () => {
|
|
24
|
-
expect(toPascalCase("Already")).toBe("Already");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("handles single character", () => {
|
|
28
|
-
expect(toPascalCase("a")).toBe("A");
|
|
29
|
-
});
|
|
30
|
-
});
|
package/src/codegen/case.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Case conversion utilities for codegen.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export function toCamelCase(name: string): string {
|
|
6
|
-
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function toPascalCase(name: string): string {
|
|
10
|
-
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
11
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { mkdirSync, writeFileSync, rmSync, readFileSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { tmpdir } from "os";
|
|
5
|
-
import { snapshotArtifacts, saveSnapshot, restoreSnapshot, listSnapshots } from "./rollback";
|
|
6
|
-
|
|
7
|
-
function makeTempDir(): string {
|
|
8
|
-
const dir = join(tmpdir(), `chant-rollback-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
9
|
-
mkdirSync(dir, { recursive: true });
|
|
10
|
-
return dir;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
describe("rollback", () => {
|
|
14
|
-
test("snapshot captures generated files with default artifact names", () => {
|
|
15
|
-
const dir = makeTempDir();
|
|
16
|
-
const genDir = join(dir, "generated");
|
|
17
|
-
mkdirSync(genDir, { recursive: true });
|
|
18
|
-
|
|
19
|
-
writeFileSync(join(genDir, "lexicon.json"), '{"Bucket":{"kind":"resource"}}');
|
|
20
|
-
writeFileSync(join(genDir, "index.d.ts"), "declare class Bucket {}");
|
|
21
|
-
writeFileSync(join(genDir, "index.ts"), "export const Bucket = {};");
|
|
22
|
-
|
|
23
|
-
const snapshot = snapshotArtifacts(genDir);
|
|
24
|
-
expect(snapshot.files["lexicon.json"]).toBeDefined();
|
|
25
|
-
expect(snapshot.files["index.d.ts"]).toBeDefined();
|
|
26
|
-
expect(snapshot.files["index.ts"]).toBeDefined();
|
|
27
|
-
expect(snapshot.hashes["lexicon.json"]).toBeDefined();
|
|
28
|
-
expect(snapshot.resourceCount).toBe(1);
|
|
29
|
-
|
|
30
|
-
rmSync(dir, { recursive: true, force: true });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("snapshot uses custom artifact names", () => {
|
|
34
|
-
const dir = makeTempDir();
|
|
35
|
-
const genDir = join(dir, "generated");
|
|
36
|
-
mkdirSync(genDir, { recursive: true });
|
|
37
|
-
|
|
38
|
-
writeFileSync(join(genDir, "my-lexicon.json"), '{"Resource":{"kind":"resource"}}');
|
|
39
|
-
writeFileSync(join(genDir, "types.d.ts"), "declare class Resource {}");
|
|
40
|
-
|
|
41
|
-
const snapshot = snapshotArtifacts(genDir, ["my-lexicon.json", "types.d.ts"]);
|
|
42
|
-
expect(snapshot.files["my-lexicon.json"]).toBeDefined();
|
|
43
|
-
expect(snapshot.files["types.d.ts"]).toBeDefined();
|
|
44
|
-
expect(snapshot.resourceCount).toBe(1);
|
|
45
|
-
|
|
46
|
-
rmSync(dir, { recursive: true, force: true });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("save and list snapshots", () => {
|
|
50
|
-
const dir = makeTempDir();
|
|
51
|
-
const snapshotsDir = join(dir, ".snapshots");
|
|
52
|
-
|
|
53
|
-
const snapshot = {
|
|
54
|
-
timestamp: "2025-01-01T00:00:00.000Z",
|
|
55
|
-
files: { "test.json": "{}" },
|
|
56
|
-
hashes: { "test.json": "abc123" },
|
|
57
|
-
resourceCount: 0,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
saveSnapshot(snapshot, snapshotsDir);
|
|
61
|
-
const list = listSnapshots(snapshotsDir);
|
|
62
|
-
expect(list.length).toBe(1);
|
|
63
|
-
expect(list[0].timestamp).toBe("2025-01-01T00:00:00.000Z");
|
|
64
|
-
|
|
65
|
-
rmSync(dir, { recursive: true, force: true });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("restore snapshot overwrites generated files", () => {
|
|
69
|
-
const dir = makeTempDir();
|
|
70
|
-
const genDir = join(dir, "generated");
|
|
71
|
-
const snapshotsDir = join(dir, ".snapshots");
|
|
72
|
-
mkdirSync(genDir, { recursive: true });
|
|
73
|
-
|
|
74
|
-
writeFileSync(join(genDir, "lexicon.json"), '{"original":true}');
|
|
75
|
-
|
|
76
|
-
const snapshot = snapshotArtifacts(genDir);
|
|
77
|
-
const snapshotPath = saveSnapshot(snapshot, snapshotsDir);
|
|
78
|
-
|
|
79
|
-
writeFileSync(join(genDir, "lexicon.json"), '{"modified":true}');
|
|
80
|
-
expect(readFileSync(join(genDir, "lexicon.json"), "utf-8")).toBe('{"modified":true}');
|
|
81
|
-
|
|
82
|
-
restoreSnapshot(snapshotPath, genDir);
|
|
83
|
-
expect(readFileSync(join(genDir, "lexicon.json"), "utf-8")).toBe('{"original":true}');
|
|
84
|
-
|
|
85
|
-
rmSync(dir, { recursive: true, force: true });
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("listSnapshots returns empty for nonexistent dir", () => {
|
|
89
|
-
const list = listSnapshots("/nonexistent/path");
|
|
90
|
-
expect(list).toHaveLength(0);
|
|
91
|
-
});
|
|
92
|
-
});
|
package/src/codegen/rollback.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Artifact snapshot and restore for generation rollback.
|
|
3
|
-
*/
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { hashArtifact } from "../lexicon-integrity";
|
|
7
|
-
|
|
8
|
-
export interface ArtifactSnapshot {
|
|
9
|
-
timestamp: string;
|
|
10
|
-
files: Record<string, string>;
|
|
11
|
-
hashes: Record<string, string>;
|
|
12
|
-
resourceCount: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface SnapshotInfo {
|
|
16
|
-
path: string;
|
|
17
|
-
timestamp: string;
|
|
18
|
-
resourceCount: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const DEFAULT_ARTIFACT_NAMES = ["lexicon.json", "index.d.ts", "index.ts"];
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Snapshot current generated artifacts.
|
|
25
|
-
*
|
|
26
|
-
* @param generatedDir - Directory containing generated artifacts
|
|
27
|
-
* @param artifactNames - List of filenames to snapshot (defaults to generic names)
|
|
28
|
-
*/
|
|
29
|
-
export function snapshotArtifacts(
|
|
30
|
-
generatedDir: string,
|
|
31
|
-
artifactNames: string[] = DEFAULT_ARTIFACT_NAMES,
|
|
32
|
-
): ArtifactSnapshot {
|
|
33
|
-
const files: Record<string, string> = {};
|
|
34
|
-
const hashes: Record<string, string> = {};
|
|
35
|
-
let resourceCount = 0;
|
|
36
|
-
|
|
37
|
-
for (const entry of artifactNames) {
|
|
38
|
-
const path = join(generatedDir, entry);
|
|
39
|
-
if (existsSync(path)) {
|
|
40
|
-
const content = readFileSync(path, "utf-8");
|
|
41
|
-
files[entry] = content;
|
|
42
|
-
hashes[entry] = hashArtifact(content);
|
|
43
|
-
|
|
44
|
-
// Count resources in any .json artifact
|
|
45
|
-
if (entry.endsWith(".json")) {
|
|
46
|
-
try {
|
|
47
|
-
const parsed = JSON.parse(content);
|
|
48
|
-
resourceCount = Object.values(parsed).filter(
|
|
49
|
-
(e: any) => e && typeof e === "object" && e.kind === "resource"
|
|
50
|
-
).length;
|
|
51
|
-
} catch {}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
timestamp: new Date().toISOString(),
|
|
58
|
-
files,
|
|
59
|
-
hashes,
|
|
60
|
-
resourceCount,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Save a snapshot to the .snapshots directory.
|
|
66
|
-
*/
|
|
67
|
-
export function saveSnapshot(snapshot: ArtifactSnapshot, snapshotsDir: string): string {
|
|
68
|
-
mkdirSync(snapshotsDir, { recursive: true });
|
|
69
|
-
|
|
70
|
-
const filename = `${snapshot.timestamp.replace(/[:.]/g, "-")}.json`;
|
|
71
|
-
const path = join(snapshotsDir, filename);
|
|
72
|
-
writeFileSync(path, JSON.stringify(snapshot, null, 2));
|
|
73
|
-
return path;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Restore a snapshot to the generated directory.
|
|
78
|
-
*/
|
|
79
|
-
export function restoreSnapshot(snapshotPath: string, generatedDir: string): void {
|
|
80
|
-
const raw = readFileSync(snapshotPath, "utf-8");
|
|
81
|
-
const snapshot: ArtifactSnapshot = JSON.parse(raw);
|
|
82
|
-
|
|
83
|
-
mkdirSync(generatedDir, { recursive: true });
|
|
84
|
-
for (const [filename, content] of Object.entries(snapshot.files)) {
|
|
85
|
-
writeFileSync(join(generatedDir, filename), content);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* List available snapshots.
|
|
91
|
-
*/
|
|
92
|
-
export function listSnapshots(snapshotsDir: string): SnapshotInfo[] {
|
|
93
|
-
if (!existsSync(snapshotsDir)) return [];
|
|
94
|
-
|
|
95
|
-
const entries = readdirSync(snapshotsDir)
|
|
96
|
-
.filter((f) => f.endsWith(".json"))
|
|
97
|
-
.sort()
|
|
98
|
-
.reverse();
|
|
99
|
-
|
|
100
|
-
const snapshots: SnapshotInfo[] = [];
|
|
101
|
-
for (const entry of entries) {
|
|
102
|
-
try {
|
|
103
|
-
const path = join(snapshotsDir, entry);
|
|
104
|
-
const raw = readFileSync(path, "utf-8");
|
|
105
|
-
const data = JSON.parse(raw);
|
|
106
|
-
snapshots.push({
|
|
107
|
-
path,
|
|
108
|
-
timestamp: data.timestamp,
|
|
109
|
-
resourceCount: data.resourceCount ?? 0,
|
|
110
|
-
});
|
|
111
|
-
} catch {}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return snapshots;
|
|
115
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import * as ts from "typescript";
|
|
3
|
-
import { barrelImportStyleRule } from "./barrel-import-style";
|
|
4
|
-
import type { LintContext } from "../rule";
|
|
5
|
-
|
|
6
|
-
function createContext(code: string, filePath = "test.ts"): LintContext {
|
|
7
|
-
const sourceFile = ts.createSourceFile(
|
|
8
|
-
filePath,
|
|
9
|
-
code,
|
|
10
|
-
ts.ScriptTarget.Latest,
|
|
11
|
-
true
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
sourceFile,
|
|
16
|
-
entities: [],
|
|
17
|
-
filePath,
|
|
18
|
-
lexicon: undefined,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe("COR002: barrel-import-style", () => {
|
|
23
|
-
test("rule metadata", () => {
|
|
24
|
-
expect(barrelImportStyleRule.id).toBe("COR002");
|
|
25
|
-
expect(barrelImportStyleRule.severity).toBe("error");
|
|
26
|
-
expect(barrelImportStyleRule.category).toBe("style");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("flags named import from ./_", () => {
|
|
30
|
-
const code = `import { bucketEncryption } from "./_";`;
|
|
31
|
-
const context = createContext(code);
|
|
32
|
-
const diagnostics = barrelImportStyleRule.check(context);
|
|
33
|
-
|
|
34
|
-
expect(diagnostics).toHaveLength(1);
|
|
35
|
-
expect(diagnostics[0].ruleId).toBe("COR002");
|
|
36
|
-
expect(diagnostics[0].severity).toBe("error");
|
|
37
|
-
expect(diagnostics[0].message).toBe(
|
|
38
|
-
`Use namespace import for local barrel — replace with: import * as _ from "./_"`
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("allows namespace import from ./_", () => {
|
|
43
|
-
const code = `import * as _ from "./_";`;
|
|
44
|
-
const context = createContext(code);
|
|
45
|
-
const diagnostics = barrelImportStyleRule.check(context);
|
|
46
|
-
expect(diagnostics).toHaveLength(0);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("allows type-only import from ./_", () => {
|
|
50
|
-
const code = `import type { Config } from "./_";`;
|
|
51
|
-
const context = createContext(code);
|
|
52
|
-
const diagnostics = barrelImportStyleRule.check(context);
|
|
53
|
-
expect(diagnostics).toHaveLength(0);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("does not flag imports from other relative paths", () => {
|
|
57
|
-
const code = `import { helper } from "./utils";`;
|
|
58
|
-
const context = createContext(code);
|
|
59
|
-
const diagnostics = barrelImportStyleRule.check(context);
|
|
60
|
-
expect(diagnostics).toHaveLength(0);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("does not flag imports from packages", () => {
|
|
64
|
-
const code = `import { useState } from "react";`;
|
|
65
|
-
const context = createContext(code);
|
|
66
|
-
const diagnostics = barrelImportStyleRule.check(context);
|
|
67
|
-
expect(diagnostics).toHaveLength(0);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("reports correct line and column numbers", () => {
|
|
71
|
-
const code = `import { foo } from "./_";`;
|
|
72
|
-
const context = createContext(code);
|
|
73
|
-
const diagnostics = barrelImportStyleRule.check(context);
|
|
74
|
-
|
|
75
|
-
expect(diagnostics).toHaveLength(1);
|
|
76
|
-
expect(diagnostics[0].line).toBe(1);
|
|
77
|
-
expect(diagnostics[0].column).toBe(1);
|
|
78
|
-
expect(diagnostics[0].file).toBe("test.ts");
|
|
79
|
-
});
|
|
80
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import * as ts from "typescript";
|
|
2
|
-
import type { LintRule, LintContext, LintDiagnostic } from "../rule";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* COR002: barrel-import-style
|
|
6
|
-
*
|
|
7
|
-
* Enforce `import * as _` for local `_.ts` barrel imports.
|
|
8
|
-
*
|
|
9
|
-
* Triggers on: import { bucketEncryption } from "./_"
|
|
10
|
-
* OK: import * as _ from "./_"
|
|
11
|
-
* OK: import type { Config } from "./_"
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const barrelPattern = /^\.\/(_|_\..*)$/;
|
|
15
|
-
|
|
16
|
-
function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
|
|
17
|
-
if (ts.isImportDeclaration(node)) {
|
|
18
|
-
const moduleSpecifier = node.moduleSpecifier;
|
|
19
|
-
if (!ts.isStringLiteral(moduleSpecifier)) return;
|
|
20
|
-
|
|
21
|
-
const modulePath = moduleSpecifier.text;
|
|
22
|
-
if (!barrelPattern.test(modulePath)) return;
|
|
23
|
-
|
|
24
|
-
// Skip type-only imports: import type { X } from "..."
|
|
25
|
-
if (node.importClause?.isTypeOnly) return;
|
|
26
|
-
|
|
27
|
-
const importClause = node.importClause;
|
|
28
|
-
if (!importClause?.namedBindings) return;
|
|
29
|
-
|
|
30
|
-
// Flag named imports (not namespace imports)
|
|
31
|
-
if (ts.isNamedImports(importClause.namedBindings)) {
|
|
32
|
-
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
33
|
-
node.getStart(context.sourceFile)
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
diagnostics.push({
|
|
37
|
-
file: context.filePath,
|
|
38
|
-
line: line + 1,
|
|
39
|
-
column: character + 1,
|
|
40
|
-
ruleId: "COR002",
|
|
41
|
-
severity: "error",
|
|
42
|
-
message: `Use namespace import for local barrel — replace with: import * as _ from "./_"`,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
ts.forEachChild(node, child => checkNode(child, context, diagnostics));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export const barrelImportStyleRule: LintRule = {
|
|
51
|
-
id: "COR002",
|
|
52
|
-
severity: "error",
|
|
53
|
-
category: "style",
|
|
54
|
-
check(context: LintContext): LintDiagnostic[] {
|
|
55
|
-
const diagnostics: LintDiagnostic[] = [];
|
|
56
|
-
checkNode(context.sourceFile, context, diagnostics);
|
|
57
|
-
return diagnostics;
|
|
58
|
-
},
|
|
59
|
-
};
|