@savvy-web/changesets 0.1.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.
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env node
2
+ import { Args, Command, Options } from "@effect/cli";
3
+ import { NodeContext, NodeRuntime } from "@effect/platform-node";
4
+ import { execSync } from "node:child_process";
5
+ import { findProjectRoot, getWorkspaceInfos } from "workspace-tools";
6
+ import { readFileSync, ChangelogTransformer, ChangesetLinter, existsSync, resolve, mkdirSync, writeFileSync, join } from "../273.js";
7
+ import { Data, Effect } from "../245.js";
8
+ const dirArg = Args.directory({
9
+ name: "dir"
10
+ }).pipe(Args.withDefault(".changeset"));
11
+ function runCheck(dir) {
12
+ return Effect.gen(function*() {
13
+ const resolved = resolve(dir);
14
+ const messages = yield* Effect["try"](()=>ChangesetLinter.validate(resolved));
15
+ const byFile = new Map();
16
+ for (const msg of messages){
17
+ const existing = byFile.get(msg.file);
18
+ if (existing) existing.push(msg);
19
+ else byFile.set(msg.file, [
20
+ msg
21
+ ]);
22
+ }
23
+ for (const [file, fileMessages] of byFile){
24
+ yield* Effect.log(`\n${file}`);
25
+ for (const msg of fileMessages)yield* Effect.log(` ${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
26
+ }
27
+ const errorCount = messages.length;
28
+ const filesWithErrors = byFile.size;
29
+ if (errorCount > 0) {
30
+ yield* Effect.log(`\n${filesWithErrors} file(s) with errors, ${errorCount} error(s) found`);
31
+ process.exitCode = 1;
32
+ } else yield* Effect.log("All changeset files passed validation.");
33
+ });
34
+ }
35
+ const checkCommand = Command.make("check", {
36
+ dir: dirArg
37
+ }, ({ dir })=>runCheck(dir)).pipe(Command.withDescription("Full changeset validation with summary"));
38
+ const CUSTOM_RULES_ENTRY = "@savvy-web/changesets/markdownlint";
39
+ const CHANGELOG_ENTRY = "@savvy-web/changesets/changelog";
40
+ const BASE_CONFIG_PATH = "lib/configs/.markdownlint-cli2.jsonc";
41
+ const RULE_NAMES = [
42
+ "changeset-heading-hierarchy",
43
+ "changeset-required-sections",
44
+ "changeset-content-structure"
45
+ ];
46
+ const DEFAULT_CONFIG = {
47
+ $schema: "https://unpkg.com/@changesets/config@3.1.1/schema.json",
48
+ changelog: [
49
+ CHANGELOG_ENTRY,
50
+ {
51
+ repo: "owner/repo"
52
+ }
53
+ ],
54
+ commit: false,
55
+ access: "restricted",
56
+ baseBranch: "main",
57
+ updateInternalDependencies: "patch",
58
+ ignore: [],
59
+ privatePackages: {
60
+ tag: true,
61
+ version: true
62
+ }
63
+ };
64
+ const InitErrorBase = Data.TaggedError("InitError");
65
+ class InitError extends InitErrorBase {
66
+ get message() {
67
+ return `Init failed at ${this.step}: ${this.reason}`;
68
+ }
69
+ }
70
+ const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite existing config files"), Options.withDefault(false));
71
+ const quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Silence warnings, always exit 0"), Options.withDefault(false));
72
+ const markdownlintOption = Options.boolean("markdownlint").pipe(Options.withDescription("Register rules in base markdownlint config"), Options.withDefault(true));
73
+ function detectGitHubRepo(cwd) {
74
+ try {
75
+ const url = execSync("git remote get-url origin", {
76
+ cwd,
77
+ encoding: "utf-8"
78
+ }).trim();
79
+ const https = url.match(/github\.com\/([^/]+)\/([^/.]+)/);
80
+ if (https) return `${https[1]}/${https[2]}`;
81
+ const ssh = url.match(/github\.com:([^/]+)\/([^/.]+)/);
82
+ if (ssh) return `${ssh[1]}/${ssh[2]}`;
83
+ } catch {}
84
+ return null;
85
+ }
86
+ function stripJsoncComments(text) {
87
+ return text.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
88
+ }
89
+ function resolveWorkspaceRoot(cwd) {
90
+ return findProjectRoot(cwd) ?? cwd;
91
+ }
92
+ function ensureChangesetDir(root) {
93
+ return Effect["try"]({
94
+ try: ()=>{
95
+ const dir = join(root, ".changeset");
96
+ mkdirSync(dir, {
97
+ recursive: true
98
+ });
99
+ return dir;
100
+ },
101
+ catch: (error)=>new InitError({
102
+ step: ".changeset directory",
103
+ reason: error instanceof Error ? error.message : String(error)
104
+ })
105
+ });
106
+ }
107
+ function handleConfig(changesetDir, repoSlug, force) {
108
+ return Effect["try"]({
109
+ try: ()=>{
110
+ const configPath = join(changesetDir, "config.json");
111
+ if (force || !existsSync(configPath)) {
112
+ const config = {
113
+ ...DEFAULT_CONFIG,
114
+ changelog: [
115
+ CHANGELOG_ENTRY,
116
+ {
117
+ repo: repoSlug
118
+ }
119
+ ]
120
+ };
121
+ writeFileSync(configPath, `${JSON.stringify(config, null, "\t")}\n`);
122
+ return force ? "Overwrote .changeset/config.json" : "Created .changeset/config.json";
123
+ }
124
+ const existing = JSON.parse(readFileSync(configPath, "utf-8"));
125
+ existing.changelog = [
126
+ CHANGELOG_ENTRY,
127
+ {
128
+ repo: repoSlug
129
+ }
130
+ ];
131
+ writeFileSync(configPath, `${JSON.stringify(existing, null, "\t")}\n`);
132
+ return "Patched changelog in .changeset/config.json";
133
+ },
134
+ catch: (error)=>new InitError({
135
+ step: ".changeset/config.json",
136
+ reason: error instanceof Error ? error.message : String(error)
137
+ })
138
+ });
139
+ }
140
+ function handleBaseMarkdownlint(root) {
141
+ return Effect["try"]({
142
+ try: ()=>{
143
+ const baseConfigPath = join(root, BASE_CONFIG_PATH);
144
+ if (!existsSync(baseConfigPath)) return null;
145
+ const raw = readFileSync(baseConfigPath, "utf-8");
146
+ const parsed = JSON.parse(stripJsoncComments(raw));
147
+ if (!Array.isArray(parsed.customRules)) parsed.customRules = [];
148
+ if (!parsed.customRules.includes(CUSTOM_RULES_ENTRY)) parsed.customRules.push(CUSTOM_RULES_ENTRY);
149
+ if ("object" != typeof parsed.config || null === parsed.config) parsed.config = {};
150
+ for (const rule of RULE_NAMES)if (!(rule in parsed.config)) parsed.config[rule] = false;
151
+ writeFileSync(baseConfigPath, `${JSON.stringify(parsed, null, "\t")}\n`);
152
+ return "Updated lib/configs/.markdownlint-cli2.jsonc";
153
+ },
154
+ catch: (error)=>new InitError({
155
+ step: "lib/configs/.markdownlint-cli2.jsonc",
156
+ reason: error instanceof Error ? error.message : String(error)
157
+ })
158
+ });
159
+ }
160
+ function handleChangesetMarkdownlint(changesetDir, root, force) {
161
+ return Effect["try"]({
162
+ try: ()=>{
163
+ const mdlintPath = join(changesetDir, ".markdownlint.json");
164
+ const hasBaseConfig = existsSync(join(root, BASE_CONFIG_PATH));
165
+ if (force || !existsSync(mdlintPath)) {
166
+ const mdlintConfig = {};
167
+ if (hasBaseConfig) mdlintConfig.extends = `../${BASE_CONFIG_PATH}`;
168
+ mdlintConfig.default = false;
169
+ mdlintConfig.MD041 = false;
170
+ for (const rule of RULE_NAMES)mdlintConfig[rule] = true;
171
+ writeFileSync(mdlintPath, `${JSON.stringify(mdlintConfig, null, "\t")}\n`);
172
+ return force ? "Overwrote .changeset/.markdownlint.json" : "Created .changeset/.markdownlint.json";
173
+ }
174
+ const existing = JSON.parse(readFileSync(mdlintPath, "utf-8"));
175
+ for (const rule of RULE_NAMES)existing[rule] = true;
176
+ writeFileSync(mdlintPath, `${JSON.stringify(existing, null, "\t")}\n`);
177
+ return "Patched rules in .changeset/.markdownlint.json";
178
+ },
179
+ catch: (error)=>new InitError({
180
+ step: ".changeset/.markdownlint.json",
181
+ reason: error instanceof Error ? error.message : String(error)
182
+ })
183
+ });
184
+ }
185
+ const initCommand = Command.make("init", {
186
+ force: forceOption,
187
+ quiet: quietOption,
188
+ markdownlint: markdownlintOption
189
+ }, ({ force, quiet, markdownlint })=>Effect.gen(function*() {
190
+ const root = resolveWorkspaceRoot(process.cwd());
191
+ const repo = detectGitHubRepo(root);
192
+ if (!repo && !quiet) yield* Effect.log("Warning: could not detect GitHub repo from git remote, using placeholder");
193
+ const repoSlug = repo ?? "owner/repo";
194
+ const changesetDir = yield* ensureChangesetDir(root);
195
+ yield* Effect.log("Ensured .changeset/ directory");
196
+ const errors = [];
197
+ const configResult = yield* handleConfig(changesetDir, repoSlug, force).pipe(Effect.either);
198
+ if ("Right" === configResult._tag) yield* Effect.log(configResult.right);
199
+ else errors.push(configResult.left);
200
+ if (markdownlint) {
201
+ const baseResult = yield* handleBaseMarkdownlint(root).pipe(Effect.either);
202
+ if ("Right" === baseResult._tag) {
203
+ if (baseResult.right) yield* Effect.log(baseResult.right);
204
+ } else errors.push(baseResult.left);
205
+ }
206
+ const mdlintResult = yield* handleChangesetMarkdownlint(changesetDir, root, force).pipe(Effect.either);
207
+ if ("Right" === mdlintResult._tag) yield* Effect.log(mdlintResult.right);
208
+ else errors.push(mdlintResult.left);
209
+ if (errors.length > 0) {
210
+ for (const err of errors)yield* Effect.logError(err.message);
211
+ if (!quiet) process.exitCode = 1;
212
+ return;
213
+ }
214
+ yield* Effect.log("Init complete.");
215
+ }).pipe(Effect.catchAll((error)=>Effect.gen(function*() {
216
+ if (!quiet) {
217
+ yield* Effect.logError(error instanceof InitError ? error.message : `Init failed: ${String(error)}`);
218
+ process.exitCode = 1;
219
+ }
220
+ })))).pipe(Command.withDescription("Bootstrap a repo for @savvy-web/changesets"));
221
+ const lint_dirArg = Args.directory({
222
+ name: "dir"
223
+ }).pipe(Args.withDefault(".changeset"));
224
+ const lint_quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Only output errors, no summary"), Options.withDefault(false));
225
+ function runLint(dir, quiet) {
226
+ return Effect.gen(function*() {
227
+ const resolved = resolve(dir);
228
+ const messages = yield* Effect["try"](()=>ChangesetLinter.validate(resolved));
229
+ for (const msg of messages)yield* Effect.log(`${msg.file}:${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
230
+ if (!quiet && 0 === messages.length) yield* Effect.log("No lint errors found.");
231
+ if (messages.length > 0) process.exitCode = 1;
232
+ });
233
+ }
234
+ const lintCommand = Command.make("lint", {
235
+ dir: lint_dirArg,
236
+ quiet: lint_quietOption
237
+ }, ({ dir, quiet })=>runLint(dir, quiet)).pipe(Command.withDescription("Validate changeset files"));
238
+ const fileArg = Args.file({
239
+ name: "file"
240
+ }).pipe(Args.withDefault("CHANGELOG.md"));
241
+ const dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDescription("Print transformed output instead of writing"), Options.withDefault(false));
242
+ const checkOption = Options.boolean("check").pipe(Options.withAlias("c"), Options.withDescription("Exit 1 if file would change (for CI)"), Options.withDefault(false));
243
+ function runTransform(file, dryRun, check) {
244
+ return Effect.gen(function*() {
245
+ const resolved = resolve(file);
246
+ const content = yield* Effect["try"](()=>readFileSync(resolved, "utf-8"));
247
+ const result = ChangelogTransformer.transformContent(content);
248
+ if (dryRun) return void (yield* Effect.log(result));
249
+ if (check) {
250
+ if (result !== content) {
251
+ yield* Effect.log(`${resolved} would be modified by transform.`);
252
+ process.exitCode = 1;
253
+ } else yield* Effect.log(`${resolved} is already formatted.`);
254
+ return;
255
+ }
256
+ yield* Effect["try"](()=>writeFileSync(resolved, result, "utf-8"));
257
+ yield* Effect.log(`Transformed ${resolved}`);
258
+ });
259
+ }
260
+ const transformCommand = Command.make("transform", {
261
+ file: fileArg,
262
+ dryRun: dryRunOption,
263
+ check: checkOption
264
+ }, ({ file, dryRun, check })=>runTransform(file, dryRun, check)).pipe(Command.withDescription("Post-process CHANGELOG.md"));
265
+ class Workspace {
266
+ static detectPackageManager(cwd = process.cwd()) {
267
+ const packageJsonPath = join(cwd, "package.json");
268
+ if (!existsSync(packageJsonPath)) return "npm";
269
+ try {
270
+ const content = readFileSync(packageJsonPath, "utf-8");
271
+ const pkg = JSON.parse(content);
272
+ if (pkg.packageManager) {
273
+ const match = pkg.packageManager.match(/^(npm|pnpm|yarn|bun)@/);
274
+ if (match) return match[1];
275
+ }
276
+ } catch {}
277
+ return "npm";
278
+ }
279
+ static getChangesetVersionCommand(pm) {
280
+ switch(pm){
281
+ case "pnpm":
282
+ return "pnpm exec changeset version";
283
+ case "yarn":
284
+ return "yarn exec changeset version";
285
+ case "bun":
286
+ return "bun x changeset version";
287
+ default:
288
+ return "npx changeset version";
289
+ }
290
+ }
291
+ static discoverChangelogs(cwd = process.cwd()) {
292
+ const resolvedCwd = resolve(cwd);
293
+ const results = [];
294
+ const seen = new Set();
295
+ try {
296
+ const workspaces = getWorkspaceInfos(resolvedCwd) ?? [];
297
+ for (const ws of workspaces){
298
+ const changelogPath = join(ws.path, "CHANGELOG.md");
299
+ if (existsSync(changelogPath) && !seen.has(ws.path)) {
300
+ seen.add(ws.path);
301
+ results.push({
302
+ name: ws.name,
303
+ path: ws.path,
304
+ changelogPath
305
+ });
306
+ }
307
+ }
308
+ } catch {}
309
+ if (!seen.has(resolvedCwd)) {
310
+ const rootChangelog = join(resolvedCwd, "CHANGELOG.md");
311
+ if (existsSync(rootChangelog)) {
312
+ let rootName = "root";
313
+ try {
314
+ const pkg = JSON.parse(readFileSync(join(resolvedCwd, "package.json"), "utf-8"));
315
+ if (pkg.name) rootName = pkg.name;
316
+ } catch {}
317
+ results.push({
318
+ name: rootName,
319
+ path: resolvedCwd,
320
+ changelogPath: rootChangelog
321
+ });
322
+ }
323
+ }
324
+ return results;
325
+ }
326
+ }
327
+ const version_dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDescription("Skip changeset version, only transform existing CHANGELOGs"), Options.withDefault(false));
328
+ function runVersion(dryRun) {
329
+ return Effect.gen(function*() {
330
+ const cwd = process.cwd();
331
+ const pm = Workspace.detectPackageManager(cwd);
332
+ yield* Effect.log(`Detected package manager: ${pm}`);
333
+ if (dryRun) yield* Effect.log("Dry run: skipping changeset version");
334
+ else {
335
+ const cmd = Workspace.getChangesetVersionCommand(pm);
336
+ yield* Effect.log(`Running: ${cmd}`);
337
+ yield* Effect["try"]({
338
+ try: ()=>execSync(cmd, {
339
+ cwd,
340
+ stdio: "inherit"
341
+ }),
342
+ catch: (error)=>new Error(`changeset version failed: ${error instanceof Error ? error.message : String(error)}`)
343
+ });
344
+ }
345
+ const changelogs = Workspace.discoverChangelogs(cwd);
346
+ if (0 === changelogs.length) return void (yield* Effect.log("No CHANGELOG.md files found."));
347
+ yield* Effect.log(`Found ${changelogs.length} CHANGELOG.md file(s)`);
348
+ for (const entry of changelogs){
349
+ yield* Effect["try"]({
350
+ try: ()=>ChangelogTransformer.transformFile(entry.changelogPath),
351
+ catch: (error)=>new Error(`Failed to transform ${entry.changelogPath}: ${error instanceof Error ? error.message : String(error)}`)
352
+ });
353
+ yield* Effect.log(`Transformed ${entry.name} → ${entry.changelogPath}`);
354
+ }
355
+ });
356
+ }
357
+ const versionCommand = Command.make("version", {
358
+ dryRun: version_dryRunOption
359
+ }, ({ dryRun })=>runVersion(dryRun)).pipe(Command.withDescription("Run changeset version and transform all CHANGELOGs"));
360
+ const rootCommand = Command.make("savvy-changesets").pipe(Command.withSubcommands([
361
+ initCommand,
362
+ lintCommand,
363
+ transformCommand,
364
+ checkCommand,
365
+ versionCommand
366
+ ]));
367
+ const cli = Command.run(rootCommand, {
368
+ name: "savvy-changesets",
369
+ version: "0.1.0"
370
+ });
371
+ function runCli() {
372
+ const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(NodeContext.layer));
373
+ NodeRuntime.runMain(main);
374
+ }
375
+ runCli();
package/changelog.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Changesets API changelog formatter.
3
+ *
4
+ * This module exports the `ChangelogFunctions` required by the Changesets API.
5
+ * Configure in `.changeset/config.json`:
6
+ *
7
+ * ```json
8
+ * {
9
+ * "changelog": ["\@savvy-web/changesets/changelog", { "repo": "savvy-web/package-name" }]
10
+ * }
11
+ * ```
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
16
+ import { ChangelogFunctions } from '@changesets/types';
17
+
18
+ declare const changelogFunctions: ChangelogFunctions;
19
+ export default changelogFunctions;
20
+
21
+ export { }
package/changelog.js ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./160.js";