gh-inari 0.2.0 → 0.5.2
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 +77 -12
- package/dist/artifact.d.ts +1 -1
- package/dist/artifact.js +1 -1
- package/dist/artifact.js.map +1 -1
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +433 -31
- package/dist/cli.js.map +1 -1
- package/dist/contract/ir.d.ts +8 -0
- package/dist/contract/ir.js +3 -1
- package/dist/contract/ir.js.map +1 -1
- package/dist/github/adapter.d.ts +6 -3
- package/dist/github/adapter.js +65 -11
- package/dist/github/adapter.js.map +1 -1
- package/dist/github/capability.js +1 -0
- package/dist/github/capability.js.map +1 -1
- package/dist/github/errors.d.ts +16 -1
- package/dist/github/errors.js +18 -0
- package/dist/github/errors.js.map +1 -1
- package/dist/github/transport.d.ts +19 -0
- package/dist/github/transport.js +87 -25
- package/dist/github/transport.js.map +1 -1
- package/dist/github/types.d.ts +6 -0
- package/dist/governance.d.ts +64 -4
- package/dist/governance.js +251 -6
- package/dist/governance.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pr-policy.js +109 -1
- package/dist/pr-policy.js.map +1 -1
- package/dist/pull-request-template.js +5 -1
- package/dist/pull-request-template.js.map +1 -1
- package/dist/semantic-template.d.ts +107 -0
- package/dist/semantic-template.js +1151 -0
- package/dist/semantic-template.js.map +1 -0
- package/gh-inari +77 -11
- package/package.json +26 -20
|
@@ -0,0 +1,1151 @@
|
|
|
1
|
+
import { lstat, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
6
|
+
import { assertCanonicalContract, } from "./contract/ir.js";
|
|
7
|
+
import { compileIssueFormYaml } from "./contract/issue-form.js";
|
|
8
|
+
import { parsePullRequestTemplate, renderPullRequestTemplate } from "./pull-request-template.js";
|
|
9
|
+
export const SEMANTIC_TEMPLATE_VERSION = 1;
|
|
10
|
+
export const SEMANTIC_TEMPLATE_DIRECTORY = ".github/inari";
|
|
11
|
+
export const SEMANTIC_ISSUE_DIRECTORY = ".github/inari/issues";
|
|
12
|
+
export const SEMANTIC_PULL_REQUEST_FILE = ".github/inari/pull-request.json";
|
|
13
|
+
export const GENERATED_TEMPLATE_NOTICE = "Generated by gh-inari from .github/inari; do not edit.";
|
|
14
|
+
export class SemanticTemplateError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
path;
|
|
17
|
+
violations;
|
|
18
|
+
constructor(violations, options) {
|
|
19
|
+
const first = violations[0];
|
|
20
|
+
if (first === undefined)
|
|
21
|
+
throw new Error("Semantic template errors require at least one violation.");
|
|
22
|
+
super(violations.map((violation) => `${violation.path}: ${violation.message}`).join("\n"), options);
|
|
23
|
+
this.name = "SemanticTemplateError";
|
|
24
|
+
this.code = first.code;
|
|
25
|
+
this.path = first.path;
|
|
26
|
+
this.violations = violations;
|
|
27
|
+
}
|
|
28
|
+
toJSON() {
|
|
29
|
+
return { code: this.code, path: this.path, message: this.message, violations: this.violations };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const ISSUE_NATIVE_DIRECTORY = ".github/ISSUE_TEMPLATE";
|
|
33
|
+
const PR_NATIVE_DEFAULT = ".github/PULL_REQUEST_TEMPLATE.md";
|
|
34
|
+
const PR_NATIVE_DIRECTORY = ".github/PULL_REQUEST_TEMPLATE";
|
|
35
|
+
const JSON_EXTENSION = ".json";
|
|
36
|
+
/** Find semantic contracts without reading their contents. */
|
|
37
|
+
export async function discoverSemanticTemplates(repositoryRoot = process.cwd()) {
|
|
38
|
+
const root = resolveRoot(repositoryRoot);
|
|
39
|
+
const identities = [];
|
|
40
|
+
const issues = path.join(root, SEMANTIC_ISSUE_DIRECTORY);
|
|
41
|
+
if (await optionalDirectory(issues)) {
|
|
42
|
+
const entries = await readdir(issues, { withFileTypes: true });
|
|
43
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name, "en-US"))) {
|
|
44
|
+
if (!entry.isFile() || path.extname(entry.name).toLocaleLowerCase("en-US") !== JSON_EXTENSION)
|
|
45
|
+
continue;
|
|
46
|
+
const id = path.basename(entry.name, path.extname(entry.name));
|
|
47
|
+
identities.push({
|
|
48
|
+
id,
|
|
49
|
+
kind: "issue",
|
|
50
|
+
name: id,
|
|
51
|
+
sourcePath: toRepositoryPath(root, path.join(issues, entry.name)),
|
|
52
|
+
generatedPath: toRepositoryPath(root, path.join(root, ISSUE_NATIVE_DIRECTORY, `${id}.yml`)),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const pullRequest = path.join(root, SEMANTIC_PULL_REQUEST_FILE);
|
|
57
|
+
if (await optionalFile(pullRequest)) {
|
|
58
|
+
identities.push({
|
|
59
|
+
id: "pull-request",
|
|
60
|
+
kind: "pull_request",
|
|
61
|
+
name: "Pull request",
|
|
62
|
+
sourcePath: toRepositoryPath(root, pullRequest),
|
|
63
|
+
generatedPath: toRepositoryPath(root, path.join(root, PR_NATIVE_DEFAULT)),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const pullRequests = path.join(root, SEMANTIC_TEMPLATE_DIRECTORY, "pull-requests");
|
|
67
|
+
if (await optionalDirectory(pullRequests)) {
|
|
68
|
+
const entries = await readdir(pullRequests, { withFileTypes: true });
|
|
69
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name, "en-US"))) {
|
|
70
|
+
if (!entry.isFile() || path.extname(entry.name).toLocaleLowerCase("en-US") !== JSON_EXTENSION)
|
|
71
|
+
continue;
|
|
72
|
+
const id = path.basename(entry.name, path.extname(entry.name));
|
|
73
|
+
identities.push({
|
|
74
|
+
id,
|
|
75
|
+
kind: "pull_request",
|
|
76
|
+
name: id,
|
|
77
|
+
sourcePath: toRepositoryPath(root, path.join(pullRequests, entry.name)),
|
|
78
|
+
generatedPath: toRepositoryPath(root, path.join(root, PR_NATIVE_DIRECTORY, `${id}.md`)),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return identities;
|
|
83
|
+
}
|
|
84
|
+
/** Synchronous counterpart used by bounded governance and tooling callers. */
|
|
85
|
+
export function discoverSemanticTemplatesSync(repositoryRoot = process.cwd()) {
|
|
86
|
+
const root = resolveRoot(repositoryRoot);
|
|
87
|
+
const identities = [];
|
|
88
|
+
const issues = path.join(root, SEMANTIC_ISSUE_DIRECTORY);
|
|
89
|
+
if (optionalDirectorySync(issues)) {
|
|
90
|
+
for (const entry of readdirSync(issues, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name, "en-US"))) {
|
|
91
|
+
if (!entry.isFile() || path.extname(entry.name).toLocaleLowerCase("en-US") !== JSON_EXTENSION)
|
|
92
|
+
continue;
|
|
93
|
+
const id = path.basename(entry.name, path.extname(entry.name));
|
|
94
|
+
identities.push({
|
|
95
|
+
id,
|
|
96
|
+
kind: "issue",
|
|
97
|
+
name: id,
|
|
98
|
+
sourcePath: toRepositoryPath(root, path.join(issues, entry.name)),
|
|
99
|
+
generatedPath: toRepositoryPath(root, path.join(root, ISSUE_NATIVE_DIRECTORY, `${id}.yml`)),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const pullRequest = path.join(root, SEMANTIC_PULL_REQUEST_FILE);
|
|
104
|
+
if (optionalFileSync(pullRequest)) {
|
|
105
|
+
identities.push({
|
|
106
|
+
id: "pull-request",
|
|
107
|
+
kind: "pull_request",
|
|
108
|
+
name: "Pull request",
|
|
109
|
+
sourcePath: toRepositoryPath(root, pullRequest),
|
|
110
|
+
generatedPath: toRepositoryPath(root, path.join(root, PR_NATIVE_DEFAULT)),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const pullRequests = path.join(root, SEMANTIC_TEMPLATE_DIRECTORY, "pull-requests");
|
|
114
|
+
if (optionalDirectorySync(pullRequests)) {
|
|
115
|
+
for (const entry of readdirSync(pullRequests, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name, "en-US"))) {
|
|
116
|
+
if (!entry.isFile() || path.extname(entry.name).toLocaleLowerCase("en-US") !== JSON_EXTENSION)
|
|
117
|
+
continue;
|
|
118
|
+
const id = path.basename(entry.name, path.extname(entry.name));
|
|
119
|
+
identities.push({
|
|
120
|
+
id,
|
|
121
|
+
kind: "pull_request",
|
|
122
|
+
name: id,
|
|
123
|
+
sourcePath: toRepositoryPath(root, path.join(pullRequests, entry.name)),
|
|
124
|
+
generatedPath: toRepositoryPath(root, path.join(root, PR_NATIVE_DIRECTORY, `${id}.md`)),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return identities;
|
|
129
|
+
}
|
|
130
|
+
export async function readSemanticTemplate(repositoryRoot, identity) {
|
|
131
|
+
const root = resolveRoot(repositoryRoot);
|
|
132
|
+
const sourcePath = safeRepositoryPath(root, identity.sourcePath);
|
|
133
|
+
let serialized;
|
|
134
|
+
try {
|
|
135
|
+
serialized = await readFile(sourcePath, "utf8");
|
|
136
|
+
}
|
|
137
|
+
catch (cause) {
|
|
138
|
+
throw new SemanticTemplateError([
|
|
139
|
+
{
|
|
140
|
+
code: "SEMANTIC_TEMPLATE_SOURCE_ERROR",
|
|
141
|
+
path: identity.sourcePath,
|
|
142
|
+
message: `Cannot read semantic source "${identity.sourcePath}".`,
|
|
143
|
+
},
|
|
144
|
+
], { cause });
|
|
145
|
+
}
|
|
146
|
+
return { ...identity, source: parseSemanticTemplate(serialized, identity.sourcePath) };
|
|
147
|
+
}
|
|
148
|
+
export function readSemanticTemplateSync(repositoryRoot, identity) {
|
|
149
|
+
const root = resolveRoot(repositoryRoot);
|
|
150
|
+
const sourcePath = safeRepositoryPath(root, identity.sourcePath);
|
|
151
|
+
try {
|
|
152
|
+
return { ...identity, source: parseSemanticTemplate(readFileSync(sourcePath, "utf8"), identity.sourcePath) };
|
|
153
|
+
}
|
|
154
|
+
catch (cause) {
|
|
155
|
+
if (cause instanceof SemanticTemplateError)
|
|
156
|
+
throw cause;
|
|
157
|
+
throw new SemanticTemplateError([
|
|
158
|
+
{
|
|
159
|
+
code: "SEMANTIC_TEMPLATE_SOURCE_ERROR",
|
|
160
|
+
path: identity.sourcePath,
|
|
161
|
+
message: `Cannot read semantic source "${identity.sourcePath}".`,
|
|
162
|
+
},
|
|
163
|
+
], { cause });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export function parseSemanticTemplate(serialized, sourcePath = "$") {
|
|
167
|
+
let value;
|
|
168
|
+
try {
|
|
169
|
+
value = JSON.parse(serialized);
|
|
170
|
+
}
|
|
171
|
+
catch (cause) {
|
|
172
|
+
throw new SemanticTemplateError([
|
|
173
|
+
{
|
|
174
|
+
code: "SEMANTIC_TEMPLATE_INVALID_JSON",
|
|
175
|
+
path: sourcePath,
|
|
176
|
+
message: "Semantic source must contain valid JSON.",
|
|
177
|
+
},
|
|
178
|
+
], { cause });
|
|
179
|
+
}
|
|
180
|
+
return normalizeSemanticTemplate(value, sourcePath);
|
|
181
|
+
}
|
|
182
|
+
export function normalizeSemanticTemplate(value, sourcePath = "$") {
|
|
183
|
+
const violations = [];
|
|
184
|
+
if (!isRecord(value)) {
|
|
185
|
+
throw new SemanticTemplateError([
|
|
186
|
+
{ code: "SEMANTIC_TEMPLATE_INVALID_ROOT", path: sourcePath, message: "Semantic source must be a JSON object." },
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
assertKeys(value, [
|
|
190
|
+
"version",
|
|
191
|
+
"kind",
|
|
192
|
+
"artifactKind",
|
|
193
|
+
"id",
|
|
194
|
+
"name",
|
|
195
|
+
"description",
|
|
196
|
+
"title",
|
|
197
|
+
"labels",
|
|
198
|
+
"sections",
|
|
199
|
+
"fields",
|
|
200
|
+
"template",
|
|
201
|
+
"metadata",
|
|
202
|
+
], sourcePath, violations);
|
|
203
|
+
const template = isRecord(value.template) ? value.template : undefined;
|
|
204
|
+
const metadata = isRecord(value.metadata) ? value.metadata : undefined;
|
|
205
|
+
if (value.template !== undefined && template === undefined)
|
|
206
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", `${sourcePath}.template`, "template must be an object.");
|
|
207
|
+
if (value.metadata !== undefined && metadata === undefined)
|
|
208
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", `${sourcePath}.metadata`, "metadata must be an object.");
|
|
209
|
+
if (template !== undefined)
|
|
210
|
+
assertKeys(template, ["kind", "artifactKind", "id", "name", "description", "title", "labels", "sections", "fields"], `${sourcePath}.template`, violations);
|
|
211
|
+
if (metadata !== undefined)
|
|
212
|
+
assertKeys(metadata, ["description", "title", "labels"], `${sourcePath}.metadata`, violations);
|
|
213
|
+
const version = value.version ?? template?.version;
|
|
214
|
+
if (version !== SEMANTIC_TEMPLATE_VERSION)
|
|
215
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_UNSUPPORTED_VERSION", `${sourcePath}.version`, `Only semantic template version ${SEMANTIC_TEMPLATE_VERSION} is supported.`);
|
|
216
|
+
const rawKind = value.kind ?? value.artifactKind ?? template?.kind ?? template?.artifactKind;
|
|
217
|
+
const kind = rawKind === "pr" ? "pull_request" : rawKind;
|
|
218
|
+
if (kind !== "issue" && kind !== "pull_request")
|
|
219
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", `${sourcePath}.kind`, "kind must be issue or pull_request.");
|
|
220
|
+
const id = stringValue(value.id ?? template?.id, `${sourcePath}.id`, violations);
|
|
221
|
+
const name = stringValue(value.name ?? template?.name, `${sourcePath}.name`, violations);
|
|
222
|
+
const description = optionalString(value.description ?? metadata?.description ?? template?.description, `${sourcePath}.description`, violations);
|
|
223
|
+
const title = optionalString(value.title ?? metadata?.title ?? template?.title, `${sourcePath}.title`, violations);
|
|
224
|
+
const labels = optionalStringList(value.labels ?? metadata?.labels ?? template?.labels, `${sourcePath}.labels`, violations);
|
|
225
|
+
const rawSections = value.sections ?? value.fields ?? template?.sections ?? template?.fields;
|
|
226
|
+
if (!Array.isArray(rawSections) || rawSections.length === 0)
|
|
227
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", `${sourcePath}.sections`, "sections must be a non-empty array.");
|
|
228
|
+
const sections = Array.isArray(rawSections)
|
|
229
|
+
? rawSections.map((section, index) => normalizeSection(section, `${sourcePath}.sections[${index}]`, violations))
|
|
230
|
+
: [];
|
|
231
|
+
const ids = new Set();
|
|
232
|
+
for (const [index, section] of sections.entries()) {
|
|
233
|
+
if (section !== undefined && ids.has(section.id))
|
|
234
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_AMBIGUOUS", `${sourcePath}.sections[${index}].id`, `Duplicate section id "${section.id}".`);
|
|
235
|
+
if (section !== undefined)
|
|
236
|
+
ids.add(section.id);
|
|
237
|
+
}
|
|
238
|
+
if (kind === "issue" && (description === undefined || description.length === 0))
|
|
239
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", `${sourcePath}.description`, "Issue templates require a non-empty description.");
|
|
240
|
+
if (violations.length > 0)
|
|
241
|
+
throw new SemanticTemplateError(violations);
|
|
242
|
+
return {
|
|
243
|
+
version: SEMANTIC_TEMPLATE_VERSION,
|
|
244
|
+
kind: kind,
|
|
245
|
+
id: id,
|
|
246
|
+
name: name,
|
|
247
|
+
...(description === undefined ? {} : { description }),
|
|
248
|
+
...(title === undefined ? {} : { title }),
|
|
249
|
+
...(labels === undefined ? {} : { labels }),
|
|
250
|
+
sections: sections,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
export async function compileSemanticTemplate(repositoryRoot, document) {
|
|
254
|
+
return compileSemanticTemplateSource(document.source, document.generatedPath);
|
|
255
|
+
}
|
|
256
|
+
export function compileSemanticTemplateSync(repositoryRoot, document) {
|
|
257
|
+
return compileSemanticTemplateSource(document.source, document.generatedPath);
|
|
258
|
+
}
|
|
259
|
+
/** Compile an already trusted semantic source without consulting the filesystem. */
|
|
260
|
+
export function compileSemanticTemplateSource(source, generatedPath) {
|
|
261
|
+
const native = renderSemanticNative(source, generatedPath);
|
|
262
|
+
const identity = {
|
|
263
|
+
id: source.id,
|
|
264
|
+
kind: source.kind,
|
|
265
|
+
name: source.name,
|
|
266
|
+
sourcePath: "<memory>",
|
|
267
|
+
generatedPath,
|
|
268
|
+
source,
|
|
269
|
+
};
|
|
270
|
+
const contract = compileNativeProjection(native, identity);
|
|
271
|
+
return applySemanticIdentityAndConstraints(contract, source);
|
|
272
|
+
}
|
|
273
|
+
/** Render one semantic contract to the committed native GitHub projection. */
|
|
274
|
+
export function renderSemanticNative(source, generatedPath) {
|
|
275
|
+
if (source.kind === "issue")
|
|
276
|
+
return renderIssueNative(source);
|
|
277
|
+
const provisional = renderPullRequestProvisional(source);
|
|
278
|
+
const identity = {
|
|
279
|
+
id: source.id,
|
|
280
|
+
type: "pull-request",
|
|
281
|
+
kind: "pull-request",
|
|
282
|
+
name: source.name,
|
|
283
|
+
path: generatedPath,
|
|
284
|
+
};
|
|
285
|
+
const parsed = parsePullRequestTemplate(provisional, identity);
|
|
286
|
+
const rebound = applySemanticIdentityAndConstraints(parsed, source);
|
|
287
|
+
return `<!-- ${GENERATED_TEMPLATE_NOTICE} -->\n\n${renderPullRequestTemplate(rebound)}`;
|
|
288
|
+
}
|
|
289
|
+
export function renderSemanticCompactSchema(contract) {
|
|
290
|
+
assertCanonicalContract(contract);
|
|
291
|
+
const properties = {};
|
|
292
|
+
const required = [];
|
|
293
|
+
for (const section of contract.sections) {
|
|
294
|
+
for (const field of section.fields) {
|
|
295
|
+
const constraints = contract.supplementalConstraints.fields.find((entry) => entry.fieldId === field.id);
|
|
296
|
+
const entry = { id: field.id, label: field.label, type: field.type };
|
|
297
|
+
if (field.description !== undefined)
|
|
298
|
+
entry.description = field.description;
|
|
299
|
+
const hasDefault = field.defaultValue !== undefined || field.nativeMetadata.defaultValue !== undefined;
|
|
300
|
+
const isRequired = (constraints?.required ?? field.required === "required") && !hasDefault;
|
|
301
|
+
if (isRequired)
|
|
302
|
+
required.push(field.id);
|
|
303
|
+
if (field.type === "enum")
|
|
304
|
+
entry.choices = field.options.map((option) => option.value);
|
|
305
|
+
if (field.type === "array")
|
|
306
|
+
entry.choices = field.items.options?.map((option) => option.value) ?? [];
|
|
307
|
+
if (field.type === "checklist")
|
|
308
|
+
entry.choices = field.items.map((item) => ({ id: item.id, label: item.label, required: item.required }));
|
|
309
|
+
for (const key of ["minLength", "maxLength", "pattern", "minItems", "maxItems"]) {
|
|
310
|
+
const value = constraints?.[key] ?? field.constraints?.[key];
|
|
311
|
+
if (value !== undefined)
|
|
312
|
+
entry[key] = value;
|
|
313
|
+
}
|
|
314
|
+
properties[field.id] = entry;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { version: SEMANTIC_TEMPLATE_VERSION, kind: contract.artifactKind, fields: properties, required };
|
|
318
|
+
}
|
|
319
|
+
export async function syncSemanticTemplates(repositoryRoot = process.cwd(), check = false) {
|
|
320
|
+
const root = resolveRoot(repositoryRoot);
|
|
321
|
+
const identities = await discoverSemanticTemplates(root);
|
|
322
|
+
const generated = new Map();
|
|
323
|
+
for (const identity of identities) {
|
|
324
|
+
const document = await readSemanticTemplate(root, identity);
|
|
325
|
+
generated.set(identity.generatedPath, renderSemanticNative(document.source, identity.generatedPath));
|
|
326
|
+
}
|
|
327
|
+
const drift = [];
|
|
328
|
+
const written = [];
|
|
329
|
+
for (const [relativePath, content] of generated) {
|
|
330
|
+
const absolutePath = safeRepositoryPath(root, relativePath);
|
|
331
|
+
let current;
|
|
332
|
+
try {
|
|
333
|
+
current = await readFile(absolutePath, "utf8");
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
current = undefined;
|
|
337
|
+
}
|
|
338
|
+
if (current !== content) {
|
|
339
|
+
drift.push(relativePath);
|
|
340
|
+
if (!check) {
|
|
341
|
+
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
342
|
+
await writeRepositoryFile(absolutePath, content);
|
|
343
|
+
written.push(relativePath);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const staleGenerated = await findStaleGeneratedFiles(root, generated);
|
|
348
|
+
if (!check) {
|
|
349
|
+
for (const relativePath of staleGenerated) {
|
|
350
|
+
const absolutePath = safeRepositoryPath(root, relativePath);
|
|
351
|
+
const current = await readFile(absolutePath, "utf8");
|
|
352
|
+
await writeRepositoryFile(absolutePath, current);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
check,
|
|
357
|
+
changed: drift.length > 0 || staleGenerated.length > 0,
|
|
358
|
+
written,
|
|
359
|
+
drift,
|
|
360
|
+
staleGenerated,
|
|
361
|
+
generated: [...generated.keys()],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
export async function importNativeTemplate(repositoryRoot, nativePath, destinationPath) {
|
|
365
|
+
const root = resolveRoot(repositoryRoot);
|
|
366
|
+
const sourcePath = safeRepositoryPath(root, nativePath);
|
|
367
|
+
const content = await readFile(sourcePath, "utf8");
|
|
368
|
+
const relative = toRepositoryPath(root, sourcePath);
|
|
369
|
+
const kind = relative.includes("ISSUE_TEMPLATE/") ? "issue" : "pull_request";
|
|
370
|
+
const base = path.basename(relative, path.extname(relative));
|
|
371
|
+
const identity = {
|
|
372
|
+
id: base,
|
|
373
|
+
type: kind === "issue" ? "issue-form" : "pull-request",
|
|
374
|
+
kind: kind === "issue" ? "issue" : "pull-request",
|
|
375
|
+
name: base,
|
|
376
|
+
path: relative,
|
|
377
|
+
};
|
|
378
|
+
const contract = kind === "issue"
|
|
379
|
+
? compileIssueFormYaml(stripGeneratedNotice(content), identity)
|
|
380
|
+
: parsePullRequestTemplate(stripGeneratedNotice(content), identity);
|
|
381
|
+
const source = semanticSourceFromContract(contract);
|
|
382
|
+
const outputPath = destinationPath ?? defaultImportPath(root, source, base);
|
|
383
|
+
const absoluteOutput = safeRepositoryPath(root, outputPath);
|
|
384
|
+
await mkdir(path.dirname(absoluteOutput), { recursive: true });
|
|
385
|
+
await writeRepositoryFile(absoluteOutput, `${JSON.stringify(source, null, 2)}\n`);
|
|
386
|
+
const writtenPath = toRepositoryPath(root, absoluteOutput);
|
|
387
|
+
const warning = destinationPath === undefined ? undefined : discoverableWarning(source.kind, writtenPath);
|
|
388
|
+
return warning === undefined ? { path: writtenPath, source } : { path: writtenPath, source, warning };
|
|
389
|
+
}
|
|
390
|
+
function discoverableWarning(kind, writtenPath) {
|
|
391
|
+
const discoverable = kind === "issue"
|
|
392
|
+
? writtenPath.startsWith(`${SEMANTIC_ISSUE_DIRECTORY}/`) && writtenPath.endsWith(JSON_EXTENSION)
|
|
393
|
+
: writtenPath === SEMANTIC_PULL_REQUEST_FILE ||
|
|
394
|
+
(writtenPath.startsWith(`${SEMANTIC_TEMPLATE_DIRECTORY}/pull-requests/`) &&
|
|
395
|
+
writtenPath.endsWith(JSON_EXTENSION));
|
|
396
|
+
if (discoverable)
|
|
397
|
+
return undefined;
|
|
398
|
+
return (`wrote ${writtenPath}, but no semantic template will be discovered there; ` +
|
|
399
|
+
`expected ${SEMANTIC_ISSUE_DIRECTORY}/<id>.json, ${SEMANTIC_PULL_REQUEST_FILE}, ` +
|
|
400
|
+
`or ${SEMANTIC_TEMPLATE_DIRECTORY}/pull-requests/<id>.json`);
|
|
401
|
+
}
|
|
402
|
+
export function semanticSourceFromContract(contractInput) {
|
|
403
|
+
assertCanonicalContract(contractInput);
|
|
404
|
+
const sections = [];
|
|
405
|
+
for (const section of contractInput.sections) {
|
|
406
|
+
if (section.kind === "documentation") {
|
|
407
|
+
sections.push({
|
|
408
|
+
id: section.id,
|
|
409
|
+
kind: "documentation",
|
|
410
|
+
content: section.content ?? "",
|
|
411
|
+
...(section.title === undefined ? {} : { label: section.title }),
|
|
412
|
+
...headingMetadata(section),
|
|
413
|
+
});
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const field = section.fields[0];
|
|
417
|
+
if (field === undefined)
|
|
418
|
+
continue;
|
|
419
|
+
sections.push({
|
|
420
|
+
id: field.id,
|
|
421
|
+
kind: "input",
|
|
422
|
+
type: field.type,
|
|
423
|
+
label: field.label,
|
|
424
|
+
...(field.description === undefined ? {} : { description: field.description }),
|
|
425
|
+
...(field.required === "unknown" ? {} : { required: field.required === "required" }),
|
|
426
|
+
...(field.nativeMetadata.elementType === "input" || field.nativeMetadata.elementType === "textarea"
|
|
427
|
+
? { element: field.nativeMetadata.elementType }
|
|
428
|
+
: {}),
|
|
429
|
+
...(field.nativeMetadata.placeholder === undefined ? {} : { placeholder: field.nativeMetadata.placeholder }),
|
|
430
|
+
...(field.nativeMetadata.render === undefined ? {} : { render: field.nativeMetadata.render }),
|
|
431
|
+
...(field.type === "string" || field.type === "enum"
|
|
432
|
+
? field.defaultValue === undefined
|
|
433
|
+
? {}
|
|
434
|
+
: { defaultValue: field.defaultValue }
|
|
435
|
+
: {}),
|
|
436
|
+
...(field.type === "array"
|
|
437
|
+
? {
|
|
438
|
+
multiple: field.selection === "multi_select",
|
|
439
|
+
options: field.items.options?.map((option) => ({
|
|
440
|
+
id: option.value,
|
|
441
|
+
label: option.label,
|
|
442
|
+
...(option.description === undefined ? {} : { description: option.description }),
|
|
443
|
+
})),
|
|
444
|
+
}
|
|
445
|
+
: {}),
|
|
446
|
+
...(field.type === "enum"
|
|
447
|
+
? {
|
|
448
|
+
options: field.options.map((option) => ({
|
|
449
|
+
id: option.value,
|
|
450
|
+
label: option.label,
|
|
451
|
+
...(option.description === undefined ? {} : { description: option.description }),
|
|
452
|
+
})),
|
|
453
|
+
}
|
|
454
|
+
: {}),
|
|
455
|
+
...(field.type === "checklist"
|
|
456
|
+
? { options: field.items.map((item) => ({ id: item.id, label: item.label, required: item.required })) }
|
|
457
|
+
: {}),
|
|
458
|
+
...headingMetadata(section),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
const result = {
|
|
462
|
+
version: SEMANTIC_TEMPLATE_VERSION,
|
|
463
|
+
kind: contractInput.artifactKind,
|
|
464
|
+
id: contractInput.templateIdentity.id,
|
|
465
|
+
name: contractInput.templateIdentity.name,
|
|
466
|
+
...(contractInput.nativeMetadata.description === undefined
|
|
467
|
+
? {}
|
|
468
|
+
: { description: contractInput.nativeMetadata.description }),
|
|
469
|
+
...(contractInput.nativeMetadata.title === undefined ? {} : { title: contractInput.nativeMetadata.title }),
|
|
470
|
+
...(contractInput.nativeMetadata.labels === undefined ? {} : { labels: contractInput.nativeMetadata.labels }),
|
|
471
|
+
sections,
|
|
472
|
+
};
|
|
473
|
+
return normalizeSemanticTemplate(result);
|
|
474
|
+
}
|
|
475
|
+
export function selectSemanticTemplate(identities, kind, selector) {
|
|
476
|
+
const candidates = identities.filter((identity) => identity.kind === kind);
|
|
477
|
+
const matches = candidates.filter((identity) => semanticSelectorMatches(identity, selector));
|
|
478
|
+
if (matches.length === 1)
|
|
479
|
+
return matches[0];
|
|
480
|
+
if (matches.length === 0)
|
|
481
|
+
throw new SemanticTemplateError([
|
|
482
|
+
{
|
|
483
|
+
code: "SEMANTIC_TEMPLATE_INVALID_VALUE",
|
|
484
|
+
path: "$.template",
|
|
485
|
+
message: "No semantic template matches the requested selector.",
|
|
486
|
+
},
|
|
487
|
+
]);
|
|
488
|
+
throw new SemanticTemplateError([
|
|
489
|
+
{
|
|
490
|
+
code: "SEMANTIC_TEMPLATE_AMBIGUOUS",
|
|
491
|
+
path: "$.template",
|
|
492
|
+
message: "The semantic template selector matches multiple contracts.",
|
|
493
|
+
},
|
|
494
|
+
]);
|
|
495
|
+
}
|
|
496
|
+
export function semanticTemplateGeneratedPath(kind, id) {
|
|
497
|
+
return kind === "issue"
|
|
498
|
+
? `${ISSUE_NATIVE_DIRECTORY}/${id}.yml`
|
|
499
|
+
: id === "pull-request"
|
|
500
|
+
? PR_NATIVE_DEFAULT
|
|
501
|
+
: `${PR_NATIVE_DIRECTORY}/${id}.md`;
|
|
502
|
+
}
|
|
503
|
+
function compileNativeProjection(native, document) {
|
|
504
|
+
if (document.source.kind === "issue") {
|
|
505
|
+
const identity = {
|
|
506
|
+
id: document.source.id,
|
|
507
|
+
name: document.source.name,
|
|
508
|
+
path: document.generatedPath,
|
|
509
|
+
type: "issue-form",
|
|
510
|
+
kind: "issue",
|
|
511
|
+
};
|
|
512
|
+
return compileIssueFormYaml(native, identity);
|
|
513
|
+
}
|
|
514
|
+
const identity = {
|
|
515
|
+
id: document.source.id,
|
|
516
|
+
type: "pull-request",
|
|
517
|
+
kind: "pull-request",
|
|
518
|
+
name: document.source.name,
|
|
519
|
+
path: document.generatedPath,
|
|
520
|
+
};
|
|
521
|
+
return parsePullRequestTemplate(native, identity);
|
|
522
|
+
}
|
|
523
|
+
function renderIssueNative(source) {
|
|
524
|
+
const body = source.sections.map((section) => {
|
|
525
|
+
if (section.kind === "documentation")
|
|
526
|
+
return { type: "markdown", attributes: { value: section.content ?? "" } };
|
|
527
|
+
const type = issueElementType(section);
|
|
528
|
+
const attributes = { label: section.label ?? section.id };
|
|
529
|
+
if (section.description !== undefined)
|
|
530
|
+
attributes.description = section.description;
|
|
531
|
+
if (section.placeholder !== undefined)
|
|
532
|
+
attributes.placeholder = section.placeholder;
|
|
533
|
+
if (section.defaultValue !== undefined && (type === "input" || type === "textarea"))
|
|
534
|
+
attributes.value = section.defaultValue;
|
|
535
|
+
if (type === "textarea" && section.render !== undefined)
|
|
536
|
+
attributes.render = section.render;
|
|
537
|
+
if (type === "dropdown") {
|
|
538
|
+
attributes.options = (section.options ?? []).map((option) => option.label);
|
|
539
|
+
if (section.multiple === true)
|
|
540
|
+
attributes.multiple = true;
|
|
541
|
+
const defaultValue = Array.isArray(section.defaultValue) ? section.defaultValue[0] : section.defaultValue;
|
|
542
|
+
const index = defaultValue === undefined
|
|
543
|
+
? undefined
|
|
544
|
+
: (section.options ?? []).findIndex((option) => option.value === defaultValue || option.id === defaultValue || option.label === defaultValue);
|
|
545
|
+
if (index !== undefined && index >= 0)
|
|
546
|
+
attributes.default = index;
|
|
547
|
+
}
|
|
548
|
+
if (type === "checkboxes")
|
|
549
|
+
attributes.options = (section.options ?? []).map((option) => ({
|
|
550
|
+
label: option.label,
|
|
551
|
+
...(option.required === undefined ? {} : { required: option.required }),
|
|
552
|
+
}));
|
|
553
|
+
return {
|
|
554
|
+
type,
|
|
555
|
+
id: section.id,
|
|
556
|
+
attributes,
|
|
557
|
+
...(section.required === undefined ? {} : { validations: { required: section.required } }),
|
|
558
|
+
};
|
|
559
|
+
});
|
|
560
|
+
const native = {
|
|
561
|
+
name: source.name,
|
|
562
|
+
...(source.description === undefined ? {} : { description: source.description }),
|
|
563
|
+
...(source.title === undefined ? {} : { title: source.title }),
|
|
564
|
+
...(source.labels === undefined ? {} : { labels: source.labels }),
|
|
565
|
+
body,
|
|
566
|
+
};
|
|
567
|
+
return `# ${GENERATED_TEMPLATE_NOTICE}\n${stringifyYaml(native, { lineWidth: 0 })}`;
|
|
568
|
+
}
|
|
569
|
+
function renderPullRequestProvisional(source) {
|
|
570
|
+
const blocks = source.sections.map((section) => {
|
|
571
|
+
if (section.kind === "documentation") {
|
|
572
|
+
const content = section.content ?? "";
|
|
573
|
+
return section.headingLevel === undefined
|
|
574
|
+
? content
|
|
575
|
+
: `${"#".repeat(section.headingLevel)} ${section.label ?? section.id}\n\n${content}`;
|
|
576
|
+
}
|
|
577
|
+
const headingLevel = section.headingLevel ?? 2;
|
|
578
|
+
const heading = `${"#".repeat(headingLevel)} ${section.label ?? section.id}`;
|
|
579
|
+
if (section.type === "checklist") {
|
|
580
|
+
const placeholder = section.placeholder === undefined ? [] : [section.placeholder];
|
|
581
|
+
const items = (section.options ?? []).map((option) => `- [ ] ${option.label}`);
|
|
582
|
+
return [heading, ...placeholder, items.join("\n")].filter((entry) => entry.length > 0).join("\n\n");
|
|
583
|
+
}
|
|
584
|
+
return [heading, section.placeholder ?? ""].join("\n\n");
|
|
585
|
+
});
|
|
586
|
+
return `${blocks.join("\n\n")}\n`;
|
|
587
|
+
}
|
|
588
|
+
function applySemanticIdentityAndConstraints(contractInput, source) {
|
|
589
|
+
const sections = [];
|
|
590
|
+
let sourceIndex = 0;
|
|
591
|
+
for (const section of contractInput.sections) {
|
|
592
|
+
const sourceSection = source.sections[sourceIndex];
|
|
593
|
+
if (sourceSection === undefined) {
|
|
594
|
+
sections.push(section);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
sourceIndex += 1;
|
|
598
|
+
if (sourceSection.kind === "documentation") {
|
|
599
|
+
sections.push({
|
|
600
|
+
...section,
|
|
601
|
+
id: sourceSection.id,
|
|
602
|
+
...(sourceSection.label === undefined ? {} : { title: sourceSection.label }),
|
|
603
|
+
nativeMetadata: { ...section.nativeMetadata, sourceId: sourceSection.id },
|
|
604
|
+
});
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const field = section.fields[0];
|
|
608
|
+
if (field === undefined) {
|
|
609
|
+
sections.push(section);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const reboundField = rebindField(field, sourceSection);
|
|
613
|
+
sections.push({
|
|
614
|
+
...section,
|
|
615
|
+
id: sourceSection.id,
|
|
616
|
+
...(sourceSection.label === undefined ? {} : { title: sourceSection.label }),
|
|
617
|
+
nativeMetadata: { ...section.nativeMetadata, sourceId: sourceSection.id },
|
|
618
|
+
fields: [reboundField],
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const sourceConstraints = source.sections
|
|
622
|
+
.filter((section) => section.kind === "input")
|
|
623
|
+
.flatMap((section) => {
|
|
624
|
+
const constraints = { fieldId: section.id };
|
|
625
|
+
for (const key of ["required", "minLength", "maxLength", "pattern", "minItems", "maxItems"]) {
|
|
626
|
+
if (section[key] !== undefined)
|
|
627
|
+
constraints[key] = section[key];
|
|
628
|
+
}
|
|
629
|
+
return Object.keys(constraints).length === 1 ? [] : [constraints];
|
|
630
|
+
});
|
|
631
|
+
const sourceFieldIds = new Set(source.sections.filter((section) => section.kind === "input").map((section) => section.id));
|
|
632
|
+
const existing = contractInput.supplementalConstraints.fields.filter((entry) => !sourceFieldIds.has(entry.fieldId));
|
|
633
|
+
const supplemental = [...existing, ...sourceConstraints];
|
|
634
|
+
const rebound = {
|
|
635
|
+
...contractInput,
|
|
636
|
+
templateIdentity: { ...contractInput.templateIdentity, id: source.id, name: source.name },
|
|
637
|
+
nativeMetadata: {
|
|
638
|
+
...contractInput.nativeMetadata,
|
|
639
|
+
...(source.description === undefined ? {} : { description: source.description }),
|
|
640
|
+
...(source.title === undefined ? {} : { title: source.title }),
|
|
641
|
+
...(source.labels === undefined ? {} : { labels: source.labels }),
|
|
642
|
+
},
|
|
643
|
+
sections,
|
|
644
|
+
supplementalConstraints: { fields: supplemental },
|
|
645
|
+
};
|
|
646
|
+
try {
|
|
647
|
+
assertCanonicalContract(rebound);
|
|
648
|
+
}
|
|
649
|
+
catch (cause) {
|
|
650
|
+
throw new SemanticTemplateError([
|
|
651
|
+
{
|
|
652
|
+
code: "SEMANTIC_TEMPLATE_INVALID_VALUE",
|
|
653
|
+
path: "$",
|
|
654
|
+
message: cause instanceof Error ? cause.message : "Semantic source did not compile to a valid canonical contract.",
|
|
655
|
+
},
|
|
656
|
+
], { cause });
|
|
657
|
+
}
|
|
658
|
+
return rebound;
|
|
659
|
+
}
|
|
660
|
+
function rebindField(field, source) {
|
|
661
|
+
const nativeMetadata = { ...field.nativeMetadata, sourceId: source.id };
|
|
662
|
+
if (field.type === "checklist") {
|
|
663
|
+
const options = source.options ?? [];
|
|
664
|
+
const items = field.items.map((item, index) => {
|
|
665
|
+
const option = options[index];
|
|
666
|
+
return option === undefined
|
|
667
|
+
? item
|
|
668
|
+
: {
|
|
669
|
+
...item,
|
|
670
|
+
id: option.id ?? option.value ?? checklistIdentifier(option.label, index),
|
|
671
|
+
label: option.label,
|
|
672
|
+
required: option.required ?? item.required,
|
|
673
|
+
};
|
|
674
|
+
});
|
|
675
|
+
return {
|
|
676
|
+
...field,
|
|
677
|
+
id: source.id,
|
|
678
|
+
...(source.label === undefined ? {} : { label: source.label }),
|
|
679
|
+
...(source.description === undefined ? {} : { description: source.description }),
|
|
680
|
+
nativeMetadata: {
|
|
681
|
+
...nativeMetadata,
|
|
682
|
+
options: items.map((item) => ({ value: item.id, label: item.label, required: item.required })),
|
|
683
|
+
},
|
|
684
|
+
items,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
if (field.type === "enum") {
|
|
688
|
+
const options = field.options.map((option, index) => {
|
|
689
|
+
const sourceOption = source.options?.[index];
|
|
690
|
+
return sourceOption === undefined
|
|
691
|
+
? option
|
|
692
|
+
: {
|
|
693
|
+
...option,
|
|
694
|
+
value: sourceOption.value ?? sourceOption.id ?? sourceOption.label,
|
|
695
|
+
label: sourceOption.label,
|
|
696
|
+
...(sourceOption.description === undefined ? {} : { description: sourceOption.description }),
|
|
697
|
+
};
|
|
698
|
+
});
|
|
699
|
+
return {
|
|
700
|
+
...field,
|
|
701
|
+
id: source.id,
|
|
702
|
+
...(source.label === undefined ? {} : { label: source.label }),
|
|
703
|
+
...(source.description === undefined ? {} : { description: source.description }),
|
|
704
|
+
nativeMetadata: {
|
|
705
|
+
...nativeMetadata,
|
|
706
|
+
options: options.map((option) => ({
|
|
707
|
+
value: option.value,
|
|
708
|
+
label: option.label,
|
|
709
|
+
...(option.description === undefined ? {} : { description: option.description }),
|
|
710
|
+
})),
|
|
711
|
+
},
|
|
712
|
+
options,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
if (field.type === "array") {
|
|
716
|
+
const options = field.items.options?.map((option, index) => {
|
|
717
|
+
const sourceOption = source.options?.[index];
|
|
718
|
+
return sourceOption === undefined
|
|
719
|
+
? option
|
|
720
|
+
: {
|
|
721
|
+
...option,
|
|
722
|
+
value: sourceOption.value ?? sourceOption.id ?? sourceOption.label,
|
|
723
|
+
label: sourceOption.label,
|
|
724
|
+
...(sourceOption.description === undefined ? {} : { description: sourceOption.description }),
|
|
725
|
+
};
|
|
726
|
+
});
|
|
727
|
+
return {
|
|
728
|
+
...field,
|
|
729
|
+
id: source.id,
|
|
730
|
+
...(source.label === undefined ? {} : { label: source.label }),
|
|
731
|
+
...(source.description === undefined ? {} : { description: source.description }),
|
|
732
|
+
nativeMetadata,
|
|
733
|
+
items: { ...field.items, ...(options === undefined ? {} : { options }) },
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
...field,
|
|
738
|
+
id: source.id,
|
|
739
|
+
...(source.label === undefined ? {} : { label: source.label }),
|
|
740
|
+
...(source.description === undefined ? {} : { description: source.description }),
|
|
741
|
+
nativeMetadata,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function semanticSourceFromField(field, section) {
|
|
745
|
+
return {
|
|
746
|
+
id: field.id,
|
|
747
|
+
kind: "input",
|
|
748
|
+
type: field.type,
|
|
749
|
+
label: field.label,
|
|
750
|
+
...(field.description === undefined ? {} : { description: field.description }),
|
|
751
|
+
...(field.required === "unknown" ? {} : { required: field.required === "required" }),
|
|
752
|
+
...(field.nativeMetadata.placeholder === undefined ? {} : { placeholder: field.nativeMetadata.placeholder }),
|
|
753
|
+
...(field.nativeMetadata.render === undefined ? {} : { render: field.nativeMetadata.render }),
|
|
754
|
+
...(field.type === "checklist"
|
|
755
|
+
? { options: field.items.map((item) => ({ id: item.id, label: item.label, required: item.required })) }
|
|
756
|
+
: {}),
|
|
757
|
+
...(field.type === "enum"
|
|
758
|
+
? {
|
|
759
|
+
options: field.options.map((option) => ({
|
|
760
|
+
id: option.value,
|
|
761
|
+
label: option.label,
|
|
762
|
+
...(option.description === undefined ? {} : { description: option.description }),
|
|
763
|
+
})),
|
|
764
|
+
}
|
|
765
|
+
: {}),
|
|
766
|
+
...(field.type === "array"
|
|
767
|
+
? {
|
|
768
|
+
multiple: field.selection === "multi_select",
|
|
769
|
+
options: field.items.options?.map((option) => ({ id: option.value, label: option.label })),
|
|
770
|
+
}
|
|
771
|
+
: {}),
|
|
772
|
+
...headingMetadata(section),
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function semanticSourceFromContractLegacy(contract) {
|
|
776
|
+
const sections = [];
|
|
777
|
+
for (const section of contract.sections) {
|
|
778
|
+
const field = section.fields[0];
|
|
779
|
+
if (section.kind === "documentation")
|
|
780
|
+
sections.push({
|
|
781
|
+
id: section.id,
|
|
782
|
+
kind: "documentation",
|
|
783
|
+
content: section.content ?? "",
|
|
784
|
+
...headingMetadata(section),
|
|
785
|
+
});
|
|
786
|
+
else if (field !== undefined)
|
|
787
|
+
sections.push(semanticSourceFromField(field, section));
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
version: SEMANTIC_TEMPLATE_VERSION,
|
|
791
|
+
kind: contract.artifactKind,
|
|
792
|
+
id: contract.templateIdentity.id,
|
|
793
|
+
name: contract.templateIdentity.name,
|
|
794
|
+
...(contract.nativeMetadata.description === undefined ? {} : { description: contract.nativeMetadata.description }),
|
|
795
|
+
...(contract.nativeMetadata.title === undefined ? {} : { title: contract.nativeMetadata.title }),
|
|
796
|
+
...(contract.nativeMetadata.labels === undefined ? {} : { labels: contract.nativeMetadata.labels }),
|
|
797
|
+
sections,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function issueElementType(section) {
|
|
801
|
+
if (section.element !== undefined)
|
|
802
|
+
return section.element;
|
|
803
|
+
if (section.type === "enum" || section.type === "array")
|
|
804
|
+
return "dropdown";
|
|
805
|
+
if (section.type === "checklist")
|
|
806
|
+
return "checkboxes";
|
|
807
|
+
return "textarea";
|
|
808
|
+
}
|
|
809
|
+
function defaultImportPath(root, source, base) {
|
|
810
|
+
return source.kind === "issue"
|
|
811
|
+
? toRepositoryPath(root, path.join(root, SEMANTIC_ISSUE_DIRECTORY, `${base}.json`))
|
|
812
|
+
: toRepositoryPath(root, path.join(root, SEMANTIC_TEMPLATE_DIRECTORY, "pull-request.json"));
|
|
813
|
+
}
|
|
814
|
+
function headingMetadata(section) {
|
|
815
|
+
return {
|
|
816
|
+
...(section.title === undefined ? {} : { label: section.title }),
|
|
817
|
+
...(section.render.headingLevel === undefined ? {} : { headingLevel: section.render.headingLevel }),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function findStaleGeneratedFiles(root, generated) {
|
|
821
|
+
return findGeneratedFiles(root).then((files) => files.filter((file) => !generated.has(file)));
|
|
822
|
+
}
|
|
823
|
+
async function findGeneratedFiles(root) {
|
|
824
|
+
const files = [];
|
|
825
|
+
for (const directory of [ISSUE_NATIVE_DIRECTORY, PR_NATIVE_DIRECTORY]) {
|
|
826
|
+
const absolute = path.join(root, directory);
|
|
827
|
+
if (!(await optionalDirectory(absolute)))
|
|
828
|
+
continue;
|
|
829
|
+
const entries = await readdir(absolute, { withFileTypes: true });
|
|
830
|
+
for (const entry of entries) {
|
|
831
|
+
if (!entry.isFile())
|
|
832
|
+
continue;
|
|
833
|
+
const relative = toRepositoryPath(root, path.join(absolute, entry.name));
|
|
834
|
+
const content = await readFile(path.join(absolute, entry.name), "utf8");
|
|
835
|
+
if (content.includes(GENERATED_TEMPLATE_NOTICE))
|
|
836
|
+
files.push(relative);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const defaultPr = path.join(root, PR_NATIVE_DEFAULT);
|
|
840
|
+
if (await optionalFile(defaultPr)) {
|
|
841
|
+
const relative = toRepositoryPath(root, defaultPr);
|
|
842
|
+
const content = await readFile(defaultPr, "utf8");
|
|
843
|
+
if (content.includes(GENERATED_TEMPLATE_NOTICE))
|
|
844
|
+
files.push(relative);
|
|
845
|
+
}
|
|
846
|
+
return [...new Set(files)].sort();
|
|
847
|
+
}
|
|
848
|
+
function semanticSelectorMatches(identity, selector) {
|
|
849
|
+
if (selector === undefined)
|
|
850
|
+
return true;
|
|
851
|
+
const defaultPullRequestName = identity.kind === "pull_request" && identity.generatedPath === PR_NATIVE_DEFAULT ? "default" : undefined;
|
|
852
|
+
if (typeof selector === "string")
|
|
853
|
+
return (selector === identity.id ||
|
|
854
|
+
selector === identity.sourcePath ||
|
|
855
|
+
selector === identity.generatedPath ||
|
|
856
|
+
selector.toLocaleLowerCase("en-US") === identity.name.toLocaleLowerCase("en-US") ||
|
|
857
|
+
selector.toLocaleLowerCase("en-US") === defaultPullRequestName);
|
|
858
|
+
return ((selector.id === undefined || selector.id === identity.id) &&
|
|
859
|
+
(selector.path === undefined ||
|
|
860
|
+
selector.path === identity.sourcePath ||
|
|
861
|
+
selector.path === identity.generatedPath) &&
|
|
862
|
+
(selector.name === undefined ||
|
|
863
|
+
selector.name.toLocaleLowerCase("en-US") === identity.name.toLocaleLowerCase("en-US") ||
|
|
864
|
+
selector.name.toLocaleLowerCase("en-US") === defaultPullRequestName));
|
|
865
|
+
}
|
|
866
|
+
function normalizeSection(value, pathPrefix, violations) {
|
|
867
|
+
if (!isRecord(value)) {
|
|
868
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", pathPrefix, "Sections must be objects.");
|
|
869
|
+
return undefined;
|
|
870
|
+
}
|
|
871
|
+
assertKeys(value, [
|
|
872
|
+
"id",
|
|
873
|
+
"kind",
|
|
874
|
+
"type",
|
|
875
|
+
"label",
|
|
876
|
+
"title",
|
|
877
|
+
"description",
|
|
878
|
+
"content",
|
|
879
|
+
"required",
|
|
880
|
+
"element",
|
|
881
|
+
"placeholder",
|
|
882
|
+
"default",
|
|
883
|
+
"defaultValue",
|
|
884
|
+
"render",
|
|
885
|
+
"language",
|
|
886
|
+
"multiple",
|
|
887
|
+
"options",
|
|
888
|
+
"items",
|
|
889
|
+
"minLength",
|
|
890
|
+
"maxLength",
|
|
891
|
+
"pattern",
|
|
892
|
+
"minItems",
|
|
893
|
+
"maxItems",
|
|
894
|
+
"headingLevel",
|
|
895
|
+
], pathPrefix, violations);
|
|
896
|
+
const id = stringValue(value.id, `${pathPrefix}.id`, violations);
|
|
897
|
+
const rawType = typeof value.type === "string" ? value.type : undefined;
|
|
898
|
+
const kind = value.kind === "documentation" || rawType === "markdown" || rawType === "documentation" ? "documentation" : "input";
|
|
899
|
+
const type = normalizeInputType(rawType, `${pathPrefix}.type`, violations, kind);
|
|
900
|
+
const label = optionalString(value.label ?? value.title, `${pathPrefix}.label`, violations);
|
|
901
|
+
const description = optionalString(value.description, `${pathPrefix}.description`, violations);
|
|
902
|
+
const content = optionalString(value.content, `${pathPrefix}.content`, violations);
|
|
903
|
+
if (kind === "documentation" && content === undefined)
|
|
904
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", `${pathPrefix}.content`, "Documentation sections require content.");
|
|
905
|
+
if (kind === "input" && label === undefined)
|
|
906
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", `${pathPrefix}.label`, "Input sections require a label.");
|
|
907
|
+
const required = optionalBoolean(value.required, `${pathPrefix}.required`, violations);
|
|
908
|
+
const element = optionalElement(value.element, `${pathPrefix}.element`, violations);
|
|
909
|
+
const placeholder = optionalString(value.placeholder, `${pathPrefix}.placeholder`, violations);
|
|
910
|
+
const defaultValue = value.defaultValue ?? value.default;
|
|
911
|
+
const normalizedDefault = normalizeDefault(defaultValue, `${pathPrefix}.defaultValue`, violations);
|
|
912
|
+
const render = optionalString(value.render ?? value.language, `${pathPrefix}.render`, violations);
|
|
913
|
+
const multiple = optionalBoolean(value.multiple, `${pathPrefix}.multiple`, violations);
|
|
914
|
+
const rawOptions = value.options ?? value.items;
|
|
915
|
+
const options = normalizeOptions(rawOptions, `${pathPrefix}.options`, violations);
|
|
916
|
+
const minLength = optionalInteger(value.minLength, `${pathPrefix}.minLength`, violations);
|
|
917
|
+
const maxLength = optionalInteger(value.maxLength, `${pathPrefix}.maxLength`, violations);
|
|
918
|
+
const pattern = optionalString(value.pattern, `${pathPrefix}.pattern`, violations);
|
|
919
|
+
const minItems = optionalInteger(value.minItems, `${pathPrefix}.minItems`, violations);
|
|
920
|
+
const maxItems = optionalInteger(value.maxItems, `${pathPrefix}.maxItems`, violations);
|
|
921
|
+
const headingLevel = optionalInteger(value.headingLevel, `${pathPrefix}.headingLevel`, violations);
|
|
922
|
+
if (headingLevel !== undefined && (headingLevel < 1 || headingLevel > 6))
|
|
923
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", `${pathPrefix}.headingLevel`, "headingLevel must be between 1 and 6.");
|
|
924
|
+
if (minLength !== undefined && maxLength !== undefined && minLength > maxLength)
|
|
925
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", pathPrefix, "minLength cannot exceed maxLength.");
|
|
926
|
+
if (minItems !== undefined && maxItems !== undefined && minItems > maxItems)
|
|
927
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", pathPrefix, "minItems cannot exceed maxItems.");
|
|
928
|
+
if (id === undefined)
|
|
929
|
+
return undefined;
|
|
930
|
+
return {
|
|
931
|
+
id,
|
|
932
|
+
kind,
|
|
933
|
+
...(type === undefined ? {} : { type }),
|
|
934
|
+
...(label === undefined ? {} : { label }),
|
|
935
|
+
...(description === undefined ? {} : { description }),
|
|
936
|
+
...(content === undefined ? {} : { content }),
|
|
937
|
+
...(required === undefined ? {} : { required }),
|
|
938
|
+
...(element === undefined ? {} : { element }),
|
|
939
|
+
...(placeholder === undefined ? {} : { placeholder }),
|
|
940
|
+
...(normalizedDefault === undefined ? {} : { defaultValue: normalizedDefault }),
|
|
941
|
+
...(render === undefined ? {} : { render }),
|
|
942
|
+
...(multiple === undefined ? {} : { multiple }),
|
|
943
|
+
...(options === undefined ? {} : { options }),
|
|
944
|
+
...(minLength === undefined ? {} : { minLength }),
|
|
945
|
+
...(maxLength === undefined ? {} : { maxLength }),
|
|
946
|
+
...(pattern === undefined ? {} : { pattern }),
|
|
947
|
+
...(minItems === undefined ? {} : { minItems }),
|
|
948
|
+
...(maxItems === undefined ? {} : { maxItems }),
|
|
949
|
+
...(headingLevel === undefined ? {} : { headingLevel }),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function normalizeInputType(value, pathPrefix, violations, kind) {
|
|
953
|
+
if (kind === "documentation")
|
|
954
|
+
return undefined;
|
|
955
|
+
if (value === undefined || value === "string" || value === "input" || value === "textarea")
|
|
956
|
+
return "string";
|
|
957
|
+
if (value === "enum" || value === "select" || value === "dropdown")
|
|
958
|
+
return "enum";
|
|
959
|
+
if (value === "array" || value === "multi_select" || value === "multi-select")
|
|
960
|
+
return "array";
|
|
961
|
+
if (value === "checklist" || value === "checkboxes")
|
|
962
|
+
return "checklist";
|
|
963
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_UNSUPPORTED_FIELD", pathPrefix, `Unsupported semantic field type "${value}".`);
|
|
964
|
+
return undefined;
|
|
965
|
+
}
|
|
966
|
+
function normalizeOptions(value, pathPrefix, violations) {
|
|
967
|
+
if (value === undefined)
|
|
968
|
+
return undefined;
|
|
969
|
+
if (!Array.isArray(value)) {
|
|
970
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", pathPrefix, "options must be an array.");
|
|
971
|
+
return undefined;
|
|
972
|
+
}
|
|
973
|
+
return value.map((entry, index) => {
|
|
974
|
+
const optionPath = `${pathPrefix}[${index}]`;
|
|
975
|
+
if (typeof entry === "string")
|
|
976
|
+
return { label: entry, value: entry, id: entry };
|
|
977
|
+
if (!isRecord(entry)) {
|
|
978
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", optionPath, "Options must be strings or objects.");
|
|
979
|
+
return { label: `option-${index + 1}` };
|
|
980
|
+
}
|
|
981
|
+
assertKeys(entry, ["id", "value", "label", "description", "required"], optionPath, violations);
|
|
982
|
+
const label = stringValue(entry.label ?? entry.value ?? entry.id, `${optionPath}.label`, violations) ?? `option-${index + 1}`;
|
|
983
|
+
const id = optionalString(entry.id, `${optionPath}.id`, violations);
|
|
984
|
+
const valueText = optionalString(entry.value, `${optionPath}.value`, violations);
|
|
985
|
+
const description = optionalString(entry.description, `${optionPath}.description`, violations);
|
|
986
|
+
const required = optionalBoolean(entry.required, `${optionPath}.required`, violations);
|
|
987
|
+
return {
|
|
988
|
+
label,
|
|
989
|
+
...(id === undefined ? {} : { id }),
|
|
990
|
+
...(valueText === undefined ? {} : { value: valueText }),
|
|
991
|
+
...(description === undefined ? {} : { description }),
|
|
992
|
+
...(required === undefined ? {} : { required }),
|
|
993
|
+
};
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
function normalizeDefault(value, pathPrefix, violations) {
|
|
997
|
+
if (value === undefined)
|
|
998
|
+
return undefined;
|
|
999
|
+
if (typeof value === "string")
|
|
1000
|
+
return value;
|
|
1001
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
|
|
1002
|
+
return value;
|
|
1003
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", pathPrefix, "defaultValue must be a string or string array.");
|
|
1004
|
+
return undefined;
|
|
1005
|
+
}
|
|
1006
|
+
function semanticTypeOf(field) {
|
|
1007
|
+
return field.type ?? "string";
|
|
1008
|
+
}
|
|
1009
|
+
function rebindSemanticFieldIds(source, contract) {
|
|
1010
|
+
return applySemanticIdentityAndConstraints(contract, source);
|
|
1011
|
+
}
|
|
1012
|
+
function stripGeneratedNotice(content) {
|
|
1013
|
+
return content.replace(`# ${GENERATED_TEMPLATE_NOTICE}\n`, "").replace(`<!-- ${GENERATED_TEMPLATE_NOTICE} -->\n`, "");
|
|
1014
|
+
}
|
|
1015
|
+
function checklistIdentifier(label, index) {
|
|
1016
|
+
const normalized = label
|
|
1017
|
+
.normalize("NFKD")
|
|
1018
|
+
.replace(/[\\u0300-\\u036f]/gu, "")
|
|
1019
|
+
.replace(/[^A-Za-z0-9_-]+/gu, "-")
|
|
1020
|
+
.replace(/^-+|-+$/gu, "");
|
|
1021
|
+
if (normalized.length === 0)
|
|
1022
|
+
return `item-${index + 1}`;
|
|
1023
|
+
return /^[A-Za-z]/u.test(normalized) ? normalized : `item-${normalized}`;
|
|
1024
|
+
}
|
|
1025
|
+
function assertKeys(record, allowed, pathPrefix, violations) {
|
|
1026
|
+
const allowedSet = new Set(allowed);
|
|
1027
|
+
for (const key of Object.keys(record).sort())
|
|
1028
|
+
if (!allowedSet.has(key))
|
|
1029
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_UNKNOWN_PROPERTY", `${pathPrefix}.${key}`, `Unknown semantic template property "${key}".`);
|
|
1030
|
+
}
|
|
1031
|
+
function stringValue(value, pathPrefix, violations) {
|
|
1032
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
1033
|
+
return value;
|
|
1034
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", pathPrefix, "Value must be a non-empty string.");
|
|
1035
|
+
return undefined;
|
|
1036
|
+
}
|
|
1037
|
+
function optionalString(value, pathPrefix, violations) {
|
|
1038
|
+
if (value === undefined)
|
|
1039
|
+
return undefined;
|
|
1040
|
+
if (typeof value === "string")
|
|
1041
|
+
return value;
|
|
1042
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", pathPrefix, "Value must be a string.");
|
|
1043
|
+
return undefined;
|
|
1044
|
+
}
|
|
1045
|
+
function optionalStringList(value, pathPrefix, violations) {
|
|
1046
|
+
if (value === undefined)
|
|
1047
|
+
return undefined;
|
|
1048
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.trim().length > 0))
|
|
1049
|
+
return value;
|
|
1050
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", pathPrefix, "Value must be an array of non-empty strings.");
|
|
1051
|
+
return undefined;
|
|
1052
|
+
}
|
|
1053
|
+
function optionalBoolean(value, pathPrefix, violations) {
|
|
1054
|
+
if (value === undefined)
|
|
1055
|
+
return undefined;
|
|
1056
|
+
if (typeof value === "boolean")
|
|
1057
|
+
return value;
|
|
1058
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", pathPrefix, "Value must be boolean.");
|
|
1059
|
+
return undefined;
|
|
1060
|
+
}
|
|
1061
|
+
function optionalElement(value, pathPrefix, violations) {
|
|
1062
|
+
if (value === undefined)
|
|
1063
|
+
return undefined;
|
|
1064
|
+
if (value === "input" || value === "textarea" || value === "dropdown" || value === "checkboxes")
|
|
1065
|
+
return value;
|
|
1066
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", pathPrefix, "element is not a supported native Issue Form element.");
|
|
1067
|
+
return undefined;
|
|
1068
|
+
}
|
|
1069
|
+
function optionalInteger(value, pathPrefix, violations) {
|
|
1070
|
+
if (value === undefined)
|
|
1071
|
+
return undefined;
|
|
1072
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
|
|
1073
|
+
return value;
|
|
1074
|
+
addViolation(violations, "SEMANTIC_TEMPLATE_INVALID_VALUE", pathPrefix, "Value must be a non-negative safe integer.");
|
|
1075
|
+
return undefined;
|
|
1076
|
+
}
|
|
1077
|
+
function addViolation(violations, code, pathPrefix, message) {
|
|
1078
|
+
violations.push({ code, path: pathPrefix, message });
|
|
1079
|
+
}
|
|
1080
|
+
function isRecord(value) {
|
|
1081
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1082
|
+
}
|
|
1083
|
+
function resolveRoot(repositoryRoot) {
|
|
1084
|
+
return path.resolve(typeof repositoryRoot === "string" ? repositoryRoot : fileURLToPath(repositoryRoot));
|
|
1085
|
+
}
|
|
1086
|
+
function toRepositoryPath(root, absolutePath) {
|
|
1087
|
+
return path.relative(root, absolutePath).split(path.sep).join("/");
|
|
1088
|
+
}
|
|
1089
|
+
function safeRepositoryPath(root, relativePath) {
|
|
1090
|
+
const absolute = path.resolve(root, relativePath);
|
|
1091
|
+
const relative = path.relative(root, absolute);
|
|
1092
|
+
if (relative.length === 0 || relative.startsWith("..") || path.isAbsolute(relative))
|
|
1093
|
+
throw new SemanticTemplateError([
|
|
1094
|
+
{
|
|
1095
|
+
code: "SEMANTIC_TEMPLATE_INVALID_VALUE",
|
|
1096
|
+
path: relativePath,
|
|
1097
|
+
message: "Repository path must remain inside the repository root.",
|
|
1098
|
+
},
|
|
1099
|
+
]);
|
|
1100
|
+
return absolute;
|
|
1101
|
+
}
|
|
1102
|
+
async function writeRepositoryFile(absolutePath, content) {
|
|
1103
|
+
try {
|
|
1104
|
+
if ((await lstat(absolutePath)).isSymbolicLink())
|
|
1105
|
+
throw new SemanticTemplateError([
|
|
1106
|
+
{
|
|
1107
|
+
code: "SEMANTIC_TEMPLATE_INVALID_VALUE",
|
|
1108
|
+
path: absolutePath,
|
|
1109
|
+
message: "Refusing to write through a symbolic link.",
|
|
1110
|
+
},
|
|
1111
|
+
]);
|
|
1112
|
+
}
|
|
1113
|
+
catch (cause) {
|
|
1114
|
+
if (cause instanceof SemanticTemplateError)
|
|
1115
|
+
throw cause;
|
|
1116
|
+
}
|
|
1117
|
+
await writeFile(absolutePath, content, "utf8");
|
|
1118
|
+
}
|
|
1119
|
+
async function optionalDirectory(directory) {
|
|
1120
|
+
try {
|
|
1121
|
+
return (await stat(directory)).isDirectory();
|
|
1122
|
+
}
|
|
1123
|
+
catch {
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
async function optionalFile(file) {
|
|
1128
|
+
try {
|
|
1129
|
+
return (await stat(file)).isFile();
|
|
1130
|
+
}
|
|
1131
|
+
catch {
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function optionalDirectorySync(directory) {
|
|
1136
|
+
try {
|
|
1137
|
+
return statSync(directory).isDirectory();
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
function optionalFileSync(file) {
|
|
1144
|
+
try {
|
|
1145
|
+
return statSync(file).isFile();
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
//# sourceMappingURL=semantic-template.js.map
|