diff --git a/package.json b/package.json index a0ee01dc891..e50a6f43b34 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@types/glob": "7.2.0", "@types/jest": "^27.4.1", "@types/jsonfile": "^6.1.0", + "@types/lodash": "^4.14.182", "@types/node-fetch": "^2.5.7", "@types/react": "^17.0.24", "@types/react-dom": "^17.0.9", @@ -91,6 +92,7 @@ "jest-watch-select-projects": "^2.0.0", "jest-watch-typeahead": "^0.6.5", "jsonfile": "^6.0.1", + "lodash": "^4.17.21", "npm-run-all": "^4.1.5", "prettier": "^2.1.2", "prompt-confirm": "^2.0.4", diff --git a/packages/remix-architect/magicExports/remix.ts b/packages/remix-architect/magicExports/remix.ts deleted file mode 100644 index 7fb1d71d168..00000000000 --- a/packages/remix-architect/magicExports/remix.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ - -// Re-export everything from this package that is available in `remix`. - -export { createArcTableSessionStorage } from "@remix-run/architect"; diff --git a/packages/remix-cloudflare-pages/magicExports/remix.ts b/packages/remix-cloudflare-pages/magicExports/remix.ts deleted file mode 100644 index 3b2dbefafa4..00000000000 --- a/packages/remix-cloudflare-pages/magicExports/remix.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Re-export everything from this package that is available in `remix`. - -export { createCloudflareKVSessionStorage } from "@remix-run/cloudflare"; diff --git a/packages/remix-cloudflare-workers/magicExports/remix.ts b/packages/remix-cloudflare-workers/magicExports/remix.ts deleted file mode 100644 index 3b2dbefafa4..00000000000 --- a/packages/remix-cloudflare-workers/magicExports/remix.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Re-export everything from this package that is available in `remix`. - -export { createCloudflareKVSessionStorage } from "@remix-run/cloudflare"; diff --git a/packages/remix-cloudflare/magicExports/remix.ts b/packages/remix-cloudflare/magicExports/remix.ts deleted file mode 100644 index ff6741b25cd..00000000000 --- a/packages/remix-cloudflare/magicExports/remix.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ - -// Re-export everything from this package that is available in `remix`. - -export { - createCloudflareKVSessionStorage, - createCookie, - createSessionStorage, - createCookieSessionStorage, - createMemorySessionStorage, -} from "@remix-run/cloudflare"; diff --git a/packages/remix-node/magicExports/remix.ts b/packages/remix-node/magicExports/remix.ts deleted file mode 100644 index c20a24a48df..00000000000 --- a/packages/remix-node/magicExports/remix.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ - -// Re-export everything from this package that is available in `remix`. - -export { - createCookie, - createSessionStorage, - createCookieSessionStorage, - createMemorySessionStorage, - createFileSessionStorage, - unstable_createFileUploadHandler, - unstable_createMemoryUploadHandler, - unstable_parseMultipartFormData, -} from "@remix-run/node"; - -export type { UploadHandler, UploadHandlerPart } from "@remix-run/node"; diff --git a/packages/remix-react/magicExports/remix.ts b/packages/remix-react/magicExports/remix.ts deleted file mode 100644 index 651d4ed9401..00000000000 --- a/packages/remix-react/magicExports/remix.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ - -// Re-export everything from this package that is available in `remix`. -// Note: We need to name all exports individually so the compiler is able -// to remove the ones we don't need in the browser builds. - -export type { - RemixBrowserProps, - FormProps, - SubmitOptions, - SubmitFunction, - FormMethod, - FormEncType, - RemixServerProps, - ShouldReloadFunction, - ThrownResponse, - LinkProps, - NavLinkProps, -} from "@remix-run/react"; - -export { - RemixBrowser, - Meta, - Links, - Scripts, - Link, - NavLink, - Form, - PrefetchPageLinks, - ScrollRestoration, - LiveReload, - useFormAction, - useSubmit, - useTransition, - useFetcher, - useFetchers, - useCatch, - useLoaderData, - useActionData, - useBeforeUnload, - useMatches, - RemixServer, -} from "@remix-run/react"; - -// react-router-dom exports -export { - Outlet, - useHref, - useLocation, - useNavigate, - useNavigationType, - useOutlet, - useParams, - useResolvedPath, - useSearchParams, - useOutletContext, -} from "@remix-run/react"; diff --git a/packages/remix-server-runtime/magicExports/remix.ts b/packages/remix-server-runtime/magicExports/remix.ts deleted file mode 100644 index 7b4ba55f42c..00000000000 --- a/packages/remix-server-runtime/magicExports/remix.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ - -// Re-export everything from this package that is available in `remix`. - -export type { - ServerBuild, - ServerEntryModule, - HandleDataRequestFunction, - HandleDocumentRequestFunction, - CookieParseOptions, - CookieSerializeOptions, - CookieSignatureOptions, - CookieOptions, - Cookie, - AppLoadContext, - AppData, - EntryContext, - LinkDescriptor, - HtmlLinkDescriptor, - PageLinkDescriptor, - ErrorBoundaryComponent, - ActionFunction, - HeadersFunction, - LinksFunction, - LoaderFunction, - MetaDescriptor, - HtmlMetaDescriptor, - MetaFunction, - RouteComponent, - RouteHandle, - RequestHandler, - SessionData, - Session, - SessionStorage, - SessionIdStorageStrategy, -} from "@remix-run/server-runtime"; - -export { - isCookie, - createSession, - isSession, - json, - redirect, -} from "@remix-run/server-runtime"; diff --git a/rollup.config.js b/rollup.config.js index 43da5834d5d..496eaa86bb7 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,6 +4,7 @@ import nodeResolve from "@rollup/plugin-node-resolve"; import copy from "rollup-plugin-copy"; import fse from "fs-extra"; import fs from "fs"; +import camelCase from "lodash/camelCase"; const executableBanner = "#!/usr/bin/env node\n"; @@ -268,8 +269,9 @@ function remixDev() { /** @returns {import("rollup").RollupOptions[]} */ function remixServerRuntime() { + let packageName = "@remix-run/server-runtime"; let sourceDir = "packages/remix-server-runtime"; - let outputDir = getOutputDir("@remix-run/server-runtime"); + let outputDir = getOutputDir(packageName); let version = getVersion(sourceDir); return [ @@ -279,7 +281,7 @@ function remixServerRuntime() { }, input: `${sourceDir}/index.ts`, output: { - banner: createBanner("@remix-run/server-runtime", version), + banner: createBanner(packageName, version), dir: outputDir, format: "cjs", preserveModules: true, @@ -299,6 +301,10 @@ function remixServerRuntime() { { src: `${sourceDir}/README.md`, dest: outputDir }, ], }), + magicExportsPlugin(getMagicExports(packageName), { + packageName, + version, + }), copyToPlaygrounds(), ], }, @@ -308,7 +314,7 @@ function remixServerRuntime() { }, input: `${sourceDir}/index.ts`, output: { - banner: createBanner("@remix-run/server-runtime", version), + banner: createBanner(packageName, version), dir: `${outputDir}/esm`, format: "esm", preserveModules: true, @@ -323,51 +329,14 @@ function remixServerRuntime() { copyToPlaygrounds(), ], }, - { - external() { - return true; - }, - input: `${sourceDir}/magicExports/remix.ts`, - output: { - banner: createBanner("@remix-run/server-runtime", version), - dir: `${outputDir}/magicExports`, - format: "cjs", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - copyToPlaygrounds(), - ], - }, - { - external() { - return true; - }, - input: `${sourceDir}/magicExports/remix.ts`, - output: { - banner: createBanner("@remix-run/server-runtime", version), - dir: `${outputDir}/magicExports/esm`, - format: "esm", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - copyToPlaygrounds(), - ], - }, ]; } /** @returns {import("rollup").RollupOptions[]} */ function remixNode() { + let packageName = "@remix-run/node"; let sourceDir = "packages/remix-node"; - let outputDir = getOutputDir("@remix-run/node"); + let outputDir = getOutputDir(packageName); let version = getVersion(sourceDir); return [ @@ -377,7 +346,7 @@ function remixNode() { }, input: `${sourceDir}/index.ts`, output: { - banner: createBanner("@remix-run/node", version), + banner: createBanner(packageName, version), dir: outputDir, format: "cjs", preserveModules: true, @@ -397,43 +366,9 @@ function remixNode() { { src: `${sourceDir}/README.md`, dest: outputDir }, ], }), - copyToPlaygrounds(), - ], - }, - { - external() { - return true; - }, - input: `${sourceDir}/magicExports/remix.ts`, - output: { - banner: createBanner("@remix-run/node", version), - dir: `${outputDir}/magicExports`, - format: "cjs", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - copyToPlaygrounds(), - ], - }, - { - external() { - return true; - }, - input: `${sourceDir}/magicExports/remix.ts`, - output: { - banner: createBanner("@remix-run/node", version), - dir: `${outputDir}/magicExports/esm`, - format: "esm", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], + magicExportsPlugin(getMagicExports(packageName), { + packageName, + version, }), copyToPlaygrounds(), ], @@ -443,8 +378,9 @@ function remixNode() { /** @returns {import("rollup").RollupOptions[]} */ function remixCloudflare() { + let packageName = "@remix-run/cloudflare"; let sourceDir = "packages/remix-cloudflare"; - let outputDir = getOutputDir("@remix-run/cloudflare"); + let outputDir = getOutputDir(packageName); let version = getVersion(sourceDir); return [ @@ -454,7 +390,7 @@ function remixCloudflare() { }, input: `${sourceDir}/index.ts`, output: { - banner: createBanner("@remix-run/cloudflare", version), + banner: createBanner(packageName, version), dir: outputDir, format: "cjs", preserveModules: true, @@ -474,43 +410,9 @@ function remixCloudflare() { { src: `${sourceDir}/README.md`, dest: outputDir }, ], }), - copyToPlaygrounds(), - ], - }, - { - external() { - return true; - }, - input: `${sourceDir}/magicExports/remix.ts`, - output: { - banner: createBanner("@remix-run/cloudflare", version), - dir: `${outputDir}/magicExports/esm`, - format: "esm", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - copyToPlaygrounds(), - ], - }, - { - external() { - return true; - }, - input: `${sourceDir}/magicExports/remix.ts`, - output: { - banner: createBanner("@remix-run/cloudflare", version), - dir: `${outputDir}/magicExports`, - format: "cjs", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], + magicExportsPlugin(getMagicExports(packageName), { + packageName, + version, }), copyToPlaygrounds(), ], @@ -602,14 +504,19 @@ function remixCloudflarePages() { ]; } -/** @returns {import("rollup").RollupOptions[]} */ +/** + * @param {RemixAdapter} adapterName + * @returns {import("rollup").RollupOptions[]} + */ function getAdapterConfig(adapterName) { + /** @type {`@remix-run/${RemixPackage}`} */ let packageName = `@remix-run/${adapterName}`; let sourceDir = `packages/remix-${adapterName}`; let outputDir = getOutputDir(packageName); let version = getVersion(sourceDir); - let hasMagicExports = fse.existsSync(`${sourceDir}/magicExports/remix.ts`); + // TODO: Remove in v2 + let magicExports = getMagicExports(packageName); return [ { @@ -638,56 +545,261 @@ function getAdapterConfig(adapterName) { { src: `${sourceDir}/README.md`, dest: outputDir }, ], }), + magicExportsPlugin(magicExports, { + packageName, + version, + }), copyToPlaygrounds(), ], }, - ...(hasMagicExports - ? [ - { - external() { - return true; - }, - input: `${sourceDir}/magicExports/remix.ts`, - output: { - banner: createBanner(packageName, version), - dir: `${outputDir}/magicExports`, - format: "cjs", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - copyToPlaygrounds(), - ], - }, - { - external() { - return true; - }, - input: `${sourceDir}/magicExports/remix.ts`, - output: { - banner: createBanner(packageName, version), - dir: `${outputDir}/magicExports/esm`, - format: "esm", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - copyToPlaygrounds(), - ], - }, - ] - : []), ]; } +/** + * TODO: Remove in v2 + * @param {RemixPackage} packageName + * @returns {MagicExports | null} + */ +function getMagicExports(packageName) { + // Re-export everything from packages that is available in `remix` + switch (packageName) { + case "@remix-run/architect": + return { + values: { + "@remix-run/architect": ["createArcTableSessionStorage"], + }, + }; + case "@remix-run/cloudflare-pages": + return { + values: { + "@remix-run/cloudflare": ["createCloudflareKVSessionStorage"], + }, + }; + case "@remix-run/cloudflare-workers": + return { + values: { + "@remix-run/cloudflare": ["createCloudflareKVSessionStorage"], + }, + }; + case "@remix-run/cloudflare": + return { + values: { + "@remix-run/cloudflare": [ + "createCloudflareKVSessionStorage", + "createCookie", + "createSessionStorage", + "createCookieSessionStorage", + "createMemorySessionStorage", + ], + }, + }; + case "@remix-run/node": + return { + values: { + "@remix-run/node": [ + "createCookie", + "createSessionStorage", + "createCookieSessionStorage", + "createMemorySessionStorage", + "createFileSessionStorage", + "unstable_createFileUploadHandler", + "unstable_createMemoryUploadHandler", + "unstable_parseMultipartFormData", + ], + }, + types: { + "@remix-run/node": ["UploadHandler", "UploadHandlerPart"], + }, + }; + case "@remix-run/react": + return { + values: { + "@remix-run/react": [ + "RemixBrowser", + "Meta", + "Links", + "Scripts", + "Link", + "NavLink", + "Form", + "PrefetchPageLinks", + "ScrollRestoration", + "LiveReload", + "useFormAction", + "useSubmit", + "useTransition", + "useFetcher", + "useFetchers", + "useCatch", + "useLoaderData", + "useActionData", + "useBeforeUnload", + "useMatches", + "RemixServer", + + // react-router-dom exports + "Outlet", + "useHref", + "useLocation", + "useNavigate", + "useNavigationType", + "useOutlet", + "useParams", + "useResolvedPath", + "useSearchParams", + "useOutletContext", + ], + }, + types: { + "@remix-run/react": [ + "RemixBrowserProps", + "FormProps", + "SubmitOptions", + "SubmitFunction", + "FormMethod", + "FormEncType", + "RemixServerProps", + "ShouldReloadFunction", + "ThrownResponse", + "LinkProps", + "NavLinkProps", + ], + }, + }; + case "@remix-run/server-runtime": + return { + values: { + "@remix-run/server-runtime": [ + "isCookie", + "createSession", + "isSession", + "json", + "redirect", + ], + }, + types: { + "@remix-run/server-runtime": [ + "ServerBuild", + "ServerEntryModule", + "HandleDataRequestFunction", + "HandleDocumentRequestFunction", + "CookieParseOptions", + "CookieSerializeOptions", + "CookieSignatureOptions", + "CookieOptions", + "Cookie", + "AppLoadContext", + "AppData", + "EntryContext", + "LinkDescriptor", + "HtmlLinkDescriptor", + "PageLinkDescriptor", + "ErrorBoundaryComponent", + "ActionFunction", + "HeadersFunction", + "LinksFunction", + "LoaderFunction", + "MetaDescriptor", + "HtmlMetaDescriptor", + "MetaFunction", + "RouteComponent", + "RouteHandle", + "RequestHandler", + "SessionData", + "Session", + "SessionStorage", + "SessionIdStorageStrategy", + ], + }, + }; + default: + return null; + } +} + +/** + * TODO: Remove in v2 + * @param {MagicExports | null} magicExports + * @param {{ packageName: string; version: string }} buildInfo + * @returns {import("rollup").Plugin} + */ +function magicExportsPlugin(magicExports, { packageName, version }) { + return { + name: `${packageName}:generate-magic-exports`, + generateBundle() { + if (!magicExports) return; + + let tsContents = ""; + let cjsContents = ""; + let esmContents = ""; + let banner = createBanner(packageName, version); + + if (magicExports.values) { + for (let pkgName of Object.keys(magicExports.values)) { + if (!esmContents) esmContents = banner + "\n"; + if (!tsContents) tsContents = banner + "\n"; + if (!cjsContents) { + cjsContents = + banner + + "\n" + + "'use strict';\n" + + "Object.defineProperty(exports, '__esModule', { value: true });\n"; + } + + let exportList = magicExports.values[pkgName].join(", "); + esmContents += `export { ${exportList} } from '${pkgName}';\n`; + tsContents += `export { ${exportList} } from '${pkgName}';\n`; + + let cjsModule = camelCase( + pkgName.startsWith("@remix-run/") ? pkgName.slice(11) : pkgName + ); + cjsContents += `var ${cjsModule} = require('${pkgName}');\n`; + for (let symbol of magicExports.values[pkgName]) { + cjsContents += + `Object.defineProperty(exports, '${symbol}', {\n` + + " enumerable: true,\n" + + ` get: function () { return ${cjsModule}.${symbol}; }\n` + + "});\n"; + } + } + } + + if (magicExports.types) { + for (let pkgName of Object.keys(magicExports.types)) { + if (!tsContents) tsContents = banner + "\n"; + let exportList = magicExports.types[pkgName].join(", "); + tsContents += `export type { ${exportList} } from '${pkgName}';\n`; + } + } + + tsContents && + this.emitFile({ + type: "asset", + fileName: path.join("magicExports", "remix.d.ts"), + source: tsContents, + }); + + cjsContents && + this.emitFile({ + type: "asset", + fileName: path.join("magicExports", "remix.js"), + source: cjsContents, + }); + + esmContents && + this.emitFile({ + type: "asset", + fileName: path.join("magicExports", "esm", "remix.js"), + source: esmContents, + }); + }, + }; +} + /** @returns {import("rollup").RollupOptions[]} */ function remixServerAdapters() { + // magicExports: Re-export everything from each package that is available in `remix` + // TODO: Remove this in v2 when we get rid of magic exports altogether return [ ...getAdapterConfig("architect"), ...getAdapterConfig("cloudflare-pages"), @@ -700,8 +812,9 @@ function remixServerAdapters() { /** @returns {import("rollup").RollupOptions[]} */ function remixReact() { + let packageName = "@remix-run/react"; let sourceDir = "packages/remix-react"; - let outputDir = getOutputDir("@remix-run/react"); + let outputDir = getOutputDir(packageName); let version = getVersion(sourceDir); // This CommonJS build of remix-react is for node; both for use in running our @@ -713,7 +826,7 @@ function remixReact() { }, input: `${sourceDir}/index.tsx`, output: { - banner: createBanner("@remix-run/react", version), + banner: createBanner(packageName, version), dir: outputDir, format: "cjs", preserveModules: true, @@ -733,6 +846,10 @@ function remixReact() { { src: `${sourceDir}/README.md`, dest: outputDir }, ], }), + magicExportsPlugin(getMagicExports(packageName), { + packageName, + version, + }), copyToPlaygrounds(), ], }; @@ -761,54 +878,7 @@ function remixReact() { ], }; - /** @type {import("rollup").RollupOptions[]} */ - let remixReactMagicExportsCJS = { - external() { - return true; - }, - input: `${sourceDir}/magicExports/remix.ts`, - output: { - banner: createBanner("@remix-run/react", version), - dir: `${outputDir}/magicExports`, - format: "cjs", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - copyToPlaygrounds(), - ], - }; - - /** @type {import("rollup").RollupOptions[]} */ - let remixReactMagicExportsESM = { - external() { - return true; - }, - input: `${sourceDir}/magicExports/remix.ts`, - output: { - banner: createBanner("@remix-run/react", version), - dir: `${outputDir}/magicExports/esm`, - format: "esm", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - copyToPlaygrounds(), - ], - }; - - return [ - remixReactCJS, - remixReactESM, - remixReactMagicExportsCJS, - remixReactMagicExportsESM, - ]; + return [remixReactCJS, remixReactESM]; } /** @returns {import("rollup").RollupOptions[]} */ @@ -936,3 +1006,11 @@ function copyToPlaygrounds() { }, }; } + +/** + * @typedef {Record<"values" | "types", Record>} MagicExports + * @typedef {"architect" | "cloudflare-pages" | "cloudflare-workers" | "express" | "netlify" | "vercel"} RemixAdapter + * @typedef {"cloudflare" | "node" | "deno"} RemixRuntime + * @typedef {`@remix-run/${RemixAdapter | RemixRuntime | "dev" | "eslint-config" | "react" | "serve" | "server-runtime"}`} ScopedRemixPackage + * @typedef {"create-remix" | "remix" | ScopedRemixPackage} RemixPackage + */ diff --git a/yarn.lock b/yarn.lock index 17b82dc7cd1..47ad47d6ce0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2207,6 +2207,11 @@ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.174.tgz" integrity sha512-KMBLT6+g9qrGXpDt7ohjWPUD34WA/jasrtjTEHStF0NPdEwJ1N9SZ+4GaMVDeuk/y0+X5j9xFm6mNiXS7UoaLQ== +"@types/lodash@^4.14.182": + version "4.14.182" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== + "@types/mdast@^3.0.0": version "3.0.10" resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz"