diff options
author | 2021-09-05 21:44:43 -0700 | |
---|---|---|
committer | 2021-09-05 21:44:43 -0700 | |
commit | 2fc89a69e35525acbd3613779c31ad6b37ea885f (patch) | |
tree | 4d06b02306435cc823fb532ccf84cd93b01a4663 /packages/bun-framework-next | |
parent | cb70d08573e72a0c470f1aaf2aa3eed755912014 (diff) | |
download | bun-2fc89a69e35525acbd3613779c31ad6b37ea885f.tar.gz bun-2fc89a69e35525acbd3613779c31ad6b37ea885f.tar.zst bun-2fc89a69e35525acbd3613779c31ad6b37ea885f.zip |
Move
Former-commit-id: b795168d774fa0b3dcde7955e30baa0eefbc88ab
Diffstat (limited to 'packages/bun-framework-next')
-rw-r--r-- | packages/bun-framework-next/.npmignore | 2 | ||||
-rw-r--r-- | packages/bun-framework-next/client.development.tsx | 415 | ||||
-rw-r--r-- | packages/bun-framework-next/fallback.development.tsx | 43 | ||||
-rw-r--r-- | packages/bun-framework-next/index.js | 0 | ||||
-rw-r--r-- | packages/bun-framework-next/next-server.tsx | 0 | ||||
-rw-r--r-- | packages/bun-framework-next/package.json | 81 | ||||
-rw-r--r-- | packages/bun-framework-next/page-loader.ts | 142 | ||||
-rw-r--r-- | packages/bun-framework-next/render.tsx | 0 | ||||
-rw-r--r-- | packages/bun-framework-next/renderDocument.tsx | 722 | ||||
-rw-r--r-- | packages/bun-framework-next/route-loader.ts | 0 | ||||
-rw-r--r-- | packages/bun-framework-next/server.development.tsx | 78 | ||||
-rw-r--r-- | packages/bun-framework-next/server.production.tsx | 0 | ||||
-rw-r--r-- | packages/bun-framework-next/tsconfig.json | 21 |
13 files changed, 1504 insertions, 0 deletions
diff --git a/packages/bun-framework-next/.npmignore b/packages/bun-framework-next/.npmignore new file mode 100644 index 000000000..dc0954477 --- /dev/null +++ b/packages/bun-framework-next/.npmignore @@ -0,0 +1,2 @@ +*.bun +node_modules
\ No newline at end of file diff --git a/packages/bun-framework-next/client.development.tsx b/packages/bun-framework-next/client.development.tsx new file mode 100644 index 000000000..b93b1fcce --- /dev/null +++ b/packages/bun-framework-next/client.development.tsx @@ -0,0 +1,415 @@ +globalThis.global = globalThis; +globalThis.Bun_disableCSSImports = true; + +import * as React from "react"; +var onlyChildPolyfill = React.Children.only; +React.Children.only = function (children) { + if (children && typeof children === "object" && children.length == 1) { + return onlyChildPolyfill(children[0]); + } + + return onlyChildPolyfill(children); +}; + +import * as ReactDOM from "react-dom"; +import App from "next/app"; +import mitt, { MittEmitter } from "next/dist/shared/lib/mitt"; +import { RouterContext } from "next/dist/shared/lib/router-context"; +import Router, { + AppComponent, + AppProps, + delBasePath, + hasBasePath, + PrivateRouteInfo, +} from "next/dist/shared/lib/router/router"; + +import * as NextRouteLoader from "next/dist/client/route-loader"; +import { isDynamicRoute } from "next/dist/shared/lib/router/utils/is-dynamic"; +import { + urlQueryToSearchParams, + assign, +} from "next/dist/shared/lib/router/utils/querystring"; +import { setConfig } from "next/dist/shared/lib/runtime-config"; +import { + getURL, + loadGetInitialProps, + NEXT_DATA, + ST, +} from "next/dist/shared/lib/utils"; +// import { Portal } from "next/dist/client/portal"; +import initHeadManager from "next/dist/client/head-manager"; +import { HeadManagerContext } from "next/dist/shared/lib/head-manager-context"; +import PageLoader from "./page-loader"; +import measureWebVitals from "next/dist/client/performance-relayer"; +import { RouteAnnouncer } from "next/dist/client/route-announcer"; +import { + createRouter, + makePublicRouterInstance, +} from "next/dist/client/router"; +export const emitter: MittEmitter<string> = mitt(); + +declare global { + interface Window { + /* test fns */ + __NEXT_HYDRATED?: boolean; + __NEXT_HYDRATED_CB?: () => void; + + /* prod */ + __NEXT_PRELOADREADY?: (ids?: (string | number)[]) => void; + __NEXT_DATA__: NEXT_DATA; + __NEXT_P: any[]; + } +} + +function nextDataFromBunData() { + const { + router: { routes, route, params: paramsList }, + problems, + } = globalThis.__BUN_DATA__; + + const paramsMap = new Map(); + for (let i = 0; i < paramsList.keys.length; i++) { + paramsMap.set( + decodeURIComponent(paramsList.keys[i]), + decodeURIComponent(paramsList.values[i]) + ); + } + + const params = {}; + var url = new URL(location.href); + Object.assign(params, Object.fromEntries(url.searchParams.entries())); + Object.assign(params, Object.fromEntries(paramsMap.entries())); + + const pages = routes.reduce((acc, route) => { + var name = route.substring(route.indexOf("_next") + "_next/".length); + + while (name.startsWith("/")) { + name = name.substring(1); + } + + if (name.startsWith("pages")) { + name = name.substring("pages".length); + } + + while (name.startsWith("/")) { + name = name.substring(1); + } + + if (name.endsWith(".jsx")) { + name = name.substring(0, name.length - ".jsx".length); + } + + if (name.endsWith(".tsx")) { + name = name.substring(0, name.length - ".tsx".length); + } + + if (name.endsWith(".ts")) { + name = name.substring(0, name.length - ".ts".length); + } + + if (name.endsWith(".js")) { + name = name.substring(0, name.length - ".js".length); + } + + acc["/" + name] = [route]; + return acc; + }, {}); + + return { + page: routes[route], + buildId: "1234", + assetPrefix: "", + isPreview: false, + locale: null, + locales: [], + isFallback: false, + err: null, + props: {}, + query: params, + pages, + }; +} + +type RenderRouteInfo = PrivateRouteInfo & { + App: AppComponent; + scroll?: { x: number; y: number } | null; +}; +type RenderErrorProps = Omit<RenderRouteInfo, "Component" | "styleSheets">; + +const nextDataTag = document.getElementById("__NEXT_DATA__"); + +const data: typeof window["__NEXT_DATA__"] = nextDataTag + ? JSON.parse(document.getElementById("__NEXT_DATA__")!.textContent!) + : nextDataFromBunData(); +window.__NEXT_DATA__ = data; + +const { + props: hydrateProps, + err: hydrateErr, + page, + query, + buildId, + assetPrefix, + runtimeConfig, + dynamicIds, + isFallback, + locale, + locales, + domainLocales, + isPreview, +} = data; + +const prefix: string = assetPrefix || ""; + +setConfig({ + serverRuntimeConfig: {}, + publicRuntimeConfig: runtimeConfig || {}, +}); + +let asPath: string = getURL(); + +// make sure not to attempt stripping basePath for 404s +if (hasBasePath(asPath)) { + asPath = delBasePath(asPath); +} + +export const pageLoader: PageLoader = new PageLoader( + buildId, + prefix, + data.pages +); + +const headManager: { + mountedInstances: Set<unknown>; + updateHead: (head: JSX.Element[]) => void; +} = initHeadManager(); +const appElement: HTMLElement | null = document.getElementById("__next"); + +let lastRenderReject: (() => void) | null; +let webpackHMR: any; +export let router: Router; +let CachedApp: AppComponent, onPerfEntry: (metric: any) => void; + +export default function boot(EntryPointNamespace, loader) { + _boot(EntryPointNamespace).then(() => {}, false); +} + +class Container extends React.Component<{ + fn: (err: Error, info?: any) => void; +}> { + componentDidCatch(componentErr: Error, info: any) { + this.props.fn(componentErr, info); + } + + componentDidMount() { + this.scrollToHash(); + + // We need to replace the router state if: + // - the page was (auto) exported and has a query string or search (hash) + // - it was auto exported and is a dynamic route (to provide params) + // - if it is a client-side skeleton (fallback render) + if ( + router.isSsr && + // We don't update for 404 requests as this can modify + // the asPath unexpectedly e.g. adding basePath when + // it wasn't originally present + page !== "/404" && + page !== "/_error" && + (isFallback || + (data.nextExport && + (isDynamicRoute(router.pathname) || + location.search || + process.env.__NEXT_HAS_REWRITES)) || + (hydrateProps && + hydrateProps.__N_SSG && + (location.search || process.env.__NEXT_HAS_REWRITES))) + ) { + // update query on mount for exported pages + router.replace( + router.pathname + + "?" + + String( + assign( + urlQueryToSearchParams(router.query), + new URLSearchParams(location.search) + ) + ), + asPath, + { + // @ts-ignore + // WARNING: `_h` is an internal option for handing Next.js + // client-side hydration. Your app should _never_ use this property. + // It may change at any time without notice. + _h: 1, + // Fallback pages must trigger the data fetch, so the transition is + // not shallow. + // Other pages (strictly updating query) happens shallowly, as data + // requirements would already be present. + shallow: !isFallback, + } + ); + } + } + + componentDidUpdate() { + this.scrollToHash(); + } + + scrollToHash() { + let { hash } = location; + hash = hash && hash.substring(1); + if (!hash) return; + + const el: HTMLElement | null = document.getElementById(hash); + if (!el) return; + + // If we call scrollIntoView() in here without a setTimeout + // it won't scroll properly. + setTimeout(() => el.scrollIntoView(), 0); + } + + render() { + return this.props.children; + } +} + +let CachedComponent: React.ComponentType; + +const wrapApp = + (App: AppComponent) => + (wrappedAppProps: Record<string, any>): JSX.Element => { + const appProps: AppProps = { + ...wrappedAppProps, + Component: CachedComponent, + err: hydrateErr, + router, + }; + return ( + <AppContainer> + <App {...appProps} /> + </AppContainer> + ); + }; + +function AppContainer({ + children, +}: React.PropsWithChildren<{}>): React.ReactElement { + return ( + <Container fn={(error) => <div>{JSON.stringify(error)}</div>}> + <RouterContext.Provider value={makePublicRouterInstance(router)}> + <HeadManagerContext.Provider value={headManager}> + {children} + </HeadManagerContext.Provider> + </RouterContext.Provider> + </Container> + ); +} + +export async function _boot(EntryPointNamespace, isError) { + NextRouteLoader.default.getClientBuildManifest = () => Promise.resolve({}); + + const PageComponent = EntryPointNamespace.default; + + const appScripts = globalThis.__NEXT_DATA__.pages["/_app"]; + if (appScripts && appScripts.length > 0) { + let appSrc; + for (let asset of appScripts) { + if (!asset.endsWith(".css")) { + appSrc = asset; + break; + } + } + + if (appSrc) { + const AppModule = await import(appSrc); + console.assert( + AppModule.default, + appSrc + " must have a default export'd React component" + ); + + CachedApp = AppModule.default; + } else { + CachedApp = App; + } + } + + router = createRouter(page, query, asPath, { + initialProps: hydrateProps, + pageLoader, + App: CachedApp, + Component: CachedComponent, + wrapApp, + err: null, + isFallback: Boolean(isFallback), + subscription: async (info, App, scroll) => { + return render( + Object.assign< + {}, + Omit<RenderRouteInfo, "App" | "scroll">, + Pick<RenderRouteInfo, "App" | "scroll"> + >({}, info, { + App, + scroll, + }) + ); + }, + locale, + locales, + defaultLocale: "", + domainLocales, + isPreview, + }); + + globalThis.next.router = router; + + if (isError) { + ReactDOM.render( + <TopLevelRender + App={CachedApp} + Component={PageComponent} + props={{ pageProps: hydrateProps }} + />, + document.querySelector("#__next") + ); + } else { + ReactDOM.hydrate( + <TopLevelRender + App={CachedApp} + Component={PageComponent} + props={{ pageProps: hydrateProps }} + />, + document.querySelector("#__next") + ); + } +} + +function TopLevelRender({ App, Component, props, scroll }) { + return ( + <AppContainer scroll={scroll}> + <App Component={Component} {...props}></App> + </AppContainer> + ); +} + +export function render(props) { + ReactDOM.render( + <TopLevelRender {...props} />, + document.querySelector("#__next") + ); +} + +export function renderError(e) { + ReactDOM.render( + <AppContainer> + <App Component={<div>UH OH!!!!</div>} pageProps={data.props}></App> + </AppContainer>, + document.querySelector("#__next") + ); +} + +globalThis.next = { + version: "11.1.0", + emitter, + render, + renderError, +}; diff --git a/packages/bun-framework-next/fallback.development.tsx b/packages/bun-framework-next/fallback.development.tsx new file mode 100644 index 000000000..34e6cb349 --- /dev/null +++ b/packages/bun-framework-next/fallback.development.tsx @@ -0,0 +1,43 @@ +import { insertStyleSheet } from "./page-loader"; + +const globalCSSQueue = []; +function insertGlobalStyleSheet({ detail }) { + globalCSSQueue.push(insertStyleSheet(detail)); +} + +document.addEventListener("onimportcss", insertGlobalStyleSheet, { + passive: true, +}); + +import { renderError, _boot, pageLoader } from "./client.development"; + +export default function render({ router, reason, problems }) { + const route = router.routes[router.route]; + if (!document.getElementById("__next")) { + const next = document.createElement("div"); + next.id = "__next"; + document.body.prepend(next); + document.head.insertAdjacentHTML( + "beforeend", + `<meta name="next-head-count" content="2">` + ); + } + + document.removeEventListener("onimportcss", insertGlobalStyleSheet); + document.addEventListener("onimportcss", pageLoader.onImportCSS, { + passive: true, + }); + import(route) + .then((Namespace) => { + return _boot(Namespace, true); + }) + .then(() => { + const cssQueue = pageLoader.cssQueue; + pageLoader.cssQueue = []; + return Promise.all([...cssQueue, ...globalCSSQueue]); + }) + .finally(() => { + document.body.style.visibility = "visible"; + document.removeEventListener("onimportcss", pageLoader.onImportCSS); + }); +} diff --git a/packages/bun-framework-next/index.js b/packages/bun-framework-next/index.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/packages/bun-framework-next/index.js diff --git a/packages/bun-framework-next/next-server.tsx b/packages/bun-framework-next/next-server.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/packages/bun-framework-next/next-server.tsx diff --git a/packages/bun-framework-next/package.json b/packages/bun-framework-next/package.json new file mode 100644 index 000000000..af6286343 --- /dev/null +++ b/packages/bun-framework-next/package.json @@ -0,0 +1,81 @@ +{ + "name": "bun-framework-next", + "version": "0.0.0-18", + "description": "", + "framework": { + "displayName": "Next.js", + "static": "public", + "assetPrefix": "_next/", + "router": { + "dir": [ + "pages", + "src/pages" + ], + "extensions": [ + ".js", + ".ts", + ".tsx", + ".jsx" + ] + }, + "css": "onimportcss", + "development": { + "client": "client.development.tsx", + "fallback": "fallback.development.tsx", + "server": "server.development.tsx", + "css": "onimportcss", + "define": { + "client": { + ".env": "NEXT_PUBLIC_", + "defaults": { + "process.env.__NEXT_TRAILING_SLASH": "false", + "process.env.NODE_ENV": "\"development\"", + "process.env.__NEXT_ROUTER_BASEPATH": "''", + "process.env.__NEXT_SCROLL_RESTORATION": "false", + "process.env.__NEXT_I18N_SUPPORT": "false", + "process.env.__NEXT_HAS_REWRITES": "false", + "process.env.__NEXT_ANALYTICS_ID": "null", + "process.env.__NEXT_OPTIMIZE_CSS": "false", + "process.env.__NEXT_CROSS_ORIGIN": "''", + "process.env.__NEXT_STRICT_MODE": "false", + "process.env.__NEXT_IMAGE_OPTS": "null" + } + }, + "server": { + ".env": "NEXT_", + "defaults": { + "process.env.__NEXT_TRAILING_SLASH": "false", + "process.env.__NEXT_OPTIMIZE_FONTS": "false", + "process.env.NODE_ENV": "\"development\"", + "process.env.__NEXT_OPTIMIZE_IMAGES": "false", + "process.env.__NEXT_OPTIMIZE_CSS": "false", + "process.env.__NEXT_ROUTER_BASEPATH": "''", + "process.env.__NEXT_SCROLL_RESTORATION": "false", + "process.env.__NEXT_I18N_SUPPORT": "false", + "process.env.__NEXT_HAS_REWRITES": "false", + "process.env.__NEXT_ANALYTICS_ID": "null", + "process.env.__NEXT_CROSS_ORIGIN": "''", + "process.env.__NEXT_STRICT_MODE": "false", + "process.env.__NEXT_IMAGE_OPTS": "null", + "global": "globalThis", + "window": "undefined" + } + } + } + }, + "production": { + "client": "client.production.tsx", + "server": "server.production.tsx", + "fallback": "fallback.production.tsx", + "css": "onimportcss" + } + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "react-is": "^17.0.2" + } +} diff --git a/packages/bun-framework-next/page-loader.ts b/packages/bun-framework-next/page-loader.ts new file mode 100644 index 000000000..98e132a5f --- /dev/null +++ b/packages/bun-framework-next/page-loader.ts @@ -0,0 +1,142 @@ +import NextPageLoader from "next/dist/client/page-loader"; +import getAssetPathFromRoute from "next/dist/shared/lib/router/utils/get-asset-path-from-route"; +// import createRouteLoader from "./route-loader"; + +export function insertStyleSheet(url: string) { + if (document.querySelector(`link[href="${url}"]`)) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const link = document.createElement("link"); + link.rel = "stylesheet"; + + link.onload = () => resolve(); + link.onerror = () => reject(); + + link.href = url; + document.head.appendChild(link); + }); +} + +export default class PageLoader extends NextPageLoader { + public routeLoader: RouteLoader; + + constructor(_, __, pages) { + super(_, __); + + // TODO: assetPrefix? + // this.routeLoader = {}; //createRouteLoader(""); + + // Rewrite the pages object to omit the entry script + // At this point, the entry point has been loaded so we don't want to do that again. + for (let name in pages) { + for (let i = 0; i < pages[name].length; i += 1) { + const lastDot = pages[name][i].lastIndexOf("."); + if (lastDot == -1) continue; + if ( + pages[name][i].substring(lastDot - ".entry".length, lastDot) !== + ".entry" + ) + continue; + + pages[name][i] = + pages[name][i].substring(0, lastDot - ".entry".length) + + pages[name][i].substring(lastDot); + } + } + + this.pages = pages; + this.pageList = Object.keys(this.pages); + } + + pageList: string[]; + pages: Record<string, string[]>; + + getPageList() { + return this.pageList; + } + + cssQueue = []; + + onImportCSS = (event) => { + this.cssQueue.push( + insertStyleSheet(event.detail).then( + () => {}, + () => {} + ) + ); + }; + + prefetch(route) { + return Promise.resolve({}); + } + + async loadPage(route: string): Promise<GoodPageCache> { + const assets = + this.pages[route] || this.pages[getAssetPathFromRoute(route)]; + + var src; + console.log(getAssetPathFromRoute(route), assets); + for (let asset of assets) { + if (!asset.endsWith(".css")) { + src = asset; + break; + } + } + console.assert(src, "Invalid or unknown route passed to loadPage"); + + document.removeEventListener("onimportcss", this.onImportCSS); + this.cssQueue.length = 0; + document.addEventListener("onimportcss", this.onImportCSS, { + passive: true, + }); + + try { + const res = await import(src); + + if (this.cssQueue.length > 0) { + await Promise.all(this.cssQueue); + this.cssQueue.length = 0; + } + + document.removeEventListener("onimportcss", this.onImportCSS); + + if (this.cssQueue.length > 0) { + await Promise.all(this.cssQueue); + + this.cssQueue.length = 0; + } + + return { + page: res.default, + mod: res, + styleSheets: [], + __N_SSG: false, + __N_SSP: false, + }; + } catch (exception) { + console.error({ exception }); + } + + // return this.routeLoader.loadRoute(route).then((res) => { + // debugger; + // if ("component" in res) { + // return { + // page: res.component, + // mod: res.exports, + // styleSheets: res.styles.map((o) => ({ + // href: o.href, + // text: o.content, + // })), + // }; + // } + // throw res.error; + // }); + } + + // not used in development! + // prefetch(route: string): Promise<void> { + // return this.routeLoader.prefetch(route); + // } +} diff --git a/packages/bun-framework-next/render.tsx b/packages/bun-framework-next/render.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/packages/bun-framework-next/render.tsx diff --git a/packages/bun-framework-next/renderDocument.tsx b/packages/bun-framework-next/renderDocument.tsx new file mode 100644 index 000000000..356ff788b --- /dev/null +++ b/packages/bun-framework-next/renderDocument.tsx @@ -0,0 +1,722 @@ +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, + HtmlContext, +} 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"; +import * as React from "react"; +import * as ReactIs from "react-is"; +import { BODY_RENDER_TARGET } from "next/constants"; + +const dev = process.env.NODE_ENV === "development"; + +type ParsedUrlQuery = Record<string, string | string[]>; + +const isJSFile = (file: string) => + file.endsWith(".js") || + file.endsWith(".jsx") || + file.endsWith(".mjs") || + file.endsWith(".ts") || + file.endsWith(".tsx"); + +const notImplementedProxy = (base) => + new Proxy( + {}, + { + deleteProperty: function (target, prop) { + return undefined; + }, + enumerate: function (oTarget, sKey) { + return [].entries(); + }, + ownKeys: function (oTarget, sKey) { + return [].values(); + }, + has: function (oTarget, sKey) { + return false; + }, + defineProperty: function (oTarget, sKey, oDesc) { + return undefined; + }, + getOwnPropertyDescriptor: function (oTarget, sKey) { + return undefined; + }, + get(this, prop) { + throw new ReferenceError( + `${base} is not available for this environment.` + ); + }, + set(this, prop, value) { + throw new ReferenceError( + `${base} is not available for this environment.` + ); + }, + } + ); + +globalThis.fetch = (url, options) => { + return Promise.reject(new Error(`fetch is not implemented yet. sorry!!`)); +}; + +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={`${encodeURI(file)}${devOnlyCacheBusterQueryString}`} + nonce={props.nonce} + async + crossOrigin={props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN} + type="module" + /> + ); + }); +} + +// function fixLink(from: string) { +// if (from.startsWith("/_next/http://") || from.startsWith("/_next/https://")) +// return from.substring("/_next".length); +// return from; +// } + +// function cloneWithOverwrittenLink(element: React.ReactElement<any>) { +// const props = { ...element.props }; +// if ("href" in element.props) { +// props.href = fixLink(props.href); +// } + +// if ("n-href" in element.props) { +// props["n-href"] = fixLink(props["n-href"]); +// } + +// if ("n-src" in element.props) { +// props["n-src"] = fixLink(props["n-src"]); +// } + +// if ("src" in element.props) { +// props["src"] = fixLink(props.src); +// } + +// return React.cloneElement(element, props); +// } + +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 { + const htmlProps = { + __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, + + pages: buildManifest.pages, + }, + buildManifest, + docComponentsRendered, + dangerousAsPath, + canonicalBase, + ampPath, + inAmpMode, + isDevelopment: !!dev, + hybridAmp, + dynamicImports, + assetPrefix, + headTags, + unstable_runtimeJS, + unstable_JsPreload, + devOnlyCacheBusterQueryString, + scriptLoader, + locale, + disableOptimizedLoading, + ...docProps, + }; + return ( + "<!DOCTYPE html>" + + ReactDOMServer.renderToStaticMarkup( + <AmpStateContext.Provider value={ampState}> + <HtmlContext.Provider value={htmlProps}> + <Document {...htmlProps} {...docProps}></Document> + </HtmlContext.Provider> + </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, + routePaths = [], +}: { + buildId: number; + route: any; + PageNamespace: { default: NextComponentType<any> }; + AppNamespace: { default: NextComponentType<any> } | null; + DocumentNamespace: Object | null; + appStylesheets: string[]; + pageStylesheets: string[]; + routePaths: 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 pages = {}; + + for (let path of routePaths) { + const filePath = path.substring( + path.indexOf("_next/pages/") + "_next/pages".length + ); + const name = filePath.substring(0, filePath.indexOf(".")); + pages[name] = [path]; + } + + pages[pathname] = [route.scriptSrc, ...pageStylesheets]; + + if (appStylesheets.length > 0) { + if (pages["/_app"]) { + pages["/_app"].push(...appStylesheets); + } else { + pages["/_app"] = appStylesheets; + } + } + + 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); + + if (!ReactIs.isValidElementType(Component)) { + const exportNames = Object.keys(PageNamespace || {}); + + const reactComponents = exportNames.filter(ReactIs.isValidElementType); + if (reactComponents.length > 2) { + throw new Error( + `\"export default\" missing in ${ + route.filePath + }.\nTry exporting one of ${reactComponents.join(", ")}\n` + ); + } else if (reactComponents.length === 2) { + throw new Error( + `\"export default\" missing in ${route.filePath}.\n\nTry exporting <${reactComponents[0]} /> or <${reactComponents[1]} />\n` + ); + } else if (reactComponents.length == 1) { + throw new Error( + `\"export default\" missing in ${route.filePath}. Try adding this to the bottom of the file:\n\n export default ${reactComponents[0]};\n` + ); + } else if (reactComponents.length == 0) { + throw new Error( + `\"export default\" missing in ${route.filePath}. Try exporting a React component.\n` + ); + } + } + + 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, + Bun.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, + }); + + // This isn't correct. + // We don't call getServerSideProps on clients. + const getServerSideProps = PageNamespace.getServerSideProps; + if (typeof getServerSideProps === "function") { + const result = await getServerSideProps({ + params: route.params, + query: route.query, + req: notImplementedProxy("req"), + res: notImplementedProxy("res"), + resolvedUrl: route.pathname, + preview: false, + previewData: null, + locale: null, + locales: [], + defaultLocale: null, + }); + + if (result) { + if ("props" in result) { + if (typeof result.props === "object") { + Object.assign(props, result.props); + } + } + } + } + + 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; + + let html = renderDocument(Document, { + docComponentsRendered, + ...renderOpts, + disableOptimizedLoading: false, + canonicalBase: Bun.origin, + buildManifest: { + devFiles: [], + allFiles: [], + polyfillFiles: [], + lowPriorityFiles: [], + pages: pages, + }, + // 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, + }); + const bodyRenderIdx = html.indexOf(BODY_RENDER_TARGET); + html = + html.substring(0, bodyRenderIdx) + + (false ? "<!-- __NEXT_DATA__ -->" : "") + + docProps.html + + html.substring(bodyRenderIdx + BODY_RENDER_TARGET.length); + return new Response( + html + .replaceAll("/_next/http://", "http://") + .replaceAll("/_next/https://", "https://") + ); +} diff --git a/packages/bun-framework-next/route-loader.ts b/packages/bun-framework-next/route-loader.ts new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/packages/bun-framework-next/route-loader.ts diff --git a/packages/bun-framework-next/server.development.tsx b/packages/bun-framework-next/server.development.tsx new file mode 100644 index 000000000..c6a7beebf --- /dev/null +++ b/packages/bun-framework-next/server.development.tsx @@ -0,0 +1,78 @@ +import * as React from "react"; +import { Buffer } from "buffer"; +globalThis.Buffer = Buffer; + +class URL { + constructor(base, source) { + this.pathname = source; + this.href = base + source; + } +} +var onlyChildPolyfill = React.Children.only; +React.Children.only = function (children) { + if (children && typeof children === "object" && children.length == 1) { + return onlyChildPolyfill(children[0]); + } + + return onlyChildPolyfill(children); +}; +globalThis.URL = URL; +globalThis.global = globalThis; +import { render } from "./renderDocument"; + +let buildId = 0; + +var DocumentLoaded = false; +var DocumentNamespace; + +import(Bun.routesDir + "_document").then( + (doc) => { + DocumentNamespace = doc; + DocumentLoaded = true; + }, + (err) => { + if (err instanceof ResolveError) { + DocumentLoaded = true; + } else { + console.error(err); + } + } +); + +addEventListener("fetch", async (event: FetchEvent) => { + var appRoute; + + try { + appRoute = await import(Bun.routesDir + "_app"); + } catch (exception) { + appRoute = null; + } + const appStylesheets = (Bun.getImportedStyles() as string[]).slice(); + var route = Bun.match(event); + + // This imports the currently matched route. + const PageNamespace = 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 pageStylesheets = (Bun.getImportedStyles() as string[]).slice(); + + event.respondWith( + render({ + route, + PageNamespace, + appStylesheets, + pageStylesheets, + DocumentNamespace, + AppNamespace: appRoute, + buildId, + routePaths: Bun.getRouteFiles(), + }) + ); + buildId++; +}); + +// typescript isolated modules +export {}; + +declare var Bun: any; diff --git a/packages/bun-framework-next/server.production.tsx b/packages/bun-framework-next/server.production.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/packages/bun-framework-next/server.production.tsx diff --git a/packages/bun-framework-next/tsconfig.json b/packages/bun-framework-next/tsconfig.json new file mode 100644 index 000000000..d14767c9f --- /dev/null +++ b/packages/bun-framework-next/tsconfig.json @@ -0,0 +1,21 @@ +{ + "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": {} + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} |