diff --git a/.gitignore b/.gitignore index abd703b3b3893..dfc41dfbf6d0c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ target/ /benchmark/node_modules/ /tasks/benchmark/codspeed/node_modules/ /tasks/transform_conformance/node_modules/ +/tasks/compat_data/node_modules/ # vscode /editors/vscode/node_modules/ diff --git a/Cargo.lock b/Cargo.lock index 0dab5c61c93ba..d495e28ec1337 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1567,6 +1567,10 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "oxc_compat_data" +version = "0.0.0" + [[package]] name = "oxc_coverage" version = "0.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78662f2efd1a9..b6bf35630fd0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,12 @@ importers: specifier: ^7.2.0 version: 7.4.3 + tasks/compat_data: + devDependencies: + degit: + specifier: 2.8.4 + version: 2.8.4 + tasks/transform_conformance: devDependencies: '@babel/runtime': @@ -1294,6 +1300,11 @@ packages: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} + degit@2.8.4: + resolution: {integrity: sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng==} + engines: {node: '>=8.0.0'} + hasBin: true + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -3301,6 +3312,8 @@ snapshots: define-lazy-prop@2.0.0: {} + degit@2.8.4: {} + delayed-stream@1.0.0: {} depd@2.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2c2fae36084fb..50a625a47c4c2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,10 +1,11 @@ packages: - 'napi/**' - 'wasm/**' + - 'npm/**' - 'editors/**' - 'tasks/benchmark/codspeed' - 'tasks/transform_conformance' - - 'npm/**' + - 'tasks/compat_data' catalog: "@napi-rs/cli": 3.0.0-alpha.61 diff --git a/tasks/compat_data/.gitignore b/tasks/compat_data/.gitignore new file mode 100644 index 0000000000000..e97c3b53b5c0b --- /dev/null +++ b/tasks/compat_data/.gitignore @@ -0,0 +1 @@ +compat-table/ diff --git a/tasks/compat_data/Cargo.toml b/tasks/compat_data/Cargo.toml new file mode 100644 index 0000000000000..272aa18fa4384 --- /dev/null +++ b/tasks/compat_data/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "oxc_compat_data" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish = false + +[lints] +workspace = true + +[lib] +test = false +doctest = false diff --git a/tasks/compat_data/README.md b/tasks/compat_data/README.md new file mode 100644 index 0000000000000..d7acd5aec278b --- /dev/null +++ b/tasks/compat_data/README.md @@ -0,0 +1 @@ +Code extracted from https://github.com/babel/babel/tree/main/packages/babel-compat-data diff --git a/tasks/compat_data/build.js b/tasks/compat_data/build.js new file mode 100644 index 0000000000000..b4e11880895e2 --- /dev/null +++ b/tasks/compat_data/build.js @@ -0,0 +1,172 @@ +// https://github.com/babel/babel/blob/main/packages/babel-compat-data/scripts/build-data.js +// https://github.com/babel/babel/blob/main/packages/babel-compat-data/scripts/utils-build-data.js + +const fs = require('node:fs'); +const envs = require('./compat-table/environments'); +const parseEnvsVersions = require('./compat-table/build-utils/parse-envs-versions'); +const interpolateAllResults = require('./compat-table/build-utils/interpolate-all-results'); +const compareVersions = require('./compat-table/build-utils/compare-versions'); +const { addElectronSupportFromChromium } = require('./chromium-to-electron'); + +const environments = [ + 'chrome', + 'opera', + 'edge', + 'firefox', + 'safari', + 'node', + 'deno', + 'ie', + 'android', + 'ios', + // 'phantom', + 'samsung', + 'rhino', + 'opera_mobile', +]; + +const envsVersions = parseEnvsVersions(envs); + +const compatSources = ['es5', 'es6', 'es2016plus', 'esnext'].map(source => { + const data = require(`./compat-table/data-${source}`); + interpolateAllResults(data.tests, envs); + return data; +}); + +const compatibilityTests = compatSources.flatMap(data => + data.tests.flatMap(test => { + if (!test.subtests) return test; + + return test.subtests.map(subtest => + Object.assign({}, subtest, { + name: test.name + ' / ' + subtest.name, + group: test.name, + }) + ); + }) +); + +const getLowestImplementedVersion = ( + { features }, + env, + exclude = () => false, +) => { + const tests = compatibilityTests.filter(test => { + let ok = features.includes(test.name); + ok ||= test.group && features.includes(test.group); + ok ||= features.length === 1 && test.name.startsWith(features[0]); + ok &&= !exclude(test.name); + return ok; + }); + + const envTests = tests.map(({ res }) => { + const versions = envsVersions[env]; + let i = versions.length - 1; + + // Find the last not-implemented version + for (; i >= 0; i--) { + const { id } = versions[i]; + // Babel assumes strict mode + if (res[id] !== true && res[id] !== 'strict') { + break; + } + } + + return envsVersions[env][i + 1]; + }); + + if (envTests.length === 0 || envTests.some(t => !t)) return null; + + const result = envTests.reduce((a, b) => { + return compareVersions(a, b) > 0 ? a : b; + }); + + // NOTE(bng): A number of environments in compat-table changed to + // include a trailing zero (node10 -> node10_0), so for now stripping + // it to be consistent + return result.version.join('.').replace(/\.0$/, ''); +}; + +const expandFeatures = features => + features.flatMap(feat => { + if (feat.includes('/')) return [feat]; + return compatibilityTests + .map(test => test.name) + .filter(name => name === feat || name.startsWith(feat + ' / ')); + }); + +const generateData = (environments, features) => { + const data = {}; + + const normalized = {}; + for (const [key, options] of Object.entries(features)) { + if (options.overwrite) { + if (!options.replaces || options.features) { + throw new Error( + `.overwrite is only supported when using .replace and not defining .features (${key})`, + ); + } + options.features = features[options.replaces].features; + } + if (!options.features) { + normalized[key] = { + features: expandFeatures([options]), + }; + } else { + normalized[key] = { + ...options, + features: expandFeatures(options.features), + }; + } + } + + const overlapping = {}; + + // Apply bugfixes + for ( + const [key, { features, replaces, overwrite }] of Object.entries( + normalized, + ) + ) { + if (replaces) { + if (normalized[replaces].replaces) { + throw new Error(`Transitive replacement is not supported (${key})`); + } + + if (overwrite) { + normalized[key] = { + features: normalized[replaces].features, + overwrite, + }; + } else { + normalized[replaces].features = normalized[replaces].features.filter( + feat => !features.includes(feat), + ); + } + + if (!overlapping[replaces]) overlapping[replaces] = []; + overlapping[replaces].push(key); + } + } + + // eslint-disable-next-line prefer-const + for (let [key, options] of Object.entries(normalized)) { + const plugin = {}; + + environments.forEach(env => { + const version = getLowestImplementedVersion(options, env); + if (version) plugin[env] = version; + }); + addElectronSupportFromChromium(plugin); + + if (options.overwrite) Object.assign(plugin, options.overwrite); + + data[key] = plugin; + } + + return { data, overlapping }; +}; + +const { data } = generateData(environments, require(`./plugin-features`)); + +fs.writeFileSync('./data.json', JSON.stringify(data, null, 2)); diff --git a/tasks/compat_data/chromium-to-electron.js b/tasks/compat_data/chromium-to-electron.js new file mode 100644 index 0000000000000..ed01aeda05d1b --- /dev/null +++ b/tasks/compat_data/chromium-to-electron.js @@ -0,0 +1,25 @@ +// https://github.com/babel/babel/blob/main/packages/babel-compat-data/scripts/chromium-to-electron.js + +const chromiumVersions = require('./chromium-versions'); +const chromiumVersionList = Object.keys(chromiumVersions); + +function chromiumToElectron(version) { + if (chromiumVersions[version]) { + return chromiumVersions[version]; + } + const supportedVersion = chromiumVersionList.concat(version); + supportedVersion.sort((a, b) => +a - +b); + const nextSupportedVersion = supportedVersion[supportedVersion.indexOf(version) + 1]; + return chromiumVersions[nextSupportedVersion]; +} + +function addElectronSupportFromChromium(supportData) { + if (supportData.chrome) { + const electronVersion = chromiumToElectron(supportData.chrome); + if (electronVersion) { + supportData.electron = electronVersion; + } + } +} + +module.exports.addElectronSupportFromChromium = addElectronSupportFromChromium; diff --git a/tasks/compat_data/chromium-versions.js b/tasks/compat_data/chromium-versions.js new file mode 100644 index 0000000000000..c1e1ea0159e2d --- /dev/null +++ b/tasks/compat_data/chromium-versions.js @@ -0,0 +1,74 @@ +// https://github.com/Kilian/electron-to-chromium/blob/master/chromium-versions.js + +module.exports = { + '39': '0.20', + '40': '0.21', + '41': '0.21', + '42': '0.25', + '43': '0.27', + '44': '0.30', + '45': '0.31', + '47': '0.36', + '49': '0.37', + '50': '1.1', + '51': '1.2', + '52': '1.3', + '53': '1.4', + '54': '1.4', + '56': '1.6', + '58': '1.7', + '59': '1.8', + '61': '2.0', + '66': '3.0', + '69': '4.0', + '72': '5.0', + '73': '5.0', + '76': '6.0', + '78': '7.0', + '79': '8.0', + '80': '8.0', + '82': '9.0', + '83': '9.0', + '84': '10.0', + '85': '10.0', + '86': '11.0', + '87': '11.0', + '89': '12.0', + '90': '13.0', + '91': '13.0', + '92': '14.0', + '93': '14.0', + '94': '15.0', + '95': '16.0', + '96': '16.0', + '98': '17.0', + '99': '18.0', + '100': '18.0', + '102': '19.0', + '103': '20.0', + '104': '20.0', + '105': '21.0', + '106': '21.0', + '107': '22.0', + '108': '22.0', + '110': '23.0', + '111': '24.0', + '112': '24.0', + '114': '25.0', + '116': '26.0', + '118': '27.0', + '119': '28.0', + '120': '28.0', + '121': '29.0', + '122': '29.0', + '123': '30.0', + '124': '30.0', + '125': '31.0', + '126': '31.0', + '127': '32.0', + '128': '32.0', + '129': '33.0', + '130': '33.0', + '131': '34.0', + '132': '34.0', +}; diff --git a/tasks/compat_data/package.json b/tasks/compat_data/package.json new file mode 100644 index 0000000000000..bf0b293b01e34 --- /dev/null +++ b/tasks/compat_data/package.json @@ -0,0 +1,10 @@ +{ + "name": "compat-data", + "scripts": { + "init": "degit compat-table/compat-table#4fd1acb5bdd34846cb18887310f7a8000989c34e compat-table", + "build": "node build.js" + }, + "devDependencies": { + "degit": "2.8.4" + } +} diff --git a/tasks/compat_data/plugin-features.js b/tasks/compat_data/plugin-features.js new file mode 100644 index 0000000000000..dd8326d025901 --- /dev/null +++ b/tasks/compat_data/plugin-features.js @@ -0,0 +1,224 @@ +// https://github.com/babel/babel/blob/main/packages/babel-compat-data/scripts/data/plugin-features.js + +// WARNING: Plugin ordering is important. Don't reorder this file +// without checking that it doesn't break anything. + +const es5 = { + 'transform-member-expression-literals': 'Object/array literal extensions / Reserved words as property names', + 'transform-property-literals': 'Object/array literal extensions / Reserved words as property names', + 'transform-reserved-words': 'Miscellaneous / Unreserved words', +}; + +// https://github.com/babel/babel/issues/11278 +// transform-parameters should run before object-rest-spread +const es2015Parameter = { + 'transform-parameters': { + features: [ + 'default function parameters', + 'rest parameters', + 'destructuring, parameters / aliased defaults, arrow function', + 'destructuring, parameters / shorthand defaults, arrow function', + 'destructuring, parameters / duplicate identifier', + ], + }, +}; + +const es2015 = { + 'transform-template-literals': { + features: ['template literals'], + }, + 'transform-literals': { + features: ['Unicode code point escapes'], + }, + 'transform-function-name': { + features: ['function "name" property'], + }, + 'transform-arrow-functions': { + features: [ + 'arrow functions / 0 parameters', + 'arrow functions / 1 parameter, no brackets', + 'arrow functions / multiple parameters', + 'arrow functions / lexical "this" binding', + 'arrow functions / "this" unchanged by call or apply', + "arrow functions / can't be bound, can be curried", + 'arrow functions / lexical "arguments" binding', + 'arrow functions / no line break between params and =>', + 'arrow functions / correct precedence', + 'arrow functions / no "prototype" property', + ], + }, + 'transform-block-scoped-functions': { + features: ['block-level function declaration'], + }, + 'transform-classes': { + features: [ + 'class', + 'super', + 'arrow functions / lexical "super" binding in constructors', + 'arrow functions / lexical "super" binding in methods', + ], + }, + 'transform-object-super': { + features: ['super'], + }, + 'transform-shorthand-properties': { + features: ['object literal extensions / shorthand properties'], + }, + 'transform-duplicate-keys': { + features: ['miscellaneous / duplicate property names in strict mode'], + }, + 'transform-computed-properties': { + features: ['object literal extensions / computed properties'], + }, + 'transform-for-of': { + features: ['for..of loops'], + }, + 'transform-sticky-regex': { + features: [ + 'RegExp "y" and "u" flags / "y" flag, lastIndex', + 'RegExp "y" and "u" flags / "y" flag', + ], + }, + 'transform-unicode-escapes': 'Unicode code point escapes', + 'transform-unicode-regex': { + features: [ + 'RegExp "y" and "u" flags / "u" flag, case folding', + 'RegExp "y" and "u" flags / "u" flag, Unicode code point escapes', + 'RegExp "y" and "u" flags / "u" flag, non-BMP Unicode characters', + 'RegExp "y" and "u" flags / "u" flag', + ], + }, + 'transform-spread': { + features: [ + 'spread syntax for iterable objects', + // We need to compile classes when spread is not supported, because + // we cannot compile super(...args) without also rewriting the + // "super" handling. There is a bugfix that makes it better. + 'class', + 'super', + ], + }, + 'transform-destructuring': { + features: ['destructuring, assignment', 'destructuring, declarations'], + }, + 'transform-block-scoping': { + features: [ + 'const', + 'let', + // regenerator-transform doesn't support let/const, + // so we must compile them when compiling generators. + 'generators', + ], + }, + 'transform-typeof-symbol': { + features: ['Symbol / typeof support'], + }, + 'transform-new-target': { + features: ['new.target', 'arrow functions / lexical "new.target" binding'], + }, + 'transform-regenerator': { + features: ['generators'], + }, +}; + +const es2016 = { + 'transform-exponentiation-operator': { + features: ['exponentiation (**) operator'], + }, +}; + +const es2017 = { + 'transform-async-to-generator': { + features: ['async functions'], + }, +}; + +const es2018 = { + 'transform-async-generator-functions': 'Asynchronous Iterators', + 'transform-object-rest-spread': 'object rest/spread properties', + + 'transform-dotall-regex': 's (dotAll) flag for regular expressions', + 'transform-unicode-property-regex': 'RegExp Unicode Property Escapes / basic', + 'transform-named-capturing-groups-regex': 'RegExp named capture groups', +}; + +const es2019 = { + 'transform-json-strings': 'JSON superset', + 'transform-optional-catch-binding': 'optional catch binding', +}; + +const es2020 = { + 'transform-nullish-coalescing-operator': 'nullish coalescing operator (??)', + 'transform-optional-chaining': 'optional chaining operator (?.)', +}; + +const es2021 = { + 'transform-numeric-separator': 'numeric separators', + 'transform-logical-assignment-operators': 'Logical Assignment', +}; + +const es2022 = { + 'bugfix/transform-v8-static-class-fields-redefine-readonly': { + features: ['static class fields / static class fields use [[Define]]'], + replaces: 'transform-class-properties', + }, + 'bugfix/transform-firefox-class-in-computed-class-key': { + replaces: 'transform-class-properties', + overwrite: { + // TODO: Once Firefox releases the fix, write the correct version here. + firefox: undefined, + }, + }, + 'bugfix/transform-safari-class-field-initializer-scope': { + features: ['instance class fields / resolving identifier in parent scope'], + replaces: 'transform-class-properties', + }, + 'transform-class-static-block': 'Class static initialization blocks', + 'transform-private-property-in-object': 'Ergonomic brand checks for private fields', + 'transform-class-properties': { + features: [ + 'static class fields', + 'instance class fields / public instance class fields', + 'instance class fields / private instance class fields basic support', + 'instance class fields / computed instance class fields', + 'instance class fields / resolving identifier in parent scope', + ], + }, + 'transform-private-methods': 'private class methods', +}; + +const es2024 = { + 'transform-unicode-sets-regex': { + features: [ + 'RegExp `v` flag / set notations', + 'RegExp `v` flag / properties of Strings', + 'RegExp `v` flag / constructor supports it', + 'RegExp `v` flag / shows up in flags', + ], + }, +}; + +const es2025 = { + 'transform-duplicate-named-capturing-groups-regex': 'Duplicate named capturing groups', + 'transform-regexp-modifiers': 'RegExp Pattern Modifiers', +}; + +const shippedProposal = {}; + +// Run plugins for modern features first +module.exports = Object.assign( + {}, + shippedProposal, + es2025, + es2024, + es2022, + es2021, + es2020, + es2019, + es2015Parameter, + es2018, + es2017, + es2016, + es2015, + es5, +); diff --git a/tasks/compat_data/src/lib.rs b/tasks/compat_data/src/lib.rs new file mode 100644 index 0000000000000..8b137891791fe --- /dev/null +++ b/tasks/compat_data/src/lib.rs @@ -0,0 +1 @@ + diff --git a/tasks/compat_data/src/main.rs b/tasks/compat_data/src/main.rs new file mode 100644 index 0000000000000..f328e4d9d04c3 --- /dev/null +++ b/tasks/compat_data/src/main.rs @@ -0,0 +1 @@ +fn main() {}