merge upstream 0.28.2+ (34 commits)

Disjoint lineage: our fork point has the same tree as upstream 6ff1d8b0d but a
different SHA, so a plain merge would diff against the root. Merged with
upstream as a second parent and the fork-point..upstream/main patch applied, so
the tree is exactly upstream/main plus our two files.

Includes the symlink 403 for keyfile/certfile and the input-file overwrite fix.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-17 14:12:57 -07:00
co-authored by Hanzo Dev
88 changed files with 2149 additions and 1838 deletions
+18 -114
View File
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
permissions:
contents: read # to fetch code (actions/checkout)
contents: read # to fetch code (actions/checkout)
jobs:
esbuild-platforms:
@@ -18,20 +18,20 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # actions/checkout@v3
- name: Read go.version
run: |
echo "GO_VERSION=$(cat go.version)" >> $GITHUB_ENV
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v3
uses: actions/setup-go@be3c94b385c4f180051c996d336f57a34c397495 # actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
id: go
- name: Setup Node.js environment
uses: actions/setup-node@v3
- name: Set up Node 18
uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # actions/setup-node@v3
with:
node-version: 18
@@ -46,20 +46,20 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # actions/checkout@v3
- name: Read go.version
run: |
echo "GO_VERSION=$(cat go.version)" >> $GITHUB_ENV
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v3
uses: actions/setup-go@be3c94b385c4f180051c996d336f57a34c397495 # actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
id: go
- name: Setup Node.js environment
uses: actions/setup-node@v3
- name: Set up Node 16
uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # actions/setup-node@v3
with:
node-version: 16
@@ -92,7 +92,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # actions/checkout@v3
- name: Read go.version (non-Windows)
if: matrix.os != 'windows-latest'
@@ -105,35 +105,24 @@ jobs:
echo "GO_VERSION=$(cat go.version)" >> $Env:GITHUB_ENV
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v3
uses: actions/setup-go@be3c94b385c4f180051c996d336f57a34c397495 # actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
id: go
- name: Setup Node.js environment
uses: actions/setup-node@v3
- name: Set up Node 18
uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # actions/setup-node@v3
with:
node-version: 18
- name: Setup Deno 1.40.0
uses: denoland/setup-deno@main
with:
deno-version: v1.40.0
- name: go test
run: go test -race ./internal/...
env:
WT_SESSION: 1 # Ensure that FormatMessages output on Windows matches macOS
run: go test -race ./cmd/... ./internal/... ./pkg/...
- name: go vet
run: go vet ./cmd/... ./internal/... ./pkg/...
- name: Deno Tests (non-Windows)
if: matrix.os != 'windows-latest'
run: make test-deno
- name: Deno Tests (Windows)
if: matrix.os == 'windows-latest'
run: make test-deno-windows
- name: Test for path/filepath
if: matrix.os == 'ubuntu-latest'
run: make no-filepath
@@ -230,10 +219,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # actions/checkout@v3
- name: Set up Go 1.13 (the minimum required Go version for esbuild)
uses: actions/setup-go@v3
uses: actions/setup-go@be3c94b385c4f180051c996d336f57a34c397495 # actions/setup-go@v3
with:
go-version: 1.13
id: go
@@ -243,88 +232,3 @@ jobs:
- name: go test
run: go test ./internal/...
esbuild-old-deno-version:
name: esbuild CI (old Deno version)
runs-on: ${{ matrix.os }}
strategy:
matrix:
# Note: I'm excluding "macos-latest" here because GitHub recently
# changed their macOS CI VMs from x86_64 to aarch64 (i.e. from Intel
# to ARM) and it looks like old Deno versions have WASM bugs on ARM.
# Specifically, this test now crashes like this when run on macOS:
#
# #
# # Fatal error in , line 0
# # Check failed: RwxMemoryWriteScope::IsAllowed().
# #
# #
# #
# #FailureMessage Object: 0x16f282368
# ==== C stack trace ===============================
#
# 0 deno 0x0000000101bb8e78 v8::base::debug::StackTrace::StackTrace() + 24
# 1 deno 0x0000000101bbda84 v8::platform::(anonymous namespace)::PrintStackTrace() + 24
# 2 deno 0x0000000101bb6230 V8_Fatal(char const*, ...) + 268
# 3 deno 0x000000010227e468 v8::internal::wasm::WasmCodeManager::MemoryProtectionKeysEnabled() const + 0
# 4 deno 0x0000000102299994 v8::internal::wasm::WasmEngine::InitializeOncePerProcess() + 44
# 5 deno 0x0000000101e78fd0 v8::internal::V8::Initialize() + 1576
# 6 deno 0x0000000101c3b7d8 v8::V8::Initialize(int) + 32
# 7 deno 0x00000001011833dc _ZN3std4sync4once4Once9call_once28_$u7b$$u7b$closure$u7d$$u7d$17h2bbe74d315ab3e84E + 488
# 8 deno 0x00000001017f8854 std::sync::once::Once::call_inner::h70fbdd48fe002a01 + 724
# 9 deno 0x000000010115ca80 deno_core::runtime::JsRuntime::new::h9c5f1a9c910f1eed + 192
# 10 deno 0x00000001014d3b50 deno_runtime::worker::MainWorker::bootstrap_from_options::h91a0eaac48dfc18e + 4260
# 11 deno 0x0000000100ee692c deno::create_main_worker::h0d1622755821ae7f + 1608
# 12 deno 0x0000000100f6c688 _ZN97_$LT$core..future..from_generator..GenFuture$LT$T$GT$$u20$as$u20$core..future..future..Future$GT$4poll17h87ddfac9566887c8E + 492
# 13 deno 0x0000000100f6ba18 tokio::runtime::task::raw::poll::h7d51f1a7d5a61c15 + 1396
# 14 deno 0x0000000101917b98 std::sys_common::backtrace::__rust_begin_short_backtrace::hd384935dcffe6f2d + 332
# 15 deno 0x0000000101917954 _ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17he2755732d5d29cf0E + 124
# 16 deno 0x0000000101829684 std::sys::unix::thread::Thread::new::thread_start::h432bc30153e41f60 + 48
# 17 libsystem_pthread.dylib 0x000000018a436f94 _pthread_start + 136
# 18 libsystem_pthread.dylib 0x000000018a431d34 thread_start + 8
#
# Hopefully running this old Deno version on Linux is sufficiently
# close to running it on macOS. For reference, I believe this is the
# change that GitHub made which broke this test:
# https://github.blog/changelog/2023-10-02-github-actions-apple-silicon-m1-macos-runners-are-now-available-in-public-beta/
os: [ubuntu-latest, windows-latest]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Read go.version (non-Windows)
if: matrix.os != 'windows-latest'
run: |
echo "GO_VERSION=$(cat go.version)" >> $GITHUB_ENV
- name: Read go.version (Windows)
if: matrix.os == 'windows-latest'
run: |
echo "GO_VERSION=$(cat go.version)" >> $Env:GITHUB_ENV
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
id: go
# Make sure esbuild works with old versions of Deno. Note: It's important
# to test a version before 1.31.0, which introduced the "Deno.Command" API.
- name: Setup Deno 1.24.0
uses: denoland/setup-deno@main
with:
deno-version: v1.24.0
- name: Setup Node.js environment
uses: actions/setup-node@v3
with:
node-version: 18
- name: Deno Tests (non-Windows)
if: matrix.os != 'windows-latest'
run: make test-deno
- name: Deno Tests (Windows)
if: matrix.os == 'windows-latest'
run: make test-deno-windows
+4 -15
View File
@@ -6,27 +6,20 @@ on:
workflow_dispatch:
permissions:
contents: read # to fetch code (actions/checkout)
contents: read # to fetch code (actions/checkout)
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # actions/checkout@v3
- name: Setup Node.js environment
uses: actions/setup-node@v3
- name: Set up Node 18
uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # actions/setup-node@v3
with:
node-version: 18
# The version of Deno is pinned because version 1.25.1 was causing test
# flakes due to random segfaults.
- name: Setup Deno 1.24.0
uses: denoland/setup-deno@main
with:
deno-version: v1.24.0
- name: Test npm
run: |
npm i -g npm@next-7
@@ -46,7 +39,3 @@ jobs:
run: |
npm i -g yarn@latest
time make test-e2e-yarn-berry
- name: Test deno
run: |
time make test-e2e-deno
+6 -6
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # actions/checkout@v3
- name: Read version info
run: |
@@ -42,12 +42,12 @@ jobs:
test -n "$CHANGELOG"
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v3
uses: actions/setup-go@be3c94b385c4f180051c996d336f57a34c397495 # actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Setup Node.js environment
uses: actions/setup-node@v3
- name: Set up Node 24
uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # actions/setup-node@v3
with:
node-version: 24
@@ -63,7 +63,7 @@ jobs:
test -z "$(git status --porcelain)"
# Trusted publishing requires this specific version of npm
- name: Install npm
- name: Install npm 11.5.1
run: |
npm install -g [email protected]
@@ -77,7 +77,7 @@ jobs:
# Only do this after publishing was successful
- name: Create a GitHub Release
uses: actions/create-release@v1
uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+3 -3
View File
@@ -6,21 +6,21 @@ on:
workflow_dispatch:
permissions:
contents: read # to fetch code (actions/checkout)
contents: read # to fetch code (actions/checkout)
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # actions/checkout@v3
- name: Read go.version
run: |
echo "GO_VERSION=$(cat go.version)" >> $GITHUB_ENV
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v3
uses: actions/setup-go@be3c94b385c4f180051c996d336f57a34c397495 # actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
id: go
-2
View File
@@ -3,11 +3,9 @@
.vscode/
/bench/
/demo/
/deno/
/esbuild
/github/
/go/
/lib/deno/lib.deno.d.ts
/npm/@esbuild/android-arm/esbuild.wasm
/npm/@esbuild/android-arm/wasm_exec_node.js
/npm/@esbuild/android-arm/wasm_exec.js
+148
View File
@@ -1,5 +1,153 @@
# Changelog
## Unreleased
* Allow `es2026` as a target in `tsconfig.json`
TypeScript is [adding `es2026`](https://github.com/microsoft/TypeScript/issues/63704) as a compilation target, so esbuild now supports this in the `target` field of `tsconfig.json` files, such as in the following configuration file:
```json
{
"compilerOptions": {
"target": "ES2026"
}
}
```
As a reminder, the only thing that esbuild uses this field for is determining whether or not to use legacy TypeScript behavior for class fields. You can read more in [the documentation](https://esbuild.github.io/content-types/#tsconfig-json).
* Stop publishing to https://deno.land/x/esbuild
Deno has made the https://deno.land/x package registry read-only, so new versions of esbuild can no longer be published to https://deno.land/x/esbuild. The last published version was [v0.28.1](https://deno.land/x/[email protected]). I have removed the parts of esbuild that publish a Deno-specific package. Instead you can install `npm:esbuild` to use esbuild in Deno (this wasn't the case when esbuild's Deno-specific package was first published).
* Emit an error when code splitting chunks would be merged ([#4411](https://github.com/evanw/esbuild/issues/4411))
It's possible to configure esbuild such that separate output files end up with the same output path. For example, you could name all code splitting chunks `chunk` via the `chunkNames` setting, which might generate multiple output files with the same path `chunk.js`. This does not happen by default since by default the chunk names include a hash to make sure they're unique (e.g. named something like `chunk-GX7G2SBE.js` instead).
Previously esbuild allowed output files to be merged if both the file path and content were the same. This behavior was intended for assets (e.g. images) but is not appropriate for code, as code modules may still have their own internal state that needs to stay separate. This configuration is no longer allowed starting with this release. Doing this is now a build error. If your code structure generates conflicting chunk names, then you should make sure the chunk names include a placeholder for the hash.
## 0.28.2
* Fix tree shaking bug due to TypeScript import alias ([#4507](https://github.com/evanw/esbuild/issues/4507))
This release fixes a bug that could cause esbuild to incorrectly tree-shake imports that are used in a TypeScript type alias under certain circumstances. Affected code uses a TypeScript-specific `import` assignment and looks something like this:
```ts
import Base from './dep.js';
import Alias = Base.SomeType;
```
* Fix CSS minification bug involving `&` ([#4497](https://github.com/evanw/esbuild/issues/4497))
This release fixes a bug where esbuild's CSS minifier incorrectly removed a `&` when it was unsafe to do so. Here is an example:
```css
/* Original code */
.a .b {
& .b:not(& .c) {
color: red;
}
}
/* Old output (with --minify) */
.a .b{.b:not(& .c){color:red}}
/* New output (with --minify) */
.a .b{& .b:not(& .c){color:red}}
```
This should match `<span class="a"><span class="b"><span class="b">yes</span></span></span>` but not `<span class="a"><span class="b">no</span></span>`. The old output incorrectly matched both.
* Avoid overwriting input files without `--allow-overwrite` ([#4484](https://github.com/evanw/esbuild/issues/4484))
For example: `esbuild input.js --outfile=input.js` tells esbuild to overwrite `input.js` with the output of running esbuild on it. This was supposed to already be prevented by default, but it accidentally regressed in version 0.17.0 and apparently didn't have any test coverage. The error message was being printed but the input file was still being overwritten. Oops.
This release puts the original behavior back. With this release, esbuild should now actually avoid overwriting input files unless `--allow-overwrite` is explicitly present. This is done by not writing out any files when a build error is encountered.
* Fix incorrect code generated when using top-level await ([#4498](https://github.com/evanw/esbuild/issues/4498))
Previously esbuild could generate code containing a syntax error in complex scenarios involving top-level await used in a dependency cycle. The problem was a missing `async` on one or more module wrapper closures. With this release, esbuild now uses a fixed-point iteration algorithm to correctly annotate all dependencies in the cycle as needing an `async` module wrapper.
* Fix a minification bug with lowered logical assignment operators ([#4508](https://github.com/evanw/esbuild/issues/4508))
This release fixes a bug that could cause esbuild to generate incorrect code for logical assignment operators when lowering them to an older target environment. Specifically the lowering process requires duplicating the left-hand side, but esbuild incorrectly failed to count the duplicate as a new usage when the left-hand side is an identifier. That then caused the minifier to believe that the left-hand side was only used once and could attempt to incorrectly inline an initializer into the first usage. This bug has now been fixed:
```js
// Original code
function foo() {
let x
bar(x ||= {})
}
// Old output (with --minify-syntax --target=es6)
function foo() {
bar(void 0 || (x = {}));
}
// New output (with --minify-syntax --target=es6)
function foo() {
let x;
bar(x || (x = {}));
}
```
* Fix a potential deadlock when the JavaScript API is used incorrectly ([#4503](https://github.com/evanw/esbuild/issues/4503), [#4506](https://github.com/evanw/esbuild/pull/4506))
The JavaScript API runs the native esbuild executable as a long-lived child process and communicates with it over stdin/stdout/stderr. Each API request is asynchronous and the executable stays open as long as it has work to do, which is as long as either stdin is still open (meaning there may be more API requests) or there are currently requests being processed.
Previously esbuild's tracking of outstanding API requests missed decrementing a reference count in an edge case where esbuild's JavaScript API was used incorrectly and the API request returned an error. This could in some cases cause esbuild's native executable to exit with an error message about a deadlock. This release fixes the reference counting bug.
This fix was submitted by [@ZuBB](https://github.com/ZuBB).
* Handle target collisions ([#4509](https://github.com/evanw/esbuild/issues/4509))
It's possible to specify the same target engine multiple times, such as with `--target=chrome1,chrome99`. This edge case wasn't anticipated and previously took the last version for the duplicated target engine instead of the minimum version (so `chrome99` in this case instead of `chrome1`). With this release, esbuild will now pick the minimum version between all duplicated target engines.
* Force `.mp3` files to use the `audio/mpeg` MIME type ([#4485](https://github.com/evanw/esbuild/issues/4485))
MIME type detection for esbuild's data URLs uses Go's built-in MIME type detection, which is based on the [MIME sniffing standard](https://mimesniff.spec.whatwg.org/). This works correctly for MP3 files that start with the byte sequence `ID3`, which is commonly the case. However, it's possible to construct valid MP3 files that do not start with `ID3`, and that perhaps Go's built-in MIME type detection doesn't implement the "Signature for MP3 without ID3" part of the algorithm. This results in some `.mp3` files incorrectly using the `application/octet-stream` MIME type instead of `audio/mpeg`. With this release, esbuild will now always use the `audio/mpeg` MIME type for files ending in `.mp3`.
* Add a new TypeScript syntax warning
TypeScript 7 turned some previously-valid TypeScript syntax into a syntax error because it was confusing. TypeScript 6 accepts `1 + 2 as number * 3` as valid syntax but confusingly converts it to `(1 + 2) * 3` instead of the more intuitive conversion to `1 + (2 * 3)`. This syntax is now an error in TypeScript 7+. With this release, esbuild will now warn about the use of this syntax:
```ts
▲ [WARNING] Operator "*" should not directly follow a TypeScript type cast after the "+" operator [confusing-typescript-cast]
example.ts:1:28:
1 │ console.log(1 + 2 as number * 3)
╵ ^
This is a syntax error in newer versions of TypeScript because the type cast has unintuitive
precedence in this case. Surround the inner expression in parentheses to silence this warning:
example.ts:1:12:
1 │ console.log(1 + 2 as number * 3)
│ ~~~~~~~~~~~~~~~
╵ ( )
```
See [microsoft/TypeScript#63527](https://github.com/microsoft/TypeScript/issues/63527) for more information.
* Add support for formatting errors for Visual Studio ([#4460](https://github.com/evanw/esbuild/issues/4460))
Visual Studio has a specific style that it expects log messages to be in for them to show up in the UI when esbuild is run as a custom build step. The current log style that esbuild uses doesn't conform to this specific style.
With this release, esbuild has a new log style for Visual Studio (and other tools in the MSBuild ecosystem) that can be enabled with `--log-style=visualstudio`. Here is an example log message in this style:
```
$ esbuild example.ts --log-style=visualstudio
/Users/evan/dev/esbuild/example.ts(1,29): warning ES0010: Operator "*" should not directly follow a TypeScript type cast after the "+" operator
```
This log style is also available via the JS and Go APIs, and can now be used with the existing `formatMessages` API.
* Fix a bug with CSS gamut mapping ([#4488](https://github.com/evanw/esbuild/pull/4488))
Due to a typo, the fallback colors generated for CSS colors outside of the sRGB gamut weren't correct. This release fixes the generated colors to use the intended algorithm.
This fix was submitted by [@chatman-media](https://github.com/chatman-media).
## 0.28.1
* Disallow ``\`` in local development server HTTP requests ([GHSA-g7r4-m6w7-qqqr](https://github.com/evanw/esbuild/security/advisories/GHSA-g7r4-m6w7-qqqr))
+6 -31
View File
@@ -24,7 +24,7 @@ test-common: test-go vet-go no-filepath verify-source-map end-to-end-tests js-ap
# These tests are for release (the extra tests are not included in "test" because they are pretty slow)
test-all:
@$(MAKE) --no-print-directory -j6 test-common test-deno ts-type-tests test-wasm-node test-wasm-browser lib-typecheck test-yarnpnp
@$(MAKE) --no-print-directory -j6 test-common ts-type-tests test-wasm-node test-wasm-browser lib-typecheck test-yarnpnp
check-go-version:
@go version | grep -F " go$(GO_VERSION) " || (echo 'Please install Go version $(GO_VERSION)' && false)
@@ -47,7 +47,7 @@ check-go-version:
# variable like this: "ESBUILD_RACE=-race make test". Or you can permanently
# enable it by adding "export ESBUILD_RACE=-race" to your shell profile.
test-go:
go test $(ESBUILD_RACE) ./internal/... ./pkg/...
go test $(ESBUILD_RACE) ./cmd/... ./internal/... ./pkg/...
vet-go:
go vet ./cmd/... ./internal/... ./pkg/...
@@ -76,7 +76,8 @@ go-compiler: go/$(GO_VERSION)
# into other text, and a local development server that serves a read-only
# view of your source code and is not intended for production use.
go/$(GO_VERSION):
mkdir -p go/$(GO_VERSION) && cd go/$(GO_VERSION) && curl -LO https://go.dev/dl/go$(GO_VERSION).src.tar.gz && tar xzf go$(GO_VERSION).src.tar.gz
mkdir -p go/$(GO_VERSION) && touch go/go.mod # Disable the Go language server in this folder
cd go/$(GO_VERSION) && curl -LO https://go.dev/dl/go$(GO_VERSION).src.tar.gz && tar xzf go$(GO_VERSION).src.tar.gz
cd go/$(GO_VERSION)/go && grep -qF "ctxt.buildinfo()" src/cmd/link/internal/ld/main.go # Make sure this call is still there
cd go/$(GO_VERSION)/go/src/cmd/link/internal/ld && sed "s/ctxt.buildinfo/\\/\\/ctxt.buildinfo/" main.go > temp && mv temp main.go
cd go/$(GO_VERSION)/go/src && if uname | grep -qE "MINGW|WIN32|CYGWIN"; then ./make.bat; else ./make.bash; fi
@@ -97,16 +98,6 @@ test-wasm-node: esbuild
test-wasm-browser: platform-wasm | scripts/browser/node_modules
cd scripts/browser && node browser-tests.js
test-deno: esbuild platform-deno
ESBUILD_BINARY_PATH="$(shell pwd)/esbuild" deno test --allow-run --allow-env --allow-net --allow-read --allow-write --no-check scripts/deno-tests.js
@echo '✅ deno tests passed' # I couldn't find a Deno API for telling when tests have failed, so I'm doing this here instead
ESBUILD_BINARY_PATH="$(shell pwd)/esbuild" deno eval 'import { transform, stop } from "file://$(shell pwd)/deno/mod.js"; console.log((await transform("1+2")).code); stop()' | grep "1 + 2;"
ESBUILD_BINARY_PATH="$(shell pwd)/esbuild" deno eval 'import { transform, stop } from "file://$(shell pwd)/deno/wasm.js"; console.log((await transform("1+2")).code); stop()' | grep "1 + 2;"
ESBUILD_BINARY_PATH="$(shell pwd)/esbuild" deno run -A './deno/mod.js' # See: https://github.com/evanw/esbuild/pull/3917
test-deno-windows: esbuild platform-deno
ESBUILD_BINARY_PATH=./esbuild.exe deno test --allow-run --allow-env --allow-net --allow-read --allow-write --no-check scripts/deno-tests.js
register-test: version-go | scripts/node_modules
node scripts/esbuild.js npm/esbuild/package.json --version
node scripts/register-test.js
@@ -151,7 +142,7 @@ test-old-ts: | require/old-ts/node_modules
node-unref-tests: | scripts/node_modules
node scripts/node-unref-tests.js
lib-typecheck: lib-typecheck-node lib-typecheck-node-nolib lib-typecheck-deno
lib-typecheck: lib-typecheck-node lib-typecheck-node-nolib
lib-typecheck-node: | lib/node_modules
cd lib && node_modules/.bin/tsc -noEmit -p tsconfig.json
@@ -159,14 +150,8 @@ lib-typecheck-node: | lib/node_modules
lib-typecheck-node-nolib: | lib/node_modules
cd lib && node_modules/.bin/tsc -noEmit -p tsconfig-nolib.json
lib-typecheck-deno: lib/deno/lib.deno.d.ts | lib/node_modules
cd lib && node_modules/.bin/tsc -noEmit -p tsconfig-deno.json
lib/deno/lib.deno.d.ts:
deno types > lib/deno/lib.deno.d.ts
# End-to-end tests
test-e2e: test-e2e-npm test-e2e-pnpm test-e2e-yarn test-e2e-yarn-berry test-e2e-deno
test-e2e: test-e2e-npm test-e2e-pnpm test-e2e-yarn test-e2e-yarn-berry
test-e2e-npm:
# Test normal install
@@ -295,10 +280,6 @@ test-e2e-yarn-berry:
# Clean up
rm -fr e2e-yb
test-e2e-deno:
deno eval 'import { transform, stop } from "https://deno.land/x/esbuild@v$(ESBUILD_VERSION)/mod.js"; console.log((await transform("1+2")).code); stop()' | grep "1 + 2;"
deno eval 'import { transform, stop } from "https://deno.land/x/esbuild@v$(ESBUILD_VERSION)/wasm.js"; console.log((await transform("1+2")).code); stop()' | grep "1 + 2;"
test-yarnpnp: platform-wasm
node scripts/test-yarnpnp.js
@@ -319,7 +300,6 @@ version-go:
platform-all: \
platform-android-arm \
platform-android-x64 \
platform-deno \
platform-neutral \
platform-openharmony-arm64 \
platform-wasi-preview1 \
@@ -462,11 +442,6 @@ platform-wasm: esbuild go-compiler
$(GO_COMPILER) "$(NODE)" scripts/esbuild.js ./esbuild --wasm
@shasum -a 256 npm/esbuild-wasm/esbuild.wasm
platform-deno: platform-wasm go-compiler
@echo
@echo "# Build: deno"
$(GO_COMPILER) "$(NODE)" scripts/esbuild.js ./esbuild --deno
publish-all: check-go-version
# Make sure the npm directory is pristine (including .gitignored files) since it will be published
rm -fr npm && git checkout npm
+4 -14
View File
@@ -23,19 +23,6 @@ Pushing a change to [`version.txt`](./version.txt) causes the following:
2. Create a tag for the release that looks like `v0.X.Y`
3. Publish a [GitHub Release](https://github.com/evanw/esbuild/releases) containing the release notes in [`CHANGELOG.md`](./CHANGELOG.md)
- The [`release.yml`](https://github.com/esbuild/deno-esbuild/blob/main/.github/workflows/release.yml)
workflow in the https://github.com/esbuild/deno-esbuild repo runs
occasionally. On the next run, it will notice the version change and:
1. Clone this repo
2. Run `make platform-deno`
3. Commit and push the new contents of the `deno` folder to the `deno-esbuild` repo
4. Create a tag for the release that looks like `v0.X.Y`
5. Post an event to the https://api.deno.land/webhook/gh/esbuild webhook
6. Deno will then add a new version to https://deno.land/x/esbuild
You can also manually trigger this workflow if you want it to happen immediately.
- The [`release.yml`](https://github.com/esbuild/esbuild.github.io/blob/main/.github/workflows/release.yml)
workflow in the https://github.com/esbuild/esbuild.github.io repo runs
occasionally. On the next run, it will notice the version change and:
@@ -45,7 +32,10 @@ Pushing a change to [`version.txt`](./version.txt) causes the following:
3. Commit and push these new scripts to the `gh-pages` branch of the `esbuild.github.io` repo
4. GitHub Pages will then deploy these updates to https://esbuild.github.io/
You can also manually trigger this workflow if you want it to happen immediately.
You can also manually trigger this workflow if you want it to happen
immediately. Note that this workflow periodically disables itself due to
inactivity so it's important to check that it's still running after a
release.
## Adding a new package
+26 -4
View File
@@ -64,7 +64,7 @@ func (service *serviceType) createActiveBuild(key int) *activeBuild {
activeBuild := &activeBuild{}
service.activeBuilds[key] = activeBuild
// This pairs with "Done()" in "decRefCount"
// This pairs with "Done()" in "destroyActiveBuild"
service.keepAliveWaitGroup.Add(1)
return activeBuild
}
@@ -77,7 +77,7 @@ func (service *serviceType) destroyActiveBuild(key int) {
delete(service.activeBuilds, key)
service.mutex.Unlock()
// This pairs with "Add()" in "trackActiveBuild"
// This pairs with "Add()" in "createActiveBuild"
service.keepAliveWaitGroup.Done()
}
@@ -639,6 +639,12 @@ func (service *serviceType) handleBuildRequest(id uint32, request map[string]int
}
activeBuild := service.createActiveBuild(key)
shouldDestroyActiveBuild := true
defer func() {
if shouldDestroyActiveBuild {
service.destroyActiveBuild(key)
}
}()
hasOnEndCallbacks := false
if plugins, ok := request["plugins"]; ok {
@@ -769,6 +775,7 @@ func (service *serviceType) handleBuildRequest(id uint32, request map[string]int
// Keep the build alive until "dispose" has been called
activeBuild.disposeWaitGroup.Add(1)
activeBuild.ctx = ctx
shouldDestroyActiveBuild = false
return encodePacket(packet{
id: id,
@@ -782,8 +789,6 @@ func (service *serviceType) handleBuildRequest(id uint32, request map[string]int
result := api.Build(options)
response := resultToResponse(result)
service.destroyActiveBuild(key)
return encodePacket(packet{
id: id,
value: response,
@@ -1240,6 +1245,23 @@ func (service *serviceType) handleFormatMessagesRequest(id uint32, request map[s
if value, ok := request["terminalWidth"].(int); ok {
options.TerminalWidth = value
}
if logStyle, ok := request["logStyle"].(string); ok {
switch logStyle {
case "default":
options.LogStyle = api.LogStyleDefault
case "clang":
options.LogStyle = api.LogStyleClang
case "visualstudio":
options.LogStyle = api.LogStyleVisualStudio
default:
return encodePacket(packet{
id: id,
value: map[string]interface{}{
"error": fmt.Sprintf("Invalid log style: %q", logStyle),
},
})
}
}
result := api.FormatMessages(msgs, options)
+102
View File
@@ -0,0 +1,102 @@
package main
import (
"os"
"testing"
"time"
"github.com/evanw/esbuild/internal/helpers"
)
func TestHandleBuildRequestReleasesActiveBuildAfterError(t *testing.T) {
absWorkingDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
modifyRequest func(map[string]interface{})
expectedError string
expectedMessage string
}{
{
name: "InvalidPluginRegExp",
modifyRequest: func(request map[string]interface{}) {
request["plugins"] = []interface{}{map[string]interface{}{
"name": "test",
"onEnd": false,
"onResolve": []interface{}{map[string]interface{}{
"id": 0,
"filter": "x(?=y)",
"namespace": "",
}},
"onLoad": []interface{}{},
}}
},
expectedError: "[test] \"onResolve\" filter is not a valid Go regular expression: \"x(?=y)\"",
},
{
name: "InvalidContextOptions",
modifyRequest: func(request map[string]interface{}) {
request["context"] = true
request["flags"] = []interface{}{"--external:test"}
},
expectedMessage: "Cannot use \"external\" without \"bundle\"",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
service := serviceType{
activeBuilds: make(map[int]*activeBuild),
keepAliveWaitGroup: helpers.MakeThreadSafeWaitGroup(),
}
request := map[string]interface{}{
"context": false,
"key": 0,
"write": false,
"entries": []interface{}{},
"flags": []interface{}{},
"absWorkingDir": absWorkingDir,
"nodePaths": []interface{}{},
}
test.modifyRequest(request)
encoded := service.handleBuildRequest(0, request)
bytes, _, ok := readLengthPrefixedSlice(encoded)
if !ok {
t.Fatal("Invalid encoded response")
}
response, ok := decodePacket(bytes)
if !ok {
t.Fatal("Invalid response packet")
}
value := response.value.(map[string]interface{})
if test.expectedError != "" && value["error"] != test.expectedError {
t.Fatalf("Expected error %q, got %q", test.expectedError, value["error"])
}
if test.expectedMessage != "" {
errors := value["errors"].([]interface{})
if len(errors) != 1 || errors[0].(map[string]interface{})["text"] != test.expectedMessage {
t.Fatalf("Expected message %q, got %v", test.expectedMessage, errors)
}
}
if service.getActiveBuild(0) != nil {
t.Fatal("Active build was not released")
}
done := make(chan struct{})
go func() {
service.keepAliveWaitGroup.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("Keep-alive wait group did not reach zero")
}
})
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
package main
const esbuildVersion = "0.28.1"
const esbuildVersion = "0.28.2"
+1 -1
View File
@@ -1 +1 @@
1.26.4
1.26.5
+68 -26
View File
@@ -116,7 +116,8 @@ type globResolveResult struct {
type tlaCheck struct {
parent ast.Index32
depth uint32
depth ast.Index32
pass uint32
importRecordIndex uint32
}
@@ -2804,19 +2805,24 @@ func (s *scanner) processScannedFiles(entryPointMeta []graph.EntryPoint) []scann
AbsPath: s.fs.Join(s.options.AbsOutputDir, relPath),
Contents: bytes,
JSONMetadataChunk: jsonMetadataChunk,
CanBeMerged: true,
}}
}
s.results[sourceIndex] = result
}
// Traverse the graph to check top-level await
if s.iterativelyValidateTLA() {
s.reportInvalidTLA()
}
// The linker operates on an array of files, so construct that now. This
// can't be constructed earlier because we generate new parse results for
// JavaScript stub files for CSS imports above.
files := make([]scannerFile, len(s.results))
for sourceIndex := range s.results {
if result := &s.results[sourceIndex]; result.ok {
s.validateTLA(uint32(sourceIndex))
files[sourceIndex] = result.file
}
}
@@ -2824,33 +2830,69 @@ func (s *scanner) processScannedFiles(entryPointMeta []graph.EntryPoint) []scann
return files
}
func (s *scanner) validateTLA(sourceIndex uint32) tlaCheck {
func (s *scanner) iterativelyValidateTLA() bool {
pass := uint32(1)
hasTLA := false
// Iterate until a fixed point has been reached to handle graph cycles
for {
didChange := false
for sourceIndex := range s.results {
s.recursivelyValidateTLA(uint32(sourceIndex), pass, &didChange)
}
if !didChange {
return hasTLA
}
pass++
hasTLA = true
}
}
func (s *scanner) recursivelyValidateTLA(sourceIndex uint32, pass uint32, didChange *bool) tlaCheck {
result := &s.results[sourceIndex]
if result.ok && result.tlaCheck.depth == 0 {
// Use a "pass" integer instead of a separate "visited" set
if result.ok && result.tlaCheck.pass != pass {
result.tlaCheck.pass = pass
if repr, ok := result.file.inputFile.Repr.(*graph.JSRepr); ok {
result.tlaCheck.depth = 1
if repr.AST.LiveTopLevelAwaitKeyword.Len > 0 {
// If this module contains top-level await, set its parent to itself
if repr.AST.LiveTopLevelAwaitKeyword.Len > 0 && result.tlaCheck.parent.GetIndex() != sourceIndex {
result.tlaCheck.parent = ast.MakeIndex32(sourceIndex)
result.tlaCheck.depth = ast.MakeIndex32(1)
*didChange = true
}
// Check all import statements and require calls (only import statements are valid)
for importRecordIndex, record := range repr.AST.ImportRecords {
if record.SourceIndex.IsValid() && (record.Kind == ast.ImportRequire || record.Kind == ast.ImportStmt) {
parent := s.validateTLA(record.SourceIndex.GetIndex())
if !parent.parent.IsValid() {
continue
}
parent := s.recursivelyValidateTLA(record.SourceIndex.GetIndex(), pass, didChange)
// Follow any import chains
if record.Kind == ast.ImportStmt && (!result.tlaCheck.parent.IsValid() || parent.depth < result.tlaCheck.depth) {
result.tlaCheck.depth = parent.depth + 1
// Track the shallowest top-level await parent (used to report invalid import chains later on)
if record.Kind == ast.ImportStmt && parent.depth.GetIndex() < result.tlaCheck.depth.GetIndex()-1 {
result.tlaCheck.parent = record.SourceIndex
result.tlaCheck.depth = ast.MakeIndex32(parent.depth.GetIndex() + 1)
result.tlaCheck.importRecordIndex = uint32(importRecordIndex)
*didChange = true
continue
}
}
}
}
}
return result.tlaCheck
}
func (s *scanner) reportInvalidTLA() {
for sourceIndex := range s.results {
result := &s.results[sourceIndex]
if result.ok && result.tlaCheck.parent.IsValid() {
if repr, ok := result.file.inputFile.Repr.(*graph.JSRepr); ok {
for _, record := range repr.AST.ImportRecords {
// Require of a top-level await chain is forbidden
if record.Kind == ast.ImportRequire {
if record.Kind == ast.ImportRequire && record.SourceIndex.IsValid() && s.results[record.SourceIndex.GetIndex()].tlaCheck.parent.IsValid() {
var notes []logger.MsgData
var tlaPrettyPaths logger.PrettyPaths
otherSourceIndex := record.SourceIndex.GetIndex()
@@ -2899,18 +2941,14 @@ func (s *scanner) validateTLA(sourceIndex uint32) tlaCheck {
s.log.AddErrorWithNotes(&tracker, record.Range, text, notes)
}
}
}
// Make sure that if we wrap this module in a closure, the closure is also
// async. This happens when you call "import()" on this module and code
// splitting is off.
if result.tlaCheck.parent.IsValid() {
// Make sure that if we wrap this module in a closure, the closure is also
// async. This happens when you call "import()" on this module and code
// splitting is off.
repr.Meta.IsAsyncOrHasAsyncDependency = true
}
}
}
return result.tlaCheck
}
func DefaultExtensionToLoaderMap() map[string]config.Loader {
@@ -3128,22 +3166,25 @@ func (b *Bundle) Compile(log logger.Log, timer *helpers.Timer, mangleCache map[s
// Make an exception for files that have identical contents. In that case
// the duplicate is just silently filtered out. This can happen with the
// "file" loader, for example.
outputFileMap := make(map[string][]byte)
outputFileMap := make(map[string]graph.OutputFile)
end := 0
for _, outputFile := range outputFiles {
absPathKey := canonicalFileSystemPathForWindows(outputFile.AbsPath)
contents, ok := outputFileMap[absPathKey]
existingFile, ok := outputFileMap[absPathKey]
// If this isn't a duplicate, keep the output file
if !ok {
outputFileMap[absPathKey] = outputFile.Contents
outputFileMap[absPathKey] = outputFile
outputFiles[end] = outputFile
end++
continue
}
// If the names and contents are both the same, only keep the first one
if bytes.Equal(contents, outputFile.Contents) {
// If the names and contents are both the same, only keep the first one.
// We only do this for assets, not for code, as two identical code files
// need unique identity as they might need to have their own separate
// copies of internal state. See https://github.com/evanw/esbuild/issues/4411
if existingFile.CanBeMerged && outputFile.CanBeMerged && bytes.Equal(existingFile.Contents, outputFile.Contents) {
continue
}
@@ -3152,6 +3193,7 @@ func (b *Bundle) Compile(log logger.Log, timer *helpers.Timer, mangleCache map[s
if relPath, ok := b.fs.Rel(b.fs.Cwd(), outputPath); ok {
outputPath = relPath
}
outputPath = strings.ReplaceAll(outputPath, "\\", "/")
log.AddError(nil, logger.Range{}, "Two output files share the same path but have different contents: "+outputPath)
}
outputFiles = outputFiles[:end]
+97 -3
View File
@@ -3131,6 +3131,47 @@ func TestNoOverwriteInputFileError(t *testing.T) {
})
}
// https://github.com/evanw/esbuild/issues/4411
func TestDuplicateChunkNameIssue4411(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry-a1.js": `
import { add } from "./shared-a/shared.js"
export const addA1 = () => add("a1")
`,
"/entry-a2.js": `
import { add } from "./shared-a/shared.js"
export const addA2 = () => add("a2")
`,
"/entry-b1.js": `
import { add } from "./shared-b/shared.js"
export const addB1 = () => add("b1")
`,
"/entry-b2.js": `
import { add } from "./shared-b/shared.js"
export const addB2 = () => add("b2")
`,
"/shared-a/shared.js": `
const cache = []
export const add = str => cache.push(str)
`,
"/shared-b/shared.js": `
const cache = []
export const add = str => cache.push(str)
`,
},
entryPaths: []string{"/entry-a1.js", "/entry-a2.js", "/entry-b1.js", "/entry-b2.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
CodeSplitting: true,
ChunkPathTemplate: []config.PathTemplate{{Placeholder: config.NamePlaceholder}},
},
expectedCompileLog: `ERROR: Two output files share the same path but have different contents: out/chunk.js
`,
})
}
func TestDuplicateEntryPoint(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
@@ -3141,8 +3182,10 @@ func TestDuplicateEntryPoint(t *testing.T) {
entryPaths: []string{"/entry.js", "/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out.js",
AbsOutputDir: "/out",
},
expectedCompileLog: `ERROR: Two output files share the same path but have different contents: out/entry.js
`,
})
}
@@ -3156,7 +3199,7 @@ func TestRelativeEntryPointError(t *testing.T) {
entryPaths: []string{"entry"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out.js",
AbsOutputDir: "/out",
},
expectedScanLog: `ERROR: Could not resolve "entry"
NOTE: Use the relative path "./entry" to reference the file "entry.js". Without the leading "./", the path "entry" is being interpreted as a package path instead.
@@ -3174,11 +3217,29 @@ func TestMultipleEntryPointsSameNameCollision(t *testing.T) {
entryPaths: []string{"/a/entry.js", "/b/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out/",
AbsOutputDir: "/out",
},
})
}
func TestMultipleEntryPointsSameNameCollisionError(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/a/entry.js": `import {foo} from '../common.js'; console.log(foo)`,
"/b/entry.js": `import {foo} from '../common.js'; console.log(foo)`,
"/common.js": `export let foo = 123`,
},
entryPaths: []string{"/a/entry.js", "/b/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
EntryPathTemplate: []config.PathTemplate{{Placeholder: config.NamePlaceholder}},
},
expectedCompileLog: `ERROR: Two output files share the same path but have different contents: out/entry.js
`,
})
}
func TestReExportCommonJSAsES6(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
@@ -4377,6 +4438,39 @@ func TestTopLevelAwaitAllowedImportWithSplitting(t *testing.T) {
})
}
// https://github.com/evanw/esbuild/issues/4498
func TestTopLevelAwaitCyclicDependenciesIssue4498(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.mjs": `
await import("./main.mjs");
`,
"/main.mjs": `
import { a } from "./a.mjs";
console.log(a());
`,
"/a.mjs": `
import { b } from "./b.mjs";
import { tla } from "./dep.mjs";
export function a() { return b() + tla; }
`,
"/b.mjs": `
import { a } from "./a.mjs";
export function b() { return typeof a; }
`,
"/dep.mjs": `
export const tla = await Promise.resolve("x");
`,
},
entryPaths: []string{"/entry.mjs"},
options: config.Options{
Mode: config.ModeBundle,
OutputFormat: config.FormatESModule,
AbsOutputDir: "/out",
},
})
}
func TestAssignToImport(t *testing.T) {
default_suite.expectBundled(t, bundled{
files: map[string]string{
+26
View File
@@ -3064,3 +3064,29 @@ func TestParameterPropsUseDefineForClassFieldsTrueLowered(t *testing.T) {
},
})
}
// See: https://github.com/evanw/esbuild/issues/4507
func TestTreeShakingImportAliasIssue4507(t *testing.T) {
ts_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.ts": `
import Base from './dep.js';
import Alias = Base.SomeType;
export function make() {
return new Base();
}
`,
"/dep.js": `
export default class Base {
static hello() { return 'dep code is present'; }
}
`,
},
entryPaths: []string{"/entry.ts"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.js",
},
})
}
@@ -1156,12 +1156,6 @@ var require_index = __commonJS({
var import__ = __toESM(require_index());
console.log(import__.x);
================================================================================
TestDuplicateEntryPoint
---------- /out.js/entry.js ----------
// entry.js
console.log(123);
================================================================================
TestDuplicatePropertyWarning
---------- /out/entry.js ----------
@@ -6897,6 +6891,50 @@ TestTopLevelAwaitCJSDeadBranch
if (false) foo;
if (false) for (foo of bar) ;
================================================================================
TestTopLevelAwaitCyclicDependenciesIssue4498
---------- /out/entry.js ----------
// b.mjs
function b() {
return typeof a;
}
var init_b = __esm({
async "b.mjs"() {
await init_a();
}
});
// dep.mjs
var tla;
var init_dep = __esm({
async "dep.mjs"() {
tla = await Promise.resolve("x");
}
});
// a.mjs
function a() {
return b() + tla;
}
var init_a = __esm({
async "a.mjs"() {
await init_b();
await init_dep();
}
});
// main.mjs
var main_exports = {};
var init_main = __esm({
async "main.mjs"() {
await init_a();
console.log(a());
}
});
// entry.mjs
await init_main().then(() => main_exports);
================================================================================
TestTopLevelAwaitESM
---------- /out.js ----------
@@ -2251,3 +2251,21 @@ if (nested) {
new Bar(bar2(objBar));
}
var bar;
================================================================================
TestTreeShakingImportAliasIssue4507
---------- /out.js ----------
// dep.js
var Base = class {
static hello() {
return "dep code is present";
}
};
// entry.ts
function make() {
return new Base();
}
export {
make
};
+86
View File
@@ -57,6 +57,92 @@ func compareVersions(a v, b Semver) int {
return diff
}
func CompareSemver(a Semver, b Semver) int {
an := len(a.Parts)
bn := len(b.Parts)
n := an
if bn > n {
n = bn
}
for i := 0; i < n; i++ {
ai := 0
bi := 0
if i < an {
ai = a.Parts[i]
}
if i < bn {
bi = b.Parts[i]
}
if ai != bi {
return ai - bi
}
}
if (a.PreRelease != "") != (b.PreRelease != "") {
return len(b.PreRelease) - len(a.PreRelease)
}
a_tail := strings.TrimPrefix(a.PreRelease, "-")
b_tail := strings.TrimPrefix(b.PreRelease, "-")
for a_tail != "" && b_tail != "" {
var a_head, b_head string
a_head, a_tail = splitOffNextPreReleasePart(a_tail)
b_head, b_tail = splitOffNextPreReleasePart(b_tail)
if a_head == b_head {
continue
}
// Check for numbers
a_num, a_isnum := preReleasePartToNumber(a_head)
b_num, b_isnum := preReleasePartToNumber(b_head)
if !a_isnum && !b_isnum {
// Lexicographic comparison for non-numbers
if a_head < b_head {
return -1
} else {
return 1
}
} else if a_isnum && b_isnum {
// Numeric comparison for numbers
if a_num != b_num {
return a_num - b_num
} else {
// Compare lengths for different text but equal numbers (e.g. "0" vs. "00")
return len(a_head) - len(b_head)
}
} else {
// One is a number and the other isn't
if a_isnum {
return -1
} else {
return 1
}
}
}
return len(a_tail) - len(b_tail)
}
func splitOffNextPreReleasePart(text string) (head string, tail string) {
if i := strings.IndexByte(text, '.'); i != -1 {
return text[:i], text[i+1:]
}
return text, ""
}
func preReleasePartToNumber(text string) (int, bool) {
for _, c := range text {
if c < '0' || c > '9' {
return 0, false
}
}
n, err := strconv.Atoi(text)
return n, err == nil
}
// The start is inclusive and the end is exclusive
type versionRange struct {
start v
+67
View File
@@ -61,3 +61,70 @@ func TestCompareVersions(t *testing.T) {
check(v{1, 2, 3}, Semver{Parts: []int{1, 2, 3}, PreRelease: "-pre"}, '>')
check(v{1, 2, 2}, Semver{Parts: []int{1, 2, 3}, PreRelease: "-pre"}, '<')
}
func TestCompareSemver(t *testing.T) {
t.Helper()
check := func(a Semver, b Semver, expected rune) {
t.Helper()
at := a.String()
bt := b.String()
t.Run(fmt.Sprintf("%q ? %q", at, bt), func(t *testing.T) {
observed := '='
if result := CompareSemver(a, b); result < 0 {
observed = '<'
} else if result > 0 {
observed = '>'
}
if observed != expected {
test.AssertEqual(t, fmt.Sprintf("%c", observed), fmt.Sprintf("%c", expected))
}
})
// Automatically generate reverse comparison cases to cut down on test count
if expected == '<' {
t.Run(fmt.Sprintf("%q ? %q", bt, at), func(t *testing.T) {
observed := '='
if result := CompareSemver(b, a); result < 0 {
observed = '<'
} else if result > 0 {
observed = '>'
}
if observed != '>' {
test.AssertEqual(t, fmt.Sprintf("%c", observed), ">")
}
})
} else if expected == '>' {
t.Fail()
}
}
check(Semver{}, Semver{}, '=')
check(Semver{Parts: []int{0}}, Semver{}, '=')
check(Semver{}, Semver{Parts: []int{0}}, '=')
check(Semver{Parts: []int{0}}, Semver{Parts: []int{0}}, '=')
check(Semver{Parts: []int{0, 0}}, Semver{Parts: []int{0}}, '=')
check(Semver{Parts: []int{0}}, Semver{Parts: []int{0}}, '=')
check(Semver{Parts: []int{0, 0}}, Semver{Parts: []int{0, 0}}, '=')
check(Semver{Parts: []int{1}, PreRelease: "-alpha"}, Semver{Parts: []int{1}, PreRelease: "-alpha"}, '=')
check(Semver{Parts: []int{1, 0}, PreRelease: "-alpha"}, Semver{Parts: []int{1}, PreRelease: "-alpha"}, '=')
check(Semver{Parts: []int{1, 0}, PreRelease: "-alpha"}, Semver{Parts: []int{1, 0, 0}, PreRelease: "-alpha"}, '=')
check(Semver{}, Semver{Parts: []int{1}}, '<')
check(Semver{}, Semver{Parts: []int{0, 1}}, '<')
check(Semver{Parts: []int{0}}, Semver{Parts: []int{1}}, '<')
check(Semver{Parts: []int{0, 1}}, Semver{Parts: []int{1}}, '<')
check(Semver{Parts: []int{0, 1}}, Semver{Parts: []int{1, 0}}, '<')
check(Semver{Parts: []int{2, 0, 1}}, Semver{Parts: []int{2, 1, 0}}, '<')
check(Semver{Parts: []int{2, 0, 1}}, Semver{Parts: []int{2, 0, 2}}, '<')
check(Semver{Parts: []int{1}, PreRelease: "-alpha"}, Semver{Parts: []int{1}}, '<')
check(Semver{Parts: []int{1, 0}, PreRelease: "-alpha"}, Semver{Parts: []int{1}, PreRelease: "-alpha.1"}, '<')
check(Semver{Parts: []int{1, 0}, PreRelease: "-alpha.1"}, Semver{Parts: []int{1}, PreRelease: "-alpha.beta"}, '<')
check(Semver{Parts: []int{1, 0}, PreRelease: "-alpha.1"}, Semver{Parts: []int{1}, PreRelease: "-alpha.11"}, '<')
check(Semver{Parts: []int{1, 0}, PreRelease: "-alpha.1"}, Semver{Parts: []int{1}, PreRelease: "-alpha.02"}, '<')
check(Semver{Parts: []int{1, 0}, PreRelease: "-alpha.1"}, Semver{Parts: []int{1}, PreRelease: "-alpha.01"}, '<')
check(Semver{Parts: []int{1, 0}, PreRelease: "-alpha.01"}, Semver{Parts: []int{1}, PreRelease: "-alpha.2"}, '<')
check(Semver{Parts: []int{1, 0}, PreRelease: "-alpha.08"}, Semver{Parts: []int{1}, PreRelease: "-alpha.011"}, '<')
}
+3
View File
@@ -144,6 +144,7 @@ var knownGlobals = [][]string{
{"Math", "sin"},
{"Math", "sinh"},
{"Math", "sqrt"},
{"Math", "sumPrecise"},
{"Math", "tan"},
{"Math", "tanh"},
{"Math", "trunc"},
@@ -166,7 +167,9 @@ var knownGlobals = [][]string{
// JSON: Static methods
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON#static_methods
{"JSON", "isRawJSON"},
{"JSON", "parse"},
{"JSON", "rawJSON"},
{"JSON", "stringify"},
// TypedArray: Static methods
+1 -1
View File
@@ -437,7 +437,7 @@ func gamut_mapping_xyz_to_srgb(x F64, y F64, z F64) (F64, F64, F64) {
}
clipped_r, clipped_g, clipped_b := clip(r), clip(g), clip(b)
L1, a1, b1 := srgb_to_oklab(clipped_r, clipped_b, clipped_g)
L1, a1, b1 := srgb_to_oklab(clipped_r, clipped_g, clipped_b)
L2, a2, b2 := srgb_to_oklab(r, g, b)
E := delta_eok(L1, a1, b1, L2, a2, b2)
if E.Value() < JND {
+44 -1
View File
@@ -2309,7 +2309,50 @@ func (p *parser) scanForEndOfRule() (endOfRuleScan, int) {
var initialStack [4]css_lexer.T
stack := initialStack[:0]
// Small state machine to recognize "--foo:"
const (
customPropIdent = iota
customPropColon
customPropAccept
customPropReject
)
customProp := customPropIdent
for i, t := range p.tokens[p.index:] {
// People sometimes use CSS custom properties to store JSON. This is
// problematic because the "{...}" braces look like a CSS block unless
// this edge case is special-cased. See the (quite long) discussion on
// https://github.com/w3c/csswg-drafts/issues/7961 for more information.
//
// I couldn't find any actual specification for how this is supposed to
// work. Just a lot of discussion on GitHub. I did find this text, from
// https://drafts.csswg.org/css-syntax/#consume-a-declaration:
//
// Otherwise, if decl’s value contains a top-level simple block with
// an associated token of <{-token>, and also contains any other non-
// <whitespace-token> value, return nothing. (That is, a top-level {}-
// block is only allowed as the entire value of a non-custom property.)
//
// However, current browsers appear to accept custom properties with a
// {}-block contained anywhere inside them, not just as the entire value.
// So that's what I'm attempting to do here.
if customProp < customPropAccept {
switch customProp {
case customPropIdent:
if t.Kind == css_lexer.TIdent && strings.HasPrefix(p.source.Contents[t.Range.Loc.Start:], "--") {
customProp = customPropColon
} else if t.Kind != css_lexer.TWhitespace {
customProp = customPropReject
}
case customPropColon:
if t.Kind == css_lexer.TColon {
customProp = customPropAccept
} else if t.Kind != css_lexer.TWhitespace {
customProp = customPropReject
}
}
}
switch t.Kind {
case css_lexer.TSemicolon:
if len(stack) == 0 {
@@ -2323,7 +2366,7 @@ func (p *parser) scanForEndOfRule() (endOfRuleScan, int) {
stack = append(stack, css_lexer.TCloseBracket)
case css_lexer.TOpenBrace:
if len(stack) == 0 {
if len(stack) == 0 && customProp != customPropAccept {
return endOfRuleOpenBrace, p.index + i
}
stack = append(stack, css_lexer.TCloseBrace)
+34 -1
View File
@@ -252,8 +252,17 @@ const (
func analyzeLeadingAmpersand(sel css_ast.ComplexSelector, isDeclarationContext bool) leadingAmpersand {
if len(sel.Selectors) > 1 {
if first := sel.Selectors[0]; first.IsSingleAmpersand() {
if second := sel.Selectors[1]; second.Combinator.Byte == 0 && len(second.NestingSelectorLocs) > 0 {
second := sel.Selectors[1]
rest := css_ast.ComplexSelector{Selectors: sel.Selectors[1:]}
if second.Combinator.Byte == 0 && rest.ContainsNestingCombinator() {
// ".foo { & &.bar {} }" => ".foo { & &.bar {} }"
// ".foo { & .bar:not(& .baz) {} }" => ".foo { & .bar:not(& .baz) {} }"
// The specification says: "If a selector in the <relative-selector-list>
// does not start with a combinator but does contain the nesting selector,
// it is interpreted as a non-relative selector." So we don't want to
// remove the leading "&" here as that would change the meaning to a
// non-relative selector. See: https://www.w3.org/TR/css-nesting-1/
} else if second.Combinator.Byte != 0 || second.TypeSelector == nil || !isDeclarationContext {
// "& + div {}" => "+ div {}"
// "& div {}" => "div {}"
@@ -264,6 +273,30 @@ func analyzeLeadingAmpersand(sel css_ast.ComplexSelector, isDeclarationContext b
} else {
// ".foo { & div {} }"
// ".foo { .bar, & div {} }" => ".foo { .bar, div {} }"
// The first iteration of CSS nesting didn't support an implicit "&" if
// the selector started with an identifier. This was done to avoid
// confusion with declarations in the parser, as the parser would
// otherwise need unbounded lookahead to distinguish between the two.
// Consider the following:
//
// span {
// color:blue;
// div:hover span {}
// }
//
// The "color:blue" is a declaration while "div:hover" is actually a
// relative selector in a nested style rule. The parser would need to
// scan to the ";" or "{" to distinguish between the two cases.
// Generally you want to avoid parsers with unbounded lookahead.
//
// However, this edge case is commonly encountered and it's confusing
// that it doesn't work. So the CSS working group later added unbounded
// lookahead to fix it: https://github.com/w3c/csswg-drafts/issues/7961.
//
// We deliberately don't take advantage of that newest syntax addition
// here so that our output still works in browsers before that newest
// syntax addition. We still keep a leading "&" before identifiers.
return canRemoveLeadingAmpersandIfNotFirst
}
}
+26 -3
View File
@@ -606,6 +606,16 @@ func TestColorFunctions(t *testing.T) {
expectPrintedLower(t, "a { color: oklch(0.953 16% 134) }", "a {\n color: #deface;\n}\n", "")
expectPrintedLower(t, "a { color: oklch(0.953 0.064 0.372turn) }", "a {\n color: #deface;\n}\n", "")
// Check gamut mapping for out-of-gamut colors with distinct g/b channels.
// This exercises the binary-search clipping path in "gamut_mapping_xyz_to_srgb",
// which previously called "srgb_to_oklab" with the green and blue arguments
// swapped. That bug was masked by every other test case above because they
// either stay in-gamut or happen to have green ≈ blue, so the swap was a
// no-op. These colors have green and blue far enough apart for the bug to
// produce a visibly different (and less accurate) clipped color.
expectPrintedLower(t, "a { color: oklch(0.65 0.3 50) }", "a {\n color: #e56300;\n color: oklch(0.65 0.3 50);\n}\n", "")
expectPrintedLower(t, "a { color: oklch(0.8 0.3 200) }", "a {\n color: #00dae4;\n color: oklch(0.8 0.3 200);\n}\n", "")
// Test alpha
expectPrintedLower(t, "a { color: color(srgb 0.87 0.98 0.807 / 0.5) }", "a {\n color: rgba(222, 250, 206, .5);\n}\n", "")
expectPrintedLower(t, "a { color: lab(95.38 -15 18 / 0.5) }", "a {\n color: rgba(222, 250, 206, .5);\n}\n", "")
@@ -822,9 +832,9 @@ func TestGradient(t *testing.T) {
expectPrintedMangle(t, code, "a {\n background: "+gradient+"(#ff0, color(display-p3 1 0 0));\n}\n", "")
expectPrintedMinify(t, code, "a{background:"+gradient+"(yellow,color(display-p3 1 0 0))}", "")
expectPrintedLowerUnsupported(t, compat.ColorFunctions, code,
"a {\n background:\n "+gradient+"(\n #ffff00,\n #ffe971,\n #ffd472 25%,\n "+
"#ffab5f,\n #ff7b45 75%,\n #ff5e38 87.5%,\n #ff5534,\n #ff4c30,\n "+
"#ff412c,\n #ff0e0e);\n "+
"a {\n background:\n "+gradient+"(\n #ffff00,\n #ffe96c,\n #ffd363 25%,\n "+
"#ffa852,\n #ff7a42 75%,\n #ff562c 87.5%,\n #ff4e29,\n #ff4528,\n "+
"#ff3a26,\n #ff0e0e);\n "+
"background:\n "+gradient+"(\n #ffff00,\n color(xyz 0.734 0.805 0.111),\n "+
"color(xyz 0.699 0.693 0.087) 25%,\n color(xyz 0.627 0.501 0.048),\n "+
"color(xyz 0.556 0.348 0.019) 75%,\n color(xyz 0.521 0.284 0.009) 87.5%,\n "+
@@ -1137,6 +1147,10 @@ func TestNestedSelector(t *testing.T) {
expectPrintedMangle(t, "a { @media screen { & div { color: red } } }", "a {\n @media screen {\n & div {\n color: red;\n }\n }\n}\n", "")
expectPrintedMangle(t, "a { :has(& b) { color: red } }", "a {\n :has(& b) {\n color: red;\n }\n}\n", "")
expectPrintedMangle(t, "a { :has(& + b) { color: red } }", "a {\n :has(& + b) {\n color: red;\n }\n}\n", "")
expectPrintedMangle(t, "a { & .b:not(& .c) { color: red } }", "a {\n & .b:not(& .c) {\n color: red;\n }\n}\n", "")
expectPrintedMangle(t, "a { & .b:is(.c &) { color: red } }", "a {\n & .b:is(.c &) {\n color: red;\n }\n}\n", "")
expectPrintedMangle(t, "a { & + .b:not(& .c) { color: red } }", "a {\n + .b:not(& .c) {\n color: red;\n }\n}\n", "")
expectPrintedMangle(t, "a { & + .b:is(& .c) { color: red } }", "a {\n + .b:is(& .c) {\n color: red;\n }\n}\n", "")
// Reorder selectors to enable removing "&"
expectPrintedMangle(t, "reorder { & first, .second { color: red } }", "reorder {\n .second,\n first {\n color: red;\n }\n}\n", "")
@@ -1357,6 +1371,15 @@ func TestNestedSelector(t *testing.T) {
"::placeholder {\n opacity: 0.5;\n}\n@layer base {\n ::placeholder {\n color: green;\n }\n}\n", "")
}
func TestCustomPropertyContainingBraces(t *testing.T) {
// CSS custom properties must be able to store JSON-like values
// See: https://github.com/w3c/csswg-drafts/issues/7961
expectPrinted(t, "div { --foo:hover{color:red}zoom:2; }", "div {\n --foo:hover{color:red}zoom:2;\n}\n", "")
expectPrinted(t, "div { --foo:hover { color: red } zoom: 2; }", "div {\n --foo:hover { color: red } zoom: 2;\n}\n", "")
expectPrinted(t, "div { --foo : hover { color : red } zoom : 2 ; }", "div {\n --foo: hover { color : red } zoom : 2 ;\n}\n", "")
expectPrinted(t, "div { --foo + hover { color : red } zoom : 2 ; }", "div {\n --foo + hover {\n color: red;\n }\n zoom: 2;\n}\n", "")
}
func TestBadQualifiedRules(t *testing.T) {
expectPrinted(t, "$bad: rule;", "$bad: rule; {\n}\n", "<stdin>: WARNING: Unexpected \"$\"\n")
expectPrinted(t, "$bad: rule; div { color: red }", "$bad: rule; div {\n color: red;\n}\n", "<stdin>: WARNING: Unexpected \"$\"\n")
+11 -11
View File
@@ -277,19 +277,19 @@ func TestVerbatimWhitespace(t *testing.T) {
expectPrintedMinify(t, "* { --x:[y ]; }", "*{--x:[y ]}")
expectPrintedMinify(t, "* { --x:[ y]; }", "*{--x:[ y]}")
// Note: These cases now behave like qualified rules
expectPrinted(t, "* { --x:{y}; }", "* {\n --x: {\n y;\n }\n}\n")
expectPrinted(t, "* { --x:{y} ; }", "* {\n --x: {\n y;\n }\n}\n")
expectPrinted(t, "* { --x: {y}; }", "* {\n --x: {\n y;\n }\n}\n")
expectPrinted(t, "* { --x:{y }; }", "* {\n --x: {\n y;\n }\n}\n")
expectPrinted(t, "* { --x:{ y}; }", "* {\n --x: {\n y;\n }\n}\n")
// These cases temporarily behaved like qualified rules, but now they're back to being declarations
expectPrinted(t, "* { --x:{y}; }", "* {\n --x:{y};\n}\n")
expectPrinted(t, "* { --x:{y} ; }", "* {\n --x:{y} ;\n}\n")
expectPrinted(t, "* { --x: {y}; }", "* {\n --x: {y};\n}\n")
expectPrinted(t, "* { --x:{y }; }", "* {\n --x:{y };\n}\n")
expectPrinted(t, "* { --x:{ y}; }", "* {\n --x:{ y};\n}\n")
// Note: These cases now behave like qualified rules
// These cases temporarily behaved like qualified rules, but now they're back to being declarations
expectPrintedMinify(t, "* { --x:{y}; }", "*{--x:{y}}")
expectPrintedMinify(t, "* { --x:{y} ; }", "*{--x:{y}}")
expectPrintedMinify(t, "* { --x: {y}; }", "*{--x:{y}}")
expectPrintedMinify(t, "* { --x:{y }; }", "*{--x:{y}}")
expectPrintedMinify(t, "* { --x:{ y}; }", "*{--x:{y}}")
expectPrintedMinify(t, "* { --x:{y} ; }", "*{--x:{y} }")
expectPrintedMinify(t, "* { --x: {y}; }", "*{--x: {y}}")
expectPrintedMinify(t, "* { --x:{y }; }", "*{--x:{y }}")
expectPrintedMinify(t, "* { --x:{ y}; }", "*{--x:{ y}}")
expectPrintedMinify(t, "@supports ( --x : y , z ) { a { color: red; } }", "@supports ( --x : y , z ){a{color:red}}")
expectPrintedMinify(t, "@supports ( --x : ) { a { color: red; } }", "@supports ( --x : ){a{color:red}}")
+1
View File
@@ -43,6 +43,7 @@ type OutputFile struct {
AbsPath string
Contents []byte
IsExecutable bool
CanBeMerged bool
}
type SideEffects struct {
+3
View File
@@ -24,6 +24,9 @@ var builtinTypesLower = map[string]string{
".svg": "image/svg+xml",
".webp": "image/webp",
// Audio
".mp3": "audio/mpeg",
// Fonts
".eot": "application/vnd.ms-fontobject",
".otf": "font/otf",
+4 -3
View File
@@ -540,9 +540,10 @@ type EUnary struct {
}
type EBinary struct {
Left Expr
Right Expr
Op OpCode
Left Expr
Right Expr
Op OpCode
IsParenthesized bool
}
type EBoolean struct{ Value bool }
+122 -83
View File
@@ -42,7 +42,7 @@ type parser struct {
fnOnlyDataVisit fnOnlyDataVisit
allocatedNames []string
currentScope *js_ast.Scope
scopesForCurrentPart []*js_ast.Scope
currentPart *js_ast.Part
symbols []ast.Symbol
astHelpers js_ast.HelperContext
tsUseCounts []uint32
@@ -53,10 +53,6 @@ type parser struct {
exprComments map[logger.Loc][]string
mangledProps map[string]ast.Ref
reservedProps map[string]bool
symbolUses map[ast.Ref]js_ast.SymbolUse
importSymbolPropertyUses map[ast.Ref]map[string]js_ast.SymbolUse
symbolCallUses map[ast.Ref]js_ast.SymbolCallUse
declaredSymbols []js_ast.DeclaredSymbol
globPatternImports []globPatternImport
runtimeImports map[string]ast.LocRef
duplicateCaseChecker duplicateCaseChecker
@@ -115,9 +111,8 @@ type parser struct {
enclosingNamespaceArgRef *ast.Ref
// Imports (both ES6 and CommonJS) are tracked at the top level
importRecords []ast.ImportRecord
importRecordsForCurrentPart []uint32
exportStarImportRecords []uint32
importRecords []ast.ImportRecord
exportStarImportRecords []uint32
// These are for handling ES6 imports and exports
importItemsForNamespace map[ast.Ref]namespaceImportItems
@@ -1735,9 +1730,11 @@ func (p *parser) recordUsage(ref ast.Ref) {
// code regions since those will be culled.
if !p.isControlFlowDead {
p.symbols[ref.InnerIndex].UseCountEstimate++
use := p.symbolUses[ref]
use.CountEstimate++
p.symbolUses[ref] = use
if part := p.currentPart; part != nil {
use := part.SymbolUses[ref]
use.CountEstimate++
part.SymbolUses[ref] = use
}
}
// The correctness of TypeScript-to-JavaScript conversion relies on accurate
@@ -1752,12 +1749,16 @@ func (p *parser) ignoreUsage(ref ast.Ref) {
// Roll back the use count increment in recordUsage()
if !p.isControlFlowDead {
p.symbols[ref.InnerIndex].UseCountEstimate--
use := p.symbolUses[ref]
use.CountEstimate--
if use.CountEstimate == 0 {
delete(p.symbolUses, ref)
} else {
p.symbolUses[ref] = use
// Only remove this usage if we're currently processing a part
if part := p.currentPart; part != nil {
use := part.SymbolUses[ref]
use.CountEstimate--
if use.CountEstimate == 0 {
delete(part.SymbolUses, ref)
} else {
part.SymbolUses[ref] = use
}
}
}
@@ -4262,6 +4263,7 @@ func (p *parser) parseExprCommon(level js_ast.L, errors *deferredErrors, flags e
func (p *parser) parseSuffix(left js_ast.Expr, level js_ast.L, errors *deferredErrors, flags exprFlag) js_ast.Expr {
optionalChain := js_ast.OptionalChainNone
afterAsLoc := logger.Loc{Start: -1}
for {
if p.lexer.Loc() == p.afterArrowBodyLoc {
@@ -4281,9 +4283,11 @@ func (p *parser) parseSuffix(left js_ast.Expr, level js_ast.L, errors *deferredE
}
// Stop now if this token is forbidden to follow a TypeScript "as" cast
if p.lexer.Loc() == p.forbidSuffixAfterAsLoc {
operatorRange := p.lexer.Range()
if operatorRange.Loc == p.forbidSuffixAfterAsLoc {
return left
}
isAfterAs := operatorRange.Loc == afterAsLoc
// Reset the optional chain flag by default. That way we won't accidentally
// treat "c.d" as OptionalChainContinue in "a?.b + c.d".
@@ -4915,7 +4919,7 @@ func (p *parser) parseSuffix(left js_ast.Expr, level js_ast.L, errors *deferredE
if e, ok := left.Data.(*js_ast.EUnary); ok && e.Op == js_ast.UnOpNot {
r := logger.Range{Loc: left.Loc, Len: p.source.LocBeforeWhitespace(p.lexer.Loc()).Start - left.Loc.Start}
data := p.tracker.MsgData(r, "Suspicious use of the \"!\" operator inside the \"in\" operator")
data.Location.Suggestion = fmt.Sprintf("(%s)", p.source.TextForRange(r))
data.Location.Suggestion = textForParenthesesSuggestion(p.source.TextForRange(r))
p.log.AddMsgID(logger.MsgID_JS_SuspiciousBooleanNot, logger.Msg{
Kind: kind,
Data: data,
@@ -4941,7 +4945,7 @@ func (p *parser) parseSuffix(left js_ast.Expr, level js_ast.L, errors *deferredE
if e, ok := left.Data.(*js_ast.EUnary); ok && e.Op == js_ast.UnOpNot {
r := logger.Range{Loc: left.Loc, Len: p.source.LocBeforeWhitespace(p.lexer.Loc()).Start - left.Loc.Start}
data := p.tracker.MsgData(r, "Suspicious use of the \"!\" operator inside the \"instanceof\" operator")
data.Location.Suggestion = fmt.Sprintf("(%s)", p.source.TextForRange(r))
data.Location.Suggestion = textForParenthesesSuggestion(p.source.TextForRange(r))
p.log.AddMsgID(logger.MsgID_JS_SuspiciousBooleanNot, logger.Msg{
Kind: kind,
Data: data,
@@ -4976,11 +4980,39 @@ func (p *parser) parseSuffix(left js_ast.Expr, level js_ast.L, errors *deferredE
p.forbidSuffixAfterAsLoc = p.lexer.Loc()
return left
}
afterAsLoc = p.lexer.Loc()
continue
}
return left
}
// See: https://github.com/microsoft/TypeScript/issues/63527
if isAfterAs {
if binary, ok := left.Data.(*js_ast.EBinary); ok {
if binaryLeft, ok := binary.Left.Data.(*js_ast.EBinary); ok && !binaryLeft.IsParenthesized {
rightLevel := js_ast.OpTable[binary.Op].Level
if binary.Op.IsRightAssociative() {
rightLevel++
}
if rightLevel > js_ast.OpTable[binaryLeft.Op].Level {
start := binary.Left.Loc
end := p.source.LocBeforeWhitespace(operatorRange.Loc)
wrapRange := logger.Range{Loc: start, Len: end.Start - start.Start}
note := logger.MsgData{Text: "This is a syntax error in newer versions of TypeScript because the type cast has unintuitive precedence in this case."}
if text := p.source.TextForRange(wrapRange); !strings.ContainsRune(text, '\n') {
note = p.tracker.MsgData(wrapRange, note.Text+" Surround the inner expression in parentheses to silence this warning:")
note.Location.Suggestion = textForParenthesesSuggestion(text)
}
p.log.AddIDWithNotes(logger.MsgID_JS_ConfusingTypeScriptCast, logger.Warning, &p.tracker, operatorRange,
fmt.Sprintf("Operator %q should not directly follow a TypeScript type cast after the %q operator",
js_ast.OpTable[binary.Op].Text, js_ast.OpTable[binaryLeft.Op].Text),
[]logger.MsgData{note},
)
}
}
}
}
}
}
@@ -7019,7 +7051,7 @@ loop:
var notes []logger.MsgData
if text := p.source.TextForRange(wrapRange); !strings.ContainsRune(text, '\n') {
note := p.tracker.MsgData(wrapRange, "Wrap this decorator in parentheses to allow arbitrary expressions:")
note.Location.Suggestion = fmt.Sprintf("(%s)", text)
note.Location.Suggestion = textForParenthesesSuggestion(text)
notes = []logger.MsgData{note}
}
p.log.AddMsg(logger.Msg{
@@ -7032,6 +7064,14 @@ loop:
return memberExpr
}
func textForParenthesesSuggestion(text string) string {
count := utf8.RuneCountInString(text) - 2
if count < 1 {
count = 1
}
return fmt.Sprintf("(%s)", strings.Repeat(" ", count))
}
type lexicalDecl uint8
const (
@@ -8627,7 +8667,10 @@ func (p *parser) pushScopeForVisitPass(kind js_ast.ScopeKind, loc logger.Loc) {
p.scopesInOrder = p.scopesInOrder[1:]
p.currentScope = order.scope
p.scopesForCurrentPart = append(p.scopesForCurrentPart, order.scope)
if part := p.currentPart; part != nil {
part.Scopes = append(part.Scopes, order.scope)
}
}
type findSymbolResult struct {
@@ -10027,7 +10070,7 @@ func (p *parser) recordDeclaredSymbol(ref ast.Ref) {
}
}
p.declaredSymbols = append(p.declaredSymbols, js_ast.DeclaredSymbol{
p.currentPart.DeclaredSymbols = append(p.currentPart.DeclaredSymbols, js_ast.DeclaredSymbol{
Ref: ref,
IsTopLevel: isTopLevel,
})
@@ -11867,6 +11910,8 @@ func (p *parser) markExprAsParenthesized(value js_ast.Expr, openParenLoc logger.
e.IsParenthesized = true
case *js_ast.EArrow:
e.IsParenthesized = true
case *js_ast.EBinary:
e.IsParenthesized = true
}
}
@@ -13084,22 +13129,25 @@ func (p *parser) maybeRewritePropertyAccess(
if p.options.mode == config.ModeBundle && !p.isControlFlowDead {
if id, ok := target.Data.(*js_ast.EImportIdentifier); ok {
// Remove the normal symbol use
use := p.symbolUses[id.Ref]
symbolUses := p.currentPart.SymbolUses
use := symbolUses[id.Ref]
use.CountEstimate--
if use.CountEstimate == 0 {
delete(p.symbolUses, id.Ref)
delete(symbolUses, id.Ref)
} else {
p.symbolUses[id.Ref] = use
symbolUses[id.Ref] = use
}
// Add a special symbol use instead
if p.importSymbolPropertyUses == nil {
p.importSymbolPropertyUses = make(map[ast.Ref]map[string]js_ast.SymbolUse)
importSymbolPropertyUses := p.currentPart.ImportSymbolPropertyUses
if importSymbolPropertyUses == nil {
importSymbolPropertyUses = make(map[ast.Ref]map[string]js_ast.SymbolUse)
p.currentPart.ImportSymbolPropertyUses = importSymbolPropertyUses
}
properties := p.importSymbolPropertyUses[id.Ref]
properties := importSymbolPropertyUses[id.Ref]
if properties == nil {
properties = make(map[string]js_ast.SymbolUse)
p.importSymbolPropertyUses[id.Ref] = properties
importSymbolPropertyUses[id.Ref] = properties
}
use = properties[name]
use.CountEstimate++
@@ -15122,7 +15170,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
record.Flags |= ast.HandlesImportErrors
record.ErrorHandlerLoc = p.thenCatchChain.catchLoc
}
p.importRecordsForCurrentPart = append(p.importRecordsForCurrentPart, importRecordIndex)
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, importRecordIndex)
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EImportString{
ImportRecordIndex: importRecordIndex,
CloseParenLoc: e.CloseParenLoc,
@@ -15492,7 +15540,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
record.Flags |= ast.HandlesImportErrors
record.ErrorHandlerLoc = p.fnOrArrowDataVisit.tryCatchLoc
}
p.importRecordsForCurrentPart = append(p.importRecordsForCurrentPart, importRecordIndex)
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, importRecordIndex)
// Create a new expression to represent the operation
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ERequireResolveString{
@@ -15710,7 +15758,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
record.Flags |= ast.HandlesImportErrors
record.ErrorHandlerLoc = p.fnOrArrowDataVisit.tryCatchLoc
}
p.importRecordsForCurrentPart = append(p.importRecordsForCurrentPart, importRecordIndex)
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, importRecordIndex)
// Currently "require" is not converted into "import" for ESM
if p.options.mode != config.ModeBundle && p.options.outputFormat == config.FormatESModule && !omitWarnings {
@@ -16785,24 +16833,27 @@ func (p *parser) globPatternFromExpr(expr js_ast.Expr) ([]globPart, logger.Range
func (p *parser) convertSymbolUseToCall(ref ast.Ref, isSingleNonSpreadArgCall bool) {
// Remove the normal symbol use
use := p.symbolUses[ref]
symbolUses := p.currentPart.SymbolUses
use := symbolUses[ref]
use.CountEstimate--
if use.CountEstimate == 0 {
delete(p.symbolUses, ref)
delete(symbolUses, ref)
} else {
p.symbolUses[ref] = use
symbolUses[ref] = use
}
// Add a special symbol use instead
if p.symbolCallUses == nil {
p.symbolCallUses = make(map[ast.Ref]js_ast.SymbolCallUse)
symbolCallUses := p.currentPart.SymbolCallUses
if symbolCallUses == nil {
symbolCallUses = make(map[ast.Ref]js_ast.SymbolCallUse)
p.currentPart.SymbolCallUses = symbolCallUses
}
callUse := p.symbolCallUses[ref]
callUse := symbolCallUses[ref]
callUse.CallCountEstimate++
if isSingleNonSpreadArgCall {
callUse.SingleArgNonSpreadCallCountEstimate++
}
p.symbolCallUses[ref] = callUse
symbolCallUses[ref] = callUse
}
func (p *parser) warnAboutImportNamespaceCall(target js_ast.Expr, kind importNamespaceCallKind) {
@@ -17468,7 +17519,7 @@ func (p *parser) scanForImportsAndExports(stmts []js_ast.Stmt) (result importsEx
}
// Also record these automatically-generated top-level namespace alias symbols
p.declaredSymbols = append(p.declaredSymbols, js_ast.DeclaredSymbol{
p.currentPart.DeclaredSymbols = append(p.currentPart.DeclaredSymbols, js_ast.DeclaredSymbol{
Ref: name.Ref,
IsTopLevel: true,
})
@@ -17506,7 +17557,7 @@ func (p *parser) scanForImportsAndExports(stmts []js_ast.Stmt) (result importsEx
}
}
p.importRecordsForCurrentPart = append(p.importRecordsForCurrentPart, s.ImportRecordIndex)
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, s.ImportRecordIndex)
if s.StarNameLoc != nil {
record.Flags |= ast.ContainsImportStar
@@ -17557,7 +17608,7 @@ func (p *parser) scanForImportsAndExports(stmts []js_ast.Stmt) (result importsEx
case *js_ast.SExportStar:
record := &p.importRecords[s.ImportRecordIndex]
p.importRecordsForCurrentPart = append(p.importRecordsForCurrentPart, s.ImportRecordIndex)
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, s.ImportRecordIndex)
if s.Alias != nil {
// "export * as ns from 'path'"
@@ -17578,7 +17629,7 @@ func (p *parser) scanForImportsAndExports(stmts []js_ast.Stmt) (result importsEx
case *js_ast.SExportFrom:
record := &p.importRecords[s.ImportRecordIndex]
p.importRecordsForCurrentPart = append(p.importRecordsForCurrentPart, s.ImportRecordIndex)
p.currentPart.ImportRecordIndices = append(p.currentPart.ImportRecordIndices, s.ImportRecordIndex)
for _, item := range s.Items {
// Note that the imported alias is not item.Alias, which is the
@@ -17629,17 +17680,11 @@ func (p *parser) scanForImportsAndExports(stmts []js_ast.Stmt) (result importsEx
}
func (p *parser) appendPart(parts []js_ast.Part, stmts []js_ast.Stmt) []js_ast.Part {
p.symbolUses = make(map[ast.Ref]js_ast.SymbolUse)
p.importSymbolPropertyUses = nil
p.symbolCallUses = nil
p.declaredSymbols = nil
p.importRecordsForCurrentPart = nil
p.scopesForCurrentPart = nil
part := js_ast.Part{
Stmts: p.visitStmtsAndPrependTempRefs(stmts, prependTempRefsOpts{}),
SymbolUses: p.symbolUses,
SymbolUses: make(map[ast.Ref]js_ast.SymbolUse),
}
p.currentPart = &part
part.Stmts = p.visitStmtsAndPrependTempRefs(stmts, prependTempRefsOpts{})
// Sanity check
if p.currentScope != p.moduleScope {
@@ -17681,13 +17726,11 @@ func (p *parser) appendPart(parts []js_ast.Part, stmts []js_ast.Stmt) []js_ast.P
flags |= js_ast.KeepExportClauses
}
part.CanBeRemovedIfUnused = p.astHelpers.StmtsCanBeRemovedIfUnused(part.Stmts, flags)
part.DeclaredSymbols = p.declaredSymbols
part.ImportRecordIndices = p.importRecordsForCurrentPart
part.ImportSymbolPropertyUses = p.importSymbolPropertyUses
part.SymbolCallUses = p.symbolCallUses
part.Scopes = p.scopesForCurrentPart
parts = append(parts, part)
}
// Reset the state for this part so we don't accidentally mutate it
p.currentPart = nil
return parts
}
@@ -18098,9 +18141,14 @@ func LazyExportAST(log logger.Log, source logger.Source, options Options, expr j
p := newParser(log, source, js_lexer.Lexer{}, &options)
p.prepareForVisitPass()
// Defer the actual code generation until linking
part := js_ast.Part{
SymbolUses: make(map[ast.Ref]js_ast.SymbolUse),
}
// Optionally call a runtime API function to transform the expression
if helperCall != nil {
p.symbolUses = make(map[ast.Ref]js_ast.SymbolUse)
p.currentPart = &part
if len(helperCall.Global) > 0 {
ref := p.newSymbol(ast.SymbolUnbound, helperCall.Global[0])
p.recordUsage(ref)
@@ -18114,7 +18162,9 @@ func LazyExportAST(log logger.Log, source logger.Source, options Options, expr j
} else {
expr = p.callRuntime(expr.Loc, helperCall.Runtime, []js_ast.Expr{expr})
}
p.currentPart = nil
}
part.Stmts = []js_ast.Stmt{{Loc: expr.Loc, Data: &js_ast.SLazyExport{Value: expr}}}
// Add an empty part for the namespace export that we can fill in later
nsExportPart := js_ast.Part{
@@ -18122,13 +18172,6 @@ func LazyExportAST(log logger.Log, source logger.Source, options Options, expr j
CanBeRemovedIfUnused: true,
}
// Defer the actual code generation until linking
part := js_ast.Part{
Stmts: []js_ast.Stmt{{Loc: expr.Loc, Data: &js_ast.SLazyExport{Value: expr}}},
SymbolUses: p.symbolUses,
}
p.symbolUses = nil
ast := p.toAST([]js_ast.Part{nsExportPart}, []js_ast.Part{part}, nil, "", nil)
ast.HasLazyExport = true
return ast
@@ -18157,22 +18200,22 @@ func GlobResolveAST(log logger.Log, source logger.Source, importRecords []ast.Im
importRecordIndices = append(importRecordIndices, uint32(importRecordIndex))
}
p.symbolUses = make(map[ast.Ref]js_ast.SymbolUse)
ref := p.newSymbol(ast.SymbolOther, name)
p.moduleScope.Generated = append(p.moduleScope.Generated, ref)
part := js_ast.Part{
Stmts: []js_ast.Stmt{{Data: &js_ast.SLocal{
IsExport: true,
Decls: []js_ast.Decl{{
Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: ref}},
ValueOrNil: p.callRuntime(logger.Loc{}, "__glob", []js_ast.Expr{{Data: object}}),
}},
}}},
ImportRecordIndices: importRecordIndices,
SymbolUses: p.symbolUses,
SymbolUses: make(map[ast.Ref]js_ast.SymbolUse),
}
p.symbolUses = nil
p.currentPart = &part
part.Stmts = []js_ast.Stmt{{Data: &js_ast.SLocal{
IsExport: true,
Decls: []js_ast.Decl{{
Binding: js_ast.Binding{Data: &js_ast.BIdentifier{Ref: ref}},
ValueOrNil: p.callRuntime(logger.Loc{}, "__glob", []js_ast.Expr{{Data: object}}),
}},
}}}
p.currentPart = nil
p.esmExportKeyword.Len = 1
return p.toAST([]js_ast.Part{nsExportPart}, []js_ast.Part{part}, nil, "", nil)
@@ -18628,17 +18671,13 @@ func (p *parser) toAST(before, parts, after []js_ast.Part, hashbang string, dire
removedImportEquals := false
partsEnd := 0
for partIndex, part := range parts {
p.importRecordsForCurrentPart = nil
p.declaredSymbols = nil
p.currentPart = &part
result := p.scanForImportsAndExports(part.Stmts)
p.currentPart = nil
part.Stmts = result.stmts
keptImportEquals = keptImportEquals || result.keptImportEquals
removedImportEquals = removedImportEquals || result.removedImportEquals
part.ImportRecordIndices = append(part.ImportRecordIndices, p.importRecordsForCurrentPart...)
part.DeclaredSymbols = append(part.DeclaredSymbols, p.declaredSymbols...)
if len(part.Stmts) > 0 || uint32(partIndex) == js_ast.NSExportPartIndex {
if p.moduleScope.ContainsDirectEval && len(part.DeclaredSymbols) > 0 {
// If this file contains a direct call to "eval()", all parts that
+23 -3
View File
@@ -382,7 +382,7 @@ func (p *parser) lowerFunction(
// Forward the arguments to the wrapper function
usesArgumentsRef := !isArrow && p.fnOnlyDataVisit.argumentsRef != nil &&
p.symbolUses[*p.fnOnlyDataVisit.argumentsRef].CountEstimate > 0
p.currentPart.SymbolUses[*p.fnOnlyDataVisit.argumentsRef].CountEstimate > 0
var forwardedArgs js_ast.Expr
if !couldThrowErrors && !usesArgumentsRef {
// Simple case: the arguments can stay on the outer function. It's
@@ -849,6 +849,26 @@ func (p *parser) lowerAssignmentOperator(value js_ast.Expr, callback func(js_ast
}
case *js_ast.EIdentifier:
// Make sure we record this usage in the usage count so that duplicating
// a single-use reference means it's no longer considered a single-use
// reference. Otherwise the single-use reference inlining code may
// incorrectly inline the initializer into the first reference, leaving
// the second reference without a definition. For example:
//
// let x;
// x ||= {};
//
// We're changing that to this:
//
// let x;
// x || (x = {});
//
// And we don't want to believe that "x" still has a single reference, as
// then we may incorrectly inline the undefined value like this:
//
// void 0 || (x = {});
//
p.recordUsage(left.Ref)
return callback(
js_ast.Expr{Loc: value.Loc, Data: &js_ast.EIdentifier{Ref: left.Ref}},
value,
@@ -2011,7 +2031,7 @@ func (ctx *lowerUsingDeclarationContext) finalize(p *parser, stmts []js_ast.Stmt
}
isTopLevel := scope == p.moduleScope
scope.Generated = append(scope.Generated, ctx.stackRef, caughtRef, errorRef, hasErrorRef)
p.declaredSymbols = append(p.declaredSymbols,
p.currentPart.DeclaredSymbols = append(p.currentPart.DeclaredSymbols,
js_ast.DeclaredSymbol{IsTopLevel: isTopLevel, Ref: ctx.stackRef},
js_ast.DeclaredSymbol{IsTopLevel: isTopLevel, Ref: caughtRef},
js_ast.DeclaredSymbol{IsTopLevel: isTopLevel, Ref: errorRef},
@@ -2034,7 +2054,7 @@ func (ctx *lowerUsingDeclarationContext) finalize(p *parser, stmts []js_ast.Stmt
if ctx.hasAwaitUsing {
promiseRef := p.generateTempRef(tempRefNoDeclare, "_promise")
scope.Generated = append(scope.Generated, promiseRef)
p.declaredSymbols = append(p.declaredSymbols, js_ast.DeclaredSymbol{IsTopLevel: isTopLevel, Ref: promiseRef})
p.currentPart.DeclaredSymbols = append(p.currentPart.DeclaredSymbols, js_ast.DeclaredSymbol{IsTopLevel: isTopLevel, Ref: promiseRef})
// "await" expressions turn into "yield" expressions when lowering
p.recordUsage(promiseRef)
@@ -160,6 +160,9 @@ _x = new WeakMap();
// Temporary variables should not come before "use strict"
expectPrintedTarget(t, 2019, "function f() { /*! @license */ 'use strict'; a.b ??= c.d }",
"function f() {\n /*! @license */\n \"use strict\";\n var _a;\n (_a = a.b) != null ? _a : a.b = c.d;\n}\n")
// https://github.com/evanw/esbuild/issues/4508
expectPrintedMangleTarget(t, 2020, "function foo() { let x; x ??= {} }", "function foo() {\n let x;\n x ?? (x = {});\n}\n")
}
func TestLowerLogicalAssign(t *testing.T) {
@@ -229,6 +232,10 @@ class Foo {
}
_x = new WeakMap();
`)
// https://github.com/evanw/esbuild/issues/4508
expectPrintedMangleTarget(t, 2020, "function foo() { let x; x ||= {} }", "function foo() {\n let x;\n x || (x = {});\n}\n")
expectPrintedMangleTarget(t, 2020, "function foo() { let x; x &&= {} }", "function foo() {\n let x;\n x && (x = {});\n}\n")
}
func TestLowerAsyncFunctions(t *testing.T) {
+31
View File
@@ -3324,3 +3324,34 @@ func TestTSUsing(t *testing.T) {
expectParseErrorTS(t, "export using x: any = y", "<stdin>: ERROR: Unexpected \"using\"\n")
expectParseErrorTS(t, "namespace ns { export using x: any = y }", "<stdin>: ERROR: Unexpected \"using\"\n")
}
func TestTSWarningBinaryOperatorAfterCast(t *testing.T) {
note := "<stdin>: NOTE: This is a syntax error in newer versions of TypeScript because the type cast has unintuitive precedence in this case. " +
"Surround the inner expression in parentheses to silence this warning:\n"
// See: https://github.com/microsoft/TypeScript/issues/63527
expectParseErrorTS(t, "1 as number + 2", "")
expectParseErrorTS(t, "1 + 2 as number + 3", "")
expectParseErrorTS(t, "1 + 2 as number - 3", "")
expectParseErrorTS(t, "1 * 2 as number * 3", "")
expectParseErrorTS(t, "1 * 2 as number + 3", "")
expectParseErrorTS(t, "1 + 2 as number * 3", "<stdin>: WARNING: Operator \"*\" should not directly follow a TypeScript type cast after the \"+\" operator\n"+note)
expectParseErrorTS(t, "(1 + 2) as number * 3", "")
expectParseErrorTS(t, "(1 + 2 as number) * 3", "")
expectParseErrorTS(t, "1 satisfies number + 2", "")
expectParseErrorTS(t, "1 + 2 satisfies number + 3", "")
expectParseErrorTS(t, "1 + 2 satisfies number - 3", "")
expectParseErrorTS(t, "1 * 2 satisfies number * 3", "")
expectParseErrorTS(t, "1 * 2 satisfies number + 3", "")
expectParseErrorTS(t, "1 + 2 satisfies number * 3", "<stdin>: WARNING: Operator \"*\" should not directly follow a TypeScript type cast after the \"+\" operator\n"+note)
expectParseErrorTS(t, "(1 + 2) satisfies number * 3", "")
expectParseErrorTS(t, "(1 + 2 satisfies number) * 3", "")
// See: https://github.com/microsoft/TypeScript/issues/63661
expectParseErrorTS(t, "1 ** 2 as number ** 3", "<stdin>: WARNING: Operator \"**\" should not directly follow a TypeScript type cast after the \"**\" operator\n"+note)
expectParseErrorTS(t, "(1 ** 2) as number ** 3", "")
expectParseErrorTS(t, "(1 ** 2 as number) ** 3", "")
expectParseErrorTS(t, "1 ** 2 satisfies number ** 3", "<stdin>: WARNING: Operator \"**\" should not directly follow a TypeScript type cast after the \"**\" operator\n"+note)
expectParseErrorTS(t, "(1 ** 2) satisfies number ** 3", "")
expectParseErrorTS(t, "(1 ** 2 satisfies number) ** 3", "")
}
+5
View File
@@ -663,6 +663,7 @@ func (c *linkerContext) generateChunksInParallel(additionalFiles []graph.OutputF
// output directory. This is used by the "file" and "copy" loaders.
var commentPrefix string
var commentSuffix string
var canBeMerged bool
switch chunkRepr := chunk.chunkRepr.(type) {
case *chunkReprJS:
for _, sourceIndex := range chunkRepr.filesInChunkInOrder {
@@ -678,6 +679,7 @@ func (c *linkerContext) generateChunksInParallel(additionalFiles []graph.OutputF
}
commentPrefix = "/*"
commentSuffix = " */"
canBeMerged = true
}
// Path substitution for the chunk itself
@@ -708,6 +710,7 @@ func (c *linkerContext) generateChunksInParallel(additionalFiles []graph.OutputF
JSONMetadataChunk: fmt.Sprintf(
c.options.MetafileFormat.MaybeRemoveWhitespace("{\n \"imports\": [],\n \"exports\": [],\n \"inputs\": {},\n \"bytes\": %d\n }"),
len(chunk.externalLegalComments)),
CanBeMerged: true,
})
}
@@ -747,6 +750,7 @@ func (c *linkerContext) generateChunksInParallel(additionalFiles []graph.OutputF
JSONMetadataChunk: fmt.Sprintf(
c.options.MetafileFormat.MaybeRemoveWhitespace("{\n \"imports\": [],\n \"exports\": [],\n \"inputs\": {},\n \"bytes\": %d\n }"),
len(outputSourceMap)),
CanBeMerged: true,
})
}
}
@@ -771,6 +775,7 @@ func (c *linkerContext) generateChunksInParallel(additionalFiles []graph.OutputF
Contents: outputContents,
JSONMetadataChunk: jsonMetadataChunk,
IsExecutable: chunk.isExecutable,
CanBeMerged: canBeMerged,
})
results[chunkIndex] = outputFiles
+16 -443
View File
@@ -839,6 +839,7 @@ func OutputOptionsForArgs(osArgs []string) OutputOptions {
options.Color = ColorNever
case "--color=true", "--color":
options.Color = ColorAlways
case "--log-level=info":
options.LogLevel = LevelInfo
case "--log-level=warning":
@@ -847,6 +848,13 @@ func OutputOptionsForArgs(osArgs []string) OutputOptions {
options.LogLevel = LevelError
case "--log-level=silent":
options.LogLevel = LevelSilent
case "--log-style=default":
options.LogStyle = StyleDefault
case "--log-level=clang":
options.LogStyle = StyleClang
case "--log-level=visualstudio":
options.LogStyle = StyleVisualStudio
}
}
@@ -1202,275 +1210,27 @@ type OutputOptions struct {
IncludeSource bool
Color UseColor
LogLevel LogLevel
LogStyle LogStyle
PathStyle PathStyle
Overrides map[MsgID]LogLevel
}
func (msg Msg) String(options OutputOptions, terminalInfo TerminalInfo) string {
// Format the message
var text strings.Builder
text.WriteString(msgString(options.IncludeSource, options.PathStyle, terminalInfo, msg.ID, msg.Kind, msg.Data, msg.PluginName))
switch options.LogStyle {
case StyleClang:
return msgToStringClang(msg, options, terminalInfo)
// Format the notes
var oldData MsgData
for i, note := range msg.Notes {
if options.IncludeSource && (i == 0 || strings.IndexByte(oldData.Text, '\n') >= 0 || oldData.Location != nil) {
text.WriteString("\n")
}
text.WriteString(msgString(options.IncludeSource, options.PathStyle, terminalInfo, MsgID_None, Note, note, ""))
oldData = note
}
case StyleVisualStudio:
return msgToStringVisualStudio(msg, options, terminalInfo)
// Add extra spacing between messages if source code is present
if options.IncludeSource {
text.WriteString("\n")
default:
return msgToStringDefault(msg, options, terminalInfo)
}
return text.String()
}
// The number of margin characters in addition to the line number
const extraMarginChars = 9
func marginWithLineText(maxMargin int, line int) string {
number := fmt.Sprintf("%d", line)
return fmt.Sprintf(" %s%s │ ", strings.Repeat(" ", maxMargin-len(number)), number)
}
func emptyMarginText(maxMargin int, isLast bool) string {
space := strings.Repeat(" ", maxMargin)
if isLast {
return fmt.Sprintf(" %s ╵ ", space)
}
return fmt.Sprintf(" %s │ ", space)
}
func msgString(includeSource bool, pathStyle PathStyle, terminalInfo TerminalInfo, id MsgID, kind MsgKind, data MsgData, pluginName string) string {
if !includeSource {
if loc := data.Location; loc != nil {
return fmt.Sprintf("%s: %s: %s\n", loc.File.Select(pathStyle), kind.String(), data.Text)
}
return fmt.Sprintf("%s: %s\n", kind.String(), data.Text)
}
var colors Colors
if terminalInfo.UseColorEscapes {
colors = TerminalColors
}
var iconColor string
var kindColorBrackets string
var kindColorText string
location := ""
if data.Location != nil {
maxMargin := len(fmt.Sprintf("%d", data.Location.Line))
d := detailStruct(data, pathStyle, terminalInfo, maxMargin)
if d.Suggestion != "" {
location = fmt.Sprintf("\n %s:%d:%d:\n%s%s%s%s%s%s\n%s%s%s%s%s\n%s%s%s%s%s\n%s",
d.Path, d.Line, d.Column,
colors.Dim, d.SourceBefore, colors.Green, d.SourceMarked, colors.Dim, d.SourceAfter,
emptyMarginText(maxMargin, false), d.Indent, colors.Green, d.Marker, colors.Dim,
emptyMarginText(maxMargin, true), d.Indent, colors.Green, d.Suggestion, colors.Reset,
d.ContentAfter,
)
} else {
location = fmt.Sprintf("\n %s:%d:%d:\n%s%s%s%s%s%s\n%s%s%s%s%s\n%s",
d.Path, d.Line, d.Column,
colors.Dim, d.SourceBefore, colors.Green, d.SourceMarked, colors.Dim, d.SourceAfter,
emptyMarginText(maxMargin, true), d.Indent, colors.Green, d.Marker, colors.Reset,
d.ContentAfter,
)
}
}
switch kind {
case Verbose:
iconColor = colors.Cyan
kindColorBrackets = colors.CyanBgCyan
kindColorText = colors.CyanBgBlack
case Debug:
iconColor = colors.Green
kindColorBrackets = colors.GreenBgGreen
kindColorText = colors.GreenBgWhite
case Info:
iconColor = colors.Blue
kindColorBrackets = colors.BlueBgBlue
kindColorText = colors.BlueBgWhite
case Error:
iconColor = colors.Red
kindColorBrackets = colors.RedBgRed
kindColorText = colors.RedBgWhite
case Warning:
iconColor = colors.Yellow
kindColorBrackets = colors.YellowBgYellow
kindColorText = colors.YellowBgBlack
case Note:
sb := strings.Builder{}
for _, line := range strings.Split(data.Text, "\n") {
// Special-case word wrapping
if wrapWidth := terminalInfo.Width; wrapWidth > 2 {
if !data.DisableMaximumWidth && wrapWidth > 100 {
wrapWidth = 100 // Enforce a maximum paragraph width for readability
}
for _, run := range wrapWordsInString(line, wrapWidth-2) {
sb.WriteString(" ")
sb.WriteString(linkifyText(run, colors.Underline, colors.Reset))
sb.WriteByte('\n')
}
continue
}
// Otherwise, just write an indented line
sb.WriteString(" ")
sb.WriteString(linkifyText(line, colors.Underline, colors.Reset))
sb.WriteByte('\n')
}
sb.WriteString(location)
return sb.String()
}
if pluginName != "" {
pluginName = fmt.Sprintf(" %s%s[plugin %s]%s", colors.Bold, colors.Magenta, pluginName, colors.Reset)
}
msgID := MsgIDToString(id)
if msgID != "" {
msgID = fmt.Sprintf(" [%s]", msgID)
}
return fmt.Sprintf("%s%s %s[%s%s%s]%s %s%s%s%s%s\n%s",
iconColor, kind.Icon(),
kindColorBrackets, kindColorText, kind.String(), kindColorBrackets, colors.Reset,
colors.Bold, data.Text, colors.Reset, pluginName, msgID,
location,
)
}
func linkifyText(text string, underline string, reset string) string {
if underline == "" {
return text
}
https := strings.Index(text, "https://")
if https == -1 {
return text
}
sb := strings.Builder{}
for {
https := strings.Index(text, "https://")
if https == -1 {
break
}
end := strings.IndexByte(text[https:], ' ')
if end == -1 {
end = len(text)
} else {
end += https
}
// Remove trailing punctuation
if end > https {
switch text[end-1] {
case '.', ',', '?', '!', ')', ']', '}':
end--
}
}
sb.WriteString(text[:https])
sb.WriteString(underline)
sb.WriteString(text[https:end])
sb.WriteString(reset)
text = text[end:]
}
sb.WriteString(text)
return sb.String()
}
func wrapWordsInString(text string, width int) []string {
runs := []string{}
outer:
for text != "" {
i := 0
x := 0
wordEndI := 0
// Skip over any leading spaces
for i < len(text) && text[i] == ' ' {
i++
x++
}
// Find out how many words will fit in this run
for i < len(text) {
oldWordEndI := wordEndI
wordStartI := i
// Find the end of the word
for i < len(text) {
c, width := utf8.DecodeRuneInString(text[i:])
if c == ' ' {
break
}
i += width
x += 1 // Naively assume that each unicode code point is a single column
}
wordEndI = i
// Split into a new run if this isn't the first word in the run and the end is past the width
if wordStartI > 0 && x > width {
runs = append(runs, text[:oldWordEndI])
text = text[wordStartI:]
continue outer
}
// Skip over any spaces after the word
for i < len(text) && text[i] == ' ' {
i++
x++
}
}
// If we get here, this is the last run (i.e. everything fits)
break
}
// Remove any trailing spaces on the last run
for len(text) > 0 && text[len(text)-1] == ' ' {
text = text[:len(text)-1]
}
runs = append(runs, text)
return runs
}
type MsgDetail struct {
SourceBefore string
SourceMarked string
SourceAfter string
Indent string
Marker string
Suggestion string
ContentAfter string
Path string
Line int
Column int
}
// It's not common for large files to have many warnings. But when it happens,
// we want to make sure that it's not too slow. Source code locations are
// represented as byte offsets for compactness but transforming these to
@@ -1639,193 +1399,6 @@ func (tracker *LineColumnTracker) MsgLocationOrNil(r Range) *MsgLocation {
}
}
func detailStruct(data MsgData, pathStyle PathStyle, terminalInfo TerminalInfo, maxMargin int) MsgDetail {
// Only highlight the first line of the line text
loc := *data.Location
endOfFirstLine := len(loc.LineText)
// Note: This uses "IndexByte" because Go implements this with SIMD, which
// can matter a lot for really long lines. Some people pass huge >100mb
// minified files as line text for the log message.
if i := strings.IndexByte(loc.LineText, '\n'); i >= 0 {
endOfFirstLine = i
}
firstLine := loc.LineText[:endOfFirstLine]
afterFirstLine := loc.LineText[endOfFirstLine:]
if afterFirstLine != "" && !strings.HasSuffix(afterFirstLine, "\n") {
afterFirstLine += "\n"
}
// Clamp values in range
if loc.Line < 0 {
loc.Line = 0
}
if loc.Column < 0 {
loc.Column = 0
}
if loc.Length < 0 {
loc.Length = 0
}
if loc.Column > endOfFirstLine {
loc.Column = endOfFirstLine
}
if loc.Length > endOfFirstLine-loc.Column {
loc.Length = endOfFirstLine - loc.Column
}
spacesPerTab := 2
lineText := renderTabStops(firstLine, spacesPerTab)
textUpToLoc := renderTabStops(firstLine[:loc.Column], spacesPerTab)
markerStart := len(textUpToLoc)
markerEnd := markerStart
indent := strings.Repeat(" ", estimateWidthInTerminal(textUpToLoc))
marker := "^"
// Extend markers to cover the full range of the error
if loc.Length > 0 {
markerEnd = len(renderTabStops(firstLine[:loc.Column+loc.Length], spacesPerTab))
}
// Clip the marker to the bounds of the line
if markerStart > len(lineText) {
markerStart = len(lineText)
}
if markerEnd > len(lineText) {
markerEnd = len(lineText)
}
if markerEnd < markerStart {
markerEnd = markerStart
}
// Trim the line to fit the terminal width
width := terminalInfo.Width
if width < 1 {
width = defaultTerminalWidth
}
width -= maxMargin + extraMarginChars
if width < 1 {
width = 1
}
if loc.Column == endOfFirstLine {
// If the marker is at the very end of the line, the marker will be a "^"
// character that extends one column past the end of the line. In this case
// we should reserve a column at the end so the marker doesn't wrap.
width -= 1
}
if len(lineText) > width {
// Try to center the error
sliceStart := (markerStart + markerEnd - width) / 2
if sliceStart > markerStart-width/5 {
sliceStart = markerStart - width/5
}
if sliceStart < 0 {
sliceStart = 0
}
if sliceStart > len(lineText)-width {
sliceStart = len(lineText) - width
}
sliceEnd := sliceStart + width
// Slice the line
slicedLine := lineText[sliceStart:sliceEnd]
markerStart -= sliceStart
markerEnd -= sliceStart
if markerStart < 0 {
markerStart = 0
}
if markerEnd > len(slicedLine) {
markerEnd = len(slicedLine)
}
// Truncate the ends with "..."
if len(slicedLine) > 3 && sliceStart > 0 {
slicedLine = "..." + slicedLine[3:]
if markerStart < 3 {
markerStart = 3
}
}
if len(slicedLine) > 3 && sliceEnd < len(lineText) {
slicedLine = slicedLine[:len(slicedLine)-3] + "..."
if markerEnd > len(slicedLine)-3 {
markerEnd = len(slicedLine) - 3
}
if markerEnd < markerStart {
markerEnd = markerStart
}
}
// Now we can compute the indent
lineText = slicedLine
indent = strings.Repeat(" ", estimateWidthInTerminal(lineText[:markerStart]))
}
// If marker is still multi-character after clipping, make the marker wider
if markerEnd-markerStart > 1 {
marker = strings.Repeat("~", estimateWidthInTerminal(lineText[markerStart:markerEnd]))
}
// Put a margin before the marker indent
margin := marginWithLineText(maxMargin, loc.Line)
return MsgDetail{
Path: loc.File.Select(pathStyle),
Line: loc.Line,
Column: loc.Column,
SourceBefore: margin + lineText[:markerStart],
SourceMarked: lineText[markerStart:markerEnd],
SourceAfter: lineText[markerEnd:],
Indent: indent,
Marker: marker,
Suggestion: loc.Suggestion,
ContentAfter: afterFirstLine,
}
}
// Estimate the number of columns this string will take when printed
func estimateWidthInTerminal(text string) int {
// For now just assume each code point is one column. This is wrong but is
// less wrong than assuming each code unit is one column.
width := 0
for text != "" {
c, size := utf8.DecodeRuneInString(text)
text = text[size:]
// Ignore the Zero Width No-Break Space character (UTF-8 BOM)
if c != 0xFEFF {
width++
}
}
return width
}
func renderTabStops(withTabs string, spacesPerTab int) string {
if !strings.ContainsRune(withTabs, '\t') {
return withTabs
}
withoutTabs := strings.Builder{}
count := 0
for _, c := range withTabs {
if c == '\t' {
spaces := spacesPerTab - count%spacesPerTab
for i := 0; i < spaces; i++ {
withoutTabs.WriteRune(' ')
count++
}
} else {
withoutTabs.WriteRune(c)
count++
}
}
return withoutTabs.String()
}
func (log Log) AddError(tracker *LineColumnTracker, r Range, text string) {
log.AddMsg(Msg{
Kind: Error,
+71 -56
View File
@@ -21,6 +21,7 @@ const (
MsgID_JS_CallImportNamespace
MsgID_JS_ClassNameWillThrow
MsgID_JS_CommonJSVariableInESM
MsgID_JS_ConfusingTypeScriptCast
MsgID_JS_DeleteSuperProperty
MsgID_JS_DirectEval
MsgID_JS_DuplicateCase
@@ -116,6 +117,8 @@ func StringToMsgIDs(str string, logLevel LogLevel, overrides map[MsgID]LogLevel)
overrides[MsgID_JS_ClassNameWillThrow] = logLevel
case "commonjs-variable-in-esm":
overrides[MsgID_JS_CommonJSVariableInESM] = logLevel
case "confusing-typescript-cast":
overrides[MsgID_JS_ConfusingTypeScriptCast] = logLevel
case "delete-super-property":
overrides[MsgID_JS_DeleteSuperProperty] = logLevel
case "direct-eval":
@@ -227,132 +230,144 @@ func StringToMsgIDs(str string, logLevel LogLevel, overrides map[MsgID]LogLevel)
}
}
// These are stored together to make it less accident-prone to edit them
type msgIDInfo struct {
name string
vsID vsID
}
func MsgIDToString(id MsgID) string {
return msgIDToInfo(id).name
}
func msgIDToInfo(id MsgID) msgIDInfo {
switch id {
// JS
case MsgID_JS_AssertToWith:
return "assert-to-with"
return msgIDInfo{name: "assert-to-with", vsID: vsID_JS_AssertToWith}
case MsgID_JS_AssertTypeJSON:
return "assert-type-json"
return msgIDInfo{name: "assert-type-json", vsID: vsID_JS_AssertTypeJSON}
case MsgID_JS_AssignToConstant:
return "assign-to-constant"
return msgIDInfo{name: "assign-to-constant", vsID: vsID_JS_AssignToConstant}
case MsgID_JS_AssignToDefine:
return "assign-to-define"
return msgIDInfo{name: "assign-to-define", vsID: vsID_JS_AssignToDefine}
case MsgID_JS_AssignToImport:
return "assign-to-import"
return msgIDInfo{name: "assign-to-import", vsID: vsID_JS_AssignToImport}
case MsgID_JS_BigInt:
return "bigint"
return msgIDInfo{name: "bigint", vsID: vsID_JS_BigInt}
case MsgID_JS_CallImportNamespace:
return "call-import-namespace"
return msgIDInfo{name: "call-import-namespace", vsID: vsID_JS_CallImportNamespace}
case MsgID_JS_ClassNameWillThrow:
return "class-name-will-throw"
return msgIDInfo{name: "class-name-will-throw", vsID: vsID_JS_ClassNameWillThrow}
case MsgID_JS_CommonJSVariableInESM:
return "commonjs-variable-in-esm"
return msgIDInfo{name: "commonjs-variable-in-esm", vsID: vsID_JS_CommonJSVariableInESM}
case MsgID_JS_ConfusingTypeScriptCast:
return msgIDInfo{name: "confusing-typescript-cast", vsID: vsID_JS_ConfusingTypeScriptCast}
case MsgID_JS_DeleteSuperProperty:
return "delete-super-property"
return msgIDInfo{name: "delete-super-property", vsID: vsID_JS_DeleteSuperProperty}
case MsgID_JS_DirectEval:
return "direct-eval"
return msgIDInfo{name: "direct-eval", vsID: vsID_JS_DirectEval}
case MsgID_JS_DuplicateCase:
return "duplicate-case"
return msgIDInfo{name: "duplicate-case", vsID: vsID_JS_DuplicateCase}
case MsgID_JS_DuplicateClassMember:
return "duplicate-class-member"
return msgIDInfo{name: "duplicate-class-member", vsID: vsID_JS_DuplicateClassMember}
case MsgID_JS_DuplicateObjectKey:
return "duplicate-object-key"
return msgIDInfo{name: "duplicate-object-key", vsID: vsID_JS_DuplicateObjectKey}
case MsgID_JS_EmptyImportMeta:
return "empty-import-meta"
return msgIDInfo{name: "empty-import-meta", vsID: vsID_JS_EmptyImportMeta}
case MsgID_JS_EqualsNaN:
return "equals-nan"
return msgIDInfo{name: "equals-nan", vsID: vsID_JS_EqualsNaN}
case MsgID_JS_EqualsNegativeZero:
return "equals-negative-zero"
return msgIDInfo{name: "equals-negative-zero", vsID: vsID_JS_EqualsNegativeZero}
case MsgID_JS_EqualsNewObject:
return "equals-new-object"
return msgIDInfo{name: "equals-new-object", vsID: vsID_JS_EqualsNewObject}
case MsgID_JS_HTMLCommentInJS:
return "html-comment-in-js"
return msgIDInfo{name: "html-comment-in-js", vsID: vsID_JS_HTMLCommentInJS}
case MsgID_JS_ImpossibleTypeof:
return "impossible-typeof"
return msgIDInfo{name: "impossible-typeof", vsID: vsID_JS_ImpossibleTypeof}
case MsgID_JS_IndirectRequire:
return "indirect-require"
return msgIDInfo{name: "indirect-require", vsID: vsID_JS_IndirectRequire}
case MsgID_JS_PrivateNameWillThrow:
return "private-name-will-throw"
return msgIDInfo{name: "private-name-will-throw", vsID: vsID_JS_PrivateNameWillThrow}
case MsgID_JS_SemicolonAfterReturn:
return "semicolon-after-return"
return msgIDInfo{name: "semicolon-after-return", vsID: vsID_JS_SemicolonAfterReturn}
case MsgID_JS_SuspiciousBooleanNot:
return "suspicious-boolean-not"
return msgIDInfo{name: "suspicious-boolean-not", vsID: vsID_JS_SuspiciousBooleanNot}
case MsgID_JS_SuspiciousDefine:
return "suspicious-define"
return msgIDInfo{name: "suspicious-define", vsID: vsID_JS_SuspiciousDefine}
case MsgID_JS_SuspiciousLogicalOperator:
return "suspicious-logical-operator"
return msgIDInfo{name: "suspicious-logical-operator", vsID: vsID_JS_SuspiciousLogicalOperator}
case MsgID_JS_SuspiciousNullishCoalescing:
return "suspicious-nullish-coalescing"
return msgIDInfo{name: "suspicious-nullish-coalescing", vsID: vsID_JS_SuspiciousNullishCoalescing}
case MsgID_JS_ThisIsUndefinedInESM:
return "this-is-undefined-in-esm"
return msgIDInfo{name: "this-is-undefined-in-esm", vsID: vsID_JS_ThisIsUndefinedInESM}
case MsgID_JS_UnsupportedDynamicImport:
return "unsupported-dynamic-import"
return msgIDInfo{name: "unsupported-dynamic-import", vsID: vsID_JS_UnsupportedDynamicImport}
case MsgID_JS_UnsupportedJSXComment:
return "unsupported-jsx-comment"
return msgIDInfo{name: "unsupported-jsx-comment", vsID: vsID_JS_UnsupportedJSXComment}
case MsgID_JS_UnsupportedRegExp:
return "unsupported-regexp"
return msgIDInfo{name: "unsupported-regexp", vsID: vsID_JS_UnsupportedRegExp}
case MsgID_JS_UnsupportedRequireCall:
return "unsupported-require-call"
return msgIDInfo{name: "unsupported-require-call", vsID: vsID_JS_UnsupportedRequireCall}
// CSS
case MsgID_CSS_CSSSyntaxError:
return "css-syntax-error"
return msgIDInfo{name: "css-syntax-error", vsID: vsID_CSS_CSSSyntaxError}
case MsgID_CSS_InvalidAtCharset:
return "invalid-@charset"
return msgIDInfo{name: "invalid-@charset", vsID: vsID_CSS_InvalidAtCharset}
case MsgID_CSS_InvalidAtImport:
return "invalid-@import"
return msgIDInfo{name: "invalid-@import", vsID: vsID_CSS_InvalidAtImport}
case MsgID_CSS_InvalidAtLayer:
return "invalid-@layer"
return msgIDInfo{name: "invalid-@layer", vsID: vsID_CSS_InvalidAtLayer}
case MsgID_CSS_InvalidCalc:
return "invalid-calc"
return msgIDInfo{name: "invalid-calc", vsID: vsID_CSS_InvalidCalc}
case MsgID_CSS_JSCommentInCSS:
return "js-comment-in-css"
return msgIDInfo{name: "js-comment-in-css", vsID: vsID_CSS_JSCommentInCSS}
case MsgID_CSS_UndefinedComposesFrom:
return "undefined-composes-from"
return msgIDInfo{name: "undefined-composes-from", vsID: vsID_CSS_UndefinedComposesFrom}
case MsgID_CSS_UnsupportedAtCharset:
return "unsupported-@charset"
return msgIDInfo{name: "unsupported-@charset", vsID: vsID_CSS_UnsupportedAtCharset}
case MsgID_CSS_UnsupportedAtNamespace:
return "unsupported-@namespace"
return msgIDInfo{name: "unsupported-@namespace", vsID: vsID_CSS_UnsupportedAtNamespace}
case MsgID_CSS_UnsupportedCSSProperty:
return "unsupported-css-property"
return msgIDInfo{name: "unsupported-css-property", vsID: vsID_CSS_UnsupportedCSSProperty}
case MsgID_CSS_UnsupportedCSSNesting:
return "unsupported-css-nesting"
return msgIDInfo{name: "unsupported-css-nesting", vsID: vsID_CSS_UnsupportedCSSNesting}
// Bundler
case MsgID_Bundler_AmbiguousReexport:
return "ambiguous-reexport"
return msgIDInfo{name: "ambiguous-reexport", vsID: vsID_Bundler_AmbiguousReexport}
case MsgID_Bundler_DifferentPathCase:
return "different-path-case"
return msgIDInfo{name: "different-path-case", vsID: vsID_Bundler_DifferentPathCase}
case MsgID_Bundler_EmptyGlob:
return "empty-glob"
return msgIDInfo{name: "empty-glob", vsID: vsID_Bundler_EmptyGlob}
case MsgID_Bundler_IgnoredBareImport:
return "ignored-bare-import"
return msgIDInfo{name: "ignored-bare-import", vsID: vsID_Bundler_IgnoredBareImport}
case MsgID_Bundler_IgnoredDynamicImport:
return "ignored-dynamic-import"
return msgIDInfo{name: "ignored-dynamic-import", vsID: vsID_Bundler_IgnoredDynamicImport}
case MsgID_Bundler_ImportIsUndefined:
return "import-is-undefined"
return msgIDInfo{name: "import-is-undefined", vsID: vsID_Bundler_ImportIsUndefined}
case MsgID_Bundler_RequireResolveNotExternal:
return "require-resolve-not-external"
return msgIDInfo{name: "require-resolve-not-external", vsID: vsID_Bundler_RequireResolveNotExternal}
// Source maps
case MsgID_SourceMap_InvalidSourceMappings:
return "invalid-source-mappings"
return msgIDInfo{name: "invalid-source-mappings", vsID: vsID_SourceMap_InvalidSourceMappings}
case MsgID_SourceMap_MissingSourceMap:
return "missing-source-map"
return msgIDInfo{name: "missing-source-map", vsID: vsID_SourceMap_MissingSourceMap}
case MsgID_SourceMap_UnsupportedSourceMapComment:
return "unsupported-source-map-comment"
return msgIDInfo{name: "unsupported-source-map-comment", vsID: vsID_SourceMap_UnsupportedSourceMapComment}
default:
if id >= MsgID_PackageJSON_FIRST && id <= MsgID_PackageJSON_LAST {
return "package.json"
return msgIDInfo{name: "package.json", vsID: vsID_PackageJSON}
}
if id >= MsgID_TSConfigJSON_FIRST && id <= MsgID_TSConfigJSON_LAST {
return "tsconfig.json"
return msgIDInfo{name: "tsconfig.json", vsID: vsID_TSConfigJSON}
}
}
return ""
return msgIDInfo{}
}
// Some message IDs are more diverse internally than externally (in case we
+294
View File
@@ -0,0 +1,294 @@
package logger
import (
"strings"
"unicode/utf8"
)
type LogStyle int8
const (
StyleDefault LogStyle = iota
StyleClang
StyleVisualStudio
)
func linkifyText(text string, underline string, reset string) string {
if underline == "" {
return text
}
https := strings.Index(text, "https://")
if https == -1 {
return text
}
sb := strings.Builder{}
for {
https := strings.Index(text, "https://")
if https == -1 {
break
}
end := strings.IndexByte(text[https:], ' ')
if end == -1 {
end = len(text)
} else {
end += https
}
// Remove trailing punctuation
if end > https {
switch text[end-1] {
case '.', ',', '?', '!', ')', ']', '}':
end--
}
}
sb.WriteString(text[:https])
sb.WriteString(underline)
sb.WriteString(text[https:end])
sb.WriteString(reset)
text = text[end:]
}
sb.WriteString(text)
return sb.String()
}
func wrapWordsInString(text string, width int) []string {
runs := []string{}
outer:
for text != "" {
i := 0
x := 0
wordEndI := 0
// Skip over any leading spaces
for i < len(text) && text[i] == ' ' {
i++
x++
}
// Find out how many words will fit in this run
for i < len(text) {
oldWordEndI := wordEndI
wordStartI := i
// Find the end of the word
for i < len(text) {
c, width := utf8.DecodeRuneInString(text[i:])
if c == ' ' {
break
}
i += width
x += 1 // Naively assume that each unicode code point is a single column
}
wordEndI = i
// Split into a new run if this isn't the first word in the run and the end is past the width
if wordStartI > 0 && x > width {
runs = append(runs, text[:oldWordEndI])
text = text[wordStartI:]
continue outer
}
// Skip over any spaces after the word
for i < len(text) && text[i] == ' ' {
i++
x++
}
}
// If we get here, this is the last run (i.e. everything fits)
break
}
// Remove any trailing spaces on the last run
for len(text) > 0 && text[len(text)-1] == ' ' {
text = text[:len(text)-1]
}
runs = append(runs, text)
return runs
}
type formattedLineSpan struct {
firstLineBefore string
firstLineMarked string
firstLineAfter string
afterFirstLine string
marker string
indent string
}
func formatLineSpan(lineText string, line int, column int, length int, widthLimit int) formattedLineSpan {
endOfFirstLine := len(lineText)
// Note: This uses "IndexByte" because Go implements this with SIMD, which
// can matter a lot for really long lines. Some people pass huge >100mb
// minified files as line text for the log message.
if i := strings.IndexByte(lineText, '\n'); i >= 0 {
endOfFirstLine = i
}
firstLine := lineText[:endOfFirstLine]
afterFirstLine := lineText[endOfFirstLine:]
if afterFirstLine != "" && !strings.HasSuffix(afterFirstLine, "\n") {
afterFirstLine += "\n"
}
// Clamp values in range
if line < 0 {
line = 0
}
if column < 0 {
column = 0
}
if length < 0 {
length = 0
}
if column > endOfFirstLine {
column = endOfFirstLine
}
if length > endOfFirstLine-column {
length = endOfFirstLine - column
}
spacesPerTab := 2
lineText = renderTabStops(firstLine, spacesPerTab)
textUpToLoc := renderTabStops(firstLine[:column], spacesPerTab)
markerStart := len(textUpToLoc)
markerEnd := markerStart
indent := strings.Repeat(" ", estimateWidthInTerminal(textUpToLoc))
marker := "^"
// Extend markers to cover the full range of the error
if length > 0 {
markerEnd = len(renderTabStops(firstLine[:column+length], spacesPerTab))
}
// Clip the marker to the bounds of the line
if markerStart > len(lineText) {
markerStart = len(lineText)
}
if markerEnd > len(lineText) {
markerEnd = len(lineText)
}
if markerEnd < markerStart {
markerEnd = markerStart
}
// Trim the line to fit the terminal width
width := widthLimit
if width < 1 {
width = 1
}
if column == endOfFirstLine {
// If the marker is at the very end of the line, the marker will be a "^"
// character that extends one column past the end of the line. In this case
// we should reserve a column at the end so the marker doesn't wrap.
width -= 1
}
if len(lineText) > width {
// Try to center the error
sliceStart := (markerStart + markerEnd - width) / 2
if sliceStart > markerStart-width/5 {
sliceStart = markerStart - width/5
}
if sliceStart < 0 {
sliceStart = 0
}
if sliceStart > len(lineText)-width {
sliceStart = len(lineText) - width
}
sliceEnd := sliceStart + width
// Slice the line
slicedLine := lineText[sliceStart:sliceEnd]
markerStart -= sliceStart
markerEnd -= sliceStart
if markerStart < 0 {
markerStart = 0
}
if markerEnd > len(slicedLine) {
markerEnd = len(slicedLine)
}
// Truncate the ends with "..."
if len(slicedLine) > 3 && sliceStart > 0 {
slicedLine = "..." + slicedLine[3:]
if markerStart < 3 {
markerStart = 3
}
}
if len(slicedLine) > 3 && sliceEnd < len(lineText) {
slicedLine = slicedLine[:len(slicedLine)-3] + "..."
if markerEnd > len(slicedLine)-3 {
markerEnd = len(slicedLine) - 3
}
if markerEnd < markerStart {
markerEnd = markerStart
}
}
// Now we can compute the indent
lineText = slicedLine
indent = strings.Repeat(" ", estimateWidthInTerminal(lineText[:markerStart]))
}
// If marker is still multi-character after clipping, make the marker wider
if markerEnd-markerStart > 1 {
marker = strings.Repeat("~", estimateWidthInTerminal(lineText[markerStart:markerEnd]))
}
return formattedLineSpan{
firstLineBefore: lineText[:markerStart],
firstLineMarked: lineText[markerStart:markerEnd],
firstLineAfter: lineText[markerEnd:],
afterFirstLine: afterFirstLine,
marker: marker,
indent: indent,
}
}
// Estimate the number of columns this string will take when printed
func estimateWidthInTerminal(text string) int {
// For now just assume each code point is one column. This is wrong but is
// less wrong than assuming each code unit is one column.
width := 0
for text != "" {
c, size := utf8.DecodeRuneInString(text)
text = text[size:]
// Ignore the Zero Width No-Break Space character (UTF-8 BOM)
if c != 0xFEFF {
width++
}
}
return width
}
func renderTabStops(withTabs string, spacesPerTab int) string {
if !strings.ContainsRune(withTabs, '\t') {
return withTabs
}
withoutTabs := strings.Builder{}
count := 0
for _, c := range withTabs {
if c == '\t' {
spaces := spacesPerTab - count%spacesPerTab
for i := 0; i < spaces; i++ {
withoutTabs.WriteRune(' ')
count++
}
} else {
withoutTabs.WriteRune(c)
count++
}
}
return withoutTabs.String()
}
+76
View File
@@ -0,0 +1,76 @@
package logger
import (
"fmt"
"strings"
)
// This log style is intended to help integrate esbuild with tools that expect
// clang's output style. More information:
//
// - https://clang.llvm.org/docs/UsersManual.html#formatting-of-diagnostics
// - https://clang.llvm.org/diagnostics.html
func msgToStringClang(msg Msg, options OutputOptions, terminalInfo TerminalInfo) string {
var text strings.Builder
writeMsgDataClang(&text, msg.ID, msg.Data, options, terminalInfo, msg.Kind)
for _, note := range msg.Notes {
if note.Location != nil {
writeMsgDataClang(&text, MsgID_None, note, options, terminalInfo, Note)
}
}
return text.String()
}
func writeMsgDataClang(text *strings.Builder, id MsgID, data MsgData, options OutputOptions, terminalInfo TerminalInfo, kind MsgKind) {
var colors Colors
if terminalInfo.UseColorEscapes {
colors = TerminalColors
}
text.WriteString(colors.Bold)
if loc := data.Location; loc != nil {
text.WriteString(loc.File.Select(options.PathStyle))
// Note: Need to adjust the column from 0-based to 1-based
if loc.Line > 0 {
text.WriteString(fmt.Sprintf(":%d:%d", loc.Line, loc.Column+1))
}
text.WriteString(": ")
}
var kindColor string
var kindText string
switch kind {
case Error:
kindColor, kindText = colors.Red, "error"
case Note:
kindColor, kindText = colors.Dim, "note"
default:
kindColor, kindText = colors.Magenta, "warning"
}
msgID := MsgIDToString(id)
if msgID != "" {
msgID = fmt.Sprintf(" [%s]", msgID)
}
text.WriteString(fmt.Sprintf("%s%s:%s %s%s%s%s\n",
kindColor, kindText, colors.Reset, colors.Bold, data.Text, msgID, colors.Reset))
if loc := data.Location; loc != nil && loc.Line > 0 {
widthLimit := terminalInfo.Width
if widthLimit < 1 {
widthLimit = defaultTerminalWidth
}
span := formatLineSpan(loc.LineText, loc.Line, loc.Column, loc.Length, widthLimit)
text.WriteString(fmt.Sprintf("%s%s%s%s%s%s\n%s%s%s%s\n",
colors.Dim, span.firstLineBefore, colors.Green, span.firstLineMarked, colors.Dim, span.firstLineAfter,
span.indent, colors.Green, span.marker, colors.Reset,
))
if loc.Suggestion != "" {
text.WriteString(fmt.Sprintf("%s%s%s%s\n", colors.Green, span.indent, loc.Suggestion, colors.Reset))
}
}
}
+160
View File
@@ -0,0 +1,160 @@
package logger
import (
"fmt"
"strings"
)
func msgToStringDefault(msg Msg, options OutputOptions, terminalInfo TerminalInfo) string {
// Format the message
var text strings.Builder
text.WriteString(msgString(options.IncludeSource, options.PathStyle, terminalInfo, msg.ID, msg.Kind, msg.Data, msg.PluginName))
// Format the notes
var oldData MsgData
for i, note := range msg.Notes {
if options.IncludeSource && (i == 0 || strings.IndexByte(oldData.Text, '\n') >= 0 || oldData.Location != nil) {
text.WriteString("\n")
}
text.WriteString(msgString(options.IncludeSource, options.PathStyle, terminalInfo, MsgID_None, Note, note, ""))
oldData = note
}
// Add extra spacing between messages if source code is present
if options.IncludeSource {
text.WriteString("\n")
}
return text.String()
}
func marginWithLineText(maxMargin int, line int) string {
number := fmt.Sprintf("%d", line)
return fmt.Sprintf(" %s%s │ ", strings.Repeat(" ", maxMargin-len(number)), number)
}
func emptyMarginText(maxMargin int, isLast bool) string {
space := strings.Repeat(" ", maxMargin)
if isLast {
return fmt.Sprintf(" %s ╵ ", space)
}
return fmt.Sprintf(" %s │ ", space)
}
func msgString(includeSource bool, pathStyle PathStyle, terminalInfo TerminalInfo, id MsgID, kind MsgKind, data MsgData, pluginName string) string {
if !includeSource {
if loc := data.Location; loc != nil {
return fmt.Sprintf("%s: %s: %s\n", loc.File.Select(pathStyle), kind.String(), data.Text)
}
return fmt.Sprintf("%s: %s\n", kind.String(), data.Text)
}
var colors Colors
if terminalInfo.UseColorEscapes {
colors = TerminalColors
}
var iconColor string
var kindColorBrackets string
var kindColorText string
location := ""
if loc := data.Location; loc != nil {
maxMargin := len(fmt.Sprintf("%d", loc.Line))
margin := marginWithLineText(maxMargin, loc.Line)
widthLimit := terminalInfo.Width
if widthLimit < 1 {
widthLimit = defaultTerminalWidth
}
widthLimit -= maxMargin + extraMarginChars
span := formatLineSpan(loc.LineText, loc.Line, loc.Column, loc.Length, widthLimit)
path := loc.File.Select(pathStyle)
if loc.Suggestion != "" {
location = fmt.Sprintf("\n %s:%d:%d:\n%s%s%s%s%s%s%s\n%s%s%s%s%s\n%s%s%s%s%s\n%s",
path, loc.Line, loc.Column,
colors.Dim, margin, span.firstLineBefore, colors.Green, span.firstLineMarked, colors.Dim, span.firstLineAfter,
emptyMarginText(maxMargin, false), span.indent, colors.Green, span.marker, colors.Dim,
emptyMarginText(maxMargin, true), span.indent, colors.Green, loc.Suggestion, colors.Reset,
span.afterFirstLine,
)
} else {
location = fmt.Sprintf("\n %s:%d:%d:\n%s%s%s%s%s%s%s\n%s%s%s%s%s\n%s",
path, loc.Line, loc.Column,
colors.Dim, margin, span.firstLineBefore, colors.Green, span.firstLineMarked, colors.Dim, span.firstLineAfter,
emptyMarginText(maxMargin, true), span.indent, colors.Green, span.marker, colors.Reset,
span.afterFirstLine,
)
}
}
switch kind {
case Verbose:
iconColor = colors.Cyan
kindColorBrackets = colors.CyanBgCyan
kindColorText = colors.CyanBgBlack
case Debug:
iconColor = colors.Green
kindColorBrackets = colors.GreenBgGreen
kindColorText = colors.GreenBgWhite
case Info:
iconColor = colors.Blue
kindColorBrackets = colors.BlueBgBlue
kindColorText = colors.BlueBgWhite
case Error:
iconColor = colors.Red
kindColorBrackets = colors.RedBgRed
kindColorText = colors.RedBgWhite
case Warning:
iconColor = colors.Yellow
kindColorBrackets = colors.YellowBgYellow
kindColorText = colors.YellowBgBlack
case Note:
sb := strings.Builder{}
for _, line := range strings.Split(data.Text, "\n") {
// Special-case word wrapping
if wrapWidth := terminalInfo.Width; wrapWidth > 2 {
if !data.DisableMaximumWidth && wrapWidth > 100 {
wrapWidth = 100 // Enforce a maximum paragraph width for readability
}
for _, run := range wrapWordsInString(line, wrapWidth-2) {
sb.WriteString(" ")
sb.WriteString(linkifyText(run, colors.Underline, colors.Reset))
sb.WriteByte('\n')
}
continue
}
// Otherwise, just write an indented line
sb.WriteString(" ")
sb.WriteString(linkifyText(line, colors.Underline, colors.Reset))
sb.WriteByte('\n')
}
sb.WriteString(location)
return sb.String()
}
if pluginName != "" {
pluginName = fmt.Sprintf(" %s%s[plugin %s]%s", colors.Bold, colors.Magenta, pluginName, colors.Reset)
}
msgID := MsgIDToString(id)
if msgID != "" {
msgID = fmt.Sprintf(" [%s]", msgID)
}
return fmt.Sprintf("%s%s %s[%s%s%s]%s %s%s%s%s%s\n%s",
iconColor, kind.Icon(),
kindColorBrackets, kindColorText, kind.String(), kindColorBrackets, colors.Reset,
colors.Bold, data.Text, colors.Reset, pluginName, msgID,
location,
)
}
+107
View File
@@ -0,0 +1,107 @@
package logger
import (
"fmt"
"strings"
)
// This log style is intended to help integrate esbuild with Visual Studio's
// built-in problem matcher. More information:
//
// - https://learn.microsoft.com/en-us/cpp/build/formatting-the-output-of-a-custom-build-step-or-build-event
// - https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks
func msgToStringVisualStudio(msg Msg, options OutputOptions, terminalInfo TerminalInfo) string {
var text strings.Builder
// Write the origin
if loc := msg.Data.Location; loc != nil {
// Potentially this needs to always be an absolute path?
text.WriteString(loc.File.Abs)
// Note: Need to adjust the column from 0-based to 1-based
if loc.Line > 0 {
text.WriteString(fmt.Sprintf("(%d,%d)", loc.Line, loc.Column+1))
}
text.WriteString(": ")
} else {
// If there is no file, then we must write a tool name
text.WriteString("esbuild: ")
}
// The code appears to be required, so make something up
code := fmt.Sprintf("ES%04d", msgIDToInfo(msg.ID).vsID)
// The only valid options for the category seem to be "error" and "warning"
if msg.Kind == Error {
text.WriteString(fmt.Sprintf("error %s: ", code))
} else {
text.WriteString(fmt.Sprintf("warning %s: ", code))
}
text.WriteString(msg.Data.Text)
text.WriteByte('\n')
return text.String()
}
type vsID = uint8
// These are separate from "MsgID" because they are external and "MsgID" is
// internal. Don't ever delete or reorder one of these. Only append to them.
const (
vsID_JS_AssertToWith = 1
vsID_JS_AssertTypeJSON = 2
vsID_JS_AssignToConstant = 3
vsID_JS_AssignToDefine = 4
vsID_JS_AssignToImport = 5
vsID_JS_BigInt = 6
vsID_JS_CallImportNamespace = 7
vsID_JS_ClassNameWillThrow = 8
vsID_JS_CommonJSVariableInESM = 9
vsID_JS_ConfusingTypeScriptCast = 10
vsID_JS_DeleteSuperProperty = 11
vsID_JS_DirectEval = 12
vsID_JS_DuplicateCase = 13
vsID_JS_DuplicateClassMember = 14
vsID_JS_DuplicateObjectKey = 15
vsID_JS_EmptyImportMeta = 16
vsID_JS_EqualsNaN = 17
vsID_JS_EqualsNegativeZero = 18
vsID_JS_EqualsNewObject = 19
vsID_JS_HTMLCommentInJS = 20
vsID_JS_ImpossibleTypeof = 21
vsID_JS_IndirectRequire = 22
vsID_JS_PrivateNameWillThrow = 23
vsID_JS_SemicolonAfterReturn = 24
vsID_JS_SuspiciousBooleanNot = 25
vsID_JS_SuspiciousDefine = 26
vsID_JS_SuspiciousLogicalOperator = 27
vsID_JS_SuspiciousNullishCoalescing = 28
vsID_JS_ThisIsUndefinedInESM = 29
vsID_JS_UnsupportedDynamicImport = 30
vsID_JS_UnsupportedJSXComment = 31
vsID_JS_UnsupportedRegExp = 32
vsID_JS_UnsupportedRequireCall = 33
vsID_CSS_CSSSyntaxError = 34
vsID_CSS_InvalidAtCharset = 35
vsID_CSS_InvalidAtImport = 36
vsID_CSS_InvalidAtLayer = 37
vsID_CSS_InvalidCalc = 38
vsID_CSS_JSCommentInCSS = 39
vsID_CSS_UndefinedComposesFrom = 40
vsID_CSS_UnsupportedAtCharset = 41
vsID_CSS_UnsupportedAtNamespace = 42
vsID_CSS_UnsupportedCSSProperty = 43
vsID_CSS_UnsupportedCSSNesting = 44
vsID_Bundler_AmbiguousReexport = 45
vsID_Bundler_DifferentPathCase = 46
vsID_Bundler_EmptyGlob = 47
vsID_Bundler_IgnoredBareImport = 48
vsID_Bundler_IgnoredDynamicImport = 49
vsID_Bundler_ImportIsUndefined = 50
vsID_Bundler_RequireResolveNotExternal = 51
vsID_SourceMap_InvalidSourceMappings = 52
vsID_SourceMap_MissingSourceMap = 53
vsID_SourceMap_UnsupportedSourceMapComment = 54
vsID_PackageJSON = 55
vsID_TSConfigJSON = 56
)
+1 -1
View File
@@ -221,7 +221,7 @@ func ParseTSConfigJSON(
switch lowerValue {
case "es3", "es5", "es6", "es2015", "es2016", "es2017", "es2018", "es2019", "es2020", "es2021":
result.Settings.Target = config.TSTargetBelowES2022
case "es2022", "es2023", "es2024", "es2025", "esnext":
case "es2022", "es2023", "es2024", "es2025", "es2026", "esnext":
result.Settings.Target = config.TSTargetAtOrAboveES2022
default:
ok = false
-6
View File
@@ -1,6 +0,0 @@
declare module "https://deno.land/x/[email protected]/mod.ts" {
export function gunzip(input: Uint8Array): Uint8Array
}
// This is used by "worker.ts"
declare let onmessage: (event: any) => void
-435
View File
@@ -1,435 +0,0 @@
import type * as types from "../shared/types"
import * as common from "../shared/common"
import * as ourselves from "./mod"
import * as denoflate from "https://deno.land/x/[email protected]/mod.ts"
declare const ESBUILD_VERSION: string
declare const ESBUILD_BINARY_HASHES: Record<string, string>
export let version = ESBUILD_VERSION
export let build: typeof types.build = (options: types.BuildOptions) =>
ensureServiceIsRunning().then(service =>
service.build(options))
export const context: typeof types.context = (options: types.BuildOptions) =>
ensureServiceIsRunning().then(service =>
service.context(options))
export const transform: typeof types.transform = (input: string | Uint8Array, options?: types.TransformOptions) =>
ensureServiceIsRunning().then(service =>
service.transform(input, options))
export const formatMessages: typeof types.formatMessages = (messages, options) =>
ensureServiceIsRunning().then(service =>
service.formatMessages(messages, options))
export const analyzeMetafile: typeof types.analyzeMetafile = (metafile, options) =>
ensureServiceIsRunning().then(service =>
service.analyzeMetafile(metafile, options))
export const buildSync: typeof types.buildSync = () => {
throw new Error(`The "buildSync" API does not work in Deno`)
}
export const transformSync: typeof types.transformSync = () => {
throw new Error(`The "transformSync" API does not work in Deno`)
}
export const formatMessagesSync: typeof types.formatMessagesSync = () => {
throw new Error(`The "formatMessagesSync" API does not work in Deno`)
}
export const analyzeMetafileSync: typeof types.analyzeMetafileSync = () => {
throw new Error(`The "analyzeMetafileSync" API does not work in Deno`)
}
export const stop = async () => {
if (stopService) await stopService()
}
let initializeWasCalled = false
export const initialize: typeof types.initialize = async (options) => {
options = common.validateInitializeOptions(options || {})
if (options.wasmURL) throw new Error(`The "wasmURL" option only works in the browser`)
if (options.wasmModule) throw new Error(`The "wasmModule" option only works in the browser`)
if (options.worker) throw new Error(`The "worker" option only works in the browser`)
if (initializeWasCalled) throw new Error('Cannot call "initialize" more than once')
await ensureServiceIsRunning()
initializeWasCalled = true
}
async function binaryIntegrityCheck(pkg: string, subpath: string, bytes: Uint8Array): Promise<void> {
const hashBuffer = await crypto.subtle.digest('SHA-256', bytes as Uint8Array<ArrayBuffer>)
const hash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
const key = `${pkg}/${subpath}`
const expected = ESBUILD_BINARY_HASHES[key]
if (!expected) throw new Error(`Missing hash for "${key}"`)
if (hash !== expected) throw new Error(`"${hash.slice(0, 8)}..." doesn't match "${expected.slice(0, 8)}..." for "${pkg}"`)
}
async function installFromNPM(name: string, subpath: string): Promise<string> {
const { finalPath, finalDir } = getCachePath(name)
try {
await Deno.stat(finalPath)
return finalPath
} catch (e) {
}
const npmRegistry = Deno.env.get("NPM_CONFIG_REGISTRY") || "https://registry.npmjs.org"
const url = `${npmRegistry}/${name}/-/${name.replace("@esbuild/", "")}-${version}.tgz`
const buffer = await fetch(url).then(r => r.arrayBuffer())
const executable = extractFileFromTarGzip(new Uint8Array(buffer), subpath)
await binaryIntegrityCheck(name, subpath, executable)
await Deno.mkdir(finalDir, {
recursive: true,
mode: 0o700, // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
})
await Deno.writeFile(finalPath, executable, { mode: 0o755 })
return finalPath
}
function getCachePath(name: string): { finalPath: string, finalDir: string } {
let baseDir: string | undefined
switch (Deno.build.os) {
case 'darwin':
baseDir = Deno.env.get('HOME')
if (baseDir) baseDir += '/Library/Caches'
break
case 'windows':
baseDir = Deno.env.get('LOCALAPPDATA')
if (!baseDir) {
baseDir = Deno.env.get('USERPROFILE')
if (baseDir) baseDir += '/AppData/Local'
}
if (baseDir) baseDir += '/Cache'
break
case 'linux':
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
const xdg = Deno.env.get('XDG_CACHE_HOME')
if (xdg && xdg[0] === '/') baseDir = xdg
break
}
if (!baseDir) {
baseDir = Deno.env.get('HOME')
if (baseDir) baseDir += '/.cache'
}
if (!baseDir) throw new Error('Failed to find cache directory')
const finalDir = baseDir + `/esbuild/bin`
const finalPath = finalDir + `/${name.replace('/', '-')}@${version}`
return { finalPath, finalDir }
}
function extractFileFromTarGzip(buffer: Uint8Array, file: string): Uint8Array {
try {
buffer = denoflate.gunzip(buffer)
} catch (err: any) {
throw new Error(`Invalid gzip data in archive: ${err && err.message || err}`)
}
let str = (i: number, n: number) => String.fromCharCode(...buffer.subarray(i, i + n)).replace(/\0.*$/, '')
let offset = 0
file = `package/${file}`
while (offset < buffer.length) {
let name = str(offset, 100)
let size = parseInt(str(offset + 124, 12), 8)
offset += 512
if (!isNaN(size) && size >= 0) {
if (name === file) return buffer.subarray(offset, offset + size)
offset += (size + 511) & ~511
}
}
throw new Error(`Could not find ${JSON.stringify(file)} in archive`)
}
async function install(): Promise<string> {
const overridePath = Deno.env.get('ESBUILD_BINARY_PATH')
if (overridePath) return overridePath
const platformKey = Deno.build.target
const knownWindowsPackages: Record<string, string> = {
'x86_64-pc-windows-msvc': '@esbuild/win32-x64',
}
const knownUnixlikePackages: Record<string, string> = {
// These are the only platforms that Deno supports
'aarch64-apple-darwin': '@esbuild/darwin-arm64',
'aarch64-unknown-linux-gnu': '@esbuild/linux-arm64',
'x86_64-apple-darwin': '@esbuild/darwin-x64',
'x86_64-unknown-linux-gnu': '@esbuild/linux-x64',
// These platforms are not supported by Deno
'aarch64-linux-android': '@esbuild/android-arm64',
'x86_64-unknown-freebsd': '@esbuild/freebsd-x64',
"x86_64-alpine-linux-musl": '@esbuild/linux-x64',
}
// Pick a package to install
if (platformKey in knownWindowsPackages) {
return await installFromNPM(knownWindowsPackages[platformKey], 'esbuild.exe')
} else if (platformKey in knownUnixlikePackages) {
return await installFromNPM(knownUnixlikePackages[platformKey], 'bin/esbuild')
} else {
throw new Error(`Unsupported platform: ${platformKey}`)
}
}
interface Service {
build: typeof types.build
context: typeof types.context
transform: typeof types.transform
formatMessages: typeof types.formatMessages
analyzeMetafile: typeof types.analyzeMetafile
}
let defaultWD = Deno.cwd()
let longLivedService: Promise<Service> | undefined
let stopService: (() => Promise<void>) | undefined
// Declare a common subprocess API for the two implementations below
type SpawnFn = (cmd: string, options: {
args: string[]
stdin: 'piped' | 'inherit'
stdout: 'piped' | 'inherit'
stderr: 'inherit'
}) => {
write(bytes: Uint8Array): void
read(): Promise<Uint8Array | null>
close(): Promise<void> | void
status(): Promise<{ code: number }>
}
// Deno ≥1.40
const spawnNew: SpawnFn = (cmd, { args, stdin, stdout, stderr }) => {
const child = new Deno.Command(cmd, {
args,
cwd: defaultWD,
stdin,
stdout,
stderr,
}).spawn()
// Note: Need to check for "piped" in Deno ≥1.31.0 to avoid a crash
const writer = stdin === 'piped' ? child.stdin.getWriter() : null
const reader = stdout === 'piped' ? child.stdout.getReader() : null
return {
write: writer ? bytes => writer.write(bytes) : () => Promise.resolve(),
read: reader ? () => reader.read().then(x => x.value || null) : () => Promise.resolve(null),
close: async () => {
// We can't call "kill()" because it doesn't seem to work. Tests will
// still fail with "A child process was opened during the test, but not
// closed during the test" even though we kill the child process.
//
// And we can't call both "writer.close()" and "kill()" because then
// there's a race as the child process exits when stdin is closed, and
// "kill()" fails when the child process has already been killed.
//
// So instead we just call "writer.close()" and then hope that this
// causes the child process to exit. It won't work if the stdin consumer
// thread in the child process is hung or busy, but that may be the best
// we can do.
//
// See this for more info: https://github.com/evanw/esbuild/pull/3611
if (writer) await writer.close()
if (reader) await reader.cancel()
// Wait for the process to exit. The new "kill()" API doesn't flag the
// process as having exited because processes can technically ignore the
// kill signal. Without this, Deno will fail tests that use esbuild with
// an error because the test spawned a process but didn't wait for it.
await child.status
},
status: () => child.status,
}
}
// Deno <1.40
const spawnOld: SpawnFn = (cmd, { args, stdin, stdout, stderr }) => {
const child = Deno.run({
cmd: [cmd].concat(args),
cwd: defaultWD,
stdin,
stdout,
stderr,
})
const stdoutBuffer = new Uint8Array(4 * 1024 * 1024)
let writeQueue: Uint8Array[] = []
let isQueueLocked = false
// We need to keep calling "write()" until it actually writes the data
const startWriteFromQueueWorker = () => {
if (isQueueLocked || writeQueue.length === 0) return
isQueueLocked = true
child.stdin!.write(writeQueue[0]).then(bytesWritten => {
isQueueLocked = false
if (bytesWritten === writeQueue[0].length) writeQueue.shift()
else writeQueue[0] = writeQueue[0].subarray(bytesWritten)
startWriteFromQueueWorker()
})
}
return {
write: bytes => {
writeQueue.push(bytes)
startWriteFromQueueWorker()
},
read: () => child.stdout!.read(stdoutBuffer).then(n => n === null ? null : stdoutBuffer.subarray(0, n)),
close: () => {
child.stdin!.close()
child.stdout!.close()
child.close()
},
status: () => child.status(),
}
}
// This is a shim for "Deno.run" for newer versions of Deno
const spawn: SpawnFn = Deno.Command ? spawnNew : spawnOld
const ensureServiceIsRunning = (): Promise<Service> => {
if (!longLivedService) {
longLivedService = (async (): Promise<Service> => {
const binPath = await install()
const isTTY = Deno.stderr.isTerminal
? Deno.stderr.isTerminal() // Deno ≥1.40
: Deno.isatty(Deno.stderr.rid) // Deno <1.40
const child = spawn(binPath, {
args: [`--service=${version}`],
stdin: 'piped',
stdout: 'piped',
stderr: 'inherit',
})
stopService = async () => {
// Close all resources related to the subprocess.
await child.close()
initializeWasCalled = false
longLivedService = undefined
stopService = undefined
}
const { readFromStdout, afterClose, service } = common.createChannel({
writeToStdin(bytes) {
child.write(bytes)
},
isSync: false,
hasFS: true,
esbuild: ourselves,
})
const readMoreStdout = () => child.read().then(buffer => {
if (buffer === null) {
afterClose(null)
} else {
readFromStdout(buffer)
readMoreStdout()
}
}).catch(e => {
if (e instanceof Deno.errors.Interrupted || e instanceof Deno.errors.BadResource) {
// ignore the error if read was interrupted (stdout was closed)
afterClose(e)
} else {
throw e
}
})
readMoreStdout()
return {
build: (options: types.BuildOptions) =>
new Promise<types.BuildResult>((resolve, reject) => {
service.buildOrContext({
callName: 'build',
refs: null,
options,
isTTY,
defaultWD,
callback: (err, res) => err ? reject(err) : resolve(res as types.BuildResult),
})
}),
context: (options: types.BuildOptions) =>
new Promise<types.BuildContext>((resolve, reject) =>
service.buildOrContext({
callName: 'context',
refs: null,
options,
isTTY,
defaultWD,
callback: (err, res) => err ? reject(err) : resolve(res as types.BuildContext),
})),
transform: (input: string | Uint8Array, options?: types.TransformOptions) =>
new Promise<types.TransformResult>((resolve, reject) =>
service.transform({
callName: 'transform',
refs: null,
input,
options: options || {},
isTTY,
fs: {
readFile(tempFile, callback) {
Deno.readFile(tempFile).then(
bytes => {
let text = new TextDecoder().decode(bytes)
try {
Deno.remove(tempFile)
} catch (e) {
}
callback(null, text)
},
err => callback(err, null),
)
},
writeFile(contents, callback) {
Deno.makeTempFile().then(
tempFile => Deno.writeFile(tempFile, typeof contents === 'string' ? new TextEncoder().encode(contents) : contents).then(
() => callback(tempFile),
() => callback(null)),
() => callback(null))
},
},
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
formatMessages: (messages, options) =>
new Promise((resolve, reject) =>
service.formatMessages({
callName: 'formatMessages',
refs: null,
messages,
options,
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
analyzeMetafile: (metafile, options) =>
new Promise((resolve, reject) =>
service.analyzeMetafile({
callName: 'analyzeMetafile',
refs: null,
metafile: typeof metafile === 'string' ? metafile : JSON.stringify(metafile),
options,
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
}
})()
}
return longLivedService
}
// If we're called as the main script, forward the CLI to the underlying executable
if (import.meta.main) {
spawn(await install(), {
args: Deno.args,
stdin: 'inherit',
stdout: 'inherit',
stderr: 'inherit',
}).status().then(({ code }) => {
Deno.exit(code)
})
}
-203
View File
@@ -1,203 +0,0 @@
import type * as types from "../shared/types"
import * as common from "../shared/common"
import * as ourselves from "./wasm"
interface Go {
_scheduledTimeouts: Map<number, ReturnType<typeof setTimeout>>
}
declare const ESBUILD_VERSION: string
declare let WEB_WORKER_SOURCE_CODE: string
declare let WEB_WORKER_FUNCTION: (postMessage: (data: Uint8Array) => void) => (event: { data: Uint8Array | ArrayBuffer | WebAssembly.Module }) => Go
export let version = ESBUILD_VERSION
export let build: typeof types.build = (options: types.BuildOptions) =>
ensureServiceIsRunning().then(service =>
service.build(options))
export const context: typeof types.context = (options: types.BuildOptions) =>
ensureServiceIsRunning().then(service =>
service.context(options))
export const transform: typeof types.transform = (input: string | Uint8Array, options?: types.TransformOptions) =>
ensureServiceIsRunning().then(service =>
service.transform(input, options))
export const formatMessages: typeof types.formatMessages = (messages, options) =>
ensureServiceIsRunning().then(service =>
service.formatMessages(messages, options))
export const analyzeMetafile: typeof types.analyzeMetafile = (metafile, options) =>
ensureServiceIsRunning().then(service =>
service.analyzeMetafile(metafile, options))
export const buildSync: typeof types.buildSync = () => {
throw new Error(`The "buildSync" API does not work in Deno`)
}
export const transformSync: typeof types.transformSync = () => {
throw new Error(`The "transformSync" API does not work in Deno`)
}
export const formatMessagesSync: typeof types.formatMessagesSync = () => {
throw new Error(`The "formatMessagesSync" API does not work in Deno`)
}
export const analyzeMetafileSync: typeof types.analyzeMetafileSync = () => {
throw new Error(`The "analyzeMetafileSync" API does not work in Deno`)
}
export const stop = () => {
if (stopService) stopService()
return Promise.resolve()
}
interface Service {
build: typeof types.build
context: typeof types.context
transform: typeof types.transform
formatMessages: typeof types.formatMessages
analyzeMetafile: typeof types.analyzeMetafile
}
let initializePromise: Promise<Service> | undefined
let stopService: (() => void) | undefined
let ensureServiceIsRunning = (): Promise<Service> => {
return initializePromise || startRunningService('esbuild.wasm', undefined, true)
}
export const initialize: typeof types.initialize = async (options) => {
options = common.validateInitializeOptions(options || {})
let wasmURL = options.wasmURL
let wasmModule = options.wasmModule
let useWorker = options.worker !== false
if (initializePromise) throw new Error('Cannot call "initialize" more than once')
initializePromise = startRunningService(wasmURL || 'esbuild.wasm', wasmModule, useWorker)
initializePromise.catch(() => {
// Let the caller try again if this fails
initializePromise = void 0
})
await initializePromise
}
const startRunningService = async (wasmURL: string | URL, wasmModule: WebAssembly.Module | undefined, useWorker: boolean): Promise<Service> => {
let worker: {
onmessage?: ((event: any) => void) | null | undefined
postMessage: (data: Uint8Array | ArrayBuffer | WebAssembly.Module) => void
terminate: () => void
}
if (useWorker) {
// Run esbuild off the main thread
let blob = new Blob([`onmessage=${WEB_WORKER_SOURCE_CODE}(postMessage)`], { type: 'text/javascript' })
worker = new Worker(URL.createObjectURL(blob), { type: 'module' })
} else {
// Run esbuild on the main thread
let onmessage = WEB_WORKER_FUNCTION((data: Uint8Array) => worker.onmessage!({ data }))
let go: Go | undefined
worker = {
onmessage: null,
postMessage: data => setTimeout(() => go = onmessage({ data })),
terminate() {
if (go)
for (let timeout of go._scheduledTimeouts.values())
clearTimeout(timeout)
},
}
}
let firstMessageResolve: (value: void) => void
let firstMessageReject: (error: any) => void
const firstMessagePromise = new Promise((resolve, reject) => {
firstMessageResolve = resolve
firstMessageReject = reject
})
worker.onmessage = ({ data: error }) => {
worker.onmessage = ({ data }) => readFromStdout(data)
if (error) firstMessageReject(error)
else firstMessageResolve()
}
worker.postMessage(wasmModule || new URL(wasmURL, import.meta.url).toString())
let { readFromStdout, service } = common.createChannel({
writeToStdin(bytes) {
worker.postMessage(bytes)
},
isSync: false,
hasFS: false,
esbuild: ourselves,
})
// This will throw if WebAssembly module instantiation fails
await firstMessagePromise
stopService = () => {
worker.terminate()
initializePromise = undefined
stopService = undefined
}
return {
build: (options: types.BuildOptions) =>
new Promise<types.BuildResult>((resolve, reject) =>
service.buildOrContext({
callName: 'build',
refs: null,
options,
isTTY: false,
defaultWD: '/',
callback: (err, res) => err ? reject(err) : resolve(res as types.BuildResult),
})),
context: (options: types.BuildOptions) =>
new Promise<types.BuildContext>((resolve, reject) =>
service.buildOrContext({
callName: 'context',
refs: null,
options,
isTTY: false,
defaultWD: '/',
callback: (err, res) => err ? reject(err) : resolve(res as types.BuildContext),
})),
transform: (input: string | Uint8Array, options?: types.TransformOptions) =>
new Promise<types.TransformResult>((resolve, reject) =>
service.transform({
callName: 'transform',
refs: null,
input,
options: options || {},
isTTY: false,
fs: {
readFile(_, callback) { callback(new Error('Internal error'), null); },
writeFile(_, callback) { callback(null); },
},
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
formatMessages: (messages, options) =>
new Promise((resolve, reject) =>
service.formatMessages({
callName: 'formatMessages',
refs: null,
messages,
options,
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
analyzeMetafile: (metafile, options) =>
new Promise((resolve, reject) =>
service.analyzeMetafile({
callName: 'analyzeMetafile',
refs: null,
metafile: typeof metafile === 'string' ? metafile : JSON.stringify(metafile),
options,
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
}
}
+13 -5
View File
@@ -222,10 +222,9 @@ export let analyzeMetafileSync: typeof types.analyzeMetafileSync = (metafile, op
return result!
}
export const stop = () => {
if (stopService) stopService()
export const stop = async () => {
if (stopService) await stopService()
if (workerThreadService) workerThreadService.stop()
return Promise.resolve()
}
let initializeWasCalled = false
@@ -251,7 +250,7 @@ interface Service {
let defaultWD = process.cwd()
let longLivedService: Service | undefined
let stopService: (() => void) | undefined
let stopService: (() => Promise<void>) | undefined
let ensureServiceIsRunning = (): Service => {
if (longLivedService) return longLivedService
@@ -288,13 +287,22 @@ let ensureServiceIsRunning = (): Service => {
stdout.on('end', afterClose)
stopService = () => {
// Close all resources related to the subprocess.
// Deno's implementation of node's API needs us to have a reference to the
// child while we kill it. See: https://github.com/evanw/esbuild/pull/3701
const promise = new Promise<void>(resolve => child.on('exit', () => {
child.unref()
resolve()
}))
child.ref()
// Close all resources related to the subprocess
stdin.destroy()
stdout.destroy()
child.kill()
initializeWasCalled = false
longLivedService = undefined
stopService = undefined
return promise
}
let refCount = 0
+4
View File
@@ -128,11 +128,13 @@ function pushLogFlags(flags: string[], options: CommonOptions, keys: OptionKeys,
let color = getFlag(options, keys, 'color', mustBeBoolean)
let logLevel = getFlag(options, keys, 'logLevel', mustBeString)
let logLimit = getFlag(options, keys, 'logLimit', mustBeInteger)
let logStyle = getFlag(options, keys, 'logStyle', mustBeString)
if (color !== void 0) flags.push(`--color=${color}`)
else if (isTTY) flags.push(`--color=true`); // This is needed to fix "execFileSync" which buffers stderr
flags.push(`--log-level=${logLevel || logLevelDefault}`)
flags.push(`--log-limit=${logLimit || 0}`)
if (logStyle) flags.push(`--log-style=${logStyle}`)
}
function validateStringValue(value: unknown, what: string, key?: string): string {
@@ -791,6 +793,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
let kind = getFlag(options, keys, 'kind', mustBeString)
let color = getFlag(options, keys, 'color', mustBeBoolean)
let terminalWidth = getFlag(options, keys, 'terminalWidth', mustBeInteger)
let logStyle = getFlag(options, keys, 'logStyle', mustBeString)
checkForInvalidFlags(options, keys, `in ${callName}() call`)
if (kind === void 0) throw new Error(`Missing "kind" in ${callName}() call`)
if (kind !== 'error' && kind !== 'warning') throw new Error(`Expected "kind" to be "error" or "warning" in ${callName}() call`)
@@ -801,6 +804,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
}
if (color !== void 0) request.color = color
if (terminalWidth !== void 0) request.terminalWidth = terminalWidth
if (logStyle !== void 0) request.logStyle = logStyle
sendRequest<protocol.FormatMsgsRequest, protocol.FormatMsgsResponse>(refs, request, (error, response) => {
if (error) return callback(new Error(error), null)
callback(null, response!.messages)
+1
View File
@@ -135,6 +135,7 @@ export interface FormatMsgsRequest {
isWarning: boolean
color?: boolean
terminalWidth?: number
logStyle?: string
}
export interface FormatMsgsResponse {
+6 -11
View File
@@ -2,6 +2,7 @@ export type Platform = 'browser' | 'node' | 'neutral'
export type Format = 'iife' | 'cjs' | 'esm'
export type Loader = 'base64' | 'binary' | 'copy' | 'css' | 'dataurl' | 'default' | 'empty' | 'file' | 'js' | 'json' | 'jsx' | 'local-css' | 'text' | 'ts' | 'tsx'
export type LogLevel = 'verbose' | 'debug' | 'info' | 'warning' | 'error' | 'silent'
export type LogStyle = 'default' | 'clang' | 'visualstudio'
export type Charset = 'ascii' | 'utf8'
export type Drop = 'console' | 'debugger'
export type AbsPaths = 'code' | 'log' | 'metafile'
@@ -86,6 +87,8 @@ interface CommonOptions {
logLimit?: number
/** Documentation: https://esbuild.github.io/api/#log-override */
logOverride?: Record<string, LogLevel>
/** Documentation: https://esbuild.github.io/api/#log-style */
logStyle?: LogStyle
/** Documentation: https://esbuild.github.io/api/#tsconfig-raw */
tsconfigRaw?: string | TsconfigRaw
@@ -509,6 +512,7 @@ export interface FormatMessagesOptions {
kind: 'error' | 'warning'
color?: boolean
terminalWidth?: number
logStyle?: LogStyle
}
export interface AnalyzeMetafileOptions {
@@ -682,23 +686,14 @@ export let version: string
// In node this happens automatically before the parent node process exits. So
// you only need to call this if you know you will not make any more esbuild
// API calls and you want to clean up resources.
//
// Unlike node, Deno lacks the necessary APIs to clean up child processes
// automatically. You must manually call stop() in Deno when you're done
// using esbuild or Deno will continue running forever.
//
// Another reason you might want to call this is if you are using esbuild from
// within a Deno test. Deno fails tests that create a child process without
// killing it before the test ends, so you have to call this function (and
// await the returned promise) in every Deno test that uses esbuild.
export declare function stop(): Promise<void>
// Note: These declarations exist to avoid type errors when you omit "dom" from
// "lib" in your "tsconfig.json" file. TypeScript confusingly declares the
// global "WebAssembly" type in "lib.dom.d.ts" even though it has nothing to do
// with the browser DOM and is present in many non-browser JavaScript runtimes
// (e.g. node and deno). Declaring it here allows esbuild's API to be used in
// these scenarios.
// (e.g. node). Declaring it here allows esbuild's API to be used in these
// scenarios.
//
// There's an open issue about getting this problem corrected (although these
// declarations will need to remain even if this is fixed for backward
-3
View File
@@ -8,7 +8,4 @@
"node"
],
},
"exclude": [
"deno",
],
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/aix-ppc64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The IBM AIX PowerPC 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/android-arm",
"version": "0.28.1",
"version": "0.28.2",
"description": "A WebAssembly shim for esbuild on Android ARM.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/android-arm64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Android ARM 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/android-x64",
"version": "0.28.1",
"version": "0.28.2",
"description": "A WebAssembly shim for esbuild on Android x64.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/darwin-arm64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The macOS ARM 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/darwin-x64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The macOS 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/freebsd-arm64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The FreeBSD ARM 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/freebsd-x64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The FreeBSD 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/linux-arm",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Linux ARM binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/linux-arm64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Linux ARM 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/linux-ia32",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Linux 32-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/linux-loong64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Linux LoongArch 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/linux-mips64el",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Linux MIPS 64-bit Little Endian binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/linux-ppc64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Linux PowerPC 64-bit Little Endian binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/linux-riscv64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Linux RISC-V 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/linux-s390x",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Linux IBM Z 64-bit Big Endian binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/linux-x64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/netbsd-arm64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The NetBSD ARM 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/netbsd-x64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The NetBSD AMD64 binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/openbsd-arm64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The OpenBSD ARM 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/openbsd-x64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The OpenBSD 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/openharmony-arm64",
"version": "0.28.1",
"version": "0.28.2",
"description": "A WebAssembly shim for esbuild on OpenHarmony ARM64.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/sunos-x64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The illumos 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/wasi-preview1",
"version": "0.28.1",
"version": "0.28.2",
"description": "The WASI (WebAssembly System Interface) preview 1 binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/win32-arm64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Windows ARM 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/win32-ia32",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Windows 32-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esbuild/win32-x64",
"version": "0.28.1",
"version": "0.28.2",
"description": "The Windows 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "esbuild-wasm",
"version": "0.28.1",
"version": "0.28.2",
"description": "The cross-platform WebAssembly binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
+50 -50
View File
@@ -1,6 +1,6 @@
{
"name": "esbuild",
"version": "0.28.1",
"version": "0.28.2",
"description": "An extremely fast JavaScript and CSS bundler and minifier.",
"repository": {
"type": "git",
@@ -18,57 +18,57 @@
"esbuild": "bin/esbuild"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
"@esbuild/aix-ppc64": "0.28.2",
"@esbuild/android-arm": "0.28.2",
"@esbuild/android-arm64": "0.28.2",
"@esbuild/android-x64": "0.28.2",
"@esbuild/darwin-arm64": "0.28.2",
"@esbuild/darwin-x64": "0.28.2",
"@esbuild/freebsd-arm64": "0.28.2",
"@esbuild/freebsd-x64": "0.28.2",
"@esbuild/linux-arm": "0.28.2",
"@esbuild/linux-arm64": "0.28.2",
"@esbuild/linux-ia32": "0.28.2",
"@esbuild/linux-loong64": "0.28.2",
"@esbuild/linux-mips64el": "0.28.2",
"@esbuild/linux-ppc64": "0.28.2",
"@esbuild/linux-riscv64": "0.28.2",
"@esbuild/linux-s390x": "0.28.2",
"@esbuild/linux-x64": "0.28.2",
"@esbuild/netbsd-arm64": "0.28.2",
"@esbuild/netbsd-x64": "0.28.2",
"@esbuild/openbsd-arm64": "0.28.2",
"@esbuild/openbsd-x64": "0.28.2",
"@esbuild/openharmony-arm64": "0.28.2",
"@esbuild/sunos-x64": "0.28.2",
"@esbuild/win32-arm64": "0.28.2",
"@esbuild/win32-ia32": "0.28.2",
"@esbuild/win32-x64": "0.28.2"
},
"license": "MIT",
"esbuild.binaryHashes": {
"@esbuild/aix-ppc64/bin/esbuild": "0dd860154344f94b7a9367e64ffc2bc5b89a4313b4a399e5127cdc4ff5000303",
"@esbuild/android-arm64/bin/esbuild": "087671da02d323e7915fafc13567c0929192633ed544bd8adee9bc020fddf5b4",
"@esbuild/darwin-arm64/bin/esbuild": "e2dc9a52440a2a34f09434a2f4843cb1e30f84e40dcf238976ec61ef8cd7f36a",
"@esbuild/darwin-x64/bin/esbuild": "dd53ccf32f9b5b3ab30d41388ef1fc8f81c44ca57ee7a32a7364a1753308d009",
"@esbuild/freebsd-arm64/bin/esbuild": "5fef5a5ef24d9660d7711b756be5ddeba1fb3ca8dd62d703682ebb9fab3b68b0",
"@esbuild/freebsd-x64/bin/esbuild": "4427a8cbfa0661943ccfef2606a65b35f3411ac73e447e86f7732aace61e4609",
"@esbuild/linux-arm/bin/esbuild": "a2d93f7a8b1c9f91d47beec987888b4765c890501b6274f4bdff4061a5e0d9fd",
"@esbuild/linux-arm64/bin/esbuild": "51e829ba36f36be6d9aea6e329ddc4f9350302339b16aaca96a3cb97f64a8ebb",
"@esbuild/linux-ia32/bin/esbuild": "9cd7515a75d6f96b0aa055861cf987498315428088cd6d18de5082f904ae5794",
"@esbuild/linux-loong64/bin/esbuild": "73fa76af9b13f7a27fdb20673b8c2af927fe1511d14e51edeb4b7b71f7f03d0e",
"@esbuild/linux-mips64el/bin/esbuild": "d734787abc3c99d95242805047fe32da85a5822f04bf33d766a966bf5fd85488",
"@esbuild/linux-ppc64/bin/esbuild": "90709144617c2adf8f6c14937f03b114c259de0b9309666e3a9d89a1fb266d04",
"@esbuild/linux-riscv64/bin/esbuild": "86df35a46bedde8b756edb6565a1df7b5a832ad74dd0e179608cbf6509a2582a",
"@esbuild/linux-s390x/bin/esbuild": "f8cad0db35132e5ac3065c4345c36e7ffe18fdc98417a61bcf5a95a6c694e57c",
"@esbuild/linux-x64/bin/esbuild": "0c6588b092a2c291a72bab90659f3c9e0e25e0fe59c9ac12b4dae4d945e5548c",
"@esbuild/netbsd-arm64/bin/esbuild": "490cfdac2d96d5608d91cf55bdf4052bbde7013dafe87ec7adc7784778ad4d4c",
"@esbuild/netbsd-x64/bin/esbuild": "c61e0e4c553a4986fd0e9d63a0425e93be831ff7b13bfb8a564368fb7590f85d",
"@esbuild/openbsd-arm64/bin/esbuild": "f9d814698e26c09c3f90d4c103cbdf3832c9f2c841d64a93889d63d23da9b8cf",
"@esbuild/openbsd-x64/bin/esbuild": "2e30dce11f7236aa362c1c57212cc696f51472169aa417f30e9b641f3b7dcade",
"@esbuild/sunos-x64/bin/esbuild": "770e0d2bc930b1a1d19d3f43243ff8f393efddb599e7f5843160feacd253267d",
"@esbuild/win32-arm64/esbuild.exe": "bfb8798ab678f1ce4a723739f4a3eabab3244d7a04eeb12be2eb9f58095c13ef",
"@esbuild/win32-ia32/esbuild.exe": "8fa99b6e0945830fce8d7e208fdb21763aa4aea875751f4a0eec7f6e262af1dd",
"@esbuild/win32-x64/esbuild.exe": "ec02ee9b14ab332416fedd10614dfb80eed5304d94f67745067c011934a8c3c3"
"@esbuild/aix-ppc64/bin/esbuild": "6de148b932fc5d3b28a97f9f0bc4de8e2b47db478b8ad679e0eaa6b96e00420d",
"@esbuild/android-arm64/bin/esbuild": "84efbbd2cbf21bbfec2aaf304bee79cb2a4a05fe2ea7b861a87f7b8f213ac27d",
"@esbuild/darwin-arm64/bin/esbuild": "10b6243df618d374bb2d5c9cfbe7052e1405f6aa4e53a6164f11a91b9f2e1384",
"@esbuild/darwin-x64/bin/esbuild": "7fe5c9c905fff0a05d92db98929434ab4d3b6dd92d7a7688922db74380c75df3",
"@esbuild/freebsd-arm64/bin/esbuild": "6aba108a68b93064e491bfb1ecfa28843b4d4e504dff0627f53f68f6bb015c01",
"@esbuild/freebsd-x64/bin/esbuild": "8a237e222bee0e50556e7b00d83ec2ff43721bf72f19811a5f0ee56f1d40177c",
"@esbuild/linux-arm/bin/esbuild": "d88f751b22095b2f3b42c8c2e8a3f36fabc1705438c5d6e78366b681f8b4e7a5",
"@esbuild/linux-arm64/bin/esbuild": "90bd269553d258e80b19e3ccab4f98f0831f87442a2f056510ce24fd1b425fc2",
"@esbuild/linux-ia32/bin/esbuild": "b1e2d87d79f9a2baa0d3043cdf0dc6716f9f2eea66c58f47588641fd6019f36f",
"@esbuild/linux-loong64/bin/esbuild": "55a66f9854d6798f36b2b5471db74f2e552481887ea14d8bda1634a512383a43",
"@esbuild/linux-mips64el/bin/esbuild": "6bf7921a3af4a801051e056d85c360fe24bb446f488ef3da25f0b8e5cf973db1",
"@esbuild/linux-ppc64/bin/esbuild": "c30df60d09bb0ba4853d7b06e244ce966890d61ce60cadd9b22335f6379c316a",
"@esbuild/linux-riscv64/bin/esbuild": "36bdcdc8230eef385838cdfeb0d9a1565cbe4b9ace30097a511d77e894de36a1",
"@esbuild/linux-s390x/bin/esbuild": "bbbc1f92a76a5ca6e2976065222e42b02797363a1f119490dda315f74047741f",
"@esbuild/linux-x64/bin/esbuild": "e1698a3d5c6c0798fee4fd3b5cc816651f460c63d390a7a26ea4beb0b1884100",
"@esbuild/netbsd-arm64/bin/esbuild": "62548519d7ae875d060aa8c94bfb8ed38ef191671a5173d9f626bdb6b7516970",
"@esbuild/netbsd-x64/bin/esbuild": "f9d0e59d2b404559e3a0c958164d96960ebf0e04583b878b1d64d8f1c3698fcc",
"@esbuild/openbsd-arm64/bin/esbuild": "e10c45a0b62456632da41df058d7626b2e7cfe710780a2dc7f9f84681753f3aa",
"@esbuild/openbsd-x64/bin/esbuild": "720bbe607736716f2ecd37962436c5589d801966e868a2ae4e0e7e62d1e4e887",
"@esbuild/sunos-x64/bin/esbuild": "01a3bd9183631ca749f34d15601ea21ba6834614184c5c859f99d54bd683203d",
"@esbuild/win32-arm64/esbuild.exe": "530fb3933af14eefe85dfd06502f826e6ef60fdf4e75228ea67c696db312ecb1",
"@esbuild/win32-ia32/esbuild.exe": "b2e62af5c17e00939583be0a0762fdc469f352869a617edc91371b00454c76f0",
"@esbuild/win32-x64/esbuild.exe": "c7bee37877d0aa6a046e52783fa0a2cf1a9ce5579d68bb3083bda99d4bff18ef"
}
}
+12
View File
@@ -135,6 +135,7 @@ const (
ES2023
ES2024
ES2025
ES2026
)
type Loader uint16
@@ -236,6 +237,14 @@ const (
LogLevelError
)
type LogStyle uint8
const (
LogStyleDefault LogStyle = iota
LogStyleClang
LogStyleVisualStudio
)
type Charset uint8
const (
@@ -280,6 +289,7 @@ const (
type BuildOptions struct {
Color StderrColor // Documentation: https://esbuild.github.io/api/#color
LogLevel LogLevel // Documentation: https://esbuild.github.io/api/#log-level
LogStyle LogStyle // Documentation: https://esbuild.github.io/api/#log-style
LogLimit int // Documentation: https://esbuild.github.io/api/#log-limit
LogOverride map[string]LogLevel // Documentation: https://esbuild.github.io/api/#log-override
AbsPaths AbsPaths // Documentation: https://esbuild.github.io/api/#abs-path
@@ -412,6 +422,7 @@ func Build(options BuildOptions) BuildResult {
type TransformOptions struct {
Color StderrColor // Documentation: https://esbuild.github.io/api/#color
LogLevel LogLevel // Documentation: https://esbuild.github.io/api/#log-level
LogStyle LogStyle // Documentation: https://esbuild.github.io/api/#log-style
LogLimit int // Documentation: https://esbuild.github.io/api/#log-limit
LogOverride map[string]LogLevel // Documentation: https://esbuild.github.io/api/#log-override
AbsPaths AbsPaths // Documentation: https://esbuild.github.io/api/#abs-path
@@ -717,6 +728,7 @@ type FormatMessagesOptions struct {
TerminalWidth int
Kind MessageKind
Color bool
LogStyle LogStyle
}
func FormatMessages(msgs []Message, opts FormatMessagesOptions) []string {
+50 -24
View File
@@ -205,6 +205,19 @@ func validateLogLevel(value LogLevel) logger.LogLevel {
}
}
func validateLogStyle(value LogStyle) logger.LogStyle {
switch value {
case LogStyleDefault:
return logger.StyleDefault
case LogStyleClang:
return logger.StyleClang
case LogStyleVisualStudio:
return logger.StyleVisualStudio
default:
panic("Invalid log style")
}
}
func validateASCIIOnly(value Charset) bool {
switch value {
case CharsetDefault, CharsetASCII:
@@ -330,6 +343,8 @@ func validateFeatures(log logger.Log, target Target, engines []Engine) (compat.J
constraints[compat.ES] = compat.Semver{Parts: []int{2024}}
case ES2025:
constraints[compat.ES] = compat.Semver{Parts: []int{2025}}
case ES2026:
constraints[compat.ES] = compat.Semver{Parts: []int{2026}}
case ESNext, DefaultTarget:
default:
panic("Invalid target")
@@ -345,10 +360,15 @@ func validateFeatures(log logger.Log, target Target, engines []Engine) (compat.J
parts = append(parts, patch)
}
}
constraints[convertEngineName(engine.Name)] = compat.Semver{
name := convertEngineName(engine.Name)
new := compat.Semver{
Parts: parts,
PreRelease: match[4],
}
if old, ok := constraints[name]; ok && compat.CompareSemver(old, new) < 0 {
continue
}
constraints[name] = new
continue
}
}
@@ -884,6 +904,7 @@ func contextImpl(buildOpts BuildOptions) (*internalContext, []Message) {
MessageLimit: buildOpts.LogLimit,
Color: validateColor(buildOpts.Color),
LogLevel: validateLogLevel(buildOpts.LogLevel),
LogStyle: validateLogStyle(buildOpts.LogStyle),
PathStyle: extractPathStyle(buildOpts.AbsPaths, LogAbsPath),
Overrides: validateLogOverrides(buildOpts.LogOverride),
}
@@ -1494,10 +1515,7 @@ func rebuildImpl(args rebuildArgs, oldHashes map[string]string) (rebuildState, m
// Scan over the bundle
bundle := bundler.ScanBundle(config.BuildCall, log, realFS, args.caches, args.entryPoints, args.options, timer)
watchData = realFS.WatchData()
// The new build summary remains the same as the old one when there are
// errors. A failed build shouldn't erase the previous successful build.
newHashes := oldHashes
newHashes := make(map[string]string)
// Stop now if there were errors
var results []graph.OutputFile
@@ -1515,27 +1533,26 @@ func rebuildImpl(args rebuildArgs, oldHashes map[string]string) (rebuildState, m
// Stop now if there were errors
if !log.HasErrors() {
result.Metafile = metafile
}
}
// Populate the results to return
var hashBytes [8]byte
result.OutputFiles = make([]OutputFile, len(results))
newHashes = make(map[string]string)
for i, item := range results {
if args.options.WriteToStdout {
item.AbsPath = "<stdout>"
// Populate the results to return
var hashBytes [8]byte
result.OutputFiles = make([]OutputFile, len(results))
for i, item := range results {
if args.options.WriteToStdout {
item.AbsPath = "<stdout>"
}
hasher := xxhash.New()
hasher.Write(item.Contents)
binary.LittleEndian.PutUint64(hashBytes[:], hasher.Sum64())
hash := base64.RawStdEncoding.EncodeToString(hashBytes[:])
result.OutputFiles[i] = OutputFile{
Path: item.AbsPath,
Contents: item.Contents,
Hash: hash,
}
newHashes[item.AbsPath] = hash
}
}
hasher := xxhash.New()
hasher.Write(item.Contents)
binary.LittleEndian.PutUint64(hashBytes[:], hasher.Sum64())
hash := base64.RawStdEncoding.EncodeToString(hashBytes[:])
result.OutputFiles[i] = OutputFile{
Path: item.AbsPath,
Contents: item.Contents,
Hash: hash,
}
newHashes[item.AbsPath] = hash
}
// Write output files before "OnEnd" callbacks run so they can expect
@@ -1563,12 +1580,19 @@ func rebuildImpl(args rebuildArgs, oldHashes map[string]string) (rebuildState, m
}
}
// Only write output files if there were no errors. See this issue
// for why: https://github.com/evanw/esbuild/issues/3643
shouldWriteFiles := !log.HasErrors()
// Process all file operations in parallel
waitGroup := sync.WaitGroup{}
waitGroup.Add(len(results) + len(toDelete))
for _, result := range results {
go func(result graph.OutputFile) {
defer waitGroup.Done()
if !shouldWriteFiles {
return
}
fs.BeforeFileOpen()
defer fs.AfterFileClose()
if oldHash, ok := oldHashes[result.AbsPath]; ok && oldHash == newHashes[result.AbsPath] {
@@ -1690,6 +1714,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult
MessageLimit: transformOpts.LogLimit,
Color: validateColor(transformOpts.Color),
LogLevel: validateLogLevel(transformOpts.LogLevel),
LogStyle: validateLogStyle(transformOpts.LogStyle),
PathStyle: extractPathStyle(transformOpts.AbsPaths, LogAbsPath),
Overrides: validateLogOverrides(transformOpts.LogOverride),
})
@@ -2199,6 +2224,7 @@ func formatMsgsImpl(msgs []Message, opts FormatMessagesOptions) []string {
strings[i] = msg.String(
logger.OutputOptions{
IncludeSource: true,
LogStyle: validateLogStyle(opts.LogStyle),
},
logger.TerminalInfo{
UseColorEscapes: opts.Color,
+18 -1
View File
@@ -153,7 +153,7 @@ func TestFormatMessages(t *testing.T) {
}},
`✘ [ERROR] This is a test
some file.js:100:59:
some file.js:100:200:
100 │ ...defghi ABCDEFGHI
╵ ^
@@ -319,3 +319,20 @@ func TestFormatMessages(t *testing.T) {
`,
)
}
func TestEngines(t *testing.T) {
checkChromeVersions := func(a string, b string) string {
result := api.Transform("a ?? b", api.TransformOptions{
Engines: []api.Engine{
{Name: api.EngineChrome, Version: a},
{Name: api.EngineChrome, Version: b},
},
})
return string(result.Code)
}
test.AssertEqualWithDiff(t, checkChromeVersions("99", "99"), "a ?? b;\n")
test.AssertEqualWithDiff(t, checkChromeVersions("9", "9"), "a != null ? a : b;\n")
test.AssertEqualWithDiff(t, checkChromeVersions("9", "99"), "a != null ? a : b;\n")
test.AssertEqualWithDiff(t, checkChromeVersions("99", "9"), "a != null ? a : b;\n")
}
+37 -14
View File
@@ -227,18 +227,19 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
// Check for a file in the "servedir" directory
if h.servedir != "" && kind != fs.FileEntry {
absPath := h.fs.Join(h.servedir, queryPath)
if symlink, ok := h.fs.EvalSymlinks(absPath); ok {
absPath = symlink
}
if absDir := h.fs.Dir(absPath); absDir != absPath {
if entries, err, _ := h.fs.ReadDirectory(absDir); err == nil {
if entry, _ := entries.Get(h.fs.Base(absPath)); entry != nil && entry.Kind(h.fs) == fs.FileEntry {
if h.keyfileToLower != "" || h.certfileToLower != "" {
if toLower := strings.ToLower(absPath); toLower == h.keyfileToLower || toLower == h.certfileToLower {
// Don't serve the HTTPS key or certificate. This uses a case-
// insensitive check because some file systems are case-sensitive.
go h.notifyRequest(time.Since(start), req, http.StatusForbidden)
res.WriteHeader(http.StatusForbidden)
maybeWriteResponseBody([]byte("403 - Forbidden"))
return
}
if strings.EqualFold(absPath, h.keyfileToLower) || strings.EqualFold(absPath, h.certfileToLower) {
// Don't serve the HTTPS key or certificate. This uses a case-
// insensitive check because some file systems are case-sensitive.
go h.notifyRequest(time.Since(start), req, http.StatusForbidden)
res.WriteHeader(http.StatusForbidden)
maybeWriteResponseBody([]byte("403 - Forbidden"))
return
}
if contents, err, _ := h.fs.OpenFile(absPath); err == nil {
defer contents.Close()
@@ -346,12 +347,28 @@ func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
return
}
// If we get here, the request was successful
if contentType := helpers.MimeTypeByExtension(h.fs.Ext(file.absPath)); contentType != "" {
res.Header().Set("Content-Type", contentType)
} else {
res.Header().Set("Content-Type", "application/octet-stream")
// Try to detect the MIME type
contentType := helpers.MimeTypeByExtension(h.fs.Ext(file.absPath))
if contentType == "" {
if begin == 0 {
contentType = http.DetectContentType(fileBytes)
} else {
// Read the file header for MIME type detection of HTTP range requests
limit := 512
if limit > fileContentsLen {
limit = fileContentsLen
}
if headerBytes, err := file.contents.Read(0, limit); err == nil {
contentType = http.DetectContentType(headerBytes)
}
}
}
if contentType == "" {
contentType = "application/octet-stream"
}
// If we get here, the request was successful
res.Header().Set("Content-Type", contentType)
if isRange {
res.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", begin, end-1, fileContentsLen))
}
@@ -888,6 +905,12 @@ func (ctx *internalContext) Serve(serveOptions ServeOptions) (ServeResult, error
if isHTTPS {
serveOptions.Keyfile, _ = ctx.realFS.Abs(serveOptions.Keyfile)
serveOptions.Certfile, _ = ctx.realFS.Abs(serveOptions.Certfile)
if symlink, ok := ctx.realFS.EvalSymlinks(serveOptions.Keyfile); ok {
serveOptions.Keyfile = symlink
}
if symlink, ok := ctx.realFS.EvalSymlinks(serveOptions.Certfile); ok {
serveOptions.Certfile = symlink
}
}
var shouldStop int32
+30
View File
@@ -836,6 +836,19 @@ func parseOptionsImpl(
transformOpts.LogLevel = logLevel
}
// Make sure this stays in sync with "PrintErrorToStderr"
case strings.HasPrefix(arg, "--log-style="):
value := arg[len("--log-style="):]
logStyle, err := parseLogStyle(value, arg)
if err != nil {
return parseOptionsExtras{}, err
}
if buildOpts != nil {
buildOpts.LogStyle = logStyle
} else {
transformOpts.LogStyle = logStyle
}
case strings.HasPrefix(arg, "'--"):
return parseOptionsExtras{}, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Unexpected single quote character before flag: %s", arg),
@@ -1025,6 +1038,7 @@ func parseTargets(targets []string, arg string) (target api.Target, engines []ap
"es2023": api.ES2023,
"es2024": api.ES2024,
"es2025": api.ES2025,
"es2026": api.ES2026,
}
outer:
@@ -1562,3 +1576,19 @@ func parseLogLevel(value string, arg string) (api.LogLevel, *cli_helpers.ErrorWi
)
}
}
func parseLogStyle(value string, arg string) (api.LogStyle, *cli_helpers.ErrorWithNote) {
switch value {
case "default":
return api.LogStyleDefault, nil
case "clang":
return api.LogStyleClang, nil
case "visualstudio":
return api.LogStyleVisualStudio, nil
default:
return api.LogStyleDefault, cli_helpers.MakeErrorWithNote(
fmt.Sprintf("Invalid value %q in %q", value, arg),
"Valid values are \"default\", \"clang\", or \"visualstudio\".",
)
}
}
+28
View File
@@ -302,6 +302,34 @@
`,
})
},
// See: https://github.com/evanw/esbuild/issues/4497
async cssNestingIssue4497() {
await assertSameColorsWithNestingTransform(esbuild, {
css: `
.a {
color: red;
.b:not(.a .b .b) {
color: green;
& .b:not(& .b .b) {
color: blue;
}
}
}
`,
html: `
<div class="a">
<span>1</span>
<div class="b">
<span>2</span>
<div class="b"><span>3</span></div>
</div>
</div>
`,
})
},
})
async function runTest(test, fn) {
-181
View File
@@ -1,181 +0,0 @@
// 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/[email protected]/path/mod.ts'
import * as asserts from 'https://deno.land/[email protected]/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%
`)
})
+39
View File
@@ -1519,6 +1519,45 @@ for (const target of ['--target=es5', '--target=es6', '--target=es2020']) {
)
}
// Check lowering of logical assignment and nullish coalescing operators
// See: https://github.com/evanw/esbuild/issues/4508
for (const minify of [[], ['--minify']]) {
for (const target of ['es6', 'esnext']) {
tests.push(
test(['in.js', '--outfile=node.js', '--format=esm', '--target=' + target].concat(minify), {
'in.js': `
function foo() {
let x
x ||= {}
return 123
}
if (foo() !== 123) throw 'fail'
`,
}),
test(['in.js', '--outfile=node.js', '--format=esm', '--target=' + target].concat(minify), {
'in.js': `
function foo() {
let x
x &&= {}
return 123
}
if (foo() !== 123) throw 'fail'
`,
}),
test(['in.js', '--outfile=node.js', '--format=esm', '--target=' + target].concat(minify), {
'in.js': `
function foo() {
let x
x ??= {}
return 123
}
if (foo() !== 123) throw 'fail'
`,
}),
)
}
}
let simpleCyclicImportTestCase542 = {
'in.js': `
import {Test} from './lib';
-43
View File
@@ -5,11 +5,9 @@ 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"
@@ -271,46 +269,6 @@ exports.buildWasmLib = async (esbuildPath) => {
}
}
const buildDenoLib = async (esbuildPath) => {
// Generate "deno/mod.js"
const packageJSON = JSON.parse(fs.readFileSync(path.join(npmDir, 'package.json'), 'utf8'))
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),
'--define:ESBUILD_BINARY_HASHES=' + JSON.stringify(packageJSON['esbuild.binaryHashes'], null, 2),
'--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.
@@ -402,7 +360,6 @@ 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()
+67 -1
View File
@@ -77,6 +77,33 @@ let buildTests = {
}
},
async doNotOverwriteInputFiles({ esbuild, testDir }) {
let input = path.join(testDir, 'input.js')
await writeFileAsync(input, 'x=1+2')
try {
await esbuild.build({
entryPoints: [input],
outfile: input,
logLevel: 'silent',
})
throw new Error('Expected build failure')
} catch (e) {
if (!e.errors || !e.errors[0] || !e.errors[0].text.startsWith('Refusing to overwrite input file')) {
throw e
}
}
assert.strictEqual(await readFileAsync(input, 'utf8'), 'x=1+2')
await esbuild.build({
entryPoints: [input],
outfile: input,
logLevel: 'silent',
allowOverwrite: true,
})
assert.strictEqual(await readFileAsync(input, 'utf8'), 'x = 1 + 2;\n')
},
async mangleCacheBuild({ esbuild }) {
var result = await esbuild.build({
stdin: {
@@ -4945,6 +4972,7 @@ let serveTests = {
const big = path.join(testDir, 'big.txt')
const byteCount = 16 * 1024 * 1024
const buffer = require('crypto').randomBytes(byteCount)
for (let i = 0; i < 1; i++) buffer[i] = 0xFF * (i & 1) // Make sure the MIME type is "application/octet-stream"
await writeFileAsync(big, buffer)
const context = await esbuild.context({});
@@ -7589,12 +7617,50 @@ let formatTests = {
const messages = await esbuild.formatMessages([
{ text: 'This is an error' },
{ text: 'Another error', location: { file: 'file.js' } },
{ text: 'Error with line and column', location: { file: 'file.js', line: 3, column: 6 } },
{ text: 'Error with source code', location: { file: 'file.js', line: 3, column: 6, length: 5, lineText: 'foo = 1 + 2;' } },
], {
kind: 'error',
})
assert.strictEqual(messages.length, 2)
assert.strictEqual(messages.length, 4)
assert.strictEqual(messages[0], `${errorIcon} [ERROR] This is an error\n\n`)
assert.strictEqual(messages[1], `${errorIcon} [ERROR] Another error\n\n file.js:0:0:\n 0 │ \n ╵ ^\n\n`)
assert.strictEqual(messages[2], `${errorIcon} [ERROR] Error with line and column\n\n file.js:3:6:\n 3 │ \n ╵ ^\n\n`)
assert.strictEqual(messages[3], `${errorIcon} [ERROR] Error with source code\n\n file.js:3:6:\n 3 │ foo = 1 + 2;\n ╵ ~~~~~\n\n`)
},
async formatMessagesVisualStudio({ esbuild }) {
const messages = await esbuild.formatMessages([
{ text: 'This is an error' },
{ text: 'Another error', location: { file: 'file.js' } },
{ text: 'Error with line and column', location: { file: 'file.js', line: 3, column: 6 } },
{ text: 'Error with source code', location: { file: 'file.js', line: 3, column: 6, length: 5, lineText: 'foo = 1 + 2;' } },
], {
kind: 'error',
logStyle: 'visualstudio',
})
assert.strictEqual(messages.length, 4)
assert.strictEqual(messages[0], `esbuild: error ES0000: This is an error\n`)
assert.strictEqual(messages[1], `file.js: error ES0000: Another error\n`)
assert.strictEqual(messages[2], `file.js(3,7): error ES0000: Error with line and column\n`)
assert.strictEqual(messages[3], `file.js(3,7): error ES0000: Error with source code\n`)
},
async formatMessagesClang({ esbuild }) {
const messages = await esbuild.formatMessages([
{ text: 'This is an error' },
{ text: 'Another error', location: { file: 'file.js' } },
{ text: 'Error with line and column', location: { file: 'file.js', line: 3, column: 6 } },
{ text: 'Error with source code', location: { file: 'file.js', line: 3, column: 6, length: 5, lineText: 'foo = 1 + 2;' } },
], {
kind: 'error',
logStyle: 'clang',
})
assert.strictEqual(messages.length, 4)
assert.strictEqual(messages[0], `error: This is an error\n`)
assert.strictEqual(messages[1], `file.js: error: Another error\n`)
assert.strictEqual(messages[2], `file.js:3:7: error: Error with line and column\n\n^\n`)
assert.strictEqual(messages[3], `file.js:3:7: error: Error with source code\nfoo = 1 + 2;\n ~~~~~\n`)
},
}
+1 -1
View File
@@ -1 +1 @@
0.28.1
0.28.2