From 45b1884905172c6fd1010588e9069374a33c2863 Mon Sep 17 00:00:00 2001 From: Craig Martin Date: Thu, 21 May 2026 14:50:43 -0400 Subject: [PATCH 1/2] [Fix] Move scaffolded project to its final directory before installing 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. --- .changeset/fix-init-install-in-place.md | 5 +++++ packages/app/src/cli/services/init/init.ts | 22 +++++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-init-install-in-place.md diff --git a/.changeset/fix-init-install-in-place.md b/.changeset/fix-init-install-in-place.md new file mode 100644 index 00000000000..3da21318a54 --- /dev/null +++ b/.changeset/fix-init-install-in-place.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Fix `shopify app init` leaving dangling `node_modules` symlinks on Windows when using `pnpm` (and similarly affected package managers). The scaffolded project is now moved to its final directory before dependencies are installed, so package-manager-managed symlinks/junctions resolve to the final location instead of the temporary scaffold path. diff --git a/packages/app/src/cli/services/init/init.ts b/packages/app/src/cli/services/init/init.ts index e4d0cc9e34b..985e23cadba 100644 --- a/packages/app/src/cli/services/init/init.ts +++ b/packages/app/src/cli/services/init/init.ts @@ -170,32 +170,40 @@ async function init(options: InitOptions) { }) } + // Move the scaffolded template into its final directory BEFORE installing + // dependencies. pnpm (and other package managers) create absolute-path + // junctions/symlinks on Windows, so installing in the temp dir and then + // moving the tree orphans every link under node_modules/.pnpm/*. + tasks.push({ + title: 'Preparing project directory', + task: async () => { + await ensureAppDirectoryIsAvailable(outputDirectory, hyphenizedName) + await moveFile(templateScaffoldDir, outputDirectory) + }, + }) + tasks.push( { title: `Installing dependencies with ${packageManager}`, task: async () => { - await getDeepInstallNPMTasks({from: templateScaffoldDir, packageManager}) + await getDeepInstallNPMTasks({from: outputDirectory, packageManager}) }, }, { title: 'Cleaning up', task: async () => { - await cleanup(templateScaffoldDir, packageManager) + await cleanup(outputDirectory, packageManager) }, }, { title: 'Initializing a Git repository...', task: async () => { - await initializeGitRepository(templateScaffoldDir) + await initializeGitRepository(outputDirectory) }, }, ) await renderTasks(tasks) - - // Ensure the app directory is available before moving the template scaffold - await ensureAppDirectoryIsAvailable(outputDirectory, hyphenizedName) - await moveFile(templateScaffoldDir, outputDirectory) }) let app: OrganizationApp From 36deafe50949dfc1f36917cbc9f3b5c1cbe76ee1 Mon Sep 17 00:00:00 2001 From: Craig Martin Date: Fri, 22 May 2026 08:45:07 -0400 Subject: [PATCH 2/2] Clean up partial project on init failure 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) --- packages/app/src/cli/services/init/init.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/app/src/cli/services/init/init.ts b/packages/app/src/cli/services/init/init.ts index 985e23cadba..de97889c1cb 100644 --- a/packages/app/src/cli/services/init/init.ts +++ b/packages/app/src/cli/services/init/init.ts @@ -27,6 +27,7 @@ import { mkdir, moveFile, readFile, + rmdir, writeFile, } from '@shopify/cli-kit/node/fs' import {joinPath, normalizePath} from '@shopify/cli-kit/node/path' @@ -174,10 +175,12 @@ async function init(options: InitOptions) { // dependencies. pnpm (and other package managers) create absolute-path // junctions/symlinks on Windows, so installing in the temp dir and then // moving the tree orphans every link under node_modules/.pnpm/*. + let outputDirectoryCreated = false tasks.push({ title: 'Preparing project directory', task: async () => { await ensureAppDirectoryIsAvailable(outputDirectory, hyphenizedName) + outputDirectoryCreated = true await moveFile(templateScaffoldDir, outputDirectory) }, }) @@ -203,7 +206,17 @@ async function init(options: InitOptions) { }, ) - await renderTasks(tasks) + try { + await renderTasks(tasks) + } catch (error) { + // If a task failed after the project was moved to its final directory, + // remove the partial project so the user isn't left with a half-baked + // scaffold (no node_modules, no cleanup, no git init). + if (outputDirectoryCreated) { + await rmdir(outputDirectory).catch(() => {}) + } + throw error + } }) let app: OrganizationApp