@platforma-sdk/block-tools 2.10.14 → 2.10.15
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/dist/cli.js +23 -2
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1944 -100
- package/dist/cli.mjs.map +1 -1
- package/dist/cmd/index.d.ts +10 -4
- package/dist/cmd/structure/check.d.ts +10 -0
- package/dist/cmd/structure/init.d.ts +18 -0
- package/dist/cmd/structure/refresh.d.ts +11 -0
- package/dist/cmd/update-deps.d.ts +7 -0
- package/dist/structure/cli/run-structure.d.ts +19 -0
- package/dist/structure/engine/api.d.ts +55 -0
- package/dist/structure/engine/builders.d.ts +95 -0
- package/dist/structure/engine/content-rules.d.ts +148 -0
- package/dist/structure/engine/ctx.d.ts +34 -0
- package/dist/structure/engine/discovery-fs.d.ts +22 -0
- package/dist/structure/engine/discovery.d.ts +39 -0
- package/dist/structure/engine/flatten.d.ts +3 -0
- package/dist/structure/engine/fs/api.d.ts +27 -0
- package/dist/structure/engine/fs/memory.d.ts +16 -0
- package/dist/structure/engine/fs/node.d.ts +14 -0
- package/dist/structure/engine/glob.d.ts +4 -0
- package/dist/structure/engine/ir.d.ts +124 -0
- package/dist/structure/engine/parsers/json.d.ts +37 -0
- package/dist/structure/engine/parsers/lines.d.ts +9 -0
- package/dist/structure/engine/parsers/yaml.d.ts +18 -0
- package/dist/structure/engine/registry-client.d.ts +34 -0
- package/dist/structure/engine/runner.d.ts +45 -0
- package/dist/structure/engine/templates.d.ts +30 -0
- package/dist/structure/engine/testing.d.ts +34 -0
- package/dist/structure/engine/version.d.ts +18 -0
- package/dist/structure/init-block-constructor.d.ts +32 -0
- package/dist/structure/rules/block-package-json.d.ts +9 -0
- package/dist/structure/rules/block.d.ts +1 -0
- package/dist/structure/rules/migrations.d.ts +2 -0
- package/dist/structure/rules/model-package-json.d.ts +3 -0
- package/dist/structure/rules/model.d.ts +1 -0
- package/dist/structure/rules/root-catalog-bump.d.ts +1 -0
- package/dist/structure/rules/root-gitignore.d.ts +2 -0
- package/dist/structure/rules/root-package-json.d.ts +3 -0
- package/dist/structure/rules/root-pnpm-workspace.d.ts +15 -0
- package/dist/structure/rules/root.d.ts +1 -0
- package/dist/structure/rules/shared/colocated-tests.d.ts +2 -0
- package/dist/structure/rules/shared/key-order.d.ts +1 -0
- package/dist/structure/rules/software-package-json.d.ts +3 -0
- package/dist/structure/rules/software.d.ts +1 -0
- package/dist/structure/rules/test-package-json.d.ts +3 -0
- package/dist/structure/rules/test.d.ts +1 -0
- package/dist/structure/rules/ui-package-json.d.ts +3 -0
- package/dist/structure/rules/ui.d.ts +1 -0
- package/dist/structure/rules/workflow-package-json.d.ts +3 -0
- package/dist/structure/rules/workflow.d.ts +1 -0
- package/dist/structure/structure-definition.d.ts +1 -0
- package/package.json +9 -7
- package/src/cmd/index.ts +10 -4
- package/src/cmd/structure/check.ts +51 -0
- package/src/cmd/structure/init.ts +65 -0
- package/src/cmd/structure/refresh.ts +44 -0
- package/src/cmd/update-deps.ts +15 -3
- package/src/structure/__tests__/colocated-test-wiring.test.ts +107 -0
- package/src/structure/__tests__/floor-version.test.ts +52 -0
- package/src/structure/__tests__/init-catalog-resolution.test.ts +82 -0
- package/src/structure/__tests__/init-check-parity.test.ts +59 -0
- package/src/structure/__tests__/model-package-json.snapshot.test.ts +60 -0
- package/src/structure/__tests__/oxfmt-clean-init.test.ts +64 -0
- package/src/structure/__tests__/root-catalog-bump.test.ts +110 -0
- package/src/structure/__tests__/seed-disjointness.test.ts +79 -0
- package/src/structure/cli/run-structure.ts +89 -0
- package/src/structure/engine/__tests__/content-rules/active-state-guards.test.ts +61 -0
- package/src/structure/engine/__tests__/content-rules/catalog-pin.test.ts +46 -0
- package/src/structure/engine/__tests__/content-rules/enforce-alphabetical-order.test.ts +55 -0
- package/src/structure/engine/__tests__/content-rules/enforce-field-order.test.ts +73 -0
- package/src/structure/engine/__tests__/content-rules/ensure-catalog-absent.test.ts +29 -0
- package/src/structure/engine/__tests__/content-rules/ensure-catalog-latest.test.ts +70 -0
- package/src/structure/engine/__tests__/content-rules/ensure-dep.test.ts +63 -0
- package/src/structure/engine/__tests__/content-rules/ensure-deps-bulk.test.ts +46 -0
- package/src/structure/engine/__tests__/content-rules/ensure-field-entries.test.ts +35 -0
- package/src/structure/engine/__tests__/content-rules/ensure-field.test.ts +56 -0
- package/src/structure/engine/__tests__/content-rules/ensure-script.test.ts +39 -0
- package/src/structure/engine/__tests__/content-rules/ensure-workspace-scope-deps.test.ts +92 -0
- package/src/structure/engine/__tests__/content-rules/gitignore.test.ts +50 -0
- package/src/structure/engine/__tests__/content-rules/pin-catalog-to.test.ts +64 -0
- package/src/structure/engine/__tests__/content-rules/prune-keys.test.ts +59 -0
- package/src/structure/engine/__tests__/content-rules/registry-client.test.ts +116 -0
- package/src/structure/engine/__tests__/content-rules/remove-field.test.ts +49 -0
- package/src/structure/engine/__tests__/content-rules/require-field.test.ts +40 -0
- package/src/structure/engine/__tests__/content-rules/transform-at.test.ts +33 -0
- package/src/structure/engine/__tests__/content-rules/when-inner.test.ts +244 -0
- package/src/structure/engine/__tests__/content-rules/workspace-module-paths.test.ts +53 -0
- package/src/structure/engine/__tests__/discovery.test.ts +160 -0
- package/src/structure/engine/__tests__/fs-memory.test.ts +59 -0
- package/src/structure/engine/__tests__/glob.test.ts +43 -0
- package/src/structure/engine/__tests__/parsers-json.test.ts +189 -0
- package/src/structure/engine/__tests__/parsers-lines.test.ts +44 -0
- package/src/structure/engine/__tests__/parsers-yaml.test.ts +90 -0
- package/src/structure/engine/__tests__/recheck.test.ts +99 -0
- package/src/structure/engine/__tests__/registry-lookup.test.ts +49 -0
- package/src/structure/engine/__tests__/runner-when-inside-managed.test.ts +136 -0
- package/src/structure/engine/__tests__/runner.test.ts +136 -0
- package/src/structure/engine/__tests__/synthetic.test.ts +153 -0
- package/src/structure/engine/__tests__/version.test.ts +44 -0
- package/src/structure/engine/__tests__/when-files-exist.test.ts +89 -0
- package/src/structure/engine/api.ts +131 -0
- package/src/structure/engine/builders.ts +341 -0
- package/src/structure/engine/content-rules.ts +616 -0
- package/src/structure/engine/ctx.ts +94 -0
- package/src/structure/engine/discovery-fs.ts +209 -0
- package/src/structure/engine/discovery.ts +168 -0
- package/src/structure/engine/flatten.ts +117 -0
- package/src/structure/engine/fs/api.ts +40 -0
- package/src/structure/engine/fs/memory.ts +132 -0
- package/src/structure/engine/fs/node.ts +89 -0
- package/src/structure/engine/glob.ts +42 -0
- package/src/structure/engine/ir.ts +149 -0
- package/src/structure/engine/parsers/json.ts +212 -0
- package/src/structure/engine/parsers/lines.ts +42 -0
- package/src/structure/engine/parsers/yaml.ts +72 -0
- package/src/structure/engine/registry-client.ts +140 -0
- package/src/structure/engine/runner.ts +519 -0
- package/src/structure/engine/templates.ts +74 -0
- package/src/structure/engine/testing.ts +93 -0
- package/src/structure/engine/version.ts +60 -0
- package/src/structure/init-block-constructor.ts +164 -0
- package/src/structure/rules/block-package-json.ts +156 -0
- package/src/structure/rules/block.ts +29 -0
- package/src/structure/rules/migrations.ts +63 -0
- package/src/structure/rules/model-package-json.ts +123 -0
- package/src/structure/rules/model.ts +65 -0
- package/src/structure/rules/root-catalog-bump.ts +77 -0
- package/src/structure/rules/root-gitignore.ts +27 -0
- package/src/structure/rules/root-package-json.ts +119 -0
- package/src/structure/rules/root-pnpm-workspace.ts +103 -0
- package/src/structure/rules/root.ts +43 -0
- package/src/structure/rules/shared/colocated-tests.ts +11 -0
- package/src/structure/rules/shared/key-order.ts +34 -0
- package/src/structure/rules/software-package-json.ts +86 -0
- package/src/structure/rules/software.ts +33 -0
- package/src/structure/rules/test-package-json.ts +70 -0
- package/src/structure/rules/test.ts +40 -0
- package/src/structure/rules/ui-package-json.ts +108 -0
- package/src/structure/rules/ui.ts +75 -0
- package/src/structure/rules/workflow-package-json.ts +92 -0
- package/src/structure/rules/workflow.ts +51 -0
- package/src/structure/structure-definition.ts +28 -0
- package/src/structure/templates/static/block/index.d.ts +6 -0
- package/src/structure/templates/static/block/index.js +8 -0
- package/src/structure/templates/static/block/logos/block-logo.png +0 -0
- package/src/structure/templates/static/block/logos/organization-logo.png +0 -0
- package/src/structure/templates/static/model/.oxfmtrc.json +4 -0
- package/src/structure/templates/static/model/.oxlintrc.json +3 -0
- package/src/structure/templates/static/model/src/index.ts +13 -0
- package/src/structure/templates/static/model/tsconfig.json +4 -0
- package/src/structure/templates/static/root/.gitignore +13 -0
- package/src/structure/templates/static/root/.vscode/settings.json +3 -0
- package/src/structure/templates/static/root/turbo.json +35 -0
- package/src/structure/templates/static/test/tsconfig.json +5 -0
- package/src/structure/templates/static/test/vitest.config.mts +8 -0
- package/src/structure/templates/static/ui/.oxfmtrc.json +3 -0
- package/src/structure/templates/static/ui/.oxlintrc.json +3 -0
- package/src/structure/templates/static/ui/index.html +12 -0
- package/src/structure/templates/static/ui/src/main.ts +5 -0
- package/src/structure/templates/static/ui/tsconfig.json +4 -0
- package/src/structure/templates/static/workflow/format.el +42 -0
- package/src/structure/templates/static/workflow/tsconfig.json +16 -0
- package/src/structure/templates/static/workflow/vitest.config.mts +9 -0
- package/src/structure/templates/text/README.md +5 -0
- package/src/structure/templates/text/docs/description.md +3 -0
- package/src/structure/templates/text/ui/src/MainPage.tpl.vue +9 -0
- package/src/structure/templates/text/ui/src/app.tpl.ts +11 -0
- package/src/structure/templates/text/workflow/src/main.tpl.tengo +10 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// In-memory FS implementation. Files are stored as a flat
|
|
2
|
+
// `Map<path, string>`; directories exist implicitly when any contained
|
|
3
|
+
// file exists. Used by tests, by `simulateInit`, and by the runner's
|
|
4
|
+
// post-run recheck dry-run pass.
|
|
5
|
+
|
|
6
|
+
import type { DirEntry, FileSystem } from "./api";
|
|
7
|
+
|
|
8
|
+
function normalise(p: string): string {
|
|
9
|
+
// Collapse repeated slashes, strip leading "./" and trailing "/".
|
|
10
|
+
let out = p.replace(/\/{2,}/g, "/");
|
|
11
|
+
if (out.startsWith("./")) out = out.slice(2);
|
|
12
|
+
if (out.length > 1 && out.endsWith("/")) out = out.slice(0, -1);
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class MemoryFileSystem implements FileSystem {
|
|
17
|
+
private files = new Map<string, string | Uint8Array>();
|
|
18
|
+
|
|
19
|
+
constructor(initial?: Record<string, string>) {
|
|
20
|
+
if (initial) {
|
|
21
|
+
for (const [k, v] of Object.entries(initial)) {
|
|
22
|
+
this.files.set(normalise(k), v);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Snapshot of all files — handy for assertions. Binary entries are
|
|
28
|
+
* rendered as a placeholder so the snapshot stays a string map. */
|
|
29
|
+
snapshot(): Record<string, string> {
|
|
30
|
+
const out: Record<string, string> = {};
|
|
31
|
+
for (const [k, v] of [...this.files.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
32
|
+
out[k] = typeof v === "string" ? v : `<binary:${v.byteLength} bytes>`;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
read(path: string): string {
|
|
38
|
+
const n = normalise(path);
|
|
39
|
+
const v = this.files.get(n);
|
|
40
|
+
if (v === undefined) throw new Error(`ENOENT: ${path}`);
|
|
41
|
+
if (typeof v !== "string") throw new Error(`read: ${path} is a binary file`);
|
|
42
|
+
return v;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
write(path: string, content: string): void {
|
|
46
|
+
this.files.set(normalise(path), content);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
writeBinary(path: string, content: Uint8Array): void {
|
|
50
|
+
this.files.set(normalise(path), content);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
exists(path: string): boolean {
|
|
54
|
+
const n = normalise(path);
|
|
55
|
+
if (this.files.has(n)) return true;
|
|
56
|
+
// Directory if any file is under `n/`.
|
|
57
|
+
const prefix = n.endsWith("/") ? n : `${n}/`;
|
|
58
|
+
for (const k of this.files.keys()) {
|
|
59
|
+
if (k.startsWith(prefix)) return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
list(dir: string): string[] {
|
|
65
|
+
const n = normalise(dir);
|
|
66
|
+
const prefix = n === "" ? "" : `${n}/`;
|
|
67
|
+
const out: string[] = [];
|
|
68
|
+
for (const k of this.files.keys()) {
|
|
69
|
+
if (n === "" || k.startsWith(prefix)) out.push(k);
|
|
70
|
+
}
|
|
71
|
+
return out.sort();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
listDir(dir: string): DirEntry[] {
|
|
75
|
+
const n = normalise(dir);
|
|
76
|
+
const prefix = n === "" ? "" : `${n}/`;
|
|
77
|
+
// Map immediate child name → isDirectory (a child is a directory if
|
|
78
|
+
// the remainder after the first segment still contains a "/").
|
|
79
|
+
const children = new Map<string, boolean>();
|
|
80
|
+
for (const k of this.files.keys()) {
|
|
81
|
+
if (n !== "" && !k.startsWith(prefix)) continue;
|
|
82
|
+
const rest = n === "" ? k : k.slice(prefix.length);
|
|
83
|
+
if (rest === "") continue;
|
|
84
|
+
const slash = rest.indexOf("/");
|
|
85
|
+
if (slash < 0) {
|
|
86
|
+
children.set(rest, children.get(rest) ?? false);
|
|
87
|
+
} else {
|
|
88
|
+
children.set(rest.slice(0, slash), true);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return [...children.entries()]
|
|
92
|
+
.map(([name, isDirectory]) => ({ name, isDirectory }))
|
|
93
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
move(from: string, to: string): void {
|
|
97
|
+
const fromN = normalise(from);
|
|
98
|
+
const toN = normalise(to);
|
|
99
|
+
const fromPrefix = `${fromN}/`;
|
|
100
|
+
const toPrefix = `${toN}/`;
|
|
101
|
+
const moved: Array<[string, string]> = [];
|
|
102
|
+
if (this.files.has(fromN)) {
|
|
103
|
+
moved.push([fromN, toN]);
|
|
104
|
+
}
|
|
105
|
+
for (const k of this.files.keys()) {
|
|
106
|
+
if (k.startsWith(fromPrefix)) {
|
|
107
|
+
moved.push([k, toPrefix + k.slice(fromPrefix.length)]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (moved.length === 0) {
|
|
111
|
+
throw new Error(`move: source missing: ${from}`);
|
|
112
|
+
}
|
|
113
|
+
if (this.exists(to)) {
|
|
114
|
+
throw new Error(`move: dest exists: ${to}`);
|
|
115
|
+
}
|
|
116
|
+
for (const [src, dst] of moved) {
|
|
117
|
+
this.files.set(dst, this.files.get(src)!);
|
|
118
|
+
this.files.delete(src);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
delete(path: string): void {
|
|
123
|
+
const n = normalise(path);
|
|
124
|
+
const prefix = `${n}/`;
|
|
125
|
+
this.files.delete(n);
|
|
126
|
+
const toDelete: string[] = [];
|
|
127
|
+
for (const k of this.files.keys()) {
|
|
128
|
+
if (k.startsWith(prefix)) toDelete.push(k);
|
|
129
|
+
}
|
|
130
|
+
for (const k of toDelete) this.files.delete(k);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// node:fs-backed implementation (synchronous). Anchored at a configured
|
|
2
|
+
// `root`. All FileSystem methods take block-relative paths; this wrapper
|
|
3
|
+
// joins them against root before touching disk.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import type { DirEntry, FileSystem } from "./api";
|
|
8
|
+
|
|
9
|
+
export class NodeFileSystem implements FileSystem {
|
|
10
|
+
constructor(private root: string) {}
|
|
11
|
+
|
|
12
|
+
private abs(p: string): string {
|
|
13
|
+
return path.resolve(this.root, p);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
read(p: string): string {
|
|
17
|
+
return fs.readFileSync(this.abs(p), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
write(p: string, content: string): void {
|
|
21
|
+
const a = this.abs(p);
|
|
22
|
+
fs.mkdirSync(path.dirname(a), { recursive: true });
|
|
23
|
+
fs.writeFileSync(a, content, "utf-8");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
writeBinary(p: string, content: Uint8Array): void {
|
|
27
|
+
const a = this.abs(p);
|
|
28
|
+
fs.mkdirSync(path.dirname(a), { recursive: true });
|
|
29
|
+
fs.writeFileSync(a, content);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
exists(p: string): boolean {
|
|
33
|
+
try {
|
|
34
|
+
fs.statSync(this.abs(p));
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
list(dir: string): string[] {
|
|
42
|
+
const a = this.abs(dir);
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.statSync(a);
|
|
45
|
+
if (!stat.isDirectory()) return [];
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const out: string[] = [];
|
|
50
|
+
function walk(rel: string, absDir: string): void {
|
|
51
|
+
const entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
52
|
+
for (const e of entries) {
|
|
53
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
54
|
+
const childAbs = path.join(absDir, e.name);
|
|
55
|
+
if (e.isDirectory()) {
|
|
56
|
+
walk(childRel, childAbs);
|
|
57
|
+
} else if (e.isFile()) {
|
|
58
|
+
out.push(childRel);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
walk(dir === "" || dir === "." ? "" : dir, a);
|
|
63
|
+
return out.sort();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
listDir(dir: string): DirEntry[] {
|
|
67
|
+
const a = this.abs(dir);
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(a, { withFileTypes: true });
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
return entries
|
|
75
|
+
.map((e) => ({ name: e.name, isDirectory: e.isDirectory() }))
|
|
76
|
+
.sort((x, y) => x.name.localeCompare(y.name));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
move(from: string, to: string): void {
|
|
80
|
+
const fromA = this.abs(from);
|
|
81
|
+
const toA = this.abs(to);
|
|
82
|
+
fs.mkdirSync(path.dirname(toA), { recursive: true });
|
|
83
|
+
fs.renameSync(fromA, toA);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
delete(p: string): void {
|
|
87
|
+
fs.rmSync(this.abs(p), { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Minimal glob matcher for the structurer's file-existence triggers.
|
|
2
|
+
//
|
|
3
|
+
// Supports `**` (any number of path segments, including none), `*` (any
|
|
4
|
+
// run of chars except `/`), and `?` (a single non-`/` char); every other
|
|
5
|
+
// character is matched literally. No brace expansion / char classes — the
|
|
6
|
+
// patterns we match are deliberately simple (e.g. `src/**/*.test.ts`).
|
|
7
|
+
// Paths are matched whole (anchored at both ends).
|
|
8
|
+
|
|
9
|
+
const REGEXP_SPECIALS = /[.+^${}()|[\]\\]/g;
|
|
10
|
+
|
|
11
|
+
/** Compile a glob to an anchored RegExp. */
|
|
12
|
+
export function globToRegExp(glob: string): RegExp {
|
|
13
|
+
let re = "";
|
|
14
|
+
for (let i = 0; i < glob.length; i++) {
|
|
15
|
+
const c = glob[i]!;
|
|
16
|
+
if (c === "*") {
|
|
17
|
+
if (glob[i + 1] === "*") {
|
|
18
|
+
// `**`: any path segments. Absorb a following `/` so `**/x` also
|
|
19
|
+
// matches `x` at depth 0 (zero segments).
|
|
20
|
+
i++;
|
|
21
|
+
if (glob[i + 1] === "/") {
|
|
22
|
+
i++;
|
|
23
|
+
re += "(?:.*/)?";
|
|
24
|
+
} else {
|
|
25
|
+
re += ".*";
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
re += "[^/]*";
|
|
29
|
+
}
|
|
30
|
+
} else if (c === "?") {
|
|
31
|
+
re += "[^/]";
|
|
32
|
+
} else {
|
|
33
|
+
re += c.replace(REGEXP_SPECIALS, "\\$&");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return new RegExp(`^${re}$`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True if `path` matches `glob` (whole-path, anchored). */
|
|
40
|
+
export function matchesGlob(glob: string, path: string): boolean {
|
|
41
|
+
return globToRegExp(glob).test(path);
|
|
42
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Tree IR for the structurer engine.
|
|
2
|
+
//
|
|
3
|
+
// `defineStructure(fn)` invokes module-global builders that append to a
|
|
4
|
+
// tree of group frames (`scope` / `when` / mode frames) and leaf
|
|
5
|
+
// primitives (file / folder actions). `flatten` walks this tree to a
|
|
6
|
+
// flat ordered list of `FlatItem`s — each with concrete scope, bound
|
|
7
|
+
// module path, composed trigger, resolved filesystem path, and the set
|
|
8
|
+
// of run modes the leaf fires in.
|
|
9
|
+
|
|
10
|
+
import type { Scope, RunContext } from "./api";
|
|
11
|
+
|
|
12
|
+
/** The three leaf-firing modes. `check` and `refresh` share the
|
|
13
|
+
* `"default"` mode — they run the same leaves, the `dryRun` flag (carried
|
|
14
|
+
* separately on `RunContext`) only decides whether writes happen.
|
|
15
|
+
* `"updateDeps"` is `--update-deps-only`; `"init"` is the init
|
|
16
|
+
* constructor. A leaf declares which modes it fires in via its enclosing
|
|
17
|
+
* mode frame (see `ModeFrame`); the runner derives the current mode from
|
|
18
|
+
* the run context and keeps only the leaves whose membership includes it. */
|
|
19
|
+
export type RunMode = "default" | "updateDeps" | "init";
|
|
20
|
+
|
|
21
|
+
/** Predicate evaluated against derived trigger context. */
|
|
22
|
+
export type TriggerContext = {
|
|
23
|
+
ctx: RunContext;
|
|
24
|
+
version: number;
|
|
25
|
+
/** Block-relative path queries against the post-structural FS snapshot. */
|
|
26
|
+
pathExists(path: string): boolean;
|
|
27
|
+
pathMissing(path: string): boolean;
|
|
28
|
+
/** True if any file in the snapshot matches `glob`, resolved
|
|
29
|
+
* MODULE-RELATIVE to the leaf's bound module: a co-located-test glob
|
|
30
|
+
* inside `scope("model")` is checked under the `model/` dir. At the
|
|
31
|
+
* block root the glob is block-relative. Unlike `pathExists`/`pathMissing`
|
|
32
|
+
* (block-relative), `filesMatch` is the module-aware predicate used by
|
|
33
|
+
* `whenFilesExist`. */
|
|
34
|
+
filesMatch(glob: string): boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type TriggerFn = (tctx: TriggerContext) => boolean;
|
|
38
|
+
|
|
39
|
+
/** Content forms — what to write into a file when creating it.
|
|
40
|
+
* `tpl.vars` may be a record or a thunk returning a record; the latter
|
|
41
|
+
* lets rule authors call `blockVars()` lazily at resolve time. */
|
|
42
|
+
export type TplVarsLike = Record<string, string> | (() => Record<string, string>);
|
|
43
|
+
export type ContentForm =
|
|
44
|
+
| { kind: "file"; path: string }
|
|
45
|
+
| { kind: "text"; value: string }
|
|
46
|
+
| { kind: "tpl"; path: string; vars: TplVarsLike }
|
|
47
|
+
| { kind: "generate"; fn: () => unknown }
|
|
48
|
+
// A binary static asset (e.g. a logo PNG) copied verbatim from the
|
|
49
|
+
// template tree. Carried as raw bytes, never decoded to a string —
|
|
50
|
+
// only `seed` / `scaffold` consume it (via fs.writeBinary).
|
|
51
|
+
| { kind: "binary"; path: string };
|
|
52
|
+
|
|
53
|
+
/** Builder-body for a `managed` file (the content-rules lambda). */
|
|
54
|
+
export type ManagedBody = () => void;
|
|
55
|
+
|
|
56
|
+
// --- Leaf primitives ---
|
|
57
|
+
|
|
58
|
+
export type FixedItem = {
|
|
59
|
+
kind: "fixed";
|
|
60
|
+
path: string;
|
|
61
|
+
content: ContentForm;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type ManagedItem = {
|
|
65
|
+
kind: "managed";
|
|
66
|
+
path: string;
|
|
67
|
+
initial: ContentForm;
|
|
68
|
+
body: ManagedBody;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type ScaffoldItem = {
|
|
72
|
+
kind: "scaffold";
|
|
73
|
+
path: string;
|
|
74
|
+
initial: ContentForm;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type SeedItem = {
|
|
78
|
+
kind: "seed";
|
|
79
|
+
path: string;
|
|
80
|
+
initial: ContentForm;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type RemoveItem = {
|
|
84
|
+
kind: "remove";
|
|
85
|
+
path: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type RenameItem = {
|
|
89
|
+
kind: "rename";
|
|
90
|
+
from: string;
|
|
91
|
+
to: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type LeafItem = FixedItem | ManagedItem | ScaffoldItem | SeedItem | RemoveItem | RenameItem;
|
|
95
|
+
|
|
96
|
+
// --- Group frames ---
|
|
97
|
+
|
|
98
|
+
export type ScopeFrame = {
|
|
99
|
+
kind: "scope";
|
|
100
|
+
scope: Scope;
|
|
101
|
+
children: TreeNode[];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type WhenFrame = {
|
|
105
|
+
kind: "when";
|
|
106
|
+
trigger: TriggerFn;
|
|
107
|
+
children: TreeNode[];
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Top-level mode frame: contained leaves fire only in the listed run
|
|
111
|
+
* modes. Leaves outside any mode frame fire in `["default", "init"]`
|
|
112
|
+
* (refresh/check + init). `onUpdateDeps()` produces `["updateDeps"]`;
|
|
113
|
+
* `onInitOrUpdate()` produces `["init", "updateDeps"]`. */
|
|
114
|
+
export type ModeFrame = {
|
|
115
|
+
kind: "mode";
|
|
116
|
+
modes: RunMode[];
|
|
117
|
+
children: TreeNode[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type TreeNode = LeafItem | ScopeFrame | WhenFrame | ModeFrame;
|
|
121
|
+
|
|
122
|
+
/** Structure object returned by `defineStructure`. */
|
|
123
|
+
export type Structure = {
|
|
124
|
+
/** Top-level children — may contain leaves, scope frames, when
|
|
125
|
+
* frames, and mode frames. */
|
|
126
|
+
children: TreeNode[];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// --- Post-flatten ---
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* After fan-out: one entry per (leaf × matching module). `scope` is
|
|
133
|
+
* always set; `modulePath` is the workspace path the leaf is bound to
|
|
134
|
+
* (empty string for the workspace root). `triggers` is the AND of all
|
|
135
|
+
* enclosing `when` predicates. `modes` is the set of run modes the leaf
|
|
136
|
+
* fires in — `["default", "init"]` outside any mode frame, otherwise the
|
|
137
|
+
* enclosing mode frame's `modes`.
|
|
138
|
+
*/
|
|
139
|
+
export type FlatItem = {
|
|
140
|
+
leaf: LeafItem;
|
|
141
|
+
scope: Scope;
|
|
142
|
+
modulePath: string;
|
|
143
|
+
/** Block-relative resolved path(s). */
|
|
144
|
+
resolvedPath: string;
|
|
145
|
+
/** For rename, the resolved destination. */
|
|
146
|
+
resolvedTo?: string;
|
|
147
|
+
triggers: TriggerFn[];
|
|
148
|
+
modes: RunMode[];
|
|
149
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// JSON parser + serialiser + jsonPath helpers + top-level key reorder.
|
|
2
|
+
//
|
|
3
|
+
// `parseJson`/`stringifyJson` are the canonical round-trip primitives the
|
|
4
|
+
// managed pass uses. The dot-notation helpers (`getAtPath`, `setAtPath`,
|
|
5
|
+
// `hasAtPath`, `deleteAtPath`) back the content-rule builders. `reorderTopLevel`
|
|
6
|
+
// implements the "known keys in declared order, unknown keys stay adjacent to
|
|
7
|
+
// preceding known key" projection.
|
|
8
|
+
|
|
9
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
10
|
+
|
|
11
|
+
export type JsonObject = { [k: string]: unknown };
|
|
12
|
+
|
|
13
|
+
/** Parse JSON; throws on invalid input. */
|
|
14
|
+
export function parseJson(raw: string): unknown {
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// oxfmt formats JSON two ways, keyed on filename:
|
|
19
|
+
// - package.json: every array/object fully expanded, one element per line,
|
|
20
|
+
// regardless of width (what `JSON.stringify(…, 2)` already produces).
|
|
21
|
+
// - any other .json (e.g. tsconfig.json): oxfmt's generic shape —
|
|
22
|
+
// * objects: always expanded, one property per line (oxfmt preserves
|
|
23
|
+
// the brace newline, so the expanded form is a fixpoint);
|
|
24
|
+
// * arrays: collapsed onto one line when that line — its indent, the
|
|
25
|
+
// `"key": ` prefix, the flat array, and a trailing comma if a sibling
|
|
26
|
+
// follows — fits `PRINT_WIDTH` (oxfmt's default 100), else broken one
|
|
27
|
+
// element per line.
|
|
28
|
+
// This is exactly the form `oxfmt` itself produces, verified against
|
|
29
|
+
// oxfmt 0.35 at the array boundary (≤100 collapses, 101 breaks; a
|
|
30
|
+
// trailing comma counts) and across all in-mono blocks. The single-
|
|
31
|
+
// element `include` array was the regression: `JSON.stringify` expanded
|
|
32
|
+
// it while oxfmt collapses it.
|
|
33
|
+
// `stringifyJson` defaults to the expanded form; callers serialising a
|
|
34
|
+
// generic .json pass `{ collapse: true }` so the output is oxfmt-clean by
|
|
35
|
+
// construction (no prior `pnpm fmt` needed for `check` — the same
|
|
36
|
+
// emit-clean-by-design contract as the canonical key/dependency ordering).
|
|
37
|
+
const PRINT_WIDTH = 100;
|
|
38
|
+
const INDENT = 2;
|
|
39
|
+
|
|
40
|
+
/** One-line rendering of an array, matching oxfmt's flat spacing: `[a, b]`
|
|
41
|
+
* (no inner padding), nested objects inline as `{ "k": v }`, empty `[]`.
|
|
42
|
+
* Only arrays collapse, so this is the array-fits primitive. */
|
|
43
|
+
function flatArray(value: unknown[]): string {
|
|
44
|
+
if (value.length === 0) return "[]";
|
|
45
|
+
return "[" + value.map(flatValue).join(", ") + "]";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One-line rendering of any value (used inside a collapsed array, where
|
|
49
|
+
* contained objects render inline). */
|
|
50
|
+
function flatValue(value: unknown): string {
|
|
51
|
+
if (Array.isArray(value)) return flatArray(value);
|
|
52
|
+
if (isObject(value)) {
|
|
53
|
+
const entries = Object.entries(value);
|
|
54
|
+
if (entries.length === 0) return "{}";
|
|
55
|
+
return (
|
|
56
|
+
"{ " + entries.map(([k, v]) => `${JSON.stringify(k)}: ${flatValue(v)}`).join(", ") + " }"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Print `value` starting at `column`. Objects always break (one property per
|
|
63
|
+
* line); arrays collapse when their flat line — `column` + the flat array +
|
|
64
|
+
* `reserved` (1 for a trailing comma when a sibling follows) — fits
|
|
65
|
+
* `PRINT_WIDTH`, else break. `indent` is the leading-space width of the line
|
|
66
|
+
* the container opens on. */
|
|
67
|
+
function printValue(value: unknown, indent: number, column: number, reserved: number): string {
|
|
68
|
+
const inner = indent + INDENT;
|
|
69
|
+
const pad = " ".repeat(inner);
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
const flat = flatArray(value);
|
|
72
|
+
if (column + flat.length + reserved <= PRINT_WIDTH) return flat;
|
|
73
|
+
const last = value.length - 1;
|
|
74
|
+
const lines = value.map((item, i) => pad + printValue(item, inner, inner, i === last ? 0 : 1));
|
|
75
|
+
return "[\n" + lines.join(",\n") + "\n" + " ".repeat(indent) + "]";
|
|
76
|
+
}
|
|
77
|
+
if (isObject(value)) {
|
|
78
|
+
const entries = Object.entries(value);
|
|
79
|
+
if (entries.length === 0) return "{}";
|
|
80
|
+
const last = entries.length - 1;
|
|
81
|
+
const lines = entries.map(([k, v], i) => {
|
|
82
|
+
const prefix = `${pad}${JSON.stringify(k)}: `;
|
|
83
|
+
return prefix + printValue(v, inner, prefix.length, i === last ? 0 : 1);
|
|
84
|
+
});
|
|
85
|
+
return "{\n" + lines.join(",\n") + "\n" + " ".repeat(indent) + "}";
|
|
86
|
+
}
|
|
87
|
+
return JSON.stringify(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Canonical serialiser: 2-space indent, trailing newline.
|
|
91
|
+
* Default — every container expanded (the package.json shape, byte-identical
|
|
92
|
+
* to `JSON.stringify(…, 2)`).
|
|
93
|
+
* `{ collapse: true }` — oxfmt's generic-.json shape: containers that fit
|
|
94
|
+
* `PRINT_WIDTH` collapse onto one line. Used for tsconfig.json and any other
|
|
95
|
+
* non-package.json the engine emits, so `oxfmt --check` passes without a
|
|
96
|
+
* prior format pass. */
|
|
97
|
+
export function stringifyJson(value: unknown, opts?: { collapse?: boolean }): string {
|
|
98
|
+
if (!opts?.collapse) return JSON.stringify(value, null, 2) + "\n";
|
|
99
|
+
// Normalise through a round-trip so non-JSON inputs (undefined values,
|
|
100
|
+
// functions) are dropped exactly as `JSON.stringify` would.
|
|
101
|
+
const normalized = JSON.parse(JSON.stringify(value ?? null));
|
|
102
|
+
return printValue(normalized, 0, 0, 0) + "\n";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function splitPath(jsonPath: string): string[] {
|
|
106
|
+
if (jsonPath === "" || jsonPath === undefined) return [];
|
|
107
|
+
return jsonPath.split(".");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isObject(v: unknown): v is JsonObject {
|
|
111
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Get the value at `jsonPath`. Returns `undefined` if any segment is
|
|
115
|
+
* missing or not an object. */
|
|
116
|
+
export function getAtPath(obj: JsonObject, jsonPath: string): unknown {
|
|
117
|
+
const parts = splitPath(jsonPath);
|
|
118
|
+
let cur: unknown = obj;
|
|
119
|
+
for (const p of parts) {
|
|
120
|
+
if (!isObject(cur)) return undefined;
|
|
121
|
+
cur = cur[p];
|
|
122
|
+
}
|
|
123
|
+
return cur;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** True if `jsonPath` resolves to a defined value (including `null`). */
|
|
127
|
+
export function hasAtPath(obj: JsonObject, jsonPath: string): boolean {
|
|
128
|
+
const parts = splitPath(jsonPath);
|
|
129
|
+
if (parts.length === 0) return true;
|
|
130
|
+
let cur: unknown = obj;
|
|
131
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
132
|
+
if (!isObject(cur)) return false;
|
|
133
|
+
cur = cur[parts[i]!];
|
|
134
|
+
}
|
|
135
|
+
if (!isObject(cur)) return false;
|
|
136
|
+
return parts[parts.length - 1]! in cur;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Set `value` at `jsonPath`. Auto-creates intermediate objects. Throws
|
|
140
|
+
* if a non-object value is on the path (would be silently overwritten
|
|
141
|
+
* otherwise — caller bug). */
|
|
142
|
+
export function setAtPath(obj: JsonObject, jsonPath: string, value: unknown): void {
|
|
143
|
+
const parts = splitPath(jsonPath);
|
|
144
|
+
if (parts.length === 0) {
|
|
145
|
+
throw new Error("setAtPath: jsonPath must be non-empty");
|
|
146
|
+
}
|
|
147
|
+
let cur: JsonObject = obj;
|
|
148
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
149
|
+
const k = parts[i]!;
|
|
150
|
+
const next = cur[k];
|
|
151
|
+
if (next === undefined) {
|
|
152
|
+
const fresh: JsonObject = {};
|
|
153
|
+
cur[k] = fresh;
|
|
154
|
+
cur = fresh;
|
|
155
|
+
} else if (isObject(next)) {
|
|
156
|
+
cur = next;
|
|
157
|
+
} else {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`setAtPath: cannot descend into non-object at '${parts.slice(0, i + 1).join(".")}'`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
cur[parts[parts.length - 1]!] = value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Delete the value at `jsonPath`. No-op if any segment is missing. */
|
|
167
|
+
export function deleteAtPath(obj: JsonObject, jsonPath: string): void {
|
|
168
|
+
const parts = splitPath(jsonPath);
|
|
169
|
+
if (parts.length === 0) {
|
|
170
|
+
throw new Error("deleteAtPath: jsonPath must be non-empty");
|
|
171
|
+
}
|
|
172
|
+
let cur: unknown = obj;
|
|
173
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
174
|
+
if (!isObject(cur)) return;
|
|
175
|
+
cur = cur[parts[i]!];
|
|
176
|
+
}
|
|
177
|
+
if (!isObject(cur)) return;
|
|
178
|
+
delete cur[parts[parts.length - 1]!];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Reorder top-level keys of `obj`:
|
|
183
|
+
* - Known keys (those listed in `order`) appear in declared order.
|
|
184
|
+
* - Unknown keys (not in `order`) stay adjacent to the preceding known
|
|
185
|
+
* key they followed in the source. Unknown keys appearing before any
|
|
186
|
+
* known key go first, in source order.
|
|
187
|
+
*/
|
|
188
|
+
export function reorderTopLevel(obj: JsonObject, order: readonly string[]): JsonObject {
|
|
189
|
+
const known = new Set(order);
|
|
190
|
+
const sourceKeys = Object.keys(obj);
|
|
191
|
+
|
|
192
|
+
const adjacents = new Map<string, string[]>();
|
|
193
|
+
adjacents.set("", []);
|
|
194
|
+
let anchor = "";
|
|
195
|
+
for (const k of sourceKeys) {
|
|
196
|
+
if (known.has(k)) {
|
|
197
|
+
anchor = k;
|
|
198
|
+
adjacents.set(anchor, []);
|
|
199
|
+
} else {
|
|
200
|
+
adjacents.get(anchor)!.push(k);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const out: JsonObject = {};
|
|
205
|
+
for (const k of adjacents.get("")!) out[k] = obj[k];
|
|
206
|
+
for (const k of order) {
|
|
207
|
+
if (!(k in obj)) continue;
|
|
208
|
+
out[k] = obj[k];
|
|
209
|
+
for (const u of adjacents.get(k) ?? []) out[u] = obj[u];
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// .gitignore-style line list parser/serialiser.
|
|
2
|
+
//
|
|
3
|
+
// `parseLines` keeps the file verbatim as an array of lines (one per
|
|
4
|
+
// "\n"). `serializeLines` joins them with "\n" and guarantees a trailing
|
|
5
|
+
// newline. `normaliseLine` strips comments and surrounding whitespace —
|
|
6
|
+
// used by the `ensureGitignoreEntries` family to compare an existing
|
|
7
|
+
// line to a required entry without being fooled by inline `#` comments
|
|
8
|
+
// or extra indentation.
|
|
9
|
+
|
|
10
|
+
export function parseLines(raw: string): string[] {
|
|
11
|
+
// Drop the synthetic empty entry caused by a trailing newline so a
|
|
12
|
+
// serialise-then-parse round-trip is stable.
|
|
13
|
+
const lines = raw.split("\n");
|
|
14
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
15
|
+
return lines;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function serializeLines(lines: readonly string[]): string {
|
|
19
|
+
return lines.join("\n") + "\n";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Strip leading/trailing whitespace and trailing inline `# ...` comments.
|
|
23
|
+
* Returns empty string for blank / comment-only lines. */
|
|
24
|
+
export function normaliseLine(line: string): string {
|
|
25
|
+
const noComment = line.replace(/(^|\s)#.*$/, "$1");
|
|
26
|
+
return noComment.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True if `line` (after normalisation) equals `entry` (after trim). */
|
|
30
|
+
export function lineEqualsEntry(line: string, entry: string): boolean {
|
|
31
|
+
const n = normaliseLine(line);
|
|
32
|
+
if (n === "") return false;
|
|
33
|
+
return n === entry.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True if any line in `lines` normalises to `entry`. */
|
|
37
|
+
export function containsEntry(lines: readonly string[], entry: string): boolean {
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
if (lineEqualsEntry(line, entry)) return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|