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
393 lines
12 KiB
TypeScript
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";
|