@@ -21,10 +21,13 @@ import {describe, expect, test, vi} from 'vitest'
2121import { inTemporaryDirectory , readFile , mkdir , writeFile , fileExistsSync } from '@shopify/cli-kit/node/fs'
2222import { slugify } from '@shopify/cli-kit/common/string'
2323import { hashString , nonRandomUUID } from '@shopify/cli-kit/node/crypto'
24+ import { extractImportPathsRecursively } from '@shopify/cli-kit/node/import-extractor'
2425import { Writable } from 'stream'
2526
2627const developerPlatformClient : DeveloperPlatformClient = testDeveloperPlatformClient ( )
2728
29+ vi . mock ( '@shopify/cli-kit/node/import-extractor' )
30+
2831function 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-
12443describe ( '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