diff options
author | 2021-08-13 02:01:41 -0700 | |
---|---|---|
committer | 2021-08-13 02:01:41 -0700 | |
commit | f59892f647ceef1c05e40c9cdef4f79d0a530c2f (patch) | |
tree | 6fed07dc02e722de634618a6f2d246f52510d141 | |
parent | 86642cbdd528bbf37708dbb30a815c9a68b6aea1 (diff) | |
download | bun-f59892f647ceef1c05e40c9cdef4f79d0a530c2f.tar.gz bun-f59892f647ceef1c05e40c9cdef4f79d0a530c2f.tar.zst bun-f59892f647ceef1c05e40c9cdef4f79d0a530c2f.zip |
late
Former-commit-id: 1d598bb05a3bac62d86063125e1fe2962f0b5cc6
-rw-r--r-- | demos/css-stress-test/framework.tsx | 38 | ||||
-rw-r--r-- | demos/css-stress-test/next-env.d.ts | 4 | ||||
-rw-r--r-- | demos/css-stress-test/nexty/client.development.tsx | 33 | ||||
-rw-r--r-- | demos/css-stress-test/nexty/next-server.tsx | 0 | ||||
-rw-r--r-- | demos/css-stress-test/nexty/render.tsx | 0 | ||||
-rw-r--r-- | demos/css-stress-test/nexty/renderDocument.tsx | 568 | ||||
-rw-r--r-- | demos/css-stress-test/nexty/server.development.tsx | 103 | ||||
-rw-r--r-- | demos/css-stress-test/nexty/tsconfig.json | 23 | ||||
-rw-r--r-- | src/bundler.zig | 10 | ||||
-rw-r--r-- | src/css_scanner.zig | 23 | ||||
-rw-r--r-- | src/http.zig | 4 | ||||
-rw-r--r-- | src/http/url_path.zig | 72 | ||||
-rw-r--r-- | src/javascript/jsc/api/router.zig | 46 | ||||
-rw-r--r-- | src/javascript/jsc/base.zig | 4 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/ZigGlobalObject.cpp | 40 | ||||
-rw-r--r-- | src/javascript/jsc/javascript.zig | 3 | ||||
-rw-r--r-- | src/js_parser/js_parser.zig | 57 | ||||
-rw-r--r-- | src/js_printer.zig | 34 | ||||
-rw-r--r-- | src/options.zig | 34 | ||||
-rw-r--r-- | src/runtime.js | 4 | ||||
-rw-r--r-- | src/runtime.version | 2 |
21 files changed, 948 insertions, 154 deletions
diff --git a/demos/css-stress-test/framework.tsx b/demos/css-stress-test/framework.tsx deleted file mode 100644 index 22b1a3109..000000000 --- a/demos/css-stress-test/framework.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import ReactDOMServer from "react-dom/server.browser"; - -addEventListener("fetch", async (event: FetchEvent) => { - var route = Wundle.match(event); - console.log("Route", JSON.stringify(route.query, null, 2)); - const { default: PageComponent } = await import(route.filepath); - // const router = Wundle.Router.match(event); - // console.log("Route", router.name); - - // const { Base: Page } = await router.import(); - - const response = new Response(` - <!DOCTYPE html> -<html> - <head> - <link rel="stylesheet" href="./src/index.css" /> - <link - rel="stylesheet" - crossorigin="anonymous" - href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;700&family=Space+Mono:wght@400;700" - /> - </head> - <body> - <link rel="stylesheet" href="./src/index.css" /> - <div id="reactroot">${ReactDOMServer.renderToString( - <PageComponent /> - )}</div> - - <script src="./src/index.tsx" async type="module"></script> - </body> -</html> - `); - - event.respondWith(response); -}); - -// typescript isolated modules -export {}; diff --git a/demos/css-stress-test/next-env.d.ts b/demos/css-stress-test/next-env.d.ts index 7b7aa2c77..9bc3dd46b 100644 --- a/demos/css-stress-test/next-env.d.ts +++ b/demos/css-stress-test/next-env.d.ts @@ -1,2 +1,6 @@ /// <reference types="next" /> /// <reference types="next/types/global" /> +/// <reference types="next/image-types/global" /> + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/demos/css-stress-test/nexty/client.development.tsx b/demos/css-stress-test/nexty/client.development.tsx index 1bc63a0c7..f15ccee28 100644 --- a/demos/css-stress-test/nexty/client.development.tsx +++ b/demos/css-stress-test/nexty/client.development.tsx @@ -1,5 +1,34 @@ -import ReactDOM from "react-dom"; +globalThis.process = { + platform: "posix", + env: {}, +}; + +import * as ReactDOM from "react-dom"; +import App from "next/app"; export default function boot(EntryPointNamespace, loader) { - + _boot(EntryPointNamespace); +} + +function _boot(EntryPointNamespace) { + const next_data_node = document.querySelector("#__NEXT_DATA__"); + if (!next_data_node) { + throw new Error( + "__NEXT_DATA__ is missing. That means something went wrong while rendering on the server." + ); + } + + try { + globalThis.NEXT_DATA = JSON.parse(next_data_node.innerHTML); + } catch (error) { + error.message = `Error parsing __NEXT_DATA__\n${error.message}`; + throw error; + } + + const props = { ...globalThis.NEXT_DATA.props }; + const PageComponent = EntryPointNamespace.default; + ReactDOM.hydrate( + <App Component={PageComponent} pageProps={props.pageProps}></App>, + document.querySelector("#__next") + ); } diff --git a/demos/css-stress-test/nexty/next-server.tsx b/demos/css-stress-test/nexty/next-server.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/demos/css-stress-test/nexty/next-server.tsx diff --git a/demos/css-stress-test/nexty/render.tsx b/demos/css-stress-test/nexty/render.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/demos/css-stress-test/nexty/render.tsx diff --git a/demos/css-stress-test/nexty/renderDocument.tsx b/demos/css-stress-test/nexty/renderDocument.tsx new file mode 100644 index 000000000..3ada60c86 --- /dev/null +++ b/demos/css-stress-test/nexty/renderDocument.tsx @@ -0,0 +1,568 @@ +import * as App from "next/app"; +import { AmpStateContext } from "next/dist/shared/lib/amp-context"; +import { HeadManagerContext } from "next/dist/shared/lib/head-manager-context"; +import Loadable from "next/dist/shared/lib/loadable"; +import { LoadableContext } from "next/dist/shared/lib/loadable-context"; +import { RouterContext } from "next/dist/shared/lib/router-context"; +import { NextRouter } from "next/dist/shared/lib/router/router"; +import { + AppType, + ComponentsEnhancer, + DocumentInitialProps, + DocumentProps, + DocumentType, + getDisplayName, + loadGetInitialProps, + NextComponentType, + RenderPage, + RenderPageResult, +} from "next/dist/shared/lib/utils"; +import * as NextDocument from "next/document"; +import * as ReactDOMServer from "react-dom/server.browser"; +import * as url from "url"; + +const dev = process.env.NODE_ENV === "development"; + +type ParsedUrlQuery = Record<string, string | string[]>; + +const isJSFile = (file: string) => + file.endsWith(".js") || + file.endsWith(".mjs") || + file.endsWith(".ts") || + file.endsWith(".tsx"); + +function getScripts(files: DocumentFiles) { + const { context, props } = this; + const { + assetPrefix, + buildManifest, + isDevelopment, + devOnlyCacheBusterQueryString, + disableOptimizedLoading, + } = context; + const normalScripts = files.allFiles.filter(isJSFile); + const lowPriorityScripts = buildManifest.lowPriorityFiles?.filter(isJSFile); + + return [...normalScripts, ...lowPriorityScripts].map((file) => { + return ( + <script + key={file} + src={`${assetPrefix}/_next/${encodeURI( + file + )}${devOnlyCacheBusterQueryString}`} + nonce={props.nonce} + async + crossOrigin={props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN} + type="module" + /> + ); + }); +} + +interface DomainLocale { + defaultLocale: string; + domain: string; + http?: true; + locales?: string[]; +} + +function renderDocument( + Document: DocumentType, + { + buildManifest, + docComponentsRendered, + props, + docProps, + pathname, + query, + buildId, + canonicalBase, + assetPrefix, + runtimeConfig, + nextExport, + autoExport, + isFallback, + dynamicImportsIds, + dangerousAsPath, + err, + dev, + ampPath, + ampState, + inAmpMode, + hybridAmp, + dynamicImports, + headTags, + gsp, + gssp, + customServer, + gip, + appGip, + unstable_runtimeJS, + unstable_JsPreload, + devOnlyCacheBusterQueryString, + scriptLoader, + locale, + locales, + defaultLocale, + domainLocales, + isPreview, + disableOptimizedLoading, + }: RenderOpts & { + props: any; + docComponentsRendered: DocumentProps["docComponentsRendered"]; + docProps: DocumentInitialProps; + pathname: string; + query: ParsedUrlQuery; + dangerousAsPath: string; + ampState: any; + ampPath: string; + inAmpMode: boolean; + hybridAmp: boolean; + dynamicImportsIds: (string | number)[]; + dynamicImports: string[]; + headTags: any; + isFallback?: boolean; + gsp?: boolean; + gssp?: boolean; + customServer?: boolean; + gip?: boolean; + appGip?: boolean; + devOnlyCacheBusterQueryString: string; + scriptLoader: any; + isPreview?: boolean; + autoExport?: boolean; + } +): string { + return ( + "<!DOCTYPE html>" + + ReactDOMServer.renderToStaticMarkup( + <AmpStateContext.Provider value={ampState}> + {Document.renderDocument(Document, { + __NEXT_DATA__: { + props, // The result of getInitialProps + page: pathname, // The rendered page + query, // querystring parsed / passed by the user + buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles + assetPrefix: assetPrefix === "" ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML + runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML + nextExport, // If this is a page exported by `next export` + autoExport, // If this is an auto exported page + isFallback, + dynamicIds: + dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds, + err: err || undefined, // err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML + gsp, // whether the page is getStaticProps + gssp, // whether the page is getServerSideProps + customServer, // whether the user is using a custom server + gip, // whether the page has getInitialProps + appGip, // whether the _app has getInitialProps + locale, + locales, + defaultLocale, + domainLocales, + isPreview, + }, + buildManifest, + docComponentsRendered, + dangerousAsPath, + canonicalBase, + ampPath, + inAmpMode, + isDevelopment: !!dev, + hybridAmp, + dynamicImports, + assetPrefix, + headTags, + unstable_runtimeJS, + unstable_JsPreload, + devOnlyCacheBusterQueryString, + scriptLoader, + locale, + disableOptimizedLoading, + ...docProps, + })} + </AmpStateContext.Provider> + ) + ); +} + +class ServerRouter implements NextRouter { + route: string; + pathname: string; + query: ParsedUrlQuery; + asPath: string; + basePath: string; + events: any; + isFallback: boolean; + locale?: string; + isReady: boolean; + locales?: string[]; + defaultLocale?: string; + domainLocales?: DomainLocale[]; + isPreview: boolean; + isLocaleDomain: boolean; + + constructor( + pathname: string, + query: ParsedUrlQuery, + as: string, + { isFallback }: { isFallback: boolean }, + isReady: boolean, + basePath: string, + locale?: string, + locales?: string[], + defaultLocale?: string, + domainLocales?: DomainLocale[], + isPreview?: boolean, + isLocaleDomain?: boolean + ) { + this.route = pathname.replace(/\/$/, "") || "/"; + this.pathname = pathname; + this.query = query; + this.asPath = as; + this.isFallback = isFallback; + this.basePath = basePath; + this.locale = locale; + this.locales = locales; + this.defaultLocale = defaultLocale; + this.isReady = isReady; + this.domainLocales = domainLocales; + this.isPreview = !!isPreview; + this.isLocaleDomain = !!isLocaleDomain; + } + + push(): any { + noRouter(); + } + replace(): any { + noRouter(); + } + reload() { + noRouter(); + } + back() { + noRouter(); + } + prefetch(): any { + noRouter(); + } + beforePopState() { + noRouter(); + } +} + +function noRouter() { + const message = + 'No router instance found. you should only use "next/router" inside the client side of your app. https://nextjs.org/docs/messages/no-router-instance'; + throw new Error(message); +} + +function enhanceComponents( + options: ComponentsEnhancer, + App: AppType, + Component: NextComponentType +): { + App: AppType; + Component: NextComponentType; +} { + // For backwards compatibility + if (typeof options === "function") { + return { + App, + Component: options(Component), + }; + } + + return { + App: options.enhanceApp ? options.enhanceApp(App) : App, + Component: options.enhanceComponent + ? options.enhanceComponent(Component) + : Component, + }; +} + +Object.defineProperty(NextDocument.Head.prototype, "getScripts", { + get() { + return getScripts; + }, +}); +Object.defineProperty(NextDocument.NextScript.prototype, "getScripts", { + get() { + return getScripts; + }, +}); + +export async function render({ + route, + PageNamespace, + AppNamespace, + appStylesheets = [], + pageStylesheets = [], + DocumentNamespace = null, + buildId, +}: { + buildId: number; + route: any; + PageNamespace: { default: NextComponentType<any> }; + AppNamespace: { default: NextComponentType<any> } | null; + DocumentNamespace: Object | null; + appStylesheets: string[]; + pageStylesheets: string[]; +}): Promise<Response> { + const { default: Component, getStaticProps = null } = PageNamespace || {}; + const { default: AppComponent_ } = AppNamespace || {}; + var query = Object.assign({}, route.query); + + // These are reversed in our Router versus Next.js...mostly due to personal preference. + const pathname = "/" + route.name; + var asPath = route.pathname; + + const AppComponent = AppComponent_ || App.default; + const Document = + (DocumentNamespace && DocumentNamespace.default) || NextDocument.default; + // Document.Html.prototype.getScripts = getScripts; + // } + + const callMiddleware = async (method: string, args: any[], props = false) => { + let results: any = props ? {} : []; + + if ((Document as any)[`${method}Middleware`]) { + let middlewareFunc = await (Document as any)[`${method}Middleware`]; + middlewareFunc = middlewareFunc.default || middlewareFunc; + + const curResults = await middlewareFunc(...args); + if (props) { + for (const result of curResults) { + results = { + ...results, + ...result, + }; + } + } else { + results = curResults; + } + } + return results; + }; + + const headTags = (...args: any) => callMiddleware("headTags", args); + + const isFallback = !!query.__nextFallback; + delete query.__nextFallback; + delete query.__nextLocale; + delete query.__nextDefaultLocale; + + const isSSG = !!getStaticProps; + const isBuildTimeSSG = isSSG && false; + const defaultAppGetInitialProps = + App.getInitialProps === (App as any).origGetInitialProps; + + const hasPageGetInitialProps = !!(Component as any).getInitialProps; + const pageIsDynamic = route.kind === "dynamic"; + const isAutoExport = false; + + if (isAutoExport || isFallback) { + // remove query values except ones that will be set during export + query = { + ...(query.amp + ? { + amp: query.amp, + } + : {}), + }; + asPath = `${asPath}${ + // ensure trailing slash is present for non-dynamic auto-export pages + asPath.endsWith("/") && asPath !== "/" && !pageIsDynamic ? "/" : "" + }`; + } + + let head: JSX.Element[] = [ + <meta charSet="utf-8" />, + <meta name="viewport" content="width=device-width" />, + ]; + + const nextExport = isAutoExport || isFallback; + const reactLoadableModules: string[] = []; + var scriptLoader = {}; + const AppContainer = ({ children }: any) => ( + <RouterContext.Provider value={router}> + {/* <AmpStateContext.Provider value={ampState}> */} + <HeadManagerContext.Provider + value={{ + updateHead: (state) => { + head = state; + }, + updateScripts: (scripts) => { + scriptLoader = scripts; + }, + scripts: {}, + mountedInstances: new Set(), + }} + > + <LoadableContext.Provider + value={(moduleName) => reactLoadableModules.push(moduleName)} + > + {children} + </LoadableContext.Provider> + </HeadManagerContext.Provider> + {/* </AmpStateContext.Provider> */} + </RouterContext.Provider> + ); + + await Loadable.preloadAll(); // Make sure all dynamic imports are loaded + + const router = new ServerRouter( + pathname, + query, + asPath, + { + isFallback: isFallback, + }, + true, + Wundle.origin, + null, + [], // renderOpts.locales, + null, //renderOpts.defaultLocale, + [], // renderOpts.domainLocales, + false, + false + ); + + const ctx = { + err: null, + req: undefined, + res: undefined, + pathname, + query, + asPath, + locale: null, + locales: [], + defaultLocale: null, + AppTree: (props: any) => { + return ( + <AppContainer> + <App {...props} Component={Component} router={router} /> + </AppContainer> + ); + }, + }; + + var props = await loadGetInitialProps(AppComponent, { + AppTree: ctx.AppTree, + Component, + router, + ctx, + }); + + const renderToString = ReactDOMServer.renderToString; + const ErrorDebug = null; + + const renderPage: RenderPage = ( + options: ComponentsEnhancer = {} + ): RenderPageResult | Promise<RenderPageResult> => { + if (ctx.err && ErrorDebug) { + const htmlOrPromise = renderToString(<ErrorDebug error={ctx.err} />); + return typeof htmlOrPromise === "string" + ? { html: htmlOrPromise, head } + : htmlOrPromise.then((html) => ({ + html, + head, + })); + } + + if (dev && (props.router || props.Component)) { + throw new Error( + `'router' and 'Component' can not be returned in getInitialProps from _app.js https://nextjs.org/docs/messages/cant-override-next-props` + ); + } + + const { App: EnhancedApp, Component: EnhancedComponent } = + enhanceComponents(options, AppComponent, Component); + + const htmlOrPromise = renderToString( + <AppContainer> + <EnhancedApp Component={EnhancedComponent} router={router} {...props} /> + </AppContainer> + ); + return typeof htmlOrPromise === "string" + ? { html: htmlOrPromise, head } + : htmlOrPromise.then((html) => ({ + html, + head, + })); + }; + + const documentCtx = { ...ctx, renderPage }; + const docProps: DocumentInitialProps = await loadGetInitialProps( + Document, + documentCtx + ); + + if (!docProps || typeof docProps.html !== "string") { + const message = `"${getDisplayName( + Document + )}.getInitialProps()" should resolve to an object with a "html" prop set with a valid html string`; + throw new Error(message); + } + + const renderOpts = { + params: route.params, + }; + // renderOpts.params = _params || params; + + // parsedUrl.pathname = denormalizePagePath(parsedUrl.pathname!); + // renderOpts.resolvedUrl = formatUrl({ + // ...parsedUrl, + // query: origQuery, + // }); + const docComponentsRendered: DocumentProps["docComponentsRendered"] = {}; + const isPreview = false; + const getServerSideProps = PageNamespace.getServerSideProps; + let html = renderDocument(Document, { + docComponentsRendered, + ...renderOpts, + disableOptimizedLoading: false, + canonicalBase: Wundle.origin, + buildManifest: { + devFiles: [], + allFiles: [], + polyfillFiles: [], + lowPriorityFiles: [], + pages: { + "/_app": [Wundle.routesDir + "_app", ...appStylesheets], + [pathname]: [...pageStylesheets, route.scriptSrc], + }, + }, + // Only enabled in production as development mode has features relying on HMR (style injection for example) + unstable_runtimeJS: true, + // process.env.NODE_ENV === "production" + // ? pageConfig.unstable_runtimeJS + // : undefined, + // unstable_JsPreload: pageConfig.unstable_JsPreload, + unstable_JsPreload: true, + dangerousAsPath: router.asPath, + ampState: undefined, + props, + assetPrefix: "", + headTags: await headTags(documentCtx), + isFallback, + docProps, + pathname, + ampPath: undefined, + query, + inAmpMode: false, + hybridAmp: undefined, + dynamicImportsIds: [], // Array.from(dynamicImportsIds), + dynamicImports: [], //Array.from(dynamicImports), + gsp: !!getStaticProps ? true : undefined, + gssp: !!getServerSideProps ? true : undefined, + gip: hasPageGetInitialProps ? true : undefined, + appGip: !defaultAppGetInitialProps ? true : undefined, + devOnlyCacheBusterQueryString: "", + scriptLoader, + isPreview: isPreview === true ? true : undefined, + autoExport: isAutoExport === true ? true : undefined, + nextExport: nextExport === true ? true : undefined, + }).replaceAll("/_next//_next", "/_next"); + return new Response(html); +} diff --git a/demos/css-stress-test/nexty/server.development.tsx b/demos/css-stress-test/nexty/server.development.tsx index ab330359e..78017f885 100644 --- a/demos/css-stress-test/nexty/server.development.tsx +++ b/demos/css-stress-test/nexty/server.development.tsx @@ -1,70 +1,67 @@ -import ReactDOMServer from "react-dom/server.browser"; +import { render } from "./renderDocument"; -addEventListener( - "fetch", +let buildId = 0; - // Anything imported in here will automatically reload in development. - // The module registry cache is reset at the end of each page load - async (event: FetchEvent) => { - var appRoute; +var DocumentNamespacePromise; +DocumentNamespacePromise = import(Wundle.routesDir + "_document"); +var DocumentLoaded = false; +var DocumentNamespace; + +addEventListener("fetch", async (event: FetchEvent) => { + if (!DocumentLoaded) { + DocumentLoaded = true; try { - appRoute = await import(Wundle.routesDir + "_app"); + DocumentNamespace = await DocumentNamespacePromise; } catch (exception) { - appRoute = null; + DocumentNamespace = null; } + } - var route = Wundle.match(event); - - // This imports the currently matched route. - const { default: PageComponent } = await import(route.filePath); - - // This returns all .css files that were imported in the line above. - // It's recursive, so any file that imports a CSS file will be included. - const stylesheets = Wundle.getImportedStyles() as string[]; - - // Ordinarily, this is just the formatted filepath URL (rewritten to match the public url of the HTTP server) - // But, when you set `client` in the package.json for the framework, this becomes a path like this: - // "/pages/index.js" -> "pages/index.entry.js" ("entry" is for entry point) - const src = route.scriptSrc; - - // From there, the inside of that script like this: - // ``` - // import * as Framework from 'framework-path'; - // import * as EntryPoint from 'entry-point'; - // - // Framework.default(EntryPoint); - // ``` - // That's how the client-side framework loads + var appRoute; - const response = new Response(` - <!DOCTYPE html> -<html> - <head> - ${stylesheets - .map((style) => `<link rel="stylesheet" href="${style}">`) - .join("\n")} + try { + appRoute = await import(Wundle.routesDir + "_app"); + } catch (exception) { + appRoute = null; + } + const appStylesheets = Wundle.getImportedStyles() as string[]; - <link - rel="stylesheet" - crossorigin="anonymous" - href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;700&family=Space+Mono:wght@400;700" - /> - </head> - <body> + var route = Wundle.match(event); - <div id="#__next">${ReactDOMServer.renderToString(<PageComponent />)}</div> + // This imports the currently matched route. + const PageNamespace = await import(route.filePath); - <script src="${src}" async type="module"></script> - </body> -</html> - `); + // This returns all .css files that were imported in the line above. + // It's recursive, so any file that imports a CSS file will be included. + const pageStylesheets = Wundle.getImportedStyles() as string[]; - event.respondWith(response); - } -); + event.respondWith( + await render({ + route, + PageNamespace, + appStylesheets, + pageStylesheets, + DocumentNamespace, + AppNamespace: appRoute, + buildId, + }) + ); + buildId++; +}); // typescript isolated modules export {}; declare var Wundle: any; + +function getNextData(request: Request, route) { + return { + NEXT_DATA: { + query: route.query, + props: {}, + page: route.path, + buildId: buildId.toString(16), + }, + }; +} diff --git a/demos/css-stress-test/nexty/tsconfig.json b/demos/css-stress-test/nexty/tsconfig.json new file mode 100644 index 000000000..679268d71 --- /dev/null +++ b/demos/css-stress-test/nexty/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["dom", "dom.iterable", "esnext", "WebWorker"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "baseUrl": ".", + "paths": { + "path": ["node_modules/path-browserify"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/src/bundler.zig b/src/bundler.zig index 49b9f496b..377079355 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -101,13 +101,13 @@ pub const ClientEntryPoint = struct { \\ globalThis.onerror.loaded = loaded; \\}} \\ - \\import * as boot from '{s}'; + \\import boot from '{s}'; \\loaded.boot = true; \\if ('setLoaded' in boot) boot.setLoaded(loaded); \\import * as EntryPoint from '{s}{s}'; \\loaded.entry = true; \\ - \\if (!('default' in boot) ) {{ + \\if (!boot) {{ \\ const now = Date.now(); \\ debugger; \\ const elapsed = Date.now() - now; @@ -116,7 +116,7 @@ pub const ClientEntryPoint = struct { \\ }} \\}} \\ - \\boot.default(EntryPoint, loaded); + \\boot(EntryPoint, loaded); , .{ client, @@ -2126,15 +2126,15 @@ pub const Transformer = struct { ) !options.TransformResult { js_ast.Expr.Data.Store.create(allocator); js_ast.Stmt.Data.Store.create(allocator); + const platform = options.Platform.from(opts.platform); - var define = try options.definesFromTransformOptions(allocator, log, opts.define, false); + var define = try options.definesFromTransformOptions(allocator, log, opts.define, false, platform); const cwd = if (opts.absolute_working_dir) |workdir| try std.fs.realpathAlloc(allocator, workdir) else try std.process.getCwdAlloc(allocator); const output_dir_parts = [_]string{ try std.process.getCwdAlloc(allocator), opts.output_dir orelse "out" }; const output_dir = try std.fs.path.join(allocator, &output_dir_parts); var output_files = try std.ArrayList(options.OutputFile).initCapacity(allocator, opts.entry_points.len); - const platform = options.Platform.from(opts.platform); const out_extensions = platform.outExtensions(allocator); var loader_map = try options.loadersFromTransformOptions(allocator, opts.loaders); diff --git a/src/css_scanner.zig b/src/css_scanner.zig index e39515292..298c5f81a 100644 --- a/src/css_scanner.zig +++ b/src/css_scanner.zig @@ -898,16 +898,21 @@ pub fn NewWriter( switch (chunk.content) { .t_url => |url| {}, .t_import => |import| { - try writer.buildCtx.addCSSImport( - try writer.linker.resolveCSS( - writer.source.path, - import.text.utf8, - chunk.range, - import_record.ImportKind.at, - Options.BundleOptions.ImportPathFormat.absolute_path, - true, - ), + const resolved = try writer.linker.resolveCSS( + writer.source.path, + import.text.utf8, + chunk.range, + import_record.ImportKind.at, + Options.BundleOptions.ImportPathFormat.absolute_path, + true, ); + + // TODO: just check is_external instead + if (strings.startsWith(import.text.utf8, "https://") or strings.startsWith(import.text.utf8, "http://")) { + return; + } + + try writer.buildCtx.addCSSImport(resolved); }, .t_verbatim => |verbatim| {}, } diff --git a/src/http.zig b/src/http.zig index 06eaf7a18..ffb7a3d72 100644 --- a/src/http.zig +++ b/src/http.zig @@ -1573,7 +1573,7 @@ pub const RequestContext = struct { break :brk try ctx.bundler.buildFile( &ctx.log, ctx.allocator, - ctx.url.path, + ctx.url.pathWithoutOrigin(ctx.bundler.options.origin), ctx.url.extname, true, ); @@ -1581,7 +1581,7 @@ pub const RequestContext = struct { break :brk try ctx.bundler.buildFile( &ctx.log, ctx.allocator, - ctx.url.path, + ctx.url.pathWithoutOrigin(ctx.bundler.options.origin), ctx.url.extname, false, ); diff --git a/src/http/url_path.zig b/src/http/url_path.zig index a2e55555d..e82d5601a 100644 --- a/src/http/url_path.zig +++ b/src/http/url_path.zig @@ -1,6 +1,8 @@ usingnamespace @import("../global.zig"); -const std = @import("std"); +const PercentEncoding = @import("../query_string_map.zig").PercentEncoding; +const std = @import("std"); +const allocators = @import("../allocators.zig"); const URLPath = @This(); extname: string = "", @@ -8,17 +10,63 @@ path: string = "", pathname: string = "", first_segment: string = "", query_string: string = "", +const toMutable = allocators.constStrToU8; + +// TODO: use a real URL parser +// this treats a URL like /_next/ identically to / +pub fn pathWithoutOrigin(this: *const URLPath, _origin: string) string { + if (_origin.len == 0) return this.path; + const leading_slash_offset: usize = if (_origin[0] == '/') 1 else 0; + const base = this.path; + const origin = _origin[leading_slash_offset..]; + + return if (base.len >= origin.len and strings.eql(base[0..origin.len], origin)) base[origin.len..] else base; +} + +// optimization: very few long strings will be URL-encoded +// we're allocating virtual memory here, so if we never use it, it won't be allocated +// and even when they're, they're probably rarely going to be > 1024 chars long +// so we can have a big and little one and almost always use the little one +threadlocal var temp_path_buf: [1024]u8 = undefined; +threadlocal var big_temp_path_buf: [16384]u8 = undefined; + +pub fn parse(possibly_encoded_pathname_: string) URLPath { + var decoded_pathname = possibly_encoded_pathname_; + + if (strings.indexOfChar(decoded_pathname, '%') != null) { + var possibly_encoded_pathname = switch (decoded_pathname.len) { + 0...1024 => &temp_path_buf, + else => &big_temp_path_buf, + }; + possibly_encoded_pathname = possibly_encoded_pathname[0..std.math.min( + possibly_encoded_pathname_.len, + possibly_encoded_pathname.len, + )]; + + std.mem.copy(u8, possibly_encoded_pathname, possibly_encoded_pathname_[0..possibly_encoded_pathname.len]); + var clone = possibly_encoded_pathname[0..possibly_encoded_pathname.len]; + + var fbs = std.io.fixedBufferStream( + // This is safe because: + // - this comes from a non-const buffer + // - percent *decoding* will always be <= length of the original string (no buffer overflow) + toMutable( + possibly_encoded_pathname, + ), + ); + var writer = fbs.writer(); + decoded_pathname = possibly_encoded_pathname[0 .. PercentEncoding.decode(@TypeOf(writer), writer, clone) catch unreachable]; + } -// This does one pass over the URL path instead of like 4 -pub fn parse(raw_path: string) URLPath { var question_mark_i: i16 = -1; var period_i: i16 = -1; var first_segment_end: i16 = std.math.maxInt(i16); var last_slash: i16 = -1; - var i: i16 = @intCast(i16, raw_path.len) - 1; + var i: i16 = @intCast(i16, decoded_pathname.len) - 1; + while (i >= 0) : (i -= 1) { - const c = raw_path[@intCast(usize, i)]; + const c = decoded_pathname[@intCast(usize, i)]; switch (c) { '?' => { @@ -52,24 +100,24 @@ pub fn parse(raw_path: string) URLPath { const extname = brk: { if (question_mark_i > -1 and period_i > -1) { period_i += 1; - break :brk raw_path[@intCast(usize, period_i)..@intCast(usize, question_mark_i)]; + break :brk decoded_pathname[@intCast(usize, period_i)..@intCast(usize, question_mark_i)]; } else if (period_i > -1) { period_i += 1; - break :brk raw_path[@intCast(usize, period_i)..]; + break :brk decoded_pathname[@intCast(usize, period_i)..]; } else { break :brk &([_]u8{}); } }; - const path = if (question_mark_i < 0) raw_path[1..] else raw_path[1..@intCast(usize, question_mark_i)]; + const path = if (question_mark_i < 0) decoded_pathname[1..] else decoded_pathname[1..@intCast(usize, question_mark_i)]; - const first_segment = raw_path[1..std.math.min(@intCast(usize, first_segment_end), raw_path.len)]; + const first_segment = decoded_pathname[1..std.math.min(@intCast(usize, first_segment_end), decoded_pathname.len)]; return URLPath{ .extname = extname, - .pathname = raw_path, + .pathname = decoded_pathname, .first_segment = first_segment, - .path = if (raw_path.len == 1) "." else path, - .query_string = if (question_mark_i > -1) raw_path[@intCast(usize, question_mark_i)..@intCast(usize, raw_path.len)] else "", + .path = if (decoded_pathname.len == 1) "." else path, + .query_string = if (question_mark_i > -1) decoded_pathname[@intCast(usize, question_mark_i)..@intCast(usize, decoded_pathname.len)] else "", }; } diff --git a/src/javascript/jsc/api/router.zig b/src/javascript/jsc/api/router.zig index e2283e9c5..84562fee3 100644 --- a/src/javascript/jsc/api/router.zig +++ b/src/javascript/jsc/api/router.zig @@ -16,6 +16,7 @@ const Fs = @import("../../../fs.zig"); route: *const FilesystemRouter.Match, query_string_map: ?QueryStringMap = null, +param_map: ?QueryStringMap = null, script_src: ?string = null, script_src_buf: [1024]u8 = undefined, @@ -223,6 +224,23 @@ pub const Instance = NewClass( .ts = d.ts{ .@"return" = "Record<string, string | string[]>", .tsdoc = + \\Route parameters & parsed query string values as a key-value object + \\ + \\@example + \\```js + \\console.assert(router.query.id === "123"); + \\console.assert(router.pathname === "/blog/posts/123"); + \\console.assert(router.route === "blog/posts/[id]"); + \\``` + , + }, + }, + .params = .{ + .@"get" = getParams, + .ro = true, + .ts = d.ts{ + .@"return" = "Record<string, string | string[]>", + .tsdoc = \\Route parameters as a key-value object \\ \\@example @@ -372,6 +390,34 @@ pub fn getScriptSrc( return js.JSValueMakeString(ctx, ZigString.init(src).toJSStringRef()); } +pub fn getParams( + this: *Router, + ctx: js.JSContextRef, + thisObject: js.JSObjectRef, + prop: js.JSStringRef, + exception: js.ExceptionRef, +) js.JSValueRef { + if (this.param_map == null) { + if (this.route.params.len > 0) { + if (QueryStringMap.initWithScanner(getAllocator(ctx), CombinedScanner.init( + "", + this.route.pathnameWithoutLeadingSlash(), + this.route.name, + this.route.params, + ))) |map| { + this.param_map = map; + } else |err| {} + } + } + + // If it's still null, there are no params + if (this.param_map) |*map| { + return createQueryObject(ctx, map, exception); + } else { + return JSValue.createEmptyObject(VirtualMachine.vm.global, 0).asRef(); + } +} + pub fn getQuery( this: *Router, ctx: js.JSContextRef, diff --git a/src/javascript/jsc/base.zig b/src/javascript/jsc/base.zig index 4902c731a..e17d4c7ab 100644 --- a/src/javascript/jsc/base.zig +++ b/src/javascript/jsc/base.zig @@ -205,6 +205,8 @@ pub const Properties = struct { pub const default: string = "default"; pub const include: string = "include"; + pub const env: string = "env"; + pub const GET = "GET"; pub const PUT = "PUT"; pub const POST = "POST"; @@ -278,6 +280,8 @@ pub const Properties = struct { pub var navigate: js.JSStringRef = undefined; pub var follow: js.JSStringRef = undefined; + + pub const env: js.JSStringRef = undefined; }; pub fn init() void { diff --git a/src/javascript/jsc/bindings/ZigGlobalObject.cpp b/src/javascript/jsc/bindings/ZigGlobalObject.cpp index fbe3c22ae..45a68954e 100644 --- a/src/javascript/jsc/bindings/ZigGlobalObject.cpp +++ b/src/javascript/jsc/bindings/ZigGlobalObject.cpp @@ -121,9 +121,40 @@ void GlobalObject::setConsole(void *console) { this->setConsoleClient(makeWeakPtr(m_console)); } +// This is not a publicly exposed API currently. +// This is used by the bundler to make Response, Request, FetchEvent, +// and any other objects available globally. void GlobalObject::installAPIGlobals(JSClassRef *globals, int count) { WTF::Vector<GlobalPropertyInfo> extraStaticGlobals; - extraStaticGlobals.reserveCapacity((size_t)count); + extraStaticGlobals.reserveCapacity((size_t)count + 1); + + // This is not nearly a complete implementation. It's just enough to make some npm packages that + // were compiled with Webpack to run without crashing in this environment. + JSC::JSObject *process = JSC::constructEmptyObject(this, this->objectPrototype(), 4); + + // The transpiler inlines all defined process.env vars & dead code eliminates as relevant + // so this is just to return undefined for any missing ones and not crash if something tries to + // modify it or it wasn't statically analyzable + JSC::JSObject *processDotEnv = JSC::constructEmptyObject(this, this->objectPrototype(), 0); + + process->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "env"), processDotEnv); + + // this should be transpiled out, but just incase + process->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "browser"), + JSC::JSValue(false)); + + // this gives some way of identifying at runtime whether the SSR is happening in node or not. + // this should probably be renamed to what the name of the bundler is, instead of "notNodeJS" + // but it must be something that won't evaluate to truthy in Node.js + process->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "notNodeJS"), + JSC::JSValue(true)); +#if defined(__APPLE__) + process->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "platform"), + JSC::jsString(this->vm(), WTF::String("darwin"))); +#else + process->putDirect(this->vm(), JSC::Identifier::fromString(this->vm(), "platform"), + JSC::jsString(this->vm(), WTF::String("linux"))); +#endif for (int i = 0; i < count; i++) { auto jsClass = globals[i]; @@ -137,7 +168,12 @@ void GlobalObject::installAPIGlobals(JSClassRef *globals, int count) { GlobalPropertyInfo{JSC::Identifier::fromString(vm(), jsClass->className()), JSC::JSValue(object), JSC::PropertyAttribute::DontDelete | 0}); } - this->addStaticGlobals(extraStaticGlobals.data(), count); + + extraStaticGlobals.uncheckedAppend( + GlobalPropertyInfo{JSC::Identifier::fromString(vm(), "process"), JSC::JSValue(process), + JSC::PropertyAttribute::DontDelete | 0}); + + this->addStaticGlobals(extraStaticGlobals.data(), extraStaticGlobals.size()); extraStaticGlobals.releaseBuffer(); } diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index a2d9220ad..0b96ca3ed 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -117,7 +117,7 @@ pub const Wundle = struct { prop: js.JSStringRef, exception: js.ExceptionRef, ) js.JSValueRef { - if (!VirtualMachine.vm.bundler.options.routes.routes_enabled or VirtualMachine.vm.bundler.options.routes.dir.len > 0) { + if (!VirtualMachine.vm.bundler.options.routes.routes_enabled or VirtualMachine.vm.bundler.options.routes.dir.len == 0) { return js.JSValueMakeUndefined(ctx); } @@ -238,6 +238,7 @@ pub const VirtualMachine = struct { log: *logger.Log, event_listeners: EventListenerMixin.Map, main: string = "", + process: js.JSObjectRef = null, pub var vm_loaded = false; pub var vm: *VirtualMachine = undefined; diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig index 8b9649aa2..f6b59141c 100644 --- a/src/js_parser/js_parser.zig +++ b/src/js_parser/js_parser.zig @@ -70,7 +70,7 @@ pub const ImportScanner = struct { var stmt: Stmt = _stmt; switch (stmt.data) { .s_import => |st| { - var record: ImportRecord = p.import_records.items[st.import_record_index]; + var record: *ImportRecord = &p.import_records.items[st.import_record_index]; // The official TypeScript compiler always removes unused imported // symbols. However, we deliberately deviate from the official @@ -285,7 +285,7 @@ pub const ImportScanner = struct { // it's really stupid to import all 1,000 components from that design system // when you just want <Button /> const namespace_ref = st.namespace_ref; - const convert_star_to_clause = p.symbols.items[namespace_ref.inner_index].use_count_estimate == 0; + const convert_star_to_clause = p.symbols.items[namespace_ref.inner_index].use_count_estimate == 0 and st.default_name == null; if (convert_star_to_clause and !keep_unused_imports) { st.star_name_loc = null; @@ -1879,7 +1879,7 @@ pub const Parser = struct { var decls = try p.allocator.alloc(G.Decl, decls_count); var jsx_part_stmts = try p.allocator.alloc(Stmt, stmts_count); // Use the same array for storing the require call target of potentially both JSX runtimes - var require_call_args_base = p.allocator.alloc(Expr, imports_count) catch unreachable; + var require_call_args_base = p.allocator.alloc(Expr, if (p.options.can_import_from_bundle) 0 else imports_count) catch unreachable; var import_records = try p.allocator.alloc(u32, imports_count); var decl_i: usize = 0; @@ -1889,16 +1889,21 @@ pub const Parser = struct { var stmt_i: usize = 0; if (jsx_symbol.use_count_estimate > 0) { - require_call_args_base[require_call_args_i] = p.e(E.Identifier{ .ref = automatic_namespace_ref }, loc); - require_call_args_i += 1; - - var require_call_args = require_call_args_base[0..require_call_args_i]; - declared_symbols[declared_symbols_i] = .{ .ref = p.jsx_runtime_ref, .is_top_level = true }; declared_symbols_i += 1; declared_symbols[declared_symbols_i] = .{ .ref = automatic_namespace_ref, .is_top_level = true }; declared_symbols_i += 1; - var require_call = p.callRequireOrBundledRequire(require_call_args); + + const automatic_identifier = p.e(E.Identifier{ .ref = automatic_namespace_ref }, loc); + const dot_call_target = brk: { + if (p.options.can_import_from_bundle) { + break :brk automatic_identifier; + } else { + require_call_args_base[require_call_args_i] = automatic_identifier; + require_call_args_i += 1; + break :brk p.callUnbundledRequire(require_call_args_base[0..require_call_args_i]); + } + }; decls[decl_i] = G.Decl{ .binding = p.b( @@ -1909,7 +1914,7 @@ pub const Parser = struct { ), .value = p.e( E.Dot{ - .target = require_call, + .target = dot_call_target, .name = p.options.jsx.jsx, .name_loc = loc, .can_be_removed_if_unused = true, @@ -1938,6 +1943,7 @@ pub const Parser = struct { // When everything is CommonJS // We import JSX like this: // var {jsxDev} = require("react/jsx-dev") + jsx_part_stmts[stmt_i] = p.s(S.Import{ .namespace_ref = automatic_namespace_ref, .star_name_loc = loc, @@ -1962,9 +1968,20 @@ pub const Parser = struct { } if (jsx_classic_symbol.use_count_estimate > 0) { - require_call_args_base[require_call_args_i] = p.e(E.Identifier{ .ref = classic_namespace_ref }, loc); - var require_call_args = require_call_args_base[require_call_args_i..]; - var require_call = p.callRequireOrBundledRequire(require_call_args); + const classic_identifier = p.e(E.Identifier{ .ref = classic_namespace_ref }, loc); + + const dot_call_target = brk: { + // var react = $aopaSD123(); + + if (p.options.can_import_from_bundle) { + break :brk classic_identifier; + } else { + require_call_args_base[require_call_args_i] = classic_identifier; + require_call_args_i += 1; + break :brk p.callUnbundledRequire(require_call_args_base[0..require_call_args_i]); + } + }; + if (jsx_factory_symbol.use_count_estimate > 0) { declared_symbols[declared_symbols_i] = .{ .ref = p.jsx_factory_ref, .is_top_level = true }; declared_symbols_i += 1; @@ -1977,7 +1994,7 @@ pub const Parser = struct { ), .value = p.e( E.Dot{ - .target = require_call, + .target = dot_call_target, .name = p.options.jsx.factory[p.options.jsx.factory.len - 1], .name_loc = loc, .can_be_removed_if_unused = true, @@ -2000,7 +2017,7 @@ pub const Parser = struct { ), .value = p.e( E.Dot{ - .target = require_call, + .target = dot_call_target, .name = p.options.jsx.fragment[p.options.jsx.fragment.len - 1], .name_loc = loc, .can_be_removed_if_unused = true, @@ -2676,7 +2693,7 @@ pub fn NewParser( cjs_import_name, base_identifier_name, ); - std.mem.copy(u8, cjs_import_name[base_identifier_name.len..], suffix); + std.mem.copy(u8, cjs_import_name[base_identifier_name.len - 1 ..], suffix); const namespace_ref = p.declareSymbol(.hoisted, arg.loc, cjs_import_name) catch unreachable; @@ -2895,12 +2912,8 @@ pub fn NewParser( // If we're auto-importing JSX and it's bundled, we use the bundled version // This means we need to transform from require(react) to react() // unless we're building inside of speedy, then it's just normal commonjs - pub fn callRequireOrBundledRequire(p: *P, require_args: []Expr) Expr { - if (p.options.can_import_from_bundle) { - return p.e(E.Call{ .target = require_args[0] }, require_args[0].loc); - } else { - return p.callRuntime(require_args[0].loc, "__require", require_args); - } + pub fn callUnbundledRequire(p: *P, require_args: []Expr) Expr { + return p.callRuntime(require_args[0].loc, "__require", require_args); } pub fn recordExport(p: *P, loc: logger.Loc, alias: string, ref: Ref) !void { diff --git a/src/js_printer.zig b/src/js_printer.zig index 1f43479a6..8a5c81c8c 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -2835,9 +2835,15 @@ pub fn NewPrinter( } } else if (record.is_bundled) { p.print("import {"); + p.printLoadFromBundleWithoutCall(s.import_record_index); - p.print(" as "); - p.printSymbol(s.namespace_ref); + + if (!record.contains_import_star) { + p.print(" as "); + + p.printSymbol(s.namespace_ref); + } + p.print("} from "); p.printQuotedUTF8(record.path.text, false); p.printSemicolonAfterStatement(); @@ -2846,6 +2852,7 @@ pub fn NewPrinter( p.printIndent(); p.printSpaceBeforeIdentifier(); p.print("var {"); + for (s.items) |item, i| { p.print(item.alias); const name = p.renamer.nameForSymbol(item.name.ref.?); @@ -2859,22 +2866,43 @@ pub fn NewPrinter( } } p.print("} = "); - p.printSymbol(s.namespace_ref); + + if (!record.contains_import_star) { + p.printSymbol(s.namespace_ref); + } else { + p.printLoadFromBundleWithoutCall(s.import_record_index); + } p.print("()"); p.printSemicolonAfterStatement(); } else if (s.default_name) |default_name| { p.printIndent(); p.printSpaceBeforeIdentifier(); + p.print("var {default: "); + p.printSymbol(default_name.ref.?); p.print("} = "); + + if (!record.contains_import_star) { + p.printSymbol(s.namespace_ref); + } else { + p.printLoadFromBundleWithoutCall(s.import_record_index); + } + + p.print("()"); + p.printSemicolonAfterStatement(); + } else if (record.contains_import_star) { + p.print("var "); p.printSymbol(s.namespace_ref); + p.print(" = "); + p.printLoadFromBundleWithoutCall(s.import_record_index); p.print("()"); p.printSemicolonAfterStatement(); } return; } + p.print("import"); p.printSpace(); diff --git a/src/options.zig b/src/options.zig index f93937b24..a4302a703 100644 --- a/src/options.zig +++ b/src/options.zig @@ -250,6 +250,17 @@ pub const Platform = enum { speedy, node, + const browser_define_value_true = "true"; + const browser_define_value_false = "false"; + + pub fn processBrowserDefineValue(this: Platform) ?string { + return switch (this) { + .browser => browser_define_value_true, + .speedy, .node => browser_define_value_false, + else => null, + }; + } + pub fn isWebLike(platform: Platform) bool { return switch (platform) { .neutral, .browser => true, @@ -545,9 +556,20 @@ pub const DefaultUserDefines = struct { pub const Key = "process.env.NODE_ENV"; pub const Value = "\"development\""; }; + + pub const PlatformDefine = struct { + pub const Key = "process.browser"; + pub const Value = []string{ "false", "true" }; + }; }; -pub fn definesFromTransformOptions(allocator: *std.mem.Allocator, log: *logger.Log, _input_define: ?Api.StringMap, hmr: bool) !*defines.Define { +pub fn definesFromTransformOptions( + allocator: *std.mem.Allocator, + log: *logger.Log, + _input_define: ?Api.StringMap, + hmr: bool, + platform: Platform, +) !*defines.Define { var input_user_define = _input_define orelse std.mem.zeroes(Api.StringMap); var user_defines = try stringHashMapFromArrays( @@ -564,6 +586,12 @@ pub fn definesFromTransformOptions(allocator: *std.mem.Allocator, log: *logger.L try user_defines.put(DefaultUserDefines.HotModuleReloading.Key, DefaultUserDefines.HotModuleReloading.Value); } + // Automatically set `process.browser` to `true` for browsers and false for node+js + // This enables some extra dead code elimination + if (platform.processBrowserDefineValue()) |value| { + _ = try user_defines.getOrPutValue(DefaultUserDefines.PlatformDefine.Key, value); + } + var resolved_defines = try defines.DefineData.from_input(user_defines, log, allocator); return try defines.Define.init( allocator, @@ -672,7 +700,7 @@ pub const BundleOptions = struct { var opts: BundleOptions = BundleOptions{ .log = log, .resolve_mode = transform.resolve orelse .dev, - .define = try definesFromTransformOptions(allocator, log, transform.define, transform.serve orelse false), + .define = undefined, .loaders = try loadersFromTransformOptions(allocator, transform.loaders), .output_dir = try fs.absAlloc(allocator, &output_dir_parts), .platform = Platform.from(transform.platform), @@ -712,6 +740,8 @@ pub const BundleOptions = struct { else => {}, } + opts.define = try definesFromTransformOptions(allocator, log, transform.define, transform.serve orelse false, opts.platform); + if (!(transform.generate_node_module_bundle orelse false)) { if (node_modules_bundle_existing) |node_mods| { opts.node_modules_bundle = node_mods; diff --git a/src/runtime.js b/src/runtime.js index f065a1c4e..affd3bc4d 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -63,7 +63,7 @@ export var __commonJS = (cb, name) => { get() { return mod.exports; }, - enumerable: false, + enumerable: true, }); // If it's a namespace export without .default, pretend .default is the same as mod.exports } else if ( @@ -74,7 +74,7 @@ export var __commonJS = (cb, name) => { get() { return mod.exports; }, - enumerable: false, + enumerable: true, }); } diff --git a/src/runtime.version b/src/runtime.version index 1a0bb1fa9..4cda1e7bd 100644 --- a/src/runtime.version +++ b/src/runtime.version @@ -1 +1 @@ -ca3ca9f44c907d4a
\ No newline at end of file +7d529e0a92952d7a
\ No newline at end of file |