diff --git a/.eslintrc.js b/.eslintrc.js index a7c8a1014fa..be11e2f9814 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,4 +5,12 @@ module.exports = { "plugin:markdown/recommended", ], plugins: ["markdown"], + overrides: [ + { + files: ["rollup.config.js"], + rules: { + "import/no-extraneous-dependencies": 0, + }, + }, + ], }; diff --git a/package.json b/package.json index ed281b9f8c0..edef75ee83c 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@octokit/rest": "^18.12.0", "@playwright/test": "1.20.2", "@rollup/plugin-babel": "^5.2.2", + "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^11.0.1", "@testing-library/cypress": "^8.0.2", "@testing-library/jest-dom": "^5.16.2", diff --git a/packages/create-remix/.eslintrc.js b/packages/create-remix/.eslintrc.js index db68b19170c..9c07d5bba99 100644 --- a/packages/create-remix/.eslintrc.js +++ b/packages/create-remix/.eslintrc.js @@ -1,6 +1,14 @@ module.exports = { rules: { // we have an example where we need this - "no-undef": "off", + "no-undef": 0, }, + overrides: [ + { + files: ["rollup.config.js"], + rules: { + "no-undef": 2, + }, + }, + ], }; diff --git a/packages/create-remix/rollup.config.js b/packages/create-remix/rollup.config.js new file mode 100644 index 00000000000..3f28293d065 --- /dev/null +++ b/packages/create-remix/rollup.config.js @@ -0,0 +1,7 @@ +const { getCliConfig } = require("../../rollup.utils"); +const { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + return [getCliConfig({ packageName, version })]; +}; diff --git a/packages/remix-architect/rollup.config.js b/packages/remix-architect/rollup.config.js new file mode 100644 index 00000000000..56e9ae4f964 --- /dev/null +++ b/packages/remix-architect/rollup.config.js @@ -0,0 +1,17 @@ +const { getAdapterConfig } = require("../../rollup.utils"); + +// Re-export everything from this package that is available in `remix` +/** @type {import('../../rollup.utils').MagicExports} */ +const magicExports = { + "@remix-run/architect": { + values: ["createArcTableSessionStorage"], + }, + "@remix-run/node": { + types: ["UploadHandler", "UploadHandlerPart"], + }, +}; + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + return [getAdapterConfig("architect", magicExports)]; +}; diff --git a/packages/remix-cloudflare-pages/rollup.config.js b/packages/remix-cloudflare-pages/rollup.config.js new file mode 100644 index 00000000000..1bf7bb14d3b --- /dev/null +++ b/packages/remix-cloudflare-pages/rollup.config.js @@ -0,0 +1,44 @@ +const path = require("path"); +const babel = require("@rollup/plugin-babel").default; +const nodeResolve = require("@rollup/plugin-node-resolve").default; + +const { + copyToPlaygrounds, + createBanner, + getAdapterConfig, + getOutputDir, + isBareModuleId, +} = require("../../rollup.utils"); +const { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + let sourceDir = "packages/remix-cloudflare-pages"; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + + return [ + { + external(id) { + return isBareModuleId(id); + }, + input: `${sourceDir}/index.ts`, + output: { + banner: createBanner("@remix-run/cloudflare-pages", version), + dir: `${outputDist}/esm`, + format: "esm", + preserveModules: true, + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + nodeResolve({ extensions: [".ts", ".tsx"] }), + copyToPlaygrounds(), + ], + }, + getAdapterConfig("cloudflare-pages"), + ]; +}; diff --git a/packages/remix-cloudflare-workers/rollup.config.js b/packages/remix-cloudflare-workers/rollup.config.js new file mode 100644 index 00000000000..0d15a2ebf94 --- /dev/null +++ b/packages/remix-cloudflare-workers/rollup.config.js @@ -0,0 +1,44 @@ +const path = require("path"); +const babel = require("@rollup/plugin-babel").default; +const nodeResolve = require("@rollup/plugin-node-resolve").default; + +const { + copyToPlaygrounds, + createBanner, + getAdapterConfig, + getOutputDir, + isBareModuleId, +} = require("../../rollup.utils"); +const { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + let sourceDir = "packages/remix-cloudflare-workers"; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + + return [ + { + external(id) { + return isBareModuleId(id); + }, + input: `${sourceDir}/index.ts`, + output: { + banner: createBanner("@remix-run/cloudflare-workers", version), + dir: `${outputDist}/esm`, + format: "esm", + preserveModules: true, + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + nodeResolve({ extensions: [".ts", ".tsx"] }), + copyToPlaygrounds(), + ], + }, + getAdapterConfig("cloudflare-workers"), + ]; +}; diff --git a/packages/remix-cloudflare/rollup.config.js b/packages/remix-cloudflare/rollup.config.js new file mode 100644 index 00000000000..8ee1a4134f4 --- /dev/null +++ b/packages/remix-cloudflare/rollup.config.js @@ -0,0 +1,53 @@ +const path = require("path"); +const babel = require("@rollup/plugin-babel").default; +const nodeResolve = require("@rollup/plugin-node-resolve").default; +const copy = require("rollup-plugin-copy"); + +const { + getOutputDir, + copyToPlaygrounds, + magicExportsPlugin, + isBareModuleId, + createBanner, +} = require("../../rollup.utils"); +const { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + let sourceDir = "packages/remix-cloudflare"; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + + return [ + { + external(id) { + return isBareModuleId(id); + }, + input: `${sourceDir}/index.ts`, + output: { + banner: createBanner(packageName, version), + dir: outputDist, + format: "cjs", + preserveModules: true, + exports: "named", + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + nodeResolve({ extensions: [".ts", ".tsx"] }), + copy({ + targets: [ + { src: "LICENSE.md", dest: [outputDir, sourceDir] }, + { src: `${sourceDir}/package.json`, dest: outputDir }, + { src: `${sourceDir}/README.md`, dest: outputDir }, + ], + }), + magicExportsPlugin({ packageName, version }), + copyToPlaygrounds(), + ], + }, + ]; +}; diff --git a/packages/remix-deno/rollup.config.js b/packages/remix-deno/rollup.config.js new file mode 100644 index 00000000000..cdb2d4f611a --- /dev/null +++ b/packages/remix-deno/rollup.config.js @@ -0,0 +1,27 @@ +// deno-lint-ignore-file +const copy = require("rollup-plugin-copy"); + +const { getOutputDir, copyToPlaygrounds } = require("../../rollup.utils"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + let sourceDir = "packages/remix-deno"; + let outputDir = getOutputDir("@remix-run/deno"); + + return [ + { + input: `${sourceDir}/.empty.js`, + plugins: [ + copy({ + targets: [ + { src: "LICENSE.md", dest: [outputDir, sourceDir] }, + { src: `${sourceDir}/**/*`, dest: outputDir }, + { src: `!${sourceDir}/rollup.config.js`, dest: outputDir }, + ], + gitignore: true, + }), + copyToPlaygrounds(), + ], + }, + ]; +}; diff --git a/packages/remix-dev/rollup.config.js b/packages/remix-dev/rollup.config.js new file mode 100644 index 00000000000..6bb4b9ab048 --- /dev/null +++ b/packages/remix-dev/rollup.config.js @@ -0,0 +1,124 @@ +const path = require("path"); +const babel = require("@rollup/plugin-babel").default; +const nodeResolve = require("@rollup/plugin-node-resolve").default; +const copy = require("rollup-plugin-copy"); + +const { + copyToPlaygrounds, + createBanner, + getCliConfig, + getOutputDir, + isBareModuleId, +} = require("../../rollup.utils"); +const { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + let sourceDir = "packages/remix-dev"; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + + return [ + { + external(id, parent) { + if ( + id === "../package.json" && + parent === path.resolve(__dirname, "cli/create.ts") + ) { + return true; + } + + return isBareModuleId(id); + }, + input: `${sourceDir}/index.ts`, + output: { + banner: createBanner("@remix-run/dev", version), + dir: outputDist, + format: "cjs", + preserveModules: true, + exports: "named", + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts"], + }), + nodeResolve({ extensions: [".ts"] }), + copy({ + targets: [ + { src: `LICENSE.md`, dest: [outputDir, sourceDir] }, + { src: `${sourceDir}/package.json`, dest: [outputDir, outputDist] }, + { src: `${sourceDir}/README.md`, dest: outputDir }, + { + src: `${sourceDir}/compiler/shims`, + dest: [`${outputDir}/compiler`, `${outputDist}/compiler`], + }, + ], + }), + // Allow dynamic imports in CJS code to allow us to utilize + // ESM modules as part of the compiler. + { + name: "dynamic-import-polyfill", + renderDynamicImport() { + return { + left: "import(", + right: ")", + }; + }, + }, + copyToPlaygrounds(), + ], + }, + getCliConfig({ packageName, version }), + { + external: (id) => isBareModuleId(id), + input: [`${sourceDir}/cli/migrate/migrations/transforms.ts`], + output: { + banner: createBanner("@remix-run/dev", version), + dir: `${outputDist}/cli/migrate/migrations`, + exports: "named", + format: "cjs", + preserveModules: true, + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts"], + }), + nodeResolve({ extensions: [".ts"] }), + copyToPlaygrounds(), + ], + }, + { + external() { + return true; + }, + input: `${sourceDir}/server-build.ts`, + output: [ + { + // TODO: Remove deep import support in v2 or move to package.json + // "exports" field + banner: createBanner("@remix-run/dev", version, true), + dir: outputDir, + format: "cjs", + }, + { + banner: createBanner("@remix-run/dev", version, true), + dir: outputDist, + format: "cjs", + }, + ], + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts"], + }), + nodeResolve({ extensions: [".ts"] }), + copyToPlaygrounds(), + ], + }, + ]; +}; diff --git a/packages/remix-express/rollup.config.js b/packages/remix-express/rollup.config.js new file mode 100644 index 00000000000..fdb2d76f79d --- /dev/null +++ b/packages/remix-express/rollup.config.js @@ -0,0 +1,6 @@ +const { getAdapterConfig } = require("../../rollup.utils"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + return [getAdapterConfig("express")]; +}; diff --git a/packages/remix-netlify/rollup.config.js b/packages/remix-netlify/rollup.config.js new file mode 100644 index 00000000000..127d95b5fef --- /dev/null +++ b/packages/remix-netlify/rollup.config.js @@ -0,0 +1,6 @@ +const { getAdapterConfig } = require("../../rollup.utils"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + return [getAdapterConfig("netlify")]; +}; diff --git a/packages/remix-node/rollup.config.js b/packages/remix-node/rollup.config.js new file mode 100644 index 00000000000..bc2cef39ebb --- /dev/null +++ b/packages/remix-node/rollup.config.js @@ -0,0 +1,53 @@ +const path = require("path"); +const babel = require("@rollup/plugin-babel").default; +const nodeResolve = require("@rollup/plugin-node-resolve").default; +const copy = require("rollup-plugin-copy"); + +const { + copyToPlaygrounds, + createBanner, + getOutputDir, + isBareModuleId, + magicExportsPlugin, +} = require("../../rollup.utils"); +const { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + let sourceDir = "packages/remix-node"; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + + return [ + { + external(id) { + return isBareModuleId(id); + }, + input: `${sourceDir}/index.ts`, + output: { + banner: createBanner(packageName, version), + dir: outputDist, + format: "cjs", + preserveModules: true, + exports: "named", + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + nodeResolve({ extensions: [".ts", ".tsx"] }), + copy({ + targets: [ + { src: "LICENSE.md", dest: [outputDir, sourceDir] }, + { src: `${sourceDir}/package.json`, dest: outputDir }, + { src: `${sourceDir}/README.md`, dest: outputDir }, + ], + }), + magicExportsPlugin({ packageName, version }), + copyToPlaygrounds(), + ], + }, + ]; +}; diff --git a/packages/remix-react/rollup.config.js b/packages/remix-react/rollup.config.js new file mode 100644 index 00000000000..125bbb8b5e6 --- /dev/null +++ b/packages/remix-react/rollup.config.js @@ -0,0 +1,80 @@ +const path = require("path"); +const babel = require("@rollup/plugin-babel").default; +const nodeResolve = require("@rollup/plugin-node-resolve").default; +const copy = require("rollup-plugin-copy"); + +const { + copyToPlaygrounds, + createBanner, + getOutputDir, + isBareModuleId, + magicExportsPlugin, +} = require("../../rollup.utils"); +const { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + let sourceDir = "packages/remix-react"; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + + // This CommonJS build of remix-react is for node; both for use in running our + // server and for 3rd party tools that work with node. + /** @type {import("rollup").RollupOptions} */ + let remixReactCJS = { + external(id) { + return isBareModuleId(id); + }, + input: `${sourceDir}/index.tsx`, + output: { + banner: createBanner(packageName, version), + dir: outputDist, + format: "cjs", + preserveModules: true, + exports: "auto", + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + nodeResolve({ extensions: [".ts", ".tsx"] }), + copy({ + targets: [ + { src: "LICENSE.md", dest: [outputDir, sourceDir] }, + { src: `${sourceDir}/package.json`, dest: outputDir }, + { src: `${sourceDir}/README.md`, dest: outputDir }, + ], + }), + magicExportsPlugin({ packageName, version }), + copyToPlaygrounds(), + ], + }; + + // The browser build of remix-react is ESM so we can treeshake it. + /** @type {import("rollup").RollupOptions} */ + let remixReactESM = { + external(id) { + return isBareModuleId(id); + }, + input: `${sourceDir}/index.tsx`, + output: { + banner: createBanner("@remix-run/react", version), + dir: `${outputDist}/esm`, + format: "esm", + preserveModules: true, + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + nodeResolve({ extensions: [".ts", ".tsx"] }), + copyToPlaygrounds(), + ], + }; + + return [remixReactCJS, remixReactESM]; +}; diff --git a/packages/remix-serve/rollup.config.js b/packages/remix-serve/rollup.config.js new file mode 100644 index 00000000000..5ba7a47a47a --- /dev/null +++ b/packages/remix-serve/rollup.config.js @@ -0,0 +1,53 @@ +const path = require("path"); +const babel = require("@rollup/plugin-babel").default; +const nodeResolve = require("@rollup/plugin-node-resolve").default; +const copy = require("rollup-plugin-copy"); + +const { + copyToPlaygrounds, + createBanner, + getCliConfig, + getOutputDir, + isBareModuleId, +} = require("../../rollup.utils"); +const { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + let sourceDir = "packages/remix-serve"; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + + return [ + { + external(id) { + return isBareModuleId(id); + }, + input: [`${sourceDir}/index.ts`, `${sourceDir}/env.ts`], + output: { + banner: createBanner(packageName, version), + dir: outputDist, + format: "cjs", + preserveModules: true, + exports: "auto", + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + nodeResolve({ extensions: [".ts", ".tsx"] }), + copy({ + targets: [ + { src: "LICENSE.md", dest: [outputDir, sourceDir] }, + { src: `${sourceDir}/package.json`, dest: outputDir }, + { src: `${sourceDir}/README.md`, dest: outputDir }, + ], + }), + copyToPlaygrounds(), + ], + }, + getCliConfig({ packageName, version }), + ]; +}; diff --git a/packages/remix-server-runtime/rollup.config.js b/packages/remix-server-runtime/rollup.config.js new file mode 100644 index 00000000000..256dfeab6ea --- /dev/null +++ b/packages/remix-server-runtime/rollup.config.js @@ -0,0 +1,75 @@ +/* eslint-disable no-restricted-globals, import/no-nodejs-modules */ +const path = require("path"); +const babel = require("@rollup/plugin-babel").default; +const nodeResolve = require("@rollup/plugin-node-resolve").default; +const copy = require("rollup-plugin-copy"); + +const { + getOutputDir, + isBareModuleId, + createBanner, + copyToPlaygrounds, + magicExportsPlugin, +} = require("../../rollup.utils"); +const { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + let sourceDir = "packages/remix-server-runtime"; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + + return [ + { + external(id) { + return isBareModuleId(id); + }, + input: `${sourceDir}/index.ts`, + output: { + banner: createBanner(packageName, version), + dir: outputDist, + format: "cjs", + preserveModules: true, + exports: "named", + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + nodeResolve({ extensions: [".ts", ".tsx"] }), + copy({ + targets: [ + { src: "LICENSE.md", dest: [outputDir, sourceDir] }, + { src: `${sourceDir}/package.json`, dest: outputDir }, + { src: `${sourceDir}/README.md`, dest: outputDir }, + ], + }), + magicExportsPlugin({ packageName, version }), + copyToPlaygrounds(), + ], + }, + { + external(id) { + return isBareModuleId(id); + }, + input: `${sourceDir}/index.ts`, + output: { + banner: createBanner(packageName, version), + dir: `${outputDist}/esm`, + format: "esm", + preserveModules: true, + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + nodeResolve({ extensions: [".ts", ".tsx"] }), + copyToPlaygrounds(), + ], + }, + ]; +}; diff --git a/packages/remix-vercel/rollup.config.js b/packages/remix-vercel/rollup.config.js new file mode 100644 index 00000000000..e934a106632 --- /dev/null +++ b/packages/remix-vercel/rollup.config.js @@ -0,0 +1,6 @@ +const { getAdapterConfig } = require("../../rollup.utils"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + return [getAdapterConfig("vercel")]; +}; diff --git a/packages/remix/rollup.config.js b/packages/remix/rollup.config.js new file mode 100644 index 00000000000..0e7fc8f1222 --- /dev/null +++ b/packages/remix/rollup.config.js @@ -0,0 +1,67 @@ +/* eslint-disable import/no-extraneous-dependencies */ +const babel = require("@rollup/plugin-babel").default; +const path = require("path"); + +const { + copyPublishFiles, + copyToPlaygrounds, + createBanner, + getOutputDir, +} = require("../../rollup.utils"); +let { name: packageName, version } = require("./package.json"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + // Don't blow away remix magic exports on local builds, since they've + // already been configured by postinstall + if (process.env.REMIX_LOCAL_BUILD_DIRECTORY) { + return []; + } + + let sourceDir = "packages/remix"; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + + return [ + { + external() { + return true; + }, + input: `${sourceDir}/index.ts`, + output: { + format: "cjs", + dir: outputDist, + banner: createBanner(packageName, version), + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + copyPublishFiles(packageName), + copyToPlaygrounds(), + ], + }, + { + external() { + return true; + }, + input: `${sourceDir}/index.ts`, + output: { + format: "esm", + dir: path.join(outputDist, "esm"), + banner: createBanner(packageName, version), + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + copyPublishFiles(packageName), + copyToPlaygrounds(), + ], + }, + ]; +}; diff --git a/rollup.config.js b/rollup.config.js index 74cd6a502e1..109e63ca56d 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,982 +1,15 @@ -import path from "path"; -import babel from "@rollup/plugin-babel"; -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"; - -let activeOutputDir = "build"; - -if (process.env.REMIX_LOCAL_BUILD_DIRECTORY) { - let appDir = path.join( - process.cwd(), - process.env.REMIX_LOCAL_BUILD_DIRECTORY - ); - try { - fse.readdirSync(path.join(appDir, "node_modules")); - } catch (e) { - console.error( - "Oops! You pointed REMIX_LOCAL_BUILD_DIRECTORY to a directory that " + - "does not have a node_modules/ folder. Please `npm install` in that " + - "directory and try again." - ); - process.exit(1); - } - console.log("Writing rollup output to", appDir); - activeOutputDir = appDir; -} - -function getOutputDir(pkg) { - return path.join(activeOutputDir, "node_modules", pkg); -} - -function createBanner(packageName, version) { - return `/** - * ${packageName} v${version} - * - * Copyright (c) Remix Software Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE.md file in the root directory of this source tree. - * - * @license MIT - */`; -} - -function getVersion(packageDir) { - return require(`./${packageDir}/package.json`).version; -} - -function isBareModuleId(id) { - return !id.startsWith(".") && !path.isAbsolute(id); -} - -/** @returns {import("rollup").RollupOptions[]} */ -function createRemix() { - let sourceDir = "packages/create-remix"; - let outputDir = getOutputDir("create-remix"); - let outputDist = path.join(outputDir, "dist"); - let version = getVersion(sourceDir); - - return [ - { - external() { - return true; - }, - input: `${sourceDir}/cli.ts`, - output: { - format: "cjs", - dir: outputDist, - banner: executableBanner + createBanner("create-remix", version), - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts"], - }), - nodeResolve({ extensions: [".ts"] }), - copy({ - targets: [ - { src: `LICENSE.md`, dest: [outputDir, sourceDir] }, - { src: `${sourceDir}/package.json`, dest: outputDir }, - { src: `${sourceDir}/README.md`, dest: outputDir }, - ], - }), - copyToPlaygrounds(), - ], - }, - ]; -} - -/** @returns {import("rollup").RollupOptions[]} */ -function remix() { - let sourceDir = "packages/remix"; - let outputDir = getOutputDir("remix"); - let outputDist = path.join(outputDir, "dist"); - let version = getVersion(sourceDir); - - return [ - { - external() { - return true; - }, - input: `${sourceDir}/index.ts`, - output: { - format: "cjs", - dir: outputDist, - banner: createBanner("remix", version), - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts"], - }), - copy({ - targets: [ - { src: `LICENSE.md`, dest: [outputDir, sourceDir] }, - { src: `${sourceDir}/package.json`, dest: outputDir }, - { src: `${sourceDir}/README.md`, dest: outputDir }, - ], - }), - copyToPlaygrounds(), - ], - }, - { - external() { - return true; - }, - input: `${sourceDir}/index.ts`, - output: { - banner: createBanner("remix", version), - dir: `${outputDist}/esm`, - format: "esm", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts"], - }), - copyToPlaygrounds(), - ], - }, - ]; -} - -/** @returns {import("rollup").RollupOptions[]} */ -function remixDev() { - let sourceDir = "packages/remix-dev"; - let outputDir = getOutputDir("@remix-run/dev"); - let outputDist = path.join(outputDir, "dist"); - let version = getVersion(sourceDir); - - return [ - { - external(id, parent) { - if ( - id === "../package.json" && - parent === path.resolve(__dirname, "packages/remix-dev/cli/create.ts") - ) { - return true; - } - - return isBareModuleId(id); - }, - input: `${sourceDir}/index.ts`, - output: { - banner: createBanner("@remix-run/dev", version), - dir: outputDist, - format: "cjs", - preserveModules: true, - exports: "named", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts"], - }), - nodeResolve({ extensions: [".ts"] }), - copy({ - targets: [ - { src: `LICENSE.md`, dest: [outputDir, sourceDir] }, - { src: `${sourceDir}/package.json`, dest: [outputDir, outputDist] }, - { src: `${sourceDir}/README.md`, dest: outputDir }, - { - src: `${sourceDir}/compiler/shims`, - dest: [`${outputDir}/compiler`, `${outputDist}/compiler`], - }, - ], - }), - // Allow dynamic imports in CJS code to allow us to utilize - // ESM modules as part of the compiler. - { - name: "dynamic-import-polyfill", - renderDynamicImport() { - return { - left: "import(", - right: ")", - }; - }, - }, - copyToPlaygrounds(), - ], - }, - { - external() { - return true; - }, - input: `${sourceDir}/cli.ts`, - output: { - banner: executableBanner + createBanner("@remix-run/dev", version), - dir: outputDist, - format: "cjs", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts"], - }), - nodeResolve({ extensions: [".ts"] }), - copyToPlaygrounds(), - ], - }, - { - external: (id) => isBareModuleId(id), - input: [`${sourceDir}/cli/migrate/migrations/transforms.ts`], - output: { - banner: createBanner("@remix-run/dev", version), - dir: `${outputDist}/cli/migrate/migrations`, - exports: "named", - format: "cjs", - preserveModules: true, - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts"], - }), - nodeResolve({ extensions: [".ts"] }), - copyToPlaygrounds(), - ], - }, - { - external() { - return true; - }, - input: `${sourceDir}/server-build.ts`, - output: [ - { - // TODO: Remove deep import support in v2 or move to package.json - // "exports" field - banner: executableBanner + createBanner("@remix-run/dev", version), - dir: outputDir, - format: "cjs", - }, - { - banner: executableBanner + createBanner("@remix-run/dev", version), - dir: outputDist, - format: "cjs", - }, - ], - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts"], - }), - nodeResolve({ extensions: [".ts"] }), - copyToPlaygrounds(), - ], - }, - ]; -} - -/** @returns {import("rollup").RollupOptions[]} */ -function remixServerRuntime() { - let packageName = "@remix-run/server-runtime"; - let sourceDir = "packages/remix-server-runtime"; - let outputDir = getOutputDir(packageName); - let outputDist = path.join(outputDir, "dist"); - let version = getVersion(sourceDir); - - return [ - { - external(id) { - return isBareModuleId(id); - }, - input: `${sourceDir}/index.ts`, - output: { - banner: createBanner(packageName, version), - dir: outputDist, - format: "cjs", - preserveModules: true, - exports: "named", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - nodeResolve({ extensions: [".ts", ".tsx"] }), - copy({ - targets: [ - { src: "LICENSE.md", dest: [outputDir, sourceDir] }, - { src: `${sourceDir}/package.json`, dest: outputDir }, - { src: `${sourceDir}/README.md`, dest: outputDir }, - ], - }), - magicExportsPlugin(getMagicExports(packageName), { - packageName, - version, - }), - copyToPlaygrounds(), - ], - }, - { - external(id) { - return isBareModuleId(id); - }, - input: `${sourceDir}/index.ts`, - output: { - banner: createBanner(packageName, version), - dir: `${outputDist}/esm`, - format: "esm", - preserveModules: true, - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - nodeResolve({ extensions: [".ts", ".tsx"] }), - copyToPlaygrounds(), - ], - }, - ]; -} - -/** @returns {import("rollup").RollupOptions[]} */ -function remixNode() { - let packageName = "@remix-run/node"; - let sourceDir = "packages/remix-node"; - let outputDir = getOutputDir(packageName); - let outputDist = path.join(outputDir, "dist"); - let version = getVersion(sourceDir); - - return [ - { - external(id) { - return isBareModuleId(id); - }, - input: `${sourceDir}/index.ts`, - output: { - banner: createBanner(packageName, version), - dir: outputDist, - format: "cjs", - preserveModules: true, - exports: "named", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - nodeResolve({ extensions: [".ts", ".tsx"] }), - copy({ - targets: [ - { src: "LICENSE.md", dest: [outputDir, sourceDir] }, - { src: `${sourceDir}/package.json`, dest: outputDir }, - { src: `${sourceDir}/README.md`, dest: outputDir }, - ], - }), - magicExportsPlugin(getMagicExports(packageName), { - packageName, - version, - }), - copyToPlaygrounds(), - ], - }, - ]; -} - -/** @returns {import("rollup").RollupOptions[]} */ -function remixCloudflare() { - let packageName = "@remix-run/cloudflare"; - let sourceDir = "packages/remix-cloudflare"; - let outputDir = getOutputDir(packageName); - let outputDist = path.join(outputDir, "dist"); - let version = getVersion(sourceDir); - - return [ - { - external(id) { - return isBareModuleId(id); - }, - input: `${sourceDir}/index.ts`, - output: { - banner: createBanner(packageName, version), - dir: outputDist, - format: "cjs", - preserveModules: true, - exports: "named", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - nodeResolve({ extensions: [".ts", ".tsx"] }), - copy({ - targets: [ - { src: "LICENSE.md", dest: [outputDir, sourceDir] }, - { src: `${sourceDir}/package.json`, dest: outputDir }, - { src: `${sourceDir}/README.md`, dest: outputDir }, - ], - }), - magicExportsPlugin(getMagicExports(packageName), { - packageName, - version, - }), - copyToPlaygrounds(), - ], - }, - ]; -} - -/** @returns {import("rollup").RollupOptions[]} */ -function remixDeno() { - let sourceDir = "packages/remix-deno"; - let outputDir = getOutputDir("@remix-run/deno"); - - return [ - { - input: `${sourceDir}/.empty.js`, - plugins: [ - copy({ - targets: [ - { src: "LICENSE.md", dest: [outputDir, sourceDir] }, - { src: `${sourceDir}/**/*`, dest: outputDir }, - ], - gitignore: true, - }), - copyToPlaygrounds(), - ], - }, - ]; -} - -/** @returns {import("rollup").RollupOptions[]} */ -function remixCloudflareWorkers() { - let sourceDir = "packages/remix-cloudflare-workers"; - let outputDir = getOutputDir("@remix-run/cloudflare-workers"); - let outputDist = path.join(outputDir, "dist"); - let version = getVersion(sourceDir); - - return [ - { - external(id) { - return isBareModuleId(id); - }, - input: `${sourceDir}/index.ts`, - output: { - banner: createBanner("@remix-run/cloudflare-workers", version), - dir: `${outputDist}/esm`, - format: "esm", - preserveModules: true, - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - nodeResolve({ extensions: [".ts", ".tsx"] }), - copyToPlaygrounds(), - ], - }, - ]; -} - -/** @returns {import("rollup").RollupOptions[]} */ -function remixCloudflarePages() { - let sourceDir = "packages/remix-cloudflare-pages"; - let outputDir = getOutputDir("@remix-run/cloudflare-pages"); - let outputDist = path.join(outputDir, "dist"); - let version = getVersion(sourceDir); - - return [ - { - external(id) { - return isBareModuleId(id); - }, - input: `${sourceDir}/index.ts`, - output: { - banner: createBanner("@remix-run/cloudflare-pages", version), - dir: `${outputDist}/esm`, - format: "esm", - preserveModules: true, - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - nodeResolve({ extensions: [".ts", ".tsx"] }), - copyToPlaygrounds(), - ], - }, - ]; -} - -/** - * @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 outputDist = path.join(outputDir, "dist"); - let version = getVersion(sourceDir); - - // TODO: Remove in v2 - let magicExports = getMagicExports(packageName); - - return [ - { - external(id) { - return isBareModuleId(id); - }, - input: `${sourceDir}/index.ts`, - output: { - banner: createBanner(packageName, version), - dir: outputDist, - format: "cjs", - preserveModules: true, - exports: "auto", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - nodeResolve({ extensions: [".ts", ".tsx"] }), - copy({ - targets: [ - { src: "LICENSE.md", dest: [outputDir, sourceDir] }, - { src: `${sourceDir}/package.json`, dest: outputDir }, - { src: `${sourceDir}/README.md`, dest: outputDir }, - ], - }), - magicExportsPlugin(magicExports, { - packageName, - version, - }), - copyToPlaygrounds(), - ], - }, - ]; -} - -/** - * TODO: Remove in v2 - * @param {RemixPackage} packageName - * @returns {MagicExports | null} - */ -function getMagicExports(packageName) { - // Re-export everything from packages that is available in `remix` - /** @type {Record} */ - let magicExportsByPackageName = { - "@remix-run/architect": { - values: ["createArcTableSessionStorage"], - }, - "@remix-run/cloudflare": { - values: [ - "createCloudflareKVSessionStorage", - "createCookie", - "createCookieSessionStorage", - "createMemorySessionStorage", - "createSessionStorage", - ], - }, - "@remix-run/node": { - values: [ - "createCookie", - "createCookieSessionStorage", - "createFileSessionStorage", - "createMemorySessionStorage", - "createSessionStorage", - "unstable_createFileUploadHandler", - "unstable_createMemoryUploadHandler", - "unstable_parseMultipartFormData", - ], - types: ["UploadHandler", "UploadHandlerPart"], - }, - "@remix-run/react": { - values: [ - "Form", - "Link", - "Links", - "LiveReload", - "Meta", - "NavLink", - "PrefetchPageLinks", - "RemixBrowser", - "RemixServer", - "Scripts", - "ScrollRestoration", - "useActionData", - "useBeforeUnload", - "useCatch", - "useFetcher", - "useFetchers", - "useFormAction", - "useLoaderData", - "useMatches", - "useSubmit", - "useTransition", - - // react-router-dom exports - "Outlet", - "useHref", - "useLocation", - "useNavigate", - "useNavigationType", - "useOutlet", - "useOutletContext", - "useParams", - "useResolvedPath", - "useSearchParams", - ], - types: [ - "RemixBrowserProps", - "FormProps", - "SubmitOptions", - "SubmitFunction", - "FormMethod", - "FormEncType", - "RemixServerProps", - "ShouldReloadFunction", - "ThrownResponse", - "LinkProps", - "NavLinkProps", - ], - }, - "@remix-run/server-runtime": { - values: ["createSession", "isCookie", "isSession", "json", "redirect"], - types: [ - "ActionArgs", - "ActionFunction", - "AppData", - "AppLoadContext", - "Cookie", - "CookieOptions", - "CookieParseOptions", - "CookieSerializeOptions", - "CookieSignatureOptions", - "EntryContext", - "ErrorBoundaryComponent", - "HandleDataRequestFunction", - "HandleDocumentRequestFunction", - "HeadersFunction", - "HtmlLinkDescriptor", - "HtmlMetaDescriptor", - "LinkDescriptor", - "LinksFunction", - "LoaderArgs", - "LoaderFunction", - "MetaDescriptor", - "MetaFunction", - "PageLinkDescriptor", - "RequestHandler", - "RouteComponent", - "RouteHandle", - "ServerBuild", - "ServerEntryModule", - "Session", - "SessionData", - "SessionIdStorageStrategy", - "SessionStorage", - ], - }, - }; - - return magicExportsByPackageName[packageName] || null; -} - -/** - * TODO: Remove in v2 - * @param {MagicExports | null} magicExports - * @param {{ packageName: ScopedRemixPackage; version: string }} buildInfo - * @returns {import("rollup").Plugin} - */ -const magicExportsPlugin = (magicExports, { packageName, version }) => ({ - name: `${packageName}:generate-magic-exports`, - generateBundle() { - if (!magicExports) { - return; - } - - let banner = createBanner(packageName, version); - let esmContents = banner + "\n"; - let tsContents = banner + "\n"; - let cjsContents = - banner + - "\n" + - "'use strict';\n" + - "Object.defineProperty(exports, '__esModule', { value: true });\n"; - - if (magicExports.values) { - let exportList = magicExports.values.join(", "); - esmContents += `export { ${exportList} } from '${packageName}';\n`; - tsContents += `export { ${exportList} } from '${packageName}';\n`; - - let cjsModule = camelCase(packageName.slice("@remix-run/".length)); - cjsContents += `var ${cjsModule} = require('${packageName}');\n`; - for (let symbol of magicExports.values) { - cjsContents += - `Object.defineProperty(exports, '${symbol}', {\n` + - " enumerable: true,\n" + - ` get: function () { return ${cjsModule}.${symbol}; }\n` + - "});\n"; - } - } - - if (magicExports.types) { - let exportList = magicExports.types.join(", "); - tsContents += `export type { ${exportList} } from '${packageName}';\n`; +const fs = require("fs"); +const path = require("path"); + +module.exports = function rollup(options) { + return fs.readdirSync("packages").flatMap((dir) => { + let configPath = path.join("packages", dir, "rollup.config.js"); + try { + fs.readFileSync(configPath); + } catch (e) { + return []; } - - this.emitFile({ - fileName: path.join("magicExports", "remix.d.ts"), - source: tsContents, - type: "asset", - }); - this.emitFile({ - fileName: path.join("magicExports", "remix.js"), - source: cjsContents, - type: "asset", - }); - this.emitFile({ - fileName: path.join("magicExports", "esm", "remix.js"), - source: esmContents, - type: "asset", - }); - }, -}); - -/** @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"), - ...getAdapterConfig("cloudflare-workers"), - ...getAdapterConfig("express"), - ...getAdapterConfig("netlify"), - ...getAdapterConfig("vercel"), - ]; -} - -/** @returns {import("rollup").RollupOptions[]} */ -function remixReact() { - let packageName = "@remix-run/react"; - let sourceDir = "packages/remix-react"; - let outputDir = getOutputDir(packageName); - let outputDist = path.join(outputDir, "dist"); - let version = getVersion(sourceDir); - - // This CommonJS build of remix-react is for node; both for use in running our - // server and for 3rd party tools that work with node. - /** @type {import("rollup").RollupOptions} */ - let remixReactCJS = { - external(id) { - return isBareModuleId(id); - }, - input: `${sourceDir}/index.tsx`, - output: { - banner: createBanner(packageName, version), - dir: outputDist, - format: "cjs", - preserveModules: true, - exports: "auto", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - nodeResolve({ extensions: [".ts", ".tsx"] }), - copy({ - targets: [ - { src: "LICENSE.md", dest: [outputDir, sourceDir] }, - { src: `${sourceDir}/package.json`, dest: outputDir }, - { src: `${sourceDir}/README.md`, dest: outputDir }, - ], - }), - magicExportsPlugin(getMagicExports(packageName), { - packageName, - version, - }), - copyToPlaygrounds(), - ], - }; - - // The browser build of remix-react is ESM so we can treeshake it. - /** @type {import("rollup").RollupOptions} */ - let remixReactESM = { - external(id) { - return isBareModuleId(id); - }, - input: `${sourceDir}/index.tsx`, - output: { - banner: createBanner("@remix-run/react", version), - dir: `${outputDist}/esm`, - format: "esm", - preserveModules: true, - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - nodeResolve({ extensions: [".ts", ".tsx"] }), - copyToPlaygrounds(), - ], - }; - - return [remixReactCJS, remixReactESM]; -} - -/** @returns {import("rollup").RollupOptions[]} */ -function remixServe() { - let sourceDir = "packages/remix-serve"; - let outputDir = getOutputDir("@remix-run/serve"); - let outputDist = path.join(outputDir, "dist"); - let version = getVersion(sourceDir); - - return [ - { - external(id) { - return isBareModuleId(id); - }, - input: [`${sourceDir}/index.ts`, `${sourceDir}/env.ts`], - output: { - banner: createBanner("@remix-run/serve", version), - dir: outputDist, - format: "cjs", - preserveModules: true, - exports: "auto", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts", ".tsx"], - }), - nodeResolve({ extensions: [".ts", ".tsx"] }), - copy({ - targets: [ - { src: "LICENSE.md", dest: [outputDir, sourceDir] }, - { src: `${sourceDir}/package.json`, dest: outputDir }, - { src: `${sourceDir}/README.md`, dest: outputDir }, - ], - }), - copyToPlaygrounds(), - ], - }, - { - external() { - return true; - }, - input: `${sourceDir}/cli.ts`, - output: { - banner: executableBanner + createBanner("@remix-run/serve", version), - dir: outputDist, - format: "cjs", - }, - plugins: [ - babel({ - babelHelpers: "bundled", - exclude: /node_modules/, - extensions: [".ts"], - }), - nodeResolve({ extensions: [".ts"] }), - copyToPlaygrounds(), - ], - }, - ]; -} - -export default function rollup(options) { - let builds = [ - ...createRemix(options), - // Do not blow away destination app node_modules/remix directory which is - // correct for that deploy target setup - ...(activeOutputDir === "build" ? remix(options) : []), - ...remixDev(options), - ...remixServerRuntime(options), - ...remixNode(options), - ...remixCloudflare(options), - ...remixDeno(options), - ...remixCloudflarePages(options), - ...remixCloudflareWorkers(options), - ...remixServerAdapters(options), - ...remixReact(options), - ...remixServe(options), - ]; - - return builds; -} - -async function triggerLiveReload(appDir) { - // Tickle live reload by touching the server entry - // Consider all of entry.server.{tsx,ts,jsx,js} since React may be used - // via `React.createElement` without the need for JSX. - let serverEntryPaths = [ - "entry.server.ts", - "entry.server.tsx", - "entry.server.js", - "entry.server.jsx", - ]; - let serverEntryPath = serverEntryPaths - .map((entryFile) => path.join(appDir, "app", entryFile)) - .find((entryPath) => fse.existsSync(entryPath)); - let date = new Date(); - await fs.promises.utimes(serverEntryPath, date, date); -} - -function copyToPlaygrounds() { - return { - name: "copy-to-remix-playground", - async writeBundle(options, bundle) { - if (activeOutputDir === "build") { - let playgroundsDir = path.join(__dirname, "playground"); - let playgrounds = await fs.promises.readdir(playgroundsDir); - let writtenDir = path.join(__dirname, options.dir); - for (let playground of playgrounds) { - let playgroundDir = path.join(playgroundsDir, playground); - if (!fse.statSync(playgroundDir).isDirectory()) { - continue; - } - let destDir = writtenDir.replace( - path.join(__dirname, "build"), - playgroundDir - ); - await fse.copy(writtenDir, destDir); - await triggerLiveReload(playgroundDir); - } - } else { - // If we're not building to "build" then trigger live reload on our - // external "playground" app - await triggerLiveReload(activeOutputDir); - } - }, - }; -} - -/** - * @typedef {Record<"values" | "types", string[]>} 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 - */ + let packageBuild = require(`.${path.sep}${configPath}`); + return packageBuild(options); + }); +}; diff --git a/rollup.utils.js b/rollup.utils.js new file mode 100644 index 00000000000..255c97ec1ea --- /dev/null +++ b/rollup.utils.js @@ -0,0 +1,428 @@ +const babel = require("@rollup/plugin-babel").default; +const { camelCase } = require("lodash"); +const copy = require("rollup-plugin-copy"); +const fs = require("fs"); +const fse = require("fs-extra"); +const nodeResolve = require("@rollup/plugin-node-resolve").default; +const path = require("path"); + +const REPO_ROOT_DIR = __dirname; + +let activeOutputDir = "build"; +if (process.env.REMIX_LOCAL_BUILD_DIRECTORY) { + let appDir = path.join( + process.cwd(), + process.env.REMIX_LOCAL_BUILD_DIRECTORY + ); + try { + fse.readdirSync(path.join(appDir, "node_modules")); + } catch (e) { + console.error( + "Oops! You pointed REMIX_LOCAL_BUILD_DIRECTORY to a directory that " + + "does not have a node_modules/ folder. Please `npm install` in that " + + "directory and try again." + ); + process.exit(1); + } + console.log("Writing rollup output to", appDir); + activeOutputDir = appDir; +} + +/** + * @param {string} packageName + * @param {string} version + * @param {boolean} [executable] + */ +function createBanner(packageName, version, executable = false) { + let banner = `/** + * ${packageName} v${version} + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */`; + return executable ? "#!/usr/bin/env node\n" + banner : banner; +} + +/** + * @param {string} packageDir + */ +function getVersion(packageDir) { + return require(`./${packageDir}/package.json`).version; +} + +/** + * @param {string} id + */ +function isBareModuleId(id) { + return !id.startsWith(".") && !path.isAbsolute(id); +} + +/** + * @param {string} appDir + */ +async function triggerLiveReload(appDir) { + // Tickle live reload by touching the server entry + // Consider all of entry.server.{tsx,ts,jsx,js} since React may be used + // via `React.createElement` without the need for JSX. + let serverEntryPaths = [ + "entry.server.ts", + "entry.server.tsx", + "entry.server.js", + "entry.server.jsx", + ]; + let serverEntryPath = serverEntryPaths + .map((entryFile) => path.join(appDir, "app", entryFile)) + .find((entryPath) => fse.existsSync(entryPath)); + if (serverEntryPath) { + let date = new Date(); + await fs.promises.utimes(serverEntryPath, date, date); + } +} + +/** + * @param {string} packageName + * @returns {import("rollup").Plugin} + */ +function copyPublishFiles(packageName) { + let sourceDir = `packages/${getPackageDirname(packageName)}`; + let outputDir = getOutputDir(packageName); + return copy({ + targets: [ + { src: "LICENSE.md", dest: [outputDir, sourceDir] }, + { src: `${sourceDir}/package.json`, dest: outputDir }, + { src: `${sourceDir}/README.md`, dest: outputDir }, + { src: `${sourceDir}/CHANGELOG.md`, dest: outputDir }, + ], + }); +} + +/** + * @returns {import("rollup").Plugin} + */ +function copyToPlaygrounds() { + return { + name: "copy-to-remix-playground", + async writeBundle(options, bundle) { + if (activeOutputDir === "build") { + let playgroundsDir = path.join(REPO_ROOT_DIR, "playground"); + let playgrounds = await fs.promises.readdir(playgroundsDir); + let writtenDir = path.join(REPO_ROOT_DIR, options.dir); + for (let playground of playgrounds) { + let playgroundDir = path.join(playgroundsDir, playground); + if (!fse.statSync(playgroundDir).isDirectory()) { + continue; + } + let destDir = writtenDir.replace( + path.join(REPO_ROOT_DIR, "build"), + playgroundDir + ); + await fse.copy(writtenDir, destDir); + await triggerLiveReload(playgroundDir); + } + } else { + // Otherwise, trigger live reload on our REMIX_LOCAL_BUILD_DIRECTORY folder + await triggerLiveReload(activeOutputDir); + } + }, + }; +} + +/** + * @param {RemixAdapter} adapterName + * @param {MagicExports} [magicExports] TODO: Remove in v2 + * @returns {import("rollup").RollupOptions} + */ +function getAdapterConfig(adapterName, magicExports) { + /** @type {RemixPackage} */ + let packageName = `@remix-run/${adapterName}`; + let sourceDir = `packages/remix-${adapterName}`; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + let version = getVersion(sourceDir); + + return { + external(id) { + return isBareModuleId(id); + }, + input: `${sourceDir}/index.ts`, + output: { + banner: createBanner(packageName, version), + dir: outputDist, + format: "cjs", + preserveModules: true, + exports: "auto", + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts", ".tsx"], + }), + nodeResolve({ extensions: [".ts", ".tsx"] }), + copyPublishFiles(packageName), + magicExportsPlugin({ packageName, version }), + copyToPlaygrounds(), + ], + }; +} + +/** + * TODO: Remove in v2 + * @param {{ packageName: ScopedRemixPackage; version: string }} buildInfo + * @returns {import("rollup").Plugin} + */ +function magicExportsPlugin({ packageName, version }) { + let magicExports = getMagicExports(packageName); + return { + name: `${packageName}:generate-magic-exports`, + generateBundle() { + if (!magicExports) { + return; + } + + let banner = createBanner(packageName, version); + let esmContents = banner + "\n"; + let tsContents = banner + "\n"; + let cjsContents = + banner + + "\n" + + "'use strict';\n" + + "Object.defineProperty(exports, '__esModule', { value: true });\n"; + + if (magicExports.values) { + let exportList = magicExports.values.join(", "); + esmContents += `export { ${exportList} } from '${packageName}';\n`; + tsContents += `export { ${exportList} } from '${packageName}';\n`; + + let cjsModule = camelCase(packageName.slice("@remix-run/".length)); + cjsContents += `var ${cjsModule} = require('${packageName}');\n`; + for (let symbol of magicExports.values) { + cjsContents += + `Object.defineProperty(exports, '${symbol}', {\n` + + " enumerable: true,\n" + + ` get: function () { return ${cjsModule}.${symbol}; }\n` + + "});\n"; + } + } + + if (magicExports.types) { + let exportList = magicExports.types.join(", "); + tsContents += `export type { ${exportList} } from '${packageName}';\n`; + } + + this.emitFile({ + fileName: path.join("magicExports", "remix.d.ts"), + source: tsContents, + type: "asset", + }); + this.emitFile({ + fileName: path.join("magicExports", "remix.js"), + source: cjsContents, + type: "asset", + }); + this.emitFile({ + fileName: path.join("magicExports", "esm", "remix.js"), + source: esmContents, + type: "asset", + }); + }, + }; +} + +/** + * TODO: Remove in v2 + * @param {RemixPackage} packageName + * @returns {MagicExports | null} + */ +function getMagicExports(packageName) { + // Re-export everything from packages that is available in `remix` + /** @type {Record} */ + let magicExportsByPackageName = { + "@remix-run/architect": { + values: ["createArcTableSessionStorage"], + }, + "@remix-run/cloudflare": { + values: [ + "createCloudflareKVSessionStorage", + "createCookie", + "createCookieSessionStorage", + "createMemorySessionStorage", + "createSessionStorage", + ], + }, + "@remix-run/node": { + values: [ + "createCookie", + "createCookieSessionStorage", + "createFileSessionStorage", + "createMemorySessionStorage", + "createSessionStorage", + "unstable_createFileUploadHandler", + "unstable_createMemoryUploadHandler", + "unstable_parseMultipartFormData", + ], + types: ["UploadHandler", "UploadHandlerPart"], + }, + "@remix-run/react": { + values: [ + "Form", + "Link", + "Links", + "LiveReload", + "Meta", + "NavLink", + "PrefetchPageLinks", + "RemixBrowser", + "RemixServer", + "Scripts", + "ScrollRestoration", + "useActionData", + "useBeforeUnload", + "useCatch", + "useFetcher", + "useFetchers", + "useFormAction", + "useLoaderData", + "useMatches", + "useSubmit", + "useTransition", + + // react-router-dom exports + "Outlet", + "useHref", + "useLocation", + "useNavigate", + "useNavigationType", + "useOutlet", + "useOutletContext", + "useParams", + "useResolvedPath", + "useSearchParams", + ], + types: [ + "RemixBrowserProps", + "FormProps", + "SubmitOptions", + "SubmitFunction", + "FormMethod", + "FormEncType", + "RemixServerProps", + "ShouldReloadFunction", + "ThrownResponse", + "LinkProps", + "NavLinkProps", + ], + }, + "@remix-run/server-runtime": { + values: ["createSession", "isCookie", "isSession", "json", "redirect"], + types: [ + "ActionArgs", + "ActionFunction", + "AppData", + "AppLoadContext", + "Cookie", + "CookieOptions", + "CookieParseOptions", + "CookieSerializeOptions", + "CookieSignatureOptions", + "EntryContext", + "ErrorBoundaryComponent", + "HandleDataRequestFunction", + "HandleDocumentRequestFunction", + "HeadersFunction", + "HtmlLinkDescriptor", + "HtmlMetaDescriptor", + "LinkDescriptor", + "LinksFunction", + "LoaderArgs", + "LoaderFunction", + "MetaDescriptor", + "MetaFunction", + "PageLinkDescriptor", + "RequestHandler", + "RouteComponent", + "RouteHandle", + "ServerBuild", + "ServerEntryModule", + "Session", + "SessionData", + "SessionIdStorageStrategy", + "SessionStorage", + ], + }, + }; + + return magicExportsByPackageName[packageName] || null; +} + +/** + * @param {{ packageName: string; version: string }} args + * @returns {import("rollup").RollupOptions} + */ +function getCliConfig({ packageName, version }) { + let sourceDir = `packages/${getPackageDirname(packageName)}`; + let outputDir = getOutputDir(packageName); + let outputDist = path.join(outputDir, "dist"); + return { + external() { + return true; + }, + input: `${sourceDir}/cli.ts`, + output: { + banner: createBanner(packageName, version, true), + dir: outputDist, + format: "cjs", + }, + plugins: [ + babel({ + babelHelpers: "bundled", + exclude: /node_modules/, + extensions: [".ts"], + }), + nodeResolve({ extensions: [".ts"] }), + copyPublishFiles(packageName), + copyToPlaygrounds(), + ], + }; +} + +/** + * @param {string} packageName + */ +function getOutputDir(packageName) { + return path.join(activeOutputDir, "node_modules", packageName); +} + +/** + * @param {string} packageName + */ +function getPackageDirname(packageName) { + let scope = "@remix-run/"; + return packageName.startsWith(scope) + ? `remix-${packageName.slice(scope.length)}` + : packageName; +} + +module.exports = { + copyPublishFiles, + copyToPlaygrounds, + createBanner, + getAdapterConfig, + getCliConfig, + getMagicExports, + getOutputDir, + isBareModuleId, + magicExportsPlugin, +}; + +/** + * @typedef {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/scripts/copy-build-to-dist.mjs b/scripts/copy-build-to-dist.mjs index ba3638f8307..306361a16b8 100644 --- a/scripts/copy-build-to-dist.mjs +++ b/scripts/copy-build-to-dist.mjs @@ -144,7 +144,11 @@ async function getPackageBuildPaths(moduleRootDir) { } if (path.basename(moduleDir) === "@remix-run") { packageBuilds.push(...(await getPackageBuildPaths(moduleDir))); - } else { + } else if ( + /node_modules\/@remix-run\//.test(moduleDir) || + /node_modules\/create-remix/.test(moduleDir) || + /node_modules\/remix/.test(moduleDir) + ) { packageBuilds.push(moduleDir); } } diff --git a/yarn.lock b/yarn.lock index 99ead2770c7..0d13dbe4b3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -701,7 +701,7 @@ "@babel/plugin-proposal-optional-chaining@^7.13.12": version "7.17.12" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.17.12.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.17.12.tgz#f96949e9bacace3a9066323a5cf90cfb9de67174" integrity sha512-7wigcOs/Z4YWlK7xxjkvaIw84vGhDv/P1dFGQap0nHkc8gFKY/r+hXc8Qzf5k1gY7CvGIcHqAnOagVKJJ1wVOQ== dependencies: "@babel/helper-plugin-utils" "^7.17.12" @@ -1379,7 +1379,7 @@ "@babel/preset-typescript@^7.13.0": version "7.17.12" - resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.17.12.tgz" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.17.12.tgz#40269e0a0084d56fc5731b6c40febe1c9a4a3e8c" integrity sha512-S1ViF8W2QwAKUGJXxP9NAfNaqGDdEBJKpYkxHf5Yy2C4NPPzXGeR3Lhk7G8xJaaLcFTRfNjVbtbVtm8Gb0mqvg== dependencies: "@babel/helper-plugin-utils" "^7.17.12" @@ -2344,16 +2344,23 @@ web-streams-polyfill "^3.1.1" "@rollup/plugin-babel@^5.2.2": - version "5.3.0" - resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz" - integrity sha512-9uIC8HZOnVLrLHxayq/PTzw+uS25E14KPUBh5ktF+18Mjo5yK0ToMMx6epY0uEgkjwJw0aBW4x2horYXh8juWw== + version "5.3.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" + integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== dependencies: "@babel/helper-module-imports" "^7.10.4" "@rollup/pluginutils" "^3.1.0" +"@rollup/plugin-json@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" + integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw== + dependencies: + "@rollup/pluginutils" "^3.0.8" + "@rollup/plugin-node-resolve@^11.0.1": version "11.2.1" - resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60" integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -2363,7 +2370,7 @@ is-module "^1.0.0" resolve "^1.19.0" -"@rollup/pluginutils@^3.1.0": +"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz" integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== @@ -4095,9 +4102,9 @@ buffer@^5.5.0, buffer@^5.6.0: ieee754 "^1.1.13" builtin-modules@^3.1.0: - version "3.2.0" - resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz" - integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== bytes@3.0.0: version "3.0.0" @@ -6007,7 +6014,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: fast-glob@3.2.11, fast-glob@^3.0.3, fast-glob@^3.2.9: version "3.2.11" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== dependencies: "@nodelib/fs.stat" "^2.0.2" @@ -10676,7 +10683,7 @@ rndm@1.2.0: rollup-plugin-copy@^3.3.0: version "3.4.0" - resolved "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz" + resolved "https://registry.yarnpkg.com/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz#f1228a3ffb66ffad8606e2f3fb7ff23141ed3286" integrity sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ== dependencies: "@types/fs-extra" "^8.0.1" @@ -10709,9 +10716,9 @@ rollup-pluginutils@^2.8.1: estree-walker "^0.6.1" rollup@^2.36.1: - version "2.57.0" - resolved "https://registry.npmjs.org/rollup/-/rollup-2.57.0.tgz" - integrity sha512-bKQIh1rWKofRee6mv8SrF2HdP6pea5QkwBZSMImJysFj39gQuiV8MEPBjXOCpzk3wSYp63M2v2wkWBmFC8O/rg== + version "2.75.7" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.75.7.tgz#221ff11887ae271e37dcc649ba32ce1590aaa0b9" + integrity sha512-VSE1iy0eaAYNCxEXaleThdFXqZJ42qDBatAwrfnPlENEZ8erQ+0LYX4JXOLPceWfZpV1VtZwZ3dFCuOZiSyFtQ== optionalDependencies: fsevents "~2.3.2"