userland: add archive tools and pinned GoAWK, esbuild, and QuickJS

This commit is contained in:
2026-11-07 10:56:29 +00:00
parent 7e7e49bfa2
commit d79b2bc2e1
1798 changed files with 562969 additions and 1 deletions
@@ -0,0 +1,140 @@
const http = require('http')
const path = require('path')
const url = require('url')
const fs = require('fs')
const js = fs.readFileSync(path.join(__dirname, '..', '..', 'npm', 'esbuild-wasm', 'lib', 'browser.js'))
const jsMin = fs.readFileSync(path.join(__dirname, '..', '..', 'npm', 'esbuild-wasm', 'lib', 'browser.min.js'))
const esm = fs.readFileSync(path.join(__dirname, '..', '..', 'npm', 'esbuild-wasm', 'esm', 'browser.js'))
const esmMin = fs.readFileSync(path.join(__dirname, '..', '..', 'npm', 'esbuild-wasm', 'esm', 'browser.min.js'))
const wasm = fs.readFileSync(path.join(__dirname, '..', '..', 'npm', 'esbuild-wasm', 'esbuild.wasm'))
const html = fs.readFileSync(path.join(__dirname, '..', '..', 'scripts', 'browser', 'index.html'))
const server = http.createServer((req, res) => {
console.log(`[http] ${req.method} ${req.url}`)
try {
if (req.method === 'GET' && req.url) {
const parsed = url.parse(req.url)
if (parsed.pathname === '/npm/esbuild-wasm/lib/browser.js') {
res.writeHead(200, { 'Content-Type': 'text/javascript' })
res.end(js)
return
}
if (parsed.pathname === '/npm/esbuild-wasm/lib/browser.min.js') {
res.writeHead(200, { 'Content-Type': 'text/javascript' })
res.end(jsMin)
return
}
if (parsed.pathname === '/npm/esbuild-wasm/esm/browser.js') {
res.writeHead(200, { 'Content-Type': 'text/javascript' })
res.end(esm)
return
}
if (parsed.pathname === '/npm/esbuild-wasm/esm/browser.min.js') {
res.writeHead(200, { 'Content-Type': 'text/javascript' })
res.end(esmMin)
return
}
if (parsed.pathname === '/npm/esbuild-wasm/esbuild.wasm') {
res.writeHead(200, { 'Content-Type': 'application/wasm' })
res.end(wasm)
return
}
if (parsed.pathname === '/scripts/browser/index.html') {
res.writeHead(200, { 'Content-Type': 'text/html' })
res.end(html)
return
}
if (parsed.pathname === '/scripts/browser/esbuild.wasm.bagel') {
res.writeHead(200, { 'Content-Type': 'application/octet-stream' })
res.end(wasm)
return
}
}
res.writeHead(404)
res.end('404 Not Found')
}
catch (err) {
res.writeHead(500)
res.end('500 Internal Server Error')
console.error(err)
}
})
server.listen()
const { address, port } = server.address()
const serverURL = url.format({ protocol: 'http', hostname: address, port })
console.log(`[http] listening on ${serverURL}`)
async function main() {
let allTestsPassed = true
try {
const browser = await require('puppeteer').launch({
// This is here because since December 2024, GitHub changed something about
// their CI that causes this error:
//
// [FATAL:zygote_host_impl_linux.cc(128)] No usable sandbox! If you are running
// on Ubuntu 23.10+ or another Linux distro that has disabled unprivileged user
// namespaces with AppArmor, see https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md.
// Otherwise see https://chromium.googlesource.com/chromium/src/+/main/docs/linux/suid_sandbox_development.md
// for more information on developing with the (older) SUID sandbox. If you want
// to live dangerously and need an immediate workaround, you can try using
// --no-sandbox.
args: ['--no-sandbox'],
})
const page = await browser.newPage()
page.on('console', obj => {
console.log(`[console.${obj.type()}] ${obj.text()}`)
})
page.exposeFunction('testBegin', args => {
const config = Object.entries(JSON.parse(args)).map(([k, v]) => `${k}=${v}`).join(', ')
console.log(`💬 config: ${config}`)
})
page.exposeFunction('testEnd', args => {
if (args === null) console.log(`👍 success`)
else {
const { test, error } = JSON.parse(args)
console.log(`❌ error${test ? ` [${test}]` : ``}: ${error}`)
allTestsPassed = false
}
})
const testDone = new Promise(resolve => {
page.exposeFunction('testDone', resolve)
})
await page.goto(`${serverURL}/scripts/browser/index.html`, { waitUntil: 'domcontentloaded' })
await testDone
await page.close()
await browser.close()
}
catch (e) {
allTestsPassed = false
console.log(`❌ error: ${e && e.stack || e && e.message || e}`)
}
server.close()
if (!allTestsPassed) {
console.error(`❌ browser test failed`)
process.exit(1)
} else {
console.log(`✅ browser test passed`)
}
}
main().catch(error => setTimeout(() => { throw error }))
@@ -0,0 +1 @@
../../npm/esbuild-wasm/esbuild.wasm
@@ -0,0 +1,417 @@
<!DOCTYPE html>
<head>
<meta charset="utf8">
<style></style>
</head>
<body>
</body>
<script>
async function expectThrownError(fn, err) {
try {
await fn()
throw new Error('Expected an error to be thrown')
} catch (e) {
assertStrictEqual(e.message, err)
}
}
function assertStrictEqual(a, b, message = 'Assertion failed') {
if (a !== b) {
throw new Error(`${message}:
Observed: ${JSON.stringify(a)}
Expected: ${JSON.stringify(b)}`);
}
}
async function assertSameColorsWithNestingTransform(esbuild, { css, html }) {
const crawlColors = node => {
const { color } = getComputedStyle(node)
const children = Array.from(node.children).map(crawlColors)
return children.length ? [color, children] : color
}
const style = document.querySelector('style')
document.body.innerHTML = html
style.textContent = css
const original = JSON.stringify(crawlColors(document.body))
// Test minified CSS
const minified = await esbuild.transform(css, {
loader: 'css',
minify: true,
})
style.textContent = minified.code
assertStrictEqual(original, JSON.stringify(crawlColors(document.body)), 'Compare minified CSS')
// Test lowered CSS
const lowered = await esbuild.transform(css, {
loader: 'css',
supported: { nesting: false },
})
style.textContent = lowered.code
assertStrictEqual(original, JSON.stringify(crawlColors(document.body)), 'Compare lowered CSS')
}
const coreTests = ({ esbuild }) => ({
async defaultExport() {
assertStrictEqual(typeof esbuild.version, 'string')
assertStrictEqual(esbuild.version, esbuild.default.version)
assertStrictEqual(esbuild.version, esbuild.default.default.version)
assertStrictEqual(esbuild.version, esbuild.default.default.default.version)
},
async transformJS() {
const { code } = await esbuild.transform('1+2')
assertStrictEqual(code, '1 + 2;\n')
},
async transformTS() {
const { code } = await esbuild.transform('1 as any + <any>2', { loader: 'ts' })
assertStrictEqual(code, '1 + 2;\n')
},
async transformCSS() {
const { code } = await esbuild.transform('div { color: red }', { loader: 'css' })
assertStrictEqual(code, 'div {\n color: red;\n}\n')
},
async buildFib() {
const fibonacciPlugin = {
name: 'fib',
setup(build) {
build.onResolve({ filter: /^fib\((\d+)\)/ }, args => {
return { path: args.path, namespace: 'fib' }
})
build.onLoad({ filter: /^fib\((\d+)\)/, namespace: 'fib' }, args => {
let match = /^fib\((\d+)\)/.exec(args.path), n = +match[1]
let contents = n < 2 ? `export default ${n}` : `
import n1 from 'fib(${n - 1}) ${args.path}'
import n2 from 'fib(${n - 2}) ${args.path}'
export default n1 + n2`
return { contents }
})
},
}
const result = await esbuild.build({
stdin: {
contents: `
import x from 'fib(10)'
module.exports = x
`,
},
format: 'cjs',
bundle: true,
plugins: [fibonacciPlugin],
})
assertStrictEqual(result.outputFiles.length, 1)
assertStrictEqual(result.outputFiles[0].path, '<stdout>')
const code = result.outputFiles[0].text
const answer = {}
new Function('module', code)(answer)
assertStrictEqual(answer.exports, 55)
},
async buildRelativeIssue693() {
const result = await esbuild.build({
stdin: {
contents: `const x=1`,
},
write: false,
outfile: 'esbuild.js',
});
assertStrictEqual(result.outputFiles.length, 1)
assertStrictEqual(result.outputFiles[0].path, '/esbuild.js')
assertStrictEqual(result.outputFiles[0].text, 'const x = 1;\n')
},
async watch() {
const context = await esbuild.context({})
try {
await expectThrownError(context.watch, 'Cannot use the "watch" API in this environment')
} finally {
context.dispose()
}
},
async serve() {
const context = await esbuild.context({})
try {
await expectThrownError(context.serve, 'Cannot use the "serve" API in this environment')
} finally {
context.dispose()
}
},
async esbuildBuildSync() {
await expectThrownError(esbuild.buildSync, 'The "buildSync" API only works in node')
},
async esbuildTransformSync() {
await expectThrownError(esbuild.transformSync, 'The "transformSync" API only works in node')
},
})
function setupForProblemCSS(prefix) {
// https://github.com/tailwindlabs/tailwindcss/issues/2889
const original = `
/* Variant 1 */
.${prefix}-v1 { --a: ; --b: ; max-width: var(--a) var(--b); }
.${prefix}-a { --a: 1px; }
.${prefix}-b { --b: 2px; }
/* Variant 2 */
.${prefix}-v2 { max-width: var(--a, ) var(--b, ); }
.${prefix}-a { --a: 1px; }
.${prefix}-b { --b: 2px; }
`
const style = document.querySelector('style')
const test1a = document.createElement('div')
const test1b = document.createElement('div')
const test2a = document.createElement('div')
const test2b = document.createElement('div')
test1a.className = `${prefix}-v1 ${prefix}-a`
test1b.className = `${prefix}-v1 ${prefix}-b`
test2a.className = `${prefix}-v2 ${prefix}-a`
test2b.className = `${prefix}-v2 ${prefix}-b`
return [original, css => {
style.textContent = css
document.body.innerHTML = ''
document.body.appendChild(test1a)
document.body.appendChild(test1b)
document.body.appendChild(test2a)
document.body.appendChild(test2b)
assertStrictEqual(getComputedStyle(test1a).maxWidth, `1px`)
assertStrictEqual(getComputedStyle(test1b).maxWidth, `2px`)
assertStrictEqual(getComputedStyle(test2a).maxWidth, `1px`)
assertStrictEqual(getComputedStyle(test2b).maxWidth, `2px`)
}]
}
const cssTests = ({ esbuild }) => ({
async problemCSSOriginal() {
const [original, runAsserts] = setupForProblemCSS('original')
runAsserts(original)
},
async problemCSSPrettyPrinted() {
const [original, runAsserts] = setupForProblemCSS('pretty-print')
const { code: prettyPrinted } = await esbuild.transform(original, { loader: 'css' })
runAsserts(prettyPrinted)
},
async problemCSSMinified() {
const [original, runAsserts] = setupForProblemCSS('pretty-print')
const { code: minified } = await esbuild.transform(original, { loader: 'css', minify: true })
runAsserts(minified)
},
// See: https://github.com/evanw/esbuild/issues/3877
async cssNestingIssue3877() {
await assertSameColorsWithNestingTransform(esbuild, {
css: `
.a .b:has(> span) {
.a & span {
color: green;
}
}
`,
html: `
<div class="a">
<div class="b">
<span>1</span>
<div><span>2</span></div>
</div>
<div class="b">
<div><span>3</span></div>
</div>
</div>
`,
})
},
// See: https://github.com/evanw/esbuild/issues/3877#issuecomment-2631385559
async cssNestingIssue3877Comment2631385559() {
await assertSameColorsWithNestingTransform(esbuild, {
css: `
.a {
:has(>&) {
color: red;
}
}
`,
html: `
<div class="a">a</div>
`,
})
},
// See: https://github.com/evanw/esbuild/issues/3997
async cssNestingIssue3997() {
await assertSameColorsWithNestingTransform(esbuild, {
css: `
.foo {
color: blue;
&& { color: red; }
& { color: green; }
}
`,
html: `
<div class="foo">x</div>
`,
})
},
// See: https://github.com/evanw/esbuild/issues/4005
async cssNestingIssue4005() {
await assertSameColorsWithNestingTransform(esbuild, {
css: `
.foo {
:where(& > .bar) {
color: red;
}
}
`,
html: `
<div class="foo">
<div class="bar">bar</div>
</div>
`,
})
},
// See: https://github.com/evanw/esbuild/pull/4037
async cssNestingIssue4037() {
await assertSameColorsWithNestingTransform(esbuild, {
css: `
.parent {
> .a,
> .b1 > .b2 {
color: red;
}
}
`,
html: `
<div class="parent">
<div class="a">a</div>
<div class="b1">
<div class="b2">b2</div>
</div>
</div>
`,
})
},
})
async function runTest(test, fn) {
try {
await fn()
} catch (e) {
e.test = test
throw e
}
}
async function loadScript(url) {
const tag = document.createElement('script')
document.head.appendChild(tag)
await new Promise((resolve, reject) => {
tag.onload = resolve
tag.onerror = () => reject(new Error('Failed to load script: ' + url))
tag.src = url
})
const esbuild = window.esbuild
delete window.esbuild
return esbuild
}
async function testStart() {
let allTestsPassed = true
if (!window.testBegin) window.testBegin = args => {
const config = Object.entries(JSON.parse(args)).map(([k, v]) => `${k}=${v}`).join(', ')
console.log(`💬 config: ${config}`)
}
if (!window.testEnd) window.testEnd = args => {
if (args === null) console.log(`👍 success`)
else {
const { test, stack, error } = JSON.parse(args)
console.log(`❌ error${test ? ` [${test}]` : ``}: ${error}`)
allTestsPassed = false
}
}
if (!window.testDone) window.testDone = error => {
console.log(allTestsPassed ? `✅ done` : `❌ done`)
}
// Just run CSS tests through a single configuration, but run each test separately
{
const url = '/npm/esbuild-wasm/esbuild.wasm?' + Math.random()
const initializePromise = import('/npm/esbuild-wasm/esm/browser.js?' + Math.random())
.then(esbuild => esbuild.initialize({ wasmURL: new URL(url, location.href) })
.then(() => esbuild))
const tests = cssTests({ esbuild: await initializePromise.catch(() => null) })
for (const test in tests) {
try {
testBegin(JSON.stringify({ type: 'css', test }))
await initializePromise
await runTest(test, tests[test])
testEnd(null)
} catch (e) {
testEnd(JSON.stringify({
test: e.test || null,
stack: e.stack || null,
error: (e && e.message || e) + '',
}))
}
}
}
// Run all core tests through every configuration, but run all tests together (stop after one failure)
for (const esm of [false, true]) {
for (const min of [false, true]) {
for (const worker of [false, true]) {
for (const mime of ['correct', 'incorrect']) {
for (const approach of ['string', 'url', 'module']) {
try {
testBegin(JSON.stringify({ esm, min, worker, mime, approach }))
const esbuild = esm
? await import('/npm/esbuild-wasm/esm/browser' + (min ? '.min' : '') + '.js?' + Math.random())
: await loadScript('/npm/esbuild-wasm/lib/browser' + (min ? '.min' : '') + '.js?' + Math.random())
const url = mime === 'correct' ? '/npm/esbuild-wasm/esbuild.wasm' : '/scripts/browser/esbuild.wasm.bagel'
const initializePromise = {
string: () => esbuild.initialize({ wasmURL: url, worker }),
url: () => esbuild.initialize({ wasmURL: new URL(url, location.href), worker }),
module: () => fetch(url)
.then(r => r.arrayBuffer())
.then(bytes => WebAssembly.compile(bytes))
.then(module => esbuild.initialize({ wasmModule: module, worker })),
}[approach]()
await initializePromise
const tests = coreTests({ esbuild })
const promises = []
for (const test in tests) promises.push(runTest(test, tests[test]))
await Promise.all(promises)
testEnd(null)
} catch (e) {
testEnd(JSON.stringify({
test: e.test || null,
stack: e.stack || null,
error: (e && e.message || e) + '',
}))
}
}
}
}
}
}
testDone()
}
testStart()
</script>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
{
"dependencies": {
"puppeteer": "23.11.1"
}
}
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<p>
This script checks to see what characters need to be escaped in a data URL
(in addition to % for percent-encoded hexadecimal escapes) for a browser to
parse it correctly. This information is used to implement esbuild's
<code>dataurl</code> loader. Here is what your current browser requires:
</p>
<pre id="result"></pre>
<p>
The answer that works across Chrome, Firefox, and Safari appears to be:
<br>
<br>Always percent-encode these values: <code>0x09, 0x0A, 0x0D, 0x23</code>
<br>Only percent-encode these values in the trailing position: <code>0x00 to 0x08, 0x0B, 0x0C, 0x0E to 0x20</code>
</p>
<script>
function percentEncode(i) {
if (i >= 0x80) return encodeURIComponent(String.fromCharCode(i))
return '%' + (0x100 | i).toString(16).slice(-2)
}
async function urlDoesDecodeTo(url, to) {
return to === await fetch(url).then(r => r.text())
}
async function check() {
const shouldEncode = []
for (let i = 0; i <= 0xFF; i++) {
const ch = String.fromCharCode(i)
const chPercent = percentEncode(i)
if (!await urlDoesDecodeTo('data:text/plain,' + chPercent, ch)) {
throw new Error('Assertion failed: Cannot decode ' + chPercent)
}
const leading = await urlDoesDecodeTo('data:text/plain,' + ch + 'foo', ch + 'foo')
const trailing = await urlDoesDecodeTo('data:text/plain,foo' + ch, 'foo' + ch)
const embedded = await urlDoesDecodeTo('data:text/plain,foo' + ch + 'foo', 'foo' + ch + 'foo')
if (!leading && !trailing && !embedded) {
shouldEncode.push('U+' + i.toString(16) + ' (' + ch + ')')
} else {
if (!leading) shouldEncode.push('U+' + i.toString(16) + ' (' + ch + ') leading')
if (!trailing) shouldEncode.push('U+' + i.toString(16) + ' (' + ch + ') trailing')
if (!embedded) shouldEncode.push('U+' + i.toString(16) + ' (' + ch + ') embedded')
}
}
document.getElementById('result').textContent = 'shouldEncode = ' + JSON.stringify(shouldEncode, null, 2)
}
check()
</script>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,181 @@
// To run this, you must first build the Deno package with "make platform-deno"
import * as esbuildNative from '../deno/mod.js'
import * as esbuildWASM from '../deno/wasm.js'
import * as path from 'https://deno.land/std@0.95.0/path/mod.ts'
import * as asserts from 'https://deno.land/std@0.95.0/testing/asserts.ts'
const __dirname = path.dirname(path.fromFileUrl(import.meta.url))
const rootTestDir = path.join(__dirname, '.deno-tests')
const wasmModule = await WebAssembly.compile(await Deno.readFile(path.join(__dirname, '..', 'deno', 'esbuild.wasm')))
try {
Deno.removeSync(rootTestDir, { recursive: true })
} catch {
}
Deno.mkdirSync(rootTestDir, { recursive: true })
function test(name, backends, fn) {
// Note: Do not try to add a timeout for the tests below. It turns out that
// calling "setTimeout" from within "Deno.test" changes how Deno waits for
// promises in a way that masks problems that would otherwise occur. We want
// test coverage for the way that people will likely be using these API calls.
//
// Specifically tests that Deno would otherwise have failed with "error:
// Promise resolution is still pending but the event loop has already
// resolved" were being allowed to pass instead. See this issue for more
// information: https://github.com/evanw/esbuild/issues/3682
for (const backend of backends) {
switch (backend) {
case 'native':
Deno.test(name + '-native', async () => {
let testDir = path.join(rootTestDir, name + '-native')
await Deno.mkdir(testDir, { recursive: true })
try {
await fn({ esbuild: esbuildNative, testDir })
await Deno.remove(testDir, { recursive: true }).catch(() => null)
} finally {
await esbuildNative.stop()
}
})
break
case 'wasm-main':
Deno.test(name + '-wasm-main', async () => {
let testDir = path.join(rootTestDir, name + '-wasm-main')
await esbuildWASM.initialize({ wasmModule, worker: false })
await Deno.mkdir(testDir, { recursive: true })
try {
await fn({ esbuild: esbuildWASM, testDir })
await Deno.remove(testDir, { recursive: true }).catch(() => null)
} finally {
await esbuildWASM.stop()
}
})
break
case 'wasm-worker':
Deno.test(name + '-wasm-worker', async () => {
let testDir = path.join(rootTestDir, name + '-wasm-worker')
await esbuildWASM.initialize({ wasmModule, worker: true })
await Deno.mkdir(testDir, { recursive: true })
try {
await fn({ esbuild: esbuildWASM, testDir })
await Deno.remove(testDir, { recursive: true }).catch(() => null)
} finally {
await esbuildWASM.stop()
}
})
break
}
}
}
addEventListener("unload", (e) => {
try {
Deno.removeSync(rootTestDir, { recursive: true })
} catch {
// root test dir possibly already removed, so ignore
}
})
// This test doesn't run in WebAssembly because it requires file system access
test("basicBuild", ['native'], async ({ esbuild, testDir }) => {
const input = path.join(testDir, 'in.ts')
const dep = path.join(testDir, 'dep.ts')
const output = path.join(testDir, 'out.ts')
await Deno.writeTextFile(input, 'import dep from "./dep.ts"; export default dep === 123')
await Deno.writeTextFile(dep, 'export default 123')
await esbuild.build({
entryPoints: [input],
bundle: true,
outfile: output,
format: 'esm',
})
const result = await import(path.toFileUrl(output))
asserts.assertStrictEquals(result.default, true)
})
test("basicContext", ['native'], async ({ esbuild, testDir }) => {
const input = path.join(testDir, 'in.ts')
const dep = path.join(testDir, 'dep.ts')
const output = path.join(testDir, 'out.ts')
await Deno.writeTextFile(input, 'import dep from "./dep.ts"; export default dep === 123')
await Deno.writeTextFile(dep, 'export default 123')
const ctx = await esbuild.context({
entryPoints: ['in.ts'],
bundle: true,
outfile: output,
format: 'esm',
absWorkingDir: testDir,
})
const { errors, warnings } = await ctx.rebuild()
asserts.assertStrictEquals(errors.length, 0)
asserts.assertStrictEquals(warnings.length, 0)
await ctx.dispose()
const result = await import(path.toFileUrl(output))
asserts.assertStrictEquals(result.default, true)
})
test("basicPlugin", ['native', 'wasm-main', 'wasm-worker'], async ({ esbuild }) => {
const build = await esbuild.build({
entryPoints: ['<entry>'],
bundle: true,
format: 'esm',
write: false,
plugins: [{
name: 'plug',
setup(build) {
build.onResolve({ filter: /^<.*>$/ }, args => ({ path: args.path, namespace: '<>' }))
build.onLoad({ filter: /^<entry>$/ }, () => ({ contents: `import dep from "<dep>"; export default dep === 123` }))
build.onLoad({ filter: /^<dep>$/ }, () => ({ contents: `export default 123` }))
},
}],
})
const result = await import('data:application/javascript;base64,' + btoa(build.outputFiles[0].text))
asserts.assertStrictEquals(result.default, true)
})
test("basicTransform", ['native', 'wasm-main', 'wasm-worker'], async ({ esbuild }) => {
const ts = 'let x: number = 1+2'
const result = await esbuild.transform(ts, { loader: 'ts' })
asserts.assertStrictEquals(result.code, 'let x = 1 + 2;\n')
})
// This test doesn't run in WebAssembly because of a stack overflow
test("largeTransform", ['native'], async ({ esbuild }) => {
// This should be large enough to be bigger than Deno's write buffer
let x = '0'
for (let i = 0; i < 1000; i++) x += '+' + i
x += ','
let y = 'return['
for (let i = 0; i < 1000; i++) y += x
y += ']'
const result = await esbuild.build({
stdin: {
contents: y,
},
write: false,
minifyWhitespace: true,
})
asserts.assertStrictEquals(result.outputFiles[0].text, y.slice(0, -2) + '];\n')
})
test("analyzeMetafile", ['native', 'wasm-main', 'wasm-worker'], async ({ esbuild }) => {
const result = await esbuild.analyzeMetafile({
outputs: {
'out.js': {
bytes: 4096,
inputs: {
'in.js': {
bytesInOutput: 1024,
},
},
},
},
})
asserts.assertStrictEquals(result, `
out.js 4.0kb 100.0%
└ in.js 1.0kb 25.0%
`)
})
@@ -0,0 +1,402 @@
function generateTestCase(assign) {
let sideEffectCount = 0
let patternCount = 0
let limit = 10
let depth = 0
function choice(n) {
return Math.random() * n | 0
}
function patternAndValue() {
patternCount++
switch (choice(3)) {
case 0: return array()
case 1: return object()
case 2: return [assign(), choice(10), choice(10)]
}
}
function sideEffect(result) {
return `s(${sideEffectCount++}${result ? `, ${result}` : ''})`
}
function indent(open, items, close) {
let tab = ' '
items = items.map(i => `\n${tab.repeat(depth + 1)}${i}`).join(',')
return `${open}${items}\n${tab.repeat(depth)}${close}`
}
function id() {
return String.fromCharCode('a'.charCodeAt(0) + choice(3))
}
function array() {
let count = 1 + choice(2)
let pattern = []
let value = []
depth++
for (let i = 0; i < count; i++) {
if (patternCount > limit) break
let [pat, val, defVal] = patternAndValue()
switch (choice(3)) {
case 0:
pattern.push(pat)
value.push(val)
break
case 1:
pattern.push(`${pat} = ${sideEffect(defVal)}`)
value.push(val)
break
case 2:
pattern.push(`${pat} = ${sideEffect(defVal)}`)
value.push(defVal)
break
}
if (choice(10) < 8) value.push(val)
}
if (choice(2)) {
pattern.push(`...${assign()}`)
if (choice(10) < 8) value.push(choice(10))
}
depth--
return [
indent('[', pattern, ']'),
indent('[', value, ']'),
'[]',
]
}
function object() {
let count = 1 + choice(2)
let pattern = []
let value = []
let valKeys = new Set()
depth++
for (let i = 0; i < count; i++) {
if (patternCount > limit) break
let valKey = id()
if (valKeys.has(valKey)) continue
valKeys.add(valKey)
let patKey = choice() ? valKey : `[${sideEffect(`'${valKey}'`)}]`
let [pat, val, defVal] = patternAndValue()
switch (choice(3)) {
case 0:
pattern.push(`${patKey}: ${pat}`)
value.push(`${valKey}: ${val}`)
break
case 1:
pattern.push(`${patKey}: ${pat} = ${sideEffect(defVal)}`)
value.push(`${valKey}: ${val}`)
break
case 2:
pattern.push(`${patKey}: ${pat} = ${sideEffect(defVal)}`)
value.push(`${valKey}: ${defVal}`)
break
}
}
if (choice(2)) {
pattern.push(`...${assign()}`)
if (choice(10) < 8) value.push(`${id()}: ${choice(10)}`)
}
depth--
return [
indent('{', pattern, '}'),
indent('{', value, '}'),
'{}',
]
}
return choice(2) ? array() : object()
}
function evaluate(code) {
let effectTrace = []
let assignTarget = {}
let sideEffect = (id, value) => (effectTrace.push(id), value)
new Function('a', 's', code)(assignTarget, sideEffect)
return JSON.stringify({ assignTarget, effectTrace })
}
function generateTestCases(trials) {
let testCases = []
while (testCases.length < trials) {
let ids = []
let assignCount = 0
let [pattern, value] = generateTestCase(() => {
let id = `_${assignCount++}`
ids.push(id)
return id
})
try {
evaluate(`(${pattern.replace(/_/g, 'a._')} = ${value});`)
testCases.push([pattern, value, ids])
} catch (e) {
}
}
return testCases
}
function AssignmentOperator([pattern, value]) {
let ts = `(${pattern.replace(/_/g, 'a._')} = ${value});`
let js = ts
return { js, ts }
}
function NamespaceExport([pattern, value]) {
let ts = `namespace a { export const ${`${pattern} = ${value}`} }`
let js = `(${pattern.replace(/_/g, 'a._')} = ${value});`
return { js, ts }
}
function ConstDeclaration([pattern, value, ids]) {
let ts = `const ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}`
let js = ts
return { js, ts }
}
function LetDeclaration([pattern, value, ids]) {
let ts = `let ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}`
let js = ts
return { js, ts }
}
function VarDeclaration([pattern, value, ids]) {
let ts = `var ${pattern} = ${value};${ids.map(id => `\na.${id} = ${id};`).join('')}`
let js = ts
return { js, ts }
}
function TryCatchBinding([pattern, value, ids]) {
let ts = `try { throw ${value} } catch (${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
let js = ts
return { js, ts }
}
function FunctionStatementArguments([pattern, value, ids]) {
let ts = `function foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }\nfoo(${value});`
let js = ts
return { js, ts }
}
function FunctionExpressionArguments([pattern, value, ids]) {
let ts = `(function(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} })(${value});`
let js = ts
return { js, ts }
}
function ArrowFunctionArguments([pattern, value, ids]) {
let ts = `((${pattern}) => { ${ids.map(id => `a.${id} = ${id};`).join('\n')} })(${value});`
let js = ts
return { js, ts }
}
function ObjectMethodArguments([pattern, value, ids]) {
let ts = `({ foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } }).foo(${value});`
let js = ts
return { js, ts }
}
function ClassStatementMethodArguments([pattern, value, ids]) {
let ts = `class Foo { foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } }\nnew Foo().foo(${value});`
let js = ts
return { js, ts }
}
function ClassExpressionMethodArguments([pattern, value, ids]) {
let ts = `(new (class { foo(${pattern}) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} } })).foo(${value});`
let js = ts
return { js, ts }
}
function ForLoopConst([pattern, value, ids]) {
let ts = `var i; for (const ${pattern} = ${value}; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
let js = ts
return { js, ts }
}
function ForLoopLet([pattern, value, ids]) {
let ts = `for (let ${pattern} = ${value}, i = 0; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
let js = ts
return { js, ts }
}
function ForLoopVar([pattern, value, ids]) {
let ts = `for (var ${pattern} = ${value}, i = 0; i < 1; i++) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
let js = ts
return { js, ts }
}
function ForLoop([pattern, value]) {
let ts = `for (${pattern.replace(/_/g, 'a._')} = ${value}; 0; ) ;`
let js = ts
return { js, ts }
}
function ForOfLoopConst([pattern, value, ids]) {
let ts = `for (const ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
let js = ts
return { js, ts }
}
function ForOfLoopLet([pattern, value, ids]) {
let ts = `for (let ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
let js = ts
return { js, ts }
}
function ForOfLoopVar([pattern, value, ids]) {
let ts = `for (var ${pattern} of [${value}]) { ${ids.map(id => `a.${id} = ${id};`).join('\n')} }`
let js = ts
return { js, ts }
}
function ForOfLoop([pattern, value]) {
let ts = `for (${pattern.replace(/_/g, 'a._')} of [${value}]) ;`
let js = ts
return { js, ts }
}
async function verify(test, transform, testCases) {
let verbose = process.argv.indexOf('--verbose') >= 0
let indent = t => t.replace(/\n/g, '\n ')
let newline = false
console.log(`${test.name} (${transform.name}):`)
await concurrentMap(testCases, 20, async (testCase) => {
let { js, ts } = test(testCase)
let expected
try {
expected = evaluate(js)
} catch (e) {
return
}
let transformed
try {
transformed = await transform(ts)
} catch (e) {
process.stdout.write('T')
newline = true
if (verbose) {
console.log('\n' + '='.repeat(80))
console.log(indent(`Original code:\n${ts}`))
console.log(indent(`Transform error:\n${e}`))
newline = false
}
return
}
let actual
try {
actual = evaluate(transformed)
} catch (e) {
actual = e + ''
}
if (actual !== expected) {
process.stdout.write(actual.indexOf('SyntaxError') >= 0 ? 'S' : 'X')
newline = true
if (verbose) {
console.log('\n' + '='.repeat(80))
console.log(indent(`Original code:\n${ts}`))
console.log(indent(`Transformed code:\n${transformed}`))
console.log(indent(`Expected output:\n${expected}`))
console.log(indent(`Actual output:\n${actual}`))
newline = false
}
} else {
process.stdout.write('-')
newline = true
}
})
if (newline) process.stdout.write('\n')
}
function concurrentMap(items, batch, callback) {
return new Promise((resolve, reject) => {
let index = 0
let pending = 0
let next = () => {
if (index === items.length && pending === 0) {
resolve()
} else if (index < items.length) {
let item = items[index++]
pending++
callback(item).then(() => {
pending--
next()
}, e => {
items.length = 0
reject(e)
})
}
}
for (let i = 0; i < batch; i++)next()
})
}
async function main() {
let es = require('./esbuild').installForTests()
let esbuild = async (x) => (await es.transform(x, { target: 'es6', loader: 'ts' })).code.trim()
console.log(`
Options:
--verbose = Print details for failures
Legend:
- = The test passed
X = The test failed
T = The transform function itself failed
S = The generated code has a syntax error`)
let tests = [
// Bindings
ConstDeclaration,
LetDeclaration,
VarDeclaration,
TryCatchBinding,
FunctionStatementArguments,
FunctionExpressionArguments,
ArrowFunctionArguments,
ObjectMethodArguments,
ClassStatementMethodArguments,
ClassExpressionMethodArguments,
ForLoopConst,
ForLoopLet,
ForLoopVar,
ForOfLoopConst,
ForOfLoopLet,
ForOfLoopVar,
// Destructuring
AssignmentOperator,
ForLoop,
ForOfLoop,
// TypeScript-specific
NamespaceExport,
]
let transforms = [
esbuild,
]
let testCases = generateTestCases(100)
for (let transform of transforms) {
console.log()
for (let test of tests) {
await verify(test, transform, testCases)
}
}
}
main().catch(e => setTimeout(() => { throw e }))
File diff suppressed because it is too large Load Diff
+361
View File
@@ -0,0 +1,361 @@
const childProcess = require('child_process')
const path = require('path')
const fs = require('fs')
const os = require('os')
const repoDir = path.dirname(__dirname)
const denoDir = path.join(repoDir, 'deno')
const npmDir = path.join(repoDir, 'npm', 'esbuild')
const version = fs.readFileSync(path.join(repoDir, 'version.txt'), 'utf8').trim()
const nodeTarget = 'node10'; // See: https://nodejs.org/en/about/releases/
const denoTarget = 'deno1'; // See: https://docs.deno.com/runtime/fundamentals/stability_and_releases/
const umdBrowserTarget = 'es2015'; // Transpiles "async"
const esmBrowserTarget = 'es2017'; // Preserves "async"
const buildNeutralLib = (esbuildPath) => {
const libDir = path.join(npmDir, 'lib')
const binDir = path.join(npmDir, 'bin')
fs.mkdirSync(libDir, { recursive: true })
fs.mkdirSync(binDir, { recursive: true })
// Generate "npm/esbuild/install.js"
childProcess.execFileSync(esbuildPath, [
path.join(repoDir, 'lib', 'npm', 'node-install.ts'),
'--outfile=' + path.join(npmDir, 'install.js'),
'--bundle',
'--target=' + nodeTarget,
// Note: https://socket.dev have complained that inlining the version into
// the install script messes up some internal scanning that they do by
// making it seem like esbuild's install script code changes with every
// esbuild release. So now we read it from "package.json" instead.
// '--define:ESBUILD_VERSION=' + JSON.stringify(version),
'--external:esbuild',
'--platform=node',
'--log-level=warning',
], { cwd: repoDir })
// Generate "npm/esbuild/lib/main.js"
childProcess.execFileSync(esbuildPath, [
path.join(repoDir, 'lib', 'npm', 'node.ts'),
'--outfile=' + path.join(libDir, 'main.js'),
'--bundle',
'--target=' + nodeTarget,
'--define:WASM=false',
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
'--external:esbuild',
'--platform=node',
'--log-level=warning',
], { cwd: repoDir })
// Generate "npm/esbuild/bin/esbuild"
childProcess.execFileSync(esbuildPath, [
path.join(repoDir, 'lib', 'npm', 'node-shim.ts'),
'--outfile=' + path.join(binDir, 'esbuild'),
'--bundle',
'--target=' + nodeTarget,
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
'--external:esbuild',
'--platform=node',
'--log-level=warning',
], { cwd: repoDir })
// Generate "npm/esbuild/lib/main.d.ts"
const types_ts = fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'types.ts'), 'utf8')
fs.writeFileSync(path.join(libDir, 'main.d.ts'), types_ts)
// Get supported platforms
const platforms = { exports: {} }
new Function('module', 'exports', 'require', childProcess.execFileSync(esbuildPath, [
path.join(repoDir, 'lib', 'npm', 'node-platform.ts'),
'--bundle',
'--target=' + nodeTarget,
'--external:esbuild',
'--platform=node',
'--log-level=warning',
], { cwd: repoDir }))(platforms, platforms.exports, require)
const optionalDependencies = Object.fromEntries(Object.values({
...platforms.exports.knownWindowsPackages,
...platforms.exports.knownUnixlikePackages,
...platforms.exports.knownWebAssemblyFallbackPackages,
}).sort().map(x => [x, version]))
// Update "npm/esbuild/package.json"
const pjPath = path.join(npmDir, 'package.json')
const package_json = JSON.parse(fs.readFileSync(pjPath, 'utf8'))
package_json.optionalDependencies = optionalDependencies
fs.writeFileSync(pjPath, JSON.stringify(package_json, null, 2) + '\n')
}
async function generateWorkerCode({ esbuildPath, wasm_exec_js, minify, target }) {
const input = `
let onmessage
let globalThis = {}
for (let o = self; o; o = Object.getPrototypeOf(o))
for (let k of Object.getOwnPropertyNames(o))
if (!(k in globalThis))
Object.defineProperty(globalThis, k, { get: () => self[k] })
${wasm_exec_js.replace(/\bfs\./g, 'globalThis.fs.')}
${fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'worker.ts'), 'utf8')}
return m => onmessage(m)
`
const args = [
'--loader=ts',
'--target=' + target,
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
].concat(minify ? ['--minify'] : [])
// Note: This uses "execFile" because "execFileSync" in node appears to have
// a bug. Specifically when using the "input" option of "execFileSync" to
// provide stdin, sometimes (~2% of the time?) node writes all of the input
// but then doesn't close the stream. The Go side is stuck reading from stdin
// within "ioutil.ReadAll(os.Stdin)" so I suspect it's a bug in node, not in
// Go. Explicitly calling "stdin.end()" on the node side appears to fix it.
const wasmExecAndWorker = (await new Promise((resolve, reject) => {
const proc = childProcess.execFile(esbuildPath, args, { cwd: repoDir }, (err, stdout) => {
if (err) reject(err)
else resolve(stdout)
})
proc.stdin.write(input)
proc.stdin.end()
})).toString().trim()
const commentLines = wasm_exec_js.split('\n')
const firstNonComment = commentLines.findIndex(line => !line.startsWith('//'))
const commentPrefix = '\n' + commentLines.slice(0, firstNonComment).join('\n') + '\n'
if (minify) return `(postMessage=>{${commentPrefix}${wasmExecAndWorker}})`
return `((postMessage) => {${(commentPrefix + wasmExecAndWorker).replace(/\n/g, '\n ')}\n })`
}
exports.buildWasmLib = async (esbuildPath) => {
// Asynchronously start building the WebAssembly module
const npmWasmDir = path.join(repoDir, 'npm', 'esbuild-wasm')
const goBuildPromise = new Promise((resolve, reject) => childProcess.execFile('go',
[
'build',
'-o', path.join(npmWasmDir, 'esbuild.wasm'),
'-ldflags=-s -w -buildid=', // This removes ~0.14mb of unnecessary WebAssembly code
'-buildvcs=false',
'-trimpath',
path.join(repoDir, 'cmd', 'esbuild'),
],
{ cwd: repoDir, stdio: 'inherit', env: { ...process.env, GOOS: 'js', GOARCH: 'wasm' } },
err => err ? reject(err) : resolve()))
const libDir = path.join(npmWasmDir, 'lib')
const esmDir = path.join(npmWasmDir, 'esm')
fs.mkdirSync(libDir, { recursive: true })
fs.mkdirSync(esmDir, { recursive: true })
// Generate "npm/esbuild-wasm/wasm_exec.js"
const GOROOT = childProcess.execFileSync('go', ['env', 'GOROOT']).toString().trim()
let wasm_exec_js = fs.readFileSync(path.join(GOROOT, 'lib', 'wasm', 'wasm_exec.js'), 'utf8')
let wasm_exec_node_js = fs.readFileSync(path.join(GOROOT, 'lib', 'wasm', 'wasm_exec_node.js'), 'utf8')
fs.writeFileSync(path.join(npmWasmDir, 'wasm_exec.js'), wasm_exec_js)
fs.writeFileSync(path.join(npmWasmDir, 'wasm_exec_node.js'), wasm_exec_node_js)
// Generate "npm/esbuild-wasm/lib/main.js"
childProcess.execFileSync(esbuildPath, [
path.join(repoDir, 'lib', 'npm', 'node.ts'),
'--outfile=' + path.join(libDir, 'main.js'),
'--bundle',
'--target=' + nodeTarget,
'--format=cjs',
'--define:WASM=true',
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
'--external:esbuild',
'--platform=node',
'--log-level=warning',
], { cwd: repoDir })
// Generate "npm/esbuild-wasm/lib/main.d.ts" and "npm/esbuild-wasm/lib/browser.d.ts"
const types_ts = fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'types.ts'), 'utf8')
fs.writeFileSync(path.join(libDir, 'main.d.ts'), types_ts)
fs.writeFileSync(path.join(libDir, 'browser.d.ts'), types_ts)
fs.writeFileSync(path.join(esmDir, 'browser.d.ts'), types_ts)
for (const minify of [false, true]) {
const minifyFlags = minify ? ['--minify'] : []
const wasmWorkerCodeUMD = await generateWorkerCode({ esbuildPath, wasm_exec_js, minify, target: umdBrowserTarget })
const wasmWorkerCodeESM = await generateWorkerCode({ esbuildPath, wasm_exec_js, minify, target: esmBrowserTarget })
// Generate "npm/esbuild-wasm/lib/browser.*"
const umdPrefix = `(module=>{`
const umdSuffix = `})(typeof module==="object"?module:{set exports(x){(typeof self!=="undefined"?self:this).esbuild=x}});`
const browserCJS = childProcess.execFileSync(esbuildPath, [
path.join(repoDir, 'lib', 'npm', 'browser.ts'),
'--bundle',
'--target=' + umdBrowserTarget,
'--format=cjs',
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
'--define:WEB_WORKER_SOURCE_CODE=' + JSON.stringify(wasmWorkerCodeUMD),
'--banner:js=' + umdPrefix,
'--footer:js=' + umdSuffix,
'--log-level=warning',
].concat(minifyFlags), { cwd: repoDir }).toString().replace('WEB_WORKER_FUNCTION', wasmWorkerCodeUMD)
fs.writeFileSync(path.join(libDir, minify ? 'browser.min.js' : 'browser.js'), browserCJS)
// Generate "npm/esbuild-wasm/esm/browser.*"
const browserESM = childProcess.execFileSync(esbuildPath, [
path.join(repoDir, 'lib', 'npm', 'browser.ts'),
'--bundle',
'--target=' + esmBrowserTarget,
'--format=esm',
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
'--define:WEB_WORKER_SOURCE_CODE=' + JSON.stringify(wasmWorkerCodeESM),
'--log-level=warning',
].concat(minifyFlags), { cwd: repoDir }).toString().replace('WEB_WORKER_FUNCTION', wasmWorkerCodeESM)
fs.writeFileSync(path.join(esmDir, minify ? 'browser.min.js' : 'browser.js'), browserESM)
}
// Join with the asynchronous WebAssembly build
await goBuildPromise
// Also copy this into the WebAssembly shim directories
for (const dir of [
path.join(repoDir, 'npm', '@esbuild', 'android-arm'),
path.join(repoDir, 'npm', '@esbuild', 'android-x64'),
path.join(repoDir, 'npm', '@esbuild', 'openharmony-arm64'),
]) {
fs.mkdirSync(path.join(dir, 'bin'), { recursive: true })
fs.writeFileSync(path.join(dir, 'wasm_exec.js'), wasm_exec_js)
fs.writeFileSync(path.join(dir, 'wasm_exec_node.js'), wasm_exec_node_js)
fs.copyFileSync(path.join(npmWasmDir, 'bin', 'esbuild'), path.join(dir, 'bin', 'esbuild'))
fs.copyFileSync(path.join(npmWasmDir, 'esbuild.wasm'), path.join(dir, 'esbuild.wasm'))
}
}
const buildDenoLib = async (esbuildPath) => {
// Generate "deno/mod.js"
childProcess.execFileSync(esbuildPath, [
path.join(repoDir, 'lib', 'deno', 'mod.ts'),
'--bundle',
'--outfile=' + path.join(denoDir, 'mod.js'),
'--target=' + denoTarget,
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
'--platform=neutral',
'--log-level=warning',
'--banner:js=/// <reference types="./mod.d.ts" />',
], { cwd: repoDir })
// Generate "deno/wasm.js"
const GOROOT = childProcess.execFileSync('go', ['env', 'GOROOT']).toString().trim()
let wasm_exec_js = fs.readFileSync(path.join(GOROOT, 'lib', 'wasm', 'wasm_exec.js'), 'utf8')
const wasmWorkerCode = await generateWorkerCode({ esbuildPath, wasm_exec_js, minify: true, target: denoTarget })
const modWASM = childProcess.execFileSync(esbuildPath, [
path.join(repoDir, 'lib', 'deno', 'wasm.ts'),
'--bundle',
'--target=' + denoTarget,
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
'--define:WEB_WORKER_SOURCE_CODE=' + JSON.stringify(wasmWorkerCode),
'--platform=neutral',
'--log-level=warning',
'--banner:js=/// <reference types="./wasm.d.ts" />',
], { cwd: repoDir }).toString().replace('WEB_WORKER_FUNCTION', wasmWorkerCode)
fs.writeFileSync(path.join(denoDir, 'wasm.js'), modWASM)
// Generate "deno/mod.d.ts"
const types_ts = fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'types.ts'), 'utf8')
fs.writeFileSync(path.join(denoDir, 'mod.d.ts'), types_ts)
fs.writeFileSync(path.join(denoDir, 'wasm.d.ts'), types_ts)
// And copy the WebAssembly file over to the Deno library as well
fs.copyFileSync(path.join(repoDir, 'npm', 'esbuild-wasm', 'esbuild.wasm'), path.join(repoDir, 'deno', 'esbuild.wasm'))
}
// Writing a file atomically is important for watch mode tests since we don't
// want to read the file after it has been truncated but before the new contents
// have been written.
exports.writeFileAtomic = (where, contents) => {
// Note: Can't use "os.tmpdir()" because that doesn't work on Windows. CI runs
// tests on D:\ and the temporary directory is on C:\ or the other way around.
// And apparently it's impossible to move files between C:\ and D:\ or something.
// So we have to write the file in the same directory as the destination. This is
// unfortunate because it will unnecessarily trigger extra watch mode rebuilds.
// So we have to make our tests extra robust so they can still work with random
// extra rebuilds thrown in.
const file = path.join(path.dirname(where), '.esbuild-atomic-file-' + Math.random().toString(36).slice(2))
fs.writeFileSync(file, contents)
fs.renameSync(file, where)
}
exports.buildBinary = () => {
childProcess.execFileSync('go', ['build', '-ldflags=-s -w -buildid=', '-buildvcs=false', '-trimpath', './cmd/esbuild'], { cwd: repoDir, stdio: 'ignore' })
return path.join(repoDir, process.platform === 'win32' ? 'esbuild.exe' : 'esbuild')
}
exports.removeRecursiveSync = path => {
try {
fs.rmSync(path, { recursive: true })
} catch (e) {
// Removing stuff on Windows is flaky and unreliable. Don't fail tests
// on CI if Windows is just being a pain. Common causes of flakes include
// random EPERM and ENOTEMPTY errors.
//
// The general "solution" to this is to try asking Windows to redo the
// failing operation repeatedly until eventually giving up after a
// timeout. But that doesn't guarantee that flakes will be fixed so we
// just give up instead. People that want reasonable file system
// behavior on Windows should use WSL instead.
}
}
const updateVersionPackageJSON = pathToPackageJSON => {
const version = fs.readFileSync(path.join(path.dirname(__dirname), 'version.txt'), 'utf8').trim()
const json = JSON.parse(fs.readFileSync(pathToPackageJSON, 'utf8'))
if (json.version !== version) {
json.version = version
fs.writeFileSync(pathToPackageJSON, JSON.stringify(json, null, 2) + '\n')
}
}
exports.installForTests = () => {
// Build the "esbuild" binary and library
const esbuildPath = exports.buildBinary()
buildNeutralLib(esbuildPath)
// Install the "esbuild" package to a temporary directory. On Windows, it's
// sometimes randomly impossible to delete this installation directory. My
// best guess is that this is because the esbuild process is kept alive until
// the process exits for "buildSync" and "transformSync", and that sometimes
// prevents Windows from deleting the directory it's in. The call in tests to
// "rimraf.sync()" appears to hang when this happens. Other operating systems
// don't have a problem with this. This has only been a problem on the Windows
// VM in GitHub CI. I cannot reproduce this issue myself.
const installDir = path.join(os.tmpdir(), 'esbuild-' + Math.random().toString(36).slice(2))
const env = { ...process.env, ESBUILD_BINARY_PATH: esbuildPath }
fs.mkdirSync(installDir)
fs.writeFileSync(path.join(installDir, 'package.json'), '{}')
childProcess.execSync(`npm pack --silent "${npmDir}"`, { cwd: installDir, stdio: 'inherit' })
childProcess.execSync(`npm install --silent --no-audit --no-optional --ignore-scripts=false --progress=false esbuild-${version}.tgz`, { cwd: installDir, env, stdio: 'inherit' })
// Evaluate the code
const ESBUILD_PACKAGE_PATH = path.join(installDir, 'node_modules', 'esbuild')
const mod = require(ESBUILD_PACKAGE_PATH)
Object.defineProperty(mod, 'ESBUILD_PACKAGE_PATH', { value: ESBUILD_PACKAGE_PATH })
return mod
}
const updateVersionGo = () => {
const version_txt = fs.readFileSync(path.join(repoDir, 'version.txt'), 'utf8').trim()
const version_go = `package main\n\nconst esbuildVersion = "${version_txt}"\n`
const version_go_path = path.join(repoDir, 'cmd', 'esbuild', 'version.go')
// Update this atomically to avoid issues with this being overwritten during use
const temp_path = version_go_path + Math.random().toString(36).slice(1)
fs.writeFileSync(temp_path, version_go)
fs.renameSync(temp_path, version_go_path)
}
// This is helpful for ES6 modules which don't have access to __dirname
exports.dirname = __dirname
// The main Makefile invokes this script before publishing
if (require.main === module) {
if (process.argv.indexOf('--wasm') >= 0) exports.buildWasmLib(process.argv[2])
else if (process.argv.indexOf('--deno') >= 0) buildDenoLib(process.argv[2])
else if (process.argv.indexOf('--version') >= 0) updateVersionPackageJSON(process.argv[2])
else if (process.argv.indexOf('--neutral') >= 0) buildNeutralLib(process.argv[2])
else if (process.argv.indexOf('--update-version-go') >= 0) updateVersionGo()
else throw new Error('Expected a flag')
}
@@ -0,0 +1,133 @@
const fs = require('fs')
const path = require('path')
// ES5 reference: https://es5.github.io/
//
// A conforming implementation of this International standard shall interpret
// characters in conformance with the Unicode Standard, Version 3.0 or later
// and ISO/IEC 10646-1 with either UCS-2 or UTF-16 as the adopted encoding
// form, implementation level 3. If the adopted ISO/IEC 10646-1 subset is not
// otherwise specified, it is presumed to be the BMP subset, collection 300.
//
// UnicodeLetter: any character in the Unicode categories “Uppercase letter (Lu)”,
// “Lowercase letter (Ll)”, “Titlecase letter (Lt)”, “Modifier letter (Lm)”,
// “Other letter (Lo)”, or “Letter number (Nl)”.
const idStartES5 = [].concat(
require('@unicode/unicode-3.0.0/General_Category/Uppercase_Letter/code-points'),
require('@unicode/unicode-3.0.0/General_Category/Lowercase_Letter/code-points'),
require('@unicode/unicode-3.0.0/General_Category/Titlecase_Letter/code-points'),
require('@unicode/unicode-3.0.0/General_Category/Modifier_Letter/code-points'),
require('@unicode/unicode-3.0.0/General_Category/Other_Letter/code-points'),
// The "letter number" category is not included because old versions of Safari
// had a bug where they didn't include it. This means it does not match ES5.
// We need to make sure we escape these characters so Safari can read them.
// See https://github.com/evanw/esbuild/issues/1349 for more information.
// require('@unicode/unicode-3.0.0/General_Category/Letter_Number/code-points'),
).sort((a, b) => a - b)
// UnicodeCombiningMark: any character in the Unicode categories “Non-spacing mark (Mn)”
// or “Combining spacing mark (Mc)”
// UnicodeDigit: any character in the Unicode category “Decimal number (Nd)”
// UnicodeConnectorPunctuation: any character in the Unicode category “Connector punctuation (Pc)”
const idContinueES5 = idStartES5.concat(
require('@unicode/unicode-3.0.0/General_Category/Nonspacing_Mark/code-points'),
require('@unicode/unicode-3.0.0/General_Category/Spacing_Mark/code-points'),
require('@unicode/unicode-3.0.0/General_Category/Decimal_Number/code-points'),
require('@unicode/unicode-3.0.0/General_Category/Connector_Punctuation/code-points'),
).sort((a, b) => a - b)
// ESNext reference: https://tc39.es/ecma262/
//
// A conforming implementation of ECMAScript must interpret source text input
// in conformance with the Unicode Standard, Version 5.1.0 or later and ISO/IEC
// 10646. If the adopted ISO/IEC 10646-1 subset is not otherwise specified, it
// is presumed to be the Unicode set, collection 10646.
//
// UnicodeIDStart: any Unicode code point with the Unicode property “ID_Start”
const idStartESNext = require('@unicode/unicode-15.1.0/Binary_Property/ID_Start/code-points')
const idStartESNextSet = new Set(idStartESNext)
// Unicode 4.1 through Unicode 15 omitted these two characters from ID_Continue
// by accident. However, this accident was corrected in Unicode 15.1. Any JS VM
// that supports ES6+ but that uses a version of Unicode earlier than 15.1 will
// consider these to be a syntax error, so we deliberately omit these characters
// from the set of identifiers that are valid in both ES5 and ES6+. For more info
// see 2.2 in https://www.unicode.org/L2/L2023/23160-utc176-properties-recs.pdf
const ID_Continue_mistake = new Set([0x30FB, 0xFF65])
// UnicodeIDContinue: any Unicode code point with the Unicode property “ID_Continue”
const idContinueESNext = require('@unicode/unicode-15.1.0/Binary_Property/ID_Continue/code-points')
const idContinueESNextSet = new Set(idContinueESNext)
// These identifiers are valid in both ES5 and ES6+ (i.e. an intersection of both)
const idStartES5AndESNext = idStartES5.filter(n => idStartESNextSet.has(n))
const idContinueES5AndESNext = idContinueES5.filter(n => idContinueESNextSet.has(n) && !ID_Continue_mistake.has(n))
// These identifiers are valid in either ES5 or ES6+ (i.e. a union of both)
const idStartES5OrESNext = [...new Set(idStartES5.concat(idStartESNext))].sort((a, b) => a - b)
const idContinueES5OrESNext = [...new Set(idContinueES5.concat(idContinueESNext))].sort((a, b) => a - b)
function generateRangeTable(codePoints) {
let lines = []
let index = 0
let latinOffset = 0
while (latinOffset < codePoints.length && codePoints[latinOffset] <= 0xFF) {
latinOffset++
}
lines.push(
`&unicode.RangeTable{`,
`\tLatinOffset: ${latinOffset},`,
`\tR16: []unicode.Range16{`,
)
// 16-bit code points
while (index < codePoints.length && codePoints[index] < 0x1000) {
let start = codePoints[index]
index++
while (index < codePoints.length && codePoints[index] < 0x1000 && codePoints[index] === codePoints[index - 1] + 1) {
index++
}
let end = codePoints[index - 1]
lines.push(`\t\t{Lo: 0x${start.toString(16)}, Hi: 0x${end.toString(16)}, Stride: 1},`)
}
lines.push(
`\t},`,
`\tR32: []unicode.Range32{`,
)
// 32-bit code points
while (index < codePoints.length) {
let start = codePoints[index]
index++
while (index < codePoints.length && codePoints[index] === codePoints[index - 1] + 1) {
index++
}
let end = codePoints[index - 1]
lines.push(`\t\t{Lo: 0x${start.toString(16)}, Hi: 0x${end.toString(16)}, Stride: 1},`)
}
lines.push(
`\t},`,
`}`,
)
return lines.join('\n')
}
fs.writeFileSync(path.join(__dirname, '..', 'internal', 'js_ast', 'unicode.go'),
`// This file was automatically generated by ${path.basename(__filename)}. Do not edit.
package js_ast
import "unicode"
var idStartES5AndESNext = ${generateRangeTable(idStartES5AndESNext)}
var idContinueES5AndESNext = ${generateRangeTable(idContinueES5AndESNext)}
var idStartES5OrESNext = ${generateRangeTable(idStartES5OrESNext)}
var idContinueES5OrESNext = ${generateRangeTable(idContinueES5OrESNext)}
`)
@@ -0,0 +1,95 @@
.linear.red_to_green-esbuild {
background: linear-gradient(to right, color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.radial.red_to_green-esbuild {
background: radial-gradient(color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.conic.red_to_green-esbuild {
background: conic-gradient(color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.linear.rainbow_shorter-esbuild {
background: linear-gradient(to right in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_shorter-esbuild {
background: radial-gradient(in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_shorter-esbuild {
background: conic-gradient(in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.rainbow_longer-esbuild {
background: linear-gradient(to right in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_longer-esbuild {
background: radial-gradient(in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_longer-esbuild {
background: conic-gradient(in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.rainbow_increasing-esbuild {
background: linear-gradient(to right in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.radial.rainbow_increasing-esbuild {
background: radial-gradient(in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.conic.rainbow_increasing-esbuild {
background: conic-gradient(in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.linear.rainbow_decreasing-esbuild {
background: linear-gradient(to right in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_decreasing-esbuild {
background: radial-gradient(in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_decreasing-esbuild {
background: conic-gradient(in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.midpoint_hint-esbuild {
background: linear-gradient(to right, #f00, #ff0, 75%, #0ff, #00f);
}
.radial.midpoint_hint-esbuild {
background: radial-gradient(#f00, #ff0, 75%, #0ff, #00f);
}
.conic.midpoint_hint-esbuild {
background: conic-gradient(#f00, #ff0, 75%, #0ff, #00f);
}
.linear.premultiplied_alpha-esbuild {
background: linear-gradient(to right, #f00f, 10%, #00f1, 90%, #0f0f);
}
.radial.premultiplied_alpha-esbuild {
background: radial-gradient(#f00f, 10%, #00f1, 90%, #0f0f);
}
.conic.premultiplied_alpha-esbuild {
background: conic-gradient(#f00f, 10%, #00f1, 90%, #0f0f);
}
.linear.mixed_units-esbuild {
background: linear-gradient(to right, color(display-p3 0.4 0 1) 30px, color(display-p3 0.96 0.75 0.4) 60%);
}
.radial.mixed_units-esbuild {
background: radial-gradient(color(display-p3 0.4 0 1) 30px, color(display-p3 1 0.75 0.4) 60%);
}
.conic.mixed_units-esbuild {
background: conic-gradient(color(display-p3 0.4 0 1) 30deg, color(display-p3 1 0.75 0.4) 60%);
}
@@ -0,0 +1,301 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<link rel="stylesheet" href="out.css">
<style>
body {
margin: 30px;
font: 16px/20px sans-serif;
color: black;
background: white;
}
@media (prefers-color-scheme: dark) {
body {
color: white;
background: black;
}
}
a {
color: inherit;
}
p {
margin: 20px 0;
}
pre {
margin: 20px 0 10px 0;
font: 14px/20px Monaco, monospace;
white-space: pre-wrap;
}
h2 {
margin: 50px 0 0 0;
}
.linear {
max-width: 400px;
height: 20px;
text-indent: 4px;
font-size: 14px;
color: white;
text-shadow: 0 0 3px black;
background: repeating-linear-gradient(-45deg,
rgba(191, 191, 191, 0.4) 0 7px,
transparent 7px 14px);
}
section {
margin-top: 12px;
display: flex;
}
.radial+.radial {
margin-right: 12px;
}
.radial,
.conic {
width: 97px;
height: 97px;
text-align: center;
line-height: 97px;
font-size: 14px;
color: white;
text-shadow: 0 0 3px black;
background: repeating-linear-gradient(-45deg,
transparent 0 6.86px,
rgba(191, 191, 191, 0.4) 6.86px 13.72px);
}
.checkerboard {
position: relative;
}
.checkerboard:after {
content: ' ';
display: block;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
background: url('data:image/svg+xml,<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><rect width="20" height="20" fill="white"/><path d="M0 10V0H10V10H20V20H10V10H0Z" fill="%23EEEEEE"/></svg>');
}
.linear.red_to_green-native {
background: linear-gradient(to right, color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.radial.red_to_green-native {
background: radial-gradient(color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.conic.red_to_green-native {
background: conic-gradient(color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.linear.rainbow_shorter-native {
background: linear-gradient(to right in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_shorter-native {
background: radial-gradient(in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_shorter-native {
background: conic-gradient(in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.rainbow_longer-native {
background: linear-gradient(to right in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_longer-native {
background: radial-gradient(in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_longer-native {
background: conic-gradient(in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.rainbow_increasing-native {
background: linear-gradient(to right in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.radial.rainbow_increasing-native {
background: radial-gradient(in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.conic.rainbow_increasing-native {
background: conic-gradient(in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.linear.rainbow_decreasing-native {
background: linear-gradient(to right in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_decreasing-native {
background: radial-gradient(in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_decreasing-native {
background: conic-gradient(in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.midpoint_hint-native {
background: linear-gradient(to right, #f00, #ff0, 75%, #0ff, #00f);
}
.radial.midpoint_hint-native {
background: radial-gradient(#f00, #ff0, 75%, #0ff, #00f);
}
.conic.midpoint_hint-native {
background: conic-gradient(#f00, #ff0, 75%, #0ff, #00f);
}
.linear.premultiplied_alpha-native {
background: linear-gradient(to right, #f00f, 10%, #00f1, 90%, #0f0f);
}
.radial.premultiplied_alpha-native {
background: radial-gradient(#f00f, 10%, #00f1, 90%, #0f0f);
}
.conic.premultiplied_alpha-native {
background: conic-gradient(#f00f, 10%, #00f1, 90%, #0f0f);
}
.linear.mixed_units-native {
background: linear-gradient(to right, color(display-p3 0.4 0 1) 30px, color(display-p3 1 0.75 0.4) 60%);
}
.radial.mixed_units-native {
background: radial-gradient(color(display-p3 0.4 0 1) 30px, color(display-p3 1 0.75 0.4) 60%);
}
.conic.mixed_units-native {
background: conic-gradient(color(display-p3 0.4 0 1) 30deg, color(display-p3 1 0.75 0.4) 60%);
}
</style>
</head>
<body>
<p>This page is a visual test of esbuild's CSS gradient transformation. Run it like this:</p>
<pre>./esbuild --servedir=scripts --outfile=scripts/out.css scripts/gradient-tests.css --target=firefox30</pre>
<p>Then open this page as <a
href="http://localhost:8000/gradient-tests.html">http://localhost:8000/gradient-tests.html</a>.</p>
<h2>1. Red to green in P3</h2>
<pre>gradient(
color(display-p3 1 0 0),
color(display-p3 0 0.6 0))</pre>
<div class="linear red_to_green-native">native</div>
<div class="linear red_to_green-esbuild">esbuild</div>
<section>
<div class="radial red_to_green-native">native</div>
<div class="radial red_to_green-esbuild">esbuild</div>
<div class="conic red_to_green-native">native</div>
<div class="conic red_to_green-esbuild">esbuild</div>
</section>
<h2>2. Rainbow using shorter hue</h2>
<pre>gradient(
in hwb shorter hue,
hsl(180deg 100% 75%) 10%,
hsl(240deg 100% 75%) 90%)</pre>
<div class="linear rainbow_shorter-native">native</div>
<div class="linear rainbow_shorter-esbuild">esbuild</div>
<section>
<div class="radial rainbow_shorter-native">native</div>
<div class="radial rainbow_shorter-esbuild">esbuild</div>
<div class="conic rainbow_shorter-native">native</div>
<div class="conic rainbow_shorter-esbuild">esbuild</div>
</section>
<h2>3. Rainbow using longer hue</h2>
<pre>gradient(
in hsl longer hue,
hsl(180deg 100% 75%) 10%,
hsl(240deg 100% 75%) 90%)</pre>
<div class="linear rainbow_longer-native">native</div>
<div class="linear rainbow_longer-esbuild">esbuild</div>
<section>
<div class="radial rainbow_longer-native">native</div>
<div class="radial rainbow_longer-esbuild">esbuild</div>
<div class="conic rainbow_longer-native">native</div>
<div class="conic rainbow_longer-esbuild">esbuild</div>
</section>
<h2>4. Rainbow using increasing hue</h2>
<pre>gradient(
in lch increasing hue,
hsl(240deg 100% 75%) 10%,
hsl(180deg 100% 75%) 90%)</pre>
<div class="linear rainbow_increasing-native">native</div>
<div class="linear rainbow_increasing-esbuild">esbuild</div>
<section>
<div class="radial rainbow_increasing-native">native</div>
<div class="radial rainbow_increasing-esbuild">esbuild</div>
<div class="conic rainbow_increasing-native">native</div>
<div class="conic rainbow_increasing-esbuild">esbuild</div>
</section>
<h2>5. Rainbow using decreasing hue</h2>
<pre>gradient(
in oklch decreasing hue,
hsl(180deg 100% 75%) 10%,
hsl(240deg 100% 75%) 90%)</pre>
<div class="linear rainbow_decreasing-native">native</div>
<div class="linear rainbow_decreasing-esbuild">esbuild</div>
<section>
<div class="radial rainbow_decreasing-native">native</div>
<div class="radial rainbow_decreasing-esbuild">esbuild</div>
<div class="conic rainbow_decreasing-native">native</div>
<div class="conic rainbow_decreasing-esbuild">esbuild</div>
</section>
<h2>6. Transition hint / midpoint</h2>
<pre>gradient(#f00, #ff0, 75%, #0ff, #00f)</pre>
<div class="linear midpoint_hint-native">native</div>
<div class="linear midpoint_hint-esbuild">esbuild</div>
<section>
<div class="radial midpoint_hint-native">native</div>
<div class="radial midpoint_hint-esbuild">esbuild</div>
<div class="conic midpoint_hint-native">native</div>
<div class="conic midpoint_hint-esbuild">esbuild</div>
</section>
<h2>7. Premultiplied alpha</h2>
<pre>gradient(#f00f, 10%, #00f1, 90%, #0f0f)</pre>
<div class="checkerboard linear premultiplied_alpha-native">native</div>
<div class="checkerboard linear premultiplied_alpha-esbuild">esbuild</div>
<section>
<div class="checkerboard radial premultiplied_alpha-native">native</div>
<div class="checkerboard radial premultiplied_alpha-esbuild">esbuild</div>
<div class="checkerboard conic premultiplied_alpha-native">native</div>
<div class="checkerboard conic premultiplied_alpha-esbuild">esbuild</div>
</section>
<h2>8. Mixed units</h2>
<pre>gradient(
color(display-p3 0.4 0 1) 30px,
color(display-p3 1 0.75 0.4) 60%)</pre>
<div class="linear mixed_units-native">native</div>
<div class="linear mixed_units-esbuild">esbuild</div>
<section>
<div class="radial mixed_units-native">native</div>
<div class="radial mixed_units-esbuild">esbuild</div>
<div class="conic mixed_units-native">native</div>
<div class="conic mixed_units-esbuild">esbuild</div>
</section>
</body>
</html>
@@ -0,0 +1,475 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title>Graph debugger</title>
<style>
body {
margin: 0;
overflow: hidden;
font: 14px/20px sans-serif;
background: #fff;
color: #000;
}
@media (prefers-color-scheme: dark) {
body {
background: #222;
color: #ddd;
}
}
canvas {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
#instructions {
padding: 20px;
}
</style>
</head>
<body>
<div id="instructions">
This is a debugger for some of esbuild's internals. To use:
<ol>
<li>Set "debugVerboseMetafile = true"</li>
<li>Serve the esbuild repo over localhost</li>
<li>Visit this page with "#metafile=path/to/metafile.json"</li>
</ol>
</div>
<canvas></canvas>
<script type="module">
const lightColors = {
bg: '#fff',
fg: '#000',
accent: '#7BF',
}
const darkColors = {
bg: '#222',
fg: '#ddd',
accent: '#FB0',
}
const prefersColorSchemeDark = matchMedia("(prefers-color-scheme: dark)")
function lightOrDarkColors() {
return prefersColorSchemeDark.matches ? darkColors : lightColors
}
const paddingX = 10
const paddingY = 10
class InputFile {
constructor(source, data) {
this.source = source
this.data = data
this.x = 0
this.y = 0
this.w = 0
this.h = 0
this.measure()
}
measure() {
this.w = 0
this.h = 0
// Title
c.font = titleFont
this.w = Math.max(this.w, c.measureText(this.source).width)
this.h += titleLineHeight
// Parts
let prevIndent = 0
c.font = codeFont
for (const part of this.data.parts) {
let code = part.code.replace(/\t/g, ' ')
const lastNewline = code.lastIndexOf('\n')
let nextIndent = 0
if (lastNewline >= 0) {
const lastLine = code.slice(lastNewline + 1)
nextIndent = lastLine.length
if (!/\S/.test(lastLine)) code = code.slice(0, lastNewline)
}
code = ' '.repeat(prevIndent) + code
prevIndent = nextIndent
part.lines = code.split('\n')
part.y = this.h
for (const line of part.lines) {
this.w = Math.max(this.w, c.measureText(line).width)
this.h += codeLineHeight
}
if (part.nsExportPartIndex) {
this.w = Math.max(this.w, c.measureText('/* <nsExportPartIndex> */').width)
}
if (part.wrapperPartIndex) {
this.w = Math.max(this.w, c.measureText('/* <wrapperPartIndex> */').width)
}
part.h = this.h - part.y
}
this.w += paddingX * 2
this.h += paddingY * 2
}
render() {
const colors = lightOrDarkColors()
// Background
c.clearRect(this.x, this.y, this.w, this.h)
// Title
c.font = titleFont
c.textBaseline = 'middle'
c.fillStyle = colors.fg
c.fillText(this.source, this.x + paddingX, this.y + paddingY + titleLineHeight / 2)
// Lines
c.font = codeFont
c.textBaseline = 'middle'
c.fillStyle = colors.fg
for (const part of this.data.parts) {
c.globalAlpha = part.isLive ? 1 : 0.2
for (let i = 0; i < part.lines.length; i++) {
c.fillText(part.lines[i], this.x + paddingX, this.y + paddingY + part.y + i * codeLineHeight + codeLineHeight / 2)
}
if (part.nsExportPartIndex) {
c.fillText('/* <nsExportPartIndex> */', this.x + paddingX, this.y + paddingY + part.y + codeLineHeight / 2)
}
if (part.wrapperPartIndex) {
c.fillText('/* <wrapperPartIndex> */', this.x + paddingX, this.y + paddingY + part.y + codeLineHeight / 2)
}
}
// Border
c.globalAlpha = 0.2
c.strokeStyle = colors.fg
c.strokeRect(this.x, this.y, this.w, this.h)
c.globalAlpha = 1
}
renderHover() {
const colors = lightOrDarkColors()
if (this.hoveredPart === -1) {
c.fillStyle = colors.accent
c.globalAlpha = 0.2
c.fillRect(this.x, this.y + paddingY, this.w, titleLineHeight)
c.globalAlpha = 1
c.fillRect(this.x, this.y + paddingY, 4, titleLineHeight)
c.strokeStyle = colors.fg
c.fillStyle = colors.fg
for (const part of this.data.parts) {
if (part.canBeRemovedIfUnused) continue
drawArrow(
this.x, this.y + paddingY + titleLineHeight / 2, -1,
this.x, this.y + paddingY + part.y + codeLineHeight / 2, -1,
)
}
} else if (this.hoveredPart !== null) {
const part = this.data.parts[this.hoveredPart]
c.fillStyle = colors.accent
c.globalAlpha = 0.2
c.fillRect(this.x, this.y + paddingY + part.y, this.w, part.h)
c.globalAlpha = 1
c.fillRect(this.x, this.y + paddingY + part.y, 4, part.h)
c.strokeStyle = colors.fg
c.fillStyle = colors.fg
drawArrow(
this.x, this.y + paddingY + part.y + codeLineHeight / 2, -1,
this.x, this.y + paddingY + titleLineHeight / 2, -1,
)
for (const dep of part.dependencies) {
if (dep.source === this.source) {
const otherPart = this.data.parts[dep.partIndex]
drawArrow(
this.x, this.y + paddingY + part.y + codeLineHeight / 2, -1,
this.x, this.y + paddingY + otherPart.y + codeLineHeight / 2, -1,
)
continue
}
const otherFile = inputFiles.find(file => file.source === dep.source)
if (!otherFile) continue
const otherPart = otherFile.data.parts[dep.partIndex]
drawArrow(
this.x + this.w, this.y + paddingY + part.y + codeLineHeight / 2, 1,
otherFile.x, otherFile.y + paddingY + otherPart.y + codeLineHeight / 2, -1,
)
}
for (const record of part.importRecords) {
const otherFile = inputFiles.find(file => file.source === record.source)
if (!otherFile) continue
drawArrow(
this.x + this.w, this.y + paddingY + part.y + codeLineHeight / 2, 1,
otherFile.x, otherFile.y + paddingY + titleLineHeight / 2, -1,
)
}
let lines = []
lines.push(`isLive: ${part.isLive}`)
if (part.declaredSymbols.length > 0) {
lines.push(`declaredSymbols:`)
for (const declSym of part.declaredSymbols) {
lines.push(` ${declSym.name}`)
}
}
if (part.symbolUses.length > 0) {
lines.push(`symbolUses:`)
for (const use of part.symbolUses) {
lines.push(` ${use.name} ${use.countEstimate}x`)
}
}
if (part.importRecords.length > 0) {
lines.push(`importRecords:`)
for (const record of part.importRecords) {
lines.push(` ${record.source}`)
}
}
c.font = normalFont
c.textBaseline = 'middle'
c.fillStyle = colors.fg
for (let i = 0; i < lines.length; i++) {
c.fillText(lines[i], this.x + 10, this.y + this.h + 10 + i * normalLineHeight + normalLineHeight / 2)
}
}
}
hoveredPart = null
onHover(mouseX, mouseY) {
this.hoveredPart = null
if (mouseX !== null && mouseY !== null &&
mouseX >= this.x && mouseX < this.x + this.w &&
mouseY >= this.y && mouseY < this.y + this.h) {
let y = mouseY - this.y - paddingY
if (y >= 0 && y < titleLineHeight) {
this.hoveredPart = -1
return true
}
for (let i = 0; i < this.data.parts.length; i++) {
const part = this.data.parts[i]
if (y >= part.y && y < part.y + part.h) {
this.hoveredPart = i
return true
}
}
}
}
onMouseMove(mouseX, mouseY) {
if (mouseX >= this.x && mouseX < this.x + this.w &&
mouseY >= this.y && mouseY < this.y + this.h) {
document.body.style.cursor = 'move'
return true
}
}
oldX = 0
oldY = 0
onMouseDown(mouseX, mouseY) {
if (mouseX >= this.x && mouseX < this.x + this.w &&
mouseY >= this.y && mouseY < this.y + this.h) {
this.oldX = mouseX
this.oldY = mouseY
document.body.style.cursor = 'move'
return true
}
}
onMouseDrag(mouseX, mouseY) {
this.x += mouseX - this.oldX
this.y += mouseY - this.oldY
this.oldX = mouseX
this.oldY = mouseY
document.body.style.cursor = 'move'
}
onMouseUp(e) {
}
}
function drawArrow(ax, ay, adx, bx, by, bdx) {
let dx = bx - ax
let dy = by - ay
let d = Math.sqrt(dx * dx + dy * dy)
let scale = d / 2
c.beginPath()
c.moveTo(ax, ay)
c.bezierCurveTo(
ax + adx * (10 + scale), ay,
bx + bdx * 10 + bdx * scale, by,
bx + bdx * 10, by,
)
c.stroke()
c.beginPath()
c.moveTo(bx, by)
c.lineTo(bx + bdx * 10, by - 5)
c.lineTo(bx + bdx * 10, by + 5)
c.fill()
}
const canvas = document.querySelector('canvas')
const c = canvas.getContext('2d')
const titleFont = '20px sans-serif'
const titleLineHeight = 30
const codeFont = '12px monospace'
const codeLineHeight = 18
const normalFont = '14px sans-serif'
const normalLineHeight = 18
let width = 0, height = 0
let scrollX = 0, scrollY = 0
let metafile
const instructions = document.getElementById('instructions')
try {
if (!location.hash.startsWith('#metafile=')) throw new Error('Expected "#metafile=" in URL')
metafile = await fetch(location.hash.slice('#metafile='.length)).then(r => r.json())
} catch (err) {
const error = document.createElement('div')
error.style.color = 'red'
error.textContent = err
instructions.append(error)
throw err
}
instructions.remove()
const outputSource = Object.keys(metafile.outputs)[0]
const output = metafile.outputs[outputSource]
const inputFiles = Object.entries(output.inputs).map(([source, data]) => new InputFile(source, data)).reverse()
for (let i = 0; i < inputFiles.length; i++) {
const file = inputFiles[i]
file.y = 100
if (i === 0) {
file.x = 100
} else {
const prevFile = inputFiles[i - 1]
file.x = prevFile.x + prevFile.w + 100
}
}
function render() {
const colors = lightOrDarkColors()
width = innerWidth
height = innerHeight
const ratio = devicePixelRatio
canvas.width = Math.round(width * ratio)
canvas.height = Math.round(height * ratio)
canvas.style.background = colors.bg
c.scale(ratio, ratio)
// Title
c.font = titleFont
c.textBaseline = 'top'
c.fillStyle = colors.fg
c.fillText(outputSource, 10, 10)
// Content
c.translate(-scrollX, -scrollY)
// Inputs
for (let i = inputFiles.length - 1; i >= 0; i--) inputFiles[i].render()
for (let i = inputFiles.length - 1; i >= 0; i--) inputFiles[i].renderHover()
}
addEventListener('wheel', e => {
e.preventDefault()
if (e.ctrlKey) return
scrollX += e.deltaX
scrollY += e.deltaY
}, { passive: false })
let draggingFile = null
let isDragging = false
onmousemove = e => {
let mouseX = e.pageX + scrollX
let mouseY = e.pageY + scrollY
document.body.style.cursor = 'default'
if (isDragging) {
if (draggingFile) {
draggingFile.onMouseDrag(mouseX, mouseY)
}
} else {
for (const file of inputFiles) {
if (file.onMouseMove(mouseX, mouseY)) {
break
}
}
onhover(mouseX, mouseY)
}
}
onmousedown = e => {
let mouseX = e.pageX + scrollX
let mouseY = e.pageY + scrollY
if (!isDragging) {
isDragging = true
for (const file of inputFiles) {
if (file.onMouseDown(mouseX, mouseY)) {
draggingFile = file
break
}
}
}
onhover(mouseX, mouseY)
}
onmouseup = e => {
let mouseX = e.pageX + scrollX
let mouseY = e.pageY + scrollY
if (isDragging) {
if (draggingFile) {
draggingFile.onMouseUp(mouseX, mouseY)
draggingFile = null
}
isDragging = false
}
onhover(mouseX, mouseY)
}
function onhover(mouseX, mouseY) {
for (const file of inputFiles) {
if (file.onHover(mouseX, mouseY)) {
mouseX = null
mouseY = null
}
}
}
onblur = () => {
draggingFile = null
isDragging = false
}
function tick() {
requestAnimationFrame(tick)
render()
}
tick()
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,111 @@
// This test verifies that:
// - a running service will not prevent NodeJS to exit if there is no compilation in progress.
// - the NodeJS process will continue running if there is a serve() active or a transform or build in progress.
const assert = require('assert')
const { fork } = require('child_process');
// The tests to run in the child process
async function tests() {
const esbuild = require('./esbuild').installForTests()
async function testTransform() {
const t1 = await esbuild.transform(`1+2`)
const t2 = await esbuild.transform(`1+3`)
assert.strictEqual(t1.code, `1 + 2;\n`)
assert.strictEqual(t2.code, `1 + 3;\n`)
}
async function testServe() {
const context = await esbuild.context({})
try {
const server = await context.serve({})
assert(server.hosts.includes('127.0.0.1'))
assert.strictEqual(typeof server.port, 'number')
} finally {
await context.dispose()
}
}
async function testBuild() {
const context = await esbuild.context({
stdin: { contents: '1+2' },
write: false,
})
try {
const result = await context.rebuild()
assert.deepStrictEqual(result.outputFiles.length, 1);
assert.deepStrictEqual(result.outputFiles[0].text, '1 + 2;\n');
const result2 = await context.rebuild()
assert.deepStrictEqual(result2.outputFiles.length, 1);
assert.deepStrictEqual(result2.outputFiles[0].text, '1 + 2;\n');
} finally {
await context.dispose()
}
}
async function testWatchAndIncremental() {
const context = await esbuild.context({
stdin: { contents: '1+2' },
write: false,
})
try {
await context.watch()
const result = await context.rebuild()
assert.deepStrictEqual(result.outputFiles.length, 1);
assert.deepStrictEqual(result.outputFiles[0].text, '1 + 2;\n');
} finally {
await context.dispose()
}
}
await testTransform()
await testServe()
await testBuild()
await testWatchAndIncremental()
}
// Called when this is the child process to run the tests.
function runTests() {
process.exitCode = 1;
tests().then(() => {
process.exitCode = 0;
}, (error) => {
console.error('❌', error)
});
}
// A child process need to be started to verify that a running service is not hanging node.
function startChildProcess() {
const child = fork(__filename, ['__forked__'], { stdio: 'inherit', env: process.env });
const timeout = setTimeout(() => {
console.error('❌ node unref test timeout - child_process.unref() broken?')
process.exit(1);
}, 5 * 60 * 1000);
child.on('error', (error) => {
console.error('❌', error);
process.exit(1);
})
child.on('exit', (code) => {
clearTimeout(timeout);
if (code) {
console.error(`❌ node unref tests failed: child exited with code ${code}`)
process.exit(1);
} else {
console.log(`✅ node unref tests passed`)
}
})
}
if (process.argv[2] === '__forked__') {
runTests();
} else {
startChildProcess();
}
+218
View File
@@ -0,0 +1,218 @@
{
"name": "scripts",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"@types/node": "14.14.6",
"@unicode/unicode-15.1.0": "1.5.2",
"@unicode/unicode-3.0.0": "1.0.6",
"fuse.js": "3.2.0",
"js-yaml": "3.14.0",
"react": "17.0.1",
"source-map": "0.7.4",
"typescript": "4.7.4"
}
},
"node_modules/@types/node": {
"version": "14.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz",
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw=="
},
"node_modules/@unicode/unicode-15.1.0": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@unicode/unicode-15.1.0/-/unicode-15.1.0-1.5.2.tgz",
"integrity": "sha512-7PAgnShDr8ziK6XeHB/TUVFboDFEhaQKKyrw55/Kx9o6AQDy1s7dJ9KRpRerW9nrR5qMGUQvOqTXOAek6ZIXkg=="
},
"node_modules/@unicode/unicode-3.0.0": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@unicode/unicode-3.0.0/-/unicode-3.0.0-1.0.6.tgz",
"integrity": "sha512-eHhtHouHRCE3r0Sq4Spp/P6z1S000pEYkimT2d1xRoYrUDBUJ2TXbIfn96DgdslhBDtoIrnq2kdsCHWnh5RmBg=="
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/fuse.js": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.2.0.tgz",
"integrity": "sha1-8ESOgGmFW/Kj5oPNwdMg5+KgfvQ="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz",
"integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"engines": {
"node": ">= 8"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
}
},
"dependencies": {
"@types/node": {
"version": "14.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz",
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw=="
},
"@unicode/unicode-15.1.0": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@unicode/unicode-15.1.0/-/unicode-15.1.0-1.5.2.tgz",
"integrity": "sha512-7PAgnShDr8ziK6XeHB/TUVFboDFEhaQKKyrw55/Kx9o6AQDy1s7dJ9KRpRerW9nrR5qMGUQvOqTXOAek6ZIXkg=="
},
"@unicode/unicode-3.0.0": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@unicode/unicode-3.0.0/-/unicode-3.0.0-1.0.6.tgz",
"integrity": "sha512-eHhtHouHRCE3r0Sq4Spp/P6z1S000pEYkimT2d1xRoYrUDBUJ2TXbIfn96DgdslhBDtoIrnq2kdsCHWnh5RmBg=="
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"fuse.js": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.2.0.tgz",
"integrity": "sha1-8ESOgGmFW/Kj5oPNwdMg5+KgfvQ="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"js-yaml": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"react": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz",
"integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ=="
}
}
}
@@ -0,0 +1,12 @@
{
"dependencies": {
"@types/node": "14.14.6",
"@unicode/unicode-15.1.0": "1.5.2",
"@unicode/unicode-3.0.0": "1.0.6",
"fuse.js": "3.2.0",
"js-yaml": "3.14.0",
"react": "17.0.1",
"source-map": "0.7.4",
"typescript": "4.7.4"
}
}
@@ -0,0 +1,61 @@
// This script parses all .ts and .tsx files in the current directory using
// esbuild. This is useful to check for parser bugs and/or crashes in esbuild.
const fs = require('fs');
const os = require('os');
const path = require('path');
const ts = require('typescript');
const child_process = require('child_process');
const esbuildPath = path.join(path.dirname(__dirname), 'esbuild');
function walkDir(root, cb) {
for (const entry of fs.readdirSync(root)) {
const absolute = path.join(root, entry);
if (fs.statSync(absolute).isDirectory()) {
walkDir(absolute, cb);
} else if ((entry.endsWith('.ts') && !entry.endsWith('.d.ts')) || entry.endsWith('.tsx')) {
cb(absolute)
}
}
}
child_process.execSync('make', { cwd: path.dirname(__dirname) });
// Doing one file at a time is useful for debugging crashes
if (process.argv.includes('--individual')) {
walkDir(process.cwd(), absolute => {
let output = child_process.spawnSync(esbuildPath, [absolute, '--outfile=/dev/null'], { stdio: ['inherit', 'pipe', 'pipe'] });
if (output.status) {
let result;
try {
result = ts.transpileModule(fs.readFileSync(absolute, 'utf8'), { reportDiagnostics: true });
} catch (e) {
// Ignore this file if the TypeScript compiler crashes on it
return
}
if (result.diagnostics.length > 0) {
// Ignore this file if the TypeScript compiler has parse errors
return
}
console.log('-'.repeat(80));
console.log('Failure:', absolute);
console.log('-'.repeat(20) + ' esbuild output:');
console.log(output.stdout + output.stderr);
}
});
}
// Otherwise it's much faster to do everything at once
else {
const tempDir = path.join(os.tmpdir(), 'esbuild-parse-ts-files');
try {
fs.mkdirSync(tempDir);
} catch (e) {
}
const all = [];
walkDir(process.cwd(), absolute => all.push(absolute));
try {
child_process.execFileSync(esbuildPath, ['--outdir=' + tempDir].concat(all), { stdio: 'inherit' });
} catch (e) {
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,115 @@
const { installForTests, removeRecursiveSync } = require('./esbuild')
const child_process = require('child_process')
const path = require('path')
const fs = require('fs')
const assert = require('assert')
const esbuild = installForTests()
// Create a fresh test directory
const rootTestDir = path.join(__dirname, '.register-test')
removeRecursiveSync(rootTestDir)
fs.mkdirSync(rootTestDir)
const entry = path.join(rootTestDir, 'entry.ts')
fs.writeFileSync(entry, `
console.log('in entry.ts' as string)
require('./other.ts')
`)
const other = path.join(rootTestDir, 'other.ts')
fs.writeFileSync(other, `
console.log('in other.ts' as string)
`)
const register = path.join(rootTestDir, 'register.js')
fs.writeFileSync(register, `
const esbuild = require(${JSON.stringify(esbuild.ESBUILD_PACKAGE_PATH)});
const fs = require('fs');
require.extensions['.ts'] = (mod, filename) => {
const ts = fs.readFileSync(filename, 'utf8');
const { code } = esbuild.transformSync(ts, { loader: 'ts' });
mod._compile(code, filename);
};
`)
let tests = {
async fromMainThread() {
let result = await new Promise((resolve, reject) => child_process.execFile('node', ['-r', register, entry], (err, stdout) => {
if (err) reject(err)
else resolve(stdout)
}))
assert.strictEqual(result, `in entry.ts\nin other.ts\n`)
},
async fromChildThread({ testDir }) {
const startThread = path.join(testDir, 'startThread.js')
fs.writeFileSync(startThread, `
const worker_threads = require('worker_threads')
if (worker_threads.isMainThread) {
console.log('in startThread.js')
const worker = new worker_threads.Worker(__filename)
worker.postMessage(null)
worker.on('message', logs => {
for (const log of logs) console.log(log)
worker.terminate()
})
} else {
worker_threads.parentPort.on('message', () => {
console.log('in worker')
let logs = []
console.log = x => logs.push(x)
require('../entry.ts')
worker_threads.parentPort.postMessage(logs)
})
}
`)
let result = await new Promise((resolve, reject) => child_process.execFile('node', ['-r', register, startThread], (err, stdout) => {
if (err) reject(err)
else resolve(stdout)
}))
assert.strictEqual(result, `in startThread.js\nin worker\nin entry.ts\nin other.ts\n`)
},
}
async function main() {
// Time out these tests after 5 minutes. This exists to help debug test hangs in CI.
let minutes = 5
let timeout = setTimeout(() => {
console.error(`❌ register tests timed out after ${minutes} minutes, exiting...`)
process.exit(1)
}, minutes * 60 * 1000)
const runTest = async ([name, fn]) => {
let testDir = path.join(rootTestDir, name)
try {
fs.mkdirSync(testDir)
await fn({ esbuild, testDir })
removeRecursiveSync(testDir)
return true
} catch (e) {
console.error(`❌ ${name}: ${e && e.message || e}`)
return false
}
}
// Run all tests in serial
let allTestsPassed = true
for (let test of Object.entries(tests)) {
if (!await runTest(test)) {
allTestsPassed = false
}
}
if (!allTestsPassed) {
console.error(`❌ register tests failed`)
process.exit(1)
} else {
console.log(`✅ register tests passed`)
removeRecursiveSync(rootTestDir)
}
clearTimeout(timeout);
}
main().catch(e => setTimeout(() => { throw e }))
@@ -0,0 +1,372 @@
const { installForTests } = require('./esbuild');
const childProcess = require('child_process');
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const repoDir = path.dirname(__dirname);
const testDir = path.join(repoDir, 'scripts', '.terser-tests');
const terserDir = path.join(repoDir, 'demo', 'terser');
let U;
main().catch(e => setTimeout(() => { throw e }));
async function main() {
// Terser's stdout comparisons fail if this is true since stdout contains
// terminal color escape codes
process.stdout.isTTY = false;
// Make sure the tests are installed
console.log('Downloading terser...');
childProcess.execSync('make demo/terser', { cwd: repoDir, stdio: 'pipe' });
U = require(terserDir);
// Create a fresh test directory
childProcess.execSync(`rm -fr "${testDir}"`);
fs.mkdirSync(testDir)
// Start the esbuild service
const esbuild = installForTests();
// Find test files
const compressDir = path.join(terserDir, 'test', 'compress');
const files = fs.readdirSync(compressDir).filter(name => name.endsWith('.js'));
// Run all tests concurrently
let passedTotal = 0;
let failedTotal = 0;
const runTest = file => test_file(esbuild, path.join(compressDir, file))
.then(({ passed, failed }) => {
passedTotal += passed;
failedTotal += failed;
});
await Promise.all(files.map(runTest));
// Clean up test output
childProcess.execSync(`rm -fr "${testDir}"`);
console.log(`${failedTotal} failed out of ${passedTotal + failedTotal}`);
if (failedTotal) {
process.exit(1);
}
}
async function test_file(esbuild, file) {
let passed = 0;
let failed = 0;
const tests = parse_test(file);
const runTest = name => test_case(esbuild, tests[name])
.then(() => passed++)
.catch(e => {
failed++;
console.error(`❌ ${file}: ${name}: ${(e && e.message || e).trim()}\n`);
pass = false;
});
await Promise.all(Object.keys(tests).map(runTest));
return { passed, failed };
}
// Modified from "terser/demo/test/compress.js"
async function test_case(esbuild, test) {
const sandbox = require(path.join(terserDir, 'test', 'sandbox'));
const log = (format, args) => { throw new Error(tmpl(format, args)); };
var semver = require(path.join(terserDir, 'node_modules', 'semver'));
var output_options = test.beautify || {};
// Generate the input code
if (test.input instanceof U.AST_SimpleStatement
&& test.input.body instanceof U.AST_TemplateString) {
try {
var input = U.parse(test.input.body.segments[0].value);
} catch (ex) {
return false;
}
var input_code = make_code(input, output_options);
var input_formatted = test.input.body.segments[0].value;
} else {
var input = as_toplevel(test.input, test.mangle);
var input_code = make_code(input, output_options);
var input_formatted = make_code(test.input, {
ecma: 2015,
beautify: true,
quote_style: 3,
keep_quoted_props: true
});
}
// Make sure it's valid
try {
U.parse(input_code);
} catch (ex) {
log("!!! Cannot parse input\n---INPUT---\n{input}\n--PARSE ERROR--\n{error}\n\n", {
input: input_formatted,
error: ex,
});
return false;
}
// Pretty-print it
var ast = input.to_mozilla_ast();
var mozilla_options = {
ecma: output_options.ecma,
ascii_only: output_options.ascii_only,
comments: false,
};
var ast_as_string = U.AST_Node.from_mozilla_ast(ast).print_to_string(mozilla_options);
// Run esbuild as a minifier
try {
var { code: output } = await esbuild.transform(ast_as_string, {
minify: true,
keepNames: test.options.keep_fnames,
});
} catch (e) {
const formatError = ({ text, location }) => {
if (!location) return `\nerror: ${text}`;
const { file, line, column } = location;
return `\n${file}:${line}:${column}: ERROR: ${text}`;
}
log("!!! esbuild failed\n---INPUT---\n{input}\n---ERROR---\n{error}\n", {
input: ast_as_string,
error: (e && e.message || e) + '' + (e.errors ? e.errors.map(formatError) : ''),
});
return false;
}
// Make sure esbuild generates valid JavaScript
try {
U.parse(output);
} catch (ex) {
log("!!! Test matched expected result but cannot parse output\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n--REPARSE ERROR--\n{error}\n\n", {
input: input_formatted,
output: output,
error: ex.stack,
});
return false;
}
// Verify that the stdout matches our expectations
if (test.expect_stdout
&& (!test.node_version || semver.satisfies(process.version, test.node_version))
&& !process.env.TEST_NO_SANDBOX
) {
if (test.expect_stdout === true) {
test.expect_stdout = sandbox.run_code(input_code, test.prepend_code);
}
var stdout = sandbox.run_code(output, test.prepend_code);
if (!sandbox.same_stdout(test.expect_stdout, stdout)) {
log("!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", {
input: input_formatted,
output: output,
expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR",
expected: test.expect_stdout,
actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR",
actual: stdout,
});
return false;
}
}
return true;
}
////////////////////////////////////////////////////////////////////////////////
// The code below was copied verbatim from "terser/demo/test/compress.js"
//
// UglifyJS is released under the BSD license:
//
// Copyright 2012-2019 (c) Mihai Bazon <mihai.bazon@gmail.com>
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above
// copyright notice, this list of conditions and the following
// disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
// OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
function tmpl() {
return U.string_template.apply(this, arguments);
}
function as_toplevel(input, mangle_options) {
if (!(input instanceof U.AST_BlockStatement))
throw new Error("Unsupported input syntax");
for (var i = 0; i < input.body.length; i++) {
var stat = input.body[i];
if (stat instanceof U.AST_SimpleStatement && stat.body instanceof U.AST_String)
input.body[i] = new U.AST_Directive(stat.body);
else break;
}
var toplevel = new U.AST_Toplevel(input);
toplevel.figure_out_scope(mangle_options);
return toplevel;
}
function parse_test(file) {
var script = fs.readFileSync(file, "utf8");
// TODO try/catch can be removed after fixing https://github.com/mishoo/UglifyJS2/issues/348
try {
var ast = U.parse(script, {
filename: file
});
} catch (e) {
console.log("Caught error while parsing tests in " + file + "\n");
console.log(e);
throw e;
}
var tests = {};
var tw = new U.TreeWalker(function (node, descend) {
if (node instanceof U.AST_LabeledStatement
&& tw.parent() instanceof U.AST_Toplevel) {
var name = node.label.name;
if (name in tests) {
throw new Error('Duplicated test name "' + name + '" in ' + file);
}
tests[name] = get_one_test(name, node.body);
return true;
}
if (!(node instanceof U.AST_Toplevel)) croak(node);
});
ast.walk(tw);
return tests;
function croak(node) {
throw new Error(tmpl("Can't understand test file {file} [{line},{col}]\n{code}", {
file: file,
line: node.start.line,
col: node.start.col,
code: make_code(node, { beautify: false })
}));
}
function read_boolean(stat) {
if (stat.TYPE == "SimpleStatement") {
var body = stat.body;
if (body instanceof U.AST_Boolean) {
return body.value;
}
}
throw new Error("Should be boolean");
}
function read_string(stat) {
if (stat.TYPE == "SimpleStatement") {
var body = stat.body;
switch (body.TYPE) {
case "String":
return body.value;
case "Array":
return body.elements.map(function (element) {
if (element.TYPE !== "String")
throw new Error("Should be array of strings");
return element.value;
}).join("\n");
}
}
throw new Error("Should be string or array of strings");
}
function get_one_test(name, block) {
var test = {
name: name,
options: {},
reminify: true,
};
var tw = new U.TreeWalker(function (node, descend) {
if (node instanceof U.AST_Assign) {
if (!(node.left instanceof U.AST_SymbolRef)) {
croak(node);
}
var name = node.left.name;
test[name] = evaluate(node.right);
return true;
}
if (node instanceof U.AST_LabeledStatement) {
var label = node.label;
assert.ok(
[
"input",
"prepend_code",
"expect",
"expect_error",
"expect_exact",
"expect_warnings",
"expect_stdout",
"node_version",
"reminify",
].includes(label.name),
tmpl("Unsupported label {name} [{line},{col}]", {
name: label.name,
line: label.start.line,
col: label.start.col
})
);
var stat = node.body;
if (label.name == "expect_exact" || label.name == "node_version") {
test[label.name] = read_string(stat);
} else if (label.name == "reminify") {
var value = read_boolean(stat);
test.reminify = value == null || value;
} else if (label.name == "expect_stdout") {
var body = stat.body;
if (body instanceof U.AST_Boolean) {
test[label.name] = body.value;
} else if (body instanceof U.AST_Call) {
var ctor = global[body.expression.name];
assert.ok(ctor === Error || ctor.prototype instanceof Error, tmpl("Unsupported expect_stdout format [{line},{col}]", {
line: label.start.line,
col: label.start.col
}));
test[label.name] = ctor.apply(null, body.args.map(function (node) {
assert.ok(node instanceof U.AST_Constant, tmpl("Unsupported expect_stdout format [{line},{col}]", {
line: label.start.line,
col: label.start.col
}));
return node.value;
}));
} else {
test[label.name] = read_string(stat) + "\n";
}
} else if (label.name === "prepend_code") {
test[label.name] = read_string(stat);
} else {
test[label.name] = stat;
}
return true;
}
});
block.walk(tw);
return test;
}
}
function make_code(ast, options) {
var stream = U.OutputStream(options);
ast.print(stream);
return stream.get();
}
function evaluate(code) {
if (code instanceof U.AST_Node)
code = make_code(code, { beautify: true });
return new Function("return(" + code + ")")();
}
@@ -0,0 +1,93 @@
const child_process = require('child_process')
const esbuild = require('./esbuild')
const path = require('path')
const fs = require('fs')
const ESBUILD_BINARY_PATH = esbuild.buildBinary()
const rootDir = path.join(__dirname, '..', 'require', 'yarnpnp')
function run(command) {
console.log('\n\x1B[37m' + '$ ' + command + '\x1B[0m')
child_process.execSync(command, { cwd: rootDir, stdio: 'inherit' })
}
function modTime(file) {
return fs.statSync(file).mtimeMs
}
function reinstallYarnIfNeeded() {
const yarnPath = path.join(rootDir, '.yarn', 'releases', 'yarn-4.0.0-rc.22.cjs')
if (fs.existsSync(yarnPath) && modTime(yarnPath) > Math.max(
modTime(path.join(rootDir, 'package.json')),
modTime(path.join(rootDir, 'yarn.lock')),
)) {
return
}
fs.rmSync(path.join(rootDir, '.pnp.cjs'), { recursive: true, force: true })
fs.rmSync(path.join(rootDir, '.pnp.loader.mjs'), { recursive: true, force: true })
fs.rmSync(path.join(rootDir, '.yarn'), { recursive: true, force: true })
fs.rmSync(path.join(rootDir, '.yarnrc.yml'), { recursive: true, force: true })
try {
run('yarn set version 4.0.0-rc.22')
} catch {
run('npm i -g yarn') // Install Yarn globally if it's not already installed
run('yarn set version 4.0.0-rc.22')
}
let rc
try {
rc = fs.readFileSync(path.join(rootDir, '.yarnrc.yml'), 'utf8')
} catch {
rc = '' // Sometimes this file doesn't exist, so pretend it's empty
}
fs.writeFileSync(path.join(rootDir, '.yarnrc.yml'), `
pnpEnableEsmLoader: true
pnpIgnorePatterns: ["./bar/**"]
# Note: Yarn 4 defaults to "enableGlobalCache: true" which doesn't
# work on Windows due to cross-drive issues with relative paths.
# Explicitly set "enableGlobalCache: false" to avoid this issue.
enableGlobalCache: false
` + rc)
run('yarn install')
}
function runTests() {
// Make sure the tests are valid
run('yarn node in.mjs')
// Test the native build
child_process.execFileSync(ESBUILD_BINARY_PATH, [
'in.mjs',
'--bundle',
'--log-level=debug',
'--platform=node',
'--outfile=out-native.js',
], { cwd: rootDir, stdio: 'inherit' })
run('node out-native.js')
// Test the WebAssembly build
esbuild.buildWasmLib(ESBUILD_BINARY_PATH)
run('node ../../npm/esbuild-wasm/bin/esbuild in.mjs --bundle --log-level=debug --platform=node --outfile=out-wasm.js')
run('node out-wasm.js')
// Test the WebAssembly build when run through Yarn's file system shim
esbuild.buildWasmLib(ESBUILD_BINARY_PATH)
run('yarn node ../../npm/esbuild-wasm/bin/esbuild in.mjs --bundle --log-level=debug --platform=node --outfile=out-wasm-yarn.js')
run('node out-wasm-yarn.js')
}
const minutes = 10
const timeout = setTimeout(() => {
console.error(`❌ Yarn PnP tests timed out after ${minutes} minutes`)
process.exit(1)
}, minutes * 60 * 1000)
reinstallYarnIfNeeded()
runTests()
clearTimeout(timeout)
@@ -0,0 +1,114 @@
// This file runs test262 tests for the async iteration proposal:
// https://github.com/tc39/proposal-async-iteration. It's intended
// to be informative (to identify failure points) but not to be
// run in CI. Not even node/V8 passes all of these tests.
const esbuild = require('./esbuild').installForTests()
const ts = require('./node_modules/typescript')
const fs = require('fs')
const vm = require('vm')
const test262Dir = __dirname + '/../demo/test262'
const shim = ``
+ fs.readFileSync(test262Dir + '/harness/assert.js', 'utf8')
+ fs.readFileSync(test262Dir + '/harness/asyncHelpers.js', 'utf8')
+ fs.readFileSync(test262Dir + '/harness/compareArray.js', 'utf8')
+ fs.readFileSync(test262Dir + '/harness/propertyHelper.js', 'utf8')
+ fs.readFileSync(test262Dir + '/harness/sta.js', 'utf8')
async function main() {
const tests = []
for (const dir of [
test262Dir + '/test/language/statements/async-generator',
test262Dir + '/test/language/statements/for-await-of',
]) {
for (const name of fs.readdirSync(dir)) {
if (name.endsWith('.js')) {
tests.push(dir + '/' + name)
}
}
}
for (const test of tests) {
const stuck = () => console.log(`❌ ${test}: stuck`)
process.on('exit', stuck)
let js = fs.readFileSync(test, 'utf8')
let negative = /negative:/.test(js)
let flags = /flags:.*/.exec(js)
flags = flags && new Set(/flags: \[([^\]]*)\]/.exec(js)[1].split(', '))
let didFail = false
const check = async (kind, codePromise) => {
let error
try {
let { code } = await codePromise
code = shim + code
if (flags && flags.has('onlyStrict')) {
code = '"use strict";\n' + code
}
await new Promise((resolve, reject) => {
if (flags && flags.has('async')) {
vm.runInContext(code, vm.createContext({ $DONE: err => err ? reject(err) : resolve() }))
} else {
vm.runInContext(code, vm.createContext({}))
resolve()
}
})
} catch (err) {
error = err
}
if (!error === !negative) {
return true
}
if (!didFail) {
didFail = true
console.log(`\n❌ \x1B[37m${test}\x1B[0m`)
}
if (kind === 'native') kind = '\x1B[32m[' + kind + ']\x1B[0m'
else if (kind.startsWith('ts')) kind = '\x1B[34m[' + kind + ']\x1B[0m'
else kind = '\x1B[33m[' + kind + ']\x1B[0m'
console.log(` ${kind} ${(error + '' || 'unexpected success').replace(/\n/g, '\\n')}`)
return false
}
const tsTranspile = async (compilerOptions) => {
const { outputText } = ts.transpileModule(js, { compilerOptions })
return { code: outputText }
}
// See if node's native support checks out (it currently doesn't due to recent specification changes)
await check('native', { code: js })
// Ignore tests that check whether "yield" can be used as a valid
// identifier. This obviously doesn't work when we must transform
// async functions into generator functions, but that's fine.
const hasYieldKeyword = test.includes('yield-ident-valid') || test.includes('array-elem-target-yield-valid')
// Check "tsc"
await check('ts=esnext', tsTranspile({ target: ts.ScriptTarget.ESNext }))
await check('ts=es2017', tsTranspile({ target: ts.ScriptTarget.ES2017 }))
if (!hasYieldKeyword) {
await check('ts=es2016', { code: ts.transpileModule(js, { compilerOptions: { target: ts.ScriptTarget.ES2016 } }).outputText })
}
// Check "esbuild"
await check('target=esnext', esbuild.transform(js, { keepNames: true, target: 'esnext' }))
await check('target=es2017', esbuild.transform(js, { keepNames: true, target: 'es2017' }))
await check('for-await=false', esbuild.transform(js, { keepNames: true, supported: { 'for-await': false } }))
if (!hasYieldKeyword) {
await check('target=es2016', esbuild.transform(js, { keepNames: true, target: 'es2016' }))
await check('async-await=false', esbuild.transform(js, { keepNames: true, supported: { 'async-await': false } }))
await check('for-await=false,async-await=false', esbuild.transform(js, { keepNames: true, supported: { 'for-await': false, 'async-await': false } }))
}
process.off('exit', stuck)
}
console.log(`Done`)
}
main()
+650
View File
@@ -0,0 +1,650 @@
const vm = require('vm')
const fs = require('fs')
const path = require('path')
const jsYaml = require('js-yaml')
const isatty = require('tty').isatty(process.stdout.fd)
const { installForTests } = require('./esbuild')
const testDir = path.join(__dirname, '..', 'demo', 'test262', 'test')
const harnessDir = path.join(__dirname, '..', 'demo', 'test262', 'harness')
const progressBarLength = 64
const eraseProgressBar = () => {
previousProgressBar = null
let text = `\r${' '.repeat(progressBarLength)}\r`
if (printNewlineWhenErasing) {
printNewlineWhenErasing = false
text += '\n'
}
return text
}
let previousProgressBar = null
let printNewlineWhenErasing = false
const resetColor = isatty ? `\x1b[0m` : ''
const boldColor = isatty ? `\x1b[1m` : ''
const dimColor = isatty ? `\x1b[37m` : ''
const underlineColor = isatty ? `\x1b[4m` : ''
const redColor = isatty ? `\x1b[31m` : ''
const greenColor = isatty ? `\x1b[32m` : ''
const blueColor = isatty ? `\x1b[34m` : ''
const cyanColor = isatty ? `\x1b[36m` : ''
const magentaColor = isatty ? `\x1b[35m` : ''
const yellowColor = isatty ? `\x1b[33m` : ''
const redBgRedColor = isatty ? `\x1b[41;31m` : ''
const redBgWhiteColor = isatty ? `\x1b[41;97m` : ''
const greenBgGreenColor = isatty ? `\x1b[42;32m` : ''
const greenBgWhiteColor = isatty ? `\x1b[42;97m` : ''
const blueBgBlueColor = isatty ? `\x1b[44;34m` : ''
const blueBgWhiteColor = isatty ? `\x1b[44;97m` : ''
const cyanBgCyanColor = isatty ? `\x1b[46;36m` : ''
const cyanBgBlackColor = isatty ? `\x1b[46;30m` : ''
const magentaBgMagentaColor = isatty ? `\x1b[45;35m` : ''
const magentaBgBlackColor = isatty ? `\x1b[45;30m` : ''
const yellowBgYellowColor = isatty ? `\x1b[43;33m` : ''
const yellowBgBlackColor = isatty ? `\x1b[43;30m` : ''
const whiteBgWhiteColor = isatty ? `\x1b[107;97m` : ''
const whiteBgBlackColor = isatty ? `\x1b[107;30m` : ''
const skipTheseFeatures = new Set([
'regexp-v-flag',
'regexp-match-indices',
'regexp-named-groups',
'regexp-unicode-property-escapes',
])
const skipTheseTests = new Set([
// Skip these tests because esbuild deliberately always supports ESM syntax
// in all files. Also esbuild doesn't support the script goal at all.
'language/expressions/import.meta/syntax/goal-script.js',
'language/global-code/export.js',
'language/global-code/import.js',
// Skip these tests because we deliberately support top-level return (input
// files are treated as CommonJS and/or ESM but never as global code, and
// top-level return is allowed in CommonJS)
'language/statements/return/S12.9_A1_T1.js',
'language/statements/return/S12.9_A1_T10.js',
'language/statements/return/S12.9_A1_T2.js',
'language/statements/return/S12.9_A1_T3.js',
'language/statements/return/S12.9_A1_T4.js',
'language/statements/return/S12.9_A1_T5.js',
'language/statements/return/S12.9_A1_T6.js',
'language/statements/return/S12.9_A1_T7.js',
'language/statements/return/S12.9_A1_T8.js',
'language/statements/return/S12.9_A1_T9.js',
'language/global-code/return.js',
// Skip these tests because we deliberately support parsing top-level await
// in all files. Files containing top-level await are always interpreted as
// ESM, never as CommonJS.
'language/expressions/assignmenttargettype/simple-basic-identifierreference-await.js',
'language/expressions/await/await-BindingIdentifier-in-global.js',
'language/expressions/await/await-in-global.js',
'language/expressions/await/await-in-nested-function.js',
'language/expressions/await/await-in-nested-generator.js',
'language/expressions/class/class-name-ident-await-escaped.js',
'language/expressions/class/class-name-ident-await.js',
'language/expressions/class/static-init-await-reference.js',
'language/expressions/dynamic-import/2nd-param-await-ident.js',
'language/expressions/dynamic-import/assignment-expression/await-identifier.js',
'language/expressions/function/static-init-await-reference.js',
'language/expressions/generators/static-init-await-reference.js',
'language/expressions/in/private-field-rhs-await-absent.js',
'language/expressions/object/identifier-shorthand-await-strict-mode.js',
'language/expressions/object/method-definition/static-init-await-reference-accessor.js',
'language/expressions/object/method-definition/static-init-await-reference-generator.js',
'language/expressions/object/method-definition/static-init-await-reference-normal.js',
'language/module-code/top-level-await/new-await-script-code.js',
'language/reserved-words/await-script.js',
'language/statements/class/class-name-ident-await-escaped.js',
'language/statements/class/class-name-ident-await.js',
'language/statements/labeled/value-await-non-module-escaped.js',
'language/statements/labeled/value-await-non-module.js',
// Skip these tests because we don't currently validate the contents of
// regular expressions. We could do this but it's not necessary to parse
// JavaScript successfully since we parse enough of it to be able to
// determine where the regular expression ends (just "\" and "[]" pairs).
'language/literals/regexp/early-err-pattern.js',
'language/literals/regexp/invalid-braced-quantifier-exact.js',
'language/literals/regexp/invalid-braced-quantifier-lower.js',
'language/literals/regexp/invalid-braced-quantifier-range.js',
'language/literals/regexp/invalid-optional-lookbehind.js',
'language/literals/regexp/invalid-optional-negative-lookbehind.js',
'language/literals/regexp/invalid-range-lookbehind.js',
'language/literals/regexp/invalid-range-negative-lookbehind.js',
'language/literals/regexp/u-invalid-class-escape.js',
'language/literals/regexp/u-invalid-extended-pattern-char.js',
'language/literals/regexp/u-invalid-identity-escape.js',
'language/literals/regexp/u-invalid-legacy-octal-escape.js',
'language/literals/regexp/u-invalid-non-empty-class-ranges-no-dash-a.js',
'language/literals/regexp/u-invalid-non-empty-class-ranges-no-dash-ab.js',
'language/literals/regexp/u-invalid-non-empty-class-ranges-no-dash-b.js',
'language/literals/regexp/u-invalid-non-empty-class-ranges.js',
'language/literals/regexp/u-invalid-oob-decimal-escape.js',
'language/literals/regexp/u-invalid-optional-lookahead.js',
'language/literals/regexp/u-invalid-optional-lookbehind.js',
'language/literals/regexp/u-invalid-optional-negative-lookahead.js',
'language/literals/regexp/u-invalid-optional-negative-lookbehind.js',
'language/literals/regexp/u-invalid-range-lookahead.js',
'language/literals/regexp/u-invalid-range-lookbehind.js',
'language/literals/regexp/u-invalid-range-negative-lookahead.js',
'language/literals/regexp/u-invalid-range-negative-lookbehind.js',
'language/literals/regexp/u-unicode-esc-bounds.js',
'language/literals/regexp/u-unicode-esc-non-hex.js',
'language/literals/regexp/unicode-escape-nls-err.js',
])
const skipEvaluatingTheseIncludes = new Set([
'nativeFunctionMatcher.js', // We don't preserve "toString()" on functions
])
const skipEvaluatingTheseFeatures = new Set([
// Node's version of V8 doesn't implement these
'hashbang',
'legacy-regexp',
'regexp-duplicate-named-groups',
'symbols-as-weakmap-keys',
'tail-call-optimization',
// We don't care about API-related things
'ArrayBuffer',
'change-array-by-copy',
'DataView',
'resizable-arraybuffer',
'ShadowRealm',
'SharedArrayBuffer',
'String.prototype.toWellFormed',
'Symbol.match',
'Symbol.replace',
'Symbol.unscopables',
'Temporal',
'TypedArray',
])
const skipEvaluatingTheseTests = new Set([
// This test is skipped because it crashes V8:
//
// #
// # Fatal error in , line 0
// # Check failed: i < self->length().
// #
// #
// #
// #FailureMessage Object: 0x7fffac9a1c30
// 1: 0xc2abf1 [node]
// 2: 0x1f231d4 V8_Fatal(char const*, ...) [node]
// 3: 0xdac209 v8::FixedArray::Get(v8::Local<v8::Context>, int) const [node]
// 4: 0xb74ab7 [node]
// 5: 0xb76a2d [node]
// 6: 0xf2436f v8::internal::Isolate::RunHostImportModuleDynamicallyCallback(v8::internal::MaybeHandle<v8::internal::Script>,
// v8::internal::Handle<v8::internal::Object>, v8::internal::MaybeHandle<v8::internal::Object>) [node]
// 7: 0x137e3db v8::internal::Runtime_DynamicImportCall(int, unsigned long*, v8::internal::Isolate*) [node]
// 8: 0x17fd4f4 [node]
//
'language/expressions/dynamic-import/import-assertions/2nd-param-assert-enumeration.js',
])
function findFiles() {
function visit(dir) {
for (const entry of fs.readdirSync(dir)) {
const fullEntry = path.join(dir, entry)
const stats = fs.statSync(fullEntry)
if (stats.isDirectory()) {
visit(fullEntry)
} else if (stats.isFile() && entry.endsWith('.js') && !entry.includes('_FIXTURE')) {
files.push(fullEntry)
}
}
}
const files = []
for (const entry of fs.readdirSync(testDir)) {
if (entry === 'staging' || entry === 'intl402' || entry === 'built-ins') continue // We're not interested in these
visit(path.join(testDir, entry))
}
// Reverse for faster iteration times because many of the more interesting tests come last
return files.reverse()
}
async function checkTransformAPI({ esbuild, file, content, yaml }) {
if (yaml.flags) {
if (yaml.flags.includes('onlyStrict')) content = '"use strict";' + content
if (yaml.flags.includes('module')) content += '\nexport {}'
}
// Step 1: Try transforming the file normally
const shouldParse = !yaml.negative || yaml.negative.phase !== 'parse'
let result
try {
result = await esbuild.transform(content, { sourcefile: file })
} catch (error) {
if (shouldParse) {
error.kind = 'Transform'
throw error
}
return // Stop now if this test is supposed to fail
}
if (!shouldParse) {
const error = new Error('Unexpected successful transform')
error.kind = 'Transform'
throw error
}
// Step 2: Try transforming the output again (this should always succeed)
let result2
try {
result2 = await esbuild.transform(result.code, { sourcefile: file })
} catch (error) {
error.kind = 'Reparse'
throw error
}
// Step 3: The output should be the same the second time
if (result2.code !== result.code) {
const lines = result.code.split('\n')
const lines2 = result2.code.split('\n')
let i = 0
while (i < lines.length && i < lines2.length && lines[i] === lines2[i]) i++
const error = { toString: () => `${redColor}-${lines[i]}\n${greenColor}+${lines2[i]}${resetColor}` }
error.kind = 'Reprint'
throw error
}
// Step 4: Try minifying the output once
let result4
try {
result4 = await esbuild.transform(result2.code, { sourcefile: file, minify: true })
} catch (error) {
error.kind = 'Minify'
throw error
}
// Step 5: Try minifying the output again
let result5
try {
result5 = await esbuild.transform(result4.code, { sourcefile: file, minify: true })
} catch (error) {
error.kind = 'Minify'
throw error
}
}
async function checkBuildAPI({ esbuild, file, content, yaml }) {
const plugins = []
if (yaml.flags) {
const isOnlyStrict = yaml.flags.includes('onlyStrict')
const isModule = yaml.flags.includes('module')
if (isOnlyStrict || isModule) {
plugins.push({
name: 'modify',
setup(build) {
build.onLoad({ filter: /./ }, args => {
if (args.path === file) {
let loaded = content
if (isOnlyStrict) loaded = '"use strict";' + loaded
if (isModule) loaded += '\nexport {}'
return { contents: loaded }
}
})
},
})
}
}
// Step 1: Try building the file normally
const isModule = yaml.flags && yaml.flags.includes('module')
const isDynamicImport = yaml.flags && yaml.flags.includes('dynamic-import')
const isAsync = yaml.flags && yaml.flags.includes('async')
const shouldParse = !yaml.negative || yaml.negative.phase === 'runtime'
let result
try {
const options = {
entryPoints: [file],
write: false,
keepNames: true,
logLevel: 'silent',
plugins,
target: 'node' + process.version.slice(1),
logOverride: { 'direct-eval': 'warning' },
}
if (isModule || isDynamicImport || isAsync) {
options.bundle = true
options.format = isModule ? 'esm' : 'iife'
options.external = [
'', // Some tests use this as a dummy argument to an "import('')" that is never evaluated
]
}
result = await esbuild.build(options)
} catch (error) {
if (shouldParse) {
error.kind = 'Build'
throw error
}
return // Stop now if this test is supposed to fail
}
if (!shouldParse) {
const error = new Error('Unexpected successful build')
error.kind = 'Build'
throw error
}
// Don't evaluate problematic files
const hasDirectEval = result.warnings.some(msg => msg.id === 'direct-eval')
if (
hasDirectEval ||
skipEvaluatingTheseTests.has(path.relative(testDir, file)) ||
(yaml.includes && yaml.includes.some(include => skipEvaluatingTheseIncludes.has(include))) ||
(yaml.features && yaml.features.some(feature => skipEvaluatingTheseFeatures.has(feature)))
) {
return
}
// Step 3: Try evaluating the file using node
const importDir = path.dirname(file)
const shouldEvaluate = !yaml.negative
try {
await runCodeInHarness(yaml, content, importDir)
} catch (error) {
// Ignore tests that fail when run in node
if (shouldEvaluate) console.log(eraseProgressBar() + dimColor + `IGNORING ${path.relative(testDir, file)}: ${error}` + resetColor)
return
}
if (!shouldEvaluate) {
// Ignore tests that incorrectly pass when run in node
return
}
// Step 3: Try evaluating the file we generated
const code = result.outputFiles[0].text
try {
await runCodeInHarness(yaml, code, importDir)
} catch (error) {
if (shouldEvaluate) {
if (typeof error === 'string') error = new Error(error)
error.kind = 'Evaluate'
throw error
}
return // Stop now if this test is supposed to fail
}
if (!shouldEvaluate) {
const error = new Error('Unexpected successful evaluation')
error.kind = 'Evaluate'
throw error
}
// Step 4: If evaluation worked and was supposed to work, check to see
// if it still works after running esbuild's syntax lowering pass on it
for (let version = 2015; version <= 2022; version++) {
let result
try {
result = await esbuild.transform(code, { sourcefile: file, target: `es${version}` })
} catch (error) {
continue // This means esbuild doesn't support lowering this code to this old a version
}
try {
await runCodeInHarness(yaml, code, importDir)
} catch (error) {
if (typeof error === 'string') error = new Error(error)
error.kind = 'Lower'
throw error
}
break
}
}
async function main() {
const startTime = Date.now()
// Get this warning out of the way so it's not in the middle of our output.
// Note: If this constructor is missing, you need to run node with the
// "--experimental-vm-modules" flag (and use a version of node that has it).
{
const temp = new vm.SourceTextModule('')
await temp.link(() => { throw new Error })
await temp.evaluate()
}
console.log(`\n${dimColor}Finding tests...${resetColor}`)
const files = findFiles()
console.log(`Found ${files.length} test files`)
console.log(`\n${dimColor}Installing esbuild...${resetColor}`)
const esbuild = installForTests()
console.log(`\n${dimColor}Running tests...${resetColor}\n`)
const errorCounts = {}
let skippedCount = 0
await forEachInParallel(files, 32, async (file) => {
// Don't parse files that esbuild deliberately handles differently
if (skipTheseTests.has(path.relative(testDir, file))) {
skippedCount++
return
}
try {
const content = fs.readFileSync(file, 'utf8')
const start = content.indexOf('/*---')
const end = content.indexOf('---*/')
if (start < 0 || end < 0) throw new Error(`Missing YAML metadata`)
const yaml = jsYaml.safeLoad(content.slice(start + 5, end))
// Don't parse files that test things we don't care about
if (yaml.features && yaml.features.some(feature => skipTheseFeatures.has(feature))) {
skippedCount++
return
}
await checkTransformAPI({ esbuild, file, content, yaml })
await checkBuildAPI({ esbuild, file, content, yaml })
}
catch (error) {
errorCounts[error.kind] = (errorCounts[error.kind] || 0) + 1
printError(file, error)
}
})
const table = []
table.push(['Total tests', `${files.length}`])
table.push(['Tests ran', `${files.length - skippedCount}`])
table.push(['Tests skipped', `${skippedCount}`])
for (const kind of Object.keys(errorCounts).sort()) {
table.push([kind + ' errors', `${errorCounts[kind]}`])
}
const seconds = (Date.now() - startTime) / 1000
const minutes = Math.floor(seconds / 60)
table.push(['Time taken', `${minutes ? `${minutes} min ${+(seconds - minutes * 60).toFixed(1)} sec` : `${+seconds.toFixed(1)} sec`}`])
const maxLength = Math.max(...table.map(x => x[0].length))
printNewlineWhenErasing = true
process.stdout.write(eraseProgressBar())
for (const [key, value] of table) {
console.log(`${boldColor}${(key + ':').padEnd(maxLength + 1)}${resetColor} ${value}`)
}
}
function forEachInParallel(items, batchSize, callback) {
return new Promise((resolve, reject) => {
let inFlight = 0
let i = 0
function next() {
if (i === items.length && inFlight === 0) {
process.stdout.write(eraseProgressBar())
return resolve()
}
const completed = Math.floor(progressBarLength * i / items.length)
if (previousProgressBar !== completed) {
previousProgressBar = completed
const progressHead = '\u2501'.repeat(Math.max(0, completed - 1))
const progressBoundary = completed ? '\u252B' : ''
const progressTail = '\u2500'.repeat(progressBarLength - completed)
process.stdout.write(`\r` + greenColor + progressHead + progressBoundary + dimColor + progressTail + resetColor)
}
while (i < items.length && inFlight < batchSize) {
inFlight++
callback(items[i++]).then(() => {
inFlight--
next()
}, reject)
}
}
next()
})
}
const harnessFiles = new Map
let defaultHarness = ''
for (const entry of fs.readdirSync(harnessDir)) {
if (entry.startsWith('.') || !entry.endsWith('.js')) {
continue
}
const file = path.join(harnessDir, entry)
const content = fs.readFileSync(file, 'utf8')
if (entry === 'assert.js' || entry === 'sta.js') {
defaultHarness += content
continue
}
harnessFiles.set(entry, content)
}
function createHarnessForTest(yaml) {
let harness = defaultHarness
if (yaml.includes) {
for (const include of yaml.includes) {
const content = harnessFiles.get(include)
if (!content) throw new Error(`Included file is missing: ${include}`)
harness += content
}
}
return harness
}
async function runCodeInHarness(yaml, code, importDir) {
const context = {}
const isAsync = yaml.flags && yaml.flags.includes('async')
const isModule = yaml.flags && yaml.flags.includes('module')
const isRaw = yaml.flags && yaml.flags.includes('raw')
// See: https://github.com/nodejs/node/issues/36351
const unique = () => '//' + Math.random()
const runCode = async () => {
const moduleCache = new Map
const dynamicImportCache = new Map
const findModule = (modulePath) => {
let module = moduleCache.get(modulePath)
if (!module) {
const code = fs.readFileSync(modulePath, 'utf8')
if (modulePath.endsWith('json')) {
const evaluate = function () {
this.setExport('default', vm.runInContext('JSON.parse', context)(code))
}
module = new vm.SyntheticModule(['default'], evaluate, { context })
} else {
module = new vm.SourceTextModule(code + unique(), { context, importModuleDynamically })
}
moduleCache.set(modulePath, module)
}
return module
}
const linker = (specifier, referencingModule) => {
return findModule(path.join(importDir, specifier))
}
const importModuleDynamically = (specifier, script) => {
const where = path.join(importDir, specifier)
let promise = dynamicImportCache.get(where)
if (!promise) {
const module = findModule(where, context)
if (module.status === 'unlinked') {
promise = module.link(linker)
.then(() => module.evaluate())
.then(() => module)
} else {
promise = Promise.resolve(module)
}
dynamicImportCache.set(where, promise)
}
return promise
}
vm.createContext(context)
if (!isRaw) vm.runInContext(createHarnessForTest(yaml), context)
if (isModule) {
const module = new vm.SourceTextModule(code + unique(), { context, importModuleDynamically })
await module.link(linker)
await module.evaluate()
} else {
const script = new vm.Script(code, { importModuleDynamically })
script.runInContext(context)
}
}
if (isAsync) {
await new Promise((resolve, reject) => {
context.$DONE = err => err ? reject(err) : resolve()
runCode(code, context).catch(reject)
})
} else {
await runCode(code, context)
}
}
function printError(file, error) {
let detail
if (error.errors) {
const { text, location } = error.errors[0]
if (location) {
const { file, line, column, lineText, length } = location
detail = ' ' + dimColor + path.basename(file) + ':' + line + ':' + column + ': ' + resetColor + text + '\n' +
' ' + dimColor + lineText.slice(0, column) + greenColor + lineText.slice(column, column + length) + dimColor + lineText.slice(column + length) + resetColor + '\n' +
' ' + greenColor + ' '.repeat(column) + (length > 1 ? '~'.repeat(length) : '^') + resetColor
} else {
detail = dimColor + ('\n' + text).split('\n').join('\n ').slice(1) + resetColor
}
} else {
detail = dimColor + ('\n' + error).split('\n').join('\n ').slice(1) + resetColor
}
const prettyPath = path.relative(testDir, file)
printNewlineWhenErasing = true
console.log(eraseProgressBar() + tagMap[error.kind] + ' ' + prettyPath + '\n' + detail)
printNewlineWhenErasing = true
}
const tagMap = {
Transform: redBgRedColor + `[` + redBgWhiteColor + `TRANSFORM ERROR` + redBgRedColor + `]` + resetColor,
Build: magentaBgMagentaColor + `[` + magentaBgBlackColor + `BUILD ERROR` + magentaBgMagentaColor + `]` + resetColor,
Reparse: yellowBgYellowColor + `[` + yellowBgBlackColor + `REPARSE ERROR` + yellowBgYellowColor + `]` + resetColor,
Reprint: cyanBgCyanColor + `[` + cyanBgBlackColor + `REPRINT ERROR` + cyanBgCyanColor + `]` + resetColor,
Minify: blueBgBlueColor + `[` + blueBgWhiteColor + `MINIFY ERROR` + blueBgBlueColor + `]` + resetColor,
Evaluate: greenBgGreenColor + `[` + greenBgWhiteColor + `EVALUATE ERROR` + greenBgGreenColor + `]` + resetColor,
Lower: whiteBgWhiteColor + `[` + whiteBgBlackColor + `LOWER ERROR` + whiteBgWhiteColor + `]` + resetColor,
}
process.on('unhandledRejection', () => {
// Don't exit when a test does this
})
main().catch(e => setTimeout(() => {
throw e
}))
+432
View File
@@ -0,0 +1,432 @@
<!-- This is a simple playground to try out esbuild in your browser -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title>Try esbuild live</title>
<style>
body {
font: 15px/120% sans-serif;
overflow: hidden;
}
h2,
p {
cursor: default;
user-select: none;
}
textarea {
resize: none;
width: 100%;
height: 100%;
}
textarea,
#output .outer {
box-sizing: border-box;
padding: 4px;
color: inherit;
font-size: 13px;
line-height: 110%;
font-family: monospace;
cursor: text;
tab-size: 2;
}
#output .outer {
white-space: pre-wrap;
overflow-y: auto;
}
.minified {
word-break: break-all;
}
textarea:focus {
outline: none;
border: 1px solid #999;
}
hr {
border: 1px solid #999;
margin: 1em 0;
}
section {
position: absolute;
top: 0;
bottom: 0;
padding: 0 20px;
box-sizing: border-box;
}
section p {
line-height: 26px;
}
#input {
left: 0;
width: 50%;
}
#output {
right: 0;
width: 50%;
}
.outer {
position: absolute;
left: 20px;
top: 120px;
right: 20px;
bottom: 20px;
}
#input .outer {
right: 10px;
}
#output .outer {
left: 10px;
}
label {
white-space: nowrap;
}
body {
background: #EEE;
color: #333;
}
textarea,
#output .outer {
background: #FFF;
border: 1px solid #CCC;
}
.terminal-1 {
font-weight: bold;
}
.terminal-37 {
color: #999;
}
.terminal-4 {
text-decoration: underline;
}
.terminal-31 {
color: #D00;
}
.terminal-32 {
color: #0D0;
}
.terminal-33 {
color: #CC0;
}
.terminal-35 {
color: #D0D;
}
@media (prefers-color-scheme: dark) {
body {
background: #333;
color: #DDD;
}
textarea,
#output .outer {
background: #111;
border: 1px solid #555;
}
.terminal-37 {
color: #777;
}
}
</style>
</head>
<body>
<section id="input">
<h2>Input</h2>
<p>
<label for="target">
Target: <select id="target">
<option>ES5</option>
<option>ES2015</option>
<option>ES2016</option>
<option>ES2017</option>
<option>ES2018</option>
<option>ES2019</option>
<option>ES2020</option>
<option>ES2021</option>
<option selected>ESNext</option>
<option>Chrome70</option>
<option>Chrome71</option>
<option>Chrome72</option>
<option>Chrome73</option>
<option>Chrome74</option>
<option>Chrome75</option>
<option>Chrome76</option>
<option>Chrome77</option>
<option>Chrome78</option>
<option>Chrome79</option>
<option>Chrome80</option>
<option>Chrome81</option>
<option>Chrome82</option>
<option>Chrome83</option>
<option>Chrome84</option>
<option>Chrome85</option>
<option>Chrome86</option>
<option>Chrome87</option>
<option>Chrome88</option>
<option>Chrome89</option>
</select>
</label>
&nbsp; &nbsp;
<label for="loader">
Loader: <select id="loader">
<option selected>JS</option>
<option>JSX</option>
<option>TS</option>
<option>TSX</option>
<option>CSS</option>
<option>JSON</option>
<option>Text</option>
<option>Base64</option>
<option>DataURL</option>
<option>Binary</option>
</select>
</label>
&nbsp; &nbsp;
<label for="format">
Format: <select id="format">
<option selected>Preserve</option>
<option>IIFE</option>
<option>CJS</option>
<option>ESM</option>
</select>
</label>
<br>
<label for="bundle">
<input id="bundle" type="checkbox">
Bundle
</label>
&nbsp; &nbsp;
<label for="ascii">
<input id="ascii" type="checkbox">
ASCII
</label>
&nbsp; &nbsp;
<label for="keepNames">
<input id="keepNames" type="checkbox">
Keep Names
</label>
&nbsp; &nbsp;
<label for="mangleProps">
<input id="mangleProps" type="checkbox">
Mangle Props
</label>
&nbsp; &nbsp;
Minify:
<label for="minify-syntax"><input id="minify-syntax" type="checkbox"> Syntax</label>
<label for="minify-idents"><input id="minify-idents" type="checkbox"> Identifiers</label>
<label for="minify-spaces"><input id="minify-spaces" type="checkbox"> Whitespace</label>
</p>
<div class="outer"><textarea autofocus spellcheck="false"></textarea></div>
</section>
<section id="output">
<h2>Output</h2>
<div class="outer"></div>
</section>
<script src="../npm/esbuild-wasm/lib/browser.js"></script>
<script>
const input = document.querySelector('#input textarea')
const target = document.querySelector('#target')
const loader = document.querySelector('#loader')
const format = document.querySelector('#format')
const minifySyntax = document.querySelector('#minify-syntax')
const minifyIdents = document.querySelector('#minify-idents')
const minifySpaces = document.querySelector('#minify-spaces')
const ascii = document.querySelector('#ascii')
const bundle = document.querySelector('#bundle')
const keepNames = document.querySelector('#keepNames')
const mangleProps = document.querySelector('#mangleProps')
let runIfIdle
function persistValue(el, key) {
el.onchange = () => {
runIfIdle()
try {
sessionStorage.setItem(key, el.value)
} catch (e) {
}
}
try {
const old = sessionStorage.getItem(key)
if (old !== null) el.value = old
} catch (e) {
}
}
function persistChecked(el, key) {
el.onchange = () => {
runIfIdle()
try {
sessionStorage.setItem(key, el.checked)
} catch (e) {
}
}
try {
const old = sessionStorage.getItem(key)
if (old !== null) el.checked = old === 'true'
} catch (e) {
}
}
persistValue(target, 'target')
persistValue(loader, 'loader')
persistValue(format, 'format')
persistChecked(minifySyntax, 'minifySyntax')
persistChecked(minifyIdents, 'minifyIdents')
persistChecked(minifySpaces, 'minifySpaces')
persistChecked(ascii, 'ascii')
persistChecked(bundle, 'bundle')
persistChecked(keepNames, 'keepNames')
persistChecked(mangleProps, 'mangleProps')
try {
const old = sessionStorage.getItem('input')
if (old !== null) input.value = old
} catch (e) {
}
esbuild.initialize({
wasmURL: '../npm/esbuild-wasm/esbuild.wasm',
}).then(() => {
const output = document.querySelector('#output .outer')
let debounceTimeout = 0
let isRunning = false
let needsRun = false
input.oninput = () => {
clearTimeout(debounceTimeout)
debounceTimeout = setTimeout(runIfIdle, 50)
}
runIfIdle = async () => {
clearTimeout(debounceTimeout)
if (isRunning) {
needsRun = true
return
}
isRunning = true
needsRun = false
const inputValue = input.value
var code, warnings, errors
try {
if (bundle.checked) {
const results = await esbuild.build({
stdin: {
contents: inputValue,
loader: loader.value.toLowerCase(),
},
bundle: true,
target: target.value.toLowerCase(),
format: format.value === 'Preserve' ? void 0 : format.value.toLowerCase(),
minifySyntax: minifySyntax.checked,
minifyIdentifiers: minifyIdents.checked,
minifyWhitespace: minifySpaces.checked,
charset: ascii.checked ? 'ascii' : 'utf8',
keepNames: keepNames.checked,
mangleProps: mangleProps.checked ? /_$/ : void 0,
plugins: [{
name: 'external-all',
setup(build) {
build.onResolve({ filter: /.*/ }, args => {
return { path: args.path, external: true }
})
},
}],
})
code = results.outputFiles[0].text
warnings = results.warnings
} else {
({ code, warnings } = await esbuild.transform(inputValue, {
target: target.value.toLowerCase(),
loader: loader.value.toLowerCase(),
format: format.value === 'Preserve' ? void 0 : format.value.toLowerCase(),
minifySyntax: minifySyntax.checked,
minifyIdentifiers: minifyIdents.checked,
minifyWhitespace: minifySpaces.checked,
charset: ascii.checked ? 'ascii' : 'utf8',
keepNames: keepNames.checked,
mangleProps: mangleProps.checked ? /_$/ : void 0,
}))
}
} catch (error) {
({ errors, warnings } = error)
if (!errors) {
output.textContent = (error && error.message) || (error + '')
isRunning = false
if (needsRun) runIfIdle()
return
}
}
if (warnings) warnings = await esbuild.formatMessages(warnings, { kind: 'warning', color: true })
if (errors) errors = await esbuild.formatMessages(errors, { kind: 'error', color: true })
const html = (errors || []).concat(warnings || []).map(messageToHTML).join('')
if (code) code = textToHTML(code)
if (code && minifySpaces.checked) code = `<span class="minified">${code}</span>`
output.innerHTML = (html && html + '<hr>' || '') + (code || '')
try {
sessionStorage.setItem('input', inputValue)
} catch (e) {
}
isRunning = false
if (needsRun) runIfIdle()
}
function textToHTML(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
function messageToHTML(text) {
let reset = ''
text = textToHTML(text)
text = text.replace(/\033\[([^m]+)m/g, (_, value) => {
if (value === '0') {
value = reset
reset = ''
} else {
reset += '</span>'
value = `<span class="terminal-${value}">`
}
return value
})
text += reset
return text
}
runIfIdle()
})
</script>
</body>
</html>
@@ -0,0 +1,464 @@
const { removeRecursiveSync } = require('./esbuild')
const child_process = require('child_process')
const path = require('path')
const fs = require('fs')
const tsconfigJson = {
compilerOptions: {
module: 'CommonJS',
strict: true,
},
}
const testsWithoutErrors = {
emptyBuildRequire: `
export {}
import esbuild = require('esbuild')
esbuild.buildSync({})
esbuild.build({})
`,
emptyBuildImport: `
import * as esbuild from 'esbuild'
esbuild.buildSync({})
esbuild.build({})
`,
emptyTransformRequire: `
export {}
import esbuild = require('esbuild')
esbuild.transformSync('')
esbuild.transformSync('', {})
esbuild.transform('')
esbuild.transform('', {})
`,
emptyTransformImport: `
import * as esbuild from 'esbuild'
esbuild.transformSync('')
esbuild.transformSync('', {})
esbuild.transform('')
esbuild.transform('', {})
`,
mangleCache: `
import * as esbuild from 'esbuild'
esbuild.buildSync({ mangleCache: {} }).mangleCache['x']
esbuild.build({ mangleCache: {} })
.then(result => result.mangleCache['x'])
esbuild.transformSync('', { mangleCache: {} }).mangleCache['x']
esbuild.transform('', { mangleCache: {} })
.then(result => result.mangleCache['x'])
`,
legalCommentsExternal: `
import * as esbuild from 'esbuild'
esbuild.transformSync('', { legalComments: 'external' }).legalComments.length
esbuild.transform('', { legalComments: 'external' })
.then(result => result.legalComments.length)
`,
writeFalseOutputFiles: `
import * as esbuild from 'esbuild'
esbuild.buildSync({ write: false }).outputFiles[0]
esbuild.build({ write: false })
.then(result => result.outputFiles[0])
`,
metafileTrue: `
import {build, buildSync, analyzeMetafile} from 'esbuild';
analyzeMetafile(buildSync({ metafile: true }).metafile)
build({ metafile: true })
.then(result => analyzeMetafile(result.metafile))
`,
contextMangleCache: `
import * as esbuild from 'esbuild'
esbuild.context({ mangleCache: {} })
.then(context => context.rebuild())
.then(result => result.mangleCache['x'])
`,
contextWriteFalseOutputFiles: `
import * as esbuild from 'esbuild'
esbuild.context({ write: false })
.then(context => context.rebuild())
.then(result => result.outputFiles[0])
`,
contextMetafileTrue: `
import {context, analyzeMetafile} from 'esbuild';
context({ metafile: true })
.then(context => context.rebuild())
.then(result => analyzeMetafile(result.metafile))
`,
allOptionsTransform: `
export {}
import {transform} from 'esbuild'
transform('', {
sourcemap: true,
format: 'iife',
globalName: '',
target: 'esnext',
minify: true,
minifyWhitespace: true,
minifyIdentifiers: true,
minifySyntax: true,
charset: 'utf8',
jsxFactory: '',
jsxFragment: '',
define: { 'x': 'y' },
pure: ['x'],
color: true,
logLevel: 'info',
logLimit: 0,
tsconfigRaw: {
compilerOptions: {
jsxFactory: '',
jsxFragmentFactory: '',
useDefineForClassFields: true,
importsNotUsedAsValues: 'preserve',
preserveValueImports: true,
},
},
sourcefile: '',
loader: 'ts',
}).then(result => {
let code: string = result.code;
let map: string = result.map;
for (let msg of result.warnings) {
let text: string = msg.text
if (msg.location !== null) {
let file: string = msg.location.file;
let namespace: string = msg.location.namespace;
let line: number = msg.location.line;
let column: number = msg.location.column;
let length: number = msg.location.length;
let lineText: string = msg.location.lineText;
}
}
})
`,
allOptionsBuild: `
export {}
import {build} from 'esbuild'
build({
sourcemap: true,
format: 'iife',
globalName: '',
target: 'esnext',
minify: true,
minifyWhitespace: true,
minifyIdentifiers: true,
minifySyntax: true,
charset: 'utf8',
jsxFactory: '',
jsxFragment: '',
define: { 'x': 'y' },
pure: ['x'],
color: true,
logLevel: 'info',
logLimit: 0,
bundle: true,
splitting: true,
outfile: '',
metafile: true,
outdir: '',
outbase: '',
platform: 'node',
external: ['x'],
loader: { 'x': 'ts' },
resolveExtensions: ['x'],
mainFields: ['x'],
write: true,
tsconfig: 'x',
outExtension: { 'x': 'y' },
publicPath: 'x',
inject: ['x'],
entryPoints: ['x'],
stdin: {
contents: '',
resolveDir: '',
sourcefile: '',
loader: 'ts',
},
plugins: [
{
name: 'x',
setup(build) {
build.onStart(() => {})
build.onStart(async () => {})
build.onStart(() => ({ warnings: [{text: '', location: {file: '', line: 0}}] }))
build.onStart(async () => ({ warnings: [{text: '', location: {file: '', line: 0}}] }))
build.onEnd(result => {})
build.onEnd(async result => {})
build.onLoad({filter: /./}, () => undefined)
build.onResolve({filter: /./, namespace: ''}, args => {
let path: string = args.path;
let importer: string = args.importer;
let namespace: string = args.namespace;
let resolveDir: string = args.resolveDir;
if (Math.random()) return
if (Math.random()) return {}
return {
pluginName: '',
errors: [
{},
{text: ''},
{text: '', location: {}},
{location: {file: '', line: 0}},
{location: {file: '', namespace: '', line: 0, column: 0, length: 0, lineText: ''}},
],
warnings: [
{},
{text: ''},
{text: '', location: {}},
{location: {file: '', line: 0}},
{location: {file: '', namespace: '', line: 0, column: 0, length: 0, lineText: ''}},
],
path: '',
external: true,
namespace: '',
suffix: '',
}
})
build.onLoad({filter: /./, namespace: ''}, args => {
let path: string = args.path;
let namespace: string = args.namespace;
let suffix: string = args.suffix;
if (Math.random()) return
if (Math.random()) return {}
return {
pluginName: '',
errors: [
{},
{text: ''},
{text: '', location: {}},
{location: {file: '', line: 0}},
{location: {file: '', namespace: '', line: 0, column: 0, length: 0, lineText: ''}},
],
warnings: [
{},
{text: ''},
{text: '', location: {}},
{location: {file: '', line: 0}},
{location: {file: '', namespace: '', line: 0, column: 0, length: 0, lineText: ''}},
],
contents: '',
resolveDir: '',
loader: 'ts',
}
})
},
}
],
}).then(result => {
if (result.outputFiles !== undefined) {
for (let file of result.outputFiles) {
let path: string = file.path
let bytes: Uint8Array = file.contents
}
}
for (let msg of result.warnings) {
let text: string = msg.text
if (msg.location !== null) {
let file: string = msg.location.file;
let namespace: string = msg.location.namespace;
let line: number = msg.location.line;
let column: number = msg.location.column;
let length: number = msg.location.length;
let lineText: string = msg.location.lineText;
}
}
})
`,
objectEntryPointBuild: `
export {}
import {build} from 'esbuild'
build({
entryPoints: { x: 'x', y: 'z' },
})
`,
mixedEntryPointBuild: `
export {}
import {build} from 'esbuild'
build({
entryPoints: ['x', { in: 'y', out: 'z' }],
})
`,
}
const testsWithErrors = {
badBuildRequire_invalidOption: `
export {}
import esbuild = require('esbuild')
esbuild.build({ invalidOption: true })
`,
badBuildSyncRequire_invalidOption: `
export {}
import esbuild = require('esbuild')
esbuild.buildSync({ invalidOption: true })
`,
badBuildImport_invalidOption: `
import * as esbuild from 'esbuild'
esbuild.build({ invalidOption: true })
`,
badBuildSyncImport_invalidOption: `
import * as esbuild from 'esbuild'
esbuild.build({ invalidOption: true })
`,
badTransformRequire_invalidOption: `
export {}
import esbuild = require('esbuild')
esbuild.transform('', { invalidOption: true })
`,
badTransformSyncRequire_invalidOption: `
export {}
import esbuild = require('esbuild')
esbuild.transformSync('', { invalidOption: true })
`,
badTransformImport_invalidOption: `
import * as esbuild from 'esbuild'
esbuild.transform('', { invalidOption: true })
`,
badTransformSyncImport_invalidOption: `
import * as esbuild from 'esbuild'
esbuild.transformSync('', { invalidOption: true })
`,
// mangleCache
mangleCacheBuild_undefined: `
import * as esbuild from 'esbuild'
esbuild.build({}).then(result => result.mangleCache['x'])
`,
mangleCacheBuildSync_undefined: `
import * as esbuild from 'esbuild'
esbuild.buildSync({}).mangleCache['x']
`,
mangleCacheTransform_undefined: `
import * as esbuild from 'esbuild'
esbuild.transform('', {}).then(result => result.mangleCache['x'])
`,
mangleCacheTransformSync_undefined: `
import * as esbuild from 'esbuild'
esbuild.transformSync('', {}).mangleCache['x']
`,
// legalComments
legalCommentsTransform_undefined: `
import * as esbuild from 'esbuild'
esbuild.transform('', { legalComments: 'eof' }).then(result => result.legalComments.length)
`,
legalCommentsTransformSync_undefined: `
import * as esbuild from 'esbuild'
esbuild.transformSync('', { legalComments: 'eof' }).legalComments.length
`,
// outputFiles
outputFilesBuild_undefined: `
import * as esbuild from 'esbuild'
esbuild.build({}).then(result => result.outputFiles[0])
`,
outputFilesBuildSync_undefined: `
import * as esbuild from 'esbuild'
esbuild.buildSync({}).outputFiles[0]
`,
outputFilesWriteTrueBuild_undefined: `
import * as esbuild from 'esbuild'
esbuild.build({ write: true }).then(result => result.outputFiles[0])
`,
outputFilesWriteTrueBuildSync_undefined: `
import * as esbuild from 'esbuild'
esbuild.buildSync({ write: true }).outputFiles[0]
`,
// metafile
metafileBuild_undefined: `
import * as esbuild from 'esbuild'
esbuild.build({}).then(result => esbuild.analyzeMetafile(result.metafile))
`,
metafileBuildSync_undefined: `
import * as esbuild from 'esbuild'
esbuild.analyzeMetafile(esbuild.buildSync({}).metafile)
`,
metafileFalseBuild_undefined: `
import * as esbuild from 'esbuild'
esbuild.build({ metafile: false }).then(result => esbuild.analyzeMetafile(result.metafile))
`,
metafileFalseBuildSync_undefined: `
import * as esbuild from 'esbuild'
esbuild.analyzeMetafile(esbuild.buildSync({ metafile: false }).metafile)
`,
}
async function main() {
let testDir = path.join(__dirname, '.ts-types-test')
removeRecursiveSync(testDir)
fs.mkdirSync(testDir, { recursive: true })
const types = fs.readFileSync(path.join(__dirname, '..', 'lib', 'shared', 'types.ts'), 'utf8')
const tsc = path.join(__dirname, 'node_modules', 'typescript', 'lib', 'tsc.js')
const esbuild_d_ts = path.join(testDir, 'node_modules', 'esbuild', 'index.d.ts')
fs.mkdirSync(path.dirname(esbuild_d_ts), { recursive: true })
fs.writeFileSync(esbuild_d_ts, types)
let allTestsPassed = true
// Check tests without errors
if (allTestsPassed) {
const dir = path.join(testDir, 'without-errors')
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(path.join(dir, 'tsconfig.json'), JSON.stringify(tsconfigJson))
for (const name in testsWithoutErrors) {
const input = path.join(dir, name + '.ts')
fs.writeFileSync(input, testsWithoutErrors[name])
}
allTestsPassed &&= await new Promise(resolve => {
const child = child_process.spawn('node', [tsc, '--project', '.'], { cwd: dir, stdio: 'inherit' })
child.on('close', exitCode => resolve(exitCode === 0))
})
}
// Check tests with errors
if (allTestsPassed) {
const dir = path.join(testDir, 'with-errors')
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(path.join(dir, 'tsconfig.json'), JSON.stringify(tsconfigJson))
for (const name in testsWithErrors) {
const input = path.join(dir, name.split('_')[0] + '.ts')
fs.writeFileSync(input, testsWithErrors[name])
}
try {
child_process.execFileSync('node', [tsc, '--project', '.'], { cwd: dir })
throw new Error('Expected an error to be generated')
} catch (err) {
const stdout = err.stdout.toString()
const lines = stdout.split('\n')
next: for (const name in testsWithErrors) {
const fileName = name.split('_')[0]
const expectedText = name.split('_')[1]
for (const line of lines) {
if (line.includes(fileName)) {
if (line.includes(expectedText)) {
console.log(`\x1B[32mSUCCESS:\x1B[0m ${line}`)
} else {
console.log(`\x1B[31mFAILURE: ${line}\x1B[0m`)
allTestsPassed = false
}
continue next
}
}
console.log(`\x1B[31mFAILURE:\x1B[0m ${name}: Could not find expected error in output from "tsc":`)
process.stdout.write(stdout)
allTestsPassed = false
break next
}
}
}
if (!allTestsPassed) {
console.error(`❌ typescript type tests failed`)
process.exit(1)
} else {
console.log(`✅ typescript type tests passed`)
removeRecursiveSync(testDir)
}
}
main().catch(e => {
setTimeout(() => {
throw e
})
})
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"target": "ESNext", // Fix "Symbol.metadata" errors in decorator tests
}
}
@@ -0,0 +1,485 @@
const { installForTests } = require('./esbuild');
const childProcess = require('child_process');
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const repoDir = path.dirname(__dirname);
const testDir = path.join(repoDir, 'scripts', '.uglify-tests');
const uglifyDir = path.join(repoDir, 'demo', 'uglify');
const SKIP = {};
let U;
main().catch(e => setTimeout(() => { throw e }));
async function main() {
// // Terser's stdout comparisons fail if this is true since stdout contains
// // terminal color escape codes
// process.stdout.isTTY = false;
// Make sure the tests are installed
childProcess.execSync('make demo/uglify', { cwd: repoDir, stdio: 'pipe' });
U = require(path.join(uglifyDir, 'test', 'node'));
// Create a fresh test directory
childProcess.execSync(`rm -fr "${testDir}"`);
fs.mkdirSync(testDir)
// Start the esbuild service
const esbuild = installForTests();
// Find test files
const compressDir = path.join(uglifyDir, 'test', 'compress');
const files = fs.readdirSync(compressDir).filter(name => name.endsWith('.js'));
// Run all tests concurrently
let passedTotal = 0;
let failedTotal = 0;
let skippedTotal = 0;
const runTest = file => test_file(esbuild, path.join(compressDir, file))
.then(({ passed, failed, skipped }) => {
passedTotal += passed;
failedTotal += failed;
skippedTotal += skipped;
});
await Promise.all(files.map(runTest));
// Clean up test output
childProcess.execSync(`rm -fr "${testDir}"`);
console.log(`${failedTotal} failed out of ${passedTotal + failedTotal}, with ${skippedTotal} skipped`);
if (failedTotal) {
process.exit(1);
}
}
async function test_file(esbuild, file) {
let passed = 0;
let failed = 0;
let skipped = 0;
const tests = parse_test(file);
const runTest = name => test_case(esbuild, tests[name], path.basename(file))
.then(x => {
if (x === SKIP) {
skipped++;
} else {
passed++;
}
})
.catch(e => {
failed++;
console.error(`❌ ${file}: ${name}: ${(e && e.message || e).trim()}\n`);
pass = false;
});
await Promise.all(Object.keys(tests).map(runTest));
return { passed, failed, skipped };
}
// Modified from "uglify/demo/test/compress.js"
async function test_case(esbuild, test, basename) {
const sandbox = require(path.join(uglifyDir, 'test', 'sandbox'));
const log = (format, args) => { throw new Error(tmpl(format, args)); };
var semver = require(path.join(uglifyDir, 'node_modules', 'semver'));
// Generate the input code
var input = to_toplevel(test.input, test.mangle);
var input_code = make_code(input);
var input_formatted = make_code(test.input, {
beautify: true,
comments: "all",
keep_quoted_props: true,
quote_style: 3,
});
// Make sure it's valid
try {
U.parse(input_code);
} catch (ex) {
log([
"!!! Cannot parse input",
"---INPUT---",
"{input}",
"--PARSE ERROR--",
"{error}",
"",
"",
].join("\n"), {
input: input_formatted,
error: ex,
});
}
// Ignore tests that no longer pass in modern versions of node. These tests
// contain code that is now considered a syntax error. The relevant code is
// this:
//
// try{throw 42}catch(a){console.log(a);function a(){}}
//
if (test.node_version && !semver.satisfies(process.version, test.node_version)) {
console.error("*** skipping test %j with node_version %j", test.name, test.node_version);
return SKIP;
}
// Run esbuild as a minifier
try {
var { code: output } = await esbuild.transform(input_code, {
minify: true,
target: 'esnext',
});
} catch (e) {
// These tests fail because they contain syntax errors. These test failures
// do not indicate anything wrong with esbuild so the failures are ignored.
// Here is one of the tests:
//
// try{}catch(a){const a="aa"}
//
if ([
'const.js: issue_4290_1',
'const.js: issue_4305_2',
'const.js: retain_catch',
'const.js: skip_braces',
'exports.js: defaults',
'exports.js: drop_unused',
'exports.js: hoist_exports_1',
'exports.js: hoist_exports_2',
'exports.js: keep_return_values',
'exports.js: mangle_rename',
'exports.js: mangle',
'exports.js: refs',
'imports.js: issue_4708_1',
'imports.js: issue_4708_2',
'let.js: issue_4290_1',
'let.js: issue_4305_2',
'let.js: retain_catch',
'let.js: skip_braces',
'reduce_vars.js: defun_catch_4',
'reduce_vars.js: defun_catch_5',
'templates.js: malformed_evaluate_1',
'templates.js: malformed_evaluate_2',
'templates.js: malformed_evaluate_3',
'varify.js: issue_4290_1_const',
'varify.js: issue_4290_1_let',
].indexOf(`${basename}: ${test.name}`) >= 0) {
console.error(`*** skipping test with known syntax error: ${basename}: ${test.name}`);
return SKIP;
}
// These tests fail because esbuild supports top-level await. Technically
// top-level await is only allowed inside a module, and can be used as a
// normal identifier in a script. But the script/module distinction causes
// a lot of pain due to the need to configure every single tool to say
// whether to parse the code as a script or a module, so esbuild mostly
// does away with the distinction and enables top-level await everywhere.
// This means it fails these tests but the failures are unlikely to matter
// in real-world code, so they can be ignored. Here's one test case:
//
// async function await(){console.log("PASS")}await();
//
if ([
'awaits.js: defun_name',
'awaits.js: drop_fname',
'awaits.js: functions_anonymous',
'awaits.js: functions_inner_var',
'awaits.js: issue_4335_1',
'awaits.js: keep_fname',
'classes.js: await',
].indexOf(`${basename}: ${test.name}`) >= 0) {
console.error(`*** skipping test with top-level await as identifier: ${basename}: ${test.name}`);
return SKIP;
}
// These tests fail because esbuild makes assigning to an inlined constant
// a compile error to avoid code with incorrect behavior. This is a limitation
// due to esbuild's three-pass design but it shouldn't matter in practice. It
// just means esbuild rejects bad code at compile time instead of at run time.
if ([
'const.js: issue_4212_1',
'const.js: issue_4212_2',
].indexOf(`${basename}: ${test.name}`) >= 0) {
console.error(`*** skipping test with assignment to an inlined constant: ${basename}: ${test.name}`);
return SKIP;
}
log("!!! esbuild failed\n---INPUT---\n{input}\n---ERROR---\n{error}\n", {
input: input_code,
error: e && e.message || e,
});
}
// Make sure esbuild generates valid JavaScript
try {
U.parse(output);
} catch (ex) {
log([
"!!! Test matched expected result but cannot parse output",
"---INPUT---",
"{input}",
"---OUTPUT---",
"{output}",
"--REPARSE ERROR--",
"{error}",
"",
"",
].join("\n"), {
input: input_formatted,
output: output,
error: ex && ex.stack || ex,
});
}
// Verify that the stdout matches our expectations
if (test.expect_stdout && (!test.node_version || semver.satisfies(process.version, test.node_version))) {
var stdout = [run_code(input_code), run_code(input_code, true)];
var toplevel = sandbox.has_toplevel({
compress: test.options,
mangle: test.mangle
});
var actual = stdout[toplevel ? 1 : 0];
if (test.expect_stdout === true) {
test.expect_stdout = actual;
}
actual = run_code(output, toplevel);
// Ignore the known failures in CI, but not otherwise
const isExpectingFailure = !process.env.CI ? false : [
// Stdout difference
'const.js: issue_4225',
'const.js: issue_4229',
'const.js: issue_4245',
'const.js: use_before_init_3',
'default-values.js: retain_empty_iife',
'destructured.js: funarg_side_effects_2',
'destructured.js: funarg_side_effects_3',
'let.js: issue_4225',
'let.js: issue_4229',
'let.js: issue_4245',
'let.js: use_before_init_3',
// Error difference
'dead-code.js: dead_code_2_should_warn',
// These tests fail because esbuild assumes that declaring a class that
// extends a base class has no side effects. That's not true because
// extending a non-constructible object throws an error. But refusing to
// tree-shake every class with a base class is not a useful thing for a
// bundler to do. So we pretend that this edge case doesn't exist.
'classes.js: issue_4722_1',
'classes.js: issue_4722_2',
'classes.js: issue_4722_3',
].indexOf(`${basename}: ${test.name}`) >= 0
if (!sandbox.same_stdout(test.expect_stdout, actual)) {
if (isExpectingFailure) {
console.error(`*** skipping test with known esbuild failure: ${basename}: ${test.name}`);
return SKIP;
}
log([
"!!! failed",
"---INPUT---",
"{input}",
"---EXPECTED {expected_type}---",
"{expected}",
"---ACTUAL {actual_type}---",
"{actual}",
"",
"",
].join("\n"), {
input: input_formatted,
expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR",
expected: test.expect_stdout,
actual_type: typeof actual == "string" ? "STDOUT" : "ERROR",
actual: actual,
});
} else if (isExpectingFailure) {
throw new Error(`UPDATE NEEDED: expected failure for ${basename}: ${test.name}, please remove this test from known failure list`);
}
}
}
////////////////////////////////////////////////////////////////////////////////
// The code below was copied verbatim from "uglify/demo/test/compress.js"
//
// UglifyJS is released under the BSD license:
//
// Copyright 2012-2019 (c) Mihai Bazon <mihai.bazon@gmail.com>
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above
// copyright notice, this list of conditions and the following
// disclaimer.
//
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials
// provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
// OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
// TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
function evaluate(code) {
if (code instanceof U.AST_Node) code = make_code(code, { beautify: true });
return new Function("return(" + code + ")")();
}
function make_code(ast, options) {
var stream = U.OutputStream(options);
ast.print(stream);
return stream.get();
}
function parse_test(file) {
var script = fs.readFileSync(file, "utf8");
// TODO try/catch can be removed after fixing https://github.com/mishoo/UglifyJS/issues/348
try {
var ast = U.parse(script, {
filename: file
});
} catch (e) {
console.error("Caught error while parsing tests in " + file);
console.error(e);
process.exit(1);
}
var tests = Object.create(null);
var tw = new U.TreeWalker(function (node, descend) {
if (node instanceof U.AST_LabeledStatement
&& tw.parent() instanceof U.AST_Toplevel) {
var name = node.label.name;
if (name in tests) {
throw new Error('Duplicated test name "' + name + '" in ' + file);
}
tests[name] = get_one_test(name, node.body);
return true;
}
if (!(node instanceof U.AST_Toplevel)) croak(node);
});
ast.walk(tw);
return tests;
function croak(node) {
throw new Error(tmpl("Can't understand test file {file} [{line},{col}]\n{code}", {
file: file,
line: node.start.line,
col: node.start.col,
code: make_code(node, { beautify: false })
}));
}
function read_string(stat) {
if (stat.TYPE == "SimpleStatement") {
var body = stat.body;
switch (body.TYPE) {
case "String":
return body.value;
case "Array":
return body.elements.map(function (element) {
if (element.TYPE !== "String")
throw new Error("Should be array of strings");
return element.value;
}).join("\n");
}
}
throw new Error("Should be string or array of strings");
}
function get_one_test(name, block) {
var test = { name: name, options: {} };
var tw = new U.TreeWalker(function (node, descend) {
if (node instanceof U.AST_Assign) {
if (!(node.left instanceof U.AST_SymbolRef)) {
croak(node);
}
var name = node.left.name;
test[name] = evaluate(node.right);
return true;
}
if (node instanceof U.AST_LabeledStatement) {
var label = node.label;
assert.ok([
"input",
"expect",
"expect_exact",
"expect_warnings",
"expect_stdout",
"node_version",
].indexOf(label.name) >= 0, tmpl("Unsupported label {name} [{line},{col}]", {
name: label.name,
line: label.start.line,
col: label.start.col
}));
var stat = node.body;
if (label.name == "expect_exact" || label.name == "node_version") {
test[label.name] = read_string(stat);
} else if (label.name == "expect_stdout") {
var body = stat.body;
if (body instanceof U.AST_Boolean) {
test[label.name] = body.value;
} else if (body instanceof U.AST_Call) {
var ctor = global[body.expression.name];
assert.ok(ctor === Error || ctor.prototype instanceof Error, tmpl("Unsupported expect_stdout format [{line},{col}]", {
line: label.start.line,
col: label.start.col
}));
test[label.name] = ctor.apply(null, body.args.map(function (node) {
assert.ok(node instanceof U.AST_Constant, tmpl("Unsupported expect_stdout format [{line},{col}]", {
line: label.start.line,
col: label.start.col
}));
return node.value;
}));
} else {
test[label.name] = read_string(stat) + "\n";
}
} else {
test[label.name] = stat;
}
return true;
}
});
block.walk(tw);
return test;
}
}
function run_code(code, toplevel) {
const sandbox = require(path.join(uglifyDir, 'test', 'sandbox'));
var result = sandbox.run_code(code, toplevel);
return typeof result == "string" ? result.replace(/\u001b\[\d+m/g, "") : result;
}
function tmpl() {
return U.string_template.apply(null, arguments);
}
function to_toplevel(input, mangle_options) {
if (!(input instanceof U.AST_BlockStatement)) throw new Error("Unsupported input syntax");
var directive = true;
var offset = input.start.line;
var tokens = [];
var toplevel = new U.AST_Toplevel(input.transform(new U.TreeTransformer(function (node) {
if (U.push_uniq(tokens, node.start)) node.start.line -= offset;
if (!directive || node === input) return;
if (node instanceof U.AST_SimpleStatement && node.body instanceof U.AST_String) {
return new U.AST_Directive(node.body);
} else {
directive = false;
}
})));
toplevel.figure_out_scope(mangle_options);
return toplevel;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,390 @@
const { removeRecursiveSync, buildWasmLib } = require('./esbuild.js');
const child_process = require('child_process');
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const tests = {
serveTest({ testDir, esbuildPathWASM }) {
try {
child_process.execFileSync('node', [
esbuildPathWASM,
'--servedir=.',
'--log-level=warning',
], {
stdio: 'pipe',
cwd: testDir,
});
throw new Error('Expected an error to be thrown');
} catch (err) {
assert.strictEqual(err.stderr + '', `✘ [ERROR] The "serve" API is not supported when using WebAssembly\n\n`)
}
},
basicStdinTest({ testDir, esbuildPathWASM }) {
const stdout = child_process.execFileSync('node', [
esbuildPathWASM,
'--format=cjs',
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
input: `export default 1+2`,
}).toString();
// Check that the bundle is valid
const module = { exports: {} };
new Function('module', 'exports', stdout)(module, module.exports);
assert.deepStrictEqual(module.exports.default, 3);
},
stdinOutfileTest({ testDir, esbuildPathWASM }) {
const outfile = path.join(testDir, 'out.js')
child_process.execFileSync('node', [
esbuildPathWASM,
'--bundle',
'--format=cjs',
'--outfile=' + outfile,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
input: `export default 1+2`,
}).toString();
// Check that the bundle is valid
const exports = require(outfile);
assert.deepStrictEqual(exports.default, 3);
},
stdinStdoutUnicodeTest({ testDir, esbuildPathWASM }) {
const stdout = child_process.execFileSync('node', [
esbuildPathWASM,
'--format=cjs',
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
input: `export default ['π', '🍕']`,
}).toString();
// Check that the bundle is valid
const module = { exports: {} };
new Function('module', 'exports', stdout)(module, module.exports);
assert.deepStrictEqual(module.exports.default, ['π', '🍕']);
},
stdinOutfileUnicodeTest({ testDir, esbuildPathWASM }) {
const outfile = path.join(testDir, 'out.js')
child_process.execFileSync('node', [
esbuildPathWASM,
'--bundle',
'--format=cjs',
'--outfile=' + outfile,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
input: `export default ['π', '🍕']`,
}).toString();
// Check that the bundle is valid
const exports = require(outfile);
assert.deepStrictEqual(exports.default, ['π', '🍕']);
},
stdoutLargeTest({ testDir, esbuildPathNative, esbuildPathWASM }) {
const entryPoint = path.join(__dirname, 'js-api-tests.js');
// Build with native
const stdoutNative = child_process.execFileSync(esbuildPathNative, [
entryPoint,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
}).toString();
// Build with WASM
const stdoutWASM = child_process.execFileSync('node', [
esbuildPathWASM,
entryPoint,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
}).toString();
// Check that the output is equal
assert.deepStrictEqual(stdoutNative.length, stdoutWASM.length);
assert.deepStrictEqual(stdoutNative, stdoutWASM);
},
outfileLargeTest({ testDir, esbuildPathNative, esbuildPathWASM }) {
const entryPoint = path.join(__dirname, 'js-api-tests.js');
// Build with native
const outfileNative = path.join(testDir, 'a.js');
const stdoutNative = child_process.execFileSync(esbuildPathNative, [
entryPoint,
'--outfile=' + outfileNative,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
}).toString();
const jsNative = fs.readFileSync(outfileNative, 'utf8');
// Build with WASM
const outfileWASM = path.join(testDir, 'b.js');
const stdoutWASM = child_process.execFileSync('node', [
esbuildPathWASM,
entryPoint,
'--outfile=' + outfileWASM,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
}).toString();
const jsWASM = fs.readFileSync(outfileWASM, 'utf8');
// Check that the output is equal
assert.deepStrictEqual(jsNative.length, jsWASM.length);
assert.deepStrictEqual(jsNative, jsWASM);
},
outfileNestedTest({ testDir, esbuildPathWASM }) {
const outfile = path.join(testDir, 'a', 'b', 'c', 'd', 'out.js');
child_process.execFileSync('node', [
esbuildPathWASM,
'--bundle',
'--format=cjs',
'--outfile=' + outfile,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
input: `export default 123`,
}).toString();
// Check that the bundle is valid
const exports = require(outfile);
assert.deepStrictEqual(exports.default, 123);
},
metafileNestedTest({ testDir, esbuildPathWASM }) {
const outfile = path.join(testDir, 'out.js');
const metafile = path.join(testDir, 'a', 'b', 'c', 'd', 'meta.json');
const cwd = path.join(testDir, 'a', 'b')
fs.mkdirSync(cwd, { recursive: true })
child_process.execFileSync('node', [
esbuildPathWASM,
'--bundle',
'--format=cjs',
'--outfile=' + outfile,
'--metafile=' + metafile,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd,
input: `export default 123`,
}).toString();
// Check that the bundle is valid
const exports = require(outfile);
assert.deepStrictEqual(exports.default, 123);
const json = JSON.parse(fs.readFileSync(metafile, 'utf8'));
assert.deepStrictEqual(json.outputs['../../out.js'].entryPoint, '<stdin>');
},
importRelativeFileTest({ testDir, esbuildPathWASM }) {
const outfile = path.join(testDir, 'out.js')
const packageJSON = path.join(__dirname, '..', 'npm', 'esbuild-wasm', 'package.json');
child_process.execFileSync('node', [
esbuildPathWASM,
'--bundle',
'--format=cjs',
'--outfile=' + outfile,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
input: `export {default} from ` + JSON.stringify('./' + path.relative(testDir, packageJSON)),
}).toString();
// Check that the bundle is valid
const exports = require(outfile);
assert.deepStrictEqual(exports.default, require(packageJSON));
},
importAbsoluteFileTest({ testDir, esbuildPathWASM }) {
const outfile = path.join(testDir, 'out.js')
const packageJSON = path.join(__dirname, '..', 'npm', 'esbuild-wasm', 'package.json');
child_process.execFileSync('node', [
esbuildPathWASM,
'--bundle',
'--format=cjs',
'--outfile=' + outfile,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
input: `export {default} from ` + JSON.stringify(packageJSON),
}).toString();
// Check that the bundle is valid
const exports = require(outfile);
assert.deepStrictEqual(exports.default, require(packageJSON));
},
zipFile({ testDir, esbuildPathWASM }) {
const entry = path.join(testDir, 'entry.js')
fs.writeFileSync(entry, `
import foo from './test.zip/foo.js'
import bar from './test.zip/bar/bar.js'
import __virtual__1 from './test.zip/__virtual__/ignored/0/foo.js'
import __virtual__2 from './test.zip/ignored/__virtual__/ignored/1/foo.js'
import __virtual__3 from './test.zip/__virtual__/ignored/1/test.zip/foo.js'
import $$virtual1 from './test.zip/$$virtual/ignored/0/foo.js'
import $$virtual2 from './test.zip/ignored/$$virtual/ignored/1/foo.js'
import $$virtual3 from './test.zip/$$virtual/ignored/1/test.zip/foo.js'
console.log({
foo,
bar,
__virtual__1,
__virtual__2,
__virtual__3,
$$virtual1,
$$virtual2,
$$virtual3,
})
`)
// This uses the real file system instead of the mock file system so that
// we can check that everything works as expected on Windows, which is not
// a POSIX environment.
fs.writeFileSync(path.join(testDir, 'test.zip'), Buffer.from(
`UEsDBAoAAgAAAG1qCFUSAXosFQAAABUAAAAGABwAZm9vLmpzVVQJAAOeRfFioEXxYnV4C` +
`wABBPUBAAAEFAAAAGV4cG9ydCBkZWZhdWx0ICdmb28nClBLAwQKAAIAAABzaghVwuDbLR` +
`UAAAAVAAAACgAcAGJhci9iYXIuanNVVAkAA6lF8WKrRfFidXgLAAEE9QEAAAQUAAAAZXh` +
`wb3J0IGRlZmF1bHQgJ2JhcicKUEsBAh4DCgACAAAAbWoIVRIBeiwVAAAAFQAAAAYAGAAA` +
`AAAAAQAAAKSBAAAAAGZvby5qc1VUBQADnkXxYnV4CwABBPUBAAAEFAAAAFBLAQIeAwoAA` +
`gAAAHNqCFXC4NstFQAAABUAAAAKABgAAAAAAAEAAACkgVUAAABiYXIvYmFyLmpzVVQFAA` +
`OpRfFidXgLAAEE9QEAAAQUAAAAUEsFBgAAAAACAAIAnAAAAK4AAAAAAA==`, 'base64'))
const stdout = child_process.execFileSync('node', [
esbuildPathWASM,
'--bundle',
entry,
], {
stdio: 'pipe',
cwd: testDir,
}).toString();
assert.strictEqual(stdout, `(() => {
// test.zip/foo.js
var foo_default = "foo";
// test.zip/bar/bar.js
var bar_default = "bar";
// test.zip/__virtual__/ignored/0/foo.js
var foo_default2 = "foo";
// test.zip/ignored/__virtual__/ignored/1/foo.js
var foo_default3 = "foo";
// test.zip/__virtual__/ignored/1/test.zip/foo.js
var foo_default4 = "foo";
// test.zip/$$virtual/ignored/0/foo.js
var foo_default5 = "foo";
// test.zip/ignored/$$virtual/ignored/1/foo.js
var foo_default6 = "foo";
// test.zip/$$virtual/ignored/1/test.zip/foo.js
var foo_default7 = "foo";
// entry.js
console.log({
foo: foo_default,
bar: bar_default,
__virtual__1: foo_default2,
__virtual__2: foo_default3,
__virtual__3: foo_default4,
$$virtual1: foo_default5,
$$virtual2: foo_default6,
$$virtual3: foo_default7
});
})();
`)
},
// https://github.com/evanw/esbuild/issues/3001
nodePathsReaddirEINVAL({ testDir, esbuildPathWASM }) {
const libDir = path.join(testDir, 'lib');
const libFile = path.join(libDir, 'file.js');
fs.mkdirSync(libDir, { recursive: true });
fs.writeFileSync(libFile, 'foo()');
const stdout = child_process.execFileSync('node', [
esbuildPathWASM,
'--bundle',
'--format=esm',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
input: `import "file.js"`,
env: { ...process.env, NODE_PATH: libDir },
}).toString();
assert.deepStrictEqual(stdout, '// lib/file.js\nfoo();\n');
},
};
function runTest({ testDir, esbuildPathNative, esbuildPathWASM, test }) {
try {
fs.mkdirSync(testDir, { recursive: true })
test({ testDir, esbuildPathNative, esbuildPathWASM })
return true
} catch (e) {
console.error(`❌ ${test.name} failed: ${e && e.message || e}`)
return false
}
}
async function main() {
// Generate the WebAssembly module
const esbuildPathNative = path.join(__dirname, '..', process.platform === 'win32' ? 'esbuild.exe' : 'esbuild');
await buildWasmLib(esbuildPathNative);
const esbuildPathWASM = path.join(__dirname, '..', 'npm', 'esbuild-wasm', 'bin', 'esbuild');
const testDir = path.join(__dirname, '.wasm-tests')
// Run all tests in serial because WebAssembly compilation is a CPU hog
let allTestsPassed = true;
for (const test in tests) {
if (!runTest({
testDir: path.join(testDir, test),
test: tests[test],
esbuildPathNative,
esbuildPathWASM,
})) {
allTestsPassed = false;
}
}
if (!allTestsPassed) {
console.error(`❌ wasm-tests failed`)
process.exit(1)
} else {
console.log(`✅ wasm-tests passed`)
removeRecursiveSync(testDir)
}
}
main().catch(e => setTimeout(() => { throw e }))