migrate-to-comark 0.0.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +22 -1
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +212 -0
- package/dist/index.d.mts +112 -0
- package/dist/index.mjs +1 -0
- package/dist/migrate-B1Q-GmiU.mjs +222 -0
- package/package.json +55 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 onmax
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
1
|
# migrate-to-comark
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://npmx.dev/package/migrate-to-comark)
|
|
4
|
+
[](https://npmx.dev/package/migrate-to-comark)
|
|
5
|
+
[](https://comark-ui.onmax.me/migrate-to-comark/)
|
|
6
|
+
|
|
7
|
+
A wizard that moves a project to [Comark](https://comark.dev). It reads every package.json, finds remark, markdown-it, or marked, runs the migrator for each, and asks before writing what they would write.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx migrate-to-comark # asks before writing
|
|
11
|
+
npx migrate-to-comark --dry-run # reports only
|
|
12
|
+
npx migrate-to-comark --yes # writes without asking
|
|
13
|
+
npx migrate-to-comark --json # for agents
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Library | Migrator |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| remark, unified, react-markdown, MDX, Astro, Docusaurus | [remark-to-comark](https://comark-ui.onmax.me/remark-to-comark/) |
|
|
19
|
+
| markdown-it, VitePress, VuePress, Eleventy | [markdown-it-to-comark](https://comark-ui.onmax.me/markdown-it-to-comark/) |
|
|
20
|
+
| marked | [marked-to-comark](https://comark-ui.onmax.me/marked-to-comark/) |
|
|
21
|
+
|
|
22
|
+
A file two migrators both want goes to the library its nearest package.json declares; if that doesn't settle it, the wizard leaves the file alone. It also skips any file that changed after the check read it, and keeps the code that renders Markdown on its library until every Markdown file it renders is migrated. Each migrator's `check --verbose` lists what's left to do, with a fix for each finding.
|
|
23
|
+
|
|
24
|
+
See the [docs](https://comark-ui.onmax.me/migrate-to-comark/).
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as load, r as plan, t as apply } from "./migrate-B1Q-GmiU.mjs";
|
|
3
|
+
import { existsSync, statSync } from "node:fs";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import * as prompts from "@clack/prompts";
|
|
6
|
+
//#region src/summary.ts
|
|
7
|
+
const NAMES = {
|
|
8
|
+
remark: "remark",
|
|
9
|
+
"markdown-it": "markdown-it",
|
|
10
|
+
marked: "marked"
|
|
11
|
+
};
|
|
12
|
+
/** Where the project declares a library, in one line: the first few dependencies and their package.json files. */
|
|
13
|
+
function declared(report) {
|
|
14
|
+
const { uses } = report.detection;
|
|
15
|
+
const names = [...new Set(uses.map((use) => use.dependency))];
|
|
16
|
+
const manifests = [...new Set(uses.map((use) => use.manifest))];
|
|
17
|
+
return `${names.slice(0, 4).join(", ")}${names.length > 4 ? `, and ${names.length - 4} more` : ""} in ${manifests.length === 1 ? manifests[0] : `${manifests.length} package.json files`}`;
|
|
18
|
+
}
|
|
19
|
+
/** Each migrator's result as plain lines: what it can change, then each kind of warning it leaves, with its docs. */
|
|
20
|
+
async function describe(plan, load) {
|
|
21
|
+
const lines = [];
|
|
22
|
+
for (const report of plan.reports) {
|
|
23
|
+
const { library, migrator } = report.detection;
|
|
24
|
+
if (!report.findings) {
|
|
25
|
+
lines.push(`${NAMES[library]}: ${migrator} couldn't run (${report.error}). Run \`npx ${migrator} check .\` to see why.`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const module = await load(migrator);
|
|
29
|
+
const changes = plan.changes.filter((change) => change.migrator === migrator);
|
|
30
|
+
const warnings = report.findings.filter((finding) => module.catalog[finding.code]?.severity === "warn" && !finding.edit);
|
|
31
|
+
lines.push(`${NAMES[library]} (${declared(report)}): ${counted(changes)}, ${warnings.length} ${warnings.length === 1 ? "warning" : "warnings"} left to you.`);
|
|
32
|
+
const byCode = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const finding of warnings) byCode.set(finding.code, (byCode.get(finding.code) ?? 0) + 1);
|
|
34
|
+
for (const [code, count] of [...byCode].sort((a, b) => b[1] - a[1])) {
|
|
35
|
+
const first = warnings.find((finding) => finding.code === code);
|
|
36
|
+
const { docs } = module.toDiagnostic(first);
|
|
37
|
+
lines.push(` ${count} × ${code} ${module.catalog[code].ruleId}${docs ? ` (${docs})` : ""}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return lines;
|
|
41
|
+
}
|
|
42
|
+
/** "3 Markdown files and 1 code file to change", from the edits a migrator carries. */
|
|
43
|
+
function counted(changes) {
|
|
44
|
+
const markdown = changes.filter((change) => /\.(md|mdx|markdown)$/i.test(change.file)).length;
|
|
45
|
+
const code = changes.length - markdown;
|
|
46
|
+
if (!changes.length) return "nothing to change automatically";
|
|
47
|
+
return `${[markdown && `${markdown} Markdown ${markdown === 1 ? "file" : "files"}`, code && `${code} code ${code === 1 ? "file" : "files"}`].filter(Boolean).join(" and ")} to change`;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/cli.ts
|
|
51
|
+
const USAGE = `Usage:
|
|
52
|
+
migrate-to-comark [dir] find the project's Markdown libraries and move it to Comark, asking first
|
|
53
|
+
migrate-to-comark [dir] --dry-run report what would change, and write nothing
|
|
54
|
+
migrate-to-comark [dir] --yes write every change without asking
|
|
55
|
+
migrate-to-comark [dir] --json the report as JSON, for agents (add --yes to write)
|
|
56
|
+
migrate-to-comark [dir] --only markdown-it move only this library (remark, markdown-it, or marked; repeatable)`;
|
|
57
|
+
const { positionals, values } = parseArgs({
|
|
58
|
+
allowPositionals: true,
|
|
59
|
+
options: {
|
|
60
|
+
yes: {
|
|
61
|
+
type: "boolean",
|
|
62
|
+
default: false
|
|
63
|
+
},
|
|
64
|
+
"dry-run": {
|
|
65
|
+
type: "boolean",
|
|
66
|
+
default: false
|
|
67
|
+
},
|
|
68
|
+
json: {
|
|
69
|
+
type: "boolean",
|
|
70
|
+
default: false
|
|
71
|
+
},
|
|
72
|
+
only: {
|
|
73
|
+
type: "string",
|
|
74
|
+
multiple: true
|
|
75
|
+
},
|
|
76
|
+
help: {
|
|
77
|
+
type: "boolean",
|
|
78
|
+
short: "h",
|
|
79
|
+
default: false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (values.help || positionals.length > 1) {
|
|
84
|
+
console.log(USAGE);
|
|
85
|
+
process.exit(values.help ? 0 : 1);
|
|
86
|
+
}
|
|
87
|
+
const root = positionals[0] ?? ".";
|
|
88
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
89
|
+
console.error(`Cannot migrate \`${root}\`: no such directory.`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const LIBRARIES = [
|
|
93
|
+
"remark",
|
|
94
|
+
"markdown-it",
|
|
95
|
+
"marked"
|
|
96
|
+
];
|
|
97
|
+
const unknown = values.only?.find((library) => !LIBRARIES.includes(library));
|
|
98
|
+
if (unknown) {
|
|
99
|
+
console.error(`Cannot move \`${unknown}\`: --only takes ${LIBRARIES.join(", ")}.`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
const only = values.only?.length ? values.only : void 0;
|
|
103
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY && !values.yes && !values["dry-run"] && !values.json;
|
|
104
|
+
if (values.json) await json();
|
|
105
|
+
else if (interactive) await wizard();
|
|
106
|
+
else await report();
|
|
107
|
+
async function wizard() {
|
|
108
|
+
prompts.intro("migrate-to-comark");
|
|
109
|
+
const first = await plan(root, {
|
|
110
|
+
only: [],
|
|
111
|
+
load
|
|
112
|
+
});
|
|
113
|
+
if (!found(first.detected, (line) => prompts.log.warn(line))) {
|
|
114
|
+
prompts.outro("Nothing to migrate.");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const detection of first.detected.libraries) prompts.log.info(`Found ${detection.library}, moved by ${detection.migrator}.`);
|
|
118
|
+
let picked = first.detected.libraries.map((detection) => detection.library).filter((library) => !only || only.includes(library));
|
|
119
|
+
if (!only && picked.length > 1) {
|
|
120
|
+
const chosen = await prompts.multiselect({
|
|
121
|
+
message: "Which libraries do you want to move to Comark?",
|
|
122
|
+
options: picked.map((library) => ({
|
|
123
|
+
value: library,
|
|
124
|
+
label: library
|
|
125
|
+
})),
|
|
126
|
+
initialValues: picked
|
|
127
|
+
});
|
|
128
|
+
if (prompts.isCancel(chosen)) return prompts.cancel("Nothing written.");
|
|
129
|
+
picked = chosen;
|
|
130
|
+
}
|
|
131
|
+
const spinner = prompts.spinner();
|
|
132
|
+
spinner.start("Checking the project");
|
|
133
|
+
const result = await plan(root, {
|
|
134
|
+
only: picked,
|
|
135
|
+
load
|
|
136
|
+
});
|
|
137
|
+
spinner.stop("Checked the project");
|
|
138
|
+
prompts.note((await describe(result, load)).join("\n"), "What each migrator found");
|
|
139
|
+
for (const conflict of result.conflicts) prompts.log.warn(`${conflict.file} is left as it is: ${conflict.migrators.join(" and ")} would both change it. Run each one's \`content\` command on it and keep the result that renders right.`);
|
|
140
|
+
for (const entry of result.held) prompts.log.warn(heldBack(entry));
|
|
141
|
+
if (!result.changes.length) {
|
|
142
|
+
prompts.outro(next(result));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const go = await prompts.confirm({ message: `Write ${result.changes.length} ${result.changes.length === 1 ? "file" : "files"}?` });
|
|
146
|
+
if (prompts.isCancel(go) || !go) return prompts.cancel("Nothing written.");
|
|
147
|
+
const { written, stale, held } = apply(root, result.changes);
|
|
148
|
+
prompts.log.success(`Wrote ${written.length} ${written.length === 1 ? "file" : "files"}.`);
|
|
149
|
+
for (const file of stale) prompts.log.warn(`${file} changed while the wizard ran, so it was left as it is. Run the wizard again.`);
|
|
150
|
+
for (const entry of held) prompts.log.warn(heldBack(entry));
|
|
151
|
+
prompts.outro(next(result));
|
|
152
|
+
}
|
|
153
|
+
async function report() {
|
|
154
|
+
const result = await plan(root, {
|
|
155
|
+
only,
|
|
156
|
+
load
|
|
157
|
+
});
|
|
158
|
+
if (!found(result.detected, (line) => console.log(line))) return;
|
|
159
|
+
console.log((await describe(result, load)).join("\n"));
|
|
160
|
+
for (const conflict of result.conflicts) console.log(`${conflict.file} is left as it is: ${conflict.migrators.join(" and ")} would both change it.`);
|
|
161
|
+
for (const entry of result.held) console.log(heldBack(entry));
|
|
162
|
+
if (values.yes) {
|
|
163
|
+
const { written, stale, held } = apply(root, result.changes);
|
|
164
|
+
console.log(`\nWrote ${written.length} ${written.length === 1 ? "file" : "files"}.`);
|
|
165
|
+
for (const file of stale) console.log(`${file} changed while the wizard ran; left as it is.`);
|
|
166
|
+
for (const entry of held) console.log(heldBack(entry));
|
|
167
|
+
} else if (result.changes.length) console.log(`\nNothing written. Run again with --yes to write ${result.changes.length} ${result.changes.length === 1 ? "file" : "files"}.`);
|
|
168
|
+
console.log(`\n${next(result)}`);
|
|
169
|
+
}
|
|
170
|
+
async function json() {
|
|
171
|
+
const result = await plan(root, {
|
|
172
|
+
only,
|
|
173
|
+
load
|
|
174
|
+
});
|
|
175
|
+
const applied = values.yes ? apply(root, result.changes) : void 0;
|
|
176
|
+
console.log(JSON.stringify({
|
|
177
|
+
libraries: result.detected.libraries,
|
|
178
|
+
unsupported: result.detected.unsupported,
|
|
179
|
+
comark: result.detected.comark,
|
|
180
|
+
reports: result.reports.map(({ detection, findings, error }) => ({
|
|
181
|
+
library: detection.library,
|
|
182
|
+
migrator: detection.migrator,
|
|
183
|
+
findings: findings?.length,
|
|
184
|
+
error
|
|
185
|
+
})),
|
|
186
|
+
changes: result.changes.map(({ file, migrator }) => ({
|
|
187
|
+
file,
|
|
188
|
+
migrator
|
|
189
|
+
})),
|
|
190
|
+
conflicts: result.conflicts,
|
|
191
|
+
held: [...result.held, ...applied?.held ?? []],
|
|
192
|
+
written: applied?.written,
|
|
193
|
+
stale: applied?.stale
|
|
194
|
+
}, null, 2));
|
|
195
|
+
}
|
|
196
|
+
/** Says what was found; false when there's nothing any migrator moves. */
|
|
197
|
+
function found(detected, say) {
|
|
198
|
+
for (const { name, uses } of detected.unsupported) say(`${name} (${uses[0].manifest}) has no migrator yet, so its Markdown stays as it is.`);
|
|
199
|
+
if (detected.libraries.length) return true;
|
|
200
|
+
say(`No package.json under ${root} declares remark, markdown-it, or marked${detected.comark ? ", and comark is already a dependency" : ""}.`);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
/** Why a setup file still uses its library, and which Markdown files it waits on. */
|
|
204
|
+
function heldBack({ file, migrator, waiting }) {
|
|
205
|
+
return `${file} still renders with ${migrator.replace(/-to-comark$/, "")}: switching it now would change how ${`${waiting.slice(0, 3).join(", ")}${waiting.length > 3 ? `, and ${waiting.length - 3} more` : ""}`} ${waiting.length === 1 ? "renders" : "render"}. Migrate ${waiting.length === 1 ? "it" : "them"} first (\`npx ${migrator} check ${JSON.stringify(root)} --verbose\` says how), then run the wizard again.`;
|
|
206
|
+
}
|
|
207
|
+
/** The commands that show what's left, one per migrator that ran. */
|
|
208
|
+
function next(result) {
|
|
209
|
+
return ["Review the changes with `git diff`. Every finding, with its fix:", ...result.reports.map(({ detection }) => `npx ${detection.migrator} check ${JSON.stringify(root)} --verbose`).map((command) => ` ${command}`)].join("\n");
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
export {};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//#region src/detect.d.ts
|
|
2
|
+
type Library = "remark" | "markdown-it" | "marked";
|
|
3
|
+
/** A Markdown library the project declares, and every package.json dependency that brought it in. */
|
|
4
|
+
interface Detection {
|
|
5
|
+
library: Library;
|
|
6
|
+
/** The npm package that moves this library's projects to Comark. */
|
|
7
|
+
migrator: string;
|
|
8
|
+
uses: Use[];
|
|
9
|
+
}
|
|
10
|
+
/** A Markdown library with no migrator, so the wizard can say so instead of staying silent. */
|
|
11
|
+
interface Unsupported {
|
|
12
|
+
name: string;
|
|
13
|
+
uses: Use[];
|
|
14
|
+
}
|
|
15
|
+
interface Use {
|
|
16
|
+
/** The package.json that declares it, relative to the root. */
|
|
17
|
+
manifest: string;
|
|
18
|
+
dependency: string;
|
|
19
|
+
version: string;
|
|
20
|
+
}
|
|
21
|
+
interface Detected {
|
|
22
|
+
libraries: Detection[];
|
|
23
|
+
unsupported: Unsupported[];
|
|
24
|
+
/** Whether a package.json already declares comark. */
|
|
25
|
+
comark: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** The Markdown libraries every package.json under `root` declares, skipping installed and built files. */
|
|
28
|
+
export declare function detect(root: string): Promise<Detected>;
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/migrate.d.ts
|
|
31
|
+
/** What every migrator's check reports, the shape remark-to-comark set. */
|
|
32
|
+
interface Finding {
|
|
33
|
+
code: string;
|
|
34
|
+
params: Record<string, unknown> & {
|
|
35
|
+
at: string;
|
|
36
|
+
};
|
|
37
|
+
file: string;
|
|
38
|
+
line: number;
|
|
39
|
+
edit?: {
|
|
40
|
+
source: string;
|
|
41
|
+
start: number;
|
|
42
|
+
end: number;
|
|
43
|
+
text: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** The part of a migrator package the wizard drives. Each one exports these from its root. */
|
|
47
|
+
interface Migrator {
|
|
48
|
+
check: (root: string) => Promise<Finding[]>;
|
|
49
|
+
catalog: Record<string, {
|
|
50
|
+
severity: "warn" | "info";
|
|
51
|
+
ruleId: string;
|
|
52
|
+
}>;
|
|
53
|
+
toDiagnostic: (finding: Finding) => {
|
|
54
|
+
why: string;
|
|
55
|
+
fix: string;
|
|
56
|
+
docs?: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
interface Report {
|
|
60
|
+
detection: Detection;
|
|
61
|
+
/** Undefined when the migrator couldn't be loaded or threw; `error` says why. */
|
|
62
|
+
findings?: Finding[];
|
|
63
|
+
error?: string;
|
|
64
|
+
}
|
|
65
|
+
/** One file's change: the migrator that wants it and the edit, against the text the check read. */
|
|
66
|
+
interface Change {
|
|
67
|
+
file: string;
|
|
68
|
+
migrator: string;
|
|
69
|
+
source: string;
|
|
70
|
+
edit: NonNullable<Finding["edit"]>;
|
|
71
|
+
/** A setup rewrite waits for these content files to be written by the same migrator. */
|
|
72
|
+
requires?: string[];
|
|
73
|
+
}
|
|
74
|
+
interface Plan {
|
|
75
|
+
detected: Detected;
|
|
76
|
+
reports: Report[];
|
|
77
|
+
changes: Change[];
|
|
78
|
+
/** Files more than one migrator wants to change, left as they are. */
|
|
79
|
+
conflicts: {
|
|
80
|
+
file: string;
|
|
81
|
+
migrators: string[];
|
|
82
|
+
}[];
|
|
83
|
+
/** Setup files left on their library, because switching them would change how these Markdown files render. */
|
|
84
|
+
held: Held[];
|
|
85
|
+
}
|
|
86
|
+
interface Held {
|
|
87
|
+
file: string;
|
|
88
|
+
migrator: string;
|
|
89
|
+
waiting: string[];
|
|
90
|
+
}
|
|
91
|
+
type Load = (migrator: string) => Promise<Migrator>;
|
|
92
|
+
/** Imports a migrator package by name, from the wizard's own dependencies. */
|
|
93
|
+
export declare const load: Load;
|
|
94
|
+
/**
|
|
95
|
+
* Detects the project's Markdown libraries, runs the migrator of each one in `only` (all by default), and collects the
|
|
96
|
+
* edits their findings carry: the same ones each migrator's `rewrite --write` and `content --write` make.
|
|
97
|
+
*/
|
|
98
|
+
export declare function plan(root: string, options?: {
|
|
99
|
+
only?: string[];
|
|
100
|
+
load?: Load;
|
|
101
|
+
}): Promise<Plan>;
|
|
102
|
+
/**
|
|
103
|
+
* Writes content before dependent setup rewrites, unless a file changed since the check read it.
|
|
104
|
+
* A setup stays unchanged unless all its required content edits were written by that migrator.
|
|
105
|
+
*/
|
|
106
|
+
export declare function apply(root: string, changes: Change[]): {
|
|
107
|
+
written: string[];
|
|
108
|
+
stale: string[];
|
|
109
|
+
held: Held[];
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
export type { Change, Detected, Detection, Finding, Held, Library, Load, Migrator, Plan, Report, Unsupported, Use };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as apply, i as detect, n as load, r as plan } from "./migrate-B1Q-GmiU.mjs";
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { glob } from "tinyglobby";
|
|
4
|
+
//#region src/detect.ts
|
|
5
|
+
const LIBRARIES = [
|
|
6
|
+
{
|
|
7
|
+
library: "remark",
|
|
8
|
+
migrator: "remark-to-comark",
|
|
9
|
+
match: /^(unified|remark|remark-.+|rehype|rehype-.+|mdast-.+|react-markdown|streamdown|@mdx-js\/.+|astro|@astrojs\/(markdown-remark|mdx)|@docusaurus\/core|gatsby-transformer-remark|nextra|fumadocs-core|@nuxt\/content|contentlayer2?|velite)$/
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
library: "markdown-it",
|
|
13
|
+
migrator: "markdown-it-to-comark",
|
|
14
|
+
match: /^(markdown-it|markdown-it-.+|@mdit\/.+|@mdit-vue\/.+|vitepress|vuepress|@vuepress\/.+|@slidev\/cli|@11ty\/eleventy|hexo-renderer-markdown-it|@marp-team\/marp-core|vue-markdown-render)$/
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
library: "marked",
|
|
18
|
+
migrator: "marked-to-comark",
|
|
19
|
+
match: /^(marked|marked-.+|svelte-markdown|ngx-markdown)$/
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
const UNSUPPORTED = /* @__PURE__ */ new Set([
|
|
23
|
+
"showdown",
|
|
24
|
+
"@markdoc/markdoc",
|
|
25
|
+
"markdown-to-jsx",
|
|
26
|
+
"commonmark",
|
|
27
|
+
"snarkdown",
|
|
28
|
+
"markdown-wasm"
|
|
29
|
+
]);
|
|
30
|
+
const FIELDS = [
|
|
31
|
+
"dependencies",
|
|
32
|
+
"devDependencies",
|
|
33
|
+
"peerDependencies",
|
|
34
|
+
"optionalDependencies"
|
|
35
|
+
];
|
|
36
|
+
/** The Markdown libraries every package.json under `root` declares, skipping installed and built files. */
|
|
37
|
+
async function detect(root) {
|
|
38
|
+
const manifests = (await glob("**/package.json", {
|
|
39
|
+
cwd: root,
|
|
40
|
+
ignore: [
|
|
41
|
+
"**/node_modules/**",
|
|
42
|
+
"**/dist/**",
|
|
43
|
+
"**/.output/**",
|
|
44
|
+
"**/.nuxt/**",
|
|
45
|
+
"**/.next/**"
|
|
46
|
+
]
|
|
47
|
+
})).sort();
|
|
48
|
+
const found = /* @__PURE__ */ new Map();
|
|
49
|
+
const unsupported = /* @__PURE__ */ new Map();
|
|
50
|
+
let comark = false;
|
|
51
|
+
for (const manifest of manifests) {
|
|
52
|
+
let json;
|
|
53
|
+
try {
|
|
54
|
+
json = JSON.parse(readFileSync(join(root, manifest), "utf8"));
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
for (const field of FIELDS) for (const [dependency, version] of Object.entries(json[field] ?? {})) {
|
|
59
|
+
const use = {
|
|
60
|
+
manifest,
|
|
61
|
+
dependency,
|
|
62
|
+
version: String(version)
|
|
63
|
+
};
|
|
64
|
+
if (dependency === "comark") comark = true;
|
|
65
|
+
if (UNSUPPORTED.has(dependency)) unsupported.set(dependency, [...unsupported.get(dependency) ?? [], use]);
|
|
66
|
+
for (const { library, match } of LIBRARIES) if (match.test(dependency)) found.set(library, [...found.get(library) ?? [], use]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
libraries: LIBRARIES.flatMap(({ library, migrator }) => {
|
|
71
|
+
const uses = found.get(library);
|
|
72
|
+
return uses ? [{
|
|
73
|
+
library,
|
|
74
|
+
migrator,
|
|
75
|
+
uses
|
|
76
|
+
}] : [];
|
|
77
|
+
}),
|
|
78
|
+
unsupported: [...unsupported].map(([name, uses]) => ({
|
|
79
|
+
name,
|
|
80
|
+
uses
|
|
81
|
+
})),
|
|
82
|
+
comark
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/migrate.ts
|
|
87
|
+
/** Imports a migrator package by name, from the wizard's own dependencies. */
|
|
88
|
+
const load = async (migrator) => {
|
|
89
|
+
return await import(migrator);
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Detects the project's Markdown libraries, runs the migrator of each one in `only` (all by default), and collects the
|
|
93
|
+
* edits their findings carry: the same ones each migrator's `rewrite --write` and `content --write` make.
|
|
94
|
+
*/
|
|
95
|
+
async function plan(root, options = {}) {
|
|
96
|
+
const detected = await detect(root);
|
|
97
|
+
const reports = [];
|
|
98
|
+
for (const detection of detected.libraries) {
|
|
99
|
+
if (options.only && !options.only.includes(detection.library)) continue;
|
|
100
|
+
try {
|
|
101
|
+
const migrator = await (options.load ?? load)(detection.migrator);
|
|
102
|
+
reports.push({
|
|
103
|
+
detection,
|
|
104
|
+
findings: await migrator.check(root)
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
reports.push({
|
|
108
|
+
detection,
|
|
109
|
+
error: String(error instanceof Error ? error.message : error).split("\n")[0]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
114
|
+
const held = [];
|
|
115
|
+
for (const { detection, findings } of reports) {
|
|
116
|
+
const unresolved = [...new Set(findings?.filter((finding) => finding.code.endsWith("0303") || finding.code.endsWith("0302") && !finding.edit).map((finding) => finding.file))];
|
|
117
|
+
const contentFiles = [...new Set(findings?.filter((finding) => finding.code.endsWith("0302")).map((finding) => finding.file))];
|
|
118
|
+
for (const finding of findings ?? []) {
|
|
119
|
+
if (!finding.edit) continue;
|
|
120
|
+
if (unresolved.length && finding.code.endsWith("0202")) {
|
|
121
|
+
if (!held.some((entry) => entry.file === finding.file && entry.migrator === detection.migrator)) held.push({
|
|
122
|
+
file: finding.file,
|
|
123
|
+
migrator: detection.migrator,
|
|
124
|
+
waiting: unresolved
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const changes = wanted.get(finding.file) ?? [];
|
|
129
|
+
if (changes.some((change) => change.migrator === detection.migrator && same(change.edit, finding.edit))) continue;
|
|
130
|
+
changes.push({
|
|
131
|
+
file: finding.file,
|
|
132
|
+
migrator: detection.migrator,
|
|
133
|
+
source: finding.edit.source,
|
|
134
|
+
edit: finding.edit,
|
|
135
|
+
requires: finding.code.endsWith("0202") ? contentFiles : void 0
|
|
136
|
+
});
|
|
137
|
+
wanted.set(finding.file, changes);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const changes = [];
|
|
141
|
+
const conflicts = [];
|
|
142
|
+
for (const [file, list] of [...wanted].sort(([a], [b]) => a.localeCompare(b))) {
|
|
143
|
+
const owned = list.length === 1 ? list[0] : owner(file, list, detected);
|
|
144
|
+
if (owned) changes.push(owned);
|
|
145
|
+
else conflicts.push({
|
|
146
|
+
file,
|
|
147
|
+
migrators: [...new Set(list.map((change) => change.migrator))]
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
for (const change of changes.filter((change) => change.requires)) {
|
|
151
|
+
const waiting = change.requires.filter((file) => !changes.some((other) => other.file === file && other.migrator === change.migrator));
|
|
152
|
+
if (!waiting.length) continue;
|
|
153
|
+
changes.splice(changes.indexOf(change), 1);
|
|
154
|
+
held.push({
|
|
155
|
+
file: change.file,
|
|
156
|
+
migrator: change.migrator,
|
|
157
|
+
waiting
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
detected,
|
|
162
|
+
reports,
|
|
163
|
+
changes,
|
|
164
|
+
conflicts,
|
|
165
|
+
held
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* In a monorepo every migrator wants every Markdown file, so a file goes to the library its nearest declaring
|
|
170
|
+
* package.json uses, such as a VitePress site's own package. A package.json that declares several of them decides
|
|
171
|
+
* nothing.
|
|
172
|
+
*/
|
|
173
|
+
function owner(file, list, detected) {
|
|
174
|
+
const declared = /* @__PURE__ */ new Map();
|
|
175
|
+
for (const { migrator, uses } of detected.libraries) for (const { manifest } of uses) declared.set(manifest, /* @__PURE__ */ new Set([...declared.get(manifest) ?? [], migrator]));
|
|
176
|
+
for (let dir = dirname(file);; dir = dirname(dir)) {
|
|
177
|
+
const manifest = dir === "." ? "package.json" : `${dir}/package.json`;
|
|
178
|
+
const competing = list.filter((change) => declared.get(manifest)?.has(change.migrator));
|
|
179
|
+
if (competing.length) return competing.length === 1 ? competing[0] : void 0;
|
|
180
|
+
if (dir === "." || dir === dirname(dir)) return void 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const same = (a, b) => a.start === b.start && a.end === b.end && a.text === b.text;
|
|
184
|
+
/**
|
|
185
|
+
* Writes content before dependent setup rewrites, unless a file changed since the check read it.
|
|
186
|
+
* A setup stays unchanged unless all its required content edits were written by that migrator.
|
|
187
|
+
*/
|
|
188
|
+
function apply(root, changes) {
|
|
189
|
+
const written = [];
|
|
190
|
+
const stale = [];
|
|
191
|
+
const held = [];
|
|
192
|
+
const applied = /* @__PURE__ */ new Map();
|
|
193
|
+
const ordered = [...changes.filter((change) => !change.requires), ...changes.filter((change) => change.requires)];
|
|
194
|
+
for (const { file, source, edit, migrator, requires } of ordered) {
|
|
195
|
+
const waiting = requires?.filter((required) => !applied.get(migrator)?.has(required));
|
|
196
|
+
if (waiting?.length) {
|
|
197
|
+
held.push({
|
|
198
|
+
file,
|
|
199
|
+
migrator,
|
|
200
|
+
waiting
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const path = join(root, file);
|
|
205
|
+
if (readFileSync(path, "utf8") !== source) {
|
|
206
|
+
stale.push(file);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
writeFileSync(path, source.slice(0, edit.start) + edit.text + source.slice(edit.end));
|
|
210
|
+
written.push(file);
|
|
211
|
+
const files = applied.get(migrator) ?? /* @__PURE__ */ new Set();
|
|
212
|
+
files.add(file);
|
|
213
|
+
applied.set(migrator, files);
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
written,
|
|
217
|
+
stale,
|
|
218
|
+
held
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
export { detect as i, load as n, plan as r, apply as t };
|
package/package.json
CHANGED
|
@@ -1,11 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "migrate-to-comark",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"keywords": [
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "A wizard that finds the Markdown libraries a project uses (remark, markdown-it, marked) and moves it to Comark with each library's migrator.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"codemod",
|
|
7
|
+
"comark",
|
|
8
|
+
"markdown",
|
|
9
|
+
"markdown-it",
|
|
10
|
+
"marked",
|
|
11
|
+
"migration",
|
|
12
|
+
"remark",
|
|
13
|
+
"wizard"
|
|
14
|
+
],
|
|
6
15
|
"homepage": "https://comark-ui.onmax.me/migrate-to-comark",
|
|
7
16
|
"bugs": "https://github.com/onmax/comark-ui/issues",
|
|
8
17
|
"license": "MIT",
|
|
9
18
|
"author": "onmax",
|
|
10
|
-
"repository": {
|
|
11
|
-
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/onmax/comark-ui.git",
|
|
22
|
+
"directory": "packages/migrate-to-comark"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"migrate-to-comark": "./dist/cli.mjs"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./dist/index.mjs",
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"provenance": true
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@clack/prompts": "^1.8.1",
|
|
42
|
+
"tinyglobby": "^0.2.17",
|
|
43
|
+
"marked-to-comark": "0.9.0",
|
|
44
|
+
"remark-to-comark": "0.9.0",
|
|
45
|
+
"markdown-it-to-comark": "0.9.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"comark": "^0.8.1"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"comark": "^0.8.0"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=22.12.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsdown",
|
|
58
|
+
"dev": "tsdown --watch",
|
|
59
|
+
"doctor": "vite-doctor . --no-cache --max-warnings 0 --format text"
|
|
60
|
+
}
|
|
61
|
+
}
|