Skip to content

Commit cb4d69b

Browse files
Merge pull request #6078 from Shopify/watch-imports
Watch all imported files on app dev
2 parents 23ba15d + 2ddec70 commit cb4d69b

File tree

10 files changed

+1680
-194
lines changed

10 files changed

+1680
-194
lines changed

packages/app/src/cli/models/app/app.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ export interface AppInterface<
293293
dotenv?: DotEnvFile
294294
allExtensions: ExtensionInstance[]
295295
realExtensions: ExtensionInstance[]
296+
nonConfigExtensions: ExtensionInstance[]
296297
draftableExtensions: ExtensionInstance[]
297298
errors?: AppErrors
298299
hiddenConfig: AppHiddenConfig
@@ -396,10 +397,14 @@ export class App<
396397
}
397398

398399
get allExtensions() {
399-
if (this.includeConfigOnDeploy === false) return this.realExtensions.filter((ext) => !ext.isAppConfigExtension)
400+
if (this.includeConfigOnDeploy === false) return this.nonConfigExtensions
400401
return this.realExtensions
401402
}
402403

404+
get nonConfigExtensions() {
405+
return this.realExtensions.filter((ext) => !ext.isAppConfigExtension)
406+
}
407+
403408
get draftableExtensions() {
404409
return this.realExtensions.filter(
405410
(ext) => ext.isUUIDStrategyExtension || ext.specification.identifier === AppAccessSpecIdentifier,

packages/app/src/cli/models/extensions/extension-instance.test.ts

Lines changed: 135 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ import {describe, expect, test, vi} from 'vitest'
2121
import {inTemporaryDirectory, readFile, mkdir, writeFile, fileExistsSync} from '@shopify/cli-kit/node/fs'
2222
import {slugify} from '@shopify/cli-kit/common/string'
2323
import {hashString, nonRandomUUID} from '@shopify/cli-kit/node/crypto'
24+
import {extractImportPathsRecursively} from '@shopify/cli-kit/node/import-extractor'
2425
import {Writable} from 'stream'
2526

2627
const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient()
2728

29+
vi.mock('@shopify/cli-kit/node/import-extractor')
30+
2831
function functionConfiguration(): FunctionConfigType {
2932
return {
3033
name: 'foo',
@@ -37,90 +40,6 @@ function functionConfiguration(): FunctionConfigType {
3740
}
3841
}
3942

40-
describe('watchPaths', async () => {
41-
test('returns an array for a single path', async () => {
42-
const config = functionConfiguration()
43-
config.build = {
44-
watch: 'src/single-path.foo',
45-
wasm_opt: true,
46-
}
47-
const extensionInstance = await testFunctionExtension({
48-
config,
49-
dir: 'foo',
50-
})
51-
52-
const got = extensionInstance.watchBuildPaths
53-
54-
expect(got).toEqual([joinPath('foo', 'src', 'single-path.foo'), joinPath('foo', '**', '!(.)*.graphql')])
55-
})
56-
57-
test('returns default paths for javascript', async () => {
58-
const config = functionConfiguration()
59-
config.build = {
60-
wasm_opt: true,
61-
}
62-
const extensionInstance = await testFunctionExtension({
63-
config,
64-
entryPath: 'src/index.js',
65-
dir: 'foo',
66-
})
67-
68-
const got = extensionInstance.watchBuildPaths
69-
70-
expect(got).toEqual([joinPath('foo', 'src', '**', '*.{js,ts}'), joinPath('foo', '**', '!(.)*.graphql')])
71-
})
72-
73-
test('returns js and ts paths for esbuild extensions', async () => {
74-
const extensionInstance = await testUIExtension({directory: 'foo'})
75-
76-
const got = extensionInstance.watchBuildPaths
77-
78-
expect(got).toEqual([joinPath('foo', 'src', '**', '*.{ts,tsx,js,jsx}')])
79-
})
80-
81-
test('return empty array for non-function non-esbuild extensions', async () => {
82-
const extensionInstance = await testTaxCalculationExtension('foo')
83-
84-
const got = extensionInstance.watchBuildPaths
85-
86-
expect(got).toEqual([])
87-
})
88-
89-
test('returns configured paths and input query', async () => {
90-
const config = functionConfiguration()
91-
config.build = {
92-
watch: ['src/**/*.rs', 'src/**/*.foo'],
93-
wasm_opt: true,
94-
}
95-
const extensionInstance = await testFunctionExtension({
96-
config,
97-
dir: 'foo',
98-
})
99-
100-
const got = extensionInstance.watchBuildPaths
101-
102-
expect(got).toEqual([
103-
joinPath('foo', 'src/**/*.rs'),
104-
joinPath('foo', 'src/**/*.foo'),
105-
joinPath('foo', '**', '!(.)*.graphql'),
106-
])
107-
})
108-
109-
test('returns null if not javascript and not configured', async () => {
110-
const config = functionConfiguration()
111-
config.build = {
112-
wasm_opt: true,
113-
}
114-
const extensionInstance = await testFunctionExtension({
115-
config,
116-
})
117-
118-
const got = extensionInstance.watchBuildPaths
119-
120-
expect(got).toBeNull()
121-
})
122-
})
123-
12443
describe('keepBuiltSourcemapsLocally', async () => {
12544
test('moves the appropriate source map files to the expected directory for sourcemap generating extensions', async () => {
12645
await inTemporaryDirectory(async (bundleDirectory: string) => {
@@ -607,3 +526,135 @@ describe('draftMessages', async () => {
607526
})
608527
})
609528
})
529+
530+
describe('watchedFiles', async () => {
531+
test('returns files based on devSessionWatchPaths when defined', async () => {
532+
await inTemporaryDirectory(async (tmpDir) => {
533+
// Given
534+
const config = functionConfiguration()
535+
config.build = {
536+
watch: 'src/**/*.js',
537+
wasm_opt: true,
538+
}
539+
const extensionInstance = await testFunctionExtension({
540+
config,
541+
dir: tmpDir,
542+
})
543+
544+
// Create some test files
545+
const srcDir = joinPath(tmpDir, 'src')
546+
await mkdir(srcDir)
547+
await writeFile(joinPath(srcDir, 'index.js'), 'console.log("test")')
548+
await writeFile(joinPath(srcDir, 'helper.js'), 'export const helper = () => {}')
549+
550+
// Mock import extraction to return only the starting files (no external imports)
551+
const indexFile = joinPath(srcDir, 'index.js')
552+
vi.mocked(extractImportPathsRecursively).mockImplementation((filePath) => [filePath])
553+
554+
// When
555+
const watchedFiles = extensionInstance.watchedFiles()
556+
557+
// Then
558+
expect(watchedFiles).toHaveLength(2)
559+
expect(watchedFiles).toContain(joinPath(srcDir, 'index.js'))
560+
expect(watchedFiles).toContain(joinPath(srcDir, 'helper.js'))
561+
562+
// Clean up
563+
vi.mocked(extractImportPathsRecursively).mockReset()
564+
})
565+
})
566+
567+
test('returns all files when devSessionWatchPaths is undefined', async () => {
568+
await inTemporaryDirectory(async (tmpDir) => {
569+
// Given
570+
const extensionInstance = await testUIExtension({directory: tmpDir})
571+
572+
// Create some test files
573+
const srcDir = joinPath(tmpDir, 'src')
574+
await mkdir(srcDir)
575+
await writeFile(joinPath(srcDir, 'index.ts'), 'console.log("test")')
576+
await writeFile(joinPath(tmpDir, 'config.json'), '{}')
577+
578+
// Mock import extraction to return only the starting files (no external imports)
579+
vi.mocked(extractImportPathsRecursively).mockImplementation((filePath) => [filePath])
580+
581+
// When
582+
const watchedFiles = extensionInstance.watchedFiles()
583+
584+
// Then
585+
expect(watchedFiles).toContain(joinPath(srcDir, 'index.ts'))
586+
expect(watchedFiles).toContain(joinPath(tmpDir, 'config.json'))
587+
588+
// Clean up
589+
vi.mocked(extractImportPathsRecursively).mockReset()
590+
})
591+
})
592+
593+
test('includes imported files from outside extension directory', async () => {
594+
await inTemporaryDirectory(async (tmpDir) => {
595+
// Given
596+
const extensionInstance = await testUIExtension({
597+
directory: tmpDir,
598+
entrySourceFilePath: joinPath(tmpDir, 'src', 'index.ts'),
599+
})
600+
601+
// Create test file
602+
const srcDir = joinPath(tmpDir, 'src')
603+
await mkdir(srcDir)
604+
await writeFile(joinPath(srcDir, 'index.ts'), 'import "../../../shared/utils"')
605+
606+
// Mock import extraction to return external import
607+
const externalFile = joinPath(tmpDir, '..', '..', 'shared', 'utils.ts')
608+
const entryFile = joinPath(srcDir, 'index.ts')
609+
vi.mocked(extractImportPathsRecursively).mockReturnValue([entryFile, externalFile])
610+
611+
// When
612+
const watchedFiles = extensionInstance.watchedFiles()
613+
614+
// Then
615+
expect(watchedFiles).toContain(joinPath(srcDir, 'index.ts'))
616+
expect(watchedFiles).toContain(externalFile)
617+
618+
// Clean up
619+
vi.mocked(extractImportPathsRecursively).mockReset()
620+
})
621+
})
622+
})
623+
624+
describe('rescanImports', async () => {
625+
test('clears cached import paths and rescans', async () => {
626+
await inTemporaryDirectory(async (tmpDir) => {
627+
// Given
628+
const extensionInstance = await testUIExtension({
629+
directory: tmpDir,
630+
entrySourceFilePath: joinPath(tmpDir, 'src', 'index.ts'),
631+
})
632+
633+
// Create test file
634+
const srcDir = joinPath(tmpDir, 'src')
635+
await mkdir(srcDir)
636+
await writeFile(joinPath(srcDir, 'index.ts'), 'import "./local"')
637+
638+
// Reset mock to ensure clean state
639+
vi.mocked(extractImportPathsRecursively).mockReset()
640+
641+
// First scan with one set of imports
642+
vi.mocked(extractImportPathsRecursively).mockReturnValue(['./local'])
643+
// This will populate cachedImportPaths
644+
extensionInstance.watchedFiles()
645+
646+
// Update the mock to return different imports
647+
vi.mocked(extractImportPathsRecursively).mockReturnValue(['./local', '../external'])
648+
649+
// When
650+
const newImports = await extensionInstance.rescanImports()
651+
652+
// Then
653+
expect(extractImportPathsRecursively).toHaveBeenCalledTimes(2)
654+
// Note: we can't directly test the result since resolvePath would fail in test
655+
656+
// Clean up
657+
vi.mocked(extractImportPathsRecursively).mockReset()
658+
})
659+
})
660+
})

0 commit comments

Comments
 (0)