@spectrabrain/eds 0.3.0 → 0.3.1
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/AI.md +3 -3
- package/AI_CONTEXT.json +7 -8
- package/CHANGELOG.md +34 -16
- package/CONSUMER_AGENT.md +65 -55
- package/FRONTEND_CONVENTIONS.md +27 -32
- package/QUICKSTART.md +19 -28
- package/README.md +11 -9
- package/TAILWIND_V4.md +4 -2
- package/TOKEN_CONTEXT.md +7 -7
- package/consumer/align/templates/package.scripts.json +0 -1
- package/consumer/align/templates/src/app/main.tsx +9 -19
- package/consumer/align/templates/vite.config.rolldown.ts +1 -1
- package/consumer/align-analysis.mjs +1712 -299
- package/consumer/align-plan.mjs +647 -139
- package/consumer/align-rules.mjs +59 -23
- package/consumer/align.mjs +639 -103
- package/consumer/cli.mjs +7 -7
- package/dist/components/Button/README.md +1 -1
- package/dist/components/Button/spec.json +2 -2
- package/dist/components.css +1 -1
- package/llms.txt +6 -8
- package/package.json +1 -1
- package/consumer/align/templates/env.mock +0 -1
- package/consumer/align/templates/src/mocks/browser.ts +0 -5
- package/consumer/align/templates/src/mocks/handlers.ts +0 -4
package/consumer/align.mjs
CHANGED
|
@@ -5,20 +5,52 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import {
|
|
6
6
|
analyzeFrontend,
|
|
7
7
|
checkBackendPackage,
|
|
8
|
+
checkPreservedTsconfigSettings,
|
|
8
9
|
classifyChangedPaths,
|
|
9
10
|
declaredDependencies,
|
|
10
11
|
detectFramework,
|
|
12
|
+
filesUnderPaths,
|
|
11
13
|
findFrontendPackages,
|
|
12
14
|
hashFile,
|
|
15
|
+
isLegacyPreservedTsconfigSettings,
|
|
16
|
+
isPreservedSourcePath,
|
|
13
17
|
parseJsonc,
|
|
14
18
|
summarizeStructure,
|
|
15
19
|
walkFiles,
|
|
16
20
|
} from "./align-analysis.mjs";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
import {
|
|
22
|
+
countFindings,
|
|
23
|
+
packageManagerCommands,
|
|
24
|
+
renderNotes,
|
|
25
|
+
renderPlan,
|
|
26
|
+
} from "./align-plan.mjs";
|
|
27
|
+
import {
|
|
28
|
+
alignDirectory,
|
|
29
|
+
conventionsFileName,
|
|
30
|
+
maximumSupportedViteMajor,
|
|
31
|
+
minimumVersions,
|
|
32
|
+
planFileName,
|
|
33
|
+
rootLockfiles,
|
|
34
|
+
stateFileName,
|
|
35
|
+
stateSchemaVersion,
|
|
36
|
+
templatesDirectoryName,
|
|
37
|
+
} from "./align-rules.mjs";
|
|
38
|
+
import {
|
|
39
|
+
cliCommand,
|
|
40
|
+
createExitError,
|
|
41
|
+
detectPackageManager,
|
|
42
|
+
generatedCssRelativePath,
|
|
43
|
+
installCommand,
|
|
44
|
+
packageName,
|
|
45
|
+
readPackageJson,
|
|
46
|
+
resolvePackage,
|
|
47
|
+
toPosix,
|
|
48
|
+
} from "./paths.mjs";
|
|
20
49
|
|
|
21
|
-
const packageRoot = path.resolve(
|
|
50
|
+
const packageRoot = path.resolve(
|
|
51
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
52
|
+
".."
|
|
53
|
+
);
|
|
22
54
|
const templatesRoot = path.join(packageRoot, "consumer", "align", "templates");
|
|
23
55
|
const notesFileName = "NOTES.md";
|
|
24
56
|
const findingLimitPerKind = 40;
|
|
@@ -26,7 +58,12 @@ const placeholderPattern = /\{\{eds\.(\w+)\}\}/g;
|
|
|
26
58
|
|
|
27
59
|
function git(cwd, args) {
|
|
28
60
|
try {
|
|
29
|
-
return execFileSync("git", args, {
|
|
61
|
+
return execFileSync("git", args, {
|
|
62
|
+
cwd,
|
|
63
|
+
encoding: "utf8",
|
|
64
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
65
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
66
|
+
});
|
|
30
67
|
} catch {
|
|
31
68
|
return null;
|
|
32
69
|
}
|
|
@@ -44,10 +81,24 @@ function readState(statePath, shown = (relativePath) => relativePath) {
|
|
|
44
81
|
try {
|
|
45
82
|
state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
46
83
|
} catch (error) {
|
|
47
|
-
throw createExitError(
|
|
84
|
+
throw createExitError(
|
|
85
|
+
toPosix(statePath) +
|
|
86
|
+
" could not be read (" +
|
|
87
|
+
error.message +
|
|
88
|
+
"); delete " +
|
|
89
|
+
shown(alignDirectory) +
|
|
90
|
+
" and run eds align again.",
|
|
91
|
+
2
|
|
92
|
+
);
|
|
48
93
|
}
|
|
49
94
|
if (state?.schemaVersion !== stateSchemaVersion) {
|
|
50
|
-
throw createExitError(
|
|
95
|
+
throw createExitError(
|
|
96
|
+
toPosix(statePath) +
|
|
97
|
+
" was written by another eds align version; delete " +
|
|
98
|
+
shown(alignDirectory) +
|
|
99
|
+
" and run eds align again.",
|
|
100
|
+
2
|
|
101
|
+
);
|
|
51
102
|
}
|
|
52
103
|
return state;
|
|
53
104
|
}
|
|
@@ -58,7 +109,10 @@ function resolveFrontendDirectory(consumerRoot, frontendOption) {
|
|
|
58
109
|
if (frontendOption) {
|
|
59
110
|
const directory = path.resolve(consumerRoot, frontendOption);
|
|
60
111
|
if (!fs.existsSync(path.join(directory, "package.json"))) {
|
|
61
|
-
throw createExitError(
|
|
112
|
+
throw createExitError(
|
|
113
|
+
"--frontend " + frontendOption + " has no package.json.",
|
|
114
|
+
2
|
|
115
|
+
);
|
|
62
116
|
}
|
|
63
117
|
return directory;
|
|
64
118
|
}
|
|
@@ -69,15 +123,19 @@ function resolveFrontendDirectory(consumerRoot, frontendOption) {
|
|
|
69
123
|
if (own !== null && declaredDependencies(own).has("react")) {
|
|
70
124
|
return consumerRoot;
|
|
71
125
|
}
|
|
72
|
-
const candidates = findFrontendPackages(consumerRoot).filter(
|
|
126
|
+
const candidates = findFrontendPackages(consumerRoot).filter(
|
|
127
|
+
(candidate) => candidate !== "."
|
|
128
|
+
);
|
|
73
129
|
if (candidates.length === 1) {
|
|
74
130
|
return path.join(consumerRoot, candidates[0]);
|
|
75
131
|
}
|
|
76
132
|
throw createExitError(
|
|
77
133
|
candidates.length === 0
|
|
78
134
|
? "No package here or below declares react; run eds align inside the frontend package."
|
|
79
|
-
: "Several React packages (" +
|
|
80
|
-
|
|
135
|
+
: "Several React packages (" +
|
|
136
|
+
candidates.join(", ") +
|
|
137
|
+
"); rerun with --frontend <dir>.",
|
|
138
|
+
2
|
|
81
139
|
);
|
|
82
140
|
}
|
|
83
141
|
|
|
@@ -89,6 +147,78 @@ function changedSince(gitRoot, commit) {
|
|
|
89
147
|
];
|
|
90
148
|
}
|
|
91
149
|
|
|
150
|
+
function normalizeRepositoryPaths(paths, alignRelative) {
|
|
151
|
+
if (!Array.isArray(paths)) {
|
|
152
|
+
throw createExitError(
|
|
153
|
+
"state.json `preservedPaths` must be an array of paths relative to the git root.",
|
|
154
|
+
2
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const normalized = [];
|
|
158
|
+
for (const entry of paths) {
|
|
159
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
160
|
+
throw createExitError(
|
|
161
|
+
"state.json `preservedPaths` must contain non-empty paths relative to the git root.",
|
|
162
|
+
2
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const pathText = entry.trim().replaceAll("\\", "/");
|
|
166
|
+
const relativePath = path.posix.normalize(pathText);
|
|
167
|
+
if (
|
|
168
|
+
path.posix.isAbsolute(pathText) ||
|
|
169
|
+
/^[A-Za-z]:\//.test(pathText) ||
|
|
170
|
+
relativePath === "." ||
|
|
171
|
+
relativePath === ".." ||
|
|
172
|
+
relativePath.startsWith("../")
|
|
173
|
+
) {
|
|
174
|
+
throw createExitError(
|
|
175
|
+
"state.json `preservedPaths` must use paths inside the git repository, relative to its root: " +
|
|
176
|
+
JSON.stringify(entry),
|
|
177
|
+
2
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (
|
|
181
|
+
relativePath === alignRelative ||
|
|
182
|
+
relativePath.startsWith(alignRelative + "/")
|
|
183
|
+
) {
|
|
184
|
+
throw createExitError(
|
|
185
|
+
"state.json `preservedPaths` cannot include " + alignRelative + ".",
|
|
186
|
+
2
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
normalized.push(relativePath);
|
|
190
|
+
}
|
|
191
|
+
return [...new Set(normalized)].sort();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function packagePathsFromRepository(paths, packageRelative) {
|
|
195
|
+
const prefix = packageRelative === "" ? "" : packageRelative + "/";
|
|
196
|
+
return paths.flatMap((relativePath) => {
|
|
197
|
+
if (packageRelative === "") return [relativePath];
|
|
198
|
+
if (relativePath === packageRelative) return ["."];
|
|
199
|
+
return relativePath.startsWith(prefix)
|
|
200
|
+
? [relativePath.slice(prefix.length)]
|
|
201
|
+
: [];
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isAutomaticPreservedRepositoryPath(relativePath, packageRelative) {
|
|
206
|
+
const packagePath = packagePathsFromRepository(
|
|
207
|
+
[relativePath],
|
|
208
|
+
packageRelative
|
|
209
|
+
)[0];
|
|
210
|
+
return packagePath !== undefined && isPreservedSourcePath(packagePath);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function hashPaths(root, relativePaths) {
|
|
214
|
+
const hashes = {};
|
|
215
|
+
for (const relativePath of filesUnderPaths(root, relativePaths)) {
|
|
216
|
+
const hash = hashFile(path.join(root, relativePath));
|
|
217
|
+
if (hash !== null) hashes[relativePath] = hash;
|
|
218
|
+
}
|
|
219
|
+
return hashes;
|
|
220
|
+
}
|
|
221
|
+
|
|
92
222
|
function readStrictOption(packageDirectory) {
|
|
93
223
|
for (const name of ["tsconfig.json", "tsconfig.app.json"]) {
|
|
94
224
|
const filePath = path.join(packageDirectory, name);
|
|
@@ -96,7 +226,10 @@ function readStrictOption(packageDirectory) {
|
|
|
96
226
|
continue;
|
|
97
227
|
}
|
|
98
228
|
try {
|
|
99
|
-
if (
|
|
229
|
+
if (
|
|
230
|
+
parseJsonc(fs.readFileSync(filePath, "utf8")).compilerOptions
|
|
231
|
+
?.strict === true
|
|
232
|
+
) {
|
|
100
233
|
return true;
|
|
101
234
|
}
|
|
102
235
|
} catch {
|
|
@@ -107,23 +240,40 @@ function readStrictOption(packageDirectory) {
|
|
|
107
240
|
}
|
|
108
241
|
|
|
109
242
|
function joinPosix(...parts) {
|
|
110
|
-
const joined = path.posix.join(
|
|
243
|
+
const joined = path.posix.join(
|
|
244
|
+
...parts.filter((part) => part !== "" && part !== ".")
|
|
245
|
+
);
|
|
111
246
|
return joined === "" ? "." : joined;
|
|
112
247
|
}
|
|
113
248
|
|
|
114
249
|
// Templates rendered with this repository's paths. Keys are target paths relative to the frontend package.
|
|
115
|
-
export function renderTemplates({
|
|
116
|
-
|
|
117
|
-
|
|
250
|
+
export function renderTemplates({
|
|
251
|
+
app,
|
|
252
|
+
protectedInside,
|
|
253
|
+
packageJson,
|
|
254
|
+
viteMajor,
|
|
255
|
+
strict,
|
|
256
|
+
}) {
|
|
257
|
+
if (viteMajor !== null && viteMajor > maximumSupportedViteMajor) {
|
|
258
|
+
throw createExitError(
|
|
259
|
+
"eds align supports Vite 5.2.0–" + maximumSupportedViteMajor + ".x only.",
|
|
260
|
+
2
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const eslintFileName =
|
|
264
|
+
packageJson.type === "module" ? "eslint.config.js" : "eslint.config.mjs";
|
|
265
|
+
const viteTemplate =
|
|
266
|
+
viteMajor === 8 ? "vite.config.rolldown.ts" : "vite.config.babel.ts";
|
|
118
267
|
const values = {
|
|
119
268
|
srcDir: app.srcDir,
|
|
120
269
|
strict: String(strict),
|
|
121
|
-
eslintIgnores: ["dist", "coverage", ".eds", ...protectedInside]
|
|
270
|
+
eslintIgnores: ["dist", "coverage", ".eds", ...protectedInside]
|
|
271
|
+
.map((entry) => JSON.stringify(entry))
|
|
272
|
+
.join(", "),
|
|
122
273
|
prettierIgnores: [
|
|
123
274
|
"dist",
|
|
124
275
|
"coverage",
|
|
125
276
|
".eds",
|
|
126
|
-
joinPosix(app.appDir, "public/mockServiceWorker.js"),
|
|
127
277
|
toPosix(generatedCssRelativePath),
|
|
128
278
|
"*.md",
|
|
129
279
|
...rootLockfiles,
|
|
@@ -133,15 +283,19 @@ export function renderTemplates({ app, protectedInside, packageJson, viteMajor,
|
|
|
133
283
|
const rendered = new Map();
|
|
134
284
|
for (const templatePath of walkFiles(templatesRoot)) {
|
|
135
285
|
let target;
|
|
136
|
-
if (
|
|
286
|
+
if (
|
|
287
|
+
templatePath === "vite.config.babel.ts" ||
|
|
288
|
+
templatePath === "vite.config.rolldown.ts"
|
|
289
|
+
) {
|
|
137
290
|
if (templatePath !== viteTemplate) continue;
|
|
138
291
|
target = "vite.config.ts";
|
|
139
292
|
} else if (templatePath === "eslint.config.js") {
|
|
140
293
|
target = eslintFileName;
|
|
141
|
-
} else if (
|
|
294
|
+
} else if (
|
|
295
|
+
templatePath === "prettierrc.json" ||
|
|
296
|
+
templatePath === "prettierignore"
|
|
297
|
+
) {
|
|
142
298
|
target = "." + templatePath;
|
|
143
|
-
} else if (templatePath === "env.mock") {
|
|
144
|
-
target = joinPosix(app.appDir, ".env.mock");
|
|
145
299
|
} else if (templatePath.startsWith("src/")) {
|
|
146
300
|
target = joinPosix(app.srcDir, templatePath.slice("src/".length));
|
|
147
301
|
} else if (templatePath.startsWith("public/")) {
|
|
@@ -149,12 +303,16 @@ export function renderTemplates({ app, protectedInside, packageJson, viteMajor,
|
|
|
149
303
|
} else {
|
|
150
304
|
target = templatePath;
|
|
151
305
|
}
|
|
152
|
-
const contents = fs
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
306
|
+
const contents = fs
|
|
307
|
+
.readFileSync(path.join(templatesRoot, templatePath), "utf8")
|
|
308
|
+
.replace(placeholderPattern, (match, name) => {
|
|
309
|
+
if (!Object.prototype.hasOwnProperty.call(values, name)) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
"Unknown template placeholder " + match + " in " + templatePath
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return values[name];
|
|
315
|
+
});
|
|
158
316
|
rendered.set(target, contents);
|
|
159
317
|
}
|
|
160
318
|
return rendered;
|
|
@@ -171,18 +329,39 @@ function logFindings(findings, log) {
|
|
|
171
329
|
continue;
|
|
172
330
|
}
|
|
173
331
|
shown.set(key, count + 1);
|
|
174
|
-
log(
|
|
332
|
+
log(
|
|
333
|
+
"[EDS] " +
|
|
334
|
+
(finding.level === "warning" ? "warning " : "") +
|
|
335
|
+
finding.kind +
|
|
336
|
+
": " +
|
|
337
|
+
finding.message
|
|
338
|
+
);
|
|
175
339
|
}
|
|
176
340
|
for (const [key, count] of hidden) {
|
|
177
|
-
log(
|
|
341
|
+
log(
|
|
342
|
+
"[EDS] … " +
|
|
343
|
+
count +
|
|
344
|
+
" more " +
|
|
345
|
+
key +
|
|
346
|
+
" findings (fix the ones above and run the check again)"
|
|
347
|
+
);
|
|
178
348
|
}
|
|
179
349
|
}
|
|
180
350
|
|
|
181
351
|
// The rules file as an agent reaches it from the frontend package: through node_modules (the frontend's own or a hoisted
|
|
182
352
|
// one up to the git root) when EDS is installed there, else the resolved package directory (pnpm store, linked install).
|
|
183
353
|
function conventionsPathFrom(packageDirectory, gitRoot, packageRoot) {
|
|
184
|
-
for (
|
|
185
|
-
|
|
354
|
+
for (
|
|
355
|
+
let directory = packageDirectory;
|
|
356
|
+
;
|
|
357
|
+
directory = path.dirname(directory)
|
|
358
|
+
) {
|
|
359
|
+
const candidate = path.join(
|
|
360
|
+
directory,
|
|
361
|
+
"node_modules",
|
|
362
|
+
packageName,
|
|
363
|
+
conventionsFileName
|
|
364
|
+
);
|
|
186
365
|
if (fs.existsSync(candidate)) {
|
|
187
366
|
return toPosix(path.relative(packageDirectory, candidate));
|
|
188
367
|
}
|
|
@@ -190,29 +369,42 @@ function conventionsPathFrom(packageDirectory, gitRoot, packageRoot) {
|
|
|
190
369
|
break;
|
|
191
370
|
}
|
|
192
371
|
}
|
|
193
|
-
return toPosix(
|
|
372
|
+
return toPosix(
|
|
373
|
+
path.join(path.relative(packageDirectory, packageRoot), conventionsFileName)
|
|
374
|
+
);
|
|
194
375
|
}
|
|
195
376
|
|
|
196
377
|
function verificationCommands(commands) {
|
|
197
|
-
return ["format:check", "lint", "typecheck", "build", "eds:doctor"]
|
|
378
|
+
return ["format:check", "lint", "typecheck", "build", "eds:doctor"]
|
|
379
|
+
.map((script) => commands.run(script))
|
|
380
|
+
.join(", ");
|
|
198
381
|
}
|
|
199
382
|
|
|
200
383
|
function locateFrontend({ consumerRoot, frontend }) {
|
|
201
|
-
const packageDirectory = fs.realpathSync(
|
|
384
|
+
const packageDirectory = fs.realpathSync(
|
|
385
|
+
resolveFrontendDirectory(consumerRoot, frontend)
|
|
386
|
+
);
|
|
202
387
|
const packageJson = readPackageJson(packageDirectory);
|
|
203
388
|
const framework = detectFramework(declaredDependencies(packageJson));
|
|
204
389
|
if (framework.unsupported !== null) {
|
|
205
390
|
throw createExitError(
|
|
206
|
-
framework.unsupported +
|
|
207
|
-
|
|
391
|
+
framework.unsupported +
|
|
392
|
+
" found: moving it to Vite is a framework migration, not alignment. eds align supports Vite + React single-page apps.",
|
|
393
|
+
2
|
|
208
394
|
);
|
|
209
395
|
}
|
|
210
396
|
if (!framework.vite || !framework.react) {
|
|
211
|
-
throw createExitError(
|
|
397
|
+
throw createExitError(
|
|
398
|
+
"eds align needs a Vite + React app; " +
|
|
399
|
+
toPosix(path.join(packageDirectory, "package.json")) +
|
|
400
|
+
" must declare vite and react.",
|
|
401
|
+
2
|
|
402
|
+
);
|
|
212
403
|
}
|
|
213
404
|
const topLevel = git(packageDirectory, ["rev-parse", "--show-toplevel"]);
|
|
214
405
|
const gitRoot = topLevel === null ? null : fs.realpathSync(topLevel.trim());
|
|
215
|
-
const packageRelative =
|
|
406
|
+
const packageRelative =
|
|
407
|
+
gitRoot === null ? "" : toPosix(path.relative(gitRoot, packageDirectory));
|
|
216
408
|
return { packageDirectory, packageJson, gitRoot, packageRelative };
|
|
217
409
|
}
|
|
218
410
|
|
|
@@ -222,63 +414,217 @@ function requireGit(gitRoot) {
|
|
|
222
414
|
"eds align needs git: the base commit is how --check proves the backend did not change. Ask the user to create a git repository at " +
|
|
223
415
|
"the project root (the directory that holds both frontend and backend) with a .gitignore covering node_modules, build output and " +
|
|
224
416
|
".env files, commit everything once, then rerun eds align.",
|
|
225
|
-
2
|
|
417
|
+
2
|
|
226
418
|
);
|
|
227
419
|
}
|
|
228
420
|
}
|
|
229
421
|
|
|
230
|
-
export function runAlign({
|
|
422
|
+
export function runAlign({
|
|
423
|
+
consumerRoot,
|
|
424
|
+
env = process.env,
|
|
425
|
+
check = false,
|
|
426
|
+
frontend = null,
|
|
427
|
+
dryRun = false,
|
|
428
|
+
nodeVersion = process.versions.node,
|
|
429
|
+
log = console.log,
|
|
430
|
+
}) {
|
|
231
431
|
const located = locateFrontend({ consumerRoot, frontend });
|
|
232
432
|
// Paths in the output are relative to the current directory, which differs from the frontend package with --frontend.
|
|
233
|
-
const fromCwd = toPosix(
|
|
234
|
-
|
|
433
|
+
const fromCwd = toPosix(
|
|
434
|
+
path.relative(consumerRoot, located.packageDirectory)
|
|
435
|
+
);
|
|
436
|
+
const shown = (relativePath) =>
|
|
437
|
+
fromCwd === "" ? relativePath : fromCwd + "/" + relativePath;
|
|
235
438
|
// With --frontend the rerun hint keeps the flag, or the check could not locate the package again.
|
|
236
439
|
const frontendFlag = frontend ? " --frontend " + frontend : "";
|
|
237
|
-
return check
|
|
440
|
+
return check
|
|
441
|
+
? runCheck({ ...located, env, log, shown, frontendFlag, nodeVersion })
|
|
442
|
+
: runPlan({ ...located, env, dryRun, log, fromCwd, shown, nodeVersion });
|
|
238
443
|
}
|
|
239
444
|
|
|
240
|
-
function runPlan({
|
|
445
|
+
function runPlan({
|
|
446
|
+
packageDirectory,
|
|
447
|
+
packageJson,
|
|
448
|
+
gitRoot,
|
|
449
|
+
packageRelative,
|
|
450
|
+
env,
|
|
451
|
+
dryRun,
|
|
452
|
+
log,
|
|
453
|
+
fromCwd,
|
|
454
|
+
shown,
|
|
455
|
+
nodeVersion,
|
|
456
|
+
}) {
|
|
241
457
|
requireGit(gitRoot);
|
|
242
458
|
let pkg;
|
|
243
459
|
try {
|
|
244
460
|
pkg = resolvePackage(packageDirectory);
|
|
245
461
|
} catch {
|
|
246
|
-
throw createExitError(
|
|
462
|
+
throw createExitError(
|
|
463
|
+
"Install " +
|
|
464
|
+
packageName +
|
|
465
|
+
" in " +
|
|
466
|
+
(packageRelative || ".") +
|
|
467
|
+
" first (" +
|
|
468
|
+
installCommand(detectPackageManager(packageDirectory, env)) +
|
|
469
|
+
"), then run eds align there.",
|
|
470
|
+
2
|
|
471
|
+
);
|
|
247
472
|
}
|
|
248
473
|
if (fs.realpathSync(pkg.packageRoot) !== fs.realpathSync(packageRoot)) {
|
|
249
|
-
log(
|
|
474
|
+
log(
|
|
475
|
+
"[EDS] note: this is eds from " +
|
|
476
|
+
toPosix(packageRoot) +
|
|
477
|
+
", but the frontend has " +
|
|
478
|
+
packageName +
|
|
479
|
+
"@" +
|
|
480
|
+
pkg.version +
|
|
481
|
+
" installed; run its own CLI (" +
|
|
482
|
+
cliCommand(detectPackageManager(packageDirectory, env), "align") +
|
|
483
|
+
") so the plan matches the installed documents."
|
|
484
|
+
);
|
|
250
485
|
}
|
|
251
486
|
const packageManager = detectPackageManager(packageDirectory, env);
|
|
252
487
|
const alignPath = path.join(packageDirectory, alignDirectory);
|
|
253
|
-
const alignRelative =
|
|
488
|
+
const alignRelative =
|
|
489
|
+
packageRelative === ""
|
|
490
|
+
? alignDirectory
|
|
491
|
+
: packageRelative + "/" + alignDirectory;
|
|
254
492
|
const previous = readState(path.join(alignPath, stateFileName), shown);
|
|
255
|
-
|
|
256
|
-
|
|
493
|
+
const previousPreservedPaths = normalizeRepositoryPaths(
|
|
494
|
+
previous?.preservedPaths ?? [],
|
|
495
|
+
alignRelative
|
|
496
|
+
);
|
|
497
|
+
const previousAutoPreservedPaths = normalizeRepositoryPaths(
|
|
498
|
+
previous?.autoPreservedPaths ??
|
|
499
|
+
previousPreservedPaths.filter((relativePath) =>
|
|
500
|
+
isAutomaticPreservedRepositoryPath(relativePath, packageRelative)
|
|
501
|
+
),
|
|
502
|
+
alignRelative
|
|
503
|
+
);
|
|
504
|
+
const previousAutoSet = new Set(previousAutoPreservedPaths);
|
|
505
|
+
const manualPreservedPaths = previousPreservedPaths.filter(
|
|
506
|
+
(relativePath) => !previousAutoSet.has(relativePath)
|
|
507
|
+
);
|
|
508
|
+
const analysis = analyzeFrontend({
|
|
509
|
+
packageDirectory,
|
|
510
|
+
gitRoot,
|
|
511
|
+
protectedInfo: previous?.protected ?? null,
|
|
512
|
+
preservedPaths: packagePathsFromRepository(
|
|
513
|
+
previousPreservedPaths,
|
|
514
|
+
packageRelative
|
|
515
|
+
),
|
|
516
|
+
nodeVersion,
|
|
517
|
+
});
|
|
518
|
+
if (analysis.dependencies.unsupportedVite !== null) {
|
|
519
|
+
const vite = analysis.dependencies.unsupportedVite;
|
|
520
|
+
throw createExitError(
|
|
521
|
+
"eds align supports Vite 5.2.0–" +
|
|
522
|
+
maximumSupportedViteMajor +
|
|
523
|
+
".x only; " +
|
|
524
|
+
(vite === "unknown"
|
|
525
|
+
? "the declared Vite version cannot be verified"
|
|
526
|
+
: vite === "range"
|
|
527
|
+
? "the declared Vite range must guarantee Vite 5.2.0 or later within one supported major"
|
|
528
|
+
: "Vite " + vite + " has not been verified") +
|
|
529
|
+
".",
|
|
530
|
+
2
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
if (analysis.dependencies.unsupportedNode !== null) {
|
|
534
|
+
throw createExitError(
|
|
535
|
+
"Vite 8 React Compiler setup requires Node " +
|
|
536
|
+
minimumVersions.nodeForVite8 +
|
|
537
|
+
" or later; eds align is running on Node " +
|
|
538
|
+
analysis.dependencies.unsupportedNode +
|
|
539
|
+
".",
|
|
540
|
+
2
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const currentAutoPreservedPaths = analysis.autoPreservedPaths.map(
|
|
544
|
+
(relativePath) =>
|
|
545
|
+
packageRelative === ""
|
|
546
|
+
? relativePath
|
|
547
|
+
: packageRelative + "/" + relativePath
|
|
548
|
+
);
|
|
549
|
+
const autoPreservedPaths = [
|
|
550
|
+
...new Set([...previousAutoPreservedPaths, ...currentAutoPreservedPaths]),
|
|
551
|
+
].sort();
|
|
552
|
+
const preservedRepositoryPaths = [
|
|
553
|
+
...new Set([...manualPreservedPaths, ...autoPreservedPaths]),
|
|
554
|
+
].sort();
|
|
257
555
|
let base;
|
|
258
556
|
let baseline;
|
|
259
557
|
if (previous !== null) {
|
|
260
558
|
base = previous.git.base;
|
|
261
|
-
baseline = previous.git.baseline;
|
|
559
|
+
baseline = { ...(previous.git.baseline ?? {}) };
|
|
262
560
|
} else {
|
|
263
561
|
base = git(gitRoot, ["rev-parse", "--verify", "HEAD"])?.trim() ?? null;
|
|
264
562
|
if (!base) {
|
|
265
|
-
throw createExitError(
|
|
266
|
-
"
|
|
563
|
+
throw createExitError(
|
|
564
|
+
"The repository has no commit yet, so --check has no base. Ask the user to commit the current files once " +
|
|
565
|
+
"(with a .gitignore covering node_modules, build output and .env files), then rerun eds align.",
|
|
566
|
+
2
|
|
567
|
+
);
|
|
267
568
|
}
|
|
268
|
-
//
|
|
269
|
-
//
|
|
569
|
+
// Record the frontend's existing changes so later-added preserved paths can use the original plan baseline.
|
|
570
|
+
// The check still ignores ordinary in-package paths that are not protected or preserved.
|
|
270
571
|
baseline = {};
|
|
271
572
|
const guarded = (relativePath) =>
|
|
272
|
-
analysis.protectedInfo.repository.some(
|
|
273
|
-
|
|
573
|
+
analysis.protectedInfo.repository.some(
|
|
574
|
+
(entry) =>
|
|
575
|
+
relativePath === entry || relativePath.startsWith(entry + "/")
|
|
576
|
+
) ||
|
|
577
|
+
preservedRepositoryPaths.some(
|
|
578
|
+
(entry) =>
|
|
579
|
+
relativePath === entry || relativePath.startsWith(entry + "/")
|
|
580
|
+
) ||
|
|
581
|
+
(packageRelative !== "" &&
|
|
582
|
+
relativePath !== packageRelative &&
|
|
583
|
+
!relativePath.startsWith(packageRelative + "/"));
|
|
274
584
|
for (const relativePath of changedSince(gitRoot, "HEAD")) {
|
|
275
|
-
|
|
276
|
-
|
|
585
|
+
const inPackage =
|
|
586
|
+
packageRelative === "" ||
|
|
587
|
+
relativePath === packageRelative ||
|
|
588
|
+
relativePath.startsWith(packageRelative + "/");
|
|
589
|
+
if (
|
|
590
|
+
relativePath !== alignRelative &&
|
|
591
|
+
!relativePath.startsWith(alignRelative + "/") &&
|
|
592
|
+
(inPackage || guarded(relativePath))
|
|
593
|
+
) {
|
|
594
|
+
const hash = hashFile(path.join(gitRoot, relativePath));
|
|
595
|
+
if (hash !== null) baseline[relativePath] = hash;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
Object.assign(baseline, hashPaths(gitRoot, preservedRepositoryPaths));
|
|
599
|
+
}
|
|
600
|
+
if (previous !== null) {
|
|
601
|
+
// A refreshed plan adopts newly added preserved files while keeping earlier file hashes unchanged.
|
|
602
|
+
for (const [relativePath, hash] of Object.entries(
|
|
603
|
+
hashPaths(gitRoot, preservedRepositoryPaths)
|
|
604
|
+
)) {
|
|
605
|
+
if (!Object.hasOwn(baseline, relativePath)) baseline[relativePath] = hash;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Upgrade older plans that did not record preserved paths, then report later edits against this baseline.
|
|
609
|
+
if (previous?.preservedPaths === undefined) {
|
|
610
|
+
for (const relativePath of changedSince(gitRoot, base)) {
|
|
611
|
+
if (
|
|
612
|
+
preservedRepositoryPaths.some(
|
|
613
|
+
(entry) =>
|
|
614
|
+
relativePath === entry || relativePath.startsWith(entry + "/")
|
|
615
|
+
) &&
|
|
616
|
+
!Object.hasOwn(baseline, relativePath)
|
|
617
|
+
) {
|
|
618
|
+
const hash = hashFile(path.join(gitRoot, relativePath));
|
|
619
|
+
if (hash !== null) baseline[relativePath] = hash;
|
|
277
620
|
}
|
|
278
621
|
}
|
|
279
622
|
}
|
|
280
623
|
|
|
281
|
-
analysis.structure = summarizeStructure(
|
|
624
|
+
analysis.structure = summarizeStructure(
|
|
625
|
+
analysis.allFiles,
|
|
626
|
+
analysis.preservedSourcePaths
|
|
627
|
+
);
|
|
282
628
|
const templates = renderTemplates({
|
|
283
629
|
app: analysis.app,
|
|
284
630
|
protectedInside: analysis.protectedInfo.inside,
|
|
@@ -287,7 +633,11 @@ function runPlan({ packageDirectory, packageJson, gitRoot, packageRelative, env,
|
|
|
287
633
|
strict: readStrictOption(packageDirectory),
|
|
288
634
|
});
|
|
289
635
|
const createdAt = previous?.createdAt ?? new Date().toISOString();
|
|
290
|
-
const frontendState = {
|
|
636
|
+
const frontendState = {
|
|
637
|
+
packageRelative,
|
|
638
|
+
appDir: analysis.app.appDir,
|
|
639
|
+
srcDir: analysis.app.srcDir,
|
|
640
|
+
};
|
|
291
641
|
const state = {
|
|
292
642
|
schemaVersion: stateSchemaVersion,
|
|
293
643
|
createdBy: packageName + "@" + pkg.version,
|
|
@@ -295,6 +645,14 @@ function runPlan({ packageDirectory, packageJson, gitRoot, packageRelative, env,
|
|
|
295
645
|
frontend: frontendState,
|
|
296
646
|
git: { base, baseline },
|
|
297
647
|
protected: analysis.protectedInfo,
|
|
648
|
+
preservedPaths: preservedRepositoryPaths,
|
|
649
|
+
autoPreservedPaths,
|
|
650
|
+
manualPreservedPaths,
|
|
651
|
+
preservedTsconfigSettings:
|
|
652
|
+
previous?.preservedTsconfigSettings &&
|
|
653
|
+
!isLegacyPreservedTsconfigSettings(previous.preservedTsconfigSettings)
|
|
654
|
+
? previous.preservedTsconfigSettings
|
|
655
|
+
: analysis.preservedTsconfigSettings,
|
|
298
656
|
};
|
|
299
657
|
const plan = renderPlan({
|
|
300
658
|
version: pkg.version,
|
|
@@ -305,25 +663,71 @@ function runPlan({ packageDirectory, packageJson, gitRoot, packageRelative, env,
|
|
|
305
663
|
analysis,
|
|
306
664
|
templates: [...templates.keys()],
|
|
307
665
|
alignLabel: alignDirectory,
|
|
308
|
-
conventionsPath: conventionsPathFrom(
|
|
666
|
+
conventionsPath: conventionsPathFrom(
|
|
667
|
+
packageDirectory,
|
|
668
|
+
gitRoot,
|
|
669
|
+
pkg.packageRoot
|
|
670
|
+
),
|
|
309
671
|
});
|
|
310
672
|
const counts = countFindings(analysis.findings);
|
|
311
673
|
|
|
312
|
-
log(
|
|
313
|
-
"
|
|
674
|
+
log(
|
|
675
|
+
"[EDS] align: frontend " +
|
|
676
|
+
(packageRelative || ".") +
|
|
677
|
+
" (Vite " +
|
|
678
|
+
(analysis.majors.vite ?? "?") +
|
|
679
|
+
", React " +
|
|
680
|
+
(analysis.majors.react ?? "?") +
|
|
681
|
+
", " +
|
|
682
|
+
packageManager.name +
|
|
683
|
+
"), sources " +
|
|
684
|
+
analysis.app.srcDir
|
|
685
|
+
);
|
|
314
686
|
const protectedList = analysis.protectedInfo.repository;
|
|
315
|
-
log(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
687
|
+
log(
|
|
688
|
+
"[EDS] align: protected paths: " +
|
|
689
|
+
(protectedList.length > 0 ? protectedList.join(", ") : "none detected") +
|
|
690
|
+
(packageRelative === ""
|
|
691
|
+
? ""
|
|
692
|
+
: ", plus files outside " + packageRelative + " (except repository root lockfiles)")
|
|
693
|
+
);
|
|
694
|
+
log(
|
|
695
|
+
"[EDS] align: " +
|
|
696
|
+
counts.errors +
|
|
697
|
+
" " +
|
|
698
|
+
plural(counts.errors, "error") +
|
|
699
|
+
(counts.errorSummary ? " (" + counts.errorSummary + ")" : "") +
|
|
700
|
+
", " +
|
|
701
|
+
counts.warnings +
|
|
702
|
+
" " +
|
|
703
|
+
plural(counts.warnings, "warning") +
|
|
704
|
+
" to resolve"
|
|
705
|
+
);
|
|
319
706
|
if (previous !== null) {
|
|
320
|
-
log(
|
|
707
|
+
log(
|
|
708
|
+
"[EDS] align: kept the base commit " +
|
|
709
|
+
base.slice(0, 12) +
|
|
710
|
+
" from " +
|
|
711
|
+
createdAt.slice(0, 10) +
|
|
712
|
+
"; delete " +
|
|
713
|
+
shown(alignDirectory) +
|
|
714
|
+
" to start over."
|
|
715
|
+
);
|
|
321
716
|
}
|
|
322
717
|
const planPath = path.join(alignPath, planFileName);
|
|
323
718
|
if (dryRun) {
|
|
324
719
|
const notesExist = fs.existsSync(path.join(alignPath, notesFileName));
|
|
325
|
-
log(
|
|
326
|
-
|
|
720
|
+
log(
|
|
721
|
+
"[EDS] would write " +
|
|
722
|
+
shown(alignDirectory + "/" + planFileName) +
|
|
723
|
+
", " +
|
|
724
|
+
(notesExist ? "" : shown(alignDirectory + "/" + notesFileName) + ", ") +
|
|
725
|
+
shown(alignDirectory + "/" + stateFileName) +
|
|
726
|
+
" and " +
|
|
727
|
+
templates.size +
|
|
728
|
+
" templates in " +
|
|
729
|
+
shown(alignDirectory + "/" + templatesDirectoryName)
|
|
730
|
+
);
|
|
327
731
|
return 0;
|
|
328
732
|
}
|
|
329
733
|
|
|
@@ -332,7 +736,10 @@ function runPlan({ packageDirectory, packageJson, gitRoot, packageRelative, env,
|
|
|
332
736
|
fs.mkdirSync(alignPath, { recursive: true });
|
|
333
737
|
// The directory ignores itself, so the plan never lands in a commit or in the base comparison.
|
|
334
738
|
fs.writeFileSync(path.join(alignPath, ".gitignore"), "*\n");
|
|
335
|
-
fs.writeFileSync(
|
|
739
|
+
fs.writeFileSync(
|
|
740
|
+
path.join(alignPath, stateFileName),
|
|
741
|
+
JSON.stringify(state, null, 2) + "\n"
|
|
742
|
+
);
|
|
336
743
|
fs.writeFileSync(planPath, plan);
|
|
337
744
|
const notesPath = path.join(alignPath, notesFileName);
|
|
338
745
|
if (!fs.existsSync(notesPath)) {
|
|
@@ -344,55 +751,184 @@ function runPlan({ packageDirectory, packageJson, gitRoot, packageRelative, env,
|
|
|
344
751
|
fs.writeFileSync(targetPath, contents);
|
|
345
752
|
}
|
|
346
753
|
const commands = packageManagerCommands(packageManager);
|
|
347
|
-
log(
|
|
348
|
-
|
|
349
|
-
|
|
754
|
+
log(
|
|
755
|
+
"[EDS] wrote " +
|
|
756
|
+
shown(alignDirectory + "/" + planFileName) +
|
|
757
|
+
", " +
|
|
758
|
+
notesFileName +
|
|
759
|
+
", " +
|
|
760
|
+
stateFileName +
|
|
761
|
+
" and " +
|
|
762
|
+
templates.size +
|
|
763
|
+
" templates (base " +
|
|
764
|
+
base.slice(0, 12) +
|
|
765
|
+
")"
|
|
766
|
+
);
|
|
767
|
+
log(
|
|
768
|
+
"[EDS] Next: " +
|
|
769
|
+
(fromCwd === "" ? "" : "cd " + fromCwd + ", then ") +
|
|
770
|
+
"follow " +
|
|
771
|
+
alignDirectory +
|
|
772
|
+
"/" +
|
|
773
|
+
planFileName +
|
|
774
|
+
" step by step; " +
|
|
775
|
+
commands.eds("align --check") +
|
|
776
|
+
" verifies each step" +
|
|
777
|
+
(fromCwd === ""
|
|
778
|
+
? "."
|
|
779
|
+
: " (run every PLAN.md command from " + fromCwd + ").")
|
|
780
|
+
);
|
|
350
781
|
return 0;
|
|
351
782
|
}
|
|
352
783
|
|
|
353
|
-
// With a plan, the check also proves that no protected or out-of-package file changed since its base commit.
|
|
784
|
+
// With a plan, the check also proves that no protected, preserved or out-of-package file except repository root lockfiles changed since its base commit.
|
|
354
785
|
// Without one (after step 8 removed .eds/align), it checks the conventions only.
|
|
355
|
-
function runCheck({
|
|
356
|
-
|
|
357
|
-
|
|
786
|
+
function runCheck({
|
|
787
|
+
packageDirectory,
|
|
788
|
+
gitRoot,
|
|
789
|
+
packageRelative,
|
|
790
|
+
env,
|
|
791
|
+
log,
|
|
792
|
+
shown,
|
|
793
|
+
frontendFlag = "",
|
|
794
|
+
nodeVersion,
|
|
795
|
+
}) {
|
|
796
|
+
const state = readState(
|
|
797
|
+
path.join(packageDirectory, alignDirectory, stateFileName),
|
|
798
|
+
shown
|
|
799
|
+
);
|
|
800
|
+
const commands = packageManagerCommands(
|
|
801
|
+
detectPackageManager(packageDirectory, env)
|
|
802
|
+
);
|
|
358
803
|
const findings = [];
|
|
359
804
|
let analysis;
|
|
360
805
|
if (state === null) {
|
|
361
|
-
log(
|
|
362
|
-
|
|
806
|
+
log(
|
|
807
|
+
"[EDS] note: no " +
|
|
808
|
+
shown(alignDirectory + "/" + stateFileName) +
|
|
809
|
+
": checking the conventions only (the protected-path check needs the base commit of eds align)."
|
|
810
|
+
);
|
|
811
|
+
analysis = analyzeFrontend({
|
|
812
|
+
packageDirectory,
|
|
813
|
+
gitRoot: gitRoot ?? packageDirectory,
|
|
814
|
+
nodeVersion,
|
|
815
|
+
});
|
|
363
816
|
} else {
|
|
364
817
|
requireGit(gitRoot);
|
|
365
|
-
if (
|
|
366
|
-
|
|
818
|
+
if (
|
|
819
|
+
git(gitRoot, ["cat-file", "-e", state.git.base + "^{commit}"]) === null
|
|
820
|
+
) {
|
|
821
|
+
throw createExitError(
|
|
822
|
+
"The base commit " +
|
|
823
|
+
state.git.base.slice(0, 12) +
|
|
824
|
+
" is not in this repository; delete " +
|
|
825
|
+
shown(alignDirectory) +
|
|
826
|
+
" and run eds align again.",
|
|
827
|
+
2
|
|
828
|
+
);
|
|
367
829
|
}
|
|
368
|
-
const alignRelative =
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
830
|
+
const alignRelative =
|
|
831
|
+
packageRelative === ""
|
|
832
|
+
? alignDirectory
|
|
833
|
+
: packageRelative + "/" + alignDirectory;
|
|
834
|
+
const statePreservedPaths = normalizeRepositoryPaths(
|
|
835
|
+
state.preservedPaths ?? [],
|
|
836
|
+
alignRelative
|
|
837
|
+
);
|
|
838
|
+
findings.push(
|
|
839
|
+
...classifyChangedPaths({
|
|
840
|
+
changed: changedSince(gitRoot, state.git.base),
|
|
841
|
+
baseline: state.git.baseline ?? {},
|
|
842
|
+
hashOf: (relativePath) => hashFile(path.join(gitRoot, relativePath)),
|
|
843
|
+
packageRelative,
|
|
844
|
+
protectedPaths: state.protected.repository ?? [],
|
|
845
|
+
preservedPaths: statePreservedPaths,
|
|
846
|
+
baseTrackedPaths: gitPaths(gitRoot, [
|
|
847
|
+
"ls-tree",
|
|
848
|
+
"-r",
|
|
849
|
+
"-z",
|
|
850
|
+
"--name-only",
|
|
851
|
+
state.git.base,
|
|
852
|
+
]),
|
|
853
|
+
alignRelative,
|
|
854
|
+
base: state.git.base,
|
|
855
|
+
})
|
|
856
|
+
);
|
|
857
|
+
analysis = analyzeFrontend({
|
|
858
|
+
packageDirectory,
|
|
859
|
+
gitRoot,
|
|
860
|
+
protectedInfo: state.protected,
|
|
861
|
+
preservedPaths: packagePathsFromRepository(
|
|
862
|
+
statePreservedPaths,
|
|
863
|
+
packageRelative
|
|
864
|
+
),
|
|
865
|
+
nodeVersion,
|
|
866
|
+
});
|
|
867
|
+
findings.push(
|
|
868
|
+
...checkBackendPackage({ packageJson: analysis.packageJson, state })
|
|
869
|
+
);
|
|
870
|
+
findings.push(
|
|
871
|
+
...checkPreservedTsconfigSettings({
|
|
872
|
+
baseline: state.preservedTsconfigSettings,
|
|
873
|
+
current: analysis.preservedTsconfigSettings,
|
|
874
|
+
})
|
|
875
|
+
);
|
|
380
876
|
}
|
|
381
877
|
findings.push(...analysis.findings);
|
|
382
878
|
const counts = countFindings(findings);
|
|
383
879
|
const since = state === null ? "" : " since " + state.git.base.slice(0, 12);
|
|
384
880
|
logFindings(findings, log);
|
|
385
881
|
if (counts.errors === 0) {
|
|
386
|
-
log(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
882
|
+
log(
|
|
883
|
+
"[EDS] align: " +
|
|
884
|
+
analysis.app.srcDir +
|
|
885
|
+
" follows the EVA frontend conventions" +
|
|
886
|
+
(state === null
|
|
887
|
+
? ""
|
|
888
|
+
: ", and no protected, preserved or out-of-package file changed (repository root lockfiles are excluded)" +
|
|
889
|
+
since) +
|
|
890
|
+
(counts.warnings > 0
|
|
891
|
+
? " (" +
|
|
892
|
+
counts.warnings +
|
|
893
|
+
" " +
|
|
894
|
+
plural(counts.warnings, "warning") +
|
|
895
|
+
": fix them or record why)"
|
|
896
|
+
: "") +
|
|
897
|
+
"."
|
|
898
|
+
);
|
|
899
|
+
log(
|
|
900
|
+
"[EDS] Also required: " +
|
|
901
|
+
verificationCommands(commands) +
|
|
902
|
+
" (each must exit 0, and eds:doctor must print [EDS] synced with)" +
|
|
903
|
+
(state === null
|
|
904
|
+
? "."
|
|
905
|
+
: ", then step 8 of " +
|
|
906
|
+
shown(alignDirectory + "/" + planFileName) +
|
|
907
|
+
".")
|
|
908
|
+
);
|
|
391
909
|
return 0;
|
|
392
910
|
}
|
|
393
|
-
log(
|
|
394
|
-
|
|
395
|
-
|
|
911
|
+
log(
|
|
912
|
+
"[EDS] align: " +
|
|
913
|
+
counts.errors +
|
|
914
|
+
" " +
|
|
915
|
+
plural(counts.errors, "error") +
|
|
916
|
+
" (" +
|
|
917
|
+
counts.errorSummary +
|
|
918
|
+
"), " +
|
|
919
|
+
counts.warnings +
|
|
920
|
+
" " +
|
|
921
|
+
plural(counts.warnings, "warning") +
|
|
922
|
+
since +
|
|
923
|
+
". Fix the errors and run " +
|
|
924
|
+
commands.eds("align --check" + frontendFlag) +
|
|
925
|
+
" again" +
|
|
926
|
+
(state === null
|
|
927
|
+
? "."
|
|
928
|
+
: "; " +
|
|
929
|
+
shown(alignDirectory + "/" + planFileName) +
|
|
930
|
+
" maps each kind to a step; protected, preserved, outside and backend-* errors mean revert that change.")
|
|
931
|
+
);
|
|
396
932
|
return 1;
|
|
397
933
|
}
|
|
398
934
|
|