remix-run/react-router · error · CopyTemplateError
The provided template is not a valid local directory or…
Error message
The provided template is not a valid local directory or tarball.
What it means
copyTemplateFromLocalFilePath() throws this when the template resolved to an existing local path that is neither a .tar.gz/.tgz tarball nor a directory. The path existed (that is why it was routed to the local-file branch), but fs.statSync() shows it is a regular file with an unsupported extension — only directories and gzip tarballs are usable as local templates.
Solutions
- If the template is an archive, re-pack it as a gzip tarball: tar -czf template.tar.gz -C template-dir .
- If it is a directory template, point --template at the directory itself
- Unzip the file and pass the extracted directory as the template
- Check the extension is exactly .tar.gz or .tgz
Example fix
# before npx create-react-router my-app --template ./template.zip # after (repack as tar.gz) tar -czf template.tar.gz -C template . npx create-react-router my-app --template ./template.tar.gz
Defensive patterns
Strategy: validation
Validate before calling
import fs from "node:fs";
function isUsableLocalTemplate(p: string): boolean {
if (!fs.existsSync(p)) return false;
if (p.endsWith(".tar.gz") || p.endsWith(".tgz")) return true;
try {
return fs.statSync(p).isDirectory();
} catch {
return false;
}
}
if (!isUsableLocalTemplate(templatePath)) {
throw new Error("Local template must be a directory or a .tar.gz/.tgz tarball");
} Type guard
function isLocalTemplateCandidate(p: string): p is string {
return isUsableLocalTemplate(p);
} Try / catch
try {
await copyTemplate(templatePath, dest, { onError: (e) => { throw e; } });
} catch (e) {
if (e instanceof CopyTemplateError && /not a valid local directory or tarball/.test(e.message)) {
console.error(`Repack ${templatePath} as .tar.gz or point at its extracted directory`);
process.exit(1);
}
throw e;
} Prevention
- Standardize team templates on .tar.gz produced by tar -czf, not zip
- Script template generation so the format is always directory or gzip tarball
- Remember GitHub 'Source code (zip)' links are not usable directly
When it happens
Trigger: Running with --template ./template.zip (zip is not supported); --template template.tar (plain tar, only .tar.gz/.tgz are recognized); --template pointing at any single regular file such as a .json, .md, or .tgz.txt.
Common situations: Downloading GitHub's 'Source code (zip)' archive instead of tar.gz; zipping a template directory with OS tools; renaming archives without converting their format.
Related errors
- " " is an invalid template. Run to see supported template…
- File collisions detected in a non-interactive environment
- package.json does not exist in
- package.json are invalid
- The path " " was not found in this
AI-assisted analysis of remix-run/react-router@6beaca3952 (2026-08-18).
Data as JSON: /api/errors/0c9df3d8c9303573.
Report an issue: GitHub.
Appendix: source
Thrown at packages/create-react-router/copy-template.ts:139
) {
await copyTemplateFromRemoteTarball(url, destPath, options);
}
async function copyTemplateFromLocalFilePath(
filePath: string,
destPath: string,
): Promise<boolean> {
if (filePath.endsWith(".tar.gz") || filePath.endsWith(".tgz")) {
await extractLocalTarball(filePath, destPath);
return false;
}
if (fs.statSync(filePath).isDirectory()) {
// If our template is just a directory on disk, return true here, and we'll
// just copy directly from there instead of "extracting" to a temp
// directory first
return true;
}
throw new CopyTemplateError(
"The provided template is not a valid local directory or tarball.",
);
}
const pipeline = promisify(stream.pipeline);
async function extractLocalTarball(
tarballPath: string,
destPath: string,
): Promise<void> {
try {
await pipeline(
fs.createReadStream(tarballPath),
gunzip(),
tar.extract(destPath, { strip: 1 }),
);
} catch (error: unknown) {
throw new CopyTemplateError(View on GitHub (pinned to 6beaca3952)