remix-run/react-router · error · CopyTemplateError

The provided template is not a valid local directory or tarb

Error message

The provided template is not a valid local directory or tarball.

What it means

Thrown by copyTemplateFromLocalFilePath() after the path is confirmed to exist but is neither a .tar.gz/.tgz tarball nor a directory. The function first checks the extension for tarball extraction, then fs.statSync().isDirectory() for a direct copy; any other file type (a regular file, a symlink to a file, etc.) reaches the throw. It indicates the local template target is an individual file rather than a project tree.

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 1fd704a7da)

Solutions

  1. Point --template at the directory containing the project, not a file inside it: --template ./path/to/template-dir.
  2. If the template is a compressed archive, repackage it as .tar.gz or .tgz (tar -czf template.tar.gz -C template-dir .) and pass that path.
  3. If you only have a .zip, extract it first (unzip template.zip) then pass the extracted directory.
  4. Confirm the target with ls -la <path> and ensure it shows 'directory' (d prefix) before rerunning.

Example fix

// before
create-react-router my-app --template ./template.zip
// after (extract first, then point at the directory)
unzip template.zip -d template-dir
create-react-router my-app --template ./template-dir
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs';
function isLocalDirOrTarball(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 (!isLocalDirOrTarball(template)) error before running the CLI

Type guard

function isLocalTemplate(p: string): boolean {
  return (p.endsWith('.tar.gz') || p.endsWith('.tgz')) || (fs.existsSync(p) && fs.statSync(p).isDirectory());
}

Prevention

When it happens

Trigger: Passing --template ./README.md, --template ./package.json, or any single regular file that does not end in .tar.gz/.tgz. Also a broken symlink whose target resolves to a file, or a path whose extension was expected to be a tarball but was renamed (e.g. template.zip).

Common situations: User selects a file in their editor and pastes its path; template was zipped as .zip instead of .tar.gz; the template directory was replaced by a file of the same name; pointing at a package.json thinking it is the project root.

Related errors


AI-assisted analysis of remix-run/react-router@1fd704a7da (2026-08-12). Data as JSON: /api/errors/0c9df3d8c9303573. Report an issue: GitHub.