@link-assistant/web-capture 1.12.0 → 2.0.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -1
  3. package/bin/web-capture.js +5 -7
  4. package/package.json +32 -18
  5. package/src/archive.js +1 -2
  6. package/src/binary-download.js +13 -0
  7. package/src/docx.js +14 -11
  8. package/src/fetch.js +1 -2
  9. package/src/figures.js +2 -12
  10. package/src/gdocs-images.js +0 -1
  11. package/src/gdocs.js +0 -1
  12. package/src/github.js +0 -1
  13. package/src/lib.js +1 -2
  14. package/src/localize-images.js +2 -12
  15. package/src/markdown.js +0 -1
  16. package/src/shared-dialog.js +0 -1
  17. package/src/stream.js +0 -1
  18. package/src/transport.js +23 -5
  19. package/src/txt.js +0 -1
  20. package/.changeset/README.md +0 -8
  21. package/.changeset/config.json +0 -11
  22. package/.dockerignore +0 -6
  23. package/.jscpd.json +0 -19
  24. package/.lenv +0 -8
  25. package/.prettierignore +0 -8
  26. package/.prettierrc +0 -10
  27. package/Dockerfile +0 -52
  28. package/babel.config.cjs +0 -13
  29. package/docker-compose.yml +0 -11
  30. package/eslint.config.js +0 -112
  31. package/examples/js/capture_cache.js +0 -12
  32. package/examples/js/engine_comparison.js +0 -39
  33. package/examples/js/html_download.js +0 -23
  34. package/examples/js/image_download.js +0 -36
  35. package/examples/js/markdown_download.js +0 -23
  36. package/examples/js/playwright_example.js +0 -23
  37. package/examples/python/html_download.py +0 -15
  38. package/examples/python/image_download.py +0 -20
  39. package/examples/python/markdown_download.py +0 -15
  40. package/examples/python/playwright_example.py +0 -22
  41. package/jest.config.mjs +0 -20
  42. package/tests/ci/dependency-freshness.test.js +0 -74
  43. package/tests/ci/detect-code-changes.test.js +0 -174
  44. package/tests/ci/docker-install.test.js +0 -31
  45. package/tests/ci/rust-dockerfile.test.js +0 -54
  46. package/tests/ci/workflow-policy.test.js +0 -79
  47. package/tests/e2e/docker.test.js +0 -186
  48. package/tests/e2e/process.test.js +0 -215
  49. package/tests/fixtures/bold-coalesce-and-leakage.html +0 -27
  50. package/tests/fixtures/deepwiki/deepwiki-full-page.png +0 -0
  51. package/tests/fixtures/deepwiki/index.md +0 -1500
  52. package/tests/fixtures/deepwiki/part-1.md +0 -776
  53. package/tests/fixtures/habr-articles.js +0 -45
  54. package/tests/fixtures/heading-numbering.html +0 -19
  55. package/tests/fixtures/list-item-with-br.html +0 -8
  56. package/tests/fixtures/ol-continuous-numbering.html +0 -12
  57. package/tests/fixtures/paragraph-vs-line-break.html +0 -11
  58. package/tests/helpers/nock.js +0 -10
  59. package/tests/helpers/search-fixture.cjs +0 -17
  60. package/tests/helpers/temporary-directory.js +0 -12
  61. package/tests/integration/api-endpoints.test.js +0 -418
  62. package/tests/integration/archive-default-layout.test.js +0 -37
  63. package/tests/integration/browser-engines.test.js +0 -128
  64. package/tests/integration/browser-packaged-path.test.js +0 -45
  65. package/tests/integration/deepwiki-download.test.js +0 -144
  66. package/tests/integration/deepwiki-rendered-page.mock.test.js +0 -121
  67. package/tests/integration/formalai-contract.test.js +0 -214
  68. package/tests/integration/gdocs-public-doc.test.js +0 -315
  69. package/tests/integration/github-readme.test.js +0 -137
  70. package/tests/integration/google-drive-image.test.js +0 -246
  71. package/tests/integration/habr-article.test.js +0 -337
  72. package/tests/integration/search-endpoint.test.js +0 -88
  73. package/tests/integration/stackoverflow-download.test.js +0 -173
  74. package/tests/integration/wikipedia-download.test.js +0 -142
  75. package/tests/jest.setup.mjs +0 -7
  76. package/tests/mock/index.test.js +0 -225
  77. package/tests/unit/animation.test.js +0 -33
  78. package/tests/unit/batch.test.js +0 -114
  79. package/tests/unit/bold-coalesce-and-leakage.test.js +0 -54
  80. package/tests/unit/browser-launch-options.test.js +0 -42
  81. package/tests/unit/browser.test.js +0 -172
  82. package/tests/unit/cache.test.js +0 -182
  83. package/tests/unit/cli.test.js +0 -399
  84. package/tests/unit/convertGoogleDriveUrl.test.js +0 -89
  85. package/tests/unit/convertRelativeUrls.test.js +0 -34
  86. package/tests/unit/extract-images.test.js +0 -259
  87. package/tests/unit/fetch-html.test.js +0 -54
  88. package/tests/unit/figures.test.js +0 -74
  89. package/tests/unit/gdocs-export-headers.test.js +0 -28
  90. package/tests/unit/gdocs-preprocess.test.js +0 -36
  91. package/tests/unit/gdocs.test.js +0 -1490
  92. package/tests/unit/github.test.js +0 -156
  93. package/tests/unit/heading-image.test.js +0 -49
  94. package/tests/unit/heading-numbering.test.js +0 -28
  95. package/tests/unit/html2md-br-in-list-item.test.js +0 -23
  96. package/tests/unit/html2md-ol-numbering.test.js +0 -20
  97. package/tests/unit/html2md.test.js +0 -695
  98. package/tests/unit/image-mode-defaults.test.js +0 -54
  99. package/tests/unit/kreuzberg.test.js +0 -249
  100. package/tests/unit/latex.test.js +0 -123
  101. package/tests/unit/localize-images.test.js +0 -114
  102. package/tests/unit/markdown.test.js +0 -36
  103. package/tests/unit/metadata.test.js +0 -140
  104. package/tests/unit/package-runtime-dependencies.test.js +0 -74
  105. package/tests/unit/paragraph-vs-line-break.test.js +0 -56
  106. package/tests/unit/postprocess.test.js +0 -110
  107. package/tests/unit/proxy-headers.test.js +0 -100
  108. package/tests/unit/publish-verification.test.js +0 -57
  109. package/tests/unit/retry.test.js +0 -111
  110. package/tests/unit/search.test.js +0 -339
  111. package/tests/unit/shared-dialog.test.js +0 -173
  112. package/tests/unit/stackoverflow.test.js +0 -32
  113. package/tests/unit/stream.test.js +0 -85
  114. package/tests/unit/verify.test.js +0 -182
  115. package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +0 -375
  116. package/tests/xpaste/xpaste.e2e.test.js +0 -36
  117. package/tests/xpaste/xpaste.integration.test.js +0 -41
  118. package/tests/xpaste/xpaste.mock.test.js +0 -261
  119. package/tests/xpaste/xpaste.unit.test.js +0 -119
package/eslint.config.js DELETED
@@ -1,112 +0,0 @@
1
- import js from '@eslint/js';
2
- import prettierConfig from 'eslint-config-prettier';
3
- import prettierPlugin from 'eslint-plugin-prettier';
4
-
5
- export default [
6
- js.configs.recommended,
7
- prettierConfig,
8
- {
9
- files: ['**/*.js', '**/*.mjs'],
10
- plugins: {
11
- prettier: prettierPlugin,
12
- },
13
- languageOptions: {
14
- ecmaVersion: 'latest',
15
- sourceType: 'module',
16
- globals: {
17
- // Node.js globals
18
- console: 'readonly',
19
- process: 'readonly',
20
- Buffer: 'readonly',
21
- __dirname: 'readonly',
22
- __filename: 'readonly',
23
- global: 'readonly',
24
- setTimeout: 'readonly',
25
- clearTimeout: 'readonly',
26
- setInterval: 'readonly',
27
- clearInterval: 'readonly',
28
- // Node.js 18+ globals
29
- fetch: 'readonly',
30
- // Runtime-specific globals
31
- Bun: 'readonly',
32
- Deno: 'readonly',
33
- },
34
- },
35
- rules: {
36
- // Prettier integration
37
- 'prettier/prettier': 'error',
38
-
39
- // Code quality rules
40
- 'no-unused-vars': 'error',
41
- 'no-console': 'off', // Allow console in this project
42
- 'no-debugger': 'error',
43
-
44
- // Best practices
45
- eqeqeq: ['error', 'always'],
46
- curly: ['error', 'all'],
47
- 'no-var': 'error',
48
- 'prefer-const': 'error',
49
- 'prefer-arrow-callback': 'error',
50
- 'no-duplicate-imports': 'error',
51
-
52
- // ES6+ features
53
- 'arrow-body-style': ['error', 'as-needed'],
54
- 'object-shorthand': ['error', 'always'],
55
- 'prefer-template': 'error',
56
-
57
- // Async/await
58
- 'no-async-promise-executor': 'error',
59
- 'require-await': 'warn',
60
-
61
- // Comments and documentation
62
- 'spaced-comment': ['error', 'always', { markers: ['/'] }],
63
-
64
- // Complexity rules - reasonable thresholds for maintainability
65
- complexity: ['warn', 15], // Cyclomatic complexity - allow more complex logic than strict 8
66
- 'max-depth': ['warn', 5], // Maximum nesting depth - slightly more lenient than strict 4
67
- 'max-lines-per-function': [
68
- 'warn',
69
- {
70
- max: 150, // More reasonable than strict 50 lines per function
71
- skipBlankLines: true,
72
- skipComments: true,
73
- },
74
- ],
75
- 'max-params': ['warn', 6], // Maximum function parameters - slightly more lenient than strict 5
76
- 'max-statements': ['warn', 60], // Maximum statements per function - reasonable limit for orchestration functions
77
- 'max-lines': ['error', 1500], // Maximum lines per file - counts all lines including blank lines and comments
78
- },
79
- },
80
- {
81
- // Test files have different requirements
82
- files: ['tests/**/*.js', '**/*.test.js', '**/*.test.mjs'],
83
- languageOptions: {
84
- globals: {
85
- // Jest globals
86
- describe: 'readonly',
87
- it: 'readonly',
88
- test: 'readonly',
89
- expect: 'readonly',
90
- beforeAll: 'readonly',
91
- afterAll: 'readonly',
92
- beforeEach: 'readonly',
93
- afterEach: 'readonly',
94
- jest: 'readonly',
95
- },
96
- },
97
- rules: {
98
- 'require-await': 'off', // Async functions without await are common in tests
99
- 'max-lines-per-function': 'off', // Test files can have long test functions
100
- 'max-statements': 'off', // Test files can have many statements
101
- },
102
- },
103
- {
104
- ignores: [
105
- 'node_modules/**',
106
- 'coverage/**',
107
- 'dist/**',
108
- '*.min.js',
109
- '.eslintcache',
110
- ],
111
- },
112
- ];
@@ -1,12 +0,0 @@
1
- import { CaptureStore, CachedTransport } from '../../src/cache.js';
2
- import { fetchTransport } from '../../src/transport.js';
3
-
4
- const store = new CaptureStore('.captures');
5
- const request = { url: 'https://example.com', method: 'GET', headers: {} };
6
- const online = new CachedTransport(fetchTransport, store);
7
- const first = await online.capture(request);
8
- const offline = new CachedTransport(fetchTransport, store, { offline: true });
9
- const replay = await offline.capture(request);
10
- console.log(replay.sha256, replay.cached, replay.stale);
11
- console.log('Exact replay:', first.receipt.body.equals(replay.receipt.body));
12
- console.log('Portable fixture:', await store.export(request));
@@ -1,39 +0,0 @@
1
- import fetch from 'node-fetch';
2
- import fs from 'fs';
3
-
4
- const baseUrl = 'http://localhost:3000';
5
- const targetUrl = 'https://example.com';
6
-
7
- console.log('Comparing Puppeteer and Playwright engines...\n');
8
-
9
- // Fetch using Puppeteer
10
- console.log('1. Fetching HTML using Puppeteer (default)...');
11
- const startPuppeteer = Date.now();
12
- const puppeteerResponse = await fetch(
13
- `${baseUrl}/html?url=${encodeURIComponent(targetUrl)}`
14
- );
15
- const puppeteerHtml = await puppeteerResponse.text();
16
- const puppeteerTime = Date.now() - startPuppeteer;
17
- fs.writeFileSync('output/puppeteer_comparison.html', puppeteerHtml);
18
- console.log(` Time: ${puppeteerTime}ms`);
19
- console.log(` Size: ${puppeteerHtml.length} bytes`);
20
-
21
- // Fetch using Playwright
22
- console.log('\n2. Fetching HTML using Playwright...');
23
- const startPlaywright = Date.now();
24
- const playwrightResponse = await fetch(
25
- `${baseUrl}/html?url=${encodeURIComponent(targetUrl)}&engine=playwright`
26
- );
27
- const playwrightHtml = await playwrightResponse.text();
28
- const playwrightTime = Date.now() - startPlaywright;
29
- fs.writeFileSync('output/playwright_comparison.html', playwrightHtml);
30
- console.log(` Time: ${playwrightTime}ms`);
31
- console.log(` Size: ${playwrightHtml.length} bytes`);
32
-
33
- // Summary
34
- console.log('\n=== Summary ===');
35
- console.log(`Puppeteer: ${puppeteerTime}ms, ${puppeteerHtml.length} bytes`);
36
- console.log(`Playwright: ${playwrightTime}ms, ${playwrightHtml.length} bytes`);
37
- console.log(`Difference: ${Math.abs(puppeteerTime - playwrightTime)}ms`);
38
-
39
- console.log('\nResults saved to output/ directory.');
@@ -1,23 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
-
5
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
- const url = process.argv[2] || 'https://example.com';
7
- const endpoint = `http://localhost:3000/html?url=${encodeURIComponent(url)}`;
8
-
9
- fetch(endpoint)
10
- .then((res) => {
11
- if (!res.ok) {
12
- throw new Error(`HTTP error! status: ${res.status}`);
13
- }
14
- return res.text();
15
- })
16
- .then((text) => {
17
- const outputPath = path.join(__dirname, 'downloaded.html');
18
- fs.writeFileSync(outputPath, text, 'utf-8');
19
- console.log(`HTML saved to ${outputPath}`);
20
- })
21
- .catch((err) => {
22
- console.error('Error:', err);
23
- });
@@ -1,36 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
-
5
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
- const url = process.argv[2] || 'https://example.com';
7
- const endpoint = `http://localhost:3000/image?url=${encodeURIComponent(url)}`;
8
-
9
- fetch(endpoint)
10
- .then((res) => {
11
- console.log('Status:', res.status);
12
- console.log('Content-Type:', res.headers.get('content-type'));
13
- if (!res.ok) {
14
- throw new Error(`HTTP error! status: ${res.status}`);
15
- }
16
- return res.arrayBuffer();
17
- })
18
- .then((buffer) => {
19
- const buf = Buffer.from(buffer);
20
- const pngSignature = Buffer.from([
21
- 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
22
- ]);
23
- if (buf.slice(0, 8).equals(pngSignature)) {
24
- const outputPath = path.join(__dirname, 'downloaded.png');
25
- fs.writeFileSync(outputPath, buf);
26
- console.log(`Image saved to ${outputPath} (valid PNG)`);
27
- } else {
28
- console.log(
29
- 'Response is not a valid PNG! First bytes:',
30
- buf.slice(0, 16)
31
- );
32
- }
33
- })
34
- .catch((err) => {
35
- console.error('Error:', err);
36
- });
@@ -1,23 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
-
5
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
- const url = process.argv[2] || 'https://example.com';
7
- const endpoint = `http://localhost:3000/markdown?url=${encodeURIComponent(url)}`;
8
-
9
- fetch(endpoint)
10
- .then((res) => {
11
- if (!res.ok) {
12
- throw new Error(`HTTP error! status: ${res.status}`);
13
- }
14
- return res.text();
15
- })
16
- .then((text) => {
17
- const outputPath = path.join(__dirname, 'downloaded.md');
18
- fs.writeFileSync(outputPath, text, 'utf-8');
19
- console.log(`Markdown saved to ${outputPath}`);
20
- })
21
- .catch((err) => {
22
- console.error('Error:', err);
23
- });
@@ -1,23 +0,0 @@
1
- import fetch from 'node-fetch';
2
- import fs from 'fs';
3
-
4
- const baseUrl = 'http://localhost:3000';
5
- const targetUrl = 'https://example.com';
6
-
7
- console.log('Fetching HTML using Playwright engine...');
8
- const htmlResponse = await fetch(
9
- `${baseUrl}/html?url=${encodeURIComponent(targetUrl)}&engine=playwright`
10
- );
11
- const html = await htmlResponse.text();
12
- fs.writeFileSync('output/playwright_html.html', html);
13
- console.log('HTML saved to output/playwright_html.html');
14
-
15
- console.log('Fetching screenshot using Playwright engine...');
16
- const imageResponse = await fetch(
17
- `${baseUrl}/image?url=${encodeURIComponent(targetUrl)}&engine=playwright`
18
- );
19
- const imageBuffer = await imageResponse.buffer();
20
- fs.writeFileSync('output/playwright_screenshot.png', imageBuffer);
21
- console.log('Screenshot saved to output/playwright_screenshot.png');
22
-
23
- console.log('Done! Both requests used Playwright engine.');
@@ -1,15 +0,0 @@
1
- import sys
2
- import requests
3
- import os
4
-
5
- url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
6
- endpoint = f'http://localhost:3000/html?url={url}'
7
-
8
- response = requests.get(endpoint)
9
- response.raise_for_status()
10
-
11
- output_path = os.path.join(os.path.dirname(__file__), 'downloaded.html')
12
- with open(output_path, 'w', encoding='utf-8') as f:
13
- f.write(response.text)
14
-
15
- print(f'HTML saved to {output_path}')
@@ -1,20 +0,0 @@
1
- import sys
2
- import requests
3
- import os
4
-
5
- url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
6
- endpoint = f'http://localhost:3000/image?url={url}'
7
-
8
- response = requests.get(endpoint)
9
- print('Status:', response.status_code)
10
- print('Content-Type:', response.headers.get('content-type'))
11
-
12
- # PNG signature: first 8 bytes
13
- png_signature = b'\x89PNG\r\n\x1a\n'
14
- if response.content[:8] == png_signature:
15
- output_path = os.path.join(os.path.dirname(__file__), 'downloaded.png')
16
- with open(output_path, 'wb') as f:
17
- f.write(response.content)
18
- print(f'Image saved to {output_path} (valid PNG)')
19
- else:
20
- print('Response is not a valid PNG! First bytes:', response.content[:16])
@@ -1,15 +0,0 @@
1
- import sys
2
- import requests
3
- import os
4
-
5
- url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
6
- endpoint = f'http://localhost:3000/markdown?url={url}'
7
-
8
- response = requests.get(endpoint)
9
- response.raise_for_status()
10
-
11
- output_path = os.path.join(os.path.dirname(__file__), 'downloaded.md')
12
- with open(output_path, 'w', encoding='utf-8') as f:
13
- f.write(response.text)
14
-
15
- print(f'Markdown saved to {output_path}')
@@ -1,22 +0,0 @@
1
- import requests
2
- import os
3
-
4
- base_url = 'http://localhost:3000'
5
- target_url = 'https://example.com'
6
-
7
- # Create output directory if it doesn't exist
8
- os.makedirs('output', exist_ok=True)
9
-
10
- print('Fetching HTML using Playwright engine...')
11
- html_response = requests.get(f'{base_url}/html', params={'url': target_url, 'engine': 'playwright'})
12
- with open('output/playwright_html.html', 'w') as f:
13
- f.write(html_response.text)
14
- print('HTML saved to output/playwright_html.html')
15
-
16
- print('Fetching screenshot using Playwright engine...')
17
- image_response = requests.get(f'{base_url}/image', params={'url': target_url, 'engine': 'playwright'})
18
- with open('output/playwright_screenshot.png', 'wb') as f:
19
- f.write(image_response.content)
20
- print('Screenshot saved to output/playwright_screenshot.png')
21
-
22
- print('Done! Both requests used Playwright engine.')
package/jest.config.mjs DELETED
@@ -1,20 +0,0 @@
1
- export default {
2
- transform: {
3
- '^.+\\.js$': ['babel-jest', { configFile: './babel.config.cjs' }],
4
- },
5
- transformIgnorePatterns: ['node_modules/(?!(turndown)/)'],
6
- moduleNameMapper: {
7
- '^(\\.{1,2}/.*)\\.js$': '$1',
8
- },
9
- testEnvironment: 'node',
10
- verbose: true,
11
- testMatch: [
12
- '**/tests/ci/**/*.test.js',
13
- '**/tests/unit/**/*.test.js',
14
- '**/tests/mock/**/*.test.js',
15
- '**/tests/e2e/**/*.test.js',
16
- '**/tests/integration/**/*.test.js',
17
- '**/tests/xpaste/**/*.test.js',
18
- ],
19
- setupFiles: ['./tests/jest.setup.mjs'],
20
- };
@@ -1,74 +0,0 @@
1
- import { jest } from '@jest/globals';
2
- import { checkVersions } from '../../../scripts/check-dependency-freshness.mjs';
3
-
4
- const latest = async () => '2.0.0';
5
- const dependencies = [{ name: 'example', current: '1.0.0' }];
6
- const blockers = {
7
- example: {
8
- issue: 'https://github.com/example/repo/issues/1',
9
- reason: 'Upstream API breaks packaged consumers',
10
- },
11
- };
12
-
13
- test('freshness rejects old releases, missing packages and undocumented exceptions', async () => {
14
- for (const current of ['1.0.0', '(missing)']) {
15
- await expect(
16
- checkVersions([{ name: 'example', current }], {}, latest, jest.fn())
17
- ).rejects.toThrow('behind 2.0.0');
18
- }
19
- });
20
- test('freshness permits only documented open issues', async () => {
21
- await expect(
22
- checkVersions(dependencies, blockers, latest, async () => ({
23
- state: 'open',
24
- }))
25
- ).resolves.toBeUndefined();
26
- await expect(
27
- checkVersions(dependencies, blockers, latest, async () => ({
28
- state: 'closed',
29
- }))
30
- ).rejects.toThrow('is closed');
31
- await expect(
32
- checkVersions(
33
- dependencies,
34
- { example: { ...blockers.example, reason: '' } },
35
- latest,
36
- jest.fn()
37
- )
38
- ).rejects.toThrow('document an open issue');
39
- await expect(
40
- checkVersions(
41
- dependencies,
42
- { example: { ...blockers.example, reason: ' ' } },
43
- latest,
44
- jest.fn()
45
- )
46
- ).rejects.toThrow('document an open issue');
47
- });
48
- test('registry and issue failures fail the check rather than treating dependencies as fresh', async () => {
49
- await expect(
50
- checkVersions(
51
- dependencies,
52
- {},
53
- async () => {
54
- throw new Error('registry down');
55
- },
56
- jest.fn()
57
- )
58
- ).rejects.toThrow('registry down');
59
- await expect(
60
- checkVersions(dependencies, blockers, latest, async () => {
61
- throw new Error('issue unavailable');
62
- })
63
- ).rejects.toThrow('issue unavailable');
64
- });
65
- test('current releases need no exception', async () => {
66
- const getIssue = jest.fn();
67
- await checkVersions(
68
- [{ name: 'example', current: '2.0.0' }],
69
- {},
70
- latest,
71
- getIssue
72
- );
73
- expect(getIssue).not.toHaveBeenCalled();
74
- });
@@ -1,174 +0,0 @@
1
- import { execFileSync } from 'child_process';
2
- import {
3
- mkdirSync,
4
- mkdtempSync,
5
- readFileSync,
6
- rmSync,
7
- writeFileSync,
8
- } from 'fs';
9
- import { tmpdir } from 'os';
10
- import { dirname, join, resolve } from 'path';
11
- import { fileURLToPath } from 'url';
12
-
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = dirname(__filename);
15
- const repoRoot = resolve(__dirname, '../../..');
16
- const detectorPath = resolve(repoRoot, 'scripts/detect-code-changes.mjs');
17
-
18
- function run(command, args, cwd, options = {}) {
19
- return execFileSync(command, args, {
20
- cwd,
21
- encoding: 'utf8',
22
- stdio: ['ignore', 'pipe', 'pipe'],
23
- ...options,
24
- });
25
- }
26
-
27
- function git(repoPath, args) {
28
- return run('git', args, repoPath);
29
- }
30
-
31
- function writeTrackedFile(repoPath, relativePath, contents) {
32
- const filePath = join(repoPath, relativePath);
33
- mkdirSync(dirname(filePath), { recursive: true });
34
- writeFileSync(filePath, contents);
35
- }
36
-
37
- function commitAll(repoPath, message) {
38
- git(repoPath, ['add', '.']);
39
- git(repoPath, ['commit', '-m', message]);
40
- }
41
-
42
- function createMergedFeatureRepo() {
43
- const repoPath = mkdtempSync(join(tmpdir(), 'web-capture-detect-'));
44
-
45
- initializeRepo(repoPath);
46
-
47
- writeTrackedFile(repoPath, 'README.md', '# fixture\n');
48
- commitAll(repoPath, 'initial commit');
49
-
50
- git(repoPath, ['checkout', '-b', 'feature']);
51
- writeTrackedFile(repoPath, 'rust/src/lib.rs', 'pub fn capture() {}\n');
52
- commitAll(repoPath, 'add rust code');
53
-
54
- writeTrackedFile(repoPath, 'docs/notes.md', '# docs only tail commit\n');
55
- commitAll(repoPath, 'document rust change');
56
-
57
- git(repoPath, ['checkout', 'main']);
58
- git(repoPath, ['merge', '--no-ff', 'feature', '-m', 'merge feature']);
59
-
60
- return repoPath;
61
- }
62
-
63
- function createSingleCommitRepo() {
64
- return createSingleCommitRepoWithFiles({
65
- 'js/src/index.js': 'export const ok = true;\n',
66
- });
67
- }
68
-
69
- function createSingleCommitRepoWithFiles(files) {
70
- const repoPath = mkdtempSync(join(tmpdir(), 'web-capture-detect-'));
71
-
72
- initializeRepo(repoPath);
73
- for (const [relativePath, contents] of Object.entries(files)) {
74
- writeTrackedFile(repoPath, relativePath, contents);
75
- }
76
- commitAll(repoPath, 'initial commit');
77
-
78
- return repoPath;
79
- }
80
-
81
- function initializeRepo(repoPath) {
82
- git(repoPath, ['init']);
83
- git(repoPath, ['config', 'user.email', 'ci@example.test']);
84
- git(repoPath, ['config', 'user.name', 'CI Test']);
85
- git(repoPath, ['checkout', '-b', 'main']);
86
- }
87
-
88
- function parseOutputs(outputFile) {
89
- return Object.fromEntries(
90
- readFileSync(outputFile, 'utf8')
91
- .trim()
92
- .split('\n')
93
- .filter(Boolean)
94
- .map((line) => {
95
- const separatorIndex = line.indexOf('=');
96
- return [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)];
97
- })
98
- );
99
- }
100
-
101
- function runDetector(repoPath, eventName) {
102
- const outputFile = join(repoPath, `github-output-${eventName}.txt`);
103
- const env = {
104
- ...process.env,
105
- GITHUB_EVENT_NAME: eventName,
106
- GITHUB_OUTPUT: outputFile,
107
- };
108
- delete env.GITHUB_BASE_SHA;
109
- delete env.GITHUB_HEAD_SHA;
110
-
111
- run(process.execPath, [detectorPath], repoPath, { env });
112
-
113
- return parseOutputs(outputFile);
114
- }
115
-
116
- describe('detect-code-changes', () => {
117
- let repoPath;
118
-
119
- afterEach(() => {
120
- if (repoPath) {
121
- rmSync(repoPath, { recursive: true, force: true });
122
- repoPath = undefined;
123
- }
124
- });
125
-
126
- test('detects code introduced by a real merge commit pushed to main', () => {
127
- repoPath = createMergedFeatureRepo();
128
-
129
- const outputs = runDetector(repoPath, 'push');
130
-
131
- expect(outputs['rust-code-changed']).toBe('true');
132
- expect(outputs['rust-changed']).toBe('true');
133
- expect(outputs['any-rust-code-changed']).toBe('true');
134
- expect(outputs['any-code-changed']).toBe('true');
135
- expect(outputs['docs-changed']).toBe('true');
136
- });
137
-
138
- test('keeps pull request merge commits scoped to the PR head commit', () => {
139
- repoPath = createMergedFeatureRepo();
140
-
141
- const outputs = runDetector(repoPath, 'pull_request');
142
-
143
- expect(outputs['rust-code-changed']).toBe('false');
144
- expect(outputs['rust-changed']).toBe('false');
145
- expect(outputs['any-rust-code-changed']).toBe('false');
146
- expect(outputs['any-code-changed']).toBe('false');
147
- expect(outputs['docs-changed']).toBe('true');
148
- });
149
-
150
- test('detects files when a push only has one commit', () => {
151
- repoPath = createSingleCommitRepo();
152
-
153
- const outputs = runDetector(repoPath, 'push');
154
-
155
- expect(outputs['js-code-changed']).toBe('true');
156
- expect(outputs['js-changed']).toBe('true');
157
- expect(outputs['any-js-code-changed']).toBe('true');
158
- expect(outputs['any-code-changed']).toBe('true');
159
- });
160
-
161
- test('runs JS checks for CI tests without requiring a changeset', () => {
162
- repoPath = createSingleCommitRepoWithFiles({
163
- 'js/jest.config.mjs': 'export default {};\n',
164
- 'js/tests/ci/workflow-policy.test.js': 'test("ok", () => {});\n',
165
- });
166
-
167
- const outputs = runDetector(repoPath, 'push');
168
-
169
- expect(outputs['js-code-changed']).toBe('false');
170
- expect(outputs['js-changed']).toBe('true');
171
- expect(outputs['any-js-code-changed']).toBe('true');
172
- expect(outputs['any-code-changed']).toBe('true');
173
- });
174
- });
@@ -1,31 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { spawnSync } from 'node:child_process';
3
-
4
- test.each(['js', 'rust'])(
5
- '%s Docker build fails when every browser package installation attempt fails',
6
- (implementation) => {
7
- const dockerfile = readFileSync(
8
- new globalThis.URL(
9
- `../../../${implementation}/Dockerfile`,
10
- import.meta.url
11
- ),
12
- 'utf8'
13
- );
14
- const loop = dockerfile.match(/for i in 1 2 3; do[\s\S]*?done/)[0];
15
- // Execute only the finite retry loop; stub installation and waiting.
16
- const result = spawnSync(
17
- 'bash',
18
- [
19
- '-c',
20
- `
21
- apt-get() { printf '%s\\n' "$1"; [ "$1" = update ]; }
22
- sleep() { return 0; }
23
- ${loop}
24
- `,
25
- ],
26
- { encoding: 'utf8' }
27
- );
28
- expect(result.stdout.match(/^install$/gm)).toHaveLength(3);
29
- expect(result.status).toBe(1);
30
- }
31
- );