@changesets/parse
Advanced tools
+19
-0
| # @changesets/parse | ||
| ## 1.0.0-next.2 | ||
| ### Patch Changes | ||
| - Updated dependencies [[`c19b112`](https://github.com/changesets/changesets/commit/c19b1123d27986da0e14e99d65b0f9a408def35c)]: | ||
| - @changesets/types@7.0.0-next.2 | ||
| ## 0.4.3 | ||
| ### Patch Changes | ||
| - [#1831](https://github.com/changesets/changesets/pull/1831) [`1f91879`](https://github.com/changesets/changesets/commit/1f91879d4977c49593619b07e1374cd804f16757) Thanks [@murataslan1](https://github.com/murataslan1)! - Improve error messages for malformed changeset files. The new error messages explain what went wrong, show what was received, and provide examples of the correct format. | ||
| ## 0.4.2 | ||
| ### Patch Changes | ||
| - [#1772](https://github.com/changesets/changesets/pull/1772) [`4c5a207`](https://github.com/changesets/changesets/commit/4c5a2078e45d1d269ecc65c3c24b898ed6451245) Thanks [@marcalexiei](https://github.com/marcalexiei)! - Update `js-yaml` to `v4` | ||
| ## 1.0.0-next.1 | ||
@@ -4,0 +23,0 @@ |
+37
-13
| import yaml from 'js-yaml'; | ||
| const mdRegex = /\s*---([^]*?)\n\s*---(\s*(?:\n|$)[^]*)/; | ||
| const EXAMPLE_FORMAT = `---\n"package-name": patch\n---`; | ||
| const validVersionTypes = ["major", "minor", "patch", "none"]; | ||
| function truncate(s, max = 200) { | ||
| return s.length > max ? s.slice(0, max) + "..." : s; | ||
| } | ||
| function validateReleases(releases, contents) { | ||
| for (const release of releases) { | ||
| if (typeof release.name !== "string" || release.name.trim() === "") { | ||
| throw new Error(`could not parse changeset - invalid package name in frontmatter.\n` + `Expected a non-empty string for package name, but got: ${JSON.stringify(release.name)}\n` + `Changeset contents:\n${truncate(contents)}`); | ||
| } | ||
| if (typeof release.type !== "string") { | ||
| throw new Error(`could not parse changeset - invalid release type for package "${release.name}".\n` + `Expected a string for release type, but got: ${typeof release.type}\n` + `Changeset contents:\n${truncate(contents)}`); | ||
| } | ||
| if (!validVersionTypes.includes(release.type)) { | ||
| throw new Error(`could not parse changeset - invalid version type ${JSON.stringify(release.type)} for package "${release.name}".\n` + `Valid version types are: ${validVersionTypes.join(", ")}\n` + `Changeset contents:\n${truncate(contents)}`); | ||
| } | ||
| } | ||
| } | ||
| function parseChangesetFile(contents) { | ||
| const trimmedContents = contents.trim(); | ||
| if (!trimmedContents) { | ||
| throw new Error(`could not parse changeset - file is empty.\n` + `Changesets must have frontmatter with package names and version types.\n` + `Example:\n${EXAMPLE_FORMAT}\n\nYour changeset summary here.`); | ||
| } | ||
| const execResult = mdRegex.exec(contents); | ||
| if (!execResult) { | ||
| throw new Error(`could not parse changeset - invalid frontmatter: ${contents}`); | ||
| throw new Error(`could not parse changeset - missing or invalid frontmatter.\n` + `Changesets must start with frontmatter delimited by "---".\n` + `Example:\n${EXAMPLE_FORMAT}\n\nYour changeset summary here.\n` + `Received content:\n${truncate(trimmedContents)}`); | ||
| } | ||
@@ -12,18 +34,20 @@ let [, roughReleases, roughSummary] = execResult; | ||
| let releases; | ||
| let yamlStuff; | ||
| try { | ||
| const yamlStuff = yaml.safeLoad(roughReleases); | ||
| if (yamlStuff) { | ||
| releases = Object.entries(yamlStuff).map(([name, type]) => ({ | ||
| name, | ||
| type | ||
| })); | ||
| } else { | ||
| releases = []; | ||
| } | ||
| yamlStuff = yaml.load(roughReleases); | ||
| } catch (e) { | ||
| throw new Error(`could not parse changeset - invalid frontmatter: ${contents}`); | ||
| throw new Error(`could not parse changeset - invalid YAML in frontmatter.\n` + `The frontmatter between the "---" delimiters must be valid YAML.\n` + `YAML error: ${e instanceof Error ? e.message : String(e)}\n` + `Frontmatter content:\n${roughReleases}`); | ||
| } | ||
| if (!releases) { | ||
| throw new Error(`could not parse changeset - unknown error: ${contents}`); | ||
| if (yamlStuff) { | ||
| if (typeof yamlStuff !== "object" || Array.isArray(yamlStuff)) { | ||
| throw new Error(`could not parse changeset - frontmatter must be an object mapping package names to version types.\n` + `Expected format:\n${EXAMPLE_FORMAT}\n` + `Received:\n${roughReleases}`); | ||
| } | ||
| releases = Object.entries(yamlStuff).map(([name, type]) => ({ | ||
| name, | ||
| type | ||
| })); | ||
| } else { | ||
| releases = []; | ||
| } | ||
| validateReleases(releases, contents); | ||
| return { | ||
@@ -30,0 +54,0 @@ releases, |
+5
-4
| { | ||
| "name": "@changesets/parse", | ||
| "version": "1.0.0-next.1", | ||
| "version": "1.0.0-next.2", | ||
| "description": "Parse a changeset file's contents into a usable json object", | ||
@@ -13,11 +13,12 @@ "type": "module", | ||
| "dependencies": { | ||
| "@changesets/types": "^7.0.0-next.1", | ||
| "js-yaml": "^3.13.1" | ||
| "@changesets/types": "^7.0.0-next.2", | ||
| "js-yaml": "^4.1.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/js-yaml": "^4.0.9", | ||
| "outdent": "^0.8.0" | ||
| }, | ||
| "engines": { | ||
| "node": ">=20.0.0" | ||
| "node": ">=20.19.0" | ||
| } | ||
| } |
+135
-1
@@ -218,2 +218,16 @@ import { outdent } from "outdent"; | ||
| }); | ||
| it("should handle package name unquoted and version quoted", () => { | ||
| const changesetMd = `--- | ||
| pkg: "minor" | ||
| --- | ||
| something`; | ||
| const changeset = parse(changesetMd); | ||
| expect(changeset).toEqual({ | ||
| releases: [{ name: "pkg", type: "minor" }], | ||
| summary: "something", | ||
| }); | ||
| }); | ||
| it("should throw if the frontmatter is followed by non-whitespace characters on the same line", () => { | ||
@@ -228,3 +242,12 @@ const changesetMd = outdent`--- | ||
| expect(() => parse(changesetMd)).toThrowErrorMatchingInlineSnapshot(` | ||
| [Error: could not parse changeset - invalid frontmatter: --- | ||
| [Error: could not parse changeset - missing or invalid frontmatter. | ||
| Changesets must start with frontmatter delimited by "---". | ||
| Example: | ||
| --- | ||
| "package-name": patch | ||
| --- | ||
| Your changeset summary here. | ||
| Received content: | ||
| --- | ||
| "cool-package": minor | ||
@@ -236,2 +259,113 @@ --- fail | ||
| }); | ||
| it("should throw when frontmatter hasn't a valid yml structure", () => { | ||
| const changesetMd = outdent`--- | ||
| : minor | ||
| --- | ||
| Nice simple summary | ||
| `; | ||
| expect(() => parse(changesetMd)).toThrowErrorMatchingInlineSnapshot(` | ||
| [Error: could not parse changeset - invalid YAML in frontmatter. | ||
| The frontmatter between the "---" delimiters must be valid YAML. | ||
| YAML error: incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line (2:1) | ||
| 1 | | ||
| 2 | : minor | ||
| -----^ | ||
| Frontmatter content: | ||
| : minor] | ||
| `); | ||
| }); | ||
| it("should throw when file is completely empty", () => { | ||
| expect(() => parse("")).toThrowErrorMatchingInlineSnapshot(` | ||
| [Error: could not parse changeset - file is empty. | ||
| Changesets must have frontmatter with package names and version types. | ||
| Example: | ||
| --- | ||
| "package-name": patch | ||
| --- | ||
| Your changeset summary here.] | ||
| `); | ||
| expect(() => parse(" ")).toThrowErrorMatchingInlineSnapshot(` | ||
| [Error: could not parse changeset - file is empty. | ||
| Changesets must have frontmatter with package names and version types. | ||
| Example: | ||
| --- | ||
| "package-name": patch | ||
| --- | ||
| Your changeset summary here.] | ||
| `); | ||
| expect(() => parse("\n\n")).toThrowErrorMatchingInlineSnapshot(` | ||
| [Error: could not parse changeset - file is empty. | ||
| Changesets must have frontmatter with package names and version types. | ||
| Example: | ||
| --- | ||
| "package-name": patch | ||
| --- | ||
| Your changeset summary here.] | ||
| `); | ||
| }); | ||
| it("should throw when frontmatter is missing", () => { | ||
| const changesetMd = "Just some content without frontmatter"; | ||
| expect(() => parse(changesetMd)).toThrowErrorMatchingInlineSnapshot(` | ||
| [Error: could not parse changeset - missing or invalid frontmatter. | ||
| Changesets must start with frontmatter delimited by "---". | ||
| Example: | ||
| --- | ||
| "package-name": patch | ||
| --- | ||
| Your changeset summary here. | ||
| Received content: | ||
| Just some content without frontmatter] | ||
| `); | ||
| }); | ||
| it("should throw when version type is invalid", () => { | ||
| const changesetMd = outdent`--- | ||
| "cool-package": invalid-type | ||
| --- | ||
| Nice simple summary | ||
| `; | ||
| expect(() => parse(changesetMd)).toThrowErrorMatchingInlineSnapshot(` | ||
| [Error: could not parse changeset - invalid version type "invalid-type" for package "cool-package". | ||
| Valid version types are: major, minor, patch, none | ||
| Changeset contents: | ||
| --- | ||
| "cool-package": invalid-type | ||
| --- | ||
| Nice simple summary] | ||
| `); | ||
| }); | ||
| it("should throw with helpful message when package name is empty", () => { | ||
| const changesetMd = outdent`--- | ||
| "": minor | ||
| --- | ||
| Nice simple summary | ||
| `; | ||
| expect(() => parse(changesetMd)).toThrowErrorMatchingInlineSnapshot(` | ||
| [Error: could not parse changeset - invalid package name in frontmatter. | ||
| Expected a non-empty string for package name, but got: "" | ||
| Changeset contents: | ||
| --- | ||
| "": minor | ||
| --- | ||
| Nice simple summary] | ||
| `); | ||
| }); | ||
| }); |
+82
-14
@@ -6,2 +6,47 @@ import yaml from "js-yaml"; | ||
| const EXAMPLE_FORMAT = `---\n"package-name": patch\n---`; | ||
| const validVersionTypes: readonly VersionType[] = [ | ||
| "major", | ||
| "minor", | ||
| "patch", | ||
| "none", | ||
| ]; | ||
| function truncate(s: string, max = 200): string { | ||
| return s.length > max ? s.slice(0, max) + "..." : s; | ||
| } | ||
| function validateReleases(releases: Release[], contents: string): void { | ||
| for (const release of releases) { | ||
| if (typeof release.name !== "string" || release.name.trim() === "") { | ||
| throw new Error( | ||
| `could not parse changeset - invalid package name in frontmatter.\n` + | ||
| `Expected a non-empty string for package name, but got: ${JSON.stringify( | ||
| release.name, | ||
| )}\n` + | ||
| `Changeset contents:\n${truncate(contents)}`, | ||
| ); | ||
| } | ||
| if (typeof release.type !== "string") { | ||
| throw new Error( | ||
| `could not parse changeset - invalid release type for package "${release.name}".\n` + | ||
| `Expected a string for release type, but got: ${typeof release.type}\n` + | ||
| `Changeset contents:\n${truncate(contents)}`, | ||
| ); | ||
| } | ||
| if (!validVersionTypes.includes(release.type)) { | ||
| throw new Error( | ||
| `could not parse changeset - invalid version type ${JSON.stringify( | ||
| release.type, | ||
| )} for package "${release.name}".\n` + | ||
| `Valid version types are: ${validVersionTypes.join(", ")}\n` + | ||
| `Changeset contents:\n${truncate(contents)}`, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| export default function parseChangesetFile(contents: string): { | ||
@@ -11,6 +56,19 @@ summary: string; | ||
| } { | ||
| const trimmedContents = contents.trim(); | ||
| if (!trimmedContents) { | ||
| throw new Error( | ||
| `could not parse changeset - file is empty.\n` + | ||
| `Changesets must have frontmatter with package names and version types.\n` + | ||
| `Example:\n${EXAMPLE_FORMAT}\n\nYour changeset summary here.`, | ||
| ); | ||
| } | ||
| const execResult = mdRegex.exec(contents); | ||
| if (!execResult) { | ||
| throw new Error( | ||
| `could not parse changeset - invalid frontmatter: ${contents}` | ||
| `could not parse changeset - missing or invalid frontmatter.\n` + | ||
| `Changesets must start with frontmatter delimited by "---".\n` + | ||
| `Example:\n${EXAMPLE_FORMAT}\n\nYour changeset summary here.\n` + | ||
| `Received content:\n${truncate(trimmedContents)}`, | ||
| ); | ||
@@ -22,24 +80,34 @@ } | ||
| let releases: Release[]; | ||
| let yamlStuff: Record<string, VersionType> | undefined; | ||
| try { | ||
| const yamlStuff: { [key: string]: VersionType } = | ||
| yaml.safeLoad(roughReleases); | ||
| if (yamlStuff) { | ||
| releases = Object.entries(yamlStuff).map(([name, type]) => ({ | ||
| name, | ||
| type, | ||
| })); | ||
| } else { | ||
| releases = []; | ||
| } | ||
| yamlStuff = yaml.load(roughReleases) as typeof yamlStuff; | ||
| } catch (e) { | ||
| throw new Error( | ||
| `could not parse changeset - invalid frontmatter: ${contents}` | ||
| `could not parse changeset - invalid YAML in frontmatter.\n` + | ||
| `The frontmatter between the "---" delimiters must be valid YAML.\n` + | ||
| `YAML error: ${e instanceof Error ? e.message : String(e)}\n` + | ||
| `Frontmatter content:\n${roughReleases}`, | ||
| ); | ||
| } | ||
| if (!releases) { | ||
| throw new Error(`could not parse changeset - unknown error: ${contents}`); | ||
| if (yamlStuff) { | ||
| if (typeof yamlStuff !== "object" || Array.isArray(yamlStuff)) { | ||
| throw new Error( | ||
| `could not parse changeset - frontmatter must be an object mapping package names to version types.\n` + | ||
| `Expected format:\n${EXAMPLE_FORMAT}\n` + | ||
| `Received:\n${roughReleases}`, | ||
| ); | ||
| } | ||
| releases = Object.entries(yamlStuff).map(([name, type]) => ({ | ||
| name, | ||
| type, | ||
| })); | ||
| } else { | ||
| releases = []; | ||
| } | ||
| validateReleases(releases, contents); | ||
| return { releases, summary }; | ||
| } |
27716
48.58%469
69.93%2
100%+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
Updated