browser-commander 0.17.0 → 0.17.1
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/.changeset/config.json +1 -0
- package/.jscpd-baseline.json +10 -4
- package/CHANGELOG.md +12 -1
- package/package.json +1 -1
- package/scripts/check-release-format.mjs +80 -0
- package/scripts/create-github-release.mjs +27 -6
- package/scripts/version-and-commit.mjs +12 -0
- package/tests/unit/scripts/cargo-lock-sync.test.js +110 -0
- package/tests/unit/scripts/changelog-collection-order.test.js +144 -0
- package/tests/unit/scripts/changeset-formatter.test.js +108 -0
- package/tests/unit/scripts/check-release-format.test.js +131 -0
- package/tests/unit/scripts/ci-workflow-policy.test.js +103 -0
- package/tests/unit/scripts/command-error-text.test.js +83 -0
- package/tests/unit/scripts/command-stream-errexit.test.js +132 -0
- package/tests/unit/scripts/pre-commit-config.test.js +5 -0
- package/tests/unit/scripts/release-tags.test.js +96 -0
- package/tests/unit/scripts/required-docs.test.js +145 -0
- package/tests/unit/scripts/rust-changelog.test.js +147 -0
- package/.changeset/merged-loud-river.md +0 -7
- package/deno.json +0 -7
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The documentation gate (CI/CD best practice #12).
|
|
3
|
+
*
|
|
4
|
+
* The repository already checks the two mechanical properties of its
|
|
5
|
+
* documentation: `scripts/check-file-line-limits.sh` scans every tracked `.md`
|
|
6
|
+
* file against the 1500-line limit, and `.github/workflows/links.yml` runs
|
|
7
|
+
* lychee over `./**\/*.md`, which resolves relative paths as well as URLs.
|
|
8
|
+
* Neither notices a document that stopped existing, and neither notices one of
|
|
9
|
+
* the three language READMEs quietly losing the section the other two still
|
|
10
|
+
* have - the exact drift `docs/feature-parity.md` exists to prevent.
|
|
11
|
+
*
|
|
12
|
+
* The template covers only the first half of that (a required-files list in
|
|
13
|
+
* `validate-docs`); the shared-section list is what a tri-language repository
|
|
14
|
+
* needs on top.
|
|
15
|
+
*
|
|
16
|
+
* These tests pin the enforcement mechanics against a fixture built from the
|
|
17
|
+
* script's own `--list` output, and pin the repository itself against the real
|
|
18
|
+
* requirement table.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { describe, it } from 'node:test';
|
|
26
|
+
|
|
27
|
+
import { BASH_AVAILABLE, repoPath, runBashScript } from '../../helpers/repo.js';
|
|
28
|
+
|
|
29
|
+
const SCRIPT = repoPath('scripts/check-required-docs.sh');
|
|
30
|
+
|
|
31
|
+
function run(args = [], options = {}) {
|
|
32
|
+
return runBashScript(SCRIPT, args, options);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The requirement table, read back from the script that enforces it. */
|
|
36
|
+
function requirements() {
|
|
37
|
+
const { status, output } = run(['--list']);
|
|
38
|
+
assert.equal(status, 0, output);
|
|
39
|
+
|
|
40
|
+
const table = new Map();
|
|
41
|
+
for (const line of output.split('\n')) {
|
|
42
|
+
if (!line.trim()) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const [file, section = ''] = line.split('\t');
|
|
46
|
+
if (!table.has(file)) {
|
|
47
|
+
table.set(file, []);
|
|
48
|
+
}
|
|
49
|
+
if (section) {
|
|
50
|
+
table.get(file).push(section);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return table;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A throwaway tree that satisfies the table, so a test can take exactly one
|
|
58
|
+
* thing away from it and watch the script notice.
|
|
59
|
+
*/
|
|
60
|
+
function fixture(table) {
|
|
61
|
+
const root = mkdtempSync(path.join(tmpdir(), 'required-docs-'));
|
|
62
|
+
for (const [file, sections] of table) {
|
|
63
|
+
const target = path.join(root, file);
|
|
64
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
65
|
+
writeFileSync(
|
|
66
|
+
target,
|
|
67
|
+
`# Title\n\n${sections.map((section) => `## ${section}\n\nBody.\n`).join('\n')}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return root;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('check-required-docs.sh', { skip: !BASH_AVAILABLE }, () => {
|
|
74
|
+
it('passes on this repository', () => {
|
|
75
|
+
const { status, output } = run();
|
|
76
|
+
assert.equal(status, 0, output);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('requires the three language READMEs to share a section list', () => {
|
|
80
|
+
const table = requirements();
|
|
81
|
+
const js = table.get('js/README.md');
|
|
82
|
+
assert.ok(js?.length, 'js/README.md carries no required sections');
|
|
83
|
+
assert.deepEqual(table.get('python/README.md'), js);
|
|
84
|
+
assert.deepEqual(table.get('rust/README.md'), js);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('fails on a required document that is missing', () => {
|
|
88
|
+
const table = requirements();
|
|
89
|
+
const root = fixture(table);
|
|
90
|
+
try {
|
|
91
|
+
rmSync(path.join(root, 'python/README.md'));
|
|
92
|
+
const { status, output } = run([root]);
|
|
93
|
+
assert.notEqual(status, 0);
|
|
94
|
+
assert.match(output, /python\/README\.md/);
|
|
95
|
+
assert.match(output, /missing/i);
|
|
96
|
+
} finally {
|
|
97
|
+
rmSync(root, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('fails on a required section that is missing, and names it', () => {
|
|
102
|
+
const table = requirements();
|
|
103
|
+
const root = fixture(table);
|
|
104
|
+
const dropped = table.get('rust/README.md')[0];
|
|
105
|
+
try {
|
|
106
|
+
writeFileSync(
|
|
107
|
+
path.join(root, 'rust/README.md'),
|
|
108
|
+
`# Title\n\n${table
|
|
109
|
+
.get('rust/README.md')
|
|
110
|
+
.filter((section) => section !== dropped)
|
|
111
|
+
.map((section) => `## ${section}\n\nBody.\n`)
|
|
112
|
+
.join('\n')}`
|
|
113
|
+
);
|
|
114
|
+
const { status, output } = run([root]);
|
|
115
|
+
assert.notEqual(status, 0);
|
|
116
|
+
assert.match(output, new RegExp(`rust/README\\.md`));
|
|
117
|
+
assert.ok(
|
|
118
|
+
output.includes(dropped),
|
|
119
|
+
`expected the report to name the missing section ${dropped}`
|
|
120
|
+
);
|
|
121
|
+
} finally {
|
|
122
|
+
rmSync(root, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('reads a heading only as a heading, not as prose that mentions it', () => {
|
|
127
|
+
const table = requirements();
|
|
128
|
+
const root = fixture(table);
|
|
129
|
+
const [section] = table.get('js/README.md');
|
|
130
|
+
try {
|
|
131
|
+
writeFileSync(
|
|
132
|
+
path.join(root, 'js/README.md'),
|
|
133
|
+
`# Title\n\nSee the ${section} section below.\n\n${table
|
|
134
|
+
.get('js/README.md')
|
|
135
|
+
.slice(1)
|
|
136
|
+
.map((heading) => `## ${heading}\n\nBody.\n`)
|
|
137
|
+
.join('\n')}`
|
|
138
|
+
);
|
|
139
|
+
const { status } = run([root]);
|
|
140
|
+
assert.notEqual(status, 0);
|
|
141
|
+
} finally {
|
|
142
|
+
rmSync(root, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard for the half of RC-C that survives even after errexit is fixed.
|
|
3
|
+
*
|
|
4
|
+
* `rust/scripts/version-and-commit.mjs` carried its own copy of the changelog
|
|
5
|
+
* collector, and that copy never deleted the fragments it consumed. The
|
|
6
|
+
* standalone `rust/scripts/collect-changelog.mjs` did delete them, but only
|
|
7
|
+
* the `manual-release` job ever calls it — `auto-release` used the inline
|
|
8
|
+
* copy. So every automatic release re-published the same twelve entries.
|
|
9
|
+
*
|
|
10
|
+
* Both now share `rust/scripts/changelog.mjs`. These tests pin the behaviour
|
|
11
|
+
* the inline copy was missing.
|
|
12
|
+
*
|
|
13
|
+
* See dev/log/issues/83/pulls/84/analysis/root-causes.md, RC-C.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import {
|
|
18
|
+
mkdtempSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
existsSync,
|
|
24
|
+
} from 'node:fs';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { describe, it } from 'node:test';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
collectFragments,
|
|
31
|
+
removeFragments,
|
|
32
|
+
updateChangelog,
|
|
33
|
+
INSERT_MARKER,
|
|
34
|
+
} from '../../../../rust/scripts/changelog.mjs';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a throwaway crate directory with changelog fragments.
|
|
38
|
+
* @param {{fragments?: Record<string,string>, changelog?: string|null}} [options]
|
|
39
|
+
*/
|
|
40
|
+
function fixture(options = {}) {
|
|
41
|
+
const { fragments = {}, changelog = null } = options;
|
|
42
|
+
const dir = mkdtempSync(join(tmpdir(), 'rust-changelog-'));
|
|
43
|
+
const changelogDir = join(dir, 'changelog.d');
|
|
44
|
+
mkdirSync(changelogDir);
|
|
45
|
+
writeFileSync(join(changelogDir, 'README.md'), 'not a fragment\n');
|
|
46
|
+
for (const [name, content] of Object.entries(fragments)) {
|
|
47
|
+
writeFileSync(join(changelogDir, name), content);
|
|
48
|
+
}
|
|
49
|
+
if (changelog !== null) {
|
|
50
|
+
writeFileSync(join(dir, 'CHANGELOG.md'), changelog);
|
|
51
|
+
}
|
|
52
|
+
return { dir, changelogDir };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const HEADER = `# Changelog
|
|
56
|
+
|
|
57
|
+
All notable changes to this project will be documented in this file.
|
|
58
|
+
|
|
59
|
+
${INSERT_MARKER}
|
|
60
|
+
|
|
61
|
+
## [0.1.0] - 2024-12-30
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- Initial release
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
describe('rust changelog collector', () => {
|
|
69
|
+
it('deletes the fragments it consumed', () => {
|
|
70
|
+
const { dir, changelogDir } = fixture({
|
|
71
|
+
fragments: {
|
|
72
|
+
'38.added.md': '### Added\n\n- A thing\n',
|
|
73
|
+
'49.added.md': '### Added\n\n- Another thing\n',
|
|
74
|
+
},
|
|
75
|
+
changelog: HEADER,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
updateChangelog(dir, '0.10.11', collectFragments(dir));
|
|
79
|
+
removeFragments(dir);
|
|
80
|
+
|
|
81
|
+
const left = readdirSync(changelogDir);
|
|
82
|
+
assert.deepEqual(
|
|
83
|
+
left,
|
|
84
|
+
['README.md'],
|
|
85
|
+
'every consumed fragment must be gone; leaving them makes the next ' +
|
|
86
|
+
'release re-publish the same notes'
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('never deletes README.md', () => {
|
|
91
|
+
const { dir, changelogDir } = fixture({
|
|
92
|
+
fragments: { '1.added.md': '- x\n' },
|
|
93
|
+
changelog: HEADER,
|
|
94
|
+
});
|
|
95
|
+
removeFragments(dir);
|
|
96
|
+
assert.ok(existsSync(join(changelogDir, 'README.md')));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('strips frontmatter carrying the bump type', () => {
|
|
100
|
+
const { dir } = fixture({
|
|
101
|
+
fragments: {
|
|
102
|
+
'81.fixed.md': '---\nbump: patch\n---\n\n### Fixed\n\n- A fix\n',
|
|
103
|
+
},
|
|
104
|
+
changelog: HEADER,
|
|
105
|
+
});
|
|
106
|
+
const fragments = collectFragments(dir);
|
|
107
|
+
assert.ok(!fragments.includes('bump: patch'));
|
|
108
|
+
assert.ok(fragments.includes('- A fix'));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('inserts the new entry directly after the marker, above older releases', () => {
|
|
112
|
+
const { dir } = fixture({
|
|
113
|
+
fragments: { '38.added.md': '### Added\n\n- New thing\n' },
|
|
114
|
+
changelog: HEADER,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
updateChangelog(dir, '0.10.11', collectFragments(dir));
|
|
118
|
+
|
|
119
|
+
const content = readFileSync(join(dir, 'CHANGELOG.md'), 'utf-8');
|
|
120
|
+
assert.ok(
|
|
121
|
+
content.indexOf('## [0.10.11]') < content.indexOf('## [0.1.0]'),
|
|
122
|
+
'newest release must come first'
|
|
123
|
+
);
|
|
124
|
+
assert.ok(
|
|
125
|
+
content.indexOf(INSERT_MARKER) < content.indexOf('## [0.10.11]'),
|
|
126
|
+
'the marker must stay above the entries so the next release can find it'
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('reports no fragments rather than writing an empty release section', () => {
|
|
131
|
+
const { dir } = fixture({ changelog: HEADER });
|
|
132
|
+
assert.equal(collectFragments(dir), '');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('creates CHANGELOG.md when it does not exist yet', () => {
|
|
136
|
+
const { dir } = fixture({
|
|
137
|
+
fragments: { '1.added.md': '### Added\n\n- First\n' },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
updateChangelog(dir, '0.1.0', collectFragments(dir));
|
|
141
|
+
|
|
142
|
+
const content = readFileSync(join(dir, 'CHANGELOG.md'), 'utf-8');
|
|
143
|
+
assert.ok(content.includes(INSERT_MARKER));
|
|
144
|
+
assert.ok(content.includes('## [0.1.0]'));
|
|
145
|
+
assert.ok(content.includes('- First'));
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
'browser-commander': minor
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Restore the release pipeline. `const { $ } = await use('command-stream')` returned `undefined` on the Node 24 the release jobs pin, because Node 23 added a synthetic `'module.exports'` named export for CommonJS namespaces that use-m's unwrapping does not recognise, so every JS and Rust release died on its first shell call with `TypeError: $ is not a function`. Dependencies now load through `scripts/use-module.mjs`, which detects that namespace shape and unwraps it. Manifest fields are read by TOML table through `scripts/read-manifest.mjs` instead of a line-anchored `grep`, which matched duplicate keys in other tables and wrote two lines into `$GITHUB_OUTPUT`. The duplication gate had `format` set to a reporter name, so jscpd scanned zero files and passed unconditionally; it now scans the tree against a recorded baseline.
|
|
6
|
-
|
|
7
|
-
Add the `fingerprint` subsystem, which removes the differences between a browser this library controls and one started by hand. `navigator.webdriver` stays false because the `AutomationControlled` Blink feature is disabled at launch, and `launchBrowser()` no longer passes the engine defaults a real Chrome does not carry. `resolveFingerprintProfile()` validates the 19 environment fields a page can read -- user agent and client hints, languages and locale, time zone, platform, cores, memory, screen, viewport, touch, WebGL strings, geolocation and the media preferences -- `createFingerprintPreset()` builds internally consistent Windows, macOS, Linux and Android machines, and `applyFingerprint()` installs a profile over CDP for both Playwright and Puppeteer. `FINGERPRINT_LIMITATIONS` documents what still cannot be made identical, and `relevantFingerprintLimitations()` narrows it to the entries a given profile and browser actually hit. The excluded defaults include Playwright's unconditional `--enable-unsafe-swiftshader`, which would otherwise give an automated browser a SwiftShader WebGL context on a machine where a hand-started Chrome has none.
|