From 3692a5ecdb5055d31820ba946f06bf651549d6f9 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 2 May 2022 16:11:45 -0500 Subject: [PATCH 01/22] Add falling back to wasm swc build on native load failure (#36612) Follow-up to https://github.com/vercel/next.js/pull/36527 this adds falling back to the wasm swc build when loading the native bindings fails so that we don't block the build on the native dependency being available. This continues off of https://github.com/vercel/next.js/pull/33496 but does not add a postinstall script yet and only downloads the fallback when the native dependency fails to load. --- packages/next/build/swc/index.js | 144 ++++++++++-------- packages/next/compiled/tar/LICENSE | 15 ++ packages/next/compiled/tar/index.js | 1 + packages/next/compiled/tar/package.json | 1 + packages/next/lib/download-wasm-swc.ts | 118 ++++++++++++++ packages/next/package.json | 1 + packages/next/taskfile.js | 11 ++ .../next/telemetry/events/swc-load-failure.ts | 42 ++++- packages/next/types/misc.d.ts | 6 + test/lib/next-modes/base.ts | 3 +- .../required-server-files-i18n.test.ts | 25 +++ yarn.lock | 24 +-- 12 files changed, 309 insertions(+), 82 deletions(-) create mode 100644 packages/next/compiled/tar/LICENSE create mode 100644 packages/next/compiled/tar/index.js create mode 100644 packages/next/compiled/tar/package.json create mode 100644 packages/next/lib/download-wasm-swc.ts diff --git a/packages/next/build/swc/index.js b/packages/next/build/swc/index.js index e5fd01e61063..59bf712787db 100644 --- a/packages/next/build/swc/index.js +++ b/packages/next/build/swc/index.js @@ -1,10 +1,13 @@ +import path from 'path' +import { pathToFileURL } from 'url' import { platform, arch } from 'os' import { platformArchTriples } from 'next/dist/compiled/@napi-rs/triples' -import { version as nextVersion, optionalDependencies } from 'next/package.json' import * as Log from '../output/log' import { getParserOptions } from './options' import { eventSwcLoadFailure } from '../../telemetry/events/swc-load-failure' import { patchIncorrectLockfile } from '../../lib/patch-incorrect-lockfile' +import { downloadWasmSwc } from '../../lib/download-wasm-swc' +import { version as nextVersion } from 'next/package.json' const ArchName = arch() const PlatformName = platform() @@ -12,35 +15,67 @@ const triples = platformArchTriples[PlatformName][ArchName] || [] let nativeBindings let wasmBindings +let downloadWasmPromise +let pendingBindings export const lockfilePatchPromise = {} async function loadBindings() { - if (!lockfilePatchPromise.cur) { - // always run lockfile check once so that it gets patched - // even if it doesn't fail to load locally - lockfilePatchPromise.cur = patchIncorrectLockfile(process.cwd()).catch( - console.error - ) + if (pendingBindings) { + return pendingBindings } + pendingBindings = new Promise(async (resolve, reject) => { + if (!lockfilePatchPromise.cur) { + // always run lockfile check once so that it gets patched + // even if it doesn't fail to load locally + lockfilePatchPromise.cur = patchIncorrectLockfile(process.cwd()).catch( + console.error + ) + } - let attempts = [] - try { - return loadNative() - } catch (a) { - attempts = attempts.concat(a) - } + let attempts = [] + try { + return resolve(loadNative()) + } catch (a) { + attempts = attempts.concat(a) + } - // TODO: fetch wasm and fallback when loading native fails - // so that users aren't blocked on this, we still want to - // report the native load failure so we can patch though - try { - let bindings = await loadWasm() - return bindings - } catch (a) { - attempts = attempts.concat(a) - } + try { + let bindings = await loadWasm() + eventSwcLoadFailure({ wasm: 'enabled' }) + return resolve(bindings) + } catch (a) { + attempts = attempts.concat(a) + } - logLoadFailure(attempts) + try { + // if not installed already download wasm package on-demand + // we download to a custom directory instead of to node_modules + // as node_module import attempts are cached and can't be re-attempted + // x-ref: https://github.com/nodejs/modules/issues/307 + const wasmDirectory = path.join( + path.dirname(require.resolve('next/package.json')), + 'wasm' + ) + if (!downloadWasmPromise) { + downloadWasmPromise = downloadWasmSwc(nextVersion, wasmDirectory) + } + await downloadWasmPromise + let bindings = await loadWasm(pathToFileURL(wasmDirectory).href) + eventSwcLoadFailure({ wasm: 'fallback' }) + + // still log native load attempts so user is + // aware it failed and should be fixed + for (const attempt of attempts) { + Log.warn(attempt) + } + return resolve(bindings) + } catch (a) { + attempts = attempts.concat(a) + } + + logLoadFailure(attempts, true) + }) + return pendingBindings } function loadBindingsSync() { @@ -56,7 +91,7 @@ function loadBindingsSync() { let loggingLoadFailure = false -function logLoadFailure(attempts) { +function logLoadFailure(attempts, triedWasm = false) { // make sure we only emit the event and log the failure once if (loggingLoadFailure) return loggingLoadFailure = true @@ -64,39 +99,8 @@ function logLoadFailure(attempts) { for (let attempt of attempts) { Log.warn(attempt) } - let glibcVersion - let installedSwcPackages - try { - glibcVersion = process.report?.getReport().header.glibcVersionRuntime - } catch (_) {} - - try { - const pkgNames = Object.keys(optionalDependencies || {}).filter((pkg) => - pkg.startsWith('@next/swc') - ) - const installedPkgs = [] - - for (const pkg of pkgNames) { - try { - const { version } = require(`${pkg}/package.json`) - installedPkgs.push(`${pkg}@${version}`) - } catch (_) {} - } - - if (installedPkgs.length > 0) { - installedSwcPackages = installedPkgs.sort().join(',') - } - } catch (_) {} - - eventSwcLoadFailure({ - nextVersion, - glibcVersion, - installedSwcPackages, - arch: process.arch, - platform: process.platform, - nodeVersion: process.versions.node, - }) + eventSwcLoadFailure({ wasm: triedWasm ? 'failed' : undefined }) .then(() => lockfilePatchPromise.cur || Promise.resolve()) .finally(() => { Log.error( @@ -106,7 +110,7 @@ function logLoadFailure(attempts) { }) } -async function loadWasm() { +async function loadWasm(importPath = '') { if (wasmBindings) { return wasmBindings } @@ -114,7 +118,13 @@ async function loadWasm() { let attempts = [] for (let pkg of ['@next/swc-wasm-nodejs', '@next/swc-wasm-web']) { try { - let bindings = await import(pkg) + let pkgPath = pkg + + if (importPath) { + // the import path must be exact when not in node_modules + pkgPath = path.join(importPath, pkg, 'wasm.js') + } + let bindings = await import(pkgPath) if (pkg === '@next/swc-wasm-web') { bindings = await bindings.default() } @@ -139,14 +149,16 @@ async function loadWasm() { } return wasmBindings } catch (e) { - // Do not report attempts to load wasm when it is still experimental - // if (e?.code === 'ERR_MODULE_NOT_FOUND') { - // attempts.push(`Attempted to load ${pkg}, but it was not installed`) - // } else { - // attempts.push( - // `Attempted to load ${pkg}, but an error occurred: ${e.message ?? e}` - // ) - // } + // Only log attempts for loading wasm when loading as fallback + if (importPath) { + if (e?.code === 'ERR_MODULE_NOT_FOUND') { + attempts.push(`Attempted to load ${pkg}, but it was not installed`) + } else { + attempts.push( + `Attempted to load ${pkg}, but an error occurred: ${e.message ?? e}` + ) + } + } } } diff --git a/packages/next/compiled/tar/LICENSE b/packages/next/compiled/tar/LICENSE new file mode 100644 index 000000000000..19129e315fe5 --- /dev/null +++ b/packages/next/compiled/tar/LICENSE @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/next/compiled/tar/index.js b/packages/next/compiled/tar/index.js new file mode 100644 index 000000000000..ae49d0f70e23 --- /dev/null +++ b/packages/next/compiled/tar/index.js @@ -0,0 +1 @@ +(()=>{var t={5952:(t,e,s)=>{"use strict";const i=s(7147);const n=s(1017);const r=i.lchown?"lchown":"chown";const o=i.lchownSync?"lchownSync":"chownSync";const h=i.lchown&&!process.version.match(/v1[1-9]+\./)&&!process.version.match(/v10\.[6-9]/);const lchownSync=(t,e,s)=>{try{return i[o](t,e,s)}catch(t){if(t.code!=="ENOENT")throw t}};const chownSync=(t,e,s)=>{try{return i.chownSync(t,e,s)}catch(t){if(t.code!=="ENOENT")throw t}};const l=h?(t,e,s,n)=>r=>{if(!r||r.code!=="EISDIR")n(r);else i.chown(t,e,s,n)}:(t,e,s,i)=>i;const a=h?(t,e,s)=>{try{return lchownSync(t,e,s)}catch(i){if(i.code!=="EISDIR")throw i;chownSync(t,e,s)}}:(t,e,s)=>lchownSync(t,e,s);const c=process.version;let readdir=(t,e,s)=>i.readdir(t,e,s);let readdirSync=(t,e)=>i.readdirSync(t,e);if(/^v4\./.test(c))readdir=(t,e,s)=>i.readdir(t,s);const chown=(t,e,s,n)=>{i[r](t,e,s,l(t,e,s,(t=>{n(t&&t.code!=="ENOENT"?t:null)})))};const chownrKid=(t,e,s,r,o)=>{if(typeof e==="string")return i.lstat(n.resolve(t,e),((i,n)=>{if(i)return o(i.code!=="ENOENT"?i:null);n.name=e;chownrKid(t,n,s,r,o)}));if(e.isDirectory()){chownr(n.resolve(t,e.name),s,r,(i=>{if(i)return o(i);const h=n.resolve(t,e.name);chown(h,s,r,o)}))}else{const i=n.resolve(t,e.name);chown(i,s,r,o)}};const chownr=(t,e,s,i)=>{readdir(t,{withFileTypes:true},((n,r)=>{if(n){if(n.code==="ENOENT")return i();else if(n.code!=="ENOTDIR"&&n.code!=="ENOTSUP")return i(n)}if(n||!r.length)return chown(t,e,s,i);let o=r.length;let h=null;const then=n=>{if(h)return;if(n)return i(h=n);if(--o===0)return chown(t,e,s,i)};r.forEach((i=>chownrKid(t,i,e,s,then)))}))};const chownrKidSync=(t,e,s,r)=>{if(typeof e==="string"){try{const s=i.lstatSync(n.resolve(t,e));s.name=e;e=s}catch(t){if(t.code==="ENOENT")return;else throw t}}if(e.isDirectory())chownrSync(n.resolve(t,e.name),s,r);a(n.resolve(t,e.name),s,r)};const chownrSync=(t,e,s)=>{let i;try{i=readdirSync(t,{withFileTypes:true})}catch(i){if(i.code==="ENOENT")return;else if(i.code==="ENOTDIR"||i.code==="ENOTSUP")return a(t,e,s);else throw i}if(i&&i.length)i.forEach((i=>chownrKidSync(t,i,e,s)));return a(t,e,s)};t.exports=chownr;chownr.sync=chownrSync},3597:(t,e,s)=>{"use strict";const i=s(4591);const n=s(2361).EventEmitter;const r=s(7147);let o=r.writev;if(!o){const t=process.binding("fs");const e=t.FSReqWrap||t.FSReqCallback;o=(s,i,n,r)=>{const done=(t,e)=>r(t,e,i);const o=new e;o.oncomplete=done;t.writeBuffers(s,i,n,o)}}const h=Symbol("_autoClose");const l=Symbol("_close");const a=Symbol("_ended");const c=Symbol("_fd");const u=Symbol("_finished");const f=Symbol("_flags");const d=Symbol("_flush");const p=Symbol("_handleChunk");const m=Symbol("_makeBuf");const y=Symbol("_mode");const b=Symbol("_needDrain");const w=Symbol("_onerror");const _=Symbol("_onopen");const E=Symbol("_onread");const g=Symbol("_onwrite");const S=Symbol("_open");const R=Symbol("_path");const v=Symbol("_pos");const O=Symbol("_queue");const k=Symbol("_read");const T=Symbol("_readSize");const x=Symbol("_reading");const L=Symbol("_remain");const A=Symbol("_size");const N=Symbol("_write");const I=Symbol("_writing");const D=Symbol("_defaultFlag");const B=Symbol("_errored");class ReadStream extends i{constructor(t,e){e=e||{};super(e);this.readable=true;this.writable=false;if(typeof t!=="string")throw new TypeError("path must be a string");this[B]=false;this[c]=typeof e.fd==="number"?e.fd:null;this[R]=t;this[T]=e.readSize||16*1024*1024;this[x]=false;this[A]=typeof e.size==="number"?e.size:Infinity;this[L]=this[A];this[h]=typeof e.autoClose==="boolean"?e.autoClose:true;if(typeof this[c]==="number")this[k]();else this[S]()}get fd(){return this[c]}get path(){return this[R]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[S](){r.open(this[R],"r",((t,e)=>this[_](t,e)))}[_](t,e){if(t)this[w](t);else{this[c]=e;this.emit("open",e);this[k]()}}[m](){return Buffer.allocUnsafe(Math.min(this[T],this[L]))}[k](){if(!this[x]){this[x]=true;const t=this[m]();if(t.length===0)return process.nextTick((()=>this[E](null,0,t)));r.read(this[c],t,0,t.length,null,((t,e,s)=>this[E](t,e,s)))}}[E](t,e,s){this[x]=false;if(t)this[w](t);else if(this[p](e,s))this[k]()}[l](){if(this[h]&&typeof this[c]==="number"){const t=this[c];this[c]=null;r.close(t,(t=>t?this.emit("error",t):this.emit("close")))}}[w](t){this[x]=true;this[l]();this.emit("error",t)}[p](t,e){let s=false;this[L]-=t;if(t>0)s=super.write(tthis[_](t,e)))}[_](t,e){if(this[D]&&this[f]==="r+"&&t&&t.code==="ENOENT"){this[f]="w";this[S]()}else if(t)this[w](t);else{this[c]=e;this.emit("open",e);this[d]()}}end(t,e){if(t)this.write(t,e);this[a]=true;if(!this[I]&&!this[O].length&&typeof this[c]==="number")this[g](null,0);return this}write(t,e){if(typeof t==="string")t=Buffer.from(t,e);if(this[a]){this.emit("error",new Error("write() after end()"));return false}if(this[c]===null||this[I]||this[O].length){this[O].push(t);this[b]=true;return false}this[I]=true;this[N](t);return true}[N](t){r.write(this[c],t,0,t.length,this[v],((t,e)=>this[g](t,e)))}[g](t,e){if(t)this[w](t);else{if(this[v]!==null)this[v]+=e;if(this[O].length)this[d]();else{this[I]=false;if(this[a]&&!this[u]){this[u]=true;this[l]();this.emit("finish")}else if(this[b]){this[b]=false;this.emit("drain")}}}}[d](){if(this[O].length===0){if(this[a])this[g](null,0)}else if(this[O].length===1)this[N](this[O].pop());else{const t=this[O];this[O]=[];o(this[c],t,this[v],((t,e)=>this[g](t,e)))}}[l](){if(this[h]&&typeof this[c]==="number"){const t=this[c];this[c]=null;r.close(t,(t=>t?this.emit("error",t):this.emit("close")))}}}class WriteStreamSync extends WriteStream{[S](){let t;if(this[D]&&this[f]==="r+"){try{t=r.openSync(this[R],this[f],this[y])}catch(t){if(t.code==="ENOENT"){this[f]="w";return this[S]()}else throw t}}else t=r.openSync(this[R],this[f],this[y]);this[_](null,t)}[l](){if(this[h]&&typeof this[c]==="number"){const t=this[c];this[c]=null;r.closeSync(t);this.emit("close")}}[N](t){let e=true;try{this[g](null,r.writeSync(this[c],t,0,t.length,this[v]));e=false}finally{if(e)try{this[l]()}catch(t){}}}}e.ReadStream=ReadStream;e.ReadStreamSync=ReadStreamSync;e.WriteStream=WriteStream;e.WriteStreamSync=WriteStreamSync},4591:(t,e,s)=>{"use strict";const i=s(2361);const n=s(2781);const r=s(4461);const o=s(1576).StringDecoder;const h=Symbol("EOF");const l=Symbol("maybeEmitEnd");const a=Symbol("emittedEnd");const c=Symbol("emittingEnd");const u=Symbol("closed");const f=Symbol("read");const d=Symbol("flush");const p=Symbol("flushChunk");const m=Symbol("encoding");const y=Symbol("decoder");const b=Symbol("flowing");const w=Symbol("paused");const _=Symbol("resume");const E=Symbol("bufferLength");const g=Symbol("bufferPush");const S=Symbol("bufferShift");const R=Symbol("objectMode");const v=Symbol("destroyed");const O=global._MP_NO_ITERATOR_SYMBOLS_!=="1";const k=O&&Symbol.asyncIterator||Symbol("asyncIterator not implemented");const T=O&&Symbol.iterator||Symbol("iterator not implemented");const isEndish=t=>t==="end"||t==="finish"||t==="prefinish";const isArrayBuffer=t=>t instanceof ArrayBuffer||typeof t==="object"&&t.constructor&&t.constructor.name==="ArrayBuffer"&&t.byteLength>=0;const isArrayBufferView=t=>!Buffer.isBuffer(t)&&ArrayBuffer.isView(t);t.exports=class Minipass extends n{constructor(t){super();this[b]=false;this[w]=false;this.pipes=new r;this.buffer=new r;this[R]=t&&t.objectMode||false;if(this[R])this[m]=null;else this[m]=t&&t.encoding||null;if(this[m]==="buffer")this[m]=null;this[y]=this[m]?new o(this[m]):null;this[h]=false;this[a]=false;this[c]=false;this[u]=false;this.writable=true;this.readable=true;this[E]=0;this[v]=false}get bufferLength(){return this[E]}get encoding(){return this[m]}set encoding(t){if(this[R])throw new Error("cannot set encoding in objectMode");if(this[m]&&t!==this[m]&&(this[y]&&this[y].lastNeed||this[E]))throw new Error("cannot change encoding");if(this[m]!==t){this[y]=t?new o(t):null;if(this.buffer.length)this.buffer=this.buffer.map((t=>this[y].write(t)))}this[m]=t}setEncoding(t){this.encoding=t}get objectMode(){return this[R]}set objectMode(t){this[R]=this[R]||!!t}write(t,e,s){if(this[h])throw new Error("write after end");if(this[v]){this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"}));return true}if(typeof e==="function")s=e,e="utf8";if(!e)e="utf8";if(!this[R]&&!Buffer.isBuffer(t)){if(isArrayBufferView(t))t=Buffer.from(t.buffer,t.byteOffset,t.byteLength);else if(isArrayBuffer(t))t=Buffer.from(t);else if(typeof t!=="string")this.objectMode=true}if(!this.objectMode&&!t.length){const t=this.flowing;if(this[E]!==0)this.emit("readable");if(s)s();return t}if(typeof t==="string"&&!this[R]&&!(e===this[m]&&!this[y].lastNeed)){t=Buffer.from(t,e)}if(Buffer.isBuffer(t)&&this[m])t=this[y].write(t);try{return this.flowing?(this.emit("data",t),this.flowing):(this[g](t),false)}finally{if(this[E]!==0)this.emit("readable");if(s)s()}}read(t){if(this[v])return null;try{if(this[E]===0||t===0||t>this[E])return null;if(this[R])t=null;if(this.buffer.length>1&&!this[R]){if(this.encoding)this.buffer=new r([Array.from(this.buffer).join("")]);else this.buffer=new r([Buffer.concat(Array.from(this.buffer),this[E])])}return this[f](t||null,this.buffer.head.value)}finally{this[l]()}}[f](t,e){if(t===e.length||t===null)this[S]();else{this.buffer.head.value=e.slice(t);e=e.slice(0,t);this[E]-=t}this.emit("data",e);if(!this.buffer.length&&!this[h])this.emit("drain");return e}end(t,e,s){if(typeof t==="function")s=t,t=null;if(typeof e==="function")s=e,e="utf8";if(t)this.write(t,e);if(s)this.once("end",s);this[h]=true;this.writable=false;if(this.flowing||!this[w])this[l]();return this}[_](){if(this[v])return;this[w]=false;this[b]=true;this.emit("resume");if(this.buffer.length)this[d]();else if(this[h])this[l]();else this.emit("drain")}resume(){return this[_]()}pause(){this[b]=false;this[w]=true}get destroyed(){return this[v]}get flowing(){return this[b]}get paused(){return this[w]}[g](t){if(this[R])this[E]+=1;else this[E]+=t.length;return this.buffer.push(t)}[S](){if(this.buffer.length){if(this[R])this[E]-=1;else this[E]-=this.buffer.head.value.length}return this.buffer.shift()}[d](){do{}while(this[p](this[S]()));if(!this.buffer.length&&!this[h])this.emit("drain")}[p](t){return t?(this.emit("data",t),this.flowing):false}pipe(t,e){if(this[v])return;const s=this[a];e=e||{};if(t===process.stdout||t===process.stderr)e.end=false;else e.end=e.end!==false;const i={dest:t,opts:e,ondrain:t=>this[_]()};this.pipes.push(i);t.on("drain",i.ondrain);this[_]();if(s&&i.opts.end)i.dest.end();return t}addListener(t,e){return this.on(t,e)}on(t,e){try{return super.on(t,e)}finally{if(t==="data"&&!this.pipes.length&&!this.flowing)this[_]();else if(isEndish(t)&&this[a]){super.emit(t);this.removeAllListeners(t)}}}get emittedEnd(){return this[a]}[l](){if(!this[c]&&!this[a]&&!this[v]&&this.buffer.length===0&&this[h]){this[c]=true;this.emit("end");this.emit("prefinish");this.emit("finish");if(this[u])this.emit("close");this[c]=false}}emit(t,e){if(t!=="error"&&t!=="close"&&t!==v&&this[v])return;else if(t==="data"){if(!e)return;if(this.pipes.length)this.pipes.forEach((t=>t.dest.write(e)===false&&this.pause()))}else if(t==="end"){if(this[a]===true)return;this[a]=true;this.readable=false;if(this[y]){e=this[y].end();if(e){this.pipes.forEach((t=>t.dest.write(e)));super.emit("data",e)}}this.pipes.forEach((t=>{t.dest.removeListener("drain",t.ondrain);if(t.opts.end)t.dest.end()}))}else if(t==="close"){this[u]=true;if(!this[a]&&!this[v])return}const s=new Array(arguments.length);s[0]=t;s[1]=e;if(arguments.length>2){for(let t=2;t{t.push(e);if(!this[R])t.dataLength+=e.length}));return e.then((()=>t))}concat(){return this[R]?Promise.reject(new Error("cannot concat in objectMode")):this.collect().then((t=>this[R]?Promise.reject(new Error("cannot concat in objectMode")):this[m]?t.join(""):Buffer.concat(t,t.dataLength)))}promise(){return new Promise(((t,e)=>{this.on(v,(()=>e(new Error("stream destroyed"))));this.on("end",(()=>t()));this.on("error",(t=>e(t)))}))}[k](){const next=()=>{const t=this.read();if(t!==null)return Promise.resolve({done:false,value:t});if(this[h])return Promise.resolve({done:true});let e=null;let s=null;const onerr=t=>{this.removeListener("data",ondata);this.removeListener("end",onend);s(t)};const ondata=t=>{this.removeListener("error",onerr);this.removeListener("end",onend);this.pause();e({value:t,done:!!this[h]})};const onend=()=>{this.removeListener("error",onerr);this.removeListener("data",ondata);e({done:true})};const ondestroy=()=>onerr(new Error("stream destroyed"));return new Promise(((t,i)=>{s=i;e=t;this.once(v,ondestroy);this.once("error",onerr);this.once("end",onend);this.once("data",ondata)}))};return{next:next}}[T](){const next=()=>{const t=this.read();const e=t===null;return{value:t,done:e}};return{next:next}}destroy(t){if(this[v]){if(t)this.emit("error",t);else this.emit(v);return this}this[v]=true;this.buffer=new r;this[E]=0;if(typeof this.close==="function"&&!this[u])this.close();if(t)this.emit("error",t);else this.emit(v);return this}static isStream(t){return!!t&&(t instanceof Minipass||t instanceof n||t instanceof i&&(typeof t.pipe==="function"||typeof t.write==="function"&&typeof t.end==="function"))}}},8319:(t,e,s)=>{const i=s(9796).constants||{ZLIB_VERNUM:4736};t.exports=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:Infinity,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},i))},7547:(t,e,s)=>{"use strict";const i=s(9491);const n=s(4300).Buffer;const r=s(9796);const o=e.constants=s(8319);const h=s(4591);const l=n.concat;const a=Symbol("_superWrite");class ZlibError extends Error{constructor(t){super("zlib: "+t.message);this.code=t.code;this.errno=t.errno;if(!this.code)this.code="ZLIB_ERROR";this.message="zlib: "+t.message;Error.captureStackTrace(this,this.constructor)}get name(){return"ZlibError"}}const c=Symbol("opts");const u=Symbol("flushFlag");const f=Symbol("finishFlushFlag");const d=Symbol("fullFlushFlag");const p=Symbol("handle");const m=Symbol("onError");const y=Symbol("sawError");const b=Symbol("level");const w=Symbol("strategy");const _=Symbol("ended");const E=Symbol("_defaultFullFlush");class ZlibBase extends h{constructor(t,e){if(!t||typeof t!=="object")throw new TypeError("invalid options for ZlibBase constructor");super(t);this[y]=false;this[_]=false;this[c]=t;this[u]=t.flush;this[f]=t.finishFlush;try{this[p]=new r[e](t)}catch(t){throw new ZlibError(t)}this[m]=t=>{if(this[y])return;this[y]=true;this.close();this.emit("error",t)};this[p].on("error",(t=>this[m](new ZlibError(t))));this.once("end",(()=>this.close))}close(){if(this[p]){this[p].close();this[p]=null;this.emit("close")}}reset(){if(!this[y]){i(this[p],"zlib binding closed");return this[p].reset()}}flush(t){if(this.ended)return;if(typeof t!=="number")t=this[d];this.write(Object.assign(n.alloc(0),{[u]:t}))}end(t,e,s){if(t)this.write(t,e);this.flush(this[f]);this[_]=true;return super.end(null,null,s)}get ended(){return this[_]}write(t,e,s){if(typeof e==="function")s=e,e="utf8";if(typeof t==="string")t=n.from(t,e);if(this[y])return;i(this[p],"zlib binding closed");const r=this[p]._handle;const o=r.close;r.close=()=>{};const h=this[p].close;this[p].close=()=>{};n.concat=t=>t;let c;try{const e=typeof t[u]==="number"?t[u]:this[u];c=this[p]._processChunk(t,e);n.concat=l}catch(t){n.concat=l;this[m](new ZlibError(t))}finally{if(this[p]){this[p]._handle=r;r.close=o;this[p].close=h;this[p].removeAllListeners("error")}}if(this[p])this[p].on("error",(t=>this[m](new ZlibError(t))));let f;if(c){if(Array.isArray(c)&&c.length>0){f=this[a](n.from(c[0]));for(let t=1;t{this.flush(t);e()};try{this[p].params(t,e)}finally{this[p].flush=s}if(this[p]){this[b]=t;this[w]=e}}}}class Deflate extends Zlib{constructor(t){super(t,"Deflate")}}class Inflate extends Zlib{constructor(t){super(t,"Inflate")}}const g=Symbol("_portable");class Gzip extends Zlib{constructor(t){super(t,"Gzip");this[g]=t&&!!t.portable}[a](t){if(!this[g])return super[a](t);this[g]=false;t[9]=255;return super[a](t)}}class Gunzip extends Zlib{constructor(t){super(t,"Gunzip")}}class DeflateRaw extends Zlib{constructor(t){super(t,"DeflateRaw")}}class InflateRaw extends Zlib{constructor(t){super(t,"InflateRaw")}}class Unzip extends Zlib{constructor(t){super(t,"Unzip")}}class Brotli extends ZlibBase{constructor(t,e){t=t||{};t.flush=t.flush||o.BROTLI_OPERATION_PROCESS;t.finishFlush=t.finishFlush||o.BROTLI_OPERATION_FINISH;super(t,e);this[d]=o.BROTLI_OPERATION_FLUSH}}class BrotliCompress extends Brotli{constructor(t){super(t,"BrotliCompress")}}class BrotliDecompress extends Brotli{constructor(t){super(t,"BrotliDecompress")}}e.Deflate=Deflate;e.Inflate=Inflate;e.Gzip=Gzip;e.Gunzip=Gunzip;e.DeflateRaw=DeflateRaw;e.InflateRaw=InflateRaw;e.Unzip=Unzip;if(typeof r.BrotliCompress==="function"){e.BrotliCompress=BrotliCompress;e.BrotliDecompress=BrotliDecompress}else{e.BrotliCompress=e.BrotliDecompress=class{constructor(){throw new Error("Brotli is not supported in this version of Node.js")}}}},1389:t=>{"use strict";t.exports=function(t){t.prototype[Symbol.iterator]=function*(){for(let t=this.head;t;t=t.next){yield t.value}}}},4461:(t,e,s)=>{"use strict";t.exports=Yallist;Yallist.Node=Node;Yallist.create=Yallist;function Yallist(t){var e=this;if(!(e instanceof Yallist)){e=new Yallist}e.tail=null;e.head=null;e.length=0;if(t&&typeof t.forEach==="function"){t.forEach((function(t){e.push(t)}))}else if(arguments.length>0){for(var s=0,i=arguments.length;s1){s=e}else if(this.head){i=this.head.next;s=this.head.value}else{throw new TypeError("Reduce of empty list with no initial value")}for(var n=0;i!==null;n++){s=t(s,i.value,n);i=i.next}return s};Yallist.prototype.reduceReverse=function(t,e){var s;var i=this.tail;if(arguments.length>1){s=e}else if(this.tail){i=this.tail.prev;s=this.tail.value}else{throw new TypeError("Reduce of empty list with no initial value")}for(var n=this.length-1;i!==null;n--){s=t(s,i.value,n);i=i.prev}return s};Yallist.prototype.toArray=function(){var t=new Array(this.length);for(var e=0,s=this.head;s!==null;e++){t[e]=s.value;s=s.next}return t};Yallist.prototype.toArrayReverse=function(){var t=new Array(this.length);for(var e=0,s=this.tail;s!==null;e++){t[e]=s.value;s=s.prev}return t};Yallist.prototype.slice=function(t,e){e=e||this.length;if(e<0){e+=this.length}t=t||0;if(t<0){t+=this.length}var s=new Yallist;if(ethis.length){e=this.length}for(var i=0,n=this.head;n!==null&&ithis.length){e=this.length}for(var i=this.length,n=this.tail;n!==null&&i>e;i--){n=n.prev}for(;n!==null&&i>t;i--,n=n.prev){s.push(n.value)}return s};Yallist.prototype.splice=function(t,e,...s){if(t>this.length){t=this.length-1}if(t<0){t=this.length+t}for(var i=0,n=this.head;n!==null&&i{const i=s(334);const n=s(2339);const{mkdirpNative:r,mkdirpNativeSync:o}=s(2302);const{mkdirpManual:h,mkdirpManualSync:l}=s(9435);const{useNative:a,useNativeSync:c}=s(416);const mkdirp=(t,e)=>{t=n(t);e=i(e);return a(e)?r(t,e):h(t,e)};const mkdirpSync=(t,e)=>{t=n(t);e=i(e);return c(e)?o(t,e):l(t,e)};mkdirp.sync=mkdirpSync;mkdirp.native=(t,e)=>r(n(t),i(e));mkdirp.manual=(t,e)=>h(n(t),i(e));mkdirp.nativeSync=(t,e)=>o(n(t),i(e));mkdirp.manualSync=(t,e)=>l(n(t),i(e));t.exports=mkdirp},3423:(t,e,s)=>{const{dirname:i}=s(1017);const findMade=(t,e,s=undefined)=>{if(s===e)return Promise.resolve();return t.statAsync(e).then((t=>t.isDirectory()?s:undefined),(s=>s.code==="ENOENT"?findMade(t,i(e),e):undefined))};const findMadeSync=(t,e,s=undefined)=>{if(s===e)return undefined;try{return t.statSync(e).isDirectory()?s:undefined}catch(s){return s.code==="ENOENT"?findMadeSync(t,i(e),e):undefined}};t.exports={findMade:findMade,findMadeSync:findMadeSync}},9435:(t,e,s)=>{const{dirname:i}=s(1017);const mkdirpManual=(t,e,s)=>{e.recursive=false;const n=i(t);if(n===t){return e.mkdirAsync(t,e).catch((t=>{if(t.code!=="EISDIR")throw t}))}return e.mkdirAsync(t,e).then((()=>s||t),(i=>{if(i.code==="ENOENT")return mkdirpManual(n,e).then((s=>mkdirpManual(t,e,s)));if(i.code!=="EEXIST"&&i.code!=="EROFS")throw i;return e.statAsync(t).then((t=>{if(t.isDirectory())return s;else throw i}),(()=>{throw i}))}))};const mkdirpManualSync=(t,e,s)=>{const n=i(t);e.recursive=false;if(n===t){try{return e.mkdirSync(t,e)}catch(t){if(t.code!=="EISDIR")throw t;else return}}try{e.mkdirSync(t,e);return s||t}catch(i){if(i.code==="ENOENT")return mkdirpManualSync(t,e,mkdirpManualSync(n,e,s));if(i.code!=="EEXIST"&&i.code!=="EROFS")throw i;try{if(!e.statSync(t).isDirectory())throw i}catch(t){throw i}}};t.exports={mkdirpManual:mkdirpManual,mkdirpManualSync:mkdirpManualSync}},2302:(t,e,s)=>{const{dirname:i}=s(1017);const{findMade:n,findMadeSync:r}=s(3423);const{mkdirpManual:o,mkdirpManualSync:h}=s(9435);const mkdirpNative=(t,e)=>{e.recursive=true;const s=i(t);if(s===t)return e.mkdirAsync(t,e);return n(e,t).then((s=>e.mkdirAsync(t,e).then((()=>s)).catch((s=>{if(s.code==="ENOENT")return o(t,e);else throw s}))))};const mkdirpNativeSync=(t,e)=>{e.recursive=true;const s=i(t);if(s===t)return e.mkdirSync(t,e);const n=r(e,t);try{e.mkdirSync(t,e);return n}catch(s){if(s.code==="ENOENT")return h(t,e);else throw s}};t.exports={mkdirpNative:mkdirpNative,mkdirpNativeSync:mkdirpNativeSync}},334:(t,e,s)=>{const{promisify:i}=s(3837);const n=s(7147);const optsArg=t=>{if(!t)t={mode:511,fs:n};else if(typeof t==="object")t={mode:511,fs:n,...t};else if(typeof t==="number")t={mode:t,fs:n};else if(typeof t==="string")t={mode:parseInt(t,8),fs:n};else throw new TypeError("invalid options argument");t.mkdir=t.mkdir||t.fs.mkdir||n.mkdir;t.mkdirAsync=i(t.mkdir);t.stat=t.stat||t.fs.stat||n.stat;t.statAsync=i(t.stat);t.statSync=t.statSync||t.fs.statSync||n.statSync;t.mkdirSync=t.mkdirSync||t.fs.mkdirSync||n.mkdirSync;return t};t.exports=optsArg},2339:(t,e,s)=>{const i=process.env.__TESTING_MKDIRP_PLATFORM__||process.platform;const{resolve:n,parse:r}=s(1017);const pathArg=t=>{if(/\0/.test(t)){throw Object.assign(new TypeError("path must be a string without null bytes"),{path:t,code:"ERR_INVALID_ARG_VALUE"})}t=n(t);if(i==="win32"){const e=/[*|"<>?:]/;const{root:s}=r(t);if(e.test(t.substr(s.length))){throw Object.assign(new Error("Illegal characters in path."),{path:t,code:"EINVAL"})}}return t};t.exports=pathArg},416:(t,e,s)=>{const i=s(7147);const n=process.env.__TESTING_MKDIRP_NODE_VERSION__||process.version;const r=n.replace(/^v/,"").split(".");const o=+r[0]>10||+r[0]===10&&+r[1]>=12;const h=!o?()=>false:t=>t.mkdir===i.mkdir;const l=!o?()=>false:t=>t.mkdirSync===i.mkdirSync;t.exports={useNative:h,useNativeSync:l}},6036:(t,e,s)=>{"use strict";const i=s(6924);const n=s(9698);const r=s(3597);const o=s(9650);const h=s(1017);t.exports=(t,e,s)=>{if(typeof e==="function")s=e;if(Array.isArray(t))e=t,t={};if(!e||!Array.isArray(e)||!e.length)throw new TypeError("no files or directories specified");e=Array.from(e);const n=i(t);if(n.sync&&typeof s==="function")throw new TypeError("callback not supported for sync tar functions");if(!n.file&&typeof s==="function")throw new TypeError("callback only supported with file option");return n.file&&n.sync?createFileSync(n,e):n.file?createFile(n,e,s):n.sync?createSync(n,e):create(n,e)};const createFileSync=(t,e)=>{const s=new n.Sync(t);const i=new r.WriteStreamSync(t.file,{mode:t.mode||438});s.pipe(i);addFilesSync(s,e)};const createFile=(t,e,s)=>{const i=new n(t);const o=new r.WriteStream(t.file,{mode:t.mode||438});i.pipe(o);const h=new Promise(((t,e)=>{o.on("error",e);o.on("close",t);i.on("error",e)}));addFilesAsync(i,e);return s?h.then(s,s):h};const addFilesSync=(t,e)=>{e.forEach((e=>{if(e.charAt(0)==="@"){o({file:h.resolve(t.cwd,e.substr(1)),sync:true,noResume:true,onentry:e=>t.add(e)})}else t.add(e)}));t.end()};const addFilesAsync=(t,e)=>{while(e.length){const s=e.shift();if(s.charAt(0)==="@"){return o({file:h.resolve(t.cwd,s.substr(1)),noResume:true,onentry:e=>t.add(e)}).then((s=>addFilesAsync(t,e)))}else t.add(s)}t.end()};const createSync=(t,e)=>{const s=new n.Sync(t);addFilesSync(s,e);return s};const create=(t,e)=>{const s=new n(t);addFilesAsync(s,e);return s}},7171:(t,e,s)=>{"use strict";const i=s(6924);const n=s(7932);const r=s(7147);const o=s(3597);const h=s(1017);const l=s(4387);t.exports=(t,e,s)=>{if(typeof t==="function")s=t,e=null,t={};else if(Array.isArray(t))e=t,t={};if(typeof e==="function")s=e,e=null;if(!e)e=[];else e=Array.from(e);const n=i(t);if(n.sync&&typeof s==="function")throw new TypeError("callback not supported for sync tar functions");if(!n.file&&typeof s==="function")throw new TypeError("callback only supported with file option");if(e.length)filesFilter(n,e);return n.file&&n.sync?extractFileSync(n):n.file?extractFile(n,s):n.sync?extractSync(n):extract(n)};const filesFilter=(t,e)=>{const s=new Map(e.map((t=>[l(t),true])));const i=t.filter;const mapHas=(t,e)=>{const i=e||h.parse(t).root||".";const n=t===i?false:s.has(t)?s.get(t):mapHas(h.dirname(t),i);s.set(t,n);return n};t.filter=i?(t,e)=>i(t,e)&&mapHas(l(t)):t=>mapHas(l(t))};const extractFileSync=t=>{const e=new n.Sync(t);const s=t.file;const i=r.statSync(s);const h=t.maxReadSize||16*1024*1024;const l=new o.ReadStreamSync(s,{readSize:h,size:i.size});l.pipe(e)};const extractFile=(t,e)=>{const s=new n(t);const i=t.maxReadSize||16*1024*1024;const h=t.file;const l=new Promise(((t,e)=>{s.on("error",e);s.on("close",t);r.stat(h,((t,n)=>{if(t)e(t);else{const t=new o.ReadStream(h,{readSize:i,size:n.size});t.on("error",e);t.pipe(s)}}))}));return e?l.then(e,e):l};const extractSync=t=>new n.Sync(t);const extract=t=>new n(t)},2142:(t,e,s)=>{const i=process.env.__FAKE_PLATFORM__||process.platform;const n=i==="win32";const r=global.__FAKE_TESTING_FS__||s(7147);const{O_CREAT:o,O_TRUNC:h,O_WRONLY:l,UV_FS_O_FILEMAP:a=0}=r.constants;const c=n&&!!a;const u=512*1024;const f=a|h|o|l;t.exports=!c?()=>"w":t=>t{"use strict";const i=s(8318);const n=s(1017).posix;const r=s(9038);const o=Symbol("slurp");const h=Symbol("type");class Header{constructor(t,e,s,i){this.cksumValid=false;this.needPax=false;this.nullBlock=false;this.block=null;this.path=null;this.mode=null;this.uid=null;this.gid=null;this.size=null;this.mtime=null;this.cksum=null;this[h]="0";this.linkpath=null;this.uname=null;this.gname=null;this.devmaj=0;this.devmin=0;this.atime=null;this.ctime=null;if(Buffer.isBuffer(t))this.decode(t,e||0,s,i);else if(t)this.set(t)}decode(t,e,s,i){if(!e)e=0;if(!t||!(t.length>=e+512))throw new Error("need 512 bytes for header");this.path=decString(t,e,100);this.mode=decNumber(t,e+100,8);this.uid=decNumber(t,e+108,8);this.gid=decNumber(t,e+116,8);this.size=decNumber(t,e+124,12);this.mtime=decDate(t,e+136,12);this.cksum=decNumber(t,e+148,12);this[o](s);this[o](i,true);this[h]=decString(t,e+156,1);if(this[h]==="")this[h]="0";if(this[h]==="0"&&this.path.substr(-1)==="/")this[h]="5";if(this[h]==="5")this.size=0;this.linkpath=decString(t,e+157,100);if(t.slice(e+257,e+265).toString()==="ustar\x0000"){this.uname=decString(t,e+265,32);this.gname=decString(t,e+297,32);this.devmaj=decNumber(t,e+329,8);this.devmin=decNumber(t,e+337,8);if(t[e+475]!==0){const s=decString(t,e+345,155);this.path=s+"/"+this.path}else{const s=decString(t,e+345,130);if(s)this.path=s+"/"+this.path;this.atime=decDate(t,e+476,12);this.ctime=decDate(t,e+488,12)}}let n=8*32;for(let s=e;s=e+512))throw new Error("need 512 bytes for header");const s=this.ctime||this.atime?130:155;const i=splitPrefix(this.path||"",s);const n=i[0];const r=i[1];this.needPax=i[2];this.needPax=encString(t,e,100,n)||this.needPax;this.needPax=encNumber(t,e+100,8,this.mode)||this.needPax;this.needPax=encNumber(t,e+108,8,this.uid)||this.needPax;this.needPax=encNumber(t,e+116,8,this.gid)||this.needPax;this.needPax=encNumber(t,e+124,12,this.size)||this.needPax;this.needPax=encDate(t,e+136,12,this.mtime)||this.needPax;t[e+156]=this[h].charCodeAt(0);this.needPax=encString(t,e+157,100,this.linkpath)||this.needPax;t.write("ustar\x0000",e+257,8);this.needPax=encString(t,e+265,32,this.uname)||this.needPax;this.needPax=encString(t,e+297,32,this.gname)||this.needPax;this.needPax=encNumber(t,e+329,8,this.devmaj)||this.needPax;this.needPax=encNumber(t,e+337,8,this.devmin)||this.needPax;this.needPax=encString(t,e+345,s,r)||this.needPax;if(t[e+475]!==0)this.needPax=encString(t,e+345,155,r)||this.needPax;else{this.needPax=encString(t,e+345,130,r)||this.needPax;this.needPax=encDate(t,e+476,12,this.atime)||this.needPax;this.needPax=encDate(t,e+488,12,this.ctime)||this.needPax}let o=8*32;for(let s=e;s{const s=100;let i=t;let r="";let o;const h=n.parse(t).root||".";if(Buffer.byteLength(i)s&&Buffer.byteLength(r)<=e)o=[i.substr(0,s-1),r,true];else{i=n.join(n.basename(r),i);r=n.dirname(r)}}while(r!==h&&!o);if(!o)o=[t.substr(0,s-1),"",true]}return o};const decString=(t,e,s)=>t.slice(e,e+s).toString("utf8").replace(/\0.*/,"");const decDate=(t,e,s)=>numToDate(decNumber(t,e,s));const numToDate=t=>t===null?null:new Date(t*1e3);const decNumber=(t,e,s)=>t[e]&128?r.parse(t.slice(e,e+s)):decSmallNumber(t,e,s);const nanNull=t=>isNaN(t)?null:t;const decSmallNumber=(t,e,s)=>nanNull(parseInt(t.slice(e,e+s).toString("utf8").replace(/\0.*$/,"").trim(),8));const l={12:8589934591,8:2097151};const encNumber=(t,e,s,i)=>i===null?false:i>l[s]||i<0?(r.encode(i,t.slice(e,e+s)),true):(encSmallNumber(t,e,s,i),false);const encSmallNumber=(t,e,s,i)=>t.write(octalString(i,s),e,s,"ascii");const octalString=(t,e)=>padOctal(Math.floor(t).toString(8),e);const padOctal=(t,e)=>(t.length===e-1?t:new Array(e-t.length-1).join("0")+t+" ")+"\0";const encDate=(t,e,s,i)=>i===null?false:encNumber(t,e,s,i.getTime()/1e3);const a=new Array(156).join("\0");const encString=(t,e,s,i)=>i===null?false:(t.write(i+a,e,s,"utf8"),i.length!==Buffer.byteLength(i)||i.length>s);t.exports=Header},6924:t=>{"use strict";const e=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"]]);t.exports=t=>t?Object.keys(t).map((s=>[e.has(s)?e.get(s):s,t[s]])).reduce(((t,e)=>(t[e[0]]=e[1],t)),Object.create(null)):{}},9038:t=>{"use strict";const encode=(t,e)=>{if(!Number.isSafeInteger(t))throw Error("cannot encode number outside of javascript safe integer range");else if(t<0)encodeNegative(t,e);else encodePositive(t,e);return e};const encodePositive=(t,e)=>{e[0]=128;for(var s=e.length;s>1;s--){e[s-1]=t&255;t=Math.floor(t/256)}};const encodeNegative=(t,e)=>{e[0]=255;var s=false;t=t*-1;for(var i=e.length;i>1;i--){var n=t&255;t=Math.floor(t/256);if(s)e[i-1]=onesComp(n);else if(n===0)e[i-1]=0;else{s=true;e[i-1]=twosComp(n)}}};const parse=t=>{const e=t[0];const s=e===128?pos(t.slice(1,t.length)):e===255?twos(t):null;if(s===null)throw Error("invalid base256 encoding");if(!Number.isSafeInteger(s))throw Error("parsed number outside of javascript safe integer range");return s};const twos=t=>{var e=t.length;var s=0;var i=false;for(var n=e-1;n>-1;n--){var r=t[n];var o;if(i)o=onesComp(r);else if(r===0)o=r;else{i=true;o=twosComp(r)}if(o!==0)s-=o*Math.pow(256,e-n-1)}return s};const pos=t=>{var e=t.length;var s=0;for(var i=e-1;i>-1;i--){var n=t[i];if(n!==0)s+=n*Math.pow(256,e-i-1)}return s};const onesComp=t=>(255^t)&255;const twosComp=t=>(255^t)+1&255;t.exports={encode:encode,parse:parse}},9650:(t,e,s)=>{"use strict";const i=s(6924);const n=s(2801);const r=s(7147);const o=s(3597);const h=s(1017);const l=s(4387);t.exports=(t,e,s)=>{if(typeof t==="function")s=t,e=null,t={};else if(Array.isArray(t))e=t,t={};if(typeof e==="function")s=e,e=null;if(!e)e=[];else e=Array.from(e);const n=i(t);if(n.sync&&typeof s==="function")throw new TypeError("callback not supported for sync tar functions");if(!n.file&&typeof s==="function")throw new TypeError("callback only supported with file option");if(e.length)filesFilter(n,e);if(!n.noResume)onentryFunction(n);return n.file&&n.sync?listFileSync(n):n.file?listFile(n,s):list(n)};const onentryFunction=t=>{const e=t.onentry;t.onentry=e?t=>{e(t);t.resume()}:t=>t.resume()};const filesFilter=(t,e)=>{const s=new Map(e.map((t=>[l(t),true])));const i=t.filter;const mapHas=(t,e)=>{const i=e||h.parse(t).root||".";const n=t===i?false:s.has(t)?s.get(t):mapHas(h.dirname(t),i);s.set(t,n);return n};t.filter=i?(t,e)=>i(t,e)&&mapHas(l(t)):t=>mapHas(l(t))};const listFileSync=t=>{const e=list(t);const s=t.file;let i=true;let n;try{const o=r.statSync(s);const h=t.maxReadSize||16*1024*1024;if(o.size{const s=new n(t);const i=t.maxReadSize||16*1024*1024;const h=t.file;const l=new Promise(((t,e)=>{s.on("error",e);s.on("end",t);r.stat(h,((t,n)=>{if(t)e(t);else{const t=new o.ReadStream(h,{readSize:i,size:n.size});t.on("error",e);t.pipe(s)}}))}));return e?l.then(e,e):l};const list=t=>new n(t)},3967:(t,e,s)=>{"use strict";const i=s(8828);const n=s(7147);const r=s(1017);const o=s(5952);const h=s(9408);class SymlinkError extends Error{constructor(t,e){super("Cannot extract through symbolic link");this.path=e;this.symlink=t}get name(){return"SylinkError"}}class CwdError extends Error{constructor(t,e){super(e+": Cannot cd into '"+t+"'");this.path=t;this.code=e}get name(){return"CwdError"}}const cGet=(t,e)=>t.get(h(e));const cSet=(t,e,s)=>t.set(h(e),s);const checkCwd=(t,e)=>{n.stat(t,((s,i)=>{if(s||!i.isDirectory())s=new CwdError(t,s&&s.code||"ENOTDIR");e(s)}))};t.exports=(t,e,s)=>{t=h(t);const l=e.umask;const a=e.mode|448;const c=(a&l)!==0;const u=e.uid;const f=e.gid;const d=typeof u==="number"&&typeof f==="number"&&(u!==e.processUid||f!==e.processGid);const p=e.preserve;const m=e.unlink;const y=e.cache;const b=h(e.cwd);const done=(e,i)=>{if(e)s(e);else{cSet(y,t,true);if(i&&d)o(i,u,f,(t=>done(t)));else if(c)n.chmod(t,a,s);else s()}};if(y&&cGet(y,t)===true)return done();if(t===b)return checkCwd(t,done);if(p)return i(t,{mode:a}).then((t=>done(null,t)),done);const w=h(r.relative(b,t));const _=w.split("/");mkdir_(b,_,a,y,m,b,null,done)};const mkdir_=(t,e,s,i,o,l,a,c)=>{if(!e.length)return c(null,a);const u=e.shift();const f=h(r.resolve(t+"/"+u));if(cGet(i,f))return mkdir_(f,e,s,i,o,l,a,c);n.mkdir(f,s,onmkdir(f,e,s,i,o,l,a,c))};const onmkdir=(t,e,s,i,r,o,l,a)=>c=>{if(c){n.lstat(t,((u,f)=>{if(u){u.path=u.path&&h(u.path);a(u)}else if(f.isDirectory())mkdir_(t,e,s,i,r,o,l,a);else if(r){n.unlink(t,(h=>{if(h)return a(h);n.mkdir(t,s,onmkdir(t,e,s,i,r,o,l,a))}))}else if(f.isSymbolicLink())return a(new SymlinkError(t,t+"/"+e.join("/")));else a(c)}))}else{l=l||t;mkdir_(t,e,s,i,r,o,l,a)}};const checkCwdSync=t=>{let e=false;let s="ENOTDIR";try{e=n.statSync(t).isDirectory()}catch(t){s=t.code}finally{if(!e)throw new CwdError(t,s)}};t.exports.sync=(t,e)=>{t=h(t);const s=e.umask;const l=e.mode|448;const a=(l&s)!==0;const c=e.uid;const u=e.gid;const f=typeof c==="number"&&typeof u==="number"&&(c!==e.processUid||u!==e.processGid);const d=e.preserve;const p=e.unlink;const m=e.cache;const y=h(e.cwd);const done=e=>{cSet(m,t,true);if(e&&f)o.sync(e,c,u);if(a)n.chmodSync(t,l)};if(m&&cGet(m,t)===true)return done();if(t===y){checkCwdSync(y);return done()}if(d)return done(i.sync(t,l));const b=h(r.relative(y,t));const w=b.split("/");let _=null;for(let t=w.shift(),e=y;t&&(e+="/"+t);t=w.shift()){e=h(r.resolve(e));if(cGet(m,e))continue;try{n.mkdirSync(e,l);_=_||e;cSet(m,e,true)}catch(t){const s=n.lstatSync(e);if(s.isDirectory()){cSet(m,e,true);continue}else if(p){n.unlinkSync(e);n.mkdirSync(e,l);_=_||e;cSet(m,e,true);continue}else if(s.isSymbolicLink())return new SymlinkError(e,e+"/"+w.join("/"))}}return done(_)}},4770:t=>{"use strict";t.exports=(t,e,s)=>{t&=4095;if(s)t=(t|384)&~18;if(e){if(t&256)t|=64;if(t&32)t|=8;if(t&4)t|=1}return t}},5042:t=>{const e=Object.create(null);const{hasOwnProperty:s}=Object.prototype;t.exports=t=>{if(!s.call(e,t))e[t]=t.normalize("NFKD");return e[t]}},9408:t=>{const e=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform;t.exports=e!=="win32"?t=>t:t=>t&&t.replace(/\\/g,"/")},9698:(t,e,s)=>{"use strict";class PackJob{constructor(t,e){this.path=t||"./";this.absolute=e;this.entry=null;this.stat=null;this.readdir=null;this.pending=false;this.ignore=false;this.piped=false}}const i=s(4591);const n=s(7547);const r=s(2946);const o=s(9144);const h=o.Sync;const l=o.Tar;const a=s(1071);const c=Buffer.alloc(1024);const u=Symbol("onStat");const f=Symbol("ended");const d=Symbol("queue");const p=Symbol("current");const m=Symbol("process");const y=Symbol("processing");const b=Symbol("processJob");const w=Symbol("jobs");const _=Symbol("jobDone");const E=Symbol("addFSEntry");const g=Symbol("addTarEntry");const S=Symbol("stat");const R=Symbol("readdir");const v=Symbol("onreaddir");const O=Symbol("pipe");const k=Symbol("entry");const T=Symbol("entryOpt");const x=Symbol("writeEntryClass");const L=Symbol("write");const A=Symbol("ondrain");const N=s(7147);const I=s(1017);const D=s(430);const B=s(9408);const C=D(class Pack extends i{constructor(t){super(t);t=t||Object.create(null);this.opt=t;this.file=t.file||"";this.cwd=t.cwd||process.cwd();this.maxReadSize=t.maxReadSize;this.preservePaths=!!t.preservePaths;this.strict=!!t.strict;this.noPax=!!t.noPax;this.prefix=B(t.prefix||"");this.linkCache=t.linkCache||new Map;this.statCache=t.statCache||new Map;this.readdirCache=t.readdirCache||new Map;this[x]=o;if(typeof t.onwarn==="function")this.on("warn",t.onwarn);this.portable=!!t.portable;this.zip=null;if(t.gzip){if(typeof t.gzip!=="object")t.gzip={};if(this.portable)t.gzip.portable=true;this.zip=new n.Gzip(t.gzip);this.zip.on("data",(t=>super.write(t)));this.zip.on("end",(t=>super.end()));this.zip.on("drain",(t=>this[A]()));this.on("resume",(t=>this.zip.resume()))}else this.on("drain",this[A]);this.noDirRecurse=!!t.noDirRecurse;this.follow=!!t.follow;this.noMtime=!!t.noMtime;this.mtime=t.mtime||null;this.filter=typeof t.filter==="function"?t.filter:t=>true;this[d]=new a;this[w]=0;this.jobs=+t.jobs||4;this[y]=false;this[f]=false}[L](t){return super.write(t)}add(t){this.write(t);return this}end(t){if(t)this.write(t);this[f]=true;this[m]();return this}write(t){if(this[f])throw new Error("write after end");if(t instanceof r)this[g](t);else this[E](t);return this.flowing}[g](t){const e=B(I.resolve(this.cwd,t.path));if(!this.filter(t.path,t))t.resume();else{const s=new PackJob(t.path,e,false);s.entry=new l(t,this[T](s));s.entry.on("end",(t=>this[_](s)));this[w]+=1;this[d].push(s)}this[m]()}[E](t){const e=B(I.resolve(this.cwd,t));this[d].push(new PackJob(t,e));this[m]()}[S](t){t.pending=true;this[w]+=1;const e=this.follow?"stat":"lstat";N[e](t.absolute,((e,s)=>{t.pending=false;this[w]-=1;if(e)this.emit("error",e);else this[u](t,s)}))}[u](t,e){this.statCache.set(t.absolute,e);t.stat=e;if(!this.filter(t.path,e))t.ignore=true;this[m]()}[R](t){t.pending=true;this[w]+=1;N.readdir(t.absolute,((e,s)=>{t.pending=false;this[w]-=1;if(e)return this.emit("error",e);this[v](t,s)}))}[v](t,e){this.readdirCache.set(t.absolute,e);t.readdir=e;this[m]()}[m](){if(this[y])return;this[y]=true;for(let t=this[d].head;t!==null&&this[w]this.warn(t,e,s),noPax:this.noPax,cwd:this.cwd,absolute:t.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix}}[k](t){this[w]+=1;try{return new this[x](t.path,this[T](t)).on("end",(()=>this[_](t))).on("error",(t=>this.emit("error",t)))}catch(t){this.emit("error",t)}}[A](){if(this[p]&&this[p].entry)this[p].entry.resume()}[O](t){t.piped=true;if(t.readdir){t.readdir.forEach((e=>{const s=t.path;const i=s==="./"?"":s.replace(/\/*$/,"/");this[E](i+e)}))}const e=t.entry;const s=this.zip;if(s){e.on("data",(t=>{if(!s.write(t))e.pause()}))}else{e.on("data",(t=>{if(!super.write(t))e.pause()}))}}pause(){if(this.zip)this.zip.pause();return super.pause()}});class PackSync extends C{constructor(t){super(t);this[x]=h}pause(){}resume(){}[S](t){const e=this.follow?"statSync":"lstatSync";this[u](t,N[e](t.absolute))}[R](t,e){this[v](t,N.readdirSync(t.absolute))}[O](t){const e=t.entry;const s=this.zip;if(t.readdir){t.readdir.forEach((e=>{const s=t.path;const i=s==="./"?"":s.replace(/\/*$/,"/");this[E](i+e)}))}if(s){e.on("data",(t=>{s.write(t)}))}else{e.on("data",(t=>{super[L](t)}))}}}C.Sync=PackSync;t.exports=C},2801:(t,e,s)=>{"use strict";const i=s(430);const n=s(3970);const r=s(2361);const o=s(1071);const h=1024*1024;const l=s(2946);const a=s(4065);const c=s(7547);const u=Buffer.from([31,139]);const f=Symbol("state");const d=Symbol("writeEntry");const p=Symbol("readEntry");const m=Symbol("nextEntry");const y=Symbol("processEntry");const b=Symbol("extendedHeader");const w=Symbol("globalExtendedHeader");const _=Symbol("meta");const E=Symbol("emitMeta");const g=Symbol("buffer");const S=Symbol("queue");const R=Symbol("ended");const v=Symbol("emittedEnd");const O=Symbol("emit");const k=Symbol("unzip");const T=Symbol("consumeChunk");const x=Symbol("consumeChunkSub");const L=Symbol("consumeBody");const A=Symbol("consumeMeta");const N=Symbol("consumeHeader");const I=Symbol("consuming");const D=Symbol("bufferConcat");const B=Symbol("maybeEnd");const C=Symbol("writing");const M=Symbol("aborted");const F=Symbol("onDone");const P=Symbol("sawValidEntry");const z=Symbol("sawNullBlock");const U=Symbol("sawEOF");const noop=t=>true;t.exports=i(class Parser extends r{constructor(t){t=t||{};super(t);this.file=t.file||"";this[P]=null;this.on(F,(t=>{if(this[f]==="begin"||this[P]===false){this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}}));if(t.ondone)this.on(F,t.ondone);else{this.on(F,(t=>{this.emit("prefinish");this.emit("finish");this.emit("end");this.emit("close")}))}this.strict=!!t.strict;this.maxMetaEntrySize=t.maxMetaEntrySize||h;this.filter=typeof t.filter==="function"?t.filter:noop;this.writable=true;this.readable=false;this[S]=new o;this[g]=null;this[p]=null;this[d]=null;this[f]="begin";this[_]="";this[b]=null;this[w]=null;this[R]=false;this[k]=null;this[M]=false;this[z]=false;this[U]=false;if(typeof t.onwarn==="function")this.on("warn",t.onwarn);if(typeof t.onentry==="function")this.on("entry",t.onentry)}[N](t,e){if(this[P]===null)this[P]=false;let s;try{s=new n(t,e,this[b],this[w])}catch(t){return this.warn("TAR_ENTRY_INVALID",t)}if(s.nullBlock){if(this[z]){this[U]=true;if(this[f]==="begin")this[f]="header";this[O]("eof")}else{this[z]=true;this[O]("nullBlock")}}else{this[z]=false;if(!s.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:s});else if(!s.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:s});else{const t=s.type;if(/^(Symbolic)?Link$/.test(t)&&!s.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:s});else if(!/^(Symbolic)?Link$/.test(t)&&s.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:s});else{const t=this[d]=new l(s,this[b],this[w]);if(!this[P]){if(t.remain){const onend=()=>{if(!t.invalid)this[P]=true};t.on("end",onend)}else this[P]=true}if(t.meta){if(t.size>this.maxMetaEntrySize){t.ignore=true;this[O]("ignoredEntry",t);this[f]="ignore";t.resume()}else if(t.size>0){this[_]="";t.on("data",(t=>this[_]+=t));this[f]="meta"}}else{this[b]=null;t.ignore=t.ignore||!this.filter(t.path,t);if(t.ignore){this[O]("ignoredEntry",t);this[f]=t.remain?"ignore":"header";t.resume()}else{if(t.remain)this[f]="body";else{this[f]="header";t.end()}if(!this[p]){this[S].push(t);this[m]()}else this[S].push(t)}}}}}}[y](t){let e=true;if(!t){this[p]=null;e=false}else if(Array.isArray(t))this.emit.apply(this,t);else{this[p]=t;this.emit("entry",t);if(!t.emittedEnd){t.on("end",(t=>this[m]()));e=false}}return e}[m](){do{}while(this[y](this[S].shift()));if(!this[S].length){const t=this[p];const e=!t||t.flowing||t.size===t.remain;if(e){if(!this[C])this.emit("drain")}else t.once("drain",(t=>this.emit("drain")))}}[L](t,e){const s=this[d];const i=s.blockRemain;const n=i>=t.length&&e===0?t:t.slice(e,e+i);s.write(n);if(!s.blockRemain){this[f]="header";this[d]=null;s.end()}return n.length}[A](t,e){const s=this[d];const i=this[L](t,e);if(!this[d])this[E](s);return i}[O](t,e,s){if(!this[S].length&&!this[p])this.emit(t,e,s);else this[S].push([t,e,s])}[E](t){this[O]("meta",this[_]);switch(t.type){case"ExtendedHeader":case"OldExtendedHeader":this[b]=a.parse(this[_],this[b],false);break;case"GlobalExtendedHeader":this[w]=a.parse(this[_],this[w],true);break;case"NextFileHasLongPath":case"OldGnuLongPath":this[b]=this[b]||Object.create(null);this[b].path=this[_].replace(/\0.*/,"");break;case"NextFileHasLongLinkpath":this[b]=this[b]||Object.create(null);this[b].linkpath=this[_].replace(/\0.*/,"");break;default:throw new Error("unknown meta: "+t.type)}}abort(t){this[M]=true;this.emit("abort",t);this.warn("TAR_ABORT",t,{recoverable:false})}write(t){if(this[M])return;if(this[k]===null&&t){if(this[g]){t=Buffer.concat([this[g],t]);this[g]=null}if(t.lengththis[T](t)));this[k].on("error",(t=>this.abort(t)));this[k].on("end",(t=>{this[R]=true;this[T]()}));this[C]=true;const s=this[k][e?"end":"write"](t);this[C]=false;return s}}this[C]=true;if(this[k])this[k].write(t);else this[T](t);this[C]=false;const e=this[S].length?false:this[p]?this[p].flowing:true;if(!e&&!this[S].length)this[p].once("drain",(t=>this.emit("drain")));return e}[D](t){if(t&&!this[M])this[g]=this[g]?Buffer.concat([this[g],t]):t}[B](){if(this[R]&&!this[v]&&!this[M]&&!this[I]){this[v]=true;const t=this[d];if(t&&t.blockRemain){const e=this[g]?this[g].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${t.blockRemain} more bytes, only ${e} available)`,{entry:t});if(this[g])t.write(this[g]);t.end()}this[O](F)}}[T](t){if(this[I])this[D](t);else if(!t&&!this[g])this[B]();else{this[I]=true;if(this[g]){this[D](t);const e=this[g];this[g]=null;this[x](e)}else this[x](t);while(this[g]&&this[g].length>=512&&!this[M]&&!this[U]){const t=this[g];this[g]=null;this[x](t)}this[I]=false}if(!this[g]||this[R])this[B]()}[x](t){let e=0;const s=t.length;while(e+512<=s&&!this[M]&&!this[U]){switch(this[f]){case"begin":case"header":this[N](t,e);e+=512;break;case"ignore":case"body":e+=this[L](t,e);break;case"meta":e+=this[A](t,e);break;default:throw new Error("invalid state: "+this[f])}}if(e{const i=s(9491);const n=s(5042);const r=s(4387);const{join:o}=s(1017);const h=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform;const l=h==="win32";t.exports=()=>{const t=new Map;const e=new Map;const getDirs=t=>{const e=t.split("/").slice(0,-1).reduce(((t,e)=>{if(t.length)e=o(t[t.length-1],e);t.push(e||"/");return t}),[]);return e};const s=new Set;const getQueues=s=>{const i=e.get(s);if(!i)throw new Error("function does not have any path reservations");return{paths:i.paths.map((e=>t.get(e))),dirs:[...i.dirs].map((e=>t.get(e)))}};const check=t=>{const{paths:e,dirs:s}=getQueues(t);return e.every((e=>e[0]===t))&&s.every((e=>e[0]instanceof Set&&e[0].has(t)))};const run=t=>{if(s.has(t)||!check(t))return false;s.add(t);t((()=>clear(t)));return true};const clear=n=>{if(!s.has(n))return false;const{paths:r,dirs:o}=e.get(n);const h=new Set;r.forEach((e=>{const s=t.get(e);i.equal(s[0],n);if(s.length===1)t.delete(e);else{s.shift();if(typeof s[0]==="function")h.add(s[0]);else s[0].forEach((t=>h.add(t)))}}));o.forEach((e=>{const s=t.get(e);i(s[0]instanceof Set);if(s[0].size===1&&s.length===1)t.delete(e);else if(s[0].size===1){s.shift();h.add(s[0])}else s[0].delete(n)}));s.delete(n);h.forEach((t=>run(t)));return true};const reserve=(s,i)=>{s=l?["win32 parallelization disabled"]:s.map((t=>n(r(o(t))).toLowerCase()));const h=new Set(s.map((t=>getDirs(t))).reduce(((t,e)=>t.concat(e))));e.set(i,{dirs:h,paths:s});s.forEach((e=>{const s=t.get(e);if(!s)t.set(e,[i]);else s.push(i)}));h.forEach((e=>{const s=t.get(e);if(!s)t.set(e,[new Set([i])]);else if(s[s.length-1]instanceof Set)s[s.length-1].add(i);else s.push(new Set([i]))}));return run(i)};return{check:check,reserve:reserve}}},4065:(t,e,s)=>{"use strict";const i=s(3970);const n=s(1017);class Pax{constructor(t,e){this.atime=t.atime||null;this.charset=t.charset||null;this.comment=t.comment||null;this.ctime=t.ctime||null;this.gid=t.gid||null;this.gname=t.gname||null;this.linkpath=t.linkpath||null;this.mtime=t.mtime||null;this.path=t.path||null;this.size=t.size||null;this.uid=t.uid||null;this.uname=t.uname||null;this.dev=t.dev||null;this.ino=t.ino||null;this.nlink=t.nlink||null;this.global=e||false}encode(){const t=this.encodeBody();if(t==="")return null;const e=Buffer.byteLength(t);const s=512*Math.ceil(1+e/512);const r=Buffer.allocUnsafe(s);for(let t=0;t<512;t++)r[t]=0;new i({path:("PaxHeader/"+n.basename(this.path)).slice(0,99),mode:this.mode||420,uid:this.uid||null,gid:this.gid||null,size:e,mtime:this.mtime||null,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime||null,ctime:this.ctime||null}).encode(r);r.write(t,512,e,"utf8");for(let t=e+512;t=Math.pow(10,n))n+=1;const r=n+i;return r+s}}Pax.parse=(t,e,s)=>new Pax(merge(parseKV(t),e),s);const merge=(t,e)=>e?Object.keys(t).reduce(((e,s)=>(e[s]=t[s],e)),e):t;const parseKV=t=>t.replace(/\n$/,"").split("\n").reduce(parseKVLine,Object.create(null));const parseKVLine=(t,e)=>{const s=parseInt(e,10);if(s!==Buffer.byteLength(e)+1)return t;e=e.substr((s+" ").length);const i=e.split("=");const n=i.shift().replace(/^SCHILY\.(dev|ino|nlink)/,"$1");if(!n)return t;const r=i.join("=");t[n]=/^([A-Z]+\.)?([mac]|birth|creation)time$/.test(n)?new Date(r*1e3):/^[0-9]+$/.test(r)?+r:r;return t};t.exports=Pax},2946:(t,e,s)=>{"use strict";const i=s(4591);const n=s(9408);const r=Symbol("slurp");t.exports=class ReadEntry extends i{constructor(t,e,s){super();this.pause();this.extended=e;this.globalExtended=s;this.header=t;this.startBlockSize=512*Math.ceil(t.size/512);this.blockRemain=this.startBlockSize;this.remain=t.size;this.type=t.type;this.meta=false;this.ignore=false;switch(this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=true;break;default:this.ignore=true}this.path=n(t.path);this.mode=t.mode;if(this.mode)this.mode=this.mode&4095;this.uid=t.uid;this.gid=t.gid;this.uname=t.uname;this.gname=t.gname;this.size=t.size;this.mtime=t.mtime;this.atime=t.atime;this.ctime=t.ctime;this.linkpath=n(t.linkpath);this.uname=t.uname;this.gname=t.gname;if(e)this[r](e);if(s)this[r](s,true)}write(t){const e=t.length;if(e>this.blockRemain)throw new Error("writing more to entry than is appropriate");const s=this.remain;const i=this.blockRemain;this.remain=Math.max(0,s-e);this.blockRemain=Math.max(0,i-e);if(this.ignore)return true;if(s>=e)return super.write(t);return super.write(t.slice(0,s))}[r](t,e){for(const s in t){if(t[s]!==null&&t[s]!==undefined&&!(e&&s==="path"))this[s]=s==="path"||s==="linkpath"?n(t[s]):t[s]}}}},8291:(t,e,s)=>{"use strict";const i=s(6924);const n=s(9698);const r=s(7147);const o=s(3597);const h=s(9650);const l=s(1017);const a=s(3970);t.exports=(t,e,s)=>{const n=i(t);if(!n.file)throw new TypeError("file is required");if(n.gzip)throw new TypeError("cannot append to compressed archives");if(!e||!Array.isArray(e)||!e.length)throw new TypeError("no files or directories specified");e=Array.from(e);return n.sync?replaceSync(n,e):replace(n,e,s)};const replaceSync=(t,e)=>{const s=new n.Sync(t);let i=true;let o;let h;try{try{o=r.openSync(t.file,"r+")}catch(e){if(e.code==="ENOENT")o=r.openSync(t.file,"w+");else throw e}const n=r.fstatSync(o);const l=Buffer.alloc(512);t:for(h=0;hn.size)break;h+=s;if(t.mtimeCache)t.mtimeCache.set(e.path,e.mtime)}i=false;streamSync(t,s,h,o,e)}finally{if(i){try{r.closeSync(o)}catch(t){}}}};const streamSync=(t,e,s,i,n)=>{const r=new o.WriteStreamSync(t.file,{fd:i,start:s});e.pipe(r);addFilesSync(e,n)};const replace=(t,e,s)=>{e=Array.from(e);const i=new n(t);const getPos=(e,s,i)=>{const cb=(t,s)=>{if(t)r.close(e,(e=>i(t)));else i(null,s)};let n=0;if(s===0)return cb(null,0);let o=0;const h=Buffer.alloc(512);const onread=(i,l)=>{if(i)return cb(i);o+=l;if(o<512&&l){return r.read(e,h,o,h.length-o,n+o,onread)}if(n===0&&h[0]===31&&h[1]===139)return cb(new Error("cannot append to compressed archives"));if(o<512)return cb(null,n);const c=new a(h);if(!c.cksumValid)return cb(null,n);const u=512*Math.ceil(c.size/512);if(n+u+512>s)return cb(null,n);n+=u+512;if(n>=s)return cb(null,n);if(t.mtimeCache)t.mtimeCache.set(c.path,c.mtime);o=0;r.read(e,h,0,512,n,onread)};r.read(e,h,0,512,n,onread)};const h=new Promise(((s,n)=>{i.on("error",n);let h="r+";const onopen=(l,a)=>{if(l&&l.code==="ENOENT"&&h==="r+"){h="w+";return r.open(t.file,h,onopen)}if(l)return n(l);r.fstat(a,((h,l)=>{if(h)return r.close(a,(()=>n(h)));getPos(a,l.size,((r,h)=>{if(r)return n(r);const l=new o.WriteStream(t.file,{fd:a,start:h});i.pipe(l);l.on("error",n);l.on("close",s);addFilesAsync(i,e)}))}))};r.open(t.file,h,onopen)}));return s?h.then(s,s):h};const addFilesSync=(t,e)=>{e.forEach((e=>{if(e.charAt(0)==="@"){h({file:l.resolve(t.cwd,e.substr(1)),sync:true,noResume:true,onentry:e=>t.add(e)})}else t.add(e)}));t.end()};const addFilesAsync=(t,e)=>{while(e.length){const s=e.shift();if(s.charAt(0)==="@"){return h({file:l.resolve(t.cwd,s.substr(1)),noResume:true,onentry:e=>t.add(e)}).then((s=>addFilesAsync(t,e)))}else t.add(s)}t.end()}},8117:(t,e,s)=>{const{isAbsolute:i,parse:n}=s(1017).win32;t.exports=t=>{let e="";let s=n(t);while(i(t)||s.root){const i=t.charAt(0)==="/"&&t.slice(0,4)!=="//?/"?"/":s.root;t=t.substr(i.length);e+=i;s=n(t)}return[e,t]}},4387:t=>{t.exports=t=>{let e=t.length-1;let s=-1;while(e>-1&&t.charAt(e)==="/"){s=e;e--}return s===-1?t:t.slice(0,s)}},8318:(t,e)=>{"use strict";e.name=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]);e.code=new Map(Array.from(e.name).map((t=>[t[1],t[0]])))},7932:(t,e,s)=>{"use strict";const i=s(9491);const n=s(2801);const r=s(7147);const o=s(3597);const h=s(1017);const l=s(3967);const a=s(1676);const c=s(2778);const u=s(8117);const f=s(9408);const d=s(4387);const p=s(5042);const m=Symbol("onEntry");const y=Symbol("checkFs");const b=Symbol("checkFs2");const w=Symbol("pruneCache");const _=Symbol("isReusable");const E=Symbol("makeFs");const g=Symbol("file");const S=Symbol("directory");const R=Symbol("link");const v=Symbol("symlink");const O=Symbol("hardlink");const k=Symbol("unsupported");const T=Symbol("checkPath");const x=Symbol("mkdir");const L=Symbol("onError");const A=Symbol("pending");const N=Symbol("pend");const I=Symbol("unpend");const D=Symbol("ended");const B=Symbol("maybeClose");const C=Symbol("skip");const M=Symbol("doChown");const F=Symbol("uid");const P=Symbol("gid");const z=Symbol("checkedCwd");const U=s(6113);const Y=s(2142);const Z=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform;const j=Z==="win32";const unlinkFile=(t,e)=>{if(!j)return r.unlink(t,e);const s=t+".DELETE."+U.randomBytes(16).toString("hex");r.rename(t,s,(t=>{if(t)return e(t);r.unlink(s,e)}))};const unlinkFileSync=t=>{if(!j)return r.unlinkSync(t);const e=t+".DELETE."+U.randomBytes(16).toString("hex");r.renameSync(t,e);r.unlinkSync(e)};const uint32=(t,e,s)=>t===t>>>0?t:e===e>>>0?e:s;const cacheKeyNormalize=t=>p(d(f(t))).toLowerCase();const pruneCache=(t,e)=>{e=cacheKeyNormalize(e);for(const s of t.keys()){const i=cacheKeyNormalize(s);if(i===e||i.indexOf(e+"/")===0)t.delete(s)}};const dropCache=t=>{for(const e of t.keys())t.delete(e)};class Unpack extends n{constructor(t){if(!t)t={};t.ondone=t=>{this[D]=true;this[B]()};super(t);this[z]=false;this.reservations=c();this.transform=typeof t.transform==="function"?t.transform:null;this.writable=true;this.readable=false;this[A]=0;this[D]=false;this.dirCache=t.dirCache||new Map;if(typeof t.uid==="number"||typeof t.gid==="number"){if(typeof t.uid!=="number"||typeof t.gid!=="number")throw new TypeError("cannot set owner without number uid and gid");if(t.preserveOwner){throw new TypeError("cannot preserve owner in archive and also set owner explicitly")}this.uid=t.uid;this.gid=t.gid;this.setOwner=true}else{this.uid=null;this.gid=null;this.setOwner=false}if(t.preserveOwner===undefined&&typeof t.uid!=="number")this.preserveOwner=process.getuid&&process.getuid()===0;else this.preserveOwner=!!t.preserveOwner;this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():null;this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():null;this.forceChown=t.forceChown===true;this.win32=!!t.win32||j;this.newer=!!t.newer;this.keep=!!t.keep;this.noMtime=!!t.noMtime;this.preservePaths=!!t.preservePaths;this.unlink=!!t.unlink;this.cwd=f(h.resolve(t.cwd||process.cwd()));this.strip=+t.strip||0;this.processUmask=t.noChmod?0:process.umask();this.umask=typeof t.umask==="number"?t.umask:this.processUmask;this.dmode=t.dmode||511&~this.umask;this.fmode=t.fmode||438&~this.umask;this.on("entry",(t=>this[m](t)))}warn(t,e,s={}){if(t==="TAR_BAD_ARCHIVE"||t==="TAR_ABORT")s.recoverable=false;return super.warn(t,e,s)}[B](){if(this[D]&&this[A]===0){this.emit("prefinish");this.emit("finish");this.emit("end");this.emit("close")}}[T](t){if(this.strip){const e=f(t.path).split("/");if(e.length=this.strip)t.linkpath=e.slice(this.strip).join("/");else return false}}if(!this.preservePaths){const e=f(t.path);const s=e.split("/");if(s.includes("..")||j&&/^[a-z]:\.\.$/i.test(s[0])){this.warn("TAR_ENTRY_ERROR",`path contains '..'`,{entry:t,path:e});return false}const[i,n]=u(e);if(i){t.path=n;this.warn("TAR_ENTRY_INFO",`stripping ${i} from absolute path`,{entry:t,path:e})}}if(h.isAbsolute(t.path))t.absolute=f(h.resolve(t.path));else t.absolute=f(h.resolve(this.cwd,t.path));if(!this.preservePaths&&t.absolute.indexOf(this.cwd+"/")!==0&&t.absolute!==this.cwd){this.warn("TAR_ENTRY_ERROR","path escaped extraction target",{entry:t,path:f(t.path),resolvedPath:t.absolute,cwd:this.cwd});return false}if(t.absolute===this.cwd&&t.type!=="Directory"&&t.type!=="GNUDumpDir")return false;if(this.win32){const{root:e}=h.win32.parse(t.absolute);t.absolute=e+a.encode(t.absolute.substr(e.length));const{root:s}=h.win32.parse(t.path);t.path=s+a.encode(t.path.substr(s.length))}return true}[m](t){if(!this[T](t))return t.resume();i.equal(typeof t.absolute,"string");switch(t.type){case"Directory":case"GNUDumpDir":if(t.mode)t.mode=t.mode|448;case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[y](t);case"CharacterDevice":case"BlockDevice":case"FIFO":default:return this[k](t)}}[L](t,e){if(t.name==="CwdError")this.emit("error",t);else{this.warn("TAR_ENTRY_ERROR",t,{entry:e});this[I]();e.resume()}}[x](t,e,s){l(f(t),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cache:this.dirCache,cwd:this.cwd,mode:e,noChmod:this.noChmod},s)}[M](t){return this.forceChown||this.preserveOwner&&(typeof t.uid==="number"&&t.uid!==this.processUid||typeof t.gid==="number"&&t.gid!==this.processGid)||(typeof this.uid==="number"&&this.uid!==this.processUid||typeof this.gid==="number"&&this.gid!==this.processGid)}[F](t){return uint32(this.uid,t.uid,this.processUid)}[P](t){return uint32(this.gid,t.gid,this.processGid)}[g](t,e){const s=t.mode&4095||this.fmode;const i=new o.WriteStream(t.absolute,{flags:Y(t.size),mode:s,autoClose:false});i.on("error",(s=>{if(i.fd)r.close(i.fd,(()=>{}));i.write=()=>true;this[L](s,t);e()}));let n=1;const done=s=>{if(s){if(i.fd)r.close(i.fd,(()=>{}));this[L](s,t);e();return}if(--n===0){r.close(i.fd,(s=>{if(s)this[L](s,t);else this[I]();e()}))}};i.on("finish",(e=>{const s=t.absolute;const o=i.fd;if(t.mtime&&!this.noMtime){n++;const e=t.atime||new Date;const i=t.mtime;r.futimes(o,e,i,(t=>t?r.utimes(s,e,i,(e=>done(e&&t))):done()))}if(this[M](t)){n++;const e=this[F](t);const i=this[P](t);r.fchown(o,e,i,(t=>t?r.chown(s,e,i,(e=>done(e&&t))):done()))}done()}));const h=this.transform?this.transform(t)||t:t;if(h!==t){h.on("error",(s=>{this[L](s,t);e()}));t.pipe(h)}h.pipe(i)}[S](t,e){const s=t.mode&4095||this.dmode;this[x](t.absolute,s,(s=>{if(s){this[L](s,t);e();return}let i=1;const done=s=>{if(--i===0){e();this[I]();t.resume()}};if(t.mtime&&!this.noMtime){i++;r.utimes(t.absolute,t.atime||new Date,t.mtime,done)}if(this[M](t)){i++;r.chown(t.absolute,this[F](t),this[P](t),done)}done()}))}[k](t){t.unsupported=true;this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${t.type}`,{entry:t});t.resume()}[v](t,e){this[R](t,t.linkpath,"symlink",e)}[O](t,e){const s=f(h.resolve(this.cwd,t.linkpath));this[R](t,s,"link",e)}[N](){this[A]++}[I](){this[A]--;this[B]()}[C](t){this[I]();t.resume()}[_](t,e){return t.type==="File"&&!this.unlink&&e.isFile()&&e.nlink<=1&&!j}[y](t){this[N]();const e=[t.path];if(t.linkpath)e.push(t.linkpath);this.reservations.reserve(e,(e=>this[b](t,e)))}[w](t){if(t.type==="SymbolicLink")dropCache(this.dirCache);else if(t.type!=="Directory")pruneCache(this.dirCache,t.absolute)}[b](t,e){this[w](t);const done=s=>{this[w](t);e(s)};const checkCwd=()=>{this[x](this.cwd,this.dmode,(e=>{if(e){this[L](e,t);done();return}this[z]=true;start()}))};const start=()=>{if(t.absolute!==this.cwd){const e=f(h.dirname(t.absolute));if(e!==this.cwd){return this[x](e,this.dmode,(e=>{if(e){this[L](e,t);done();return}afterMakeParent()}))}}afterMakeParent()};const afterMakeParent=()=>{r.lstat(t.absolute,((e,s)=>{if(s&&(this.keep||this.newer&&s.mtime>t.mtime)){this[C](t);done();return}if(e||this[_](t,s))return this[E](null,t,done);if(s.isDirectory()){if(t.type==="Directory"){const e=!this.noChmod&&t.mode&&(s.mode&4095)!==t.mode;const afterChmod=e=>this[E](e,t,done);if(!e)return afterChmod();return r.chmod(t.absolute,t.mode,afterChmod)}if(t.absolute!==this.cwd){return r.rmdir(t.absolute,(e=>this[E](e,t,done)))}}if(t.absolute===this.cwd)return this[E](null,t,done);unlinkFile(t.absolute,(e=>this[E](e,t,done)))}))};if(this[z])start();else checkCwd()}[E](t,e,s){if(t){this[L](t,e);s();return}switch(e.type){case"File":case"OldFile":case"ContiguousFile":return this[g](e,s);case"Link":return this[O](e,s);case"SymbolicLink":return this[v](e,s);case"Directory":case"GNUDumpDir":return this[S](e,s)}}[R](t,e,s,i){r[s](e,t.absolute,(e=>{if(e)this[L](e,t);else{this[I]();t.resume()}i()}))}}const callSync=t=>{try{return[null,t()]}catch(t){return[t,null]}};class UnpackSync extends Unpack{[E](t,e){return super[E](t,e,(()=>{}))}[y](t){this[w](t);if(!this[z]){const e=this[x](this.cwd,this.dmode);if(e)return this[L](e,t);this[z]=true}if(t.absolute!==this.cwd){const e=f(h.dirname(t.absolute));if(e!==this.cwd){const s=this[x](e,this.dmode);if(s)return this[L](s,t)}}const[e,s]=callSync((()=>r.lstatSync(t.absolute)));if(s&&(this.keep||this.newer&&s.mtime>t.mtime))return this[C](t);if(e||this[_](t,s))return this[E](null,t);if(s.isDirectory()){if(t.type==="Directory"){const e=!this.noChmod&&t.mode&&(s.mode&4095)!==t.mode;const[i]=e?callSync((()=>{r.chmodSync(t.absolute,t.mode)})):[];return this[E](i,t)}const[e]=callSync((()=>r.rmdirSync(t.absolute)));this[E](e,t)}const[i]=t.absolute===this.cwd?[]:callSync((()=>unlinkFileSync(t.absolute)));this[E](i,t)}[g](t,e){const s=t.mode&4095||this.fmode;const oner=s=>{let n;try{r.closeSync(i)}catch(t){n=t}if(s||n)this[L](s||n,t);e()};let i;try{i=r.openSync(t.absolute,Y(t.size),s)}catch(t){return oner(t)}const n=this.transform?this.transform(t)||t:t;if(n!==t){n.on("error",(e=>this[L](e,t)));t.pipe(n)}n.on("data",(t=>{try{r.writeSync(i,t,0,t.length)}catch(t){oner(t)}}));n.on("end",(e=>{let s=null;if(t.mtime&&!this.noMtime){const e=t.atime||new Date;const n=t.mtime;try{r.futimesSync(i,e,n)}catch(i){try{r.utimesSync(t.absolute,e,n)}catch(t){s=i}}}if(this[M](t)){const e=this[F](t);const n=this[P](t);try{r.fchownSync(i,e,n)}catch(i){try{r.chownSync(t.absolute,e,n)}catch(t){s=s||i}}}oner(s)}))}[S](t,e){const s=t.mode&4095||this.dmode;const i=this[x](t.absolute,s);if(i){this[L](i,t);e();return}if(t.mtime&&!this.noMtime){try{r.utimesSync(t.absolute,t.atime||new Date,t.mtime)}catch(i){}}if(this[M](t)){try{r.chownSync(t.absolute,this[F](t),this[P](t))}catch(i){}}e();t.resume()}[x](t,e){try{return l.sync(f(t),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cache:this.dirCache,cwd:this.cwd,mode:e})}catch(t){return t}}[R](t,e,s,i){try{r[s+"Sync"](e,t.absolute);i();t.resume()}catch(e){return this[L](e,t)}}}Unpack.Sync=UnpackSync;t.exports=Unpack},8489:(t,e,s)=>{"use strict";const i=s(6924);const n=s(8291);t.exports=(t,e,s)=>{const r=i(t);if(!r.file)throw new TypeError("file is required");if(r.gzip)throw new TypeError("cannot append to compressed archives");if(!e||!Array.isArray(e)||!e.length)throw new TypeError("no files or directories specified");e=Array.from(e);mtimeFilter(r);return n(r,e,s)};const mtimeFilter=t=>{const e=t.filter;if(!t.mtimeCache)t.mtimeCache=new Map;t.filter=e?(s,i)=>e(s,i)&&!(t.mtimeCache.get(s)>i.mtime):(e,s)=>!(t.mtimeCache.get(e)>s.mtime)}},430:t=>{"use strict";t.exports=t=>class extends t{warn(t,e,s={}){if(this.file)s.file=this.file;if(this.cwd)s.cwd=this.cwd;s.code=e instanceof Error&&e.code||t;s.tarCode=t;if(!this.strict&&s.recoverable!==false){if(e instanceof Error){s=Object.assign(e,s);e=e.message}this.emit("warn",s.tarCode,e,s)}else if(e instanceof Error)this.emit("error",Object.assign(e,s));else this.emit("error",Object.assign(new Error(`${t}: ${e}`),s))}}},1676:t=>{"use strict";const e=["|","<",">","?",":"];const s=e.map((t=>String.fromCharCode(61440+t.charCodeAt(0))));const i=new Map(e.map(((t,e)=>[t,s[e]])));const n=new Map(s.map(((t,s)=>[t,e[s]])));t.exports={encode:t=>e.reduce(((t,e)=>t.split(e).join(i.get(e))),t),decode:t=>s.reduce(((t,e)=>t.split(e).join(n.get(e))),t)}},9144:(t,e,s)=>{"use strict";const i=s(4591);const n=s(4065);const r=s(3970);const o=s(7147);const h=s(1017);const l=s(9408);const a=s(4387);const prefixPath=(t,e)=>{if(!e)return l(t);t=l(t).replace(/^\.(\/|$)/,"");return a(e)+"/"+t};const c=16*1024*1024;const u=Symbol("process");const f=Symbol("file");const d=Symbol("directory");const p=Symbol("symlink");const m=Symbol("hardlink");const y=Symbol("header");const b=Symbol("read");const w=Symbol("lstat");const _=Symbol("onlstat");const E=Symbol("onread");const g=Symbol("onreadlink");const S=Symbol("openfile");const R=Symbol("onopenfile");const v=Symbol("close");const O=Symbol("mode");const k=Symbol("awaitDrain");const T=Symbol("ondrain");const x=Symbol("prefix");const L=Symbol("hadError");const A=s(430);const N=s(1676);const I=s(8117);const D=s(4770);const B=A(class WriteEntry extends i{constructor(t,e){e=e||{};super(e);if(typeof t!=="string")throw new TypeError("path is required");this.path=l(t);this.portable=!!e.portable;this.myuid=process.getuid&&process.getuid()||0;this.myuser=process.env.USER||"";this.maxReadSize=e.maxReadSize||c;this.linkCache=e.linkCache||new Map;this.statCache=e.statCache||new Map;this.preservePaths=!!e.preservePaths;this.cwd=l(e.cwd||process.cwd());this.strict=!!e.strict;this.noPax=!!e.noPax;this.noMtime=!!e.noMtime;this.mtime=e.mtime||null;this.prefix=e.prefix?l(e.prefix):null;this.fd=null;this.blockLen=null;this.blockRemain=null;this.buf=null;this.offset=null;this.length=null;this.pos=null;this.remain=null;if(typeof e.onwarn==="function")this.on("warn",e.onwarn);let s=false;if(!this.preservePaths){const[t,e]=I(this.path);if(t){this.path=e;s=t}}this.win32=!!e.win32||process.platform==="win32";if(this.win32){this.path=N.decode(this.path.replace(/\\/g,"/"));t=t.replace(/\\/g,"/")}this.absolute=l(e.absolute||h.resolve(this.cwd,t));if(this.path==="")this.path="./";if(s){this.warn("TAR_ENTRY_INFO",`stripping ${s} from absolute path`,{entry:this,path:s+this.path})}if(this.statCache.has(this.absolute))this[_](this.statCache.get(this.absolute));else this[w]()}emit(t,...e){if(t==="error")this[L]=true;return super.emit(t,...e)}[w](){o.lstat(this.absolute,((t,e)=>{if(t)return this.emit("error",t);this[_](e)}))}[_](t){this.statCache.set(this.absolute,t);this.stat=t;if(!t.isFile())t.size=0;this.type=getType(t);this.emit("stat",t);this[u]()}[u](){switch(this.type){case"File":return this[f]();case"Directory":return this[d]();case"SymbolicLink":return this[p]();default:return this.end()}}[O](t){return D(t,this.type==="Directory",this.portable)}[x](t){return prefixPath(t,this.prefix)}[y](){if(this.type==="Directory"&&this.portable)this.noMtime=true;this.header=new r({path:this[x](this.path),linkpath:this.type==="Link"?this[x](this.linkpath):this.linkpath,mode:this[O](this.stat.mode),uid:this.portable?null:this.stat.uid,gid:this.portable?null:this.stat.gid,size:this.stat.size,mtime:this.noMtime?null:this.mtime||this.stat.mtime,type:this.type,uname:this.portable?null:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?null:this.stat.atime,ctime:this.portable?null:this.stat.ctime});if(this.header.encode()&&!this.noPax){super.write(new n({atime:this.portable?null:this.header.atime,ctime:this.portable?null:this.header.ctime,gid:this.portable?null:this.header.gid,mtime:this.noMtime?null:this.mtime||this.header.mtime,path:this[x](this.path),linkpath:this.type==="Link"?this[x](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?null:this.header.uid,uname:this.portable?null:this.header.uname,dev:this.portable?null:this.stat.dev,ino:this.portable?null:this.stat.ino,nlink:this.portable?null:this.stat.nlink}).encode())}super.write(this.header.block)}[d](){if(this.path.substr(-1)!=="/")this.path+="/";this.stat.size=0;this[y]();this.end()}[p](){o.readlink(this.absolute,((t,e)=>{if(t)return this.emit("error",t);this[g](e)}))}[g](t){this.linkpath=l(t);this[y]();this.end()}[m](t){this.type="Link";this.linkpath=l(h.relative(this.cwd,t));this.stat.size=0;this[y]();this.end()}[f](){if(this.stat.nlink>1){const t=this.stat.dev+":"+this.stat.ino;if(this.linkCache.has(t)){const e=this.linkCache.get(t);if(e.indexOf(this.cwd)===0)return this[m](e)}this.linkCache.set(t,this.absolute)}this[y]();if(this.stat.size===0)return this.end();this[S]()}[S](){o.open(this.absolute,"r",((t,e)=>{if(t)return this.emit("error",t);this[R](e)}))}[R](t){this.fd=t;if(this[L])return this[v]();this.blockLen=512*Math.ceil(this.stat.size/512);this.blockRemain=this.blockLen;const e=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(e);this.offset=0;this.pos=0;this.remain=this.stat.size;this.length=this.buf.length;this[b]()}[b](){const{fd:t,buf:e,offset:s,length:i,pos:n}=this;o.read(t,e,s,i,n,((t,e)=>{if(t){return this[v]((()=>this.emit("error",t)))}this[E](e)}))}[v](t){o.close(this.fd,t)}[E](t){if(t<=0&&this.remain>0){const t=new Error("encountered unexpected EOF");t.path=this.absolute;t.syscall="read";t.code="EOF";return this[v]((()=>this.emit("error",t)))}if(t>this.remain){const t=new Error("did not encounter expected EOF");t.path=this.absolute;t.syscall="read";t.code="EOF";return this[v]((()=>this.emit("error",t)))}if(t===this.remain){for(let e=t;ethis[T]()));else this[T]()}[k](t){this.once("drain",t)}write(t){if(this.blockRemaint?this.emit("error",t):this.end()))}if(this.offset>=this.length){this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length));this.offset=0}this.length=this.buf.length-this.offset;this[b]()}});class WriteEntrySync extends B{[w](){this[_](o.lstatSync(this.absolute))}[p](){this[g](o.readlinkSync(this.absolute))}[S](){this[R](o.openSync(this.absolute,"r"))}[b](){let t=true;try{const{fd:e,buf:s,offset:i,length:n,pos:r}=this;const h=o.readSync(e,s,i,n,r);this[E](h);t=false}finally{if(t){try{this[v]((()=>{}))}catch(t){}}}}[k](t){t()}[v](t){o.closeSync(this.fd);t()}}const C=A(class WriteEntryTar extends i{constructor(t,e){e=e||{};super(e);this.preservePaths=!!e.preservePaths;this.portable=!!e.portable;this.strict=!!e.strict;this.noPax=!!e.noPax;this.noMtime=!!e.noMtime;this.readEntry=t;this.type=t.type;if(this.type==="Directory"&&this.portable)this.noMtime=true;this.prefix=e.prefix||null;this.path=l(t.path);this.mode=this[O](t.mode);this.uid=this.portable?null:t.uid;this.gid=this.portable?null:t.gid;this.uname=this.portable?null:t.uname;this.gname=this.portable?null:t.gname;this.size=t.size;this.mtime=this.noMtime?null:e.mtime||t.mtime;this.atime=this.portable?null:t.atime;this.ctime=this.portable?null:t.ctime;this.linkpath=l(t.linkpath);if(typeof e.onwarn==="function")this.on("warn",e.onwarn);let s=false;if(!this.preservePaths){const[t,e]=I(this.path);if(t){this.path=e;s=t}}this.remain=t.size;this.blockRemain=t.startBlockSize;this.header=new r({path:this[x](this.path),linkpath:this.type==="Link"?this[x](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?null:this.uid,gid:this.portable?null:this.gid,size:this.size,mtime:this.noMtime?null:this.mtime,type:this.type,uname:this.portable?null:this.uname,atime:this.portable?null:this.atime,ctime:this.portable?null:this.ctime});if(s){this.warn("TAR_ENTRY_INFO",`stripping ${s} from absolute path`,{entry:this,path:s+this.path})}if(this.header.encode()&&!this.noPax){super.write(new n({atime:this.portable?null:this.atime,ctime:this.portable?null:this.ctime,gid:this.portable?null:this.gid,mtime:this.noMtime?null:this.mtime,path:this[x](this.path),linkpath:this.type==="Link"?this[x](this.linkpath):this.linkpath,size:this.size,uid:this.portable?null:this.uid,uname:this.portable?null:this.uname,dev:this.portable?null:this.readEntry.dev,ino:this.portable?null:this.readEntry.ino,nlink:this.portable?null:this.readEntry.nlink}).encode())}super.write(this.header.block);t.pipe(this)}[x](t){return prefixPath(t,this.prefix)}[O](t){return D(t,this.type==="Directory",this.portable)}write(t){const e=t.length;if(e>this.blockRemain)throw new Error("writing more to entry than is appropriate");this.blockRemain-=e;return super.write(t)}end(){if(this.blockRemain)super.write(Buffer.alloc(this.blockRemain));return super.end()}});B.Sync=WriteEntrySync;B.Tar=C;const getType=t=>t.isFile()?"File":t.isDirectory()?"Directory":t.isSymbolicLink()?"SymbolicLink":"Unsupported";t.exports=B},6861:t=>{"use strict";t.exports=function(t){t.prototype[Symbol.iterator]=function*(){for(let t=this.head;t;t=t.next){yield t.value}}}},1071:(t,e,s)=>{"use strict";t.exports=Yallist;Yallist.Node=Node;Yallist.create=Yallist;function Yallist(t){var e=this;if(!(e instanceof Yallist)){e=new Yallist}e.tail=null;e.head=null;e.length=0;if(t&&typeof t.forEach==="function"){t.forEach((function(t){e.push(t)}))}else if(arguments.length>0){for(var s=0,i=arguments.length;s1){s=e}else if(this.head){i=this.head.next;s=this.head.value}else{throw new TypeError("Reduce of empty list with no initial value")}for(var n=0;i!==null;n++){s=t(s,i.value,n);i=i.next}return s};Yallist.prototype.reduceReverse=function(t,e){var s;var i=this.tail;if(arguments.length>1){s=e}else if(this.tail){i=this.tail.prev;s=this.tail.value}else{throw new TypeError("Reduce of empty list with no initial value")}for(var n=this.length-1;i!==null;n--){s=t(s,i.value,n);i=i.prev}return s};Yallist.prototype.toArray=function(){var t=new Array(this.length);for(var e=0,s=this.head;s!==null;e++){t[e]=s.value;s=s.next}return t};Yallist.prototype.toArrayReverse=function(){var t=new Array(this.length);for(var e=0,s=this.tail;s!==null;e++){t[e]=s.value;s=s.prev}return t};Yallist.prototype.slice=function(t,e){e=e||this.length;if(e<0){e+=this.length}t=t||0;if(t<0){t+=this.length}var s=new Yallist;if(ethis.length){e=this.length}for(var i=0,n=this.head;n!==null&&ithis.length){e=this.length}for(var i=this.length,n=this.tail;n!==null&&i>e;i--){n=n.prev}for(;n!==null&&i>t;i--,n=n.prev){s.push(n.value)}return s};Yallist.prototype.splice=function(t,e,...s){if(t>this.length){t=this.length-1}if(t<0){t=this.length+t}for(var i=0,n=this.head;n!==null&&i{"use strict";t.exports=require("assert")},4300:t=>{"use strict";t.exports=require("buffer")},6113:t=>{"use strict";t.exports=require("crypto")},2361:t=>{"use strict";t.exports=require("events")},7147:t=>{"use strict";t.exports=require("fs")},1017:t=>{"use strict";t.exports=require("path")},2781:t=>{"use strict";t.exports=require("stream")},1576:t=>{"use strict";t.exports=require("string_decoder")},3837:t=>{"use strict";t.exports=require("util")},9796:t=>{"use strict";t.exports=require("zlib")}};var e={};function __nccwpck_require__(s){var i=e[s];if(i!==undefined){return i.exports}var n=e[s]={exports:{}};var r=true;try{t[s](n,n.exports,__nccwpck_require__);r=false}finally{if(r)delete e[s]}return n.exports}if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var s={};(()=>{"use strict";var t=s;t.c=t.create=__nccwpck_require__(6036);t.r=t.replace=__nccwpck_require__(8291);t.t=t.list=__nccwpck_require__(9650);t.u=t.update=__nccwpck_require__(8489);t.x=t.extract=__nccwpck_require__(7171);t.Pack=__nccwpck_require__(9698);t.Unpack=__nccwpck_require__(7932);t.Parse=__nccwpck_require__(2801);t.ReadEntry=__nccwpck_require__(2946);t.WriteEntry=__nccwpck_require__(9144);t.Header=__nccwpck_require__(3970);t.Pax=__nccwpck_require__(4065);t.types=__nccwpck_require__(8318)})();module.exports=s})(); \ No newline at end of file diff --git a/packages/next/compiled/tar/package.json b/packages/next/compiled/tar/package.json new file mode 100644 index 000000000000..f39487ec7481 --- /dev/null +++ b/packages/next/compiled/tar/package.json @@ -0,0 +1 @@ +{"name":"tar","main":"index.js","author":"Isaac Z. Schlueter (http://blog.izs.me/)","license":"ISC"} diff --git a/packages/next/lib/download-wasm-swc.ts b/packages/next/lib/download-wasm-swc.ts new file mode 100644 index 000000000000..926ed912ff0e --- /dev/null +++ b/packages/next/lib/download-wasm-swc.ts @@ -0,0 +1,118 @@ +import os from 'os' +import fs from 'fs' +import path from 'path' +import * as Log from '../build/output/log' +import { execSync } from 'child_process' +import tar from 'next/dist/compiled/tar' +import fetch from 'next/dist/compiled/node-fetch' +import { fileExists } from './file-exists' + +const MAX_VERSIONS_TO_CACHE = 5 + +export async function downloadWasmSwc( + version: string, + wasmDirectory: string, + variant: 'nodejs' | 'web' = 'nodejs' +) { + const pkgName = `@next/swc-wasm-${variant}` + const tarFileName = `${pkgName.substring(6)}-${version}.tgz` + const outputDirectory = path.join(wasmDirectory, pkgName) + + if (await fileExists(outputDirectory)) { + // if the package is already downloaded a different + // failure occurred than not being present + return + } + + // get platform specific cache directory adapted from playwright's handling + // https://github.com/microsoft/playwright/blob/7d924470d397975a74a19184c136b3573a974e13/packages/playwright-core/src/utils/registry.ts#L141 + const cacheDirectory = (() => { + let result + const envDefined = process.env['NEXT_SWC_PATH'] + + if (envDefined) { + result = envDefined + } else { + let systemCacheDirectory + if (process.platform === 'linux') { + systemCacheDirectory = + process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache') + } else if (process.platform === 'darwin') { + systemCacheDirectory = path.join(os.homedir(), 'Library', 'Caches') + } else if (process.platform === 'win32') { + systemCacheDirectory = + process.env.LOCALAPPDATA || + path.join(os.homedir(), 'AppData', 'Local') + } else { + console.error(new Error('Unsupported platform: ' + process.platform)) + process.exit(0) + } + result = path.join(systemCacheDirectory, 'next-swc') + } + + if (!path.isAbsolute(result)) { + // It is important to resolve to the absolute path: + // - for unzipping to work correctly; + // - so that registry directory matches between installation and execution. + // INIT_CWD points to the root of `npm/yarn install` and is probably what + // the user meant when typing the relative path. + result = path.resolve(process.env['INIT_CWD'] || process.cwd(), result) + } + return result + })() + + await fs.promises.mkdir(outputDirectory, { recursive: true }) + + const extractFromTar = async () => { + await tar.x({ + file: path.join(cacheDirectory, tarFileName), + cwd: outputDirectory, + strip: 1, + }) + } + + if (!(await fileExists(path.join(cacheDirectory, tarFileName)))) { + Log.info('Downloading WASM swc package...') + await fs.promises.mkdir(cacheDirectory, { recursive: true }) + const tempFile = path.join( + cacheDirectory, + `${tarFileName}.temp-${Date.now()}` + ) + let registry = `https://registry.npmjs.org/` + + try { + const output = execSync('npm config get registry').toString().trim() + if (output.startsWith('http')) { + registry = output + } + } catch (_) {} + + await fetch(`${registry}${pkgName}/-/${tarFileName}`).then((res) => { + if (!res.ok) { + throw new Error(`request failed with status ${res.status}`) + } + const cacheWriteStream = fs.createWriteStream(tempFile) + + return new Promise((resolve, reject) => { + res.body + .pipe(cacheWriteStream) + .on('error', (err) => reject(err)) + .on('finish', () => resolve()) + }).finally(() => cacheWriteStream.close()) + }) + await fs.promises.rename(tempFile, path.join(cacheDirectory, tarFileName)) + } + await extractFromTar() + + const cacheFiles = await fs.promises.readdir(cacheDirectory) + + if (cacheFiles.length > MAX_VERSIONS_TO_CACHE) { + cacheFiles.sort() + + for (let i = MAX_VERSIONS_TO_CACHE - 1; i++; i < cacheFiles.length) { + await fs.promises + .unlink(path.join(cacheDirectory, cacheFiles[i])) + .catch(() => {}) + } + } +} diff --git a/packages/next/package.json b/packages/next/package.json index 595f40d80107..9318e3509980 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -252,6 +252,7 @@ "string-hash": "1.1.3", "string_decoder": "1.3.0", "strip-ansi": "6.0.0", + "tar": "6.1.11", "taskr": "1.1.0", "terser": "5.10.0", "text-table": "0.2.0", diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index 1a3a7286b8ac..3edcde3e5a6a 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1410,6 +1410,16 @@ export async function ncc_nft(task, opts) { .ncc({ packageName: '@vercel/nft', externals }) .target('compiled/@vercel/nft') } + +// eslint-disable-next-line camelcase +externals['tar'] = 'next/dist/compiled/tar' +export async function ncc_tar(task, opts) { + await task + .source(opts.src || relative(__dirname, require.resolve('tar'))) + .ncc({ packageName: 'tar', externals }) + .target('compiled/tar') +} + // eslint-disable-next-line camelcase externals['terser'] = 'next/dist/compiled/terser' export async function ncc_terser(task, opts) { @@ -1729,6 +1739,7 @@ export async function ncc(task, opts) { 'ncc_string_hash', 'ncc_strip_ansi', 'ncc_nft', + 'ncc_tar', 'ncc_terser', 'ncc_text_table', 'ncc_unistore', diff --git a/packages/next/telemetry/events/swc-load-failure.ts b/packages/next/telemetry/events/swc-load-failure.ts index 26a348a28dd3..6414043b1ae4 100644 --- a/packages/next/telemetry/events/swc-load-failure.ts +++ b/packages/next/telemetry/events/swc-load-failure.ts @@ -1,5 +1,7 @@ import { traceGlobals } from '../../trace/shared' import { Telemetry } from '../storage' +// @ts-ignore JSON +import { version as nextVersion, optionalDependencies } from 'next/package.json' const EVENT_PLUGIN_PRESENT = 'NEXT_SWC_LOAD_FAILURE' export type EventSwcLoadFailure = { @@ -9,22 +11,56 @@ export type EventSwcLoadFailure = { arch: string nodeVersion: string nextVersion: string - wasm?: string + wasm?: 'enabled' | 'fallback' | 'failed' glibcVersion?: string installedSwcPackages?: string } } export async function eventSwcLoadFailure( - event: EventSwcLoadFailure['payload'] + event?: EventSwcLoadFailure['payload'] ): Promise { const telemetry: Telemetry = traceGlobals.get('telemetry') // can't continue if telemetry isn't set if (!telemetry) return + let glibcVersion + let installedSwcPackages + + try { + // @ts-ignore + glibcVersion = process.report?.getReport().header.glibcVersionRuntime + } catch (_) {} + + try { + const pkgNames = Object.keys(optionalDependencies || {}).filter((pkg) => + pkg.startsWith('@next/swc') + ) + const installedPkgs = [] + + for (const pkg of pkgNames) { + try { + const { version } = require(`${pkg}/package.json`) + installedPkgs.push(`${pkg}@${version}`) + } catch (_) {} + } + + if (installedPkgs.length > 0) { + installedSwcPackages = installedPkgs.sort().join(',') + } + } catch (_) {} + telemetry.record({ eventName: EVENT_PLUGIN_PRESENT, - payload: event, + payload: { + nextVersion, + glibcVersion, + installedSwcPackages, + arch: process.arch, + platform: process.platform, + nodeVersion: process.versions.node, + wasm: event?.wasm, + }, }) // ensure this event is flushed before process exits await telemetry.flush() diff --git a/packages/next/types/misc.d.ts b/packages/next/types/misc.d.ts index 9d68b3d69d37..8195d117d8a0 100644 --- a/packages/next/types/misc.d.ts +++ b/packages/next/types/misc.d.ts @@ -293,6 +293,12 @@ declare module 'next/dist/compiled/@vercel/nft' { import m from '@vercel/nft' export = m } + +declare module 'next/dist/compiled/tar' { + import m from 'tar' + export = m +} + declare module 'next/dist/compiled/terser' { import m from 'terser' export = m diff --git a/test/lib/next-modes/base.ts b/test/lib/next-modes/base.ts index 16338e57a2d5..ebd980507770 100644 --- a/test/lib/next-modes/base.ts +++ b/test/lib/next-modes/base.ts @@ -128,7 +128,8 @@ export class NextInstance { if ( process.env.NEXT_TEST_STARTER && !this.dependencies && - !this.installCommand + !this.installCommand && + !this.packageJson ) { await fs.copy(process.env.NEXT_TEST_STARTER, this.testDir) } else if (!skipIsolatedNext) { diff --git a/test/production/required-server-files-i18n.test.ts b/test/production/required-server-files-i18n.test.ts index b74cd9bd9f0d..a1b5d0d6f8d7 100644 --- a/test/production/required-server-files-i18n.test.ts +++ b/test/production/required-server-files-i18n.test.ts @@ -13,6 +13,7 @@ import { renderViaHTTP, waitFor, } from 'next-test-utils' +import nodeFetch from 'node-fetch' describe('should set-up next', () => { let next: NextInstance @@ -22,6 +23,22 @@ describe('should set-up next', () => { let requiredFilesManifest beforeAll(async () => { + let wasmPkgIsAvailable = false + + const res = await nodeFetch( + `https://registry.npmjs.com/@next/swc-wasm-nodejs/-/swc-wasm-nodejs-${ + require('next/package.json').version + }.tgz`, + { + method: 'HEAD', + } + ) + + if (res.status === 200) { + wasmPkgIsAvailable = true + console.warn(`Testing wasm fallback handling`) + } + next = await createNext({ files: { pages: new FileRef(join(__dirname, 'required-server-files/pages')), @@ -30,6 +47,14 @@ describe('should set-up next', () => { join(__dirname, 'required-server-files/data.txt') ), }, + packageJson: { + scripts: { + build: wasmPkgIsAvailable + ? 'rm -rfv node_modules/@next/swc && yarn next build' + : 'yarn next build', + }, + }, + buildCommand: 'yarn build', nextConfig: { i18n: { locales: ['en', 'fr'], diff --git a/yarn.lock b/yarn.lock index f09f0c2999a7..f3c7da9ae708 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20103,6 +20103,18 @@ tar@4.4.10: safe-buffer "^5.1.2" yallist "^3.0.3" +tar@6.1.11, tar@^6.1.11: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + tar@^4, tar@^4.4.12: version "4.4.13" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" @@ -20140,18 +20152,6 @@ tar@^6.1.0: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^6.1.11: - version "6.1.11" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" - integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - taskr@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/taskr/-/taskr-1.1.0.tgz#4f29d0ace26f4deae9a478eabf9aa0432e884438" From 837e0a6af8afc4b90866ec94672e4f04afaf81b4 Mon Sep 17 00:00:00 2001 From: OJ Kwon Date: Mon, 2 May 2022 15:20:59 -0700 Subject: [PATCH 02/22] feat(next-swc): introduce experimental tracing support for swc (#35803) * feat(next-swc/napi): expose initcustomtracesubscriber * feat(next/config): enable experimental swcTrace * feat(trace): add trace for emotion transform * refactor(swc): use .next for the default trace output * refactor(swc): teardown subscriber via drop * refactor(swc/trace): simplify config * refactor(swc/trace): adjust teardown --- packages/next-swc/Cargo.lock | 53 +++++++++++++++++ packages/next-swc/crates/core/Cargo.toml | 3 +- packages/next-swc/crates/emotion/Cargo.toml | 2 + packages/next-swc/crates/emotion/src/lib.rs | 2 + packages/next-swc/crates/napi/Cargo.toml | 5 ++ packages/next-swc/crates/napi/src/lib.rs | 5 ++ packages/next-swc/crates/napi/src/util.rs | 63 +++++++++++++++++++-- packages/next/build/index.ts | 3 +- packages/next/build/output/store.ts | 5 +- packages/next/build/swc/index.d.ts | 2 + packages/next/build/swc/index.js | 43 ++++++++++++++ packages/next/build/webpack-config.ts | 10 ++++ packages/next/server/config-shared.ts | 1 + 13 files changed, 189 insertions(+), 8 deletions(-) diff --git a/packages/next-swc/Cargo.lock b/packages/next-swc/Cargo.lock index 2de4a78596dc..87e15cb8c10f 100644 --- a/packages/next-swc/Cargo.lock +++ b/packages/next-swc/Cargo.lock @@ -717,6 +717,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + [[package]] name = "json_comments" version = "0.2.1" @@ -1043,6 +1049,10 @@ dependencies = [ "swc_ecma_loader", "swc_ecmascript", "swc_node_base", + "tracing", + "tracing-chrome", + "tracing-futures", + "tracing-subscriber", ] [[package]] @@ -1303,6 +1313,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.8" @@ -2555,7 +2585,9 @@ dependencies = [ "swc_common", "swc_ecma_transforms_testing", "swc_ecmascript", + "swc_trace_macro", "testing", + "tracing", ] [[package]] @@ -2851,6 +2883,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tracing-chrome" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb13184244c7cd22758b79e7c993c515ad67a8e730edcb7e05fe7bcabb283c7" +dependencies = [ + "json", + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-core" version = "0.1.26" @@ -2861,6 +2904,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + [[package]] name = "tracing-log" version = "0.1.2" diff --git a/packages/next-swc/crates/core/Cargo.toml b/packages/next-swc/crates/core/Cargo.toml index f2328655479c..b1bf2d334fcd 100644 --- a/packages/next-swc/crates/core/Cargo.toml +++ b/packages/next-swc/crates/core/Cargo.toml @@ -27,8 +27,7 @@ swc_common = { version = "0.17.25", features = ["concurrent", "sourcemap"] } swc_ecma_loader = { version = "0.29.1", features = ["node", "lru"] } swc_ecmascript = { version = "0.150.0", features = ["codegen", "minifier", "optimization", "parser", "react", "transforms", "typescript", "utils", "visit"] } swc_cached = "0.1.1" -tracing = { version = "0.1.32", features = ["release_max_level_off"] } - +tracing = { version = "0.1.32", features = ["release_max_level_info"] } [dev-dependencies] swc_ecma_transforms_testing = "0.82.0" diff --git a/packages/next-swc/crates/emotion/Cargo.toml b/packages/next-swc/crates/emotion/Cargo.toml index 545b5b0689a5..175fdcda5704 100644 --- a/packages/next-swc/crates/emotion/Cargo.toml +++ b/packages/next-swc/crates/emotion/Cargo.toml @@ -21,6 +21,8 @@ sourcemap = "6.0.1" swc_atoms = "0.2.11" swc_common = { version = "0.17.25", features = ["concurrent", "sourcemap"] } swc_ecmascript = { version = "0.150.0", features = ["codegen", "utils", "visit"] } +swc_trace_macro = "0.1.1" +tracing = { version = "0.1.32", features = ["release_max_level_info"] } [dev-dependencies] swc_ecma_transforms_testing = "0.82.0" diff --git a/packages/next-swc/crates/emotion/src/lib.rs b/packages/next-swc/crates/emotion/src/lib.rs index ff89be748a3e..795070aa3a51 100644 --- a/packages/next-swc/crates/emotion/src/lib.rs +++ b/packages/next-swc/crates/emotion/src/lib.rs @@ -25,6 +25,7 @@ use swc_ecmascript::{ codegen::util::SourceMapperExt, visit::{Fold, FoldWith}, }; +use swc_trace_macro::swc_trace; mod hash; @@ -164,6 +165,7 @@ pub struct EmotionTransformer { in_jsx_element: bool, } +#[swc_trace] impl EmotionTransformer { pub fn new(options: EmotionOptions, path: &Path, cm: Arc, comments: C) -> Self { EmotionTransformer { diff --git a/packages/next-swc/crates/napi/Cargo.toml b/packages/next-swc/crates/napi/Cargo.toml index 36113153c3dd..30a8df56de35 100644 --- a/packages/next-swc/crates/napi/Cargo.toml +++ b/packages/next-swc/crates/napi/Cargo.toml @@ -24,6 +24,11 @@ swc_common = { version = "0.17.25", features = ["concurrent", "sourcemap"] } swc_ecma_loader = { version = "0.29.1", features = ["node", "lru"] } swc_ecmascript = { version = "0.150.0", features = ["codegen", "minifier", "optimization", "parser", "react", "transforms", "typescript", "utils", "visit"] } swc_node_base = "0.5.2" +tracing = { version = "0.1.32", features = ["release_max_level_info"] } +tracing-futures = "0.2.5" +tracing-subscriber = "0.3.9" + +tracing-chrome = "0.5.0" [build-dependencies] napi-build = "1" diff --git a/packages/next-swc/crates/napi/src/lib.rs b/packages/next-swc/crates/napi/src/lib.rs index 477108aabefb..12f1c1e4c0cd 100644 --- a/packages/next-swc/crates/napi/src/lib.rs +++ b/packages/next-swc/crates/napi/src/lib.rs @@ -73,6 +73,11 @@ fn init(mut exports: JsObject) -> napi::Result<()> { exports.create_named_method("parse", parse::parse)?; exports.create_named_method("getTargetTriple", util::get_target_triple)?; + exports.create_named_method( + "initCustomTraceSubscriber", + util::init_custom_trace_subscriber, + )?; + exports.create_named_method("teardownTraceSubscriber", util::teardown_trace_subscriber)?; Ok(()) } diff --git a/packages/next-swc/crates/napi/src/util.rs b/packages/next-swc/crates/napi/src/util.rs index 868079445413..8476ff55d6e6 100644 --- a/packages/next-swc/crates/napi/src/util.rs +++ b/packages/next-swc/crates/napi/src/util.rs @@ -26,13 +26,14 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -use anyhow::{Context, Error}; -use napi::{CallContext, Env, JsBuffer, JsString, Status}; +use anyhow::{anyhow, Context, Error}; +use napi::{CallContext, Env, JsBuffer, JsExternal, JsString, JsUndefined, JsUnknown, Status}; use serde::de::DeserializeOwned; -use std::any::type_name; +use std::{any::type_name, cell::RefCell, convert::TryFrom, path::PathBuf}; +use tracing_chrome::{ChromeLayerBuilder, FlushGuard}; +use tracing_subscriber::{filter, prelude::*, util::SubscriberInitExt, Layer}; static TARGET_TRIPLE: &str = include_str!(concat!(env!("OUT_DIR"), "/triple.txt")); - #[contextless_function] pub fn get_target_triple(env: Env) -> napi::ContextlessResult { env.create_string(TARGET_TRIPLE).map(Some) @@ -89,3 +90,57 @@ where serde_json::from_str(s) .with_context(|| format!("failed to deserialize as {}\nJSON: {}", type_name::(), s)) } + +/// Initialize tracing subscriber to emit traces. This configures subscribers +/// for Trace Event Format (https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview). +#[js_function(1)] +pub fn init_custom_trace_subscriber(cx: CallContext) -> napi::Result { + let optional_trace_out_file_path = cx.get::(0)?; + let trace_out_file_path = match optional_trace_out_file_path.get_type()? { + napi::ValueType::String => Some(PathBuf::from( + JsString::try_from(optional_trace_out_file_path)? + .into_utf8()? + .as_str()? + .to_owned(), + )), + _ => None, + }; + + let mut layer = ChromeLayerBuilder::new().include_args(true); + if let Some(trace_out_file) = trace_out_file_path { + let dir = trace_out_file + .parent() + .ok_or_else(|| anyhow!("Not able to find path to the trace output")) + .convert_err()?; + std::fs::create_dir_all(dir)?; + + layer = layer.file(trace_out_file); + } + + let (chrome_layer, guard) = layer.build(); + tracing_subscriber::registry() + .with(chrome_layer.with_filter(filter::filter_fn(|metadata| { + !metadata.target().contains("cranelift") && !metadata.name().contains("log ") + }))) + .try_init() + .expect("Failed to register tracing subscriber"); + + let guard_cell = RefCell::new(Some(guard)); + cx.env.create_external(guard_cell, None) +} + +/// Teardown currently running tracing subscriber to flush out remaining traces. +/// This should be called when parent node.js process exits, otherwise generated +/// trace may drop traces in the buffer. +#[js_function(1)] +pub fn teardown_trace_subscriber(cx: CallContext) -> napi::Result { + let guard_external = cx.get::(0)?; + let guard_cell = &*cx + .env + .get_value_external::>>(&guard_external)?; + + if let Some(guard) = guard_cell.take() { + drop(guard); + } + cx.env.get_undefined() +} diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 1e2bd262d40c..4390a97824b7 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -113,7 +113,7 @@ import { TelemetryPlugin } from './webpack/plugins/telemetry-plugin' import { MiddlewareManifest } from './webpack/plugins/middleware-plugin' import { recursiveCopy } from '../lib/recursive-copy' import { recursiveReadDir } from '../lib/recursive-readdir' -import { lockfilePatchPromise } from './swc' +import { lockfilePatchPromise, teardownTraceSubscriber } from './swc' export type SsgRoute = { initialRevalidateSeconds: number | false @@ -2212,6 +2212,7 @@ export default async function build( // Ensure all traces are flushed before finishing the command await flushAllTraces() + teardownTraceSubscriber() } } diff --git a/packages/next/build/output/store.ts b/packages/next/build/output/store.ts index d2520f71f015..4397f6271de0 100644 --- a/packages/next/build/output/store.ts +++ b/packages/next/build/output/store.ts @@ -1,7 +1,7 @@ import createStore from 'next/dist/compiled/unistore' import stripAnsi from 'next/dist/compiled/strip-ansi' import { flushAllTraces } from '../../trace' - +import { teardownTraceSubscriber } from '../swc' import * as Log from './log' export type OutputState = @@ -91,6 +91,7 @@ store.subscribe((state) => { // Ensure traces are flushed after each compile in development mode flushAllTraces() + teardownTraceSubscriber() return } @@ -117,6 +118,7 @@ store.subscribe((state) => { Log.warn(state.warnings.join('\n\n')) // Ensure traces are flushed after each compile in development mode flushAllTraces() + teardownTraceSubscriber() return } @@ -132,4 +134,5 @@ store.subscribe((state) => { ) // Ensure traces are flushed after each compile in development mode flushAllTraces() + teardownTraceSubscriber() }) diff --git a/packages/next/build/swc/index.d.ts b/packages/next/build/swc/index.d.ts index 162a1b55e847..564090d59b22 100644 --- a/packages/next/build/swc/index.d.ts +++ b/packages/next/build/swc/index.d.ts @@ -6,3 +6,5 @@ export function minifySync(src: string, options: any): string export function bundle(options: any): Promise export function parse(src: string, options: any): any export const lockfilePatchPromise: { cur?: Promise } +export function initCustomTraceSubscriber(traceFileName?: string): void +export function teardownTraceSubscriber(): void diff --git a/packages/next/build/swc/index.js b/packages/next/build/swc/index.js index 59bf712787db..b3488bf9495b 100644 --- a/packages/next/build/swc/index.js +++ b/packages/next/build/swc/index.js @@ -17,6 +17,7 @@ let nativeBindings let wasmBindings let downloadWasmPromise let pendingBindings +let swcTraceFlushGuard export const lockfilePatchPromise = {} async function loadBindings() { @@ -261,6 +262,8 @@ function loadNative() { }, getTargetTriple: bindings.getTargetTriple, + initCustomTraceSubscriber: bindings.initCustomTraceSubscriber, + teardownTraceSubscriber: bindings.teardownTraceSubscriber, } return nativeBindings } @@ -320,3 +323,43 @@ export function getBinaryMetadata() { target: bindings?.getTargetTriple?.(), } } + +/** + * Initialize trace subscriber to emit traces. + * + */ +export const initCustomTraceSubscriber = (() => { + return (filename) => { + if (!swcTraceFlushGuard) { + // Wasm binary doesn't support trace emission + let bindings = loadNative() + swcTraceFlushGuard = bindings.initCustomTraceSubscriber(filename) + } + } +})() + +/** + * Teardown swc's trace subscriber if there's an initialized flush guard exists. + * + * This is workaround to amend behavior with process.exit + * (https://github.com/vercel/next.js/blob/4db8c49cc31e4fc182391fae6903fb5ef4e8c66e/packages/next/bin/next.ts#L134=) + * seems preventing napi's cleanup hook execution (https://github.com/swc-project/swc/blob/main/crates/node/src/util.rs#L48-L51=), + * + * instead parent process manually drops guard when process gets signal to exit. + */ +export const teardownTraceSubscriber = (() => { + let flushed = false + return () => { + if (!flushed) { + flushed = true + try { + let bindings = loadNative() + if (swcTraceFlushGuard) { + bindings.teardownTraceSubscriber(swcTraceFlushGuard) + } + } catch (e) { + // Suppress exceptions, this fn allows to fail to load native bindings + } + } + } +})() diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index fab5cbf8f8a9..9a96c22689b5 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -436,6 +436,16 @@ export default async function getBaseWebpackConfig( } const getBabelOrSwcLoader = () => { + if (useSWCLoader && config?.experimental?.swcTraceProfiling) { + // This will init subscribers once only in a single process lifecycle, + // even though it can be called multiple times. + // Subscriber need to be initialized _before_ any actual swc's call (transform, etcs) + // to collect correct trace spans when they are called. + require('./swc')?.initCustomTraceSubscriber?.( + path.join(distDir, `swc-trace-profile-${Date.now()}.json`) + ) + } + return useSWCLoader ? { loader: 'next-swc-loader', diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index c90d19cebba8..ebde47189cc0 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -132,6 +132,7 @@ export interface ExperimentalConfig { skipDefaultConversion?: boolean } > + swcTraceProfiling?: boolean } /** From 44f436b91b9201696c59754d4394f91dcdf77999 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 3 May 2022 05:37:23 -0500 Subject: [PATCH 03/22] Add initial handling for routing tests (#36635) x-ref: https://github.com/vercel/next.js/pull/36618 --- packages/next/build/entries.ts | 115 +++- packages/next/build/index.ts | 40 ++ packages/next/build/webpack-config.ts | 37 ++ .../webpack/plugins/build-manifest-plugin.ts | 16 + .../webpack/plugins/pages-manifest-plugin.ts | 44 +- packages/next/export/index.ts | 1 + packages/next/export/worker.ts | 11 +- packages/next/lib/constants.ts | 2 + packages/next/pages/root.tsx | 18 + packages/next/server/base-server.ts | 127 ++++- packages/next/server/dev/hot-reloader.ts | 6 + packages/next/server/dev/next-dev-server.ts | 71 ++- .../server/dev/on-demand-entry-handler.ts | 64 ++- packages/next/server/get-page-files.ts | 1 + .../next/server/get-route-from-entrypoint.ts | 10 +- packages/next/server/lib/find-page-file.ts | 7 +- packages/next/server/load-components.ts | 37 +- packages/next/server/next-server.ts | 28 +- packages/next/server/require.ts | 52 +- packages/next/server/root-render.tsx | 496 ++++++++++++++++++ packages/next/server/web-server.ts | 5 + packages/next/shared/lib/constants.ts | 2 + .../shared/lib/i18n/get-locale-metadata.ts | 4 +- .../lib/page-path/absolute-path-to-page.ts | 6 +- .../shared/lib/page-path/get-page-paths.ts | 17 +- .../lib/page-path/remove-page-path-tail.ts | 19 +- .../shared/lib/router/utils/root-paths.ts | 14 + packages/next/taskfile.js | 14 +- 28 files changed, 1175 insertions(+), 89 deletions(-) create mode 100644 packages/next/pages/root.tsx create mode 100644 packages/next/server/root-render.tsx create mode 100644 packages/next/shared/lib/router/utils/root-paths.ts diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 7aa7b83f6230..440cd60c3dfd 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -10,8 +10,20 @@ import fs from 'fs' import chalk from 'next/dist/compiled/chalk' import { posix, join } from 'path' import { stringify } from 'querystring' -import { API_ROUTE, DOT_NEXT_ALIAS, PAGES_DIR_ALIAS } from '../lib/constants' -import { EDGE_RUNTIME_WEBPACK } from '../shared/lib/constants' +import { + API_ROUTE, + DOT_NEXT_ALIAS, + PAGES_DIR_ALIAS, + ROOT_ALIAS, + ROOT_DIR_ALIAS, +} from '../lib/constants' +import { + CLIENT_STATIC_FILES_RUNTIME_AMP, + CLIENT_STATIC_FILES_RUNTIME_MAIN, + CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT, + CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH, + EDGE_RUNTIME_WEBPACK, +} from '../shared/lib/constants' import { MIDDLEWARE_ROUTE } from '../lib/constants' import { __ApiPreviewProps } from '../server/api-utils' import { isTargetLikeServerless } from '../server/utils' @@ -28,14 +40,22 @@ type ObjectValue = T extends { [key: string]: infer V } ? V : never * special case because it is the only page where we want to preserve the RSC * server extension. */ -export function getPageFromPath(pagePath: string, pageExtensions: string[]) { +export function getPageFromPath( + pagePath: string, + pageExtensions: string[], + isRoot?: boolean +) { const extensions = pagePath.includes('/_app.server.') ? withoutRSCExtensions(pageExtensions) : pageExtensions - const page = normalizePathSep( + let page = normalizePathSep( pagePath.replace(new RegExp(`\\.+(${extensions.join('|')})$`), '') - ).replace(/\/index$/, '') + ) + + if (!isRoot) { + page = page.replace(/\/index$/, '') + } return page === '' ? '/' : page } @@ -43,15 +63,18 @@ export function getPageFromPath(pagePath: string, pageExtensions: string[]) { export function createPagesMapping({ hasServerComponents, isDev, + isRoot, pageExtensions, pagePaths, }: { hasServerComponents: boolean isDev: boolean + isRoot?: boolean pageExtensions: string[] pagePaths: string[] }): { [page: string]: string } { const previousPages: { [key: string]: string } = {} + const pathAlias = isRoot ? ROOT_DIR_ALIAS : PAGES_DIR_ALIAS const pages = pagePaths.reduce<{ [key: string]: string }>( (result, pagePath) => { // Do not process .d.ts files inside the `pages` folder @@ -59,7 +82,7 @@ export function createPagesMapping({ return result } - const pageKey = getPageFromPath(pagePath, pageExtensions) + const pageKey = getPageFromPath(pagePath, pageExtensions, isRoot) // Assume that if there's a Client Component, that there is // a matching Server Component that will map to the page. @@ -80,7 +103,11 @@ export function createPagesMapping({ previousPages[pageKey] = pagePath } - result[pageKey] = normalizePathSep(join(PAGES_DIR_ALIAS, pagePath)) + if (pageKey === 'root') { + result['root'] = normalizePathSep(join(ROOT_ALIAS, pagePath)) + } else { + result[pageKey] = normalizePathSep(join(pathAlias, pagePath)) + } return result }, {} @@ -89,6 +116,16 @@ export function createPagesMapping({ // In development we always alias these to allow Webpack to fallback to // the correct source file so that HMR can work properly when a file is // added or removed. + + if (isRoot) { + if (isDev) { + pages['root'] = `${ROOT_ALIAS}/root` + } else { + pages['root'] = pages['root'] || 'next/dist/pages/root' + } + return pages + } + if (isDev) { delete pages['/_app'] delete pages['/_app.server'] @@ -222,6 +259,8 @@ interface CreateEntrypointsParams { pagesDir: string previewMode: __ApiPreviewProps target: 'server' | 'serverless' | 'experimental-serverless-trace' + rootDir?: string + rootPaths?: Record } export function getEdgeServerEntry(opts: { @@ -326,29 +365,45 @@ export function getClientEntry(opts: { } export async function createEntrypoints(params: CreateEntrypointsParams) { - const { config, pages, pagesDir, isDev, target } = params + const { config, pages, pagesDir, isDev, target, rootDir, rootPaths } = params const edgeServer: webpack5.EntryObject = {} const server: webpack5.EntryObject = {} const client: webpack5.EntryObject = {} - await Promise.all( - Object.keys(pages).map(async (page) => { + const getEntryHandler = + (mappings: Record, isRoot: boolean) => + async (page: string) => { const bundleFile = normalizePagePath(page) const clientBundlePath = posix.join('pages', bundleFile) - const serverBundlePath = posix.join('pages', bundleFile) + const serverBundlePath = posix.join( + isRoot ? (bundleFile === '/root' ? './' : 'root') : 'pages', + bundleFile + ) + + // Handle paths that have aliases + const pageFilePath = (() => { + const absolutePagePath = mappings[page] + if (absolutePagePath.startsWith(PAGES_DIR_ALIAS)) { + return absolutePagePath.replace(PAGES_DIR_ALIAS, pagesDir) + } + + if (absolutePagePath.startsWith(ROOT_DIR_ALIAS) && rootDir) { + return absolutePagePath.replace(ROOT_DIR_ALIAS, rootDir) + } + + if (absolutePagePath.startsWith(ROOT_ALIAS) && rootDir) { + return absolutePagePath.replace(ROOT_ALIAS, join(rootDir, '..')) + } + + return require.resolve(absolutePagePath) + })() runDependingOnPageType({ page, - pageRuntime: await getPageRuntime( - !pages[page].startsWith(PAGES_DIR_ALIAS) - ? require.resolve(pages[page]) - : join(pagesDir, pages[page].replace(PAGES_DIR_ALIAS, '')), - config, - isDev - ), + pageRuntime: await getPageRuntime(pageFilePath, config, isDev), onClient: () => { client[clientBundlePath] = getClientEntry({ - absolutePagePath: pages[page], + absolutePagePath: mappings[page], page, }) }, @@ -357,26 +412,31 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { if (page !== '/_app' && page !== '/_document') { server[serverBundlePath] = getServerlessEntry({ ...params, - absolutePagePath: pages[page], + absolutePagePath: mappings[page], page, }) } } else { - server[serverBundlePath] = [pages[page]] + server[serverBundlePath] = [mappings[page]] } }, onEdgeServer: () => { edgeServer[serverBundlePath] = getEdgeServerEntry({ ...params, - absolutePagePath: pages[page], + absolutePagePath: mappings[page], bundlePath: clientBundlePath, isDev: false, page, }) }, }) - }) - ) + } + + if (rootDir && rootPaths) { + const entryHandler = getEntryHandler(rootPaths, true) + await Promise.all(Object.keys(rootPaths).map(entryHandler)) + } + await Promise.all(Object.keys(pages).map(getEntryHandler(pages, false))) return { client, @@ -450,9 +510,10 @@ export function finalizeEntrypoint({ if ( // Client special cases name !== 'polyfills' && - name !== 'main' && - name !== 'amp' && - name !== 'react-refresh' + name !== CLIENT_STATIC_FILES_RUNTIME_MAIN && + name !== CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT && + name !== CLIENT_STATIC_FILES_RUNTIME_AMP && + name !== CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH ) { return { dependOn: diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 4390a97824b7..fa4aaad02ecc 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -114,6 +114,7 @@ import { MiddlewareManifest } from './webpack/plugins/middleware-plugin' import { recursiveCopy } from '../lib/recursive-copy' import { recursiveReadDir } from '../lib/recursive-readdir' import { lockfilePatchPromise, teardownTraceSubscriber } from './swc' +import { findPageFile } from '../server/lib/find-page-file' export type SsgRoute = { initialRevalidateSeconds: number | false @@ -309,6 +310,26 @@ export default async function build( new RegExp(`\\.(?:${config.pageExtensions.join('|')})$`) ) ) + + let rootPaths: string[] | undefined + + if (rootDir) { + rootPaths = await nextBuildSpan + .traceChild('collect-root-paths') + .traceAsyncFn(() => + recursiveReadDir( + rootDir, + new RegExp(`\\.(?:${config.pageExtensions.join('|')})$`) + ) + ) + + const rootFile = await findPageFile( + path.join(rootDir, '..'), + 'root', + config.pageExtensions + ) + if (rootFile) rootPaths.push(rootFile) + } // needed for static exporting since we want to replace with HTML // files @@ -332,6 +353,22 @@ export default async function build( }) ) + let mappedRootPaths: ReturnType | undefined + + if (rootPaths && rootDir) { + mappedRootPaths = nextBuildSpan + .traceChild('create-root-mapping') + .traceFn(() => + createPagesMapping({ + pagePaths: rootPaths!, + hasServerComponents, + isDev: false, + isRoot: true, + pageExtensions: config.pageExtensions, + }) + ) + } + const entrypoints = await nextBuildSpan .traceChild('create-entrypoints') .traceAsyncFn(() => @@ -344,6 +381,8 @@ export default async function build( pagesDir, previewMode: previewProps, target, + rootDir, + rootPaths: mappedRootPaths, }) ) @@ -649,6 +688,7 @@ export default async function build( rewrites, runWebpackSpan, target, + rootDir, } const configs = await runWebpackSpan diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 9a96c22689b5..b7afc07101fe 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -10,12 +10,15 @@ import { NEXT_PROJECT_ROOT, NEXT_PROJECT_ROOT_DIST_CLIENT, PAGES_DIR_ALIAS, + ROOT_ALIAS, + ROOT_DIR_ALIAS, } from '../lib/constants' import { fileExists } from '../lib/file-exists' import { CustomRoutes } from '../lib/load-custom-routes.js' import { CLIENT_STATIC_FILES_RUNTIME_AMP, CLIENT_STATIC_FILES_RUNTIME_MAIN, + CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT, CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL, CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH, CLIENT_STATIC_FILES_RUNTIME_WEBPACK, @@ -312,6 +315,7 @@ export default async function getBaseWebpackConfig( rewrites, runWebpackSpan, target = 'server', + rootDir, }: { buildId: string config: NextConfigComplete @@ -325,6 +329,7 @@ export default async function getBaseWebpackConfig( rewrites: CustomRoutes['rewrites'] runWebpackSpan: Span target?: string + rootDir?: string } ): Promise { const isClient = compilerType === 'client' @@ -537,6 +542,18 @@ export default async function getBaseWebpackConfig( ) ) .replace(/\\/g, '/'), + ...(config.experimental.rootDir + ? { + [CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT]: + `./` + + path + .relative( + dir, + path.join(NEXT_PROJECT_ROOT_DIST_CLIENT, 'root-next.js') + ) + .replace(/\\/g, '/'), + } + : {}), } as ClientEntries) : undefined @@ -559,6 +576,7 @@ export default async function getBaseWebpackConfig( const customAppAliases: { [key: string]: string[] } = {} const customErrorAlias: { [key: string]: string[] } = {} const customDocumentAliases: { [key: string]: string[] } = {} + const customRootAliases: { [key: string]: string[] } = {} if (dev) { customAppAliases[`${PAGES_DIR_ALIAS}/_app`] = [ @@ -589,6 +607,16 @@ export default async function getBaseWebpackConfig( }, [] as string[]), `next/dist/pages/_document.js`, ] + + if (config.experimental.rootDir && rootDir) { + customRootAliases[`${ROOT_ALIAS}/root`] = [ + ...config.pageExtensions.reduce((prev, ext) => { + prev.push(path.join(rootDir, `root.${ext}`)) + return prev + }, [] as string[]), + 'next/dist/pages/root.js', + ] + } } const resolveConfig = { @@ -620,8 +648,15 @@ export default async function getBaseWebpackConfig( ...customAppAliases, ...customErrorAlias, ...customDocumentAliases, + ...customRootAliases, [PAGES_DIR_ALIAS]: pagesDir, + ...(rootDir + ? { + [ROOT_DIR_ALIAS]: rootDir, + [ROOT_ALIAS]: path.join(rootDir, '..'), + } + : {}), [DOT_NEXT_ALIAS]: distDir, ...(isClient || isEdgeServer ? getOptimizedAliases() : {}), ...getReactProfilingInProduction(), @@ -1523,6 +1558,7 @@ export default async function getBaseWebpackConfig( serverless: isLikeServerless, dev, isEdgeRuntime: isEdgeServer, + rootEnabled: !!config.experimental.rootDir, }), // MiddlewarePlugin should be after DefinePlugin so NEXT_PUBLIC_* // replacement is done before its process.env.* handling @@ -1533,6 +1569,7 @@ export default async function getBaseWebpackConfig( rewrites, isDevFallback, exportRuntime: hasConcurrentFeatures, + rootEnabled: !!config.experimental.rootDir, }), new ProfilingPlugin({ runWebpackSpan }), config.optimizeFonts && diff --git a/packages/next/build/webpack/plugins/build-manifest-plugin.ts b/packages/next/build/webpack/plugins/build-manifest-plugin.ts index d69cbde5d13c..7d02aeb08d56 100644 --- a/packages/next/build/webpack/plugins/build-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/build-manifest-plugin.ts @@ -5,6 +5,7 @@ import { MIDDLEWARE_BUILD_MANIFEST, CLIENT_STATIC_FILES_PATH, CLIENT_STATIC_FILES_RUNTIME_MAIN, + CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT, CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL, CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH, CLIENT_STATIC_FILES_RUNTIME_AMP, @@ -95,12 +96,14 @@ export default class BuildManifestPlugin { private rewrites: CustomRoutes['rewrites'] private isDevFallback: boolean private exportRuntime: boolean + private rootEnabled: boolean constructor(options: { buildId: string rewrites: CustomRoutes['rewrites'] isDevFallback?: boolean exportRuntime?: boolean + rootEnabled: boolean }) { this.buildId = options.buildId this.isDevFallback = !!options.isDevFallback @@ -109,6 +112,7 @@ export default class BuildManifestPlugin { afterFiles: [], fallback: [], } + this.rootEnabled = options.rootEnabled this.rewrites.beforeFiles = options.rewrites.beforeFiles.map(processRoute) this.rewrites.afterFiles = options.rewrites.afterFiles.map(processRoute) this.rewrites.fallback = options.rewrites.fallback.map(processRoute) @@ -127,6 +131,7 @@ export default class BuildManifestPlugin { devFiles: [], ampDevFiles: [], lowPriorityFiles: [], + rootMainFiles: [], pages: { '/_app': [] }, ampFirstPages: [], } @@ -147,6 +152,16 @@ export default class BuildManifestPlugin { getEntrypointFiles(entrypoints.get(CLIENT_STATIC_FILES_RUNTIME_MAIN)) ) + if (this.rootEnabled) { + assetMap.rootMainFiles = [ + ...new Set( + getEntrypointFiles( + entrypoints.get(CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT) + ) + ), + ] + } + const compilationAssets: { name: string source: typeof sources.RawSource @@ -178,6 +193,7 @@ export default class BuildManifestPlugin { CLIENT_STATIC_FILES_RUNTIME_MAIN, CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH, CLIENT_STATIC_FILES_RUNTIME_AMP, + ...(this.rootEnabled ? [CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT] : []), ]) for (const entrypoint of compilation.entrypoints.values()) { diff --git a/packages/next/build/webpack/plugins/pages-manifest-plugin.ts b/packages/next/build/webpack/plugins/pages-manifest-plugin.ts index 723e841c8145..c68f7e944ad4 100644 --- a/packages/next/build/webpack/plugins/pages-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/pages-manifest-plugin.ts @@ -1,11 +1,17 @@ import { webpack, sources } from 'next/dist/compiled/webpack/webpack' -import { PAGES_MANIFEST } from '../../../shared/lib/constants' +import { + PAGES_MANIFEST, + ROOT_PATHS_MANIFEST, +} from '../../../shared/lib/constants' import getRouteFromEntrypoint from '../../../server/get-route-from-entrypoint' +import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep' export type PagesManifest = { [page: string]: string } let edgeServerPages = {} let nodeServerPages = {} +let edgeServerRootPaths = {} +let nodeServerRootPaths = {} // This plugin creates a pages-manifest.json from page entrypoints. // This is used for mapping paths like `/` to `.next/server/static//pages/index.js` when doing SSR @@ -14,27 +20,32 @@ export default class PagesManifestPlugin implements webpack.Plugin { serverless: boolean dev: boolean isEdgeRuntime: boolean + rootEnabled: boolean constructor({ serverless, dev, isEdgeRuntime, + rootEnabled, }: { serverless: boolean dev: boolean isEdgeRuntime: boolean + rootEnabled: boolean }) { this.serverless = serverless this.dev = dev this.isEdgeRuntime = isEdgeRuntime + this.rootEnabled = rootEnabled } createAssets(compilation: any, assets: any) { const entrypoints = compilation.entrypoints const pages: PagesManifest = {} + const rootPaths: PagesManifest = {} for (const entrypoint of entrypoints.values()) { - const pagePath = getRouteFromEntrypoint(entrypoint.name) + const pagePath = getRouteFromEntrypoint(entrypoint.name, this.rootEnabled) if (!pagePath) { continue @@ -54,22 +65,30 @@ export default class PagesManifestPlugin implements webpack.Plugin { continue } // Write filename, replace any backslashes in path (on windows) with forwardslashes for cross-platform consistency. - pages[pagePath] = files[files.length - 1] + let file = files[files.length - 1] if (!this.dev) { if (!this.isEdgeRuntime) { - pages[pagePath] = pages[pagePath].slice(3) + file = file.slice(3) } } - pages[pagePath] = pages[pagePath].replace(/\\/g, '/') + file = normalizePathSep(file) + + if (entrypoint.name.startsWith('root/')) { + rootPaths[pagePath] = file + } else { + pages[pagePath] = file + } } // This plugin is used by both the Node server and Edge server compilers, // we need to merge both pages to generate the full manifest. if (this.isEdgeRuntime) { edgeServerPages = pages + edgeServerRootPaths = rootPaths } else { nodeServerPages = pages + nodeServerRootPaths = rootPaths } assets[ @@ -84,6 +103,21 @@ export default class PagesManifestPlugin implements webpack.Plugin { 2 ) ) + + if (this.rootEnabled) { + assets[ + `${!this.dev && !this.isEdgeRuntime ? '../' : ''}` + ROOT_PATHS_MANIFEST + ] = new sources.RawSource( + JSON.stringify( + { + ...edgeServerRootPaths, + ...nodeServerRootPaths, + }, + null, + 2 + ) + ) + } } apply(compiler: webpack.Compiler): void { diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 8191dd75e7f3..c6c8e2fef7b3 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -581,6 +581,7 @@ export default async function exportApp( outDir, pagesDataDir, renderOpts, + rootDir: nextConfig.experimental.rootDir, serverRuntimeConfig, subFolders, buildExport: options.buildExport, diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index e68d20456d08..8934c0d441b4 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -60,6 +60,7 @@ interface ExportPageInput { parentSpanId: any httpAgentOptions: NextConfigComplete['httpAgentOptions'] serverComponents?: boolean + rootDir?: boolean } interface ExportPageResults { @@ -84,6 +85,7 @@ interface RenderOpts { locale?: string defaultLocale?: string trailingSlash?: boolean + rootDir?: boolean } type ComponentModule = ComponentType<{}> & { @@ -97,6 +99,7 @@ export default async function exportPage({ pathMap, distDir, outDir, + rootDir, pagesDataDir, renderOpts, buildExport, @@ -262,7 +265,13 @@ export default async function exportPage({ getServerSideProps, getStaticProps, pageConfig, - } = await loadComponents(distDir, page, serverless, serverComponents) + } = await loadComponents( + distDir, + page, + serverless, + serverComponents, + rootDir + ) const ampState = { ampFirst: pageConfig?.amp === true, hasQuery: Boolean(query.amp), diff --git a/packages/next/lib/constants.ts b/packages/next/lib/constants.ts index 92746a3d3ae7..ea8e0690e117 100644 --- a/packages/next/lib/constants.ts +++ b/packages/next/lib/constants.ts @@ -25,6 +25,8 @@ export const MIDDLEWARE_ROUTE = /_middleware$/ // we have to use a private alias export const PAGES_DIR_ALIAS = 'private-next-pages' export const DOT_NEXT_ALIAS = 'private-dot-next' +export const ROOT_DIR_ALIAS = 'private-next-root-dir' +export const ROOT_ALIAS = 'private-next-root' export const PUBLIC_DIR_MIDDLEWARE_CONFLICT = `You can not have a '_next' folder inside of your public folder. This conflicts with the internal '/_next' route. https://nextjs.org/docs/messages/public-next-folder-conflict` diff --git a/packages/next/pages/root.tsx b/packages/next/pages/root.tsx new file mode 100644 index 000000000000..5f972e688ce6 --- /dev/null +++ b/packages/next/pages/root.tsx @@ -0,0 +1,18 @@ +import React from 'react' + +export type RootProps = { + headChildren: any + bodyChildren: any +} + +export default function Root({ headChildren, bodyChildren }: RootProps) { + return ( + + + {headChildren} + Test + + {bodyChildren} + + ) +} diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index f84d608e4c57..51baa1aedb35 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -70,6 +70,7 @@ import { createHeaderRoute, createRedirectRoute } from './server-route-utils' import { PrerenderManifest } from '../build' import { ImageConfigComplete } from '../shared/lib/image-config' import { replaceBasePath } from './router-utils' +import { normalizeRootPath } from '../shared/lib/router/utils/root-paths' export type FindComponentsResult = { components: LoadComponentsReturnType @@ -141,6 +142,7 @@ export default abstract class Server { protected publicDir: string protected hasStaticDir: boolean protected pagesManifest?: PagesManifest + protected rootPathsManifest?: PagesManifest protected buildId: string protected minimalMode: boolean protected renderOpts: { @@ -180,6 +182,7 @@ export default abstract class Server { private responseCache: ResponseCache protected router: Router protected dynamicRoutes?: DynamicRoutes + protected rootPathRoutes?: Record protected customRoutes: CustomRoutes protected middlewareManifest?: MiddlewareManifest protected middleware?: RoutingItem[] @@ -190,6 +193,7 @@ export default abstract class Server { protected abstract getPublicDir(): string protected abstract getHasStaticDir(): boolean protected abstract getPagesManifest(): PagesManifest | undefined + protected abstract getRootPathsManifest(): PagesManifest | undefined protected abstract getBuildId(): string protected abstract generatePublicRoutes(): Route[] protected abstract generateImageRoutes(): Route[] @@ -352,6 +356,7 @@ export default abstract class Server { }) this.pagesManifest = this.getPagesManifest() + this.rootPathsManifest = this.getRootPathsManifest() this.middlewareManifest = this.getMiddlewareManifest() this.customRoutes = this.getCustomRoutes() @@ -869,6 +874,7 @@ export default abstract class Server { const { useFileSystemPublicRoutes } = this.nextConfig if (useFileSystemPublicRoutes) { + this.rootPathRoutes = this.getRootPathRoutes() this.dynamicRoutes = this.getDynamicRoutes() if (!this.minimalMode) { this.middleware = this.getMiddleware() @@ -890,6 +896,30 @@ export default abstract class Server { } } + protected isRoutableRootPath(pathname: string): boolean { + if (this.rootPathRoutes) { + const paths = Object.keys(this.rootPathRoutes) + + /** + * a root path is only routable if + * 1. has root/hello.js and no root/hello/ folder + * 2. has root/hello.js and a root/hello/index.js + */ + const hasFolderIndex = this.rootPathRoutes[`${pathname}/index`] + const hasFolder = paths.some((path) => { + return path.startsWith(`${pathname}/`) + }) + + if (hasFolder && hasFolderIndex) { + return true + } + if (!hasFolder && this.rootPathRoutes[pathname]) { + return true + } + } + return false + } + protected async hasPage(pathname: string): Promise { let found = false try { @@ -962,7 +992,10 @@ export default abstract class Server { const addedPages = new Set() return getSortedRoutes( - Object.keys(this.pagesManifest!).map( + [ + ...Object.keys(this.rootPathRoutes || {}), + ...Object.keys(this.pagesManifest!), + ].map( (page) => normalizeLocalePath(page, this.nextConfig.i18n?.locales).pathname ) @@ -978,6 +1011,35 @@ export default abstract class Server { .filter((item): item is RoutingItem => Boolean(item)) } + protected getRootPathRoutes(): Record { + const rootPathRoutes: Record = {} + + Object.keys(this.rootPathsManifest || {}).forEach((entry) => { + rootPathRoutes[normalizeRootPath(entry)] = entry + }) + return rootPathRoutes + } + + protected getRootPathLayouts(pathname: string): string[] { + const layoutPaths: string[] = [] + + if (this.rootPathRoutes) { + const paths = Object.values(this.rootPathRoutes) + const parts = pathname.split('/').filter(Boolean) + + for (let i = 1; i < parts.length; i++) { + const parentPath = `/${parts.slice(0, i).join('/')}` + + if (paths.includes(parentPath)) { + layoutPaths.push(parentPath) + } + } + // TODO: when should we bail on adding the root.js wrapper + layoutPaths.unshift('/_root') + } + return layoutPaths + } + protected async run( req: BaseNextRequest, res: BaseNextResponse, @@ -1688,14 +1750,63 @@ export default abstract class Server { let page = pathname const bubbleNoFallback = !!query._nextBubbleNoFallback delete query._nextBubbleNoFallback + // map the route to the actual bundle name e.g. + // `/dashboard/rootonly/hello` -> `/dashboard+rootonly/hello` + const getOriginalRootPath = (rootPath: string) => { + if (this.nextConfig.experimental.rootDir) { + const originalRootPath = + this.rootPathRoutes?.[`${pathname}/index`] || + this.rootPathRoutes?.[pathname] + + if (!originalRootPath) { + return null + } + const isRoutable = this.isRoutableRootPath(rootPath) + + // 404 when layout is hit and this isn't a routable path + // e.g. root/hello.js with root/hello/another.js but + // no root/hello/index.js + if (!isRoutable) { + return '' + } + return originalRootPath + } + return null + } + + const gatherRootLayouts = async ( + rootPath: string, + result: FindComponentsResult + ): Promise => { + const layoutPaths = this.getRootPathLayouts(rootPath) + result.components.rootLayouts = await Promise.all( + layoutPaths.map(async (path) => { + const layoutRes = await this.findPageComponents(path) + return { + isRoot: path === '/_root', + Component: layoutRes?.components.Component!, + getStaticProps: layoutRes?.components.getStaticProps, + getServerSideProps: layoutRes?.components.getServerSideProps, + } + }) + ) + } try { // Ensure a request to the URL /accounts/[id] will be treated as a dynamic // route correctly and not loaded immediately without parsing params. if (!isDynamicRoute(pathname)) { - const result = await this.findPageComponents(pathname, query) + const rootPath = getOriginalRootPath(pathname) + + if (typeof rootPath === 'string') { + page = rootPath + } + const result = await this.findPageComponents(page, query) if (result) { try { + if (result.components.isRootPath) { + await gatherRootLayouts(page, result) + } return await this.renderToResponseWithComponents(ctx, result) } catch (err) { const isNoFallbackError = err instanceof NoFallbackError @@ -1713,6 +1824,12 @@ export default abstract class Server { if (!params) { continue } + page = dynamicRoute.page + const rootPath = getOriginalRootPath(page) + + if (typeof rootPath === 'string') { + page = rootPath + } const dynamicRouteResult = await this.findPageComponents( dynamicRoute.page, @@ -1721,11 +1838,13 @@ export default abstract class Server { ) if (dynamicRouteResult) { try { - page = dynamicRoute.page + if (dynamicRouteResult.components.isRootPath) { + await gatherRootLayouts(page, dynamicRouteResult) + } return await this.renderToResponseWithComponents( { ...ctx, - pathname: dynamicRoute.page, + pathname: page, renderOpts: { ...ctx.renderOpts, params, diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index d17049d47966..4a71f8bfb5cd 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -168,6 +168,7 @@ export default class HotReloader { private fallbackWatcher: any private hotReloaderSpan: Span private pagesMapping: { [key: string]: string } = {} + private rootDir?: string constructor( dir: string, @@ -178,6 +179,7 @@ export default class HotReloader { buildId, previewProps, rewrites, + rootDir, }: { config: NextConfigComplete pagesDir: string @@ -185,12 +187,14 @@ export default class HotReloader { buildId: string previewProps: __ApiPreviewProps rewrites: CustomRoutes['rewrites'] + rootDir?: string } ) { this.buildId = buildId this.dir = dir this.middlewares = [] this.pagesDir = pagesDir + this.rootDir = rootDir this.distDir = distDir this.clientStats = null this.serverStats = null @@ -423,6 +427,7 @@ export default class HotReloader { pagesDir: this.pagesDir, rewrites: this.rewrites, runWebpackSpan: this.hotReloaderSpan, + rootDir: this.rootDir, } return webpackConfigSpan @@ -834,6 +839,7 @@ export default class HotReloader { multiCompiler, watcher: this.watcher, pagesDir: this.pagesDir, + rootDir: this.rootDir, nextConfig: this.config, ...(this.config.onDemandEntries as { maxInactiveAge: number diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index c935563c58b0..3a855aa3c624 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -65,6 +65,8 @@ import { getMiddlewareRegex } from '../../shared/lib/router/utils/get-middleware import { isCustomErrorPage, isReservedPage } from '../../build/utils' import { NodeNextResponse, NodeNextRequest } from '../base-http/node' import { getPageRuntime, invalidatePageRuntimeCache } from '../../build/entries' +import { normalizePathSep } from '../../shared/lib/page-path/normalize-path-sep' +import { normalizeRootPath } from '../../shared/lib/router/utils/root-paths' // Load ReactDevOverlay only when needed let ReactDevOverlayImpl: React.FunctionComponent @@ -96,7 +98,6 @@ export default class DevServer extends Server { protected sortedRoutes?: string[] private addedUpgradeListener = false private pagesDir: string - // @ts-ignore TODO: add implementation private rootDir?: string protected staticPathsWorker?: { [key: string]: any } & { @@ -264,24 +265,61 @@ export default class DevServer extends Server { }) let wp = (this.webpackWatcher = new Watchpack()) - wp.watch([], [this.pagesDir], 0) + const toWatch = [this.pagesDir!] + + if (this.rootDir) { + toWatch.push(this.rootDir) + } + wp.watch([], toWatch, 0) wp.on('aggregated', async () => { const routedMiddleware = [] - const routedPages = [] + const routedPages: string[] = [] const knownFiles = wp.getTimeInfoEntries() + const rootPaths: Record = {} const ssrMiddleware = new Set() for (const [fileName, { accuracy, safeTime }] of knownFiles) { if (accuracy === undefined || !regexPageExtension.test(fileName)) { continue } + let pageName: string = '' + let isRootPath = false - const pageName = absolutePathToPage( - this.pagesDir, - fileName, - this.nextConfig.pageExtensions - ) + if ( + this.rootDir && + normalizePathSep(fileName).startsWith( + normalizePathSep(this.rootDir) + ) + ) { + isRootPath = true + pageName = absolutePathToPage( + this.rootDir, + fileName, + this.nextConfig.pageExtensions, + false + ) + } else { + pageName = absolutePathToPage( + this.pagesDir, + fileName, + this.nextConfig.pageExtensions + ) + } + + if (isRootPath) { + // TODO: should only routes ending in /index.js be route-able? + const originalPageName = pageName + pageName = normalizeRootPath(pageName) + rootPaths[pageName] = originalPageName + + if (routedPages.includes(pageName)) { + continue + } + } else { + // /index is preserved for root folder + pageName = pageName.replace(/\/index$/, '') || '/' + } if (regexMiddleware.test(fileName)) { routedMiddleware.push( @@ -310,6 +348,7 @@ export default class DevServer extends Server { routedPages.push(pageName) } + this.rootPathRoutes = rootPaths this.middleware = getSortedRoutes(routedMiddleware).map((page) => ({ match: getRouteMatcher( getMiddlewareRegex(page, !ssrMiddleware.has(page)) @@ -398,6 +437,7 @@ export default class DevServer extends Server { previewProps: this.getPreviewProps(), buildId: this.buildId, rewrites, + rootDir: this.rootDir, }) await super.prepare() await this.addExportPathMapRoutes() @@ -445,7 +485,6 @@ export default class DevServer extends Server { protected async hasPage(pathname: string): Promise { let normalizedPath: string - try { normalizedPath = normalizePagePath(pathname) } catch (err) { @@ -456,6 +495,16 @@ export default class DevServer extends Server { return false } + // check rootDir first if enabled + if (this.rootDir) { + const pageFile = await findPageFile( + this.rootDir, + normalizedPath, + this.nextConfig.pageExtensions + ) + if (pageFile) return true + } + const pageFile = await findPageFile( this.pagesDir, normalizedPath, @@ -737,6 +786,10 @@ export default class DevServer extends Server { return undefined } + protected getRootPathsManifest(): undefined { + return undefined + } + protected getMiddleware(): never[] { return [] } diff --git a/packages/next/server/dev/on-demand-entry-handler.ts b/packages/next/server/dev/on-demand-entry-handler.ts index 08eabdff3a53..6ca20c9b068c 100644 --- a/packages/next/server/dev/on-demand-entry-handler.ts +++ b/packages/next/server/dev/on-demand-entry-handler.ts @@ -54,6 +54,7 @@ export function onDemandEntryHandler({ nextConfig, pagesBufferLength, pagesDir, + rootDir, watcher, }: { maxInactiveAge: number @@ -61,6 +62,7 @@ export function onDemandEntryHandler({ nextConfig: NextConfigComplete pagesBufferLength: number pagesDir: string + rootDir?: string watcher: any }) { const invalidator = new Invalidator(watcher) @@ -78,13 +80,16 @@ export function onDemandEntryHandler({ function getPagePathsFromEntrypoints( type: 'client' | 'server' | 'edge-server', - entrypoints: Map + entrypoints: Map, + root?: boolean ) { const pagePaths: string[] = [] for (const entrypoint of entrypoints.values()) { - const page = getRouteFromEntrypoint(entrypoint.name!) + const page = getRouteFromEntrypoint(entrypoint.name!, root) if (page) { pagePaths.push(`${type}${page}`) + } else if (root && entrypoint.name === 'root') { + pagePaths.push(`${type}/${entrypoint.name}`) } } @@ -96,19 +101,23 @@ export function onDemandEntryHandler({ return invalidator.doneBuilding() } const [clientStats, serverStats, edgeServerStats] = multiStats.stats + const root = !!rootDir const pagePaths = [ ...getPagePathsFromEntrypoints( 'client', - clientStats.compilation.entrypoints + clientStats.compilation.entrypoints, + root ), ...getPagePathsFromEntrypoints( 'server', - serverStats.compilation.entrypoints + serverStats.compilation.entrypoints, + root ), ...(edgeServerStats ? getPagePathsFromEntrypoints( 'edge-server', - edgeServerStats.compilation.entrypoints + edgeServerStats.compilation.entrypoints, + root ) : []), ] @@ -172,7 +181,8 @@ export function onDemandEntryHandler({ const pagePathData = await findPagePathData( pagesDir, page, - nextConfig.pageExtensions + nextConfig.pageExtensions, + rootDir ) let entryAdded = false @@ -329,18 +339,50 @@ class Invalidator { async function findPagePathData( pagesDir: string, page: string, - extensions: string[] + extensions: string[], + rootDir?: string ) { const normalizedPagePath = tryToNormalizePagePath(page) - const pagePath = await findPageFile(pagesDir, normalizedPagePath, extensions) + let pagePath: string | null = null + let isRoot = false + const isRootFile = rootDir && normalizedPagePath === '/_root' + + // check rootDir first + if (rootDir) { + pagePath = await findPageFile( + join(rootDir, isRootFile ? '..' : ''), + isRootFile ? 'root' : normalizedPagePath, + extensions + ) + + if (pagePath) { + isRoot = true + } + } + + if (!pagePath) { + pagePath = await findPageFile(pagesDir, normalizedPagePath, extensions) + } + if (pagePath !== null) { const pageUrl = ensureLeadingSlash( - removePagePathTail(normalizePathSep(pagePath), extensions) + removePagePathTail(normalizePathSep(pagePath), extensions, !isRoot) ) + const bundleFile = normalizePagePath(pageUrl) + let bundlePath + let absolutePagePath + + if (isRootFile) { + bundlePath = 'root' + absolutePagePath = join(rootDir!, '..', pagePath) + } else { + bundlePath = posix.join(isRoot ? 'root' : 'pages', bundleFile) + absolutePagePath = join(isRoot ? rootDir! : pagesDir, pagePath) + } return { - absolutePagePath: join(pagesDir, pagePath), - bundlePath: posix.join('pages', normalizePagePath(pageUrl)), + absolutePagePath, + bundlePath, page: posix.normalize(pageUrl), } } diff --git a/packages/next/server/get-page-files.ts b/packages/next/server/get-page-files.ts index 5e6945cbdf53..229729b3e397 100644 --- a/packages/next/server/get-page-files.ts +++ b/packages/next/server/get-page-files.ts @@ -6,6 +6,7 @@ export type BuildManifest = { ampDevFiles: readonly string[] polyfillFiles: readonly string[] lowPriorityFiles: readonly string[] + rootMainFiles: readonly string[] pages: { '/_app': readonly string[] [page: string]: readonly string[] diff --git a/packages/next/server/get-route-from-entrypoint.ts b/packages/next/server/get-route-from-entrypoint.ts index c412e2d886cc..ddeafbfff538 100644 --- a/packages/next/server/get-route-from-entrypoint.ts +++ b/packages/next/server/get-route-from-entrypoint.ts @@ -2,6 +2,8 @@ import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-ass // matches pages/:page*.js const SERVER_ROUTE_NAME_REGEX = /^pages[/\\](.*)$/ +// matches root/:path*.js +const ROOT_ROUTE_NAME_REGEX = /^root[/\\](.*)$/ // matches static/pages/:page*.js const BROWSER_ROUTE_NAME_REGEX = /^static[/\\]pages[/\\](.*)$/ @@ -16,7 +18,8 @@ function matchBundle(regex: RegExp, input: string): string | null { } export default function getRouteFromEntrypoint( - entryFile: string + entryFile: string, + root?: boolean ): string | null { let pagePath = matchBundle(SERVER_ROUTE_NAME_REGEX, entryFile) @@ -24,6 +27,11 @@ export default function getRouteFromEntrypoint( return pagePath } + if (root) { + pagePath = matchBundle(ROOT_ROUTE_NAME_REGEX, entryFile) + if (pagePath) return pagePath + } + // Potentially the passed item is a browser bundle so we try to match that also return matchBundle(BROWSER_ROUTE_NAME_REGEX, entryFile) } diff --git a/packages/next/server/lib/find-page-file.ts b/packages/next/server/lib/find-page-file.ts index b7e60c2b7dd5..7c3e8bf838d3 100644 --- a/packages/next/server/lib/find-page-file.ts +++ b/packages/next/server/lib/find-page-file.ts @@ -20,7 +20,12 @@ export async function findPageFile( normalizedPagePath: string, pageExtensions: string[] ): Promise { - const pagePaths = getPagePaths(normalizedPagePath, pageExtensions) + const isRootPaths = pagesDir.replace(/\\/g, '/').endsWith('/root') + const pagePaths = getPagePaths( + normalizedPagePath, + pageExtensions, + isRootPaths + ) const [existingPath, ...others] = ( await Promise.all( pagePaths.map(async (path) => diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index 881598c769ee..862834e523f2 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -9,7 +9,7 @@ import { MIDDLEWARE_FLIGHT_MANIFEST, } from '../shared/lib/constants' import { join } from 'path' -import { requirePage } from './require' +import { requirePage, getPagePath } from './require' import { BuildManifest } from './get-page-files' import { interopDefault } from '../lib/interop-default' import { @@ -40,6 +40,13 @@ export type LoadComponentsReturnType = { ComponentMod: any AppMod: any AppServerMod: any + isRootPath?: boolean + rootLayouts?: Array<{ + isRoot?: boolean + Component: NextComponentType + getStaticProps?: GetStaticProps + getServerSideProps?: GetServerSideProps + }> } export async function loadDefaultErrorComponents(distDir: string) { @@ -67,7 +74,8 @@ export async function loadComponents( distDir: string, pathname: string, serverless: boolean, - serverComponents?: boolean + serverComponents?: boolean, + rootEnabled?: boolean ): Promise { if (serverless) { const ComponentMod = await requirePage(pathname, distDir, serverless) @@ -103,10 +111,12 @@ export async function loadComponents( } const [DocumentMod, AppMod, ComponentMod, AppServerMod] = await Promise.all([ - requirePage('/_document', distDir, serverless), - requirePage('/_app', distDir, serverless), - requirePage(pathname, distDir, serverless), - serverComponents ? requirePage('/_app.server', distDir, serverless) : null, + requirePage('/_document', distDir, serverless, rootEnabled), + requirePage('/_app', distDir, serverless, rootEnabled), + requirePage(pathname, distDir, serverless, rootEnabled), + serverComponents + ? requirePage('/_app.server', distDir, serverless, rootEnabled) + : null, ]) const [buildManifest, reactLoadableManifest, serverComponentManifest] = @@ -124,6 +134,20 @@ export async function loadComponents( const { getServerSideProps, getStaticProps, getStaticPaths } = ComponentMod + let isRootPath = false + + if (rootEnabled) { + const pagePath = getPagePath( + pathname, + distDir, + serverless, + false, + undefined, + rootEnabled + ) + isRootPath = !!pagePath?.match(/server[/\\]root[/\\]/) + } + return { App, Document, @@ -138,5 +162,6 @@ export async function loadComponents( getStaticProps, getStaticPaths, serverComponentManifest, + isRootPath, } } diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index c0a5e91ccef7..d0b527aea940 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -31,6 +31,7 @@ import { ROUTES_MANIFEST, MIDDLEWARE_FLIGHT_MANIFEST, CLIENT_PUBLIC_FILES_PATH, + ROOT_PATHS_MANIFEST, } from '../shared/lib/constants' import { recursiveReadDirSync } from './lib/recursive-readdir-sync' import { format as formatUrl, UrlWithParsedQuery } from 'url' @@ -45,6 +46,7 @@ import { getExtension, serveStatic } from './serve-static' import { ParsedUrlQuery } from 'querystring' import { apiResolver } from './api-utils/node' import { RenderOpts, renderToHTML } from './render' +import { renderToHTML as rootRenderToHTML } from './root-render' import { ParsedUrl, parseUrl } from '../shared/lib/router/utils/parse-url' import * as Log from '../build/output/log' @@ -157,6 +159,16 @@ export default class NextNodeServer extends BaseServer { return require(join(this.serverDistDir, PAGES_MANIFEST)) } + protected getRootPathsManifest(): PagesManifest | undefined { + if (this.nextConfig.experimental.rootDir) { + const rootPathsManifestPath = join( + this.serverDistDir, + ROOT_PATHS_MANIFEST + ) + return require(rootPathsManifestPath) + } + } + protected getBuildId(): string { const buildIdFile = join(this.distDir, BUILD_ID_FILE) try { @@ -572,6 +584,16 @@ export default class NextNodeServer extends BaseServer { // https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952 renderOpts.serverComponentManifest = this.serverComponentManifest + if (renderOpts.isRootPath) { + return rootRenderToHTML( + req.originalRequest, + res.originalResponse, + pathname, + query, + renderOpts + ) + } + return renderToHTML( req.originalRequest, res.originalResponse, @@ -619,7 +641,8 @@ export default class NextNodeServer extends BaseServer { this.distDir, this._isLikeServerless, this.renderOpts.dev, - locales + locales, + this.nextConfig.experimental.rootDir ) } @@ -649,7 +672,8 @@ export default class NextNodeServer extends BaseServer { this.distDir, pagePath!, !this.renderOpts.dev && this._isLikeServerless, - this.renderOpts.serverComponents + this.renderOpts.serverComponents, + this.nextConfig.experimental.rootDir ) if ( diff --git a/packages/next/server/require.ts b/packages/next/server/require.ts index ee436ab9b64e..3205c04d624d 100644 --- a/packages/next/server/require.ts +++ b/packages/next/server/require.ts @@ -6,6 +6,7 @@ import { PAGES_MANIFEST, SERVER_DIRECTORY, SERVERLESS_DIRECTORY, + ROOT_PATHS_MANIFEST, } from '../shared/lib/constants' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' @@ -25,12 +26,21 @@ export function getPagePath( distDir: string, serverless: boolean, dev?: boolean, - locales?: string[] + locales?: string[], + rootEnabled?: boolean ): string { const serverBuildPath = join( distDir, serverless && !dev ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY ) + let rootPathsManifest: undefined | PagesManifest + + if (rootEnabled) { + if (page === '/_root') { + return join(serverBuildPath, 'root.js') + } + rootPathsManifest = require(join(serverBuildPath, ROOT_PATHS_MANIFEST)) + } const pagesManifest = require(join( serverBuildPath, PAGES_MANIFEST @@ -42,31 +52,51 @@ export function getPagePath( console.error(err) throw pageNotFoundError(page) } - let pagePath = pagesManifest[page] - if (!pagesManifest[page] && locales) { - const manifestNoLocales: typeof pagesManifest = {} + const checkManifest = (manifest: PagesManifest) => { + let curPath = manifest[page] + + if (!manifest[curPath] && locales) { + const manifestNoLocales: typeof pagesManifest = {} - for (const key of Object.keys(pagesManifest)) { - manifestNoLocales[normalizeLocalePath(key, locales).pathname] = - pagesManifest[key] + for (const key of Object.keys(manifest)) { + manifestNoLocales[normalizeLocalePath(key, locales).pathname] = + pagesManifest[key] + } + curPath = manifestNoLocales[page] } - pagePath = manifestNoLocales[page] + return curPath + } + let pagePath: string | undefined + + if (rootPathsManifest) { + pagePath = checkManifest(rootPathsManifest) } if (!pagePath) { - throw pageNotFoundError(page) + pagePath = checkManifest(pagesManifest) } + if (!pagePath) { + throw pageNotFoundError(page) + } return join(serverBuildPath, pagePath) } export function requirePage( page: string, distDir: string, - serverless: boolean + serverless: boolean, + rootEnabled?: boolean ): any { - const pagePath = getPagePath(page, distDir, serverless) + const pagePath = getPagePath( + page, + distDir, + serverless, + false, + undefined, + rootEnabled + ) if (pagePath.endsWith('.html')) { return promises.readFile(pagePath, 'utf8') } diff --git a/packages/next/server/root-render.tsx b/packages/next/server/root-render.tsx new file mode 100644 index 000000000000..6e9af6d9f7cd --- /dev/null +++ b/packages/next/server/root-render.tsx @@ -0,0 +1,496 @@ +import type { IncomingMessage, ServerResponse } from 'http' +import type { LoadComponentsReturnType } from './load-components' + +import React from 'react' +import { ParsedUrlQuery, stringify as stringifyQuery } from 'querystring' +import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack' +import { renderToReadableStream } from 'next/dist/compiled/react-server-dom-webpack/writer.browser.server' +import { StyleRegistry, createStyleRegistry } from 'styled-jsx' +import { NextParsedUrlQuery } from './request-meta' +import RenderResult from './render-result' +import { + readableStreamTee, + encodeText, + decodeText, + renderToInitialStream, + createBufferedTransformStream, + continueFromInitialStream, +} from './node-web-streams-helper' +import { FlushEffectsContext } from '../shared/lib/flush-effects' +// @ts-ignore react-dom/client exists when using React 18 +import ReactDOMServer from 'react-dom/server.browser' +import { isDynamicRoute } from '../shared/lib/router/utils' +import { tryGetPreviewData } from './api-utils/node' + +export type RenderOptsPartial = { + err?: Error | null + dev?: boolean + serverComponentManifest?: any + supportsDynamicHTML?: boolean + runtime?: 'nodejs' | 'edge' + serverComponents?: boolean + reactRoot: boolean +} + +export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial + +const rscCache = new Map() + +// Shadowing check does not work with TypeScript enums +// eslint-disable-next-line no-shadow +const enum RecordStatus { + Pending, + Resolved, + Rejected, +} + +type Record = { + status: RecordStatus + value: any +} + +function createRecordFromThenable(thenable: Promise) { + const record: Record = { + status: RecordStatus.Pending, + value: thenable, + } + thenable.then( + function (value) { + if (record.status === RecordStatus.Pending) { + const resolvedRecord = record + resolvedRecord.status = RecordStatus.Resolved + resolvedRecord.value = value + } + }, + function (err) { + if (record.status === RecordStatus.Pending) { + const rejectedRecord = record + rejectedRecord.status = RecordStatus.Rejected + rejectedRecord.value = err + } + } + ) + return record +} + +function readRecordValue(record: Record) { + if (record.status === RecordStatus.Resolved) { + return record.value + } else { + throw record.value + } +} + +function preloadDataFetchingRecord( + map: Map, + key: string, + fetcher: () => Promise | any +) { + let record = map.get(key) + + if (!record) { + const thenable = fetcher() + record = createRecordFromThenable(thenable) + map.set(key, record) + } + + return record +} + +function createFlightHook() { + return ( + writable: WritableStream, + id: string, + req: ReadableStream, + bootstrap: boolean + ) => { + let entry = rscCache.get(id) + if (!entry) { + const [renderStream, forwardStream] = readableStreamTee(req) + entry = createFromReadableStream(renderStream) + rscCache.set(id, entry) + + let bootstrapped = false + const forwardReader = forwardStream.getReader() + const writer = writable.getWriter() + function process() { + forwardReader.read().then(({ done, value }) => { + if (bootstrap && !bootstrapped) { + bootstrapped = true + writer.write( + encodeText( + `` + ) + ) + } + if (done) { + rscCache.delete(id) + writer.close() + } else { + writer.write( + encodeText( + `` + ) + ) + process() + } + }) + } + process() + } + return entry + } +} + +const useFlightResponse = createFlightHook() + +// Create the wrapper component for a Flight stream. +function createServerComponentRenderer( + ComponentToRender: React.ComponentType, + ComponentMod: any, + { + cachePrefix, + transformStream, + serverComponentManifest, + }: { + cachePrefix: string + transformStream: TransformStream + serverComponentManifest: NonNullable + } +) { + // We need to expose the `__webpack_require__` API globally for + // react-server-dom-webpack. This is a hack until we find a better way. + if (ComponentMod.__next_rsc__) { + // @ts-ignore + globalThis.__webpack_require__ = + ComponentMod.__next_rsc__.__webpack_require__ + + // @ts-ignore + globalThis.__webpack_chunk_load__ = () => Promise.resolve() + } + + const writable = transformStream.writable + const ServerComponentWrapper = (props: any) => { + const id = (React as any).useId() + const reqStream: ReadableStream = renderToReadableStream( + , + serverComponentManifest + ) + + const response = useFlightResponse( + writable, + cachePrefix + ',' + id, + reqStream, + true + ) + const root = response.readRoot() + rscCache.delete(id) + return root + } + + return ServerComponentWrapper +} + +export async function renderToHTML( + req: IncomingMessage, + res: ServerResponse, + pathname: string, + query: NextParsedUrlQuery, + renderOpts: RenderOpts +): Promise { + // don't modify original query object + query = Object.assign({}, query) + + const { + buildManifest, + serverComponentManifest, + supportsDynamicHTML, + runtime, + ComponentMod, + } = renderOpts + + const hasConcurrentFeatures = !!runtime + const pageIsDynamic = isDynamicRoute(pathname) + const layouts = renderOpts.rootLayouts || [] + + layouts.push({ + Component: renderOpts.Component, + getStaticProps: renderOpts.getStaticProps, + getServerSideProps: renderOpts.getServerSideProps, + }) + + // Reads of this are cached on the `req` object, so this should resolve + // instantly. There's no need to pass this data down from a previous + // invoke, where we'd have to consider server & serverless. + const previewData = tryGetPreviewData( + req, + res, + (renderOpts as any).previewProps + ) + const isPreview = previewData !== false + + let WrappedComponent: any + let RootLayout: any + + const dataCache = new Map() + + for (let i = layouts.length - 1; i >= 0; i--) { + const dataCacheKey = i.toString() + const layout = layouts[i] + + if (layout.isRoot) { + RootLayout = layout.Component + continue + } + let fetcher: any + + // TODO: pass a shared cache from previous getStaticProps/ + // getServerSideProps calls? + if (layout.getServerSideProps) { + fetcher = () => + Promise.resolve( + layout.getServerSideProps!({ + req: req as any, + res: res, + query, + resolvedUrl: (renderOpts as any).resolvedUrl as string, + ...(pageIsDynamic + ? { params: (renderOpts as any).params as ParsedUrlQuery } + : undefined), + ...(isPreview + ? { preview: true, previewData: previewData } + : undefined), + locales: (renderOpts as any).locales, + locale: (renderOpts as any).locale, + defaultLocale: (renderOpts as any).defaultLocale, + }) + ) + } + // TODO: implement layout specific caching for getStaticProps + if (layout.getStaticProps) { + fetcher = () => + Promise.resolve( + layout.getStaticProps!({ + ...(pageIsDynamic + ? { params: query as ParsedUrlQuery } + : undefined), + ...(isPreview + ? { preview: true, previewData: previewData } + : undefined), + locales: (renderOpts as any).locales, + locale: (renderOpts as any).locale, + defaultLocale: (renderOpts as any).defaultLocale, + }) + ) + } + + if (fetcher) { + // Kick off data fetching before rendering, this ensures there is no waterfall for layouts as + // all data fetching required to render the page is kicked off simultaneously + preloadDataFetchingRecord(dataCache, dataCacheKey, fetcher) + } + + // eslint-disable-next-line no-loop-func + const lastComponent = WrappedComponent + WrappedComponent = () => { + let props: any + if (fetcher) { + // The data fetching was kicked off before rendering (see above) + // if the data was not resolved yet the layout rendering will be suspended + const record = preloadDataFetchingRecord( + dataCache, + dataCacheKey, + fetcher + ) + // Result of calling getStaticProps or getServerSideProps. If promise is not resolve yet it will suspend. + const recordValue = readRecordValue(record) + props = recordValue.props + } + + return React.createElement( + layout.Component, + props, + React.createElement(lastComponent || React.Fragment, {}, null) + ) + } + // TODO: loading state + // const AfterWrap = WrappedComponent + // WrappedComponent = () => { + // return ( + // Loading...}> + // + // + // ) + // } + } + + if (!RootLayout) { + // TODO: fallback to our own root layout? + throw new Error('invariant RootLayout not loaded') + } + + const headChildren = buildManifest.rootMainFiles.map((src) => ( +