From af464c25eecc33caf5f44f20a6132224fc6dc5ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:05:00 +0000 Subject: [PATCH 1/2] chore(deps-dev): bump undici from 7.18.2 to 7.29.1 Bumps [undici](https://github.com/nodejs/undici) from 7.18.2 to 7.29.1. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.18.2...v7.29.1) --- updated-dependencies: - dependency-name: undici dependency-version: 7.29.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 93cfbfc..6b13497 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2339,9 +2339,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", "dev": true, "license": "MIT", "engines": { From b76696bfbe53f7d58863ad7be7bc3e5396324932 Mon Sep 17 00:00:00 2001 From: Nikolai Denissov Date: Fri, 9 Oct 2026 08:46:36 +0300 Subject: [PATCH 2/2] test: guard locked extension packaging and dependency contracts --- .github/workflows/release.yml | 8 ++--- .vscodeignore | 2 ++ package.json | 7 +++-- tests/dependencies/packaging.test.cjs | 44 +++++++++++++++++++++++++++ tests/dependencies/undici.test.cjs | 35 +++++++++++++++++++++ 5 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 tests/dependencies/packaging.test.cjs create mode 100644 tests/dependencies/undici.test.cjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ac5049..6835fdf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '24' - name: Install dependencies run: npm ci @@ -26,11 +26,11 @@ jobs: - name: Compile TypeScript run: npm run compile - - name: Install vsce - run: npm install -g @vscode/vsce + - name: Test dependency and packaging contracts + run: npm run test:dependencies - name: Package extension - run: vsce package + run: npm run package - name: Upload VSIX artifact uses: actions/upload-artifact@v4 diff --git a/.vscodeignore b/.vscodeignore index cc4b81b..30f5462 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -7,3 +7,5 @@ tsconfig.json **/*.map **/*.ts .gitignore + +tests/** diff --git a/package.json b/package.json index 7d602fc..90de050 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,9 @@ { "language": "stlcpp", "scopes": { - "boolean": ["constant.language.boolean"] + "boolean": [ + "constant.language.boolean" + ] } } ] @@ -52,7 +54,8 @@ "vscode:prepublish": "npm run compile", "compile": "tsc -p ./", "watch": "tsc -watch -p ./", - "package": "vsce package" + "package": "vsce package", + "test:dependencies": "node --test tests/dependencies/*.test.cjs" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/tests/dependencies/packaging.test.cjs b/tests/dependencies/packaging.test.cjs new file mode 100644 index 0000000..805908a --- /dev/null +++ b/tests/dependencies/packaging.test.cjs @@ -0,0 +1,44 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { mkdtempSync, readFileSync, rmSync } = require('node:fs'); +const { tmpdir } = require('node:os'); +const { join, dirname } = require('node:path'); +const { execFileSync } = require('node:child_process'); +const vsceManifest = require.resolve('@vscode/vsce/package.json'); + +function readArchive(file) { + // Python's standard ZIP reader verifies the packaged artifact independently + // of the ZIP implementation used by vsce itself. + const script = `import base64,json,sys,zipfile +with zipfile.ZipFile(sys.argv[1]) as archive: + wanted = {'extension/package.json', 'extension/wasm/tree-sitter-stlcpp.wasm'} + print(json.dumps({name: base64.b64encode(archive.read(name)).decode() if name in wanted else '' for name in archive.namelist()}))`; + const contents = JSON.parse(execFileSync('python3', ['-c', script, file], { encoding: 'utf8', timeout: 30_000 })); + return new Map(Object.entries(contents).map(([name, data]) => [name, Buffer.from(data, 'base64')])); +} + +test('the locked vsce packages a loadable extension without source or test files', async () => { + const temporary = mkdtempSync(join(tmpdir(), 'stlcpp-vsix-')); + try { + const output = join(temporary, 'extension.vsix'); + execFileSync(process.execPath, [join(dirname(vsceManifest), 'vsce'), 'package', '--out', output], { + cwd: process.cwd(), timeout: 120_000, stdio: 'pipe', + }); + const files = await readArchive(output); + const manifest = JSON.parse(files.get('extension/package.json')); + assert.equal(manifest.publisher, 'aalto-opencs'); + assert.equal(manifest.contributes.languages[0].id, 'stlcpp'); + for (const name of ['out/extension.js', 'language-configuration.json', 'wasm/tree-sitter-stlcpp.wasm', + 'node_modules/web-tree-sitter/package.json']) { + assert.ok(files.has(`extension/${name}`), `Missing runtime asset: ${name}`); + } + assert.deepEqual(files.get('extension/wasm/tree-sitter-stlcpp.wasm'), readFileSync('wasm/tree-sitter-stlcpp.wasm')); + assert.ok(files.has('extension/README.md') || files.has('extension/readme.md')); + for (const name of files.keys()) { + assert.doesNotMatch(name, /^extension\/(?:src|tests|\.github)\//); + assert.doesNotMatch(name, /^extension\/node_modules\/@vscode\/vsce\//); + } + } finally { + rmSync(temporary, { recursive: true, force: true }); + } +}); diff --git a/tests/dependencies/undici.test.cjs b/tests/dependencies/undici.test.cjs new file mode 100644 index 0000000..b2edc2d --- /dev/null +++ b/tests/dependencies/undici.test.cjs @@ -0,0 +1,35 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { createServer } = require('node:http'); +const { once } = require('node:events'); +const { fetch, Headers } = require('undici'); + +test('the packaging HTTP client follows redirects and decodes split UTF-8 JSON', async () => { + const body = Buffer.from(JSON.stringify({ name: 'STLC++', description: 'λ → ✓' })); + const server = createServer((request, response) => { + if (request.url === '/redirect') { + response.writeHead(302, { location: '/metadata?extension=stlcpp' }); + response.end(); + return; + } + assert.equal(request.url, '/metadata?extension=stlcpp'); + response.writeHead(200, { 'content-type': 'application/json' }); + for (const byte of body) response.write(Buffer.from([byte])); + response.end(); + }); + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + try { + const response = await fetch(`http://127.0.0.1:${server.address().port}/redirect`); + assert.equal(response.status, 200); + assert.equal(response.redirected, true); + assert.deepEqual(await response.json(), { name: 'STLC++', description: 'λ → ✓' }); + } finally { + server.closeAllConnections(); + await new Promise((resolve) => server.close(resolve)); + } +}); + +test('header values cannot inject a second HTTP header', () => { + assert.throws(() => new Headers({ authorization: 'token\r\nX-Injected: true' }), TypeError); +});