diff --git a/packages/create-remix/cli.ts b/packages/create-remix/cli.ts index 39c1b895789..538708a9dde 100644 --- a/packages/create-remix/cli.ts +++ b/packages/create-remix/cli.ts @@ -3,9 +3,8 @@ import chalkAnimation from "chalk-animation"; import inquirer from "inquirer"; import meow from "meow"; -import type { Lang, Server, Stack } from "."; -import { appType } from "."; -import { createApp } from "."; +import type { Lang, PackageManager, Server, Stack } from "."; +import { appType, createApp, getInstallCommand } from "."; const help = ` Usage: @@ -69,12 +68,14 @@ async function run() { stack?: never; server: Server; lang: Lang; + packageManager: PackageManager; install: boolean; } | { appType: "stack"; stack: Stack; server?: never; + packageManager?: never; install: boolean; } >([ @@ -142,10 +143,31 @@ async function run() { { name: "JavaScript", value: "js" } ] }, + { + name: "packageManager", + type: "list", + message: "Which package manager?", + when(answers) { + return answers.appType !== appType.stack; + }, + loop: false, + choices: [ + { name: "npm", value: "npm" }, + { name: "yarn (without PnP)", value: "yarn" }, + { name: "yarn (with PnP enabled)", value: "yarn-pnp" } + ] + }, { name: "install", type: "confirm", - message: "Do you want me to run `npm install`?", + message: answers => { + const packageManager = typeof answers.packageManager === 'string' + ? answers.packageManager + : 'npm'; + + const command = getInstallCommand(packageManager); + return `Do you want me to run \`${command}\`?`; + }, default: true } ]); @@ -155,14 +177,15 @@ async function run() { projectDir, lang: "ts", stack: answers.stack, - install: answers.install + install: answers.install, }); } else { await createApp({ projectDir, lang: answers.lang, server: answers.server, - install: answers.install + install: answers.install, + packageManager: answers.packageManager, }); } } diff --git a/packages/create-remix/index.ts b/packages/create-remix/index.ts index 83338ecebaa..07d179895a9 100644 --- a/packages/create-remix/index.ts +++ b/packages/create-remix/index.ts @@ -27,6 +27,11 @@ export type AppType = typeof appType[keyof typeof appType]; export type Lang = "ts" | "js"; +export type PackageManager = + | "npm" + | "yarn" + | "yarn-pnp"; + export type CreateAppArgs = | { projectDir: string; @@ -35,6 +40,7 @@ export type CreateAppArgs = stack?: never; install: boolean; quiet?: boolean; + packageManager?: PackageManager; } | { projectDir: string; @@ -43,13 +49,15 @@ export type CreateAppArgs = stack: Stack; install: boolean; quiet?: boolean; + packageManager?: PackageManager; }; -async function createApp({ +export async function createApp({ projectDir, lang, install, quiet, + packageManager = 'npm', ...rest }: CreateAppArgs) { let server = rest.stack ? rest.stack : rest.server; @@ -95,6 +103,16 @@ async function createApp({ await fse.copy(serverLangTemplate, projectDir, { overwrite: true }); } + // copy the package manager template + const packageManagerLangTemplate = path.resolve( + __dirname, + "templates", + `${packageManager}_${lang}` + ); + if (fse.existsSync(packageManagerLangTemplate)) { + await fse.copy(packageManagerLangTemplate, projectDir, { overwrite: true }); + } + // rename dotfiles let dotfiles = ["gitignore", "github", "dockerignore", "env.example"]; await Promise.all( @@ -114,8 +132,13 @@ async function createApp({ appPkg.dependencies = appPkg.dependencies || {}; appPkg.devDependencies = appPkg.devDependencies || {}; let serverPkg = require(path.join(serverTemplate, "package.json")); + let packageManagerPkgPath = path.join(packageManagerLangTemplate, "package.json"); + let packageManagerPkg = fse.existsSync(packageManagerPkgPath) + ? require(packageManagerPkgPath) + : {}; + ["dependencies", "devDependencies", "scripts"].forEach(key => { - Object.assign(appPkg[key], serverPkg[key]); + Object.assign(appPkg[key], serverPkg[key], packageManagerPkg[key]); }); appPkg.main = serverPkg.main; @@ -133,16 +156,30 @@ async function createApp({ } }); + appPkg = sortPackageJSON(appPkg); + if (packageManager === "yarn-pnp") { + // yarn PnP does not support the remix package's magic, so we remove it from + // the dependencies and delete the postinstall hook + delete appPkg.dependencies.remix; + delete appPkg.scripts.postinstall; + } + // write package.json await fse.writeFile( path.join(projectDir, "package.json"), JSON.stringify(appPkg, null, 2) ); + // enable yarn PnP if requested + if (packageManager === "yarn-pnp") { + execSync("yarn set version berry", { stdio: "inherit", cwd: projectDir }); + } + if (install) { - execSync("npm install", { stdio: "inherit", cwd: projectDir }); + const command = getInstallCommand(packageManager); + execSync(command, { stdio: "inherit", cwd: projectDir }); } let serverScript = path.resolve(serverTemplate, "scripts/init.js"); @@ -174,4 +211,12 @@ async function createApp({ } } -export { createApp }; +export function getInstallCommand(packageManager: PackageManager): string { + switch (packageManager) { + case "npm": + return "npm install"; + case "yarn": + case "yarn-pnp": + return "yarn install"; + } +} diff --git a/packages/create-remix/templates/yarn-pnp/gitignore b/packages/create-remix/templates/yarn-pnp/gitignore new file mode 100644 index 00000000000..77678a7fd7b --- /dev/null +++ b/packages/create-remix/templates/yarn-pnp/gitignore @@ -0,0 +1,14 @@ +node_modules + +/.cache +/server/build +/public/build +.env + +.yarn/* +!.yarn/cache +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/packages/create-remix/templates/yarn-pnp_js/app/entry.client.jsx b/packages/create-remix/templates/yarn-pnp_js/app/entry.client.jsx new file mode 100644 index 00000000000..57dba485b92 --- /dev/null +++ b/packages/create-remix/templates/yarn-pnp_js/app/entry.client.jsx @@ -0,0 +1,4 @@ +import { hydrate } from "react-dom"; +import { RemixBrowser } from "@remix-run/react"; + +hydrate(, document); diff --git a/packages/create-remix/templates/yarn-pnp_js/app/entry.server.jsx b/packages/create-remix/templates/yarn-pnp_js/app/entry.server.jsx new file mode 100644 index 00000000000..2bb5cb6eb99 --- /dev/null +++ b/packages/create-remix/templates/yarn-pnp_js/app/entry.server.jsx @@ -0,0 +1,20 @@ +import { renderToString } from "react-dom/server"; +import { RemixServer } from "@remix-run/react"; + +export default function handleRequest( + request, + responseStatusCode, + responseHeaders, + remixContext +) { + const markup = renderToString( + + ); + + responseHeaders.set("Content-Type", "text/html"); + + return new Response("" + markup, { + status: responseStatusCode, + headers: responseHeaders + }); +} diff --git a/packages/create-remix/templates/yarn-pnp_js/app/root.jsx b/packages/create-remix/templates/yarn-pnp_js/app/root.jsx new file mode 100644 index 00000000000..0875683f350 --- /dev/null +++ b/packages/create-remix/templates/yarn-pnp_js/app/root.jsx @@ -0,0 +1,31 @@ +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration +} from "@remix-run/react"; + +export function meta() { + return { title: "New Remix App" }; +} + +export default function App() { + return ( + + + + + + + + + + + + {process.env.NODE_ENV === "development" && } + + + ); +} diff --git a/packages/create-remix/templates/yarn-pnp_js/package.json b/packages/create-remix/templates/yarn-pnp_js/package.json new file mode 100644 index 00000000000..e1b514bd165 --- /dev/null +++ b/packages/create-remix/templates/yarn-pnp_js/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "@remix-run/serve": "*", + "@remix-run/server-runtime": "*" + }, + "devDependencies": { + "esbuild": ">=0.10.0", + "@yarnpkg/esbuild-plugin-pnp": "^2.0.0" + } +} diff --git a/packages/create-remix/templates/yarn-pnp_ts/app/entry.client.tsx b/packages/create-remix/templates/yarn-pnp_ts/app/entry.client.tsx new file mode 100644 index 00000000000..57dba485b92 --- /dev/null +++ b/packages/create-remix/templates/yarn-pnp_ts/app/entry.client.tsx @@ -0,0 +1,4 @@ +import { hydrate } from "react-dom"; +import { RemixBrowser } from "@remix-run/react"; + +hydrate(, document); diff --git a/packages/create-remix/templates/yarn-pnp_ts/app/entry.server.tsx b/packages/create-remix/templates/yarn-pnp_ts/app/entry.server.tsx new file mode 100644 index 00000000000..897f15d5787 --- /dev/null +++ b/packages/create-remix/templates/yarn-pnp_ts/app/entry.server.tsx @@ -0,0 +1,21 @@ +import { renderToString } from "react-dom/server"; +import { RemixServer } from "@remix-run/react"; +import type { EntryContext } from "@remix-run/server-runtime"; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + const markup = renderToString( + + ); + + responseHeaders.set("Content-Type", "text/html"); + + return new Response("" + markup, { + status: responseStatusCode, + headers: responseHeaders + }); +} diff --git a/packages/create-remix/templates/yarn-pnp_ts/app/root.tsx b/packages/create-remix/templates/yarn-pnp_ts/app/root.tsx new file mode 100644 index 00000000000..6be522214ab --- /dev/null +++ b/packages/create-remix/templates/yarn-pnp_ts/app/root.tsx @@ -0,0 +1,32 @@ +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration +} from "@remix-run/react"; +import type { MetaFunction } from "@remix-run/server-runtime"; + +export const meta: MetaFunction = () => { + return { title: "New Remix App" }; +}; + +export default function App() { + return ( + + + + + + + + + + + + {process.env.NODE_ENV === "development" && } + + + ); +} diff --git a/packages/create-remix/templates/yarn-pnp_ts/package.json b/packages/create-remix/templates/yarn-pnp_ts/package.json new file mode 100644 index 00000000000..c49392d819e --- /dev/null +++ b/packages/create-remix/templates/yarn-pnp_ts/package.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "@remix-run/serve": "*", + "@remix-run/server-runtime": "*" + }, + "devDependencies": { + "esbuild": ">=0.10.0", + "@types/node": "^17.0.8", + "@yarnpkg/esbuild-plugin-pnp": "^2.0.0" + } +}