@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.
package/160.js ADDED
@@ -0,0 +1,404 @@
1
+ import { getInfo } from "@changesets/get-github-info";
2
+ import { Layer, unified, Schema, remark_stringify, Data, Effect, remark_gfm, remark_parse, Context } from "./245.js";
3
+ import { external_mdast_util_to_string_toString } from "./689.js";
4
+ import { resolveCommitType, fromHeading } from "./60.js";
5
+ const ChangesetValidationErrorBase = Data.TaggedError("ChangesetValidationError");
6
+ class ChangesetValidationError extends ChangesetValidationErrorBase {
7
+ get message() {
8
+ const prefix = this.file ? `${this.file}: ` : "";
9
+ const detail = this.issues.map((i)=>` - ${i.path}: ${i.message}`).join("\n");
10
+ return `${prefix}Changeset validation failed:\n${detail}`;
11
+ }
12
+ }
13
+ const GitHubApiErrorBase = Data.TaggedError("GitHubApiError");
14
+ class GitHubApiError extends GitHubApiErrorBase {
15
+ get message() {
16
+ const status = this.statusCode ? ` (${this.statusCode})` : "";
17
+ return `GitHub API error during ${this.operation}${status}: ${this.reason}`;
18
+ }
19
+ get isRateLimited() {
20
+ return 403 === this.statusCode || 429 === this.statusCode;
21
+ }
22
+ get isRetryable() {
23
+ return void 0 !== this.statusCode && (this.statusCode >= 500 || this.isRateLimited);
24
+ }
25
+ }
26
+ const MarkdownParseErrorBase = Data.TaggedError("MarkdownParseError");
27
+ class MarkdownParseError extends MarkdownParseErrorBase {
28
+ get message() {
29
+ const loc = this.line ? `:${this.line}${this.column ? `:${this.column}` : ""}` : "";
30
+ const src = this.source ? `${this.source}${loc}: ` : "";
31
+ return `${src}Markdown parse error: ${this.reason}`;
32
+ }
33
+ }
34
+ const ConfigurationErrorBase = Data.TaggedError("ConfigurationError");
35
+ class ConfigurationError extends ConfigurationErrorBase {
36
+ get message() {
37
+ return `Configuration error (${this.field}): ${this.reason}`;
38
+ }
39
+ }
40
+ const REPO_PATTERN = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/;
41
+ const RepoSchema = Schema.String.pipe(Schema.pattern(REPO_PATTERN, {
42
+ message: ()=>'Repository must be in format "owner/repository" (e.g., "microsoft/vscode")'
43
+ }));
44
+ const ChangesetOptionsSchema = Schema.Struct({
45
+ repo: RepoSchema,
46
+ commitLinks: Schema.optional(Schema.Boolean),
47
+ prLinks: Schema.optional(Schema.Boolean),
48
+ issueLinks: Schema.optional(Schema.Boolean),
49
+ issuePrefixes: Schema.optional(Schema.Array(Schema.String))
50
+ });
51
+ function validateChangesetOptions(input) {
52
+ if (null == input) return Effect.fail(new ConfigurationError({
53
+ field: "options",
54
+ reason: 'Configuration is required. Add options to your changesets config:\n"changelog": ["@savvy-web/changesets", { "repo": "owner/repository" }]'
55
+ }));
56
+ if ("object" != typeof input) return Effect.fail(new ConfigurationError({
57
+ field: "options",
58
+ reason: "Configuration must be an object"
59
+ }));
60
+ const obj = input;
61
+ if (!("repo" in obj) || void 0 === obj.repo) return Effect.fail(new ConfigurationError({
62
+ field: "repo",
63
+ reason: 'Repository name is required. Add the "repo" option to your changesets config:\n"changelog": ["@savvy-web/changesets", { "repo": "owner/repository" }]'
64
+ }));
65
+ if ("string" == typeof obj.repo && !REPO_PATTERN.test(obj.repo)) return Effect.fail(new ConfigurationError({
66
+ field: "repo",
67
+ reason: `Invalid repository format: "${obj.repo}". Expected format is "owner/repository" (e.g., "microsoft/vscode")`
68
+ }));
69
+ return Schema.decodeUnknown(ChangesetOptionsSchema)(input).pipe(Effect.mapError((parseError)=>new ConfigurationError({
70
+ field: "options",
71
+ reason: String(parseError)
72
+ })));
73
+ }
74
+ function getGitHubInfo(params) {
75
+ return Effect.tryPromise({
76
+ try: ()=>getInfo({
77
+ commit: params.commit,
78
+ repo: params.repo
79
+ }),
80
+ catch: (error)=>new GitHubApiError({
81
+ operation: "getInfo",
82
+ reason: error instanceof Error ? error.message : String(error)
83
+ })
84
+ });
85
+ }
86
+ const _tag = Context.Tag("GitHubService");
87
+ const GitHubServiceBase = _tag();
88
+ class GitHubService extends GitHubServiceBase {
89
+ }
90
+ const GitHubLive = Layer.succeed(GitHubService, {
91
+ getInfo: getGitHubInfo
92
+ });
93
+ function makeGitHubTest(responses) {
94
+ return Layer.succeed(GitHubService, {
95
+ getInfo: (params)=>{
96
+ const info = responses.get(params.commit);
97
+ if (info) return Effect.succeed(info);
98
+ return Effect.fail(new GitHubApiError({
99
+ operation: "getInfo",
100
+ reason: `No mock response for commit ${params.commit}`
101
+ }));
102
+ }
103
+ });
104
+ }
105
+ function createRemarkProcessor() {
106
+ return unified().use(remark_parse).use(remark_gfm).use(remark_stringify);
107
+ }
108
+ function parseMarkdown(content) {
109
+ const processor = createRemarkProcessor();
110
+ return processor.parse(content);
111
+ }
112
+ function stringifyMarkdown(tree) {
113
+ const processor = createRemarkProcessor();
114
+ return processor.stringify(tree);
115
+ }
116
+ const markdown_tag = Context.Tag("MarkdownService");
117
+ const MarkdownServiceBase = markdown_tag();
118
+ class MarkdownService extends MarkdownServiceBase {
119
+ }
120
+ const MarkdownLive = Layer.succeed(MarkdownService, {
121
+ parse: (content)=>Effect.sync(()=>parseMarkdown(content)),
122
+ stringify: (tree)=>Effect.sync(()=>stringifyMarkdown(tree))
123
+ });
124
+ function logWarning(message, ...args) {
125
+ if ("u" > typeof process && "true" === process.env.GITHUB_ACTIONS) {
126
+ const text = args.length > 0 ? `${message} ${args.join(" ")}` : message;
127
+ console.warn(`::warning::${text}`);
128
+ } else console.warn(message, ...args);
129
+ }
130
+ function getDependencyReleaseLine(changesets, dependenciesUpdated, options) {
131
+ return Effect.gen(function*() {
132
+ if (0 === dependenciesUpdated.length) return "";
133
+ const github = yield* GitHubService;
134
+ let apiFailures = 0;
135
+ const totalWithCommit = changesets.filter((cs)=>cs.commit).length;
136
+ const commitLinks = yield* Effect.forEach(changesets, (cs)=>{
137
+ const commit = cs.commit;
138
+ if (!commit) return Effect.succeed(null);
139
+ return github.getInfo({
140
+ commit,
141
+ repo: options.repo
142
+ }).pipe(Effect.map((info)=>info.links.commit), Effect.catchAll((error)=>{
143
+ apiFailures++;
144
+ logWarning(`Failed to fetch GitHub info for commit ${commit}:`, String(error));
145
+ return Effect.succeed(`[\`${commit.substring(0, 7)}\`](https://github.com/${options.repo}/commit/${commit})`);
146
+ }));
147
+ }, {
148
+ concurrency: 10
149
+ });
150
+ if (apiFailures > 0) {
151
+ const successRate = ((totalWithCommit - apiFailures) / totalWithCommit * 100).toFixed(1);
152
+ logWarning(`GitHub API calls completed with ${apiFailures}/${totalWithCommit} failures (${successRate}% success rate)`);
153
+ }
154
+ const validLinks = commitLinks.filter(Boolean);
155
+ const changesetLink = validLinks.length > 0 ? `- Updated dependencies [${validLinks.join(", ")}]:` : "- Updated dependencies:";
156
+ const updatedDependenciesList = dependenciesUpdated.map((dep)=>` - ${dep.name}@${dep.newVersion}`);
157
+ return [
158
+ changesetLink,
159
+ ...updatedDependenciesList
160
+ ].join("\n");
161
+ });
162
+ }
163
+ const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/;
164
+ function extractUrlFromMarkdown(linkOrUrl) {
165
+ const match = MARKDOWN_LINK_PATTERN.exec(linkOrUrl);
166
+ return match ? match[2] : linkOrUrl;
167
+ }
168
+ const NonEmptyString = Schema.String.pipe(Schema.minLength(1));
169
+ const PositiveInteger = Schema.Number.pipe(Schema.int(), Schema.positive());
170
+ function isValidUrl(value) {
171
+ try {
172
+ new URL(value);
173
+ return true;
174
+ } catch {
175
+ return false;
176
+ }
177
+ }
178
+ const UsernameSchema = Schema.String.pipe(Schema.pattern(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/, {
179
+ message: ()=>"Invalid GitHub username format"
180
+ }));
181
+ const IssueNumberSchema = PositiveInteger.annotations({
182
+ title: "IssueNumber",
183
+ description: "GitHub issue or pull request number"
184
+ });
185
+ const UrlOrMarkdownLinkSchema = Schema.String.pipe(Schema.filter((value)=>{
186
+ if (isValidUrl(value)) return true;
187
+ const match = MARKDOWN_LINK_PATTERN.exec(value);
188
+ return match?.[2] ? isValidUrl(match[2]) : false;
189
+ }, {
190
+ message: ()=>"Invalid URL or markdown link format"
191
+ }));
192
+ const GitHubInfoSchema = Schema.Struct({
193
+ user: Schema.optional(UsernameSchema),
194
+ pull: Schema.optional(IssueNumberSchema),
195
+ links: Schema.Struct({
196
+ commit: UrlOrMarkdownLinkSchema,
197
+ pull: Schema.optional(UrlOrMarkdownLinkSchema),
198
+ user: Schema.optional(UrlOrMarkdownLinkSchema)
199
+ })
200
+ });
201
+ const CONVENTIONAL_COMMIT_PATTERN = /^(\w+)(?:\(([^)]+)\))?(!)?\s*:\s*(.+)/;
202
+ function parseCommitMessage(message) {
203
+ const match = CONVENTIONAL_COMMIT_PATTERN.exec(message);
204
+ if (match) {
205
+ const [, type, scope, bang, description] = match;
206
+ const lines = message.split("\n");
207
+ const body = lines.slice(1).join("\n").trim();
208
+ const result = {
209
+ type,
210
+ description: description.trim()
211
+ };
212
+ if (scope) result.scope = scope;
213
+ if ("!" === bang) result.breaking = true;
214
+ if (body) result.body = body;
215
+ return result;
216
+ }
217
+ return {
218
+ description: message
219
+ };
220
+ }
221
+ const CLOSES_ISSUE_PATTERN = /closes?:?\s*#?(\d+(?:, *#?\d+)*)/i;
222
+ const FIXES_ISSUE_PATTERN = /fix(?:es)?:?\s*#?(\d+(?:, *#?\d+)*)/i;
223
+ const REFS_ISSUE_PATTERN = /refs?:?\s*#?(\d+(?:, *#?\d+)*)/i;
224
+ const ISSUE_NUMBER_SPLIT_PATTERN = /, */;
225
+ function extractIssueNumbers(pattern, message) {
226
+ const safeMessage = message.slice(0, 10000);
227
+ const match = pattern.exec(safeMessage);
228
+ if (!match?.[1]) return [];
229
+ return match[1].split(ISSUE_NUMBER_SPLIT_PATTERN).map((num)=>num.replace("#", "").trim());
230
+ }
231
+ function parseIssueReferences(commitMessage) {
232
+ return {
233
+ closes: extractIssueNumbers(CLOSES_ISSUE_PATTERN, commitMessage),
234
+ fixes: extractIssueNumbers(FIXES_ISSUE_PATTERN, commitMessage),
235
+ refs: extractIssueNumbers(REFS_ISSUE_PATTERN, commitMessage)
236
+ };
237
+ }
238
+ function parseChangesetSections(summary) {
239
+ const tree = parseMarkdown(summary);
240
+ const h2Indices = [];
241
+ for(let i = 0; i < tree.children.length; i++){
242
+ const node = tree.children[i];
243
+ if ("heading" === node.type && 2 === node.depth) h2Indices.push(i);
244
+ }
245
+ if (0 === h2Indices.length) return {
246
+ preamble: summary.trim(),
247
+ sections: []
248
+ };
249
+ const result = {
250
+ sections: []
251
+ };
252
+ if (h2Indices[0] > 0) {
253
+ const preambleNodes = tree.children.slice(0, h2Indices[0]);
254
+ result.preamble = stringifyAstSlice(preambleNodes);
255
+ }
256
+ for(let i = 0; i < h2Indices.length; i++){
257
+ const headingIndex = h2Indices[i];
258
+ const headingNode = tree.children[headingIndex];
259
+ const headingText = external_mdast_util_to_string_toString(headingNode);
260
+ const nextIndex = i + 1 < h2Indices.length ? h2Indices[i + 1] : tree.children.length;
261
+ const contentNodes = tree.children.slice(headingIndex + 1, nextIndex);
262
+ const content = stringifyAstSlice(contentNodes);
263
+ const category = fromHeading(headingText);
264
+ if (category) result.sections.push({
265
+ category,
266
+ heading: headingText,
267
+ content
268
+ });
269
+ }
270
+ return result;
271
+ }
272
+ function stringifyAstSlice(nodes) {
273
+ if (0 === nodes.length) return "";
274
+ const root = {
275
+ type: "root",
276
+ children: nodes
277
+ };
278
+ return stringifyMarkdown(root).trim();
279
+ }
280
+ const ISSUE_CATEGORIES = [
281
+ {
282
+ key: "closes",
283
+ label: "Closes"
284
+ },
285
+ {
286
+ key: "fixes",
287
+ label: "Fixes"
288
+ },
289
+ {
290
+ key: "refs",
291
+ label: "Refs"
292
+ }
293
+ ];
294
+ function formatChangelogEntry(entry, options) {
295
+ const parts = [];
296
+ if (entry.commit) {
297
+ const shortHash = entry.commit.substring(0, 7);
298
+ parts.push(`[\`${shortHash}\`](https://github.com/${options.repo}/commit/${entry.commit})`);
299
+ }
300
+ parts.push(entry.summary.trim());
301
+ const issueLinks = [];
302
+ for (const { key, label } of ISSUE_CATEGORIES){
303
+ const numbers = entry.issues[key];
304
+ if (numbers.length > 0) {
305
+ const links = numbers.map((num)=>`[#${num}](https://github.com/${options.repo}/issues/${num})`);
306
+ issueLinks.push(`${label}: ${links.join(", ")}`);
307
+ }
308
+ }
309
+ if (issueLinks.length > 0) parts.push(`\n\n${issueLinks.join(". ")}`);
310
+ return parts.join(" ");
311
+ }
312
+ function formatPRAndUserAttribution(pr, user, links) {
313
+ let prReference = "";
314
+ if (pr) if (links?.pull) {
315
+ const pullUrl = extractUrlFromMarkdown(links.pull);
316
+ prReference = ` [#${String(pr)}](${pullUrl})`;
317
+ } else prReference = ` (#${String(pr)})`;
318
+ if (user) {
319
+ if (links?.user) {
320
+ const userUrl = extractUrlFromMarkdown(links.user);
321
+ return `${prReference} Thanks [@${user}](${userUrl})!`;
322
+ }
323
+ return `${prReference} Thanks @${user}!`;
324
+ }
325
+ return prReference;
326
+ }
327
+ function getReleaseLine(changeset, versionType, options) {
328
+ return Effect.gen(function*() {
329
+ let commitInfo = null;
330
+ if (changeset.commit) {
331
+ const github = yield* GitHubService;
332
+ commitInfo = yield* github.getInfo({
333
+ commit: changeset.commit,
334
+ repo: options.repo
335
+ }).pipe(Effect.flatMap((raw)=>Schema.decodeUnknown(GitHubInfoSchema)(raw).pipe(Effect.map(()=>raw), Effect.catchAll(()=>Effect.succeed(raw)))), Effect.catchAll((error)=>{
336
+ logWarning("Could not fetch GitHub info for commit:", changeset.commit ?? "", String(error));
337
+ return Effect.succeed(null);
338
+ }));
339
+ }
340
+ const parsed = parseChangesetSections(changeset.summary);
341
+ const firstLine = changeset.summary.split("\n")[0];
342
+ const commitMsg = parseCommitMessage(firstLine);
343
+ const bodyText = changeset.summary.split("\n").slice(1).join("\n");
344
+ const issueRefs = parseIssueReferences(bodyText);
345
+ const attribution = commitInfo ? formatPRAndUserAttribution(commitInfo.pull ?? void 0, commitInfo.user ?? void 0, commitInfo.links) : "";
346
+ if (parsed.sections.length > 0) {
347
+ const lines = [];
348
+ if (parsed.preamble) {
349
+ lines.push(parsed.preamble);
350
+ lines.push("");
351
+ }
352
+ for (const section of parsed.sections){
353
+ lines.push(`### ${section.category.heading}`);
354
+ lines.push("");
355
+ const commitPrefix = changeset.commit ? `[\`${changeset.commit.substring(0, 7)}\`](https://github.com/${options.repo}/commit/${changeset.commit}) ` : "";
356
+ if (section.content) {
357
+ const contentLines = section.content.split("\n");
358
+ const firstContentLine = contentLines[0];
359
+ if (firstContentLine.startsWith("- ") || firstContentLine.startsWith("* ")) {
360
+ lines.push(`${firstContentLine.substring(0, 2)}${commitPrefix}${firstContentLine.substring(2)}`);
361
+ lines.push(...contentLines.slice(1));
362
+ } else lines.push(`- ${commitPrefix}${section.content}`);
363
+ }
364
+ lines.push("");
365
+ }
366
+ const result = lines.join("\n").trimEnd();
367
+ return `${result}${attribution}`;
368
+ }
369
+ const commitType = commitMsg.type ?? versionType;
370
+ const category = resolveCommitType(commitType, commitMsg.scope, commitMsg.breaking);
371
+ const entryInput = {
372
+ type: category.heading,
373
+ summary: changeset.summary,
374
+ issues: issueRefs,
375
+ ...changeset.commit ? {
376
+ commit: changeset.commit
377
+ } : {}
378
+ };
379
+ const entry = formatChangelogEntry(entryInput, {
380
+ repo: options.repo
381
+ });
382
+ return `- ${entry}${attribution}`;
383
+ });
384
+ }
385
+ const MainLayer = Layer.mergeAll(GitHubLive, MarkdownLive);
386
+ const changelogFunctions = {
387
+ getReleaseLine: async (changeset, versionType, options)=>{
388
+ const program = Effect.gen(function*() {
389
+ const opts = yield* validateChangesetOptions(options);
390
+ return yield* getReleaseLine(changeset, versionType, opts);
391
+ });
392
+ return Effect.runPromise(program.pipe(Effect.provide(MainLayer)));
393
+ },
394
+ getDependencyReleaseLine: async (changesets, dependenciesUpdated, options)=>{
395
+ const program = Effect.gen(function*() {
396
+ const opts = yield* validateChangesetOptions(options);
397
+ return yield* getDependencyReleaseLine(changesets, dependenciesUpdated, opts);
398
+ });
399
+ return Effect.runPromise(program.pipe(Effect.provide(MainLayer)));
400
+ }
401
+ };
402
+ const changelog = changelogFunctions;
403
+ export default changelog;
404
+ export { ChangesetOptionsSchema, ChangesetValidationError, ChangesetValidationErrorBase, ConfigurationError, ConfigurationErrorBase, GitHubApiError, GitHubApiErrorBase, GitHubInfoSchema, GitHubLive, GitHubService, GitHubServiceBase, IssueNumberSchema, MarkdownLive, MarkdownParseError, MarkdownParseErrorBase, MarkdownService, MarkdownServiceBase, NonEmptyString, PositiveInteger, RepoSchema, UrlOrMarkdownLinkSchema, UsernameSchema, makeGitHubTest };