@truenas/ui-components 0.7.4 → 0.7.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truenas/ui-components",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public"
@@ -1,17 +1,21 @@
1
1
  import type { Config } from 'jest';
2
2
 
3
3
  /**
4
- * `yarn test:scripts` — every build script under this directory that has tests.
4
+ * `yarn test:scripts` — the repo's node-environment tests: every build script under this
5
+ * directory that has tests, plus `release-config`, which tests repo-root configuration
6
+ * rather than a script and lives here because this is where a node-environment Jest
7
+ * project already runs.
5
8
  *
6
- * One Jest project per script package rather than one config rooted here, because
7
- * `<rootDir>` inside a project config resolves to that project's own directory: each
8
- * package keeps pointing ts-jest at its own `tsconfig.json`, and adding the next one
9
- * costs a line here rather than a merge of compiler options.
9
+ * One Jest project per package rather than one config rooted here, because `<rootDir>`
10
+ * inside a project config resolves to that project's own directory: each package keeps
11
+ * pointing ts-jest at its own `tsconfig.json`, and adding the next one costs a line here
12
+ * rather than a merge of compiler options.
10
13
  */
11
14
  const config: Config = {
12
15
  projects: [
13
16
  '<rootDir>/harness-docs/jest.config.ts',
14
17
  '<rootDir>/icon-sprite/jest.config.ts',
18
+ '<rootDir>/release-config/jest.config.ts',
15
19
  ],
16
20
  };
17
21
 
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The PR title check and the release step both decide what a commit header means, from
3
+ * two patterns written in two files, and nothing made them agree. A scope naming two
4
+ * components with a comma — `feat(autocomplete,chip-input): …` — passed the title check,
5
+ * merged, and then matched no header pattern at all, so the commit analyzer read it as
6
+ * having no type: it triggered no release and appeared in no changelog (#315).
7
+ *
8
+ * These tests read both files rather than restating either pattern, so drift in either
9
+ * one fails here instead of silently dropping a merged commit from the next release.
10
+ *
11
+ * They live under the library's script tests because `yarn test:scripts` is where this
12
+ * repo runs Jest in a node environment; what they cover is repo-root release
13
+ * configuration, which is why they reach four directories up.
14
+ */
15
+ import { readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+
18
+ const repoRoot = join(__dirname, '..', '..', '..', '..');
19
+
20
+ interface ParserOpts {
21
+ headerPattern: string;
22
+ headerCorrespondence: string[];
23
+ breakingHeaderPattern: string;
24
+ }
25
+
26
+ type PluginEntry = string | [string, { parserOpts?: ParserOpts }];
27
+
28
+ const releaseConfig = JSON.parse(
29
+ readFileSync(join(repoRoot, '.releaserc.json'), 'utf8')
30
+ ) as { plugins: PluginEntry[] };
31
+
32
+ /** Every semantic-release plugin that parses commit headers, with its plugin name. */
33
+ const parsingPlugins: [string, ParserOpts][] = [];
34
+
35
+ for (const plugin of releaseConfig.plugins) {
36
+ if (Array.isArray(plugin)) {
37
+ const [name, options] = plugin;
38
+
39
+ if (options.parserOpts) {
40
+ parsingPlugins.push([name, options.parserOpts]);
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * The Bash pattern the PR title check runs, lifted from the workflow that runs it.
47
+ *
48
+ * `[[ =~ ]]` and `RegExp` both leave the end unanchored and the pattern anchors its own
49
+ * start, so a JS `RegExp` accepts what the check accepts for the constructs it uses:
50
+ * groups, alternation, character classes and `?`.
51
+ */
52
+ function readPrTitlePattern(): RegExp {
53
+ const workflow = readFileSync(join(repoRoot, '.github/workflows/pr-title.yml'), 'utf8');
54
+ const assignment = /^\s*pattern='([^']+)'\s*$/m.exec(workflow);
55
+
56
+ if (!assignment) {
57
+ throw new Error('no `pattern=<...>` assignment found in .github/workflows/pr-title.yml');
58
+ }
59
+
60
+ return new RegExp(assignment[1]);
61
+ }
62
+
63
+ const prTitleCheck = readPrTitlePattern();
64
+
65
+ /** What the release step reads out of a header, or null when it matches nothing. */
66
+ function parseHeader(opts: ParserOpts, header: string): Record<string, string | undefined> | null {
67
+ const match = new RegExp(opts.headerPattern).exec(header);
68
+
69
+ if (!match) {
70
+ return null;
71
+ }
72
+
73
+ return Object.fromEntries(
74
+ opts.headerCorrespondence.map((field, index) => [field, match[index + 1]])
75
+ );
76
+ }
77
+
78
+ const commaScopeHeader =
79
+ 'NAS-142381 / 27.0.0-BETA.1 / feat(autocomplete,chip-input): drive options from an async [dataSource] (#312)';
80
+
81
+ /**
82
+ * Headers the PR title check accepts. Every one is asserted to pass the check as well as
83
+ * to parse, so a case that stops being realistic fails rather than passing vacuously.
84
+ */
85
+ const acceptedHeaders: { name: string; header: string; type: string; scope?: string }[] = [
86
+ {
87
+ name: 'a single-word scope',
88
+ header: 'feat(table): let a selection survive a dataSource change',
89
+ type: 'feat',
90
+ scope: 'table',
91
+ },
92
+ {
93
+ name: 'the ticket and version prefix a squash commit carries',
94
+ header: 'NAS-143108 / 27.0.0-BETA.1 / fix(table): stop the pager aliasing pagination',
95
+ type: 'fix',
96
+ scope: 'table',
97
+ },
98
+ {
99
+ name: 'no scope at all',
100
+ header: 'chore: bump the toolchain',
101
+ type: 'chore',
102
+ scope: undefined,
103
+ },
104
+ {
105
+ name: 'two components separated by a comma (#315)',
106
+ header: commaScopeHeader,
107
+ type: 'feat',
108
+ scope: 'autocomplete,chip-input',
109
+ },
110
+ {
111
+ name: 'a comma and a space between components',
112
+ header: 'feat(autocomplete, chip-input): drive options from an async [dataSource]',
113
+ type: 'feat',
114
+ scope: 'autocomplete, chip-input',
115
+ },
116
+ {
117
+ name: 'a scope with a slash',
118
+ header: 'refactor(a11y/labels): fold the fallback into one helper',
119
+ type: 'refactor',
120
+ scope: 'a11y/labels',
121
+ },
122
+ ];
123
+
124
+ describe('.releaserc.json', () => {
125
+ it('has more than one plugin parsing commit headers, one of them the commit analyzer', () => {
126
+ expect(parsingPlugins.length).toBeGreaterThan(1);
127
+ expect(parsingPlugins.map(([name]) => name)).toContain('@semantic-release/commit-analyzer');
128
+ });
129
+
130
+ it('gives all of them the same parser options, so notes and version bumps agree', () => {
131
+ const [[, first]] = parsingPlugins;
132
+
133
+ // The plugin name rides along in the assertion so a failure says which one drifted.
134
+ for (const [name, opts] of parsingPlugins) {
135
+ expect([name, opts]).toEqual([name, first]);
136
+ }
137
+ });
138
+ });
139
+
140
+ describe.each(parsingPlugins)('%s', (_pluginName, opts) => {
141
+ it.each(acceptedHeaders)('parses $name', ({ header, type, scope }) => {
142
+ expect(parseHeader(opts, header)).toMatchObject({ type, scope, subject: expect.any(String) });
143
+ });
144
+
145
+ it('reads a comma scope as one scope, leaving the subject whole', () => {
146
+ expect(parseHeader(opts, commaScopeHeader)?.subject).toBe(
147
+ 'drive options from an async [dataSource] (#312)'
148
+ );
149
+ });
150
+
151
+ it('recognises a breaking change whose scope contains a comma', () => {
152
+ const breaking = new RegExp(opts.breakingHeaderPattern).exec(
153
+ 'feat(autocomplete,chip-input)!: options are now a dataSource'
154
+ );
155
+
156
+ expect(breaking?.[1]).toBe('feat');
157
+ expect(breaking?.[2]).toBe('autocomplete,chip-input');
158
+ });
159
+ });
160
+
161
+ describe('the PR title check and the release step', () => {
162
+ it.each(acceptedHeaders)('agree on $name', ({ header, type }) => {
163
+ expect(prTitleCheck.test(header)).toBe(true);
164
+
165
+ for (const [, opts] of parsingPlugins) {
166
+ expect(parseHeader(opts, header)).toMatchObject({ type });
167
+ }
168
+ });
169
+
170
+ it('agree on every scope character the check allows, not a narrower set', () => {
171
+ // The check's scope is `\([^)]+\)`. A release pattern with a narrower scope class is
172
+ // what produced #315: the title passed, the commit merged, the release dropped it.
173
+ const scopes = ['a,b', 'a, b', 'a/b', 'a.b', 'a-b', 'a b', 'a:b', 'a+b', 'a&b', 'a#b', '@scope/pkg'];
174
+
175
+ for (const scope of scopes) {
176
+ const header = `feat(${scope}): a subject`;
177
+
178
+ expect(prTitleCheck.test(header)).toBe(true);
179
+
180
+ for (const [, opts] of parsingPlugins) {
181
+ expect(parseHeader(opts, header)).toEqual({
182
+ type: 'feat',
183
+ scope,
184
+ subject: 'a subject',
185
+ });
186
+ }
187
+ }
188
+ });
189
+ });
@@ -0,0 +1,14 @@
1
+ import type { Config } from 'jest';
2
+
3
+ const config: Config = {
4
+ testEnvironment: 'node',
5
+ transform: {
6
+ '^.+\\.tsx?$': ['ts-jest', {
7
+ tsconfig: '<rootDir>/tsconfig.json',
8
+ }],
9
+ },
10
+ testMatch: ['<rootDir>/**/*.spec.ts'],
11
+ moduleFileExtensions: ['ts', 'js', 'json'],
12
+ };
13
+
14
+ export default config;
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "esModuleInterop": true,
7
+ "isolatedModules": true,
8
+ "strict": true,
9
+ "types": ["jest", "node"],
10
+ "rootDir": ".",
11
+ "outDir": "../../dist/out-tsc/scripts"
12
+ },
13
+ "include": ["**/*.ts"]
14
+ }