@sha3/code 1.0.0
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/AGENTS.md +75 -0
- package/README.md +554 -0
- package/ai/adapters/codex.md +7 -0
- package/ai/adapters/copilot.md +7 -0
- package/ai/adapters/cursor.md +7 -0
- package/ai/adapters/windsurf.md +8 -0
- package/ai/constitution.md +12 -0
- package/bin/code-standards.mjs +47 -0
- package/biome.json +37 -0
- package/index.mjs +11 -0
- package/lib/cli/parse-args.mjs +416 -0
- package/lib/cli/post-run-guidance.mjs +43 -0
- package/lib/cli/run-init.mjs +123 -0
- package/lib/cli/run-profile.mjs +46 -0
- package/lib/cli/run-refactor.mjs +152 -0
- package/lib/cli/run-verify.mjs +67 -0
- package/lib/constants.mjs +167 -0
- package/lib/contract/load-rule-catalog.mjs +12 -0
- package/lib/contract/render-agents.mjs +79 -0
- package/lib/contract/render-contract-json.mjs +7 -0
- package/lib/contract/resolve-contract.mjs +52 -0
- package/lib/paths.mjs +50 -0
- package/lib/profile.mjs +108 -0
- package/lib/project/ai-instructions.mjs +28 -0
- package/lib/project/biome-ignore.mjs +14 -0
- package/lib/project/managed-files.mjs +105 -0
- package/lib/project/package-metadata.mjs +132 -0
- package/lib/project/prompt-files.mjs +111 -0
- package/lib/project/template-resolution.mjs +70 -0
- package/lib/refactor/materialize-refactor-context.mjs +106 -0
- package/lib/refactor/preservation-questions.mjs +33 -0
- package/lib/refactor/public-contract-extractor.mjs +22 -0
- package/lib/refactor/render-analysis-summary.mjs +50 -0
- package/lib/refactor/source-analysis.mjs +74 -0
- package/lib/utils/fs.mjs +220 -0
- package/lib/utils/prompts.mjs +63 -0
- package/lib/utils/text.mjs +43 -0
- package/lib/verify/change-audit-verifier.mjs +140 -0
- package/lib/verify/change-context.mjs +36 -0
- package/lib/verify/error-handling-verifier.mjs +164 -0
- package/lib/verify/explain-rule.mjs +54 -0
- package/lib/verify/issue-helpers.mjs +132 -0
- package/lib/verify/project-layout-verifier.mjs +259 -0
- package/lib/verify/project-verifier.mjs +267 -0
- package/lib/verify/readme-public-api.mjs +237 -0
- package/lib/verify/readme-verifier.mjs +216 -0
- package/lib/verify/render-json-report.mjs +3 -0
- package/lib/verify/render-text-report.mjs +34 -0
- package/lib/verify/source-analysis.mjs +126 -0
- package/lib/verify/source-rule-verifier.mjs +453 -0
- package/lib/verify/testing-verifier.mjs +113 -0
- package/lib/verify/tooling-verifier.mjs +82 -0
- package/lib/verify/typescript-style-verifier.mjs +407 -0
- package/package.json +55 -0
- package/profiles/default.profile.json +40 -0
- package/profiles/schema.json +96 -0
- package/prompts/init-contract.md +25 -0
- package/prompts/init-phase-2-implement.md +25 -0
- package/prompts/init-phase-3-verify.md +23 -0
- package/prompts/init.prompt.md +24 -0
- package/prompts/refactor-contract.md +26 -0
- package/prompts/refactor-phase-2-rebuild.md +25 -0
- package/prompts/refactor-phase-3-verify.md +24 -0
- package/prompts/refactor.prompt.md +26 -0
- package/resources/ai/AGENTS.md +18 -0
- package/resources/ai/adapters/codex.md +5 -0
- package/resources/ai/adapters/copilot.md +5 -0
- package/resources/ai/adapters/cursor.md +5 -0
- package/resources/ai/adapters/windsurf.md +5 -0
- package/resources/ai/contract.schema.json +68 -0
- package/resources/ai/rule-catalog.json +878 -0
- package/resources/ai/rule-catalog.schema.json +66 -0
- package/resources/ai/templates/adapters/codex.template.md +7 -0
- package/resources/ai/templates/adapters/copilot.template.md +7 -0
- package/resources/ai/templates/adapters/cursor.template.md +7 -0
- package/resources/ai/templates/adapters/windsurf.template.md +7 -0
- package/resources/ai/templates/agents.project.template.md +141 -0
- package/resources/ai/templates/examples/demo/src/billing/billing.service.ts +73 -0
- package/resources/ai/templates/examples/demo/src/config.ts +3 -0
- package/resources/ai/templates/examples/demo/src/invoice/invoice.errors.ts +51 -0
- package/resources/ai/templates/examples/demo/src/invoice/invoice.service.ts +96 -0
- package/resources/ai/templates/examples/demo/src/invoice/invoice.types.ts +9 -0
- package/resources/ai/templates/examples/rules/async-bad.ts +52 -0
- package/resources/ai/templates/examples/rules/async-good.ts +56 -0
- package/resources/ai/templates/examples/rules/class-first-bad.ts +36 -0
- package/resources/ai/templates/examples/rules/class-first-good.ts +74 -0
- package/resources/ai/templates/examples/rules/constructor-bad.ts +68 -0
- package/resources/ai/templates/examples/rules/constructor-good.ts +71 -0
- package/resources/ai/templates/examples/rules/control-flow-bad.ts +31 -0
- package/resources/ai/templates/examples/rules/control-flow-good.ts +54 -0
- package/resources/ai/templates/examples/rules/errors-bad.ts +42 -0
- package/resources/ai/templates/examples/rules/errors-good.ts +23 -0
- package/resources/ai/templates/examples/rules/functions-bad.ts +48 -0
- package/resources/ai/templates/examples/rules/functions-good.ts +58 -0
- package/resources/ai/templates/examples/rules/returns-bad.ts +38 -0
- package/resources/ai/templates/examples/rules/returns-good.ts +44 -0
- package/resources/ai/templates/examples/rules/testing-bad.ts +34 -0
- package/resources/ai/templates/examples/rules/testing-good.ts +54 -0
- package/resources/ai/templates/rules/architecture.md +41 -0
- package/resources/ai/templates/rules/async.md +13 -0
- package/resources/ai/templates/rules/class-first.md +45 -0
- package/resources/ai/templates/rules/control-flow.md +13 -0
- package/resources/ai/templates/rules/errors.md +18 -0
- package/resources/ai/templates/rules/functions.md +29 -0
- package/resources/ai/templates/rules/naming.md +13 -0
- package/resources/ai/templates/rules/readme.md +36 -0
- package/resources/ai/templates/rules/returns.md +13 -0
- package/resources/ai/templates/rules/testing.md +18 -0
- package/resources/ai/templates/rules.project.template.md +66 -0
- package/resources/ai/templates/skills/change-synchronization/SKILL.md +42 -0
- package/resources/ai/templates/skills/feature-shaping/SKILL.md +45 -0
- package/resources/ai/templates/skills/http-api-conventions/SKILL.md +171 -0
- package/resources/ai/templates/skills/init-workflow/SKILL.md +52 -0
- package/resources/ai/templates/skills/readme-authoring/SKILL.md +51 -0
- package/resources/ai/templates/skills/refactor-workflow/SKILL.md +50 -0
- package/resources/ai/templates/skills/simplicity-audit/SKILL.md +41 -0
- package/resources/ai/templates/skills/test-scope-selection/SKILL.md +50 -0
- package/resources/ai/templates/skills.index.template.md +25 -0
- package/standards/architecture.md +72 -0
- package/standards/changelog-policy.md +12 -0
- package/standards/manifest.json +36 -0
- package/standards/readme.md +56 -0
- package/standards/schema.json +124 -0
- package/standards/style.md +106 -0
- package/standards/testing.md +20 -0
- package/standards/tooling.md +38 -0
- package/templates/node-lib/.biomeignore +10 -0
- package/templates/node-lib/.vscode/extensions.json +1 -0
- package/templates/node-lib/.vscode/settings.json +9 -0
- package/templates/node-lib/README.md +172 -0
- package/templates/node-lib/biome.json +37 -0
- package/templates/node-lib/gitignore +6 -0
- package/templates/node-lib/package.json +32 -0
- package/templates/node-lib/scripts/release-publish.mjs +106 -0
- package/templates/node-lib/scripts/run-tests.mjs +65 -0
- package/templates/node-lib/src/config.ts +3 -0
- package/templates/node-lib/src/index.ts +2 -0
- package/templates/node-lib/src/logger.ts +7 -0
- package/templates/node-lib/src/package-info/package-info.service.ts +47 -0
- package/templates/node-lib/test/package-info.test.ts +10 -0
- package/templates/node-lib/tsconfig.build.json +1 -0
- package/templates/node-lib/tsconfig.json +5 -0
- package/templates/node-service/.biomeignore +10 -0
- package/templates/node-service/.vscode/extensions.json +1 -0
- package/templates/node-service/.vscode/settings.json +9 -0
- package/templates/node-service/README.md +244 -0
- package/templates/node-service/biome.json +37 -0
- package/templates/node-service/ecosystem.config.cjs +3 -0
- package/templates/node-service/gitignore +6 -0
- package/templates/node-service/package.json +42 -0
- package/templates/node-service/scripts/release-publish.mjs +106 -0
- package/templates/node-service/scripts/run-tests.mjs +65 -0
- package/templates/node-service/src/app/service-runtime.service.ts +57 -0
- package/templates/node-service/src/app-info/app-info.service.ts +47 -0
- package/templates/node-service/src/config.ts +11 -0
- package/templates/node-service/src/http/http-server.service.ts +66 -0
- package/templates/node-service/src/index.ts +2 -0
- package/templates/node-service/src/logger.ts +7 -0
- package/templates/node-service/src/main.ts +5 -0
- package/templates/node-service/test/service-runtime.test.ts +13 -0
- package/templates/node-service/tsconfig.build.json +1 -0
- package/templates/node-service/tsconfig.json +5 -0
- package/tsconfig/base.json +16 -0
- package/tsconfig/node-lib.json +5 -0
- package/tsconfig/node-service.json +1 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
|
|
6
|
+
import { README_REQUIRED_HEADINGS_BY_TEMPLATE } from "../constants.mjs";
|
|
7
|
+
import { createContractIssue } from "./issue-helpers.mjs";
|
|
8
|
+
import { extractPublicReadmeApi } from "./readme-public-api.mjs";
|
|
9
|
+
|
|
10
|
+
function escapeRegExp(value) {
|
|
11
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function extractHeadingSection(readmeRaw, heading) {
|
|
15
|
+
const lines = readmeRaw.split("\n");
|
|
16
|
+
const headingIndex = lines.findIndex((line) => line.trim() === heading);
|
|
17
|
+
|
|
18
|
+
if (headingIndex === -1) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sectionLines = [];
|
|
23
|
+
|
|
24
|
+
for (const line of lines.slice(headingIndex + 1)) {
|
|
25
|
+
if (line.startsWith("## ") || line.startsWith("# ")) {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
sectionLines.push(line);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return sectionLines.join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasExportSection(readmeRaw, exportName) {
|
|
36
|
+
return readmeRaw.split("\n").some((line) => line.trim() === `### \`${exportName}\``);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasMethodSection(readmeRaw, methodName) {
|
|
40
|
+
return readmeRaw.split("\n").some((line) => line.trim().startsWith(`#### \`${methodName}`));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasPackageRootImportExample(readmeRaw, packageName) {
|
|
44
|
+
if (!packageName) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new RegExp(`from ["']${escapeRegExp(packageName)}["']`).test(readmeRaw);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasDocumentedHttpEndpoint(readmeRaw) {
|
|
52
|
+
const httpApiSection = extractHeadingSection(readmeRaw, "## HTTP API");
|
|
53
|
+
|
|
54
|
+
if (!httpApiSection) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return /\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b\s+\/\S*/.test(httpApiSection);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasScaffoldLikeLanguage(readmeRaw) {
|
|
62
|
+
return /\b(?:placeholder|todo|your project|lorem ipsum|scaffold(?:ed|ing)?|generated(?:\s+from\s+code)?|template README|boilerplate)\b/i.test(readmeRaw);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collectConfigKeys(configRaw, configPath) {
|
|
66
|
+
const sourceFile = ts.createSourceFile(configPath, configRaw, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
67
|
+
const keys = [];
|
|
68
|
+
|
|
69
|
+
for (const statement of sourceFile.statements) {
|
|
70
|
+
if (!ts.isVariableStatement(statement)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
75
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "config" || !declaration.initializer) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const initializer = ts.isAsExpression(declaration.initializer) ? declaration.initializer.expression : declaration.initializer;
|
|
80
|
+
if (!ts.isObjectLiteralExpression(initializer)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const property of initializer.properties) {
|
|
85
|
+
if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
|
|
86
|
+
keys.push(property.name.text);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return keys;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function verifyReadme(targetPath, options) {
|
|
96
|
+
const { contract, packageName, template } = options;
|
|
97
|
+
const readmePath = path.join(targetPath, "README.md");
|
|
98
|
+
const readmeRaw = await readFile(readmePath, "utf8");
|
|
99
|
+
const configPath = path.join(targetPath, "src", "config.ts");
|
|
100
|
+
const configRaw = await readFile(configPath, "utf8").catch(() => "");
|
|
101
|
+
const issues = [];
|
|
102
|
+
const requiredHeadings = README_REQUIRED_HEADINGS_BY_TEMPLATE[template] ?? [];
|
|
103
|
+
|
|
104
|
+
for (const heading of requiredHeadings) {
|
|
105
|
+
if (!readmeRaw.includes(heading)) {
|
|
106
|
+
issues.push(
|
|
107
|
+
createContractIssue(contract, {
|
|
108
|
+
ruleId: "readme-sections",
|
|
109
|
+
category: "readme",
|
|
110
|
+
relativePath: "README.md",
|
|
111
|
+
message: `missing required README heading: ${heading}`,
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const publicReadmeApi = await extractPublicReadmeApi(targetPath);
|
|
118
|
+
|
|
119
|
+
for (const exportedMember of publicReadmeApi.exports) {
|
|
120
|
+
if (!hasExportSection(readmeRaw, exportedMember.exportName)) {
|
|
121
|
+
issues.push(
|
|
122
|
+
createContractIssue(contract, {
|
|
123
|
+
ruleId: "readme-public-api",
|
|
124
|
+
category: "readme",
|
|
125
|
+
relativePath: "README.md",
|
|
126
|
+
message: `missing README section for public export: ${exportedMember.exportName}`,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (exportedMember.kind !== "class") {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const publicMethod of exportedMember.publicMethods) {
|
|
137
|
+
if (!hasMethodSection(readmeRaw, publicMethod)) {
|
|
138
|
+
issues.push(
|
|
139
|
+
createContractIssue(contract, {
|
|
140
|
+
ruleId: "readme-public-methods",
|
|
141
|
+
category: "readme",
|
|
142
|
+
relativePath: "README.md",
|
|
143
|
+
message: `missing README method documentation: ${exportedMember.exportName}.${publicMethod}()`,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (template === "node-lib" && !hasPackageRootImportExample(readmeRaw, packageName)) {
|
|
151
|
+
issues.push(
|
|
152
|
+
createContractIssue(contract, {
|
|
153
|
+
ruleId: "readme-usage-examples",
|
|
154
|
+
category: "readme",
|
|
155
|
+
relativePath: "README.md",
|
|
156
|
+
message: "README examples must show at least one import from the package root",
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (template === "node-service" && !hasDocumentedHttpEndpoint(readmeRaw)) {
|
|
162
|
+
issues.push(
|
|
163
|
+
createContractIssue(contract, {
|
|
164
|
+
ruleId: "readme-http-api",
|
|
165
|
+
category: "readme",
|
|
166
|
+
relativePath: "README.md",
|
|
167
|
+
message: "README HTTP API section must document at least one endpoint with method and path",
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (hasScaffoldLikeLanguage(readmeRaw)) {
|
|
173
|
+
issues.push(
|
|
174
|
+
createContractIssue(contract, {
|
|
175
|
+
ruleId: "readme-no-placeholder-language",
|
|
176
|
+
category: "readme",
|
|
177
|
+
relativePath: "README.md",
|
|
178
|
+
message: "README contains placeholder, scaffold-like, or generated-documentation language",
|
|
179
|
+
verificationMode: "heuristic",
|
|
180
|
+
confidence: "medium",
|
|
181
|
+
evidence: "placeholder/scaffold wording",
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const configKeys = collectConfigKeys(configRaw, configPath);
|
|
187
|
+
for (const configKey of configKeys) {
|
|
188
|
+
if (!readmeRaw.includes(configKey)) {
|
|
189
|
+
issues.push(
|
|
190
|
+
createContractIssue(contract, {
|
|
191
|
+
ruleId: "readme-config-coverage",
|
|
192
|
+
category: "readme",
|
|
193
|
+
relativePath: "README.md",
|
|
194
|
+
message: `README must document config key: ${configKey}`,
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const hasPlausibleCodeExample = /```(?:ts|bash)\n[\s\S]*?(?:npm |import |\bnode\b|\bconst\b)/.test(readmeRaw);
|
|
201
|
+
if (!hasPlausibleCodeExample) {
|
|
202
|
+
issues.push(
|
|
203
|
+
createContractIssue(contract, {
|
|
204
|
+
ruleId: "readme-runnable-examples",
|
|
205
|
+
category: "readme",
|
|
206
|
+
relativePath: "README.md",
|
|
207
|
+
message: "README must include plausible runnable command or TypeScript examples",
|
|
208
|
+
verificationMode: "heuristic",
|
|
209
|
+
confidence: "medium",
|
|
210
|
+
evidence: "missing runnable ts/bash example",
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return issues;
|
|
216
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function formatLocation(issue) {
|
|
2
|
+
if (!issue.relativePath) {
|
|
3
|
+
return "";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (typeof issue.line !== "number") {
|
|
7
|
+
return issue.relativePath;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof issue.column !== "number") {
|
|
11
|
+
return `${issue.relativePath}:${issue.line}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return `${issue.relativePath}:${issue.line}:${issue.column}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderTextReport(result) {
|
|
18
|
+
if (result.ok) {
|
|
19
|
+
const warningSuffix = result.hasWarnings ? ` (${result.summary.warningCount} warning(s), ${result.summary.auditCount} audit item(s))` : "";
|
|
20
|
+
return `standards verification passed${warningSuffix}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return result.issues
|
|
24
|
+
.map((issue) => {
|
|
25
|
+
const location = formatLocation(issue);
|
|
26
|
+
const mode = issue.verificationMode ? `/${issue.verificationMode}` : "";
|
|
27
|
+
const severity = issue.severity ? `${issue.severity.toUpperCase()} ` : "";
|
|
28
|
+
const evidence = issue.evidence ? ` (${issue.evidence})` : "";
|
|
29
|
+
return location.length > 0
|
|
30
|
+
? `- ${severity}[${issue.ruleId}${mode}] ${location}: ${issue.message}${evidence}`
|
|
31
|
+
: `- ${severity}[${issue.ruleId}${mode}] ${issue.message}${evidence}`;
|
|
32
|
+
})
|
|
33
|
+
.join("\n");
|
|
34
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
|
|
6
|
+
import { listRelativeFiles } from "../project/template-resolution.mjs";
|
|
7
|
+
import { isFileSelected } from "./issue-helpers.mjs";
|
|
8
|
+
|
|
9
|
+
export async function loadProjectSourceFiles(targetPath, options = {}) {
|
|
10
|
+
const files = await listRelativeFiles(targetPath);
|
|
11
|
+
const sourceFiles = [];
|
|
12
|
+
|
|
13
|
+
for (const relativePath of files) {
|
|
14
|
+
if ((!relativePath.startsWith("src/") && !relativePath.startsWith("test/")) || !relativePath.endsWith(".ts") || relativePath.endsWith(".d.ts")) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!isFileSelected(options.files, relativePath)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const absolutePath = path.join(targetPath, relativePath);
|
|
23
|
+
const raw = await readFile(absolutePath, "utf8");
|
|
24
|
+
const ast = ts.createSourceFile(absolutePath, raw, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
25
|
+
sourceFiles.push({ relativePath, absolutePath, raw, ast });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return sourceFiles;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function loadProjectAnalysisContext(targetPath, options = {}) {
|
|
32
|
+
const sourceFiles = await loadProjectSourceFiles(targetPath, options);
|
|
33
|
+
const absolutePaths = sourceFiles.map((file) => file.absolutePath);
|
|
34
|
+
const compilerOptions = {
|
|
35
|
+
target: ts.ScriptTarget.ES2022,
|
|
36
|
+
module: ts.ModuleKind.ESNext,
|
|
37
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
38
|
+
allowJs: false,
|
|
39
|
+
strict: true,
|
|
40
|
+
skipLibCheck: true,
|
|
41
|
+
noEmit: true,
|
|
42
|
+
};
|
|
43
|
+
const program = ts.createProgram(absolutePaths, compilerOptions);
|
|
44
|
+
const checker = program.getTypeChecker();
|
|
45
|
+
const sourceFilesByPath = new Map();
|
|
46
|
+
const sourceFilesByRelativePath = new Map();
|
|
47
|
+
|
|
48
|
+
for (const file of sourceFiles) {
|
|
49
|
+
const programSourceFile = program.getSourceFile(file.absolutePath);
|
|
50
|
+
const enriched = {
|
|
51
|
+
...file,
|
|
52
|
+
ast: programSourceFile ?? file.ast,
|
|
53
|
+
featureName: getFeatureName(file.relativePath),
|
|
54
|
+
isRootSourceFile: isRootSourceFile(file.relativePath),
|
|
55
|
+
};
|
|
56
|
+
sourceFilesByPath.set(file.absolutePath, enriched);
|
|
57
|
+
sourceFilesByRelativePath.set(file.relativePath, enriched);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { targetPath, compilerOptions, sourceFiles: [...sourceFilesByRelativePath.values()], sourceFilesByPath, sourceFilesByRelativePath, program, checker };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isExportedClass(node) {
|
|
64
|
+
return ts.isClassDeclaration(node) && Array.isArray(node.modifiers) && node.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function visitNodes(node, visitor) {
|
|
68
|
+
visitor(node);
|
|
69
|
+
ts.forEachChild(node, (child) => visitNodes(child, visitor));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function visitNonNestedStatements(node, visitor) {
|
|
73
|
+
ts.forEachChild(node, (child) => {
|
|
74
|
+
if (
|
|
75
|
+
ts.isFunctionDeclaration(child) ||
|
|
76
|
+
ts.isFunctionExpression(child) ||
|
|
77
|
+
ts.isArrowFunction(child) ||
|
|
78
|
+
ts.isMethodDeclaration(child) ||
|
|
79
|
+
ts.isConstructorDeclaration(child)
|
|
80
|
+
) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
visitor(child);
|
|
85
|
+
visitNonNestedStatements(child, visitor);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getIdentifierText(name) {
|
|
90
|
+
return ts.isIdentifier(name) ? name.text : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getFeatureName(relativePath) {
|
|
94
|
+
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
95
|
+
|
|
96
|
+
if (!normalizedPath.startsWith("src/")) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const segments = normalizedPath.split("/");
|
|
101
|
+
return segments.length >= 3 ? segments[1] : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function isRootSourceFile(relativePath) {
|
|
105
|
+
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
106
|
+
return normalizedPath.startsWith("src/") && normalizedPath.split("/").length === 2;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resolveImportRelativePath(relativePath, specifier) {
|
|
110
|
+
if (!specifier.startsWith(".")) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const normalizedSourceDir = path.posix.dirname(relativePath.split(path.sep).join("/"));
|
|
115
|
+
const resolved = path.posix.normalize(path.posix.join(normalizedSourceDir, specifier));
|
|
116
|
+
return resolved.endsWith(".ts") ? resolved : `${resolved}.ts`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getLineAndColumn(sourceFile, position) {
|
|
120
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(position);
|
|
121
|
+
return { line: line + 1, column: character + 1 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getNodeTextSlice(sourceFile, node) {
|
|
125
|
+
return sourceFile.text.slice(node.getStart(sourceFile), node.getEnd());
|
|
126
|
+
}
|