Files
Arian Tron 61f56f997c
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
first commit
2026-03-10 19:37:31 +03:30

393 lines
12 KiB
TypeScript

import { install } from "../helpers/install";
import { runTypegen } from "../helpers/typegen";
import { copy } from "../helpers/copy";
import { getPnpmMajorVersion } from "../helpers/get-pkg-manager";
import { async as glob } from "fast-glob";
import os from "os";
import fs from "fs/promises";
import path from "path";
import { cyan, bold } from "picocolors";
import { Sema } from "async-sema";
import pkg from "../package.json";
import { Bundler, GetTemplateFileArgs, InstallTemplateArgs } from "./types";
// Do not rename or format. sync-react script relies on this line.
// prettier-ignore
const nextjsReactPeerVersion = "19.2.4";
function sorted(obj: Record<string, string>) {
return Object.keys(obj)
.sort()
.reduce((acc: Record<string, string>, key) => {
acc[key] = obj[key];
return acc;
}, {});
}
/**
* Get the file path for a given file in a template, e.g. "next.config.js".
*/
export const getTemplateFile = ({
template,
mode,
file,
}: GetTemplateFileArgs): string => {
return path.join(__dirname, template, mode, file);
};
export const SRC_DIR_NAMES = ["app", "pages", "styles"];
/**
* Install a Next.js internal template to a given `root` directory.
*/
export const installTemplate = async ({
appName,
root,
packageManager,
isOnline,
template,
mode,
tailwind,
eslint,
biome,
srcDir,
importAlias,
skipInstall,
bundler,
reactCompiler,
}: InstallTemplateArgs) => {
console.log(bold(`Using ${packageManager}.`));
/**
* Copy the template files to the target directory.
*/
console.log("\nInitializing project with template:", template, "\n");
const isApi = template === "app-api";
const templatePath = path.join(__dirname, template, mode);
const copySource = ["**"];
if (!eslint) copySource.push("!eslint.config.mjs");
if (!biome) copySource.push("!biome.json");
if (!tailwind) copySource.push("!postcss.config.mjs");
await copy(copySource, root, {
parents: true,
cwd: templatePath,
rename(name) {
switch (name) {
case "gitignore": {
return `.${name}`;
}
// README.md is ignored by webpack-asset-relocator-loader used by ncc:
// https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
case "README-template.md": {
return "README.md";
}
default: {
return name;
}
}
},
});
if (bundler === Bundler.Rspack) {
const nextConfigFile = path.join(
root,
mode === "js" ? "next.config.mjs" : "next.config.ts",
);
await fs.writeFile(
nextConfigFile,
`import withRspack from "next-rspack";\n\n` +
(await fs.readFile(nextConfigFile, "utf8")).replace(
"export default nextConfig;",
"export default withRspack(nextConfig);",
),
);
}
if (reactCompiler) {
const nextConfigFile = path.join(
root,
mode === "js" ? "next.config.mjs" : "next.config.ts",
);
let configContent = await fs.readFile(nextConfigFile, "utf8");
configContent = configContent.replace(
"/* config options here */\n",
"/* config options here */\n reactCompiler: true,\n",
);
await fs.writeFile(nextConfigFile, configContent);
}
const tsconfigFile = path.join(
root,
mode === "js" ? "jsconfig.json" : "tsconfig.json",
);
await fs.writeFile(
tsconfigFile,
(await fs.readFile(tsconfigFile, "utf8"))
.replace(
`"@/*": ["./*"]`,
srcDir ? `"@/*": ["./src/*"]` : `"@/*": ["./*"]`,
)
.replace(`"@/*":`, `"${importAlias}":`),
);
// update import alias in any files if not using the default
if (importAlias !== "@/*") {
const files = await glob("**/*", {
cwd: root,
dot: true,
stats: false,
// We don't want to modify compiler options in [ts/js]config.json
// and none of the files in the .git folder
// TODO: Refactor this to be an allowlist, rather than a denylist,
// to avoid corrupting files that weren't intended to be replaced
ignore: [
"tsconfig.json",
"jsconfig.json",
".git/**/*",
"**/fonts/**",
"**/favicon.ico",
],
});
const writeSema = new Sema(8, { capacity: files.length });
await Promise.all(
files.map(async (file) => {
await writeSema.acquire();
const filePath = path.join(root, file);
if ((await fs.stat(filePath)).isFile()) {
await fs.writeFile(
filePath,
(await fs.readFile(filePath, "utf8")).replace(
`@/`,
`${importAlias.replace(/\*/g, "")}`,
),
);
}
writeSema.release();
}),
);
}
if (srcDir) {
await fs.mkdir(path.join(root, "src"), { recursive: true });
await Promise.all(
SRC_DIR_NAMES.map(async (file) => {
await fs
.rename(path.join(root, file), path.join(root, "src", file))
.catch((err) => {
if (err.code !== "ENOENT") {
throw err;
}
});
}),
);
if (!isApi) {
const isAppTemplate = template.startsWith("app");
// Change the `Get started by editing pages/index` / `app/page` to include `src`
const indexPageFile = path.join(
"src",
isAppTemplate ? "app" : "pages",
`${isAppTemplate ? "page" : "index"}.${mode === "ts" ? "tsx" : "js"}`,
);
await fs.writeFile(
indexPageFile,
(await fs.readFile(indexPageFile, "utf8")).replace(
isAppTemplate ? "app/page" : "pages/index",
isAppTemplate ? "src/app/page" : "src/pages/index",
),
);
}
}
/** Copy the version from package.json or override for tests. */
const version = process.env.NEXT_PRIVATE_TEST_VERSION ?? pkg.version;
const bundlerFlags = bundler === Bundler.Webpack ? " --webpack" : "";
/** Create a package.json for the new project and write it to disk. */
const packageJson: any = {
name: appName,
version: "0.1.0",
private: true,
scripts: {
dev: `next dev${bundlerFlags}`,
build: `next build${bundlerFlags}`,
start: "next start",
...(eslint && { lint: "eslint" }),
...(biome && { lint: "biome check", format: "biome format --write" }),
},
/**
* Default dependencies.
*/
dependencies: {
react: nextjsReactPeerVersion,
"react-dom": nextjsReactPeerVersion,
next: version,
},
devDependencies: {},
};
if (bundler === Bundler.Rspack) {
const NEXT_PRIVATE_TEST_VERSION = process.env.NEXT_PRIVATE_TEST_VERSION;
if (
NEXT_PRIVATE_TEST_VERSION &&
path.isAbsolute(NEXT_PRIVATE_TEST_VERSION)
) {
packageJson.dependencies["next-rspack"] = path.resolve(
path.dirname(NEXT_PRIVATE_TEST_VERSION),
"../next-rspack/next-rspack-packed.tgz",
);
} else {
packageJson.dependencies["next-rspack"] = version;
}
}
if (reactCompiler) {
packageJson.devDependencies["babel-plugin-react-compiler"] = "1.0.0";
}
/**
* TypeScript projects will have type definitions and other devDependencies.
*/
if (mode === "ts") {
packageJson.devDependencies = {
...packageJson.devDependencies,
typescript: "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
};
}
/* Add Tailwind CSS dependencies. */
if (tailwind) {
packageJson.devDependencies = {
...packageJson.devDependencies,
"@tailwindcss/postcss": "^4",
tailwindcss: "^4",
};
}
/* Default ESLint dependencies. */
if (eslint) {
packageJson.devDependencies = {
...packageJson.devDependencies,
eslint: "^9",
"eslint-config-next": version,
};
}
/* Biome dependencies. */
if (biome) {
packageJson.devDependencies = {
...packageJson.devDependencies,
"@biomejs/biome": "2.2.0",
};
}
if (isApi) {
delete packageJson.dependencies.react;
delete packageJson.dependencies["react-dom"];
// We cannot delete `@types/react` now since it is used in
// route type definitions e.g. `.next/types/app/page.ts`.
// TODO(jiwon): Implement this when we added logic to
// auto-install `react` and `react-dom` if page.tsx was used.
// We can achieve this during verify-typescript stage and see
// if a type error was thrown at `distDir/types/app/page.ts`.
delete packageJson.devDependencies["@types/react-dom"];
// Remove linting scripts for API-only templates
delete packageJson.scripts.lint;
delete packageJson.scripts.format;
}
const devDeps = Object.keys(packageJson.devDependencies).length;
if (!devDeps) delete packageJson.devDependencies;
// Sort dependencies and devDependencies alphabetically
if (packageJson.dependencies) {
packageJson.dependencies = sorted(packageJson.dependencies);
}
if (packageJson.devDependencies) {
packageJson.devDependencies = sorted(packageJson.devDependencies);
}
if (packageManager === "pnpm") {
// Only create pnpm-workspace.yaml for pnpm v10+.
// In v9, having a pnpm-workspace.yaml (even with packages: []) causes
// ERR_PNPM_ADDING_TO_ROOT errors when running `pnpm add`.
// In v10, the packages field can be omitted entirely.
// If we can't determine the version, assume latest (v10+) since we already
// know pnpm is being used at this point.
const pnpmMajorVersion = getPnpmMajorVersion();
if (pnpmMajorVersion === null || pnpmMajorVersion >= 10) {
const pnpmWorkspaceYaml = [
"ignoredBuiltDependencies:",
// Sharp has prebuilt binaries for the platforms next-swc has binaries.
// If it needs to build binaries from source, next-swc wouldn't work either.
// See https://sharp.pixelplumbing.com/install/#:~:text=When%20using%20pnpm%2C%20add%20sharp%20to%20ignoredBuiltDependencies%20to%20silence%20warnings
" - sharp",
// Not needed for pnpm: https://github.com/unrs/unrs-resolver/issues/193#issuecomment-3295510146
" - unrs-resolver",
"",
].join(os.EOL);
await fs.writeFile(
path.join(root, "pnpm-workspace.yaml"),
pnpmWorkspaceYaml,
);
}
}
if (packageManager === "bun") {
// Equivalent to pnpm's `ignoredBuiltDependencies`, added in bun 1.3.2.
// - https://bun.com/blog/bun-v1.3.2#faster-bun-install
// - https://github.com/oven-sh/bun/pull/24283
// Bun ignores `sharp` by default, but does not ignore `unrs-resolver`
// unless configured.
packageJson.ignoreScripts = ["sharp", "unrs-resolver"];
// The script must be in *both* `ignoreScripts` and `trustedDependencies` to
// suppress the warning. This could change in future versions of Bun.
// https://vercel.slack.com/archives/C06DNAH5LSG/p1763582930218709?thread_ts=1763580178.004169&cid=C06DNAH5LSG
packageJson.trustedDependencies = ["sharp", "unrs-resolver"];
}
await fs.writeFile(
path.join(root, "package.json"),
JSON.stringify(packageJson, null, 2) + os.EOL,
);
if (skipInstall) return;
console.log("\nInstalling dependencies:");
for (const dependency in packageJson.dependencies)
console.log(`- ${cyan(dependency)}`);
if (devDeps) {
console.log("\nInstalling devDependencies:");
for (const dependency in packageJson.devDependencies)
console.log(`- ${cyan(dependency)}`);
}
console.log();
await install(packageManager, isOnline);
try {
console.log();
await runTypegen(packageManager);
console.log();
} catch (err) {
console.error("Error running typegen:", err);
// Best effort: do not fail app creation if typegen fails
}
};
export * from "./types";