From 6d55815759d2d9303663378e9a7be6388dde3b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Thu, 21 Nov 2024 16:53:49 +0100 Subject: [PATCH] Remove unused setupExtensionWatcher --- .../services/dev/extension/bundler.test.ts | 386 ------------------ .../src/cli/services/dev/extension/bundler.ts | 118 ------ 2 files changed, 504 deletions(-) delete mode 100644 packages/app/src/cli/services/dev/extension/bundler.test.ts delete mode 100644 packages/app/src/cli/services/dev/extension/bundler.ts diff --git a/packages/app/src/cli/services/dev/extension/bundler.test.ts b/packages/app/src/cli/services/dev/extension/bundler.test.ts deleted file mode 100644 index d093af6cc36..00000000000 --- a/packages/app/src/cli/services/dev/extension/bundler.test.ts +++ /dev/null @@ -1,386 +0,0 @@ -import {SetupExtensionWatcherOptions, setupExtensionWatcher} from './bundler.js' -import {testFunctionExtension, testApp, testAppConfigExtensions} from '../../../models/app/app.test-data.js' -import {reloadExtensionConfig} from '../update-extension.js' -import {FunctionConfigType} from '../../../models/extensions/specifications/function.js' -import * as extensionBuild from '../../../services/build/extension.js' -import {ExtensionInstance} from '../../../models/extensions/extension-instance.js' -import {BaseConfigType} from '../../../models/extensions/schemas.js' -import {beforeEach, describe, expect, test, vi} from 'vitest' -import chokidar from 'chokidar' -import {AbortController, AbortSignal} from '@shopify/cli-kit/node/abort' -import {outputDebug, outputWarn} from '@shopify/cli-kit/node/output' -import {flushPromises} from '@shopify/cli-kit/node/promises' -import {joinPath} from '@shopify/cli-kit/node/path' -import * as cliKitFS from '@shopify/cli-kit/node/fs' -import {Writable} from 'stream' - -vi.mock('@shopify/cli-kit/node/output') -vi.mock('../../../models/app/loader.js') -vi.mock('../update-extension.js') -vi.mock('../../../services/build/extension.js') -vi.mock('../update-extension.js') - -function functionConfiguration(): FunctionConfigType { - return { - name: 'foo', - type: 'function', - api_version: '2023-07', - configuration_ui: true, - metafields: [], - build: {}, - } -} - -describe('setupExtensionWatcher', () => { - beforeEach(() => { - const config = {type: 'type', name: 'name', path: 'path', metafields: []} - vi.mocked(reloadExtensionConfig).mockResolvedValue({newConfig: config, previousConfig: config}) - }) - interface MockWatcherOptionsArgs { - watchPath: string | undefined - signal?: AbortSignal | undefined - } - - async function mockWatcherOptions({ - watchPath, - signal, - }: MockWatcherOptionsArgs): Promise { - const config = functionConfiguration() - config.build = { - watch: watchPath, - } - - return { - app: testApp(), - extension: await testFunctionExtension({ - config, - dir: 'foo', - }), - url: 'mock/url', - stdout: new Writable(), - stderr: new Writable(), - signal: signal ?? new AbortController().signal, - onChange: vi.fn(), - onReloadAndBuildError: vi.fn(), - } - } - - async function mockWatcherConfigurationOptions(): Promise { - const configurationExtension = await testAppConfigExtensions() - - return { - app: testApp(), - extension: configurationExtension, - url: 'mock/url', - stdout: new Writable(), - stderr: new Writable(), - signal: new AbortController().signal, - onChange: vi.fn(), - onReloadAndBuildError: vi.fn(), - } - } - - test('warns and does not watch if there are no watch paths', async () => { - const watchOptions = await mockWatcherOptions({ - watchPath: undefined, - }) - const chokidarSpy = vi.spyOn(chokidar, 'watch') - - await setupExtensionWatcher(watchOptions) - - expect(chokidarSpy).not.toHaveBeenCalled() - expect(outputWarn).toHaveBeenCalledWith( - expect.stringContaining(watchOptions.extension.localIdentifier), - watchOptions.stdout, - ) - }) - - test('watches the provided paths', async () => { - const watchOptions = await mockWatcherOptions({ - watchPath: '*.rs', - }) - const chokidarOnSpy = vi.fn() - const chokidarWatchSpy = vi.spyOn(chokidar, 'watch').mockReturnValue({ - on: chokidarOnSpy, - } as any) - - await setupExtensionWatcher(watchOptions) - - expect(chokidarWatchSpy).toHaveBeenCalledWith(expect.arrayContaining([joinPath('foo', '*.rs')]), { - ignored: '**/*.test.*', - }) - expect(chokidarOnSpy).toHaveBeenCalledWith('change', expect.any(Function)) - }) - - test('deploys the function on file change in locales directory', async () => { - // Given - vi.spyOn(cliKitFS, 'fileExists').mockResolvedValue(true) - - const watchOptions = await mockWatcherOptions({ - watchPath: '*.rs', - }) - - const chokidarOnSpy = vi.fn().mockImplementation((_event, handler) => { - // Call the file watch handler immediately - handler(`${watchOptions.extension.directory}/locales/en.json`) - }) - - // When - const chokidarWatchSpy = vi.spyOn(chokidar, 'watch').mockImplementation((path) => { - if (path.toString().includes('locales')) { - return { - on: chokidarOnSpy, - } as any - } - return { - on: vi.fn(), - } as any - }) - - await setupExtensionWatcher(watchOptions) - await flushPromises() - - // Then - expect(chokidarOnSpy).toHaveBeenCalled() - expect(chokidarWatchSpy).toHaveBeenCalledWith( - [ - `${watchOptions.extension.directory}/*.rs`, - `${watchOptions.extension.directory}/**/!(.)*.graphql`, - `${watchOptions.extension.directory}/locales/**.json`, - `${watchOptions.extension.directory}/**.toml`, - ], - { - ignored: '**/*.test.*', - }, - ) - expect(reloadExtensionConfig).toHaveBeenCalled() - }) - - test('builds and deploys the function on file change', async () => { - const watchOptions = await mockWatcherOptions({ - watchPath: '*.rs', - }) - const chokidarOnSpy = vi.fn().mockImplementation((_event, handler) => { - // call the file watch handler immediately - handler('foo/main.rs') - }) - vi.spyOn(chokidar, 'watch').mockImplementation((path) => { - return {on: chokidarOnSpy} as any - }) - - const buildSpy = vi.spyOn(extensionBuild, 'buildFunctionExtension').mockResolvedValue() - - await setupExtensionWatcher(watchOptions) - await flushPromises() - - expect(chokidarOnSpy).toHaveBeenCalled() - expect(outputDebug).toHaveBeenCalledWith(expect.stringContaining('foo/main.rs'), watchOptions.stdout) - expect(buildSpy).toHaveBeenCalledWith( - watchOptions.extension, - expect.objectContaining({ - app: watchOptions.app, - stdout: watchOptions.stdout, - stderr: watchOptions.stderr, - useTasks: false, - }), - ) - expect(reloadExtensionConfig).toHaveBeenCalledWith({ - extension: watchOptions.extension, - stdout: watchOptions.stdout, - }) - expect(watchOptions.onChange).toHaveBeenCalled() - expect(watchOptions.onReloadAndBuildError).not.toHaveBeenCalled() - }) - - test('does not deploy the function if the build fails', async () => { - const watchOptions = await mockWatcherOptions({ - watchPath: '*.rs', - }) - const chokidarOnSpy = vi.fn().mockImplementation((_event, handler) => { - // call the file watch handler immediately - handler('foo/main.rs') - }) - vi.spyOn(chokidar, 'watch').mockReturnValue({ - on: chokidarOnSpy, - } as any) - - const buildSpy = vi.spyOn(extensionBuild, 'buildFunctionExtension').mockRejectedValue('error') - - await setupExtensionWatcher(watchOptions) - await flushPromises() - - expect(buildSpy).toHaveBeenCalled() - expect(watchOptions.onChange).not.toHaveBeenCalled() - expect(watchOptions.onReloadAndBuildError).toHaveBeenCalled() - }) - - test('terminates existing builds on concurrent file change', async () => { - const watchOptions = await mockWatcherOptions({ - watchPath: '*.rs', - }) - const chokidarOnSpy = vi.fn().mockImplementation((_event, handler) => { - // call the file watch handler twice - handler('foo/main.rs') - handler('foo/main.rs') - }) - - vi.spyOn(chokidar, 'watch').mockReturnValue({ - on: chokidarOnSpy, - } as any) - - let signal: AbortSignal | undefined - vi.spyOn(extensionBuild, 'buildFunctionExtension') - .mockImplementationOnce( - async (extension: ExtensionInstance, options: extensionBuild.BuildFunctionExtensionOptions) => { - signal = options.signal - - // simulate a build, defer execution to next handler - return Promise.resolve() - }, - ) - .mockResolvedValue() - - await setupExtensionWatcher(watchOptions) - await flushPromises() - - expect(signal).toBeDefined() - expect(signal?.aborted).toBe(true) - }) - - test('stops watching the function when the signal aborts and close resolves', async () => { - const abortController = new AbortController() - const watchOptions = await mockWatcherOptions({ - watchPath: '*.rs', - signal: abortController.signal, - }) - const chokidarCloseSpy = vi.fn(() => Promise.resolve()) - const chokidarOnSpy = vi.fn(() => { - return { - close: chokidarCloseSpy, - } - }) - vi.spyOn(chokidar, 'watch').mockReturnValue({ - on: chokidarOnSpy, - } as any) - - await setupExtensionWatcher(watchOptions) - abortController.abort() - - expect(chokidarCloseSpy).toHaveBeenCalled() - expect(outputDebug).toHaveBeenCalledWith( - expect.stringContaining(watchOptions.extension.devUUID), - watchOptions.stdout, - ) - }) - - test('stops watching the function when the signal aborts and close rejects', async () => { - const abortController = new AbortController() - const watchOptions = await mockWatcherOptions({ - watchPath: '*.rs', - signal: abortController.signal, - }) - const chokidarCloseSpy = vi.fn(() => Promise.reject(new Error('fail'))) - const chokidarOnSpy = vi.fn(() => { - return { - close: chokidarCloseSpy, - } - }) - vi.spyOn(chokidar, 'watch').mockReturnValue({ - on: chokidarOnSpy, - } as any) - - await setupExtensionWatcher(watchOptions) - abortController.abort() - - await expect(chokidarCloseSpy).rejects.toThrow(new Error('fail')) - expect(outputDebug).toHaveBeenLastCalledWith(expect.stringContaining('fail'), watchOptions.stderr) - }) - test('deploy the configuration extension when the values are modified', async () => { - // Given - const newConfig = {pos: {embeded: true}, path: 'shopify.app.toml'} as unknown as BaseConfigType & {path: string} - const previousConfig = {pos: {embeded: false}, path: 'shopify.app.toml'} as unknown as BaseConfigType & { - path: string - } - vi.mocked(reloadExtensionConfig).mockResolvedValue({newConfig, previousConfig}) - const watchOptions = await mockWatcherConfigurationOptions() - const buildSpy = vi.spyOn(watchOptions.extension, 'build') - const chokidarOnSpy = vi.fn().mockImplementation((_event, handler) => { - // call the file watch handler immediately - handler('shopify.app.toml') - }) - vi.spyOn(chokidar, 'watch').mockImplementation((path) => { - return {on: chokidarOnSpy} as any - }) - - // When - await setupExtensionWatcher(watchOptions) - await flushPromises() - - // Then - expect(buildSpy).not.toHaveBeenCalled() - expect(chokidarOnSpy).toHaveBeenCalled() - expect(reloadExtensionConfig).toHaveBeenCalledWith({ - extension: watchOptions.extension, - stdout: watchOptions.stdout, - }) - expect(watchOptions.onChange).toHaveBeenCalled() - expect(watchOptions.onReloadAndBuildError).not.toHaveBeenCalled() - }) - test('dont deploy the configuration extension when the values are the same', async () => { - // Given - const newConfig = {pos: {embeded: true}, path: 'shopify.app.toml'} as unknown as BaseConfigType & {path: string} - const previousConfig = {pos: {embeded: true}, path: 'shopify.app.toml'} as unknown as BaseConfigType & { - path: string - } - vi.mocked(reloadExtensionConfig).mockResolvedValue({newConfig, previousConfig}) - const watchOptions = await mockWatcherConfigurationOptions() - const buildSpy = vi.spyOn(watchOptions.extension, 'build') - const chokidarOnSpy = vi.fn().mockImplementation((_event, handler) => { - // call the file watch handler immediately - handler('shopify.app.toml') - }) - vi.spyOn(chokidar, 'watch').mockImplementation((path) => { - return {on: chokidarOnSpy} as any - }) - - // When - await setupExtensionWatcher(watchOptions) - await flushPromises() - - // Them - expect(buildSpy).not.toHaveBeenCalled() - expect(chokidarOnSpy).toHaveBeenCalled() - expect(reloadExtensionConfig).toHaveBeenCalledWith({ - extension: watchOptions.extension, - stdout: watchOptions.stdout, - }) - expect(watchOptions.onChange).not.toHaveBeenCalled() - }) - test('dont deploy the configuration extension when an error is produced', async () => { - // Given - vi.mocked(reloadExtensionConfig).mockRejectedValue(new Error('config.path: wrong value')) - const watchOptions = await mockWatcherConfigurationOptions() - const buildSpy = vi.spyOn(watchOptions.extension, 'build') - const chokidarOnSpy = vi.fn().mockImplementation((_event, handler) => { - // call the file watch handler immediately - handler('shopify.app.toml') - }) - vi.spyOn(chokidar, 'watch').mockImplementation((path) => { - return {on: chokidarOnSpy} as any - }) - - // When - await setupExtensionWatcher(watchOptions) - await flushPromises() - - // Them - expect(buildSpy).not.toHaveBeenCalled() - expect(chokidarOnSpy).toHaveBeenCalled() - expect(reloadExtensionConfig).toHaveBeenCalledWith({ - extension: watchOptions.extension, - stdout: watchOptions.stdout, - }) - expect(watchOptions.onChange).not.toHaveBeenCalled() - expect(outputWarn).not.toHaveBeenCalled() - }) -}) diff --git a/packages/app/src/cli/services/dev/extension/bundler.ts b/packages/app/src/cli/services/dev/extension/bundler.ts deleted file mode 100644 index e5ecdbb1062..00000000000 --- a/packages/app/src/cli/services/dev/extension/bundler.ts +++ /dev/null @@ -1,118 +0,0 @@ -import {AppInterface} from '../../../models/app/app.js' -import {reloadExtensionConfig} from '../update-extension.js' -import {ExtensionInstance} from '../../../models/extensions/extension-instance.js' -import {ExtensionBuildOptions} from '../../build/extension.js' -import {AbortController, AbortSignal} from '@shopify/cli-kit/node/abort' -import {outputDebug, outputWarn} from '@shopify/cli-kit/node/output' -import {FSWatcher} from 'chokidar' -import micromatch from 'micromatch' -import {deepCompare} from '@shopify/cli-kit/common/object' -import {Writable} from 'stream' -import {AsyncResource} from 'async_hooks' - -export interface SetupExtensionWatcherOptions { - extension: ExtensionInstance - app: AppInterface - url?: string - stdout: Writable - stderr: Writable - signal: AbortSignal - onChange: () => Promise - onReloadAndBuildError: (error: Error) => Promise -} - -export async function setupExtensionWatcher({ - extension, - app, - url, - stdout, - stderr, - signal, - onChange, - onReloadAndBuildError, -}: SetupExtensionWatcherOptions) { - const {default: chokidar} = await import('chokidar') - - const buildPaths = extension.watchBuildPaths - - if (!buildPaths) { - outputWarn( - `Extension ${extension.localIdentifier} is missing the 'build.watch' setting, automatic builds are disabled.`, - stdout, - ) - return - } - const configurationPaths: string[] = await extension.watchConfigurationPaths() - - outputDebug( - ` -Watching extension: ${extension.localIdentifier} for: -Rebuild and Redeploy Paths: -\t${buildPaths.join('\n\t')} - -Redeploy Paths: -\t${configurationPaths.join('\n\t')} -`.trim(), - stdout, - ) - - const listenForAbortOnWatcher = (watcher: FSWatcher) => { - signal.addEventListener('abort', () => { - outputDebug(`Closing file watching for extension with ID ${extension.devUUID}`, stdout) - watcher - .close() - .then(() => { - outputDebug(`File watching closed for extension with ${extension.devUUID}`, stdout) - }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .catch((error: any) => { - outputDebug(`File watching failed to close for extension with ${extension.devUUID}: ${error.message}`, stderr) - }) - }) - } - - let buildController: AbortController | null - const allPaths = [...buildPaths, ...configurationPaths] - const functionRebuildAndRedeployWatcher = chokidar.watch(allPaths, {ignored: '**/*.test.*'}).on( - 'change', - // We need to bind the execution context to ensure the event handler can access the correct AsyncLocalStorage - // See also: https://nodejs.org/api/async_context.html#integrating-asyncresource-with-eventemitter - AsyncResource.bind((path) => { - outputDebug(`Extension file at path ${path} changed`, stdout) - if (buildController) { - // terminate any existing builds - buildController.abort() - } - buildController = new AbortController() - const buildSignal = buildController.signal - const shouldBuild = micromatch.isMatch(path, buildPaths) - - reloadAndbuildIfNecessary(extension, shouldBuild, { - app, - stdout, - stderr, - useTasks: false, - signal: buildSignal, - environment: 'development', - appURL: url, - }) - .then(({newConfig, previousConfig}) => { - if (shouldBuild) { - if (buildSignal.aborted) return - return onChange() - } - - if (deepCompare(newConfig, previousConfig)) return - return onChange() - }) - .catch((error: Error) => onReloadAndBuildError(error)) - }), - ) - listenForAbortOnWatcher(functionRebuildAndRedeployWatcher) -} - -async function reloadAndbuildIfNecessary(extension: ExtensionInstance, build: boolean, options: ExtensionBuildOptions) { - const reloadedConfig = reloadExtensionConfig({extension, stdout: options.stdout}) - if (!build) return reloadedConfig - return extension.build(options).then(() => reloadedConfig) -}