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

  1. If the template is an archive, re-pack it as a gzip tarball: tar -czf template.tar.gz -C template-dir .
  2. If it is a directory template, point --template at the directory itself
  3. Unzip the file and pass the extracted directory as the template
  4. 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

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


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)