[Fix] Move scaffolded project to its final directory before installing deps in app init#7600
[Fix] Move scaffolded project to its final directory before installing deps in app init#7600craigmichaelmartin wants to merge 2 commits into
app init#7600Conversation
|
/snapit |
|
🫰✨ Thanks @craigmichaelmartin! Your snapshot has been published to npm. Test the snapshot by installing your package globally: pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260521190126Caution After installing, validate the version by running |
…g deps in `app init` Run the dependency install, cleanup, and git-init steps of `shopify app init` against the final output directory instead of a temporary scaffold that is moved into place afterwards. pnpm (and similarly affected package managers) create absolute-path junctions/symlinks on Windows for their virtual store, so installing in the temp dir and then moving the tree orphans every link under `node_modules/.pnpm/*`, forcing every Windows + pnpm user to manually `rm -rf node_modules && pnpm install` before doing anything with the new project. Pre-install steps (template download, liquid render, package.json edits, .gitignore/.npmrc tweaks) still run in the temp directory so a failure there leaves no half-baked project on disk.
a565a89 to
45b1884
Compare
| await ensureAppDirectoryIsAvailable(outputDirectory, hyphenizedName) | ||
| await moveFile(templateScaffoldDir, outputDirectory) | ||
| }, | ||
| }) |
There was a problem hiding this comment.
so the initial reasoning of moving the template AFTER installing dependencies is to prevent leaving an app in a bad state if the installing fails.
For instance the cleanup and initializeGitRepository tasks wouldn't be executed in that case.
We should take into account that case, either by deleting the app folder if something fails, or with clear instructions to the user? not sure.
There was a problem hiding this comment.
Good call — switched to the "delete on failure" approach in 36deafe. If any task after the move fails (install, cleanup, or git init), the partial project at outputDirectory is removed before the error propagates, so we're back to the original "no half-baked project on disk" behavior.
let outputDirectoryCreated = false
tasks.push({
title: 'Preparing project directory',
task: async () => {
await ensureAppDirectoryIsAvailable(outputDirectory, hyphenizedName)
outputDirectoryCreated = true
await moveFile(templateScaffoldDir, outputDirectory)
},
})
// ...install / cleanup / git init...
try {
await renderTasks(tasks)
} catch (error) {
if (outputDirectoryCreated) {
await rmdir(outputDirectory).catch(() => {})
}
throw error
}The flag is set right after ensureAppDirectoryIsAvailable passes, so a partial moveFile is also cleaned up. The .catch(() => {}) on rmdir is so cleanup errors don't mask the original task error.
isaacroldan
left a comment
There was a problem hiding this comment.
We should handle the case where dependency install fails.
If a task after the project move fails (install, cleanup, git init), remove the half-baked scaffold at outputDirectory so users aren't left with a directory missing node_modules / git / cleanup work. Restores the prior behavior where a failed init left nothing behind. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WHY are these changes introduced?
Cross-OS QA on CLI nightly
0.0.0-nightly-20260521132110surfaced this as a release blocker on Windows + pnpm: afterapp init --package-manager=pnpm, everynode_modulessymlink was dangling and users had to manuallyrm -rf node_modules && pnpm installbefore doing anything.Root cause is the install→move ordering in
packages/app/src/cli/services/init/init.ts. The whole flow ran insideinTemporaryDirectory: download template → liquid render → install deps → cleanup → git init, thenmoveFile(templateScaffoldDir, outputDirectory). pnpm on Windows uses NTFS junctions for the virtual store undernode_modules/.pnpm/*, and junctions store absolute paths — moving the tree from%TEMP%\…\appto the destination orphans every link.What is this pull request doing?
In
packages/app/src/cli/services/init/init.ts, insert a new "Preparing project directory" task into the existing task list that doesensureAppDirectoryIsAvailable+moveFile(templateScaffoldDir → outputDirectory). Install, cleanup, and git-init then run againstoutputDirectoryinstead of the temp dir.Pre-install steps (template download, liquid render, package.json edits, .gitignore/.npmrc tweaks) still run in the temp dir, so a failure there leaves no half-baked project on disk. The earlier
inTemporaryDirectoryblock still owns cleanup of the (now mostly-empty) temp dir.Tradeoff worth flagging: a failure during the install task now leaves the scaffolded project at
outputDirectorywithout a populatednode_modules. Before, an install failure would just bail and leave nothing behind. I think this is the better tradeoff — users can re-runpnpm installthemselves rather than restarting the whole init flow — but happy to add on-failure cleanup if reviewers prefer the old behavior.How to test your changes?
On a Windows machine with pnpm installed:
pnpm build).node packages/cli/bin/dev.js app init --package-manager=pnpm --name=test-app --template=https://github.com/Shopify/shopify-app-template-react-router#main-cli(or usepnpm create-app).cd test-app && pnpm dev— should run without "module not found" errors. Without this fix it would die immediately because of dangling junctions undernode_modules/.pnpm/.Also worth a smoke test on macOS / Linux that init still produces a working project — same task list, just with the move task inserted earlier.
Local test runs done on this branch:
vitest run packages/app/src/cli/services/init/— 28/28 pass.vitest run packages/app/src/cli/commands/app/init.test packages/app/src/cli/prompts/init/— 12/12 pass.tsc --noEmiton@shopify/app— clean.eslint src/cli/services/init/init.ts— clean.Measuring impact
How do we know this change is successful? Choose one:
Checklist
(A
.changeset/fix-init-install-in-place.mdis included.)