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..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' @@ -170,32 +171,52 @@ 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/*. + let outputDirectoryCreated = false + tasks.push({ + title: 'Preparing project directory', + task: async () => { + await ensureAppDirectoryIsAvailable(outputDirectory, hyphenizedName) + outputDirectoryCreated = true + 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) + 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