Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-init-install-in-place.md
Original file line number Diff line number Diff line change
@@ -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.
37 changes: 29 additions & 8 deletions packages/app/src/cli/services/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
},
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


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
Expand Down
Loading