From 7d828209ceb1463e16c6c23618f84742a958ed5a Mon Sep 17 00:00:00 2001 From: overbalance Date: Fri, 12 Dec 2025 18:18:45 -0600 Subject: [PATCH] feat(lint): add type-aware parsing and validate script --- .github/workflows/build.yml | 4 +- eslint.config.js | 5 + eslint.dist.config.js | 23 + package-lock.json | 356 +++++++++++++++- package.json | 12 +- .../instrumentation-user-action/package.json | 6 +- scripts/validate.js | 397 ++++++++++++++++++ 7 files changed, 792 insertions(+), 11 deletions(-) create mode 100644 eslint.dist.config.js create mode 100644 scripts/validate.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 881900b..e55ed3f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,4 +27,6 @@ jobs: - run: npm run build - - run: npm run check-types + - run: npm run check:tsc + + - run: npm run validate diff --git a/eslint.config.js b/eslint.config.js index 5ca5e29..8b07a0e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -26,8 +26,13 @@ const licensePattern = export default [ { files: ['packages/*/src/**/*.{js,ts,mjs}'], + ignores: ['**/*.test.ts', '**/*.spec.ts'], languageOptions: { parser: tseslint.parser, + // enables type-aware linting to detect instance method usage + parserOptions: { + projectService: true, + }, }, plugins: { 'baseline-js': baselinePlugin, diff --git a/eslint.dist.config.js b/eslint.dist.config.js new file mode 100644 index 0000000..8858ace --- /dev/null +++ b/eslint.dist.config.js @@ -0,0 +1,23 @@ +import baselinePlugin from 'eslint-plugin-baseline-js'; +import baseConfig from './eslint.config.js'; + +// Extends base config and adds compiled output checking +export default [ + ...baseConfig, + // Compiled output - catches non-baseline APIs from dependencies + { + files: ['packages/*/dist/**/*.js'], + ignores: ['**/*.d.ts'], + plugins: { + 'baseline-js': baselinePlugin, + }, + rules: { + 'baseline-js/use-baseline': [ + 'error', + { + available: 'widely', + }, + ], + }, + }, +]; diff --git a/package-lock.json b/package-lock.json index 1eb0ad7..324ae69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@types/jsdom": "27.0.0", "@vitest/browser-playwright": "4.0.15", "@vitest/coverage-v8": "4.0.15", + "es-check": "9.5.2", "eslint": "9.39.1", "eslint-plugin-baseline-js": "0.4.2", "eslint-plugin-yet-another-license-header": "0.2.0", @@ -1548,6 +1549,44 @@ "@tybys/wasm-util": "^0.10.1" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -2925,6 +2964,16 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz", + "integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -2956,6 +3005,53 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2976,6 +3072,27 @@ "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", @@ -3284,6 +3401,13 @@ } } }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3334,6 +3458,25 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-check": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/es-check/-/es-check-9.5.2.tgz", + "integrity": "sha512-wzsnXh+zJrd8nhEBGFBWxCgH4cXC1/jny3uGdbIKB13/OQLjvQqfUMtZngH6UOTh1QCNV3Uzp063S5s9h+GJ/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "8.15.0", + "browserslist": "^4.28.0", + "fast-glob": "^3.3.3" + }, + "bin": { + "es-check": "lib/cli/index.js" + }, + "engines": { + "node": ">=18.0.0", + "pnpm": ">=9.0.0" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -3783,6 +3926,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3814,6 +3987,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3852,6 +4035,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", @@ -4200,6 +4396,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -4783,6 +4989,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4864,6 +5107,13 @@ "dev": true, "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -5169,6 +5419,27 @@ ], "license": "MIT" }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5222,6 +5493,17 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rolldown": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.53.tgz", @@ -5341,6 +5623,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -5611,6 +5917,19 @@ "dev": true, "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -5952,6 +6271,37 @@ } } }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6357,9 +6707,9 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "*", - "@opentelemetry/instrumentation": "*", - "@opentelemetry/web-utils": "*" + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/instrumentation": "0.208.0", + "@opentelemetry/web-utils": "0.1.0" } }, "packages/test-utils": { diff --git a/package.json b/package.json index b22a045..94d88fc 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,14 @@ "prepare": "lefthook install -f", "clean": "npx -y rimraf -g \"**/.turbo\" \"**/dist\" \"**/node_modules\"", "build": "turbo run build", - "check-types": "turbo run check-types", - "lint": "npm run lint:biome && npm run lint:eslint", + "check": "npm run check:tsc && npm run check:eslint", + "check:tsc": "turbo run check-types", + "check:eslint": "eslint", + "check:eslint:dist": "eslint --config eslint.dist.config.js packages/*/dist/", + "lint": "npm run lint:biome && npm run check", "lint:biome": "biome check --write", - "lint:eslint": "eslint --fix", - "lint:ci": "biome ci && eslint", + "lint:ci": "biome ci && npm run check", + "validate": "node scripts/validate.js", "test": "turbo run test", "test:watch": "turbo run test:watch", "test:coverage": "turbo run test:coverage" @@ -38,6 +41,7 @@ "@types/jsdom": "27.0.0", "@vitest/browser-playwright": "4.0.15", "@vitest/coverage-v8": "4.0.15", + "es-check": "9.5.2", "eslint": "9.39.1", "eslint-plugin-baseline-js": "0.4.2", "eslint-plugin-yet-another-license-header": "0.2.0", diff --git a/packages/instrumentation-user-action/package.json b/packages/instrumentation-user-action/package.json index 593860b..544aac8 100644 --- a/packages/instrumentation-user-action/package.json +++ b/packages/instrumentation-user-action/package.json @@ -34,9 +34,9 @@ "test:coverage": "vitest --coverage" }, "dependencies": { - "@opentelemetry/api-logs": "*", - "@opentelemetry/instrumentation": "*", - "@opentelemetry/web-utils": "*" + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/instrumentation": "0.208.0", + "@opentelemetry/web-utils": "0.1.0" }, "publishConfig": { "access": "public" diff --git a/scripts/validate.js b/scripts/validate.js new file mode 100644 index 0000000..cefc165 --- /dev/null +++ b/scripts/validate.js @@ -0,0 +1,397 @@ +#!/usr/bin/env node +/** + * Build validation for OpenTelemetry Browser packages + * + * Validates: + * 1. Syntax compliance (es-check on compiled output) + * 2. Web API baseline (eslint-plugin-baseline-js on compiled output) + * 3. Package exports & integrity (sourcemaps, imports, publint) + * 4. Bundle size + * 5. Module integrity (ESM-only, no require()) + */ + +import { execSync, spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import zlib from 'node:zlib'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const ROOT = path.join(__dirname, '..'); +const PACKAGES_DIR = path.join(ROOT, 'packages'); + +const COLORS = { + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + dim: '\x1b[2m', + bold: '\x1b[1m', + reset: '\x1b[0m', +}; + +function log(message, color = COLORS.reset) { + console.log(`${color}${message}${COLORS.reset}`); +} + +function logSection(title) { + console.log( + `\n${COLORS.blue}${COLORS.bold}═══ ${title} ═══${COLORS.reset}\n`, + ); +} + +function getPackagesWithDist() { + const packages = fs.readdirSync(PACKAGES_DIR); + return packages.filter((pkg) => { + const distPath = path.join(PACKAGES_DIR, pkg, 'dist'); + return fs.existsSync(distPath); + }); +} + +function getPackageJson(pkgName) { + const pkgJsonPath = path.join(PACKAGES_DIR, pkgName, 'package.json'); + return JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); +} + +// Verifies compiled bundles parse as expected ES version +function checkSyntaxCompliance() { + logSection('1. Syntax Compliance (es-check)'); + + const packages = getPackagesWithDist(); + + if (packages.length === 0) { + log(' ⚠ No packages with dist/ found', COLORS.yellow); + return true; + } + + const failures = []; + + for (const pkg of packages) { + const distPath = path.join(PACKAGES_DIR, pkg, 'dist'); + const pattern = `${distPath}/**/*.js`; + + const result = spawnSync( + 'npx', + ['es-check', 'es2022', pattern, '--module'], + { encoding: 'utf-8', stdio: 'pipe' }, + ); + + if (result.status !== 0) { + log(` ✗ ${pkg}`, COLORS.red); + failures.push(pkg); + } else { + log(` ✓ ${pkg}`, COLORS.green); + } + } + + if (failures.length > 0) { + return false; + } + + log('\n✓ All syntax checks passed', COLORS.green); + return true; +} + +// Runs eslint baseline-js on compiled output to catch non-baseline APIs from dependencies +function checkBaselineAPIs() { + logSection('2. Web API Baseline (eslint)'); + + const result = spawnSync('npm', ['run', 'check:eslint:dist'], { + encoding: 'utf-8', + stdio: 'pipe', + cwd: ROOT, + }); + + if (result.status !== 0) { + log(' ✗ Non-baseline APIs found in compiled output', COLORS.red); + if (result.stderr) { + log(result.stderr.trim(), COLORS.dim); + } + return false; + } + + log(' ✓ All APIs are baseline compatible', COLORS.green); + return true; +} + +function validateSourcemaps(pkgName) { + const distPath = path.join(PACKAGES_DIR, pkgName, 'dist'); + const jsFiles = fs.readdirSync(distPath).filter((f) => f.endsWith('.js')); + + for (const jsFile of jsFiles) { + const mapFile = `${jsFile}.map`; + const mapPath = path.join(distPath, mapFile); + + if (!fs.existsSync(mapPath)) { + continue; // Some files may not have sourcemaps + } + + try { + const map = JSON.parse(fs.readFileSync(mapPath, 'utf-8')); + if (!map.sources?.length) { + log(` ✗ ${mapFile} has no sources`, COLORS.red); + return false; + } + } catch (error) { + log(` ✗ Invalid sourcemap ${mapFile}: ${error.message}`, COLORS.red); + return false; + } + + // Check sourcemap reference in JS file + const jsContent = fs.readFileSync(path.join(distPath, jsFile), 'utf-8'); + if (!jsContent.includes('sourceMappingURL=')) { + log(` ✗ ${jsFile} missing sourcemap reference`, COLORS.red); + return false; + } + } + + return true; +} + +function getWorkspaceDependencies(pkgJson) { + const allDeps = { + ...pkgJson.dependencies, + ...pkgJson.peerDependencies, + }; + + const workspacePackages = getPackagesWithDist(); + const workspaceNames = workspacePackages.map( + (pkg) => getPackageJson(pkg).name, + ); + + return workspacePackages.filter( + (pkg) => + workspaceNames.includes(getPackageJson(pkg).name) && + Object.keys(allDeps).includes(getPackageJson(pkg).name), + ); +} + +function testPackageImports(pkgName) { + const pkgJson = getPackageJson(pkgName); + + // Skip import test for private packages (internal workspace deps) + if (pkgJson.private) { + log(` ⊘ Skipped (private package)`, COLORS.dim); + return true; + } + + const tempDir = fs.mkdtempSync(path.join(ROOT, '.tmp-')); + + try { + // Pack workspace dependencies first + const workspaceDeps = getWorkspaceDependencies(pkgJson); + for (const depPkg of workspaceDeps) { + const depDir = path.join(PACKAGES_DIR, depPkg); + execSync('npm pack --quiet', { cwd: depDir, stdio: 'pipe' }); + const depJson = getPackageJson(depPkg); + const safeName = depJson.name.replace('@', '').replace('/', '-'); + const tarball = fs + .readdirSync(depDir) + .find((f) => f.startsWith(safeName) && f.endsWith('.tgz')); + if (tarball) { + fs.renameSync( + path.join(depDir, tarball), + path.join(tempDir, `${depPkg}.tgz`), + ); + } + } + + // Pack the main package + const pkgDir = path.join(PACKAGES_DIR, pkgName); + execSync('npm pack --quiet', { cwd: pkgDir, stdio: 'pipe' }); + const safeName = pkgJson.name.replace('@', '').replace('/', '-'); + const tarball = fs + .readdirSync(pkgDir) + .find((f) => f.startsWith(safeName) && f.endsWith('.tgz')); + + if (!tarball) { + throw new Error(`Failed to create tarball for ${pkgName}`); + } + + fs.renameSync( + path.join(pkgDir, tarball), + path.join(tempDir, 'package.tgz'), + ); + + // Create package.json with workspace deps as file references + const testPkgJson = { type: 'module' }; + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify(testPkgJson), + ); + + // Install workspace dependencies first + for (const depPkg of workspaceDeps) { + execSync(`npm install ./${depPkg}.tgz`, { cwd: tempDir, stdio: 'pipe' }); + } + + // Install the main package + execSync('npm install ./package.tgz', { cwd: tempDir, stdio: 'pipe' }); + + // Test ESM import + execSync( + `node --input-type=module -e "import * as pkg from '${pkgJson.name}'; if (!pkg) process.exit(1);"`, + { cwd: tempDir, stdio: 'pipe' }, + ); + + return true; + } catch (error) { + log(` ✗ Import test failed: ${error.message}`, COLORS.red); + return false; + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} + +function checkPackageExports() { + logSection('3. Package Exports & Integrity'); + + const packages = getPackagesWithDist(); + let allPassed = true; + + for (const pkg of packages) { + log(` ${pkg}:`, COLORS.blue); + + // Validate sourcemaps + if (!validateSourcemaps(pkg)) { + allPassed = false; + continue; + } + log(` ✓ Sourcemaps valid`, COLORS.green); + + // Test imports + if (!testPackageImports(pkg)) { + allPassed = false; + continue; + } + log(` ✓ ESM imports work`, COLORS.green); + } + + // Run publint on each package + log(`\n Running publint...`, COLORS.blue); + for (const pkg of packages) { + const pkgDir = path.join(PACKAGES_DIR, pkg); + try { + execSync('npx publint', { cwd: pkgDir, stdio: 'pipe' }); + log(` ✓ ${pkg}`, COLORS.green); + } catch (error) { + log(` ⚠ ${pkg} (warnings)`, COLORS.yellow); + if (error.stdout) { + log(` ${error.stdout.toString().trim()}`, COLORS.dim); + } + } + } + + if (allPassed) { + log('\n✓ Package exports valid', COLORS.green); + } + return allPassed; +} + +function checkBundleSize() { + logSection('4. Bundle Size'); + + const packages = getPackagesWithDist(); + const MAX_SIZE_KB = 50; // Warn if any package exceeds this + + for (const pkg of packages) { + const distPath = path.join(PACKAGES_DIR, pkg, 'dist'); + const jsFiles = fs + .readdirSync(distPath) + .filter((f) => f.endsWith('.js') && !f.endsWith('.map')); + + let totalRaw = 0; + let totalGzip = 0; + + for (const jsFile of jsFiles) { + const content = fs.readFileSync(path.join(distPath, jsFile)); + totalRaw += content.length; + totalGzip += zlib.gzipSync(content).length; + } + + const gzipKB = totalGzip / 1024; + const color = gzipKB > MAX_SIZE_KB ? COLORS.yellow : COLORS.green; + + log( + ` ${pkg}: ${(totalRaw / 1024).toFixed(2)} KB (${gzipKB.toFixed(2)} KB gzipped)`, + color, + ); + } + + return true; +} + +// Validates ESM files don't use require() (causes runtime errors) +function validateModuleIntegrity() { + logSection('5. Module Integrity'); + + const packages = getPackagesWithDist(); + let allPassed = true; + + for (const pkg of packages) { + const distPath = path.join(PACKAGES_DIR, pkg, 'dist'); + + const result = spawnSync( + 'grep', + ['-r', 'require(', distPath, '--include=*.js'], + { + encoding: 'utf-8', + stdio: 'pipe', + }, + ); + + if (result.status === 0) { + // Found matches - this is bad + log(` ✗ ${pkg}: Found require() in ESM files`, COLORS.red); + if (result.stdout) { + log(` ${result.stdout.trim().slice(0, 200)}`, COLORS.dim); + } + allPassed = false; + } else { + log(` ✓ ${pkg}: No require() in ESM files`, COLORS.green); + } + } + + if (allPassed) { + log('\n✓ Module integrity validated', COLORS.green); + } + return allPassed; +} + +function main() { + logSection('OpenTelemetry Browser Validation'); + + const packages = getPackagesWithDist(); + if (packages.length === 0) { + log('✗ No packages with dist/ found. Run npm run build first.', COLORS.red); + process.exit(1); + } + + log(`Found ${packages.length} packages with dist/`, COLORS.dim); + + const results = [ + { name: 'Syntax compliance', passed: checkSyntaxCompliance() }, + { name: 'Web API baseline', passed: checkBaselineAPIs() }, + { name: 'Package exports', passed: checkPackageExports() }, + { name: 'Bundle size', passed: checkBundleSize() }, + { name: 'Module integrity', passed: validateModuleIntegrity() }, + ]; + + logSection('Validation Summary'); + + results.forEach(({ name, passed }) => { + log(` ${passed ? '✓' : '✗'} ${name}`, passed ? COLORS.green : COLORS.red); + }); + + const allPassed = results.every((r) => r.passed); + + log( + allPassed ? '\n✓ All validations passed!' : '\n✗ Some validations failed', + allPassed ? COLORS.green + COLORS.bold : COLORS.red + COLORS.bold, + ); + + process.exit(allPassed ? 0 : 1); +} + +main();