diff --git a/scripts/ci/publish-npm-packages.sh b/scripts/ci/publish-npm-packages.sh index c224f0b830..dabef9d779 100755 --- a/scripts/ci/publish-npm-packages.sh +++ b/scripts/ci/publish-npm-packages.sh @@ -168,12 +168,19 @@ sanitize_npm_lookup_output() { printf '%s\n' "$1" \ | sed -E \ -e '/^npm error A complete log of this run can be found in:/d' \ - -e 's#Bearer [A-Za-z0-9._~-]+#Bearer #g' \ - -e 's#([?&]token=)[^[:space:]&]+#\1#g' \ - -e 's#(_authToken=)[^[:space:]]+#\1#g' \ - -e 's#/Users/[^[:space:]]+##g' \ - -e 's#/home/[^[:space:]]+##g' \ - -e 's#/var/folders/[^[:space:]]+##g' + -e "s#Bearer [^][[:space:]\"'),]+#Bearer #g" \ + -e "s#([?&]token=)[^][[:space:]\"'),&]+#\1#g" \ + -e "s#(_authToken=)[^][[:space:]\"'),]+#\1#g" \ + -e 's#"(file://)/[^"]*"#"\1"#g' \ + -e "s#'(file://)/[^']*'#'\1'#g" \ + -e 's#\[(file://)/[^]]*\]#[\1]#g' \ + -e 's#(file://)/[^][[:space:]"),]+#\1#g' \ + -e 's#(^|[[:space:]=(])"((/|[A-Za-z]:[\\/]|\\\\)[^"]*)"#\1""#g' \ + -e "s#(^|[[:space:]=(])'((/|[A-Za-z]:[\\\\/]|\\\\\\\\)[^']*)'#\1''#g" \ + -e 's#\[(/|[A-Za-z]:[\\/]|\\\\)[^]]*\]#[]#g' \ + -e 's#(^|[[:space:]"=(])/[^][[:space:]"),]+#\1#g' \ + -e 's#(^|[[:space:]"=(])[A-Za-z]:[\\/][^][[:space:]"),]+#\1#g' \ + -e 's#(^|[[:space:]"=(])\\\\[^][[:space:]"),]+#\1#g' } ensure_package_names_registered() { diff --git a/scripts/ci/publish-npm-packages.test.ts b/scripts/ci/publish-npm-packages.test.ts index 5d8749d3ef..8ab7c61a4d 100644 --- a/scripts/ci/publish-npm-packages.test.ts +++ b/scripts/ci/publish-npm-packages.test.ts @@ -1,4 +1,5 @@ import { assertEquals, assertStringIncludes } from "#std/assert"; +import { describe, it } from "#veryfront/testing/bdd.ts"; const scriptPath = `${Deno.cwd()}/scripts/ci/publish-npm-packages.sh`; const decoder = new TextDecoder(); @@ -15,240 +16,312 @@ async function runBash( }).output(); } -Deno.test("npm gitHead verification tolerates metadata appearing after 120 seconds", async () => { - const stateDir = await Deno.makeTempDir(); - const countFile = `${stateDir}/npm-view-count`; - await Deno.writeTextFile(countFile, "0"); - - try { - const output = await runBash( - [ - "set -euo pipefail", - 'source "$SCRIPT_PATH"', - "npm() {", - ' count="$(cat "$COUNT_FILE")"', - " count=$((count + 1))", - ' printf "%s" "$count" > "$COUNT_FILE"', - ' if [ "$count" -ge 26 ]; then', - ' printf "%s\\n" "$GITHUB_SHA"', - " fi", - "}", - "sleep() { :; }", - 'wait_for_npm_git_head "@veryfront/ext-auth-jwt"', - ].join("\n"), - { - COUNT_FILE: countFile, - GITHUB_SHA: "expected-commit", - VERSION: "0.1.1069", - }, - ); +describe("npm package publishing", () => { + it("tolerates npm gitHead metadata appearing after 120 seconds", async () => { + const stateDir = await Deno.makeTempDir(); + const countFile = `${stateDir}/npm-view-count`; + await Deno.writeTextFile(countFile, "0"); - assertEquals(output.code, 0, decoder.decode(output.stderr)); - assertEquals(await Deno.readTextFile(countFile), "26"); - } finally { - await Deno.remove(stateDir, { recursive: true }); - } -}); + try { + const output = await runBash( + [ + "set -euo pipefail", + 'source "$SCRIPT_PATH"', + "npm() {", + ' count="$(cat "$COUNT_FILE")"', + " count=$((count + 1))", + ' printf "%s" "$count" > "$COUNT_FILE"', + ' if [ "$count" -ge 26 ]; then', + ' printf "%s\\n" "$GITHUB_SHA"', + " fi", + "}", + "sleep() { :; }", + 'wait_for_npm_git_head "@veryfront/ext-auth-jwt"', + ].join("\n"), + { + COUNT_FILE: countFile, + GITHUB_SHA: "expected-commit", + VERSION: "0.1.1069", + }, + ); -Deno.test("npm gitHead verification fails fast on a wrong non-empty hash", async () => { - const stateDir = await Deno.makeTempDir(); - const countFile = `${stateDir}/npm-view-count`; - const sleepFile = `${stateDir}/sleep-count`; - await Deno.writeTextFile(countFile, "0"); - await Deno.writeTextFile(sleepFile, "0"); - - try { - const output = await runBash( - [ - "set -euo pipefail", - 'source "$SCRIPT_PATH"', - "npm() {", - ' count="$(cat "$COUNT_FILE")"', - " count=$((count + 1))", - ' printf "%s" "$count" > "$COUNT_FILE"', - ' printf "%s\\n" "wrong-commit"', - "}", - "sleep() {", - ' count="$(cat "$SLEEP_FILE")"', - ' printf "%s" "$((count + 1))" > "$SLEEP_FILE"', - "}", - 'if wait_for_npm_git_head "@veryfront/ext-auth-jwt"; then', - " exit 91", - "fi", - ].join("\n"), - { - COUNT_FILE: countFile, - GITHUB_SHA: "expected-commit", - SLEEP_FILE: sleepFile, - VERSION: "0.1.1069", - }, - ); + assertEquals(output.code, 0, decoder.decode(output.stderr)); + assertEquals(await Deno.readTextFile(countFile), "26"); + } finally { + await Deno.remove(stateDir, { recursive: true }); + } + }); - assertEquals(output.code, 0, decoder.decode(output.stderr)); - assertEquals(await Deno.readTextFile(countFile), "1"); - assertEquals(await Deno.readTextFile(sleepFile), "0"); - } finally { - await Deno.remove(stateDir, { recursive: true }); - } -}); + it("fails fast when npm reports a wrong non-empty gitHead", async () => { + const stateDir = await Deno.makeTempDir(); + const countFile = `${stateDir}/npm-view-count`; + const sleepFile = `${stateDir}/sleep-count`; + await Deno.writeTextFile(countFile, "0"); + await Deno.writeTextFile(sleepFile, "0"); -Deno.test("npm release rerun skips a package already published for the commit", async () => { - const stateDir = await Deno.makeTempDir(); - const packageDir = `${stateDir}/package`; - const npmLog = `${stateDir}/npm.log`; - await Deno.mkdir(packageDir); - await Deno.writeTextFile( - `${packageDir}/package.json`, - JSON.stringify({ name: "@veryfront/ext-auth-jwt" }), - ); - await Deno.writeTextFile(npmLog, ""); - - try { - const output = await runBash( - [ - "set -euo pipefail", - 'source "$SCRIPT_PATH"', - "npm() {", - ' printf "%s\\n" "$*" >> "$NPM_LOG"', - ' if [ "$1" = "view" ] && [ "$3" = "gitHead" ]; then', - ' printf "%s\\n" "$GITHUB_SHA"', - " return 0", - " fi", - " return 92", - "}", - "sleep() { return 93; }", - 'release_publish_package_dir "$PACKAGE_DIR"', - ].join("\n"), - { - GITHUB_SHA: "expected-commit", - NPM_LOG: npmLog, - PACKAGE_DIR: packageDir, - VERSION: "0.1.1069", - }, - ); + try { + const output = await runBash( + [ + "set -euo pipefail", + 'source "$SCRIPT_PATH"', + "npm() {", + ' count="$(cat "$COUNT_FILE")"', + " count=$((count + 1))", + ' printf "%s" "$count" > "$COUNT_FILE"', + ' printf "%s\\n" "wrong-commit"', + "}", + "sleep() {", + ' count="$(cat "$SLEEP_FILE")"', + ' printf "%s" "$((count + 1))" > "$SLEEP_FILE"', + "}", + 'if wait_for_npm_git_head "@veryfront/ext-auth-jwt"; then', + " exit 91", + "fi", + ].join("\n"), + { + COUNT_FILE: countFile, + GITHUB_SHA: "expected-commit", + SLEEP_FILE: sleepFile, + VERSION: "0.1.1069", + }, + ); - assertEquals(output.code, 0, decoder.decode(output.stderr)); - const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); - assertEquals(calls, [ - "view @veryfront/ext-auth-jwt@0.1.1069 gitHead", - "view @veryfront/ext-auth-jwt@0.1.1069 gitHead", - ]); - assertStringIncludes( - decoder.decode(output.stdout), - "already published for this commit; skipping npm publish", - ); - } finally { - await Deno.remove(stateDir, { recursive: true }); - } -}); + assertEquals(output.code, 0, decoder.decode(output.stderr)); + assertEquals(await Deno.readTextFile(countFile), "1"); + assertEquals(await Deno.readTextFile(sleepFile), "0"); + } finally { + await Deno.remove(stateDir, { recursive: true }); + } + }); -Deno.test("npm release preflight rejects an E404 unbootstrapped package name", async () => { - const stateDir = await Deno.makeTempDir(); - const npmLog = `${stateDir}/npm.log`; - await Deno.writeTextFile(npmLog, ""); - - try { - const output = await runBash( - [ - "set -euo pipefail", - 'source "$SCRIPT_PATH"', - "package_names_from_workspace() {", - ' printf "%s\\n" "@veryfront/ext-existing" "@veryfront/ext-new"', - "}", - "npm() {", - ' printf "%s\\n" "$*" >> "$NPM_LOG"', - ' if [ "$1" = "view" ] && [ "$2" = "@veryfront/ext-existing@*" ] && [ "$3" = "name" ]; then', - ' printf "%s\\n" "@veryfront/ext-existing"', - " return 0", - " fi", - ' printf "%s\\n" "npm error code E404" >&2', - ' printf "%s\\n" "npm error 404 Not Found - GET https://registry.npmjs.org/@veryfront%2fext-new - Not found" >&2', - " return 1", - "}", - "run_preflight", - ].join("\n"), - { - GITHUB_SHA: "expected-commit", - NPM_LOG: npmLog, - VERSION: "0.1.1189", - }, + it("skips a package already published for the commit on a release rerun", async () => { + const stateDir = await Deno.makeTempDir(); + const packageDir = `${stateDir}/package`; + const npmLog = `${stateDir}/npm.log`; + await Deno.mkdir(packageDir); + await Deno.writeTextFile( + `${packageDir}/package.json`, + JSON.stringify({ name: "@veryfront/ext-auth-jwt" }), ); + await Deno.writeTextFile(npmLog, ""); - assertEquals(output.code, 1, decoder.decode(output.stderr)); - assertStringIncludes( - decoder.decode(output.stderr), - "@veryfront/ext-new is not registered on npm", - ); - assertStringIncludes( - decoder.decode(output.stderr), - "Publish each package once with a prerelease version and a non-latest dist-tag", - ); - assertEquals( - decoder.decode(output.stderr).includes("npm registry lookup failed"), - false, - ); - const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); - assertEquals(calls, [ - "view @veryfront/ext-existing@* name", - "view @veryfront/ext-new@* name", - ]); - } finally { - await Deno.remove(stateDir, { recursive: true }); - } -}); + try { + const output = await runBash( + [ + "set -euo pipefail", + 'source "$SCRIPT_PATH"', + "npm() {", + ' printf "%s\\n" "$*" >> "$NPM_LOG"', + ' if [ "$1" = "view" ] && [ "$3" = "gitHead" ]; then', + ' printf "%s\\n" "$GITHUB_SHA"', + " return 0", + " fi", + " return 92", + "}", + "sleep() { return 93; }", + 'release_publish_package_dir "$PACKAGE_DIR"', + ].join("\n"), + { + GITHUB_SHA: "expected-commit", + NPM_LOG: npmLog, + PACKAGE_DIR: packageDir, + VERSION: "0.1.1069", + }, + ); -Deno.test("npm release preflight reports non-E404 registry lookup failures", async () => { - const stateDir = await Deno.makeTempDir(); - const npmLog = `${stateDir}/npm.log`; - await Deno.writeTextFile(npmLog, ""); - - try { - const output = await runBash( - [ - "set -euo pipefail", - 'source "$SCRIPT_PATH"', - "package_names_from_workspace() {", - ' printf "%s\\n" "@veryfront/ext-existing" "@veryfront/ext-flaky"', - "}", - "npm() {", - ' printf "%s\\n" "$*" >> "$NPM_LOG"', - ' if [ "$1" = "view" ] && [ "$2" = "@veryfront/ext-existing@*" ] && [ "$3" = "name" ]; then', - ' printf "%s\\n" "@veryfront/ext-existing"', - " return 0", - " fi", - ' printf "%s\\n" "npm error code E503" >&2', - ' printf "%s\\n" "npm error 503 Service Unavailable" >&2', - ' printf "%s\\n" "npm error A complete log of this run can be found in: /Users//.npm/_logs/debug.log" >&2', - " return 42", - "}", - "run_preflight", - ].join("\n"), - { - GITHUB_SHA: "expected-commit", - NPM_LOG: npmLog, - VERSION: "0.1.1189", - }, - ); + assertEquals(output.code, 0, decoder.decode(output.stderr)); + const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); + assertEquals(calls, [ + "view @veryfront/ext-auth-jwt@0.1.1069 gitHead", + "view @veryfront/ext-auth-jwt@0.1.1069 gitHead", + ]); + assertStringIncludes( + decoder.decode(output.stdout), + "already published for this commit; skipping npm publish", + ); + } finally { + await Deno.remove(stateDir, { recursive: true }); + } + }); - assertEquals(output.code, 1, decoder.decode(output.stderr)); - const stderr = decoder.decode(output.stderr); - assertStringIncludes( - stderr, - "npm registry lookup failed for @veryfront/ext-flaky (status 42)", - ); - assertStringIncludes(stderr, "npm error code E503"); - assertEquals(stderr.includes("is not registered on npm"), false); - assertEquals( - stderr.includes("Publish each package once with a prerelease version"), - false, - ); - assertEquals(stderr.includes("/Users/"), false); - const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); - assertEquals(calls, [ - "view @veryfront/ext-existing@* name", - "view @veryfront/ext-flaky@* name", - ]); - } finally { - await Deno.remove(stateDir, { recursive: true }); - } + it("rejects an E404 unbootstrapped package name during release preflight", async () => { + const stateDir = await Deno.makeTempDir(); + const npmLog = `${stateDir}/npm.log`; + await Deno.writeTextFile(npmLog, ""); + + try { + const output = await runBash( + [ + "set -euo pipefail", + 'source "$SCRIPT_PATH"', + "package_names_from_workspace() {", + ' printf "%s\\n" "@veryfront/ext-existing" "@veryfront/ext-new"', + "}", + "npm() {", + ' printf "%s\\n" "$*" >> "$NPM_LOG"', + ' if [ "$1" = "view" ] && [ "$2" = "@veryfront/ext-existing@*" ] && [ "$3" = "name" ]; then', + ' printf "%s\\n" "@veryfront/ext-existing"', + " return 0", + " fi", + ' printf "%s\\n" "npm error code E404" >&2', + ' printf "%s\\n" "npm error 404 Not Found - GET https://registry.npmjs.org/@veryfront%2fext-new - Not found" >&2', + " return 1", + "}", + "run_preflight", + ].join("\n"), + { + GITHUB_SHA: "expected-commit", + NPM_LOG: npmLog, + VERSION: "0.1.1189", + }, + ); + + assertEquals(output.code, 1, decoder.decode(output.stderr)); + assertStringIncludes( + decoder.decode(output.stderr), + "@veryfront/ext-new is not registered on npm", + ); + assertStringIncludes( + decoder.decode(output.stderr), + "Publish each package once with a prerelease version and a non-latest dist-tag", + ); + assertEquals( + decoder.decode(output.stderr).includes("npm registry lookup failed"), + false, + ); + const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); + assertEquals(calls, [ + "view @veryfront/ext-existing@* name", + "view @veryfront/ext-new@* name", + ]); + } finally { + await Deno.remove(stateDir, { recursive: true }); + } + }); + + it("reports sanitized non-E404 registry lookup failures", async () => { + const stateDir = await Deno.makeTempDir(); + const npmLog = `${stateDir}/npm.log`; + await Deno.writeTextFile(npmLog, ""); + + try { + const output = await runBash( + [ + "set -euo pipefail", + 'source "$SCRIPT_PATH"', + "package_names_from_workspace() {", + ' printf "%s\\n" "@veryfront/ext-existing" "@veryfront/ext-flaky"', + "}", + "npm() {", + ' printf "%s\\n" "$*" >> "$NPM_LOG"', + ' if [ "$1" = "view" ] && [ "$2" = "@veryfront/ext-existing@*" ] && [ "$3" = "name" ]; then', + ' printf "%s\\n" "@veryfront/ext-existing"', + " return 0", + " fi", + ' printf "%s\\n" "npm error code E503" >&2', + ' printf "%s\\n" "npm error 503 Service Unavailable" >&2', + ' printf "%s\\n" "npm error auth Bearer fixture-token_~-/+=" >&2', + ' printf "%s\\n" "npm error quoted-bearer=\\"Bearer fixture-token_~-/+=\\"" >&2', + ' printf "%s\\n" "npm error comma-bearer=Bearer fixture-token_~-/+=," >&2', + ' printf "%s\\n" "npm error quoted-url=\\"https://registry.npmjs.org/?token=fixture-token_~-/+=\\"" >&2', + ' printf "%s\\n" "npm error comma-url=https://registry.npmjs.org/?token=fixture-token_~-/+=," >&2', + ' printf "%s\\n" "npm error quoted-auth=\\"_authToken=fixture-token_~-/+=\\"" >&2', + ' printf "%s\\n" "npm error comma-auth=_authToken=fixture-token_~-/+=," >&2', + ' printf "%s\\n" "npm error cache=/tmp/npm-private/cache.log" >&2', + ' printf "%s\\n" "npm error quoted=\\"/tmp/npm-private/quoted.log\\"" >&2', + ' printf "%s\\n" "npm error paren-posix=(/tmp/npm-private/paren.log)" >&2', + ' printf "%s\\n" "npm error comma-posix=/tmp/npm-private/comma.log," >&2', + ' printf "%s\\n" "npm error single-posix=\'/tmp/npm-private/single.log\'" >&2', + ' printf "%s\\n" "npm error bracket-posix=[/tmp/npm-private/bracket.log]" >&2', + ' printf "%s\\n" "npm error spaced-posix=\\"/tmp/npm private/spaced.log\\"" >&2', + ' printf "%s\\n" "npm error file-posix=file:///tmp/npm-private/file.log" >&2', + ' printf "%s\\n" "npm error paren-file=(file:///tmp/npm-private/paren.log)" >&2', + ' printf "%s\\n" "npm error comma-file=file:///tmp/npm-private/comma.log," >&2', + ' printf "%s\\n" "npm error file-windows=file:///C:/Users/runner/file.log" >&2', + ' printf "%s\\n" "npm error config C:\\\\Users\\\\runner\\\\.npmrc" >&2', + ' printf "%s\\n" "npm error quoted-win=\\"C:\\\\Users\\\\runner\\\\quoted.log\\"" >&2', + ' printf "%s\\n" "npm error paren-win=(C:\\\\Users\\\\runner\\\\paren.log)" >&2', + ' printf "%s\\n" "npm error comma-win=C:\\\\Users\\\\runner\\\\comma.log," >&2', + ' printf "%s\\n" "npm error single-win=\'C:\\\\Users\\\\CI Runner\\\\single.log\'" >&2', + ' printf "%s\\n" "npm error workspace D:/build/private/package" >&2', + ' printf "%s\\n" "npm error share \\\\\\\\server\\\\private\\\\debug.log" >&2', + ' printf "%s\\n" "npm error quoted-share=\\"\\\\\\\\server\\\\private\\\\quoted.log\\"" >&2', + ' printf "%s\\n" "npm error paren-share=(\\\\\\\\server\\\\private\\\\paren.log)" >&2', + ' printf "%s\\n" "npm error comma-share=\\\\\\\\server\\\\private\\\\comma.log," >&2', + ' printf "%s\\n" "npm error registry https://registry.npmjs.org/@veryfront%2fext-flaky" >&2', + ' printf "%s\\n" "npm error A complete log of this run can be found in: /Users/runner/.npm/_logs/debug.log" >&2', + " return 42", + "}", + "run_preflight", + ].join("\n"), + { + GITHUB_SHA: "expected-commit", + NPM_LOG: npmLog, + VERSION: "0.1.1189", + }, + ); + + assertEquals(output.code, 1, decoder.decode(output.stderr)); + const stderr = decoder.decode(output.stderr); + assertStringIncludes( + stderr, + "npm registry lookup failed for @veryfront/ext-flaky (status 42)", + ); + assertStringIncludes(stderr, "npm error code E503"); + assertStringIncludes(stderr, "Bearer "); + assertStringIncludes(stderr, 'quoted-bearer="Bearer "'); + assertStringIncludes(stderr, "comma-bearer=Bearer ,"); + assertStringIncludes( + stderr, + 'quoted-url="https://registry.npmjs.org/?token="', + ); + assertStringIncludes( + stderr, + "comma-url=https://registry.npmjs.org/?token=,", + ); + assertStringIncludes(stderr, 'quoted-auth="_authToken="'); + assertStringIncludes(stderr, "comma-auth=_authToken=,"); + assertStringIncludes(stderr, "cache="); + assertStringIncludes(stderr, 'quoted=""'); + assertStringIncludes(stderr, "paren-posix=()"); + assertStringIncludes(stderr, "comma-posix=,"); + assertStringIncludes(stderr, "single-posix=''"); + assertStringIncludes(stderr, "bracket-posix=[]"); + assertStringIncludes(stderr, 'spaced-posix=""'); + assertStringIncludes(stderr, "file-posix=file://"); + assertStringIncludes(stderr, "paren-file=(file://)"); + assertStringIncludes(stderr, "comma-file=file://,"); + assertStringIncludes(stderr, "file-windows=file://"); + assertStringIncludes(stderr, "config "); + assertStringIncludes(stderr, 'quoted-win=""'); + assertStringIncludes(stderr, "paren-win=()"); + assertStringIncludes(stderr, "comma-win=,"); + assertStringIncludes(stderr, "single-win=''"); + assertStringIncludes(stderr, 'quoted-share=""'); + assertStringIncludes(stderr, "paren-share=()"); + assertStringIncludes(stderr, "comma-share=,"); + assertStringIncludes( + stderr, + "registry https://registry.npmjs.org/@veryfront%2fext-flaky", + ); + assertEquals(stderr.includes("fixture-token"), false); + assertEquals(stderr.includes("/tmp/"), false); + assertEquals(stderr.includes("C:\\"), false); + assertEquals(stderr.includes("D:/"), false); + assertEquals(stderr.includes("\\\\server"), false); + assertEquals(stderr.includes("is not registered on npm"), false); + assertEquals( + stderr.includes("Publish each package once with a prerelease version"), + false, + ); + assertEquals(stderr.includes("/Users/"), false); + const calls = (await Deno.readTextFile(npmLog)).trim().split("\n"); + assertEquals(calls, [ + "view @veryfront/ext-existing@* name", + "view @veryfront/ext-flaky@* name", + ]); + } finally { + await Deno.remove(stateDir, { recursive: true }); + } + }); }); diff --git a/scripts/deno.lock b/scripts/deno.lock index 4111744f8d..7f18e98b97 100644 --- a/scripts/deno.lock +++ b/scripts/deno.lock @@ -18,6 +18,7 @@ "jsr:@ts-morph/common@0.27": "0.27.0", "npm:@babel/parser@7.29.2": "7.29.2", "npm:@mdx-js/mdx@3.1.1": "3.1.1", + "npm:es-module-lexer@2.3.1": "2.3.1", "npm:esbuild@0.28.1": "0.28.1" }, "jsr": { @@ -365,6 +366,9 @@ "dequal" ] }, + "es-module-lexer@2.3.1": { + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==" + }, "esast-util-from-estree@2.0.0": { "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", "dependencies": [