Skip to content
Closed
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/respect-gitignore-load.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopify/app": patch
---

Respect .gitignore when discovering web and extension configurations
77 changes: 77 additions & 0 deletions packages/app/src/cli/models/app/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,45 @@ redirect_urls = [ "https://example.com/api/auth" ]
expect(app.webs.length).toBe(0)
})

test('ignores web blocks in gitignored directories', async () => {
// Given
await writeConfig(appConfiguration)

// Create a gitignored directory with a web config
const ignoredDirectory = joinPath(tmpDir, 'ignored-worktree')
await mkdir(ignoredDirectory)
await writeWebConfiguration({webDirectory: ignoredDirectory, role: 'backend'})

// Create .gitignore file
const gitignoreContent = 'ignored-worktree/\n'
await writeFile(joinPath(tmpDir, '.gitignore'), gitignoreContent)

// When
const app = await loadTestingApp()

// Then
// Should only load the web from the non-ignored directory
expect(app.webs.length).toBe(1)
expect(app.webs[0]!.directory).not.toContain('ignored-worktree')
})

test('loads all web blocks when no .gitignore file exists', async () => {
// Given
await writeConfig(appConfiguration)

// Create another directory with a web config (but no .gitignore file)
const anotherDirectory = joinPath(tmpDir, 'another-web')
await mkdir(anotherDirectory)
await writeWebConfiguration({webDirectory: anotherDirectory, role: 'frontend'})

// When
const app = await loadTestingApp()

// Then
// Should load both web blocks since there's no .gitignore
expect(app.webs.length).toBe(2)
})

test('loads the app when it has a extension with a valid configuration', async () => {
// Given
await writeConfig(appConfiguration)
Expand Down Expand Up @@ -834,6 +873,44 @@ redirect_urls = [ "https://example.com/api/auth" ]
expect(app.allExtensions[0]!.localIdentifier).toBe('custom-extension')
})

test('ignores extensions in gitignored directories', async () => {
// Given
await writeConfig(appConfiguration)

// Create a non-ignored extension
const blockConfiguration = `
name = "my_extension"
type = "checkout_post_purchase"
`
await writeBlockConfig({
blockConfiguration,
name: 'my-extension',
})
await writeFile(joinPath(blockPath('my-extension'), 'index.js'), '')

// Create a gitignored directory with an extension
const ignoredExtensionDir = joinPath(tmpDir, 'extensions', 'ignored-extension')
await mkdir(ignoredExtensionDir)
const ignoredBlockConfiguration = `
name = "ignored_extension"
type = "checkout_post_purchase"
`
await writeFile(joinPath(ignoredExtensionDir, 'my-ignored-extension.extension.toml'), ignoredBlockConfiguration)
await writeFile(joinPath(ignoredExtensionDir, 'index.js'), '')

// Create .gitignore file
const gitignoreContent = 'extensions/ignored-extension/\n'
await writeFile(joinPath(tmpDir, '.gitignore'), gitignoreContent)

// When
const app = await loadTestingApp()

// Then
// Should only load the non-ignored extension
expect(app.allExtensions.length).toBe(1)
expect(app.allExtensions[0]!.configuration.name).toBe('my_extension')
})

test('loads the app from a extension directory when it has a extension with a valid configuration', async () => {
// Given
await writeConfig(appConfiguration)
Expand Down
26 changes: 24 additions & 2 deletions packages/app/src/cli/models/app/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,10 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
return joinPath(appDirectory, webGlob, configurationFileNames.web)
})
webConfigGlobs.push(`!${joinPath(appDirectory, '**/node_modules/**')}`)
const webTomlPaths = await glob(webConfigGlobs)
const allWebTomlPaths = await glob(webConfigGlobs)

// Filter out paths that are ignored by .gitignore
const webTomlPaths = await filterIgnoredPaths(appDirectory, allWebTomlPaths)

const webs = await Promise.all(webTomlPaths.map((path) => this.loadWeb(path)))
this.validateWebs(webs)
Expand Down Expand Up @@ -559,7 +562,10 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
return joinPath(appDirectory, extensionPath, '*.extension.toml')
})
extensionConfigPaths.push(`!${joinPath(appDirectory, '**/node_modules/**')}`)
const configPaths = await glob(extensionConfigPaths)
const allConfigPaths = await glob(extensionConfigPaths)

// Filter out paths that are ignored by .gitignore
const configPaths = await filterIgnoredPaths(appDirectory, allConfigPaths)

return configPaths.map(async (configurationPath) => {
const directory = dirname(configurationPath)
Expand Down Expand Up @@ -999,6 +1005,22 @@ async function checkIfGitTracked(appDirectory: string, configurationPath: string
return isTracked
}

/**
* Filters an array of paths to exclude those that match patterns in .gitignore
*/
async function filterIgnoredPaths(appDirectory: string, paths: string[]): Promise<string[]> {
const gitIgnorePath = joinPath(appDirectory, '.gitignore')
if (!fileExistsSync(gitIgnorePath)) return paths

const gitIgnoreContent = await readFile(gitIgnorePath)
const ignored = ignore.default().add(gitIgnoreContent)

return paths.filter((path) => {
const relative = relativePath(appDirectory, path)
return !ignored.ignores(relative)
})
}

async function getConfigurationPath(appDirectory: string, configName: string | undefined) {
const configurationFileName = getAppConfigurationFileName(configName)
const configurationPath = joinPath(appDirectory, configurationFileName)
Expand Down
Loading