@reddoorla/maintenance 0.6.6 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bin.js +356 -468
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +12 -21
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.js +338 -391
- package/dist/index.js.map +1 -1
- package/dist/recipes/sync-configs.js +89 -44
- package/dist/recipes/sync-configs.js.map +1 -1
- package/dist/util/git.js +2 -3
- package/dist/util/git.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,6 +31,11 @@ var defaultSpawn = (cmd, args, opts = {}) => new Promise((resolve, reject) => {
|
|
|
31
31
|
import { readFile } from "fs/promises";
|
|
32
32
|
import { join } from "path";
|
|
33
33
|
|
|
34
|
+
// src/util/site.ts
|
|
35
|
+
function siteLabel(site) {
|
|
36
|
+
return site.name ?? site.path;
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
// src/configs/baseline-versions.ts
|
|
35
40
|
var baselineVersions = {
|
|
36
41
|
// SvelteKit core
|
|
@@ -71,9 +76,6 @@ var baselineVersions = {
|
|
|
71
76
|
};
|
|
72
77
|
|
|
73
78
|
// src/audits/deps.ts
|
|
74
|
-
function siteLabel(site) {
|
|
75
|
-
return site.name ?? site.path;
|
|
76
|
-
}
|
|
77
79
|
function stripCaret(range) {
|
|
78
80
|
return range.replace(/^[\^~]/, "");
|
|
79
81
|
}
|
|
@@ -146,9 +148,6 @@ import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "
|
|
|
146
148
|
import { glob } from "tinyglobby";
|
|
147
149
|
var TARGET_GLOBS = ["**/*.{ts,js,svelte}"];
|
|
148
150
|
var IGNORE = ["node_modules/**", "dist/**", ".svelte-kit/**", "build/**", ".netlify/**"];
|
|
149
|
-
function siteLabel2(site) {
|
|
150
|
-
return site.name ?? site.path;
|
|
151
|
-
}
|
|
152
151
|
async function listFiles(cwd) {
|
|
153
152
|
return glob(TARGET_GLOBS, { cwd, ignore: IGNORE, absolute: false });
|
|
154
153
|
}
|
|
@@ -158,7 +157,7 @@ async function lintAudit(ctx) {
|
|
|
158
157
|
if (!existsSync(configPath)) {
|
|
159
158
|
return {
|
|
160
159
|
audit: "lint",
|
|
161
|
-
site:
|
|
160
|
+
site: siteLabel(site),
|
|
162
161
|
status: "skip",
|
|
163
162
|
summary: "no eslint config at site root"
|
|
164
163
|
};
|
|
@@ -184,7 +183,7 @@ async function lintAudit(ctx) {
|
|
|
184
183
|
const summary = status === "pass" ? `lint clean across ${relFiles.length} files` : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;
|
|
185
184
|
return {
|
|
186
185
|
audit: "lint",
|
|
187
|
-
site:
|
|
186
|
+
site: siteLabel(site),
|
|
188
187
|
status,
|
|
189
188
|
summary,
|
|
190
189
|
details: {
|
|
@@ -197,9 +196,6 @@ async function lintAudit(ctx) {
|
|
|
197
196
|
}
|
|
198
197
|
|
|
199
198
|
// src/audits/security.ts
|
|
200
|
-
function siteLabel3(site) {
|
|
201
|
-
return site.name ?? site.path;
|
|
202
|
-
}
|
|
203
199
|
function classify(v) {
|
|
204
200
|
if (v.critical > 0 || v.high > 0) return "fail";
|
|
205
201
|
if (v.moderate > 0 || v.low > 0) return "warn";
|
|
@@ -285,7 +281,8 @@ async function runAuditTool(spawn2, cmd, args, cwd) {
|
|
|
285
281
|
if (errEnvelope && typeof errEnvelope === "object") {
|
|
286
282
|
return { kind: "error", reason: errEnvelope.code ?? "error envelope returned" };
|
|
287
283
|
}
|
|
288
|
-
|
|
284
|
+
const vulnsMeta = parsed.metadata?.vulnerabilities;
|
|
285
|
+
if (!vulnsMeta || Object.keys(vulnsMeta).length === 0) {
|
|
289
286
|
return { kind: "error", reason: "no metadata.vulnerabilities in output" };
|
|
290
287
|
}
|
|
291
288
|
return { kind: "ok", parsed };
|
|
@@ -293,7 +290,7 @@ async function runAuditTool(spawn2, cmd, args, cwd) {
|
|
|
293
290
|
async function securityAudit(ctx) {
|
|
294
291
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
295
292
|
const site = ctx.site;
|
|
296
|
-
const label =
|
|
293
|
+
const label = siteLabel(site);
|
|
297
294
|
let used = "pnpm audit";
|
|
298
295
|
let result = await runAuditTool(spawn2, "pnpm", ["audit", "--json", "--prod"], site.path);
|
|
299
296
|
if (result.kind !== "ok") {
|
|
@@ -374,9 +371,6 @@ var lighthouseConfig = {
|
|
|
374
371
|
};
|
|
375
372
|
|
|
376
373
|
// src/audits/lighthouse.ts
|
|
377
|
-
function siteLabel4(site) {
|
|
378
|
-
return site.name ?? site.path;
|
|
379
|
-
}
|
|
380
374
|
async function readJsonMaybe(path) {
|
|
381
375
|
try {
|
|
382
376
|
const raw = await readFile3(path, "utf-8");
|
|
@@ -414,7 +408,7 @@ function messageForAssertion(a) {
|
|
|
414
408
|
async function lighthouseAudit(ctx) {
|
|
415
409
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
416
410
|
const site = ctx.site;
|
|
417
|
-
const label =
|
|
411
|
+
const label = siteLabel(site);
|
|
418
412
|
const configDir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
|
|
419
413
|
const configPath = join3(configDir, "lighthouserc.json");
|
|
420
414
|
await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
|
|
@@ -512,9 +506,6 @@ var playwrightA11yConfig = defineConfig({
|
|
|
512
506
|
|
|
513
507
|
// src/audits/a11y.ts
|
|
514
508
|
var RESULTS_REL = ".reddoor-a11y/results.json";
|
|
515
|
-
function siteLabel5(site) {
|
|
516
|
-
return site.name ?? site.path;
|
|
517
|
-
}
|
|
518
509
|
async function readJsonMaybe2(path) {
|
|
519
510
|
try {
|
|
520
511
|
const raw = await readFile4(path, "utf-8");
|
|
@@ -572,7 +563,7 @@ test("a11y across configured routes", async ({ page }) => {
|
|
|
572
563
|
async function a11yAudit(ctx) {
|
|
573
564
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
574
565
|
const site = ctx.site;
|
|
575
|
-
const label =
|
|
566
|
+
const label = siteLabel(site);
|
|
576
567
|
const specDir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-spec-"));
|
|
577
568
|
const specPath = join4(specDir, "a11y.spec.ts");
|
|
578
569
|
await writeFile2(specPath, buildSpec(), "utf-8");
|
|
@@ -820,9 +811,8 @@ async function git(cwd, args) {
|
|
|
820
811
|
return exec("git", args, { cwd, env: process.env });
|
|
821
812
|
}
|
|
822
813
|
function branchName(recipe, when = /* @__PURE__ */ new Date()) {
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
return `maint/${recipe}-${trimmed}`;
|
|
814
|
+
const compact = when.toISOString().replace(/[-:.]/g, "");
|
|
815
|
+
return `maint/${recipe}-${compact}`;
|
|
826
816
|
}
|
|
827
817
|
async function isWorkingTreeClean(cwd) {
|
|
828
818
|
const { stdout } = await git(cwd, ["status", "--porcelain"]);
|
|
@@ -851,11 +841,67 @@ async function commit(cwd, message) {
|
|
|
851
841
|
return sha.trim();
|
|
852
842
|
}
|
|
853
843
|
|
|
844
|
+
// src/recipes/_with-recipe.ts
|
|
845
|
+
async function withRecipe(body) {
|
|
846
|
+
const label = siteLabel(body.site);
|
|
847
|
+
if (body.checkTreeFirst && !await isWorkingTreeClean(body.site.path)) {
|
|
848
|
+
throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);
|
|
849
|
+
}
|
|
850
|
+
const planned = await body.plan();
|
|
851
|
+
if (planned.kind === "noop") {
|
|
852
|
+
return {
|
|
853
|
+
recipe: body.name,
|
|
854
|
+
site: label,
|
|
855
|
+
status: "noop",
|
|
856
|
+
commits: [],
|
|
857
|
+
...planned.notes ? { notes: planned.notes } : {}
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
if (planned.kind === "failed") {
|
|
861
|
+
return {
|
|
862
|
+
recipe: body.name,
|
|
863
|
+
site: label,
|
|
864
|
+
status: "failed",
|
|
865
|
+
commits: [],
|
|
866
|
+
notes: planned.notes
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
if (!body.checkTreeFirst && !await isWorkingTreeClean(body.site.path)) {
|
|
870
|
+
throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);
|
|
871
|
+
}
|
|
872
|
+
const branch = branchName(body.name);
|
|
873
|
+
await createBranch(body.site.path, branch);
|
|
874
|
+
const shas = [];
|
|
875
|
+
const result = await body.apply(planned.plan, {
|
|
876
|
+
cwd: body.site.path,
|
|
877
|
+
branch,
|
|
878
|
+
commit: async (msg) => {
|
|
879
|
+
const sha = await commit(body.site.path, msg);
|
|
880
|
+
if (sha) shas.push(sha);
|
|
881
|
+
return sha;
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
if (result.kind === "failed") {
|
|
885
|
+
return {
|
|
886
|
+
recipe: body.name,
|
|
887
|
+
site: label,
|
|
888
|
+
status: "failed",
|
|
889
|
+
commits: shas,
|
|
890
|
+
notes: result.notes
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
const notes = result.notes ? `${result.notes}; branch: ${branch}` : `branch: ${branch}`;
|
|
894
|
+
return {
|
|
895
|
+
recipe: body.name,
|
|
896
|
+
site: label,
|
|
897
|
+
status: shas.length > 0 ? "applied" : "noop",
|
|
898
|
+
commits: shas,
|
|
899
|
+
notes
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
854
903
|
// src/recipes/sync-configs.ts
|
|
855
904
|
var GITIGNORE_CONFIG = "gitignore";
|
|
856
|
-
function siteLabel6(site) {
|
|
857
|
-
return site.name ?? site.path;
|
|
858
|
-
}
|
|
859
905
|
async function readMaybe(path) {
|
|
860
906
|
try {
|
|
861
907
|
return await readFile5(path, "utf-8");
|
|
@@ -886,56 +932,38 @@ async function applyGitignore(cwd, plan) {
|
|
|
886
932
|
}
|
|
887
933
|
}
|
|
888
934
|
async function syncConfigs(site, opts = {}) {
|
|
889
|
-
const label = siteLabel6(site);
|
|
890
935
|
const requested = opts.which ?? ALL_TEMPLATES.map((t) => t.config).concat(GITIGNORE_CONFIG);
|
|
891
936
|
const templateNames = requested.filter((c) => c !== GITIGNORE_CONFIG);
|
|
892
937
|
const templates = templatesByName(templateNames);
|
|
893
938
|
const includeGitignore = requested.includes(GITIGNORE_CONFIG);
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
site:
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if (sha) shas.push(sha);
|
|
918
|
-
}
|
|
919
|
-
if (gitignorePlan.kind === "apply") {
|
|
920
|
-
await applyGitignore(site.path, gitignorePlan);
|
|
921
|
-
const sha = await commit(site.path, `chore: sync gitignore from @reddoorla/maintenance`);
|
|
922
|
-
if (sha) shas.push(sha);
|
|
923
|
-
}
|
|
924
|
-
return {
|
|
925
|
-
recipe: "sync-configs",
|
|
926
|
-
site: label,
|
|
927
|
-
status: "applied",
|
|
928
|
-
commits: shas,
|
|
929
|
-
notes: `branch: ${branch}`
|
|
930
|
-
};
|
|
939
|
+
return withRecipe({
|
|
940
|
+
name: "sync-configs",
|
|
941
|
+
site,
|
|
942
|
+
plan: async () => {
|
|
943
|
+
const templateDiffs = await planTemplateDiffs(site.path, templates);
|
|
944
|
+
const gitignorePlan = includeGitignore ? await planGitignore(site.path) : { kind: "noop" };
|
|
945
|
+
if (templateDiffs.length === 0 && gitignorePlan.kind === "noop") {
|
|
946
|
+
return { kind: "noop", notes: "all targeted configs already match" };
|
|
947
|
+
}
|
|
948
|
+
return { kind: "apply", plan: { templateDiffs, gitignorePlan } };
|
|
949
|
+
},
|
|
950
|
+
apply: async ({ templateDiffs, gitignorePlan }, { commit: commit2 }) => {
|
|
951
|
+
for (const t of templateDiffs) {
|
|
952
|
+
await writeFile3(join5(site.path, t.path), t.contents, "utf-8");
|
|
953
|
+
await commit2(`chore: sync ${t.config} config from @reddoorla/maintenance`);
|
|
954
|
+
}
|
|
955
|
+
if (gitignorePlan.kind === "apply") {
|
|
956
|
+
await applyGitignore(site.path, gitignorePlan);
|
|
957
|
+
await commit2(`chore: sync gitignore from @reddoorla/maintenance`);
|
|
958
|
+
}
|
|
959
|
+
return { kind: "ok" };
|
|
960
|
+
}
|
|
961
|
+
});
|
|
931
962
|
}
|
|
932
963
|
|
|
933
964
|
// src/recipes/bump-deps.ts
|
|
934
965
|
import { stat } from "fs/promises";
|
|
935
966
|
import { join as join6 } from "path";
|
|
936
|
-
function siteLabel7(site) {
|
|
937
|
-
return site.name ?? site.path;
|
|
938
|
-
}
|
|
939
967
|
async function exists(path) {
|
|
940
968
|
try {
|
|
941
969
|
await stat(path);
|
|
@@ -954,62 +982,51 @@ function upFlagsForGroup(group) {
|
|
|
954
982
|
return [];
|
|
955
983
|
}
|
|
956
984
|
async function bumpDeps(site, opts = {}) {
|
|
957
|
-
const label = siteLabel7(site);
|
|
958
985
|
const group = opts.group ?? "minor";
|
|
959
986
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
987
|
+
return withRecipe({
|
|
988
|
+
name: "bump-deps",
|
|
989
|
+
site,
|
|
990
|
+
// pnpm install (in plan) mutates the lockfile, so the clean-tree check
|
|
991
|
+
// MUST happen first — otherwise a desynced-lockfile resync would silently
|
|
992
|
+
// land on top of whatever else was in the tree.
|
|
993
|
+
checkTreeFirst: true,
|
|
994
|
+
plan: async () => {
|
|
995
|
+
const hasPnpmLock = await exists(join6(site.path, "pnpm-lock.yaml"));
|
|
996
|
+
if (!hasPnpmLock) {
|
|
997
|
+
const hasNpmLock = await exists(join6(site.path, "package-lock.json"));
|
|
998
|
+
const hasYarnLock = await exists(join6(site.path, "yarn.lock"));
|
|
999
|
+
if (hasNpmLock || hasYarnLock) {
|
|
1000
|
+
const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
1001
|
+
return {
|
|
1002
|
+
kind: "failed",
|
|
1003
|
+
notes: `site has ${competing} but no pnpm-lock.yaml \u2014 run convert-to-pnpm first`
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
await spawn2("pnpm", ["install"], { cwd: site.path, streaming: true });
|
|
1008
|
+
const outdated = await spawn2(
|
|
1009
|
+
"pnpm",
|
|
1010
|
+
["outdated", "--json", ...outdatedFlagsForGroup(group)],
|
|
1011
|
+
{ cwd: site.path }
|
|
1012
|
+
);
|
|
1013
|
+
let parsed;
|
|
1014
|
+
try {
|
|
1015
|
+
parsed = JSON.parse(outdated.stdout || "{}");
|
|
1016
|
+
} catch {
|
|
1017
|
+
parsed = {};
|
|
1018
|
+
}
|
|
1019
|
+
if (Object.keys(parsed).length === 0) {
|
|
1020
|
+
return { kind: "noop", notes: `pnpm outdated reported nothing for group=${group}` };
|
|
1021
|
+
}
|
|
1022
|
+
return { kind: "apply", plan: { group } };
|
|
1023
|
+
},
|
|
1024
|
+
apply: async ({ group: g }, { commit: commit2, cwd }) => {
|
|
1025
|
+
await spawn2("pnpm", ["up", ...upFlagsForGroup(g)], { cwd, streaming: true });
|
|
1026
|
+
await commit2(`chore(deps): bump dependencies (${g})`);
|
|
1027
|
+
return { kind: "ok" };
|
|
973
1028
|
}
|
|
974
|
-
}
|
|
975
|
-
if (!await isWorkingTreeClean(site.path)) {
|
|
976
|
-
throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
|
|
977
|
-
}
|
|
978
|
-
await spawn2("pnpm", ["install"], { cwd: site.path, streaming: true });
|
|
979
|
-
const outdated = await spawn2("pnpm", ["outdated", "--json", ...outdatedFlagsForGroup(group)], {
|
|
980
|
-
cwd: site.path
|
|
981
|
-
});
|
|
982
|
-
let parsed;
|
|
983
|
-
try {
|
|
984
|
-
parsed = JSON.parse(outdated.stdout || "{}");
|
|
985
|
-
} catch {
|
|
986
|
-
parsed = {};
|
|
987
|
-
}
|
|
988
|
-
const nothingToDo = Object.keys(parsed).length === 0;
|
|
989
|
-
if (nothingToDo) {
|
|
990
|
-
return {
|
|
991
|
-
recipe: "bump-deps",
|
|
992
|
-
site: label,
|
|
993
|
-
status: "noop",
|
|
994
|
-
commits: [],
|
|
995
|
-
notes: `pnpm outdated reported nothing for group=${group}`
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
const branch = branchName("bump-deps");
|
|
999
|
-
await createBranch(site.path, branch);
|
|
1000
|
-
await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], {
|
|
1001
|
-
cwd: site.path,
|
|
1002
|
-
streaming: true
|
|
1003
1029
|
});
|
|
1004
|
-
const sha = await commit(site.path, `chore(deps): bump dependencies (${group})`);
|
|
1005
|
-
const shas = sha ? [sha] : [];
|
|
1006
|
-
return {
|
|
1007
|
-
recipe: "bump-deps",
|
|
1008
|
-
site: label,
|
|
1009
|
-
status: shas.length > 0 ? "applied" : "noop",
|
|
1010
|
-
commits: shas,
|
|
1011
|
-
notes: `branch: ${branch}`
|
|
1012
|
-
};
|
|
1013
1030
|
}
|
|
1014
1031
|
|
|
1015
1032
|
// src/recipes/svelte-5/index.ts
|
|
@@ -1196,6 +1213,34 @@ import { glob as glob2 } from "tinyglobby";
|
|
|
1196
1213
|
// src/recipes/svelte-5/codemods/on-event-to-handler.ts
|
|
1197
1214
|
var SCRIPT_BLOCK = /<script\b[^>]*>[\s\S]*?<\/script>/g;
|
|
1198
1215
|
var SIMPLE_ON_EVENT = /\bon:([a-z]+)(?=\s*=)/g;
|
|
1216
|
+
var MODIFIER_EVENT = /\bon:[a-z]+\|[a-zA-Z]+(?=\s*=)/g;
|
|
1217
|
+
function flagEventModifiers(source) {
|
|
1218
|
+
const insertions = [];
|
|
1219
|
+
let m;
|
|
1220
|
+
MODIFIER_EVENT.lastIndex = 0;
|
|
1221
|
+
while ((m = MODIFIER_EVENT.exec(source)) !== null) {
|
|
1222
|
+
const tagStart = source.lastIndexOf("<", m.index);
|
|
1223
|
+
if (tagStart === -1) continue;
|
|
1224
|
+
const prevLineEnd = tagStart - 1;
|
|
1225
|
+
if (prevLineEnd >= 0) {
|
|
1226
|
+
const prevLineStart = source.lastIndexOf("\n", prevLineEnd - 1) + 1;
|
|
1227
|
+
const prevLine = source.slice(prevLineStart, prevLineEnd + 1);
|
|
1228
|
+
if (/<!--\s*@migration-task/.test(prevLine)) continue;
|
|
1229
|
+
}
|
|
1230
|
+
const lineStart = source.lastIndexOf("\n", tagStart - 1) + 1;
|
|
1231
|
+
const indent = source.slice(lineStart, tagStart);
|
|
1232
|
+
const safeIndent = /^[ \t]*$/.test(indent) ? indent : "";
|
|
1233
|
+
insertions.push({ tagStart, indent: safeIndent, modifier: m[0] });
|
|
1234
|
+
}
|
|
1235
|
+
let out = source;
|
|
1236
|
+
for (let i = insertions.length - 1; i >= 0; i--) {
|
|
1237
|
+
const { tagStart, indent, modifier } = insertions[i];
|
|
1238
|
+
const comment = `<!-- @migration-task: Svelte 5 removed event modifier syntax (\`${modifier}\`). Rewrite inline, e.g. onclick={(e) => { e.preventDefault(); ... }}. -->
|
|
1239
|
+
${indent}`;
|
|
1240
|
+
out = out.slice(0, tagStart) + comment + out.slice(tagStart);
|
|
1241
|
+
}
|
|
1242
|
+
return out;
|
|
1243
|
+
}
|
|
1199
1244
|
function onEventToHandler(source) {
|
|
1200
1245
|
const masked = [];
|
|
1201
1246
|
const placeholder = (i) => ` SCRIPT_${i} `;
|
|
@@ -1203,8 +1248,9 @@ function onEventToHandler(source) {
|
|
|
1203
1248
|
masked.push(match);
|
|
1204
1249
|
return placeholder(masked.length - 1);
|
|
1205
1250
|
});
|
|
1206
|
-
|
|
1207
|
-
|
|
1251
|
+
let processed = intermediate.replace(SIMPLE_ON_EVENT, (_full, name) => `on${name}`);
|
|
1252
|
+
processed = flagEventModifiers(processed);
|
|
1253
|
+
let out = processed;
|
|
1208
1254
|
masked.forEach((blk, i) => {
|
|
1209
1255
|
out = out.replace(placeholder(i), blk);
|
|
1210
1256
|
});
|
|
@@ -1254,6 +1300,22 @@ function exportLetToProps(source) {
|
|
|
1254
1300
|
return source.replace(SCRIPT_BLOCK2, (full) => full.replace(inner, body));
|
|
1255
1301
|
}
|
|
1256
1302
|
|
|
1303
|
+
// src/util/svelte-source.ts
|
|
1304
|
+
function findStringEnd(source, openIdx) {
|
|
1305
|
+
const quote = source[openIdx];
|
|
1306
|
+
let i = openIdx + 1;
|
|
1307
|
+
while (i < source.length) {
|
|
1308
|
+
const ch = source[i];
|
|
1309
|
+
if (ch === "\\") {
|
|
1310
|
+
i += 2;
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
if (ch === quote) return i;
|
|
1314
|
+
i++;
|
|
1315
|
+
}
|
|
1316
|
+
return -1;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1257
1319
|
// src/recipes/svelte-5/codemods/dollar-restprops.ts
|
|
1258
1320
|
function removeInterfaceBlock(source) {
|
|
1259
1321
|
const re = /^\s*interface\s+\$\$Props\s*\{/m;
|
|
@@ -1277,20 +1339,6 @@ function removeInterfaceBlock(source) {
|
|
|
1277
1339
|
out = out.slice(0, match.index) + out.slice(endIdx);
|
|
1278
1340
|
}
|
|
1279
1341
|
}
|
|
1280
|
-
function findStringEnd(source, openIdx) {
|
|
1281
|
-
const quote = source[openIdx];
|
|
1282
|
-
let i = openIdx + 1;
|
|
1283
|
-
while (i < source.length) {
|
|
1284
|
-
const ch = source[i];
|
|
1285
|
-
if (ch === "\\") {
|
|
1286
|
-
i += 2;
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1289
|
-
if (ch === quote) return i;
|
|
1290
|
-
i++;
|
|
1291
|
-
}
|
|
1292
|
-
return -1;
|
|
1293
|
-
}
|
|
1294
1342
|
function maskStringLiterals(source) {
|
|
1295
1343
|
const strings = [];
|
|
1296
1344
|
let out = "";
|
|
@@ -1367,7 +1415,8 @@ function stateEffectSyncToDerived(source) {
|
|
|
1367
1415
|
|
|
1368
1416
|
// src/recipes/svelte-5/codemods/dollar-props-class.ts
|
|
1369
1417
|
var PROPS_DESTRUCTURE = /let\s*\{([\s\S]*?)\}(\s*:\s*\{([\s\S]*?)\})?\s*=\s*\$props\(\)/;
|
|
1370
|
-
var
|
|
1418
|
+
var HAS_DOLLAR_PROPS_CLASS = /\$\$props\.class\b/;
|
|
1419
|
+
var DOLLAR_PROPS_CLASS_GLOBAL = /\$\$props\.class\b/g;
|
|
1371
1420
|
var DOLLAR_PROPS_ANY = /\$\$props\b/;
|
|
1372
1421
|
var SCRIPT_BLOCK4 = /<script\b[^>]*>[\s\S]*?<\/script>/g;
|
|
1373
1422
|
var MIGRATION_TASK = /^<!--\s*@migration-task[\s\S]*?-->\s*\n?/gm;
|
|
@@ -1389,8 +1438,7 @@ function restoreScripts(masked, blocks) {
|
|
|
1389
1438
|
}
|
|
1390
1439
|
function dollarPropsClass(source) {
|
|
1391
1440
|
const { masked } = maskScripts(source);
|
|
1392
|
-
if (!
|
|
1393
|
-
DOLLAR_PROPS_CLASS.lastIndex = 0;
|
|
1441
|
+
if (!HAS_DOLLAR_PROPS_CLASS.test(masked)) return source;
|
|
1394
1442
|
if (!PROPS_DESTRUCTURE.test(source)) return source;
|
|
1395
1443
|
let updated = source.replace(PROPS_DESTRUCTURE, (full, body, typeAnno, typeBody) => {
|
|
1396
1444
|
if (/\bclass\s*:/.test(body)) return full;
|
|
@@ -1404,7 +1452,7 @@ function dollarPropsClass(source) {
|
|
|
1404
1452
|
return `let { ${newBody} } = $props()`;
|
|
1405
1453
|
});
|
|
1406
1454
|
const reMasked = maskScripts(updated);
|
|
1407
|
-
const templateRewritten = reMasked.masked.replace(
|
|
1455
|
+
const templateRewritten = reMasked.masked.replace(DOLLAR_PROPS_CLASS_GLOBAL, IDENT);
|
|
1408
1456
|
updated = restoreScripts(templateRewritten, reMasked.blocks);
|
|
1409
1457
|
const stripped = updated.replace(MIGRATION_TASK, "");
|
|
1410
1458
|
if (!DOLLAR_PROPS_ANY.test(stripped)) {
|
|
@@ -1423,7 +1471,7 @@ function findMatchingClose(source, openIdx) {
|
|
|
1423
1471
|
while (i < source.length) {
|
|
1424
1472
|
const ch = source[i];
|
|
1425
1473
|
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1426
|
-
const closeStr =
|
|
1474
|
+
const closeStr = findStringEnd(source, i);
|
|
1427
1475
|
if (closeStr === -1) return -1;
|
|
1428
1476
|
i = closeStr + 1;
|
|
1429
1477
|
continue;
|
|
@@ -1437,20 +1485,6 @@ function findMatchingClose(source, openIdx) {
|
|
|
1437
1485
|
}
|
|
1438
1486
|
return -1;
|
|
1439
1487
|
}
|
|
1440
|
-
function findStringClose(source, openIdx) {
|
|
1441
|
-
const quote = source[openIdx];
|
|
1442
|
-
let i = openIdx + 1;
|
|
1443
|
-
while (i < source.length) {
|
|
1444
|
-
const ch = source[i];
|
|
1445
|
-
if (ch === "\\") {
|
|
1446
|
-
i += 2;
|
|
1447
|
-
continue;
|
|
1448
|
-
}
|
|
1449
|
-
if (ch === quote) return i;
|
|
1450
|
-
i++;
|
|
1451
|
-
}
|
|
1452
|
-
return -1;
|
|
1453
|
-
}
|
|
1454
1488
|
var MIGRATION_MARKER = "// @migration-task: $effect won't trigger UI updates on plain `let` bindings \u2014 refine mutated locals to $state or split into per-variable $derived.";
|
|
1455
1489
|
function transformBlocks(body) {
|
|
1456
1490
|
const out = [];
|
|
@@ -1562,9 +1596,6 @@ async function writeMigrationSummary(input) {
|
|
|
1562
1596
|
}
|
|
1563
1597
|
|
|
1564
1598
|
// src/recipes/svelte-5/index.ts
|
|
1565
|
-
function siteLabel8(site) {
|
|
1566
|
-
return site.name ?? site.path;
|
|
1567
|
-
}
|
|
1568
1599
|
async function alreadyOnSvelte5(cwd) {
|
|
1569
1600
|
try {
|
|
1570
1601
|
const pkg = await readPackageJson(join12(cwd, "package.json"));
|
|
@@ -1575,111 +1606,73 @@ async function alreadyOnSvelte5(cwd) {
|
|
|
1575
1606
|
}
|
|
1576
1607
|
}
|
|
1577
1608
|
async function upgradeSvelte4to5(site, opts = {}) {
|
|
1578
|
-
const label = siteLabel8(site);
|
|
1579
1609
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
site.path,
|
|
1622
|
-
`refactor(svelte5): apply gotcha codemods (${codemods.filesChanged} files)`
|
|
1623
|
-
);
|
|
1624
|
-
if (sha) shas.push(sha);
|
|
1625
|
-
}
|
|
1626
|
-
await verifyMigration(site.path, spawn2);
|
|
1627
|
-
const verifySha = await commit(site.path, "chore(svelte5): pnpm install + check");
|
|
1628
|
-
if (verifySha) shas.push(verifySha);
|
|
1629
|
-
await writeMigrationSummary({
|
|
1630
|
-
cwd: site.path,
|
|
1631
|
-
filesChangedByCodemods: codemods.filesChanged,
|
|
1632
|
-
svelteMigrateRan: migrate.ran,
|
|
1633
|
-
tailwindUpgraded: tw.ran
|
|
1610
|
+
return withRecipe({
|
|
1611
|
+
name: "svelte-4-to-5",
|
|
1612
|
+
site,
|
|
1613
|
+
plan: async () => {
|
|
1614
|
+
if (await alreadyOnSvelte5(site.path)) {
|
|
1615
|
+
return { kind: "noop", notes: "site already declares svelte ^5.x" };
|
|
1616
|
+
}
|
|
1617
|
+
return { kind: "apply", plan: true };
|
|
1618
|
+
},
|
|
1619
|
+
apply: async (_plan, { commit: commit2, cwd }) => {
|
|
1620
|
+
const bumped = await bumpToSvelte5Versions(cwd);
|
|
1621
|
+
if (bumped) {
|
|
1622
|
+
await commit2("chore(svelte5): bump svelte/kit/vite/vite-plugin-svelte");
|
|
1623
|
+
}
|
|
1624
|
+
const configChanged = await migrateSvelteConfig(cwd);
|
|
1625
|
+
if (configChanged) {
|
|
1626
|
+
await commit2("refactor(svelte5): migrate svelte.config.js (drop vitePreprocess)");
|
|
1627
|
+
}
|
|
1628
|
+
const migrate = await runSvelteMigrate(cwd, spawn2);
|
|
1629
|
+
if (migrate.ran) {
|
|
1630
|
+
await commit2("refactor(svelte5): run official svelte-migrate codemod");
|
|
1631
|
+
}
|
|
1632
|
+
const tw = await upgradeTailwind(cwd, spawn2);
|
|
1633
|
+
if (tw.ran) {
|
|
1634
|
+
await commit2("chore(svelte5): tailwindcss 3 \u2192 4 upgrade");
|
|
1635
|
+
}
|
|
1636
|
+
const codemods = await applyGotchaCodemods(cwd);
|
|
1637
|
+
if (codemods.filesChanged > 0) {
|
|
1638
|
+
await commit2(`refactor(svelte5): apply gotcha codemods (${codemods.filesChanged} files)`);
|
|
1639
|
+
}
|
|
1640
|
+
await verifyMigration(cwd, spawn2);
|
|
1641
|
+
await commit2("chore(svelte5): pnpm install + check");
|
|
1642
|
+
await writeMigrationSummary({
|
|
1643
|
+
cwd,
|
|
1644
|
+
filesChangedByCodemods: codemods.filesChanged,
|
|
1645
|
+
svelteMigrateRan: migrate.ran,
|
|
1646
|
+
tailwindUpgraded: tw.ran
|
|
1647
|
+
});
|
|
1648
|
+
await commit2("docs(svelte5): add MIGRATION_SVELTE_5.md summary");
|
|
1649
|
+
return { kind: "ok" };
|
|
1650
|
+
}
|
|
1634
1651
|
});
|
|
1635
|
-
const summarySha = await commit(site.path, "docs(svelte5): add MIGRATION_SVELTE_5.md summary");
|
|
1636
|
-
if (summarySha) shas.push(summarySha);
|
|
1637
|
-
return {
|
|
1638
|
-
recipe: "svelte-4-to-5",
|
|
1639
|
-
site: label,
|
|
1640
|
-
status: shas.length > 0 ? "applied" : "noop",
|
|
1641
|
-
commits: shas,
|
|
1642
|
-
notes: `branch: ${branch}`
|
|
1643
|
-
};
|
|
1644
1652
|
}
|
|
1645
1653
|
|
|
1646
1654
|
// src/recipes/svelte-codemods.ts
|
|
1647
1655
|
import { writeFile as writeFile8 } from "fs/promises";
|
|
1648
1656
|
import { join as join13 } from "path";
|
|
1649
|
-
function siteLabel9(site) {
|
|
1650
|
-
return site.name ?? site.path;
|
|
1651
|
-
}
|
|
1652
1657
|
async function svelteCodemods(site) {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
}
|
|
1672
|
-
const sha = await commit(
|
|
1673
|
-
site.path,
|
|
1674
|
-
`refactor(svelte5): apply codemods (${changes.length} files)`
|
|
1675
|
-
);
|
|
1676
|
-
return {
|
|
1677
|
-
recipe: "svelte-codemods",
|
|
1678
|
-
site: label,
|
|
1679
|
-
status: "applied",
|
|
1680
|
-
commits: sha ? [sha] : [],
|
|
1681
|
-
notes: `branch: ${branch}`
|
|
1682
|
-
};
|
|
1658
|
+
return withRecipe({
|
|
1659
|
+
name: "svelte-codemods",
|
|
1660
|
+
site,
|
|
1661
|
+
plan: async () => {
|
|
1662
|
+
const changes = await planGotchaCodemods(site.path);
|
|
1663
|
+
if (changes.length === 0) {
|
|
1664
|
+
return { kind: "noop", notes: "no codemod targets matched" };
|
|
1665
|
+
}
|
|
1666
|
+
return { kind: "apply", plan: changes };
|
|
1667
|
+
},
|
|
1668
|
+
apply: async (changes, { commit: commit2, cwd }) => {
|
|
1669
|
+
for (const c of changes) {
|
|
1670
|
+
await writeFile8(join13(cwd, c.rel), c.after, "utf-8");
|
|
1671
|
+
}
|
|
1672
|
+
await commit2(`refactor(svelte5): apply codemods (${changes.length} files)`);
|
|
1673
|
+
return { kind: "ok" };
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1683
1676
|
}
|
|
1684
1677
|
|
|
1685
1678
|
// src/recipes/convert-to-pnpm.ts
|
|
@@ -1714,84 +1707,56 @@ async function exists2(path) {
|
|
|
1714
1707
|
return false;
|
|
1715
1708
|
}
|
|
1716
1709
|
}
|
|
1717
|
-
function siteLabel10(site) {
|
|
1718
|
-
return site.name ?? site.path;
|
|
1719
|
-
}
|
|
1720
1710
|
async function convertToPnpm(site, opts = {}) {
|
|
1721
|
-
const label = siteLabel10(site);
|
|
1722
1711
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
1723
1712
|
const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
|
|
1724
1713
|
const pnpmLockPath = join14(site.path, "pnpm-lock.yaml");
|
|
1725
1714
|
const npmLockPath = join14(site.path, "package-lock.json");
|
|
1726
1715
|
const yarnLockPath = join14(site.path, "yarn.lock");
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1716
|
+
return withRecipe({
|
|
1717
|
+
name: "convert-to-pnpm",
|
|
1718
|
+
site,
|
|
1719
|
+
plan: async () => {
|
|
1720
|
+
if (await exists2(pnpmLockPath)) {
|
|
1721
|
+
return { kind: "noop", notes: "site already has pnpm-lock.yaml" };
|
|
1722
|
+
}
|
|
1723
|
+
const hasNpmLock = await exists2(npmLockPath);
|
|
1724
|
+
const hasYarnLock = await exists2(yarnLockPath);
|
|
1725
|
+
if (!hasNpmLock && !hasYarnLock) {
|
|
1726
|
+
return {
|
|
1727
|
+
kind: "noop",
|
|
1728
|
+
notes: "no convertible lockfile (package-lock.json or yarn.lock) at site root"
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
return { kind: "apply", plan: { hasNpmLock, hasYarnLock } };
|
|
1732
|
+
},
|
|
1733
|
+
apply: async ({ hasNpmLock, hasYarnLock }, { commit: commit2, cwd }) => {
|
|
1734
|
+
if (hasNpmLock) await rm3(npmLockPath, { force: true });
|
|
1735
|
+
if (hasYarnLock) await rm3(yarnLockPath, { force: true });
|
|
1736
|
+
const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
1737
|
+
await commit2(`chore(pnpm): remove ${sourceLock}`);
|
|
1738
|
+
const pkgPath = join14(cwd, "package.json");
|
|
1739
|
+
const pkg = await readPackageJson(pkgPath);
|
|
1740
|
+
const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
|
|
1741
|
+
if (pkg.scripts && typeof pkg.scripts === "object") {
|
|
1742
|
+
const { scripts: rewritten, changedCount } = rewriteScriptsForPnpm(
|
|
1743
|
+
pkg.scripts
|
|
1744
|
+
);
|
|
1745
|
+
if (changedCount > 0) {
|
|
1746
|
+
next.scripts = rewritten;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
await writePackageJson(pkgPath, next);
|
|
1750
|
+
await commit2("chore(pnpm): pin packageManager + rewrite npm scripts");
|
|
1751
|
+
await rm3(join14(cwd, "node_modules"), { recursive: true, force: true });
|
|
1752
|
+
const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
|
|
1753
|
+
if (installResult.code !== 0) {
|
|
1754
|
+
return { kind: "failed", notes: `pnpm install failed (exit ${installResult.code})` };
|
|
1755
|
+
}
|
|
1756
|
+
await commit2("chore(pnpm): add pnpm-lock.yaml");
|
|
1757
|
+
return { kind: "ok" };
|
|
1767
1758
|
}
|
|
1768
|
-
}
|
|
1769
|
-
await writePackageJson(pkgPath, next);
|
|
1770
|
-
const pkgSha = await commit(site.path, "chore(pnpm): pin packageManager + rewrite npm scripts");
|
|
1771
|
-
if (pkgSha) shas.push(pkgSha);
|
|
1772
|
-
await rm3(join14(site.path, "node_modules"), { recursive: true, force: true });
|
|
1773
|
-
const installResult = await spawn2("pnpm", ["install"], {
|
|
1774
|
-
cwd: site.path,
|
|
1775
|
-
streaming: true
|
|
1776
1759
|
});
|
|
1777
|
-
if (installResult.code !== 0) {
|
|
1778
|
-
return {
|
|
1779
|
-
recipe: "convert-to-pnpm",
|
|
1780
|
-
site: label,
|
|
1781
|
-
status: "failed",
|
|
1782
|
-
commits: shas,
|
|
1783
|
-
notes: `pnpm install failed (exit ${installResult.code}). branch ${branch} left for inspection.`
|
|
1784
|
-
};
|
|
1785
|
-
}
|
|
1786
|
-
const installSha = await commit(site.path, "chore(pnpm): add pnpm-lock.yaml");
|
|
1787
|
-
if (installSha) shas.push(installSha);
|
|
1788
|
-
return {
|
|
1789
|
-
recipe: "convert-to-pnpm",
|
|
1790
|
-
site: label,
|
|
1791
|
-
status: "applied",
|
|
1792
|
-
commits: shas,
|
|
1793
|
-
notes: `branch: ${branch}`
|
|
1794
|
-
};
|
|
1795
1760
|
}
|
|
1796
1761
|
|
|
1797
1762
|
// src/recipes/onboard.ts
|
|
@@ -1844,81 +1809,63 @@ async function exists3(path) {
|
|
|
1844
1809
|
return false;
|
|
1845
1810
|
}
|
|
1846
1811
|
}
|
|
1847
|
-
function siteLabel11(site) {
|
|
1848
|
-
return site.name ?? site.path;
|
|
1849
|
-
}
|
|
1850
1812
|
function isDeclared(pkg, name) {
|
|
1851
1813
|
return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
|
|
1852
1814
|
}
|
|
1853
1815
|
async function onboard(site, opts = {}) {
|
|
1854
|
-
const label = siteLabel11(site);
|
|
1855
1816
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
1856
1817
|
const audits = opts.audits ?? ["lighthouse", "a11y"];
|
|
1857
1818
|
const packageVersion = opts.packageVersion ?? selfCaretRange(import.meta.url);
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1819
|
+
return withRecipe({
|
|
1820
|
+
name: "onboard",
|
|
1821
|
+
site,
|
|
1822
|
+
plan: async () => {
|
|
1823
|
+
if (!await exists3(join16(site.path, "pnpm-lock.yaml"))) {
|
|
1824
|
+
return {
|
|
1825
|
+
kind: "failed",
|
|
1826
|
+
notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
const pkgPath = join16(site.path, "package.json");
|
|
1830
|
+
const pkg = await readPackageJson(pkgPath);
|
|
1831
|
+
const toAdd = [];
|
|
1832
|
+
if (!isDeclared(pkg, PACKAGE_NAME)) {
|
|
1833
|
+
toAdd.push({ name: PACKAGE_NAME, version: packageVersion });
|
|
1834
|
+
}
|
|
1835
|
+
for (const audit of audits) {
|
|
1836
|
+
for (const dep of AUDIT_DEPS[audit]) {
|
|
1837
|
+
if (!isDeclared(pkg, dep.name)) toAdd.push(dep);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
if (toAdd.length === 0) {
|
|
1841
|
+
return {
|
|
1842
|
+
kind: "noop",
|
|
1843
|
+
notes: `site already has ${PACKAGE_NAME} and audit deps (${audits.join("+")})`
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
return { kind: "apply", plan: { pkg, toAdd } };
|
|
1847
|
+
},
|
|
1848
|
+
apply: async ({ pkg, toAdd }, { commit: commit2, cwd }) => {
|
|
1849
|
+
const pkgPath = join16(cwd, "package.json");
|
|
1850
|
+
let next = pkg;
|
|
1851
|
+
for (const dep of toAdd) {
|
|
1852
|
+
next = bumpDep(next, dep.name, dep.version);
|
|
1853
|
+
}
|
|
1854
|
+
await writePackageJson(pkgPath, next);
|
|
1855
|
+
const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
|
|
1856
|
+
if (installResult.code !== 0) {
|
|
1857
|
+
return {
|
|
1858
|
+
kind: "failed",
|
|
1859
|
+
notes: `pnpm install failed (exit ${installResult.code})`
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
await commit2(`chore(reddoor): onboard with ${PACKAGE_NAME} ${packageVersion}`);
|
|
1863
|
+
return {
|
|
1864
|
+
kind: "ok",
|
|
1865
|
+
notes: `Added ${toAdd.length} dep(s): ${toAdd.map((d) => d.name).join(", ")}`
|
|
1866
|
+
};
|
|
1876
1867
|
}
|
|
1877
|
-
}
|
|
1878
|
-
if (toAdd.length === 0) {
|
|
1879
|
-
return {
|
|
1880
|
-
recipe: "onboard",
|
|
1881
|
-
site: label,
|
|
1882
|
-
status: "noop",
|
|
1883
|
-
commits: [],
|
|
1884
|
-
notes: `site already has ${PACKAGE_NAME} and audit deps (${audits.join("+")})`
|
|
1885
|
-
};
|
|
1886
|
-
}
|
|
1887
|
-
if (!await isWorkingTreeClean(site.path)) {
|
|
1888
|
-
throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
|
|
1889
|
-
}
|
|
1890
|
-
const branch = branchName("onboard");
|
|
1891
|
-
await createBranch(site.path, branch);
|
|
1892
|
-
let next = pkg;
|
|
1893
|
-
for (const dep of toAdd) {
|
|
1894
|
-
next = bumpDep(next, dep.name, dep.version);
|
|
1895
|
-
}
|
|
1896
|
-
await writePackageJson(pkgPath, next);
|
|
1897
|
-
const installResult = await spawn2("pnpm", ["install"], {
|
|
1898
|
-
cwd: site.path,
|
|
1899
|
-
streaming: true
|
|
1900
1868
|
});
|
|
1901
|
-
if (installResult.code !== 0) {
|
|
1902
|
-
return {
|
|
1903
|
-
recipe: "onboard",
|
|
1904
|
-
site: label,
|
|
1905
|
-
status: "failed",
|
|
1906
|
-
commits: [],
|
|
1907
|
-
notes: `pnpm install failed (exit ${installResult.code}). branch ${branch} left for inspection.`
|
|
1908
|
-
};
|
|
1909
|
-
}
|
|
1910
|
-
const sha = await commit(
|
|
1911
|
-
site.path,
|
|
1912
|
-
`chore(reddoor): onboard with ${PACKAGE_NAME} ${packageVersion}`
|
|
1913
|
-
);
|
|
1914
|
-
const shas = sha ? [sha] : [];
|
|
1915
|
-
return {
|
|
1916
|
-
recipe: "onboard",
|
|
1917
|
-
site: label,
|
|
1918
|
-
status: "applied",
|
|
1919
|
-
commits: shas,
|
|
1920
|
-
notes: `branch: ${branch}. Added ${toAdd.length} dep(s): ${toAdd.map((d) => d.name).join(", ")}`
|
|
1921
|
-
};
|
|
1922
1869
|
}
|
|
1923
1870
|
|
|
1924
1871
|
// src/recipes/index.ts
|