Repository: remix-run/remix
Pull request #3076 (2022-05-03 15:20:55)

feat(remix-dev): pass `packageManager` to `remix.init` script
This way, the `remix.init` script can call `npm/yarn/pnpm install` if needed.

The use-case I'm thinking about is having different dependencies when using JavaScript or TypeScript.


Patch:
diff --git a/integration/helpers/cf-template/app/entry.client.jsx b/integration/helpers/cf-template/app/entry.client.tsx
similarity index 100%
rename from integration/helpers/cf-template/app/entry.client.jsx
rename to integration/helpers/cf-template/app/entry.client.tsx
diff --git a/integration/helpers/node-template/app/entry.server.jsx b/integration/helpers/cf-template/app/entry.server.tsx
similarity index 71%
rename from integration/helpers/node-template/app/entry.server.jsx
rename to integration/helpers/cf-template/app/entry.server.tsx
index d967f467f..7a06117a3 100644
--- a/integration/helpers/node-template/app/entry.server.jsx
+++ b/integration/helpers/cf-template/app/entry.server.tsx
@@ -1,11 +1,12 @@
+import type { EntryContext } from "@remix-run/cloudflare";
 import { RemixServer } from "@remix-run/react";
 import { renderToString } from "react-dom/server";
 
 export default function handleRequest(
-  request,
-  responseStatusCode,
-  responseHeaders,
-  remixContext
+  request: Request,
+  responseStatusCode: number,
+  responseHeaders: Headers,
+  remixContext: EntryContext
 ) {
   let markup = renderToString(
     <RemixServer context={remixContext} url={request.url} />
diff --git a/integration/helpers/node-template/app/root.jsx b/integration/helpers/cf-template/app/root.tsx
similarity index 82%
rename from integration/helpers/node-template/app/root.jsx
rename to integration/helpers/cf-template/app/root.tsx
index c0488f6e7..43a72817b 100644
--- a/integration/helpers/node-template/app/root.jsx
+++ b/integration/helpers/cf-template/app/root.tsx
@@ -1,3 +1,4 @@
+import type { MetaFunction } from "@remix-run/cloudflare";
 import {
   Links,
   LiveReload,
@@ -7,7 +8,7 @@ import {
   ScrollRestoration,
 } from "@remix-run/react";
 
-export const meta = () => ({
+export const meta: MetaFunction = () => ({
   charset: "utf-8",
   title: "New Remix App",
   viewport: "width=device-width,initial-scale=1",
diff --git a/integration/helpers/cf-template/package.json b/integration/helpers/cf-template/package.json
index 746f08b2f..b6c4d4605 100644
--- a/integration/helpers/cf-template/package.json
+++ b/integration/helpers/cf-template/package.json
@@ -7,14 +7,18 @@
   "main": "build/index.js",
   "scripts": {},
   "dependencies": {
+    "@remix-run/cloudflare": "0.0.0-local-version",
     "@remix-run/cloudflare-workers": "0.0.0-local-version",
     "@remix-run/react": "0.0.0-local-version",
     "react": "0.0.0-local-version",
     "react-dom": "0.0.0-local-version"
   },
   "devDependencies": {
+    "@cloudflare/workers-types": "0.0.0-local-version",
     "@remix-run/dev": "0.0.0-local-version",
-    "@remix-run/eslint-config": "0.0.0-local-version"
+    "@types/react": "0.0.0-local-version",
+    "@types/react-dom": "0.0.0-local-version",
+    "typescript": "0.0.0-local-version"
   },
   "engines": {
     "node": ">=14"
diff --git a/integration/helpers/cf-template/remix.env.d.ts b/integration/helpers/cf-template/remix.env.d.ts
new file mode 100644
index 000000000..71ee668d0
--- /dev/null
+++ b/integration/helpers/cf-template/remix.env.d.ts
@@ -0,0 +1,3 @@
+/// <reference types="@remix-run/dev" />
+/// <reference types="@remix-run/cloudflare-workers/globals" />
+/// <reference types="@cloudflare/workers-types" />
diff --git a/integration/helpers/cf-template/jsconfig.json b/integration/helpers/cf-template/tsconfig.json
similarity index 100%
rename from integration/helpers/cf-template/jsconfig.json
rename to integration/helpers/cf-template/tsconfig.json
diff --git a/integration/helpers/node-template/app/entry.client.jsx b/integration/helpers/node-template/app/entry.client.tsx
similarity index 100%
rename from integration/helpers/node-template/app/entry.client.jsx
rename to integration/helpers/node-template/app/entry.client.tsx
diff --git a/integration/helpers/cf-template/app/entry.server.jsx b/integration/helpers/node-template/app/entry.server.tsx
similarity index 72%
rename from integration/helpers/cf-template/app/entry.server.jsx
rename to integration/helpers/node-template/app/entry.server.tsx
index 1103ff6c7..aa0aa0978 100644
--- a/integration/helpers/cf-template/app/entry.server.jsx
+++ b/integration/helpers/node-template/app/entry.server.tsx
@@ -1,11 +1,12 @@
-import { renderToString } from "react-dom/server";
+import type { EntryContext } from "@remix-run/node";
 import { RemixServer } from "@remix-run/react";
+import { renderToString } from "react-dom/server";
 
 export default function handleRequest(
-  request,
-  responseStatusCode,
-  responseHeaders,
-  remixContext
+  request: Request,
+  responseStatusCode: number,
+  responseHeaders: Headers,
+  remixContext: EntryContext
 ) {
   let markup = renderToString(
     <RemixServer context={remixContext} url={request.url} />
diff --git a/integration/helpers/cf-template/app/root.jsx b/integration/helpers/node-template/app/root.tsx
similarity index 83%
rename from integration/helpers/cf-template/app/root.jsx
rename to integration/helpers/node-template/app/root.tsx
index c0488f6e7..927a0f745 100644
--- a/integration/helpers/cf-template/app/root.jsx
+++ b/integration/helpers/node-template/app/root.tsx
@@ -1,3 +1,4 @@
+import type { MetaFunction } from "@remix-run/node";
 import {
   Links,
   LiveReload,
@@ -7,7 +8,7 @@ import {
   ScrollRestoration,
 } from "@remix-run/react";
 
-export const meta = () => ({
+export const meta: MetaFunction = () => ({
   charset: "utf-8",
   title: "New Remix App",
   viewport: "width=device-width,initial-scale=1",
diff --git a/integration/helpers/node-template/package.json b/integration/helpers/node-template/package.json
index c546928fd..c54f021fc 100644
--- a/integration/helpers/node-template/package.json
+++ b/integration/helpers/node-template/package.json
@@ -17,6 +17,12 @@
     "react-dom": "0.0.0-local-version"
   },
   "devDependencies": {
-    "@remix-run/dev": "0.0.0-local-version"
+    "@remix-run/dev": "0.0.0-local-version",
+    "@types/react": "0.0.0-local-version",
+    "@types/react-dom": "0.0.0-local-version",
+    "typescript": "0.0.0-local-version"
+  },
+  "engines": {
+    "node": ">=14"
   }
 }
diff --git a/integration/helpers/node-template/remix.env.d.ts b/integration/helpers/node-template/remix.env.d.ts
new file mode 100644
index 000000000..72e2affe3
--- /dev/null
+++ b/integration/helpers/node-template/remix.env.d.ts
@@ -0,0 +1,2 @@
+/// <reference types="@remix-run/dev" />
+/// <reference types="@remix-run/node/globals" />
diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts
index 6df0361a8..3201296fd 100644
--- a/packages/remix-dev/cli/commands.ts
+++ b/packages/remix-dev/cli/commands.ts
@@ -64,17 +64,21 @@ export async function init(
 ) {
   let initScriptDir = path.join(projectDir, "remix.init");
   let initScript = path.resolve(initScriptDir, "index.js");
+  let initPackageJson = path.resolve(initScriptDir, "package.json");
 
   let isTypeScript = fse.existsSync(path.join(projectDir, "tsconfig.json"));
 
   if (await fse.pathExists(initScript)) {
-    execSync(`${packageManager} install`, {
-      stdio: "ignore",
-      cwd: initScriptDir,
-    });
+    if (await fse.pathExists(initPackageJson)) {
+      execSync(`${packageManager} install`, {
+        stdio: "ignore",
+        cwd: initScriptDir,
+      });
+    }
+
     let initFn = require(initScript);
     try {
-      await initFn({ rootDirectory: projectDir, isTypeScript });
+      await initFn({ isTypeScript, packageManager, rootDirectory: projectDir });
     } catch (error) {
       if (error instanceof Error) {
         error.message = `${colors.error("🚨 Oops, remix.init failed")}\n\n${
diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts
index 903c81d73..1a52600a8 100644
--- a/packages/remix-dev/cli/run.ts
+++ b/packages/remix-dev/cli/run.ts
@@ -19,12 +19,11 @@ import { validateNewProjectPath, validateTemplate } from "./create";
  * that can be used to determine which package manager ran
  * the command.
  */
-function getPreferredPackageManager() {
-  return ((process.env.npm_user_agent ?? "").split("/")[0] || "npm") as
+const getPreferredPackageManager = () =>
+  ((process.env.npm_config_user_agent ?? "").split("/")[0] || "npm") as
     | "npm"
     | "yarn"
     | "pnpm";
-}
 
 const helpText = `
 ${colors.logoBlue("R")} ${colors.logoGreen("E")} ${colors.logoYellow(


Test patch:
diff --git a/integration/helpers/node-template/jsconfig.json b/integration/helpers/node-template/tsconfig.json
similarity index 100%
rename from integration/helpers/node-template/jsconfig.json
rename to integration/helpers/node-template/tsconfig.json
diff --git a/packages/remix-dev/__tests__/create-test.ts b/packages/remix-dev/__tests__/create-test.ts
index 571e29a1b..b0f09fdbd 100644
--- a/packages/remix-dev/__tests__/create-test.ts
+++ b/packages/remix-dev/__tests__/create-test.ts
@@ -450,8 +450,8 @@ describe("the create command", () => {
   });
 
   it("recognizes when Yarn was used to run the command", async () => {
-    let originalUserAgent = process.env.npm_user_agent;
-    process.env.npm_user_agent = yarnUserAgent;
+    let originalUserAgent = process.env.npm_config_user_agent;
+    process.env.npm_config_user_agent = yarnUserAgent;
 
     let projectDir = await getProjectDir("yarn-create");
     await run([
@@ -464,12 +464,12 @@ describe("the create command", () => {
     ]);
 
     expect(execSync).toBeCalledWith("yarn install", expect.anything());
-    process.env.npm_user_agent = originalUserAgent;
+    process.env.npm_config_user_agent = originalUserAgent;
   });
 
   it("recognizes when pnpm was used to run the command", async () => {
-    let originalUserAgent = process.env.npm_user_agent;
-    process.env.npm_user_agent = pnpmUserAgent;
+    let originalUserAgent = process.env.npm_config_user_agent;
+    process.env.npm_config_user_agent = pnpmUserAgent;
 
     let projectDir = await getProjectDir("pnpm-create");
     await run([
@@ -482,12 +482,12 @@ describe("the create command", () => {
     ]);
 
     expect(execSync).toBeCalledWith("pnpm install", expect.anything());
-    process.env.npm_user_agent = originalUserAgent;
+    process.env.npm_config_user_agent = originalUserAgent;
   });
 
   it("prompts to run the install command for the preferred package manager", async () => {
-    let originalUserAgent = process.env.npm_user_agent;
-    process.env.npm_user_agent = pnpmUserAgent;
+    let originalUserAgent = process.env.npm_config_user_agent;
+    process.env.npm_config_user_agent = pnpmUserAgent;
 
     let projectDir = await getProjectDir("pnpm-prompt-install");
     let mockPrompt = jest.mocked(inquirer.prompt);
@@ -512,12 +512,12 @@ describe("the create command", () => {
       "message",
       "Do you want me to run `pnpm install`?"
     );
-    process.env.npm_user_agent = originalUserAgent;
+    process.env.npm_config_user_agent = originalUserAgent;
   });
 
   it("suggests to run the init command with the preferred package manager", async () => {
-    let originalUserAgent = process.env.npm_user_agent;
-    process.env.npm_user_agent = pnpmUserAgent;
+    let originalUserAgent = process.env.npm_config_user_agent;
+    process.env.npm_config_user_agent = pnpmUserAgent;
 
     let projectDir = await getProjectDir("pnpm-suggest-install");
     let mockPrompt = jest.mocked(inquirer.prompt);
@@ -537,7 +537,7 @@ describe("the create command", () => {
     ]);
 
     expect(output).toContain(getOptOutOfInstallMessage("pnpm exec remix init"));
-    process.env.npm_user_agent = originalUserAgent;
+    process.env.npm_config_user_agent = originalUserAgent;
   });
 
   describe("errors", () => {
@@ -702,14 +702,11 @@ function getSuccessMessage(projectDirectory: string) {
   return `💿 That's it! \`cd\` into "${projectDirectory}" and check the README for development and deploy instructions!`;
 }
 
-function getOptOutOfInstallMessage(command = "npx remix init") {
-  return (
-    "💿 You've opted out of installing dependencies so we won't run the " +
-    path.join("remix.init", "index.js") +
-    " script for you just yet. Once you've installed dependencies, you can run " +
-    `it manually with \`${command}\``
-  );
-}
+const getOptOutOfInstallMessage = (command = "yarn remix init") =>
+  "💿 You've opted out of installing dependencies so we won't run the " +
+  path.join("remix.init", "index.js") +
+  " script for you just yet. Once you've installed dependencies, you can run " +
+  `it manually with \`${command}\``;
 
 /*
 eslint