diff --git a/packages/app/src/cli/commands/app/demo/watcher.ts b/packages/app/src/cli/commands/app/demo/watcher.ts index 8b28ae5632..14e1539ebd 100644 --- a/packages/app/src/cli/commands/app/demo/watcher.ts +++ b/packages/app/src/cli/commands/app/demo/watcher.ts @@ -46,9 +46,6 @@ export default class DemoWatcher extends Command { case EventType.Updated: outputInfo(` 🔄 Updated: ${colors.yellow(event.extension.handle)}`) break - case EventType.UpdatedSourceFile: - outputInfo(` 🔄 Updated: ${colors.yellow(event.extension.handle)} (🏗️ needs rebuild)`) - break } }) }) diff --git a/packages/app/src/cli/models/app/app.test-data.ts b/packages/app/src/cli/models/app/app.test-data.ts index 48be01d7ea..80c3b4dbb0 100644 --- a/packages/app/src/cli/models/app/app.test-data.ts +++ b/packages/app/src/cli/models/app/app.test-data.ts @@ -208,7 +208,10 @@ export async function testUIExtension( sources: [], }, }, - targeting: [{target: 'target1'}, {target: 'target2'}], + extension_points: [ + {target: 'target1', module: 'module1'}, + {target: 'target2', module: 'module2'}, + ], } const configurationPath = uiExtension?.configurationPath ?? `${directory}/shopify.ui.extension.toml` const entryPath = uiExtension?.entrySourceFilePath ?? `${directory}/src/index.js` diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index 009c8b8038..c51707fd02 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -330,12 +330,11 @@ export class ExtensionInstance { // Given - vi.mocked(loadApp).mockResolvedValue(testApp({allExtensions: finalExtensions})) - vi.mocked(startFileWatcher).mockImplementation(async (app, options, onChange) => onChange(fileWatchEvent)) + await inTemporaryDirectory(async (tmpDir) => { + vi.mocked(loadApp).mockResolvedValue(testApp({allExtensions: finalExtensions})) + vi.mocked(startFileWatcher).mockImplementation(async (app, options, onChange) => onChange(fileWatchEvent)) - // When - const app = testApp({ - allExtensions: initialExtensions, - configuration: {scopes: '', extension_directories: [], path: 'shopify.app.custom.toml'}, - }) - const watcher = new AppEventWatcher(app, outputOptions) - const emitSpy = vi.spyOn(watcher, 'emit') - await watcher.start() + const buildOutputPath = joinPath(tmpDir, '.shopify', 'bundle') + + // When + const app = testApp({ + allExtensions: initialExtensions, + configuration: {scopes: '', extension_directories: [], path: 'shopify.app.custom.toml'}, + }) - await flushPromises() + const watcher = new AppEventWatcher(app, 'url', outputOptions, buildOutputPath) + const emitSpy = vi.spyOn(watcher, 'emit') + await watcher.start() - expect(emitSpy).toHaveBeenCalledWith('all', { - app: expect.objectContaining({realExtensions: finalExtensions}), - extensionEvents: expect.arrayContaining(extensionEvents), - startTime: expect.anything(), - path: expect.anything(), - }) + await flushPromises() - if (needsAppReload) { - expect(loadApp).toHaveBeenCalledWith({ - specifications: expect.anything(), - directory: expect.anything(), - // The app is loaded with the same configuration file - userProvidedConfigName: 'shopify.app.custom.toml', - remoteFlags: expect.anything(), + // Wait until emitSpy has been called at least once + // We need this because there are i/o operations that make the test finish before the event is emitted + await new Promise((resolve, reject) => { + const interval = setInterval(() => { + if (emitSpy.mock.calls.length > 0) { + clearInterval(interval) + resolve() + } + }, 100) + // Wait max 3 seconds, if not resolved, reject. + setTimeout(() => { + clearInterval(interval) + reject(new Error('Timeout waiting for emitSpy to be called')) + }, 3000) }) - } else { - expect(loadApp).not.toHaveBeenCalled() - } + + expect(emitSpy).toHaveBeenCalledWith('all', { + app: expect.objectContaining({realExtensions: finalExtensions}), + extensionEvents: expect.arrayContaining(extensionEvents), + startTime: expect.anything(), + path: expect.anything(), + }) + + if (needsAppReload) { + expect(loadApp).toHaveBeenCalledWith({ + specifications: expect.anything(), + directory: expect.anything(), + // The app is loaded with the same configuration file + userProvidedConfigName: 'shopify.app.custom.toml', + remoteFlags: expect.anything(), + }) + } else { + expect(loadApp).not.toHaveBeenCalled() + } + }) }, ) }) - -async function waitForEvent(watcher: AppEventWatcher) { - return new Promise((resolve) => { - watcher.onEvent((event) => { - resolve(event) - }) - }) -} diff --git a/packages/app/src/cli/services/dev/app-events/app-event-watcher.ts b/packages/app/src/cli/services/dev/app-events/app-event-watcher.ts index 1f9423762c..9ec850db1a 100644 --- a/packages/app/src/cli/services/dev/app-events/app-event-watcher.ts +++ b/packages/app/src/cli/services/dev/app-events/app-event-watcher.ts @@ -1,15 +1,17 @@ /* eslint-disable tsdoc/syntax */ import {OutputContextOptions, WatcherEvent, startFileWatcher} from './file-watcher.js' -import {AppExtensionsDiff, appDiff} from './app-diffing.js' +import {appDiff} from './app-diffing.js' +import {ESBuildContextManager} from './app-watcher-esbuild.js' import {AppInterface} from '../../../models/app/app.js' import {ExtensionInstance} from '../../../models/extensions/extension-instance.js' import {loadApp} from '../../../models/app/loader.js' +import {ExtensionBuildOptions} from '../../build/extension.js' import {AbortError} from '@shopify/cli-kit/node/error' -import micromatch from 'micromatch' import {outputDebug, outputWarn} from '@shopify/cli-kit/node/output' import {AbortSignal} from '@shopify/cli-kit/node/abort' import {endHRTimeInMs, startHRTime} from '@shopify/cli-kit/node/hrtime' -import {basename} from '@shopify/cli-kit/node/path' +import {basename, joinPath} from '@shopify/cli-kit/node/path' +import {fileExistsSync, mkdir, rmdir} from '@shopify/cli-kit/node/fs' import EventEmitter from 'events' /** @@ -51,14 +53,12 @@ Examples: /** * The type of the extension event * - * - Updated: The extension was updated (a file changed, but is not a source file, so it won't require a rebuild) - * - UpdatedSourceFile: The extension was updated and a source file was changed, so it will require a rebuild + * - Updated: The extension was updated * - Deleted: The extension was deleted * - Created: The extension was created */ export enum EventType { Updated, - UpdatedSourceFile, Deleted, Created, } @@ -74,7 +74,7 @@ export interface ExtensionEvent { * The startTime is the time when the initial file-system event was received, it can be used by the consumer * to determine how long it took to process the event. */ -interface AppEvent { +export interface AppEvent { app: AppInterface extensionEvents: ExtensionEvent[] path: string @@ -89,14 +89,15 @@ interface HandlerInput { } type Handler = (input: HandlerInput) => Promise +type ExtensionBuildResult = {status: 'ok'; handle: string} | {status: 'error'; error: string; handle: string} const handlers: {[key in WatcherEvent['type']]: Handler} = { extension_folder_deleted: ExtensionFolderDeletedHandler, - extension_folder_created: ExtensionFolderCreatedHandler, file_created: FileChangeHandler, file_deleted: FileChangeHandler, file_updated: FileChangeHandler, - extensions_config_updated: TomlChangeHandler, + extension_folder_created: ReloadAppHandler, + extensions_config_updated: ReloadAppHandler, app_config_deleted: AppConfigDeletedHandler, } @@ -104,26 +105,69 @@ const handlers: {[key in WatcherEvent['type']]: Handler} = { * App event watcher will emit events when changes are detected in the file system. */ export class AppEventWatcher extends EventEmitter { + buildOutputPath: string private app: AppInterface private options: OutputContextOptions + private appURL?: string + private esbuildManager: ESBuildContextManager - constructor(app: AppInterface, options?: OutputContextOptions) { + constructor( + app: AppInterface, + appURL?: string, + options?: OutputContextOptions, + buildOutputPath?: string, + contextManager?: ESBuildContextManager, + ) { super() this.app = app + this.appURL = appURL + this.buildOutputPath = buildOutputPath ?? joinPath(app.directory, '.shopify', 'bundle') this.options = options ?? {stdout: process.stdout, stderr: process.stderr, signal: new AbortSignal()} + this.esbuildManager = + contextManager ?? + new ESBuildContextManager({ + outputPath: this.buildOutputPath, + dotEnvVariables: this.app.dotenv?.variables ?? {}, + url: this.appURL ?? '', + ...this.options, + }) } async start() { + // If there is a previous build folder, delete it + if (fileExistsSync(this.buildOutputPath)) await rmdir(this.buildOutputPath, {force: true}) + await mkdir(this.buildOutputPath) + + // Start the esbuild bundler for extensions that require it + await this.esbuildManager.createContexts(this.app.realExtensions.filter((ext) => ext.isESBuildExtension)) + + // Initial build of all extensions + await this.buildExtensions(this.app.realExtensions) + + // Start the file system watcher await startFileWatcher(this.app, this.options, (event) => { // A file/folder can contain multiple extensions, this is the list of extensions possibly affected by the change const extensions = this.app.realExtensions.filter((ext) => ext.directory === event.extensionPath) + handlers[event.type]({event, app: this.app, extensions, options: this.options}) - .then((appEvent) => { + .then(async (appEvent) => { this.app = appEvent.app if (appEvent.extensionEvents.length === 0) { outputDebug('Change detected, but no extensions were affected', this.options.stdout) return } + await this.esbuildManager.updateContexts(appEvent) + + // Find affected created/updated extensions and build them + const extensions = appEvent.extensionEvents + .filter((extEvent) => extEvent.type !== EventType.Deleted) + .map((extEvent) => extEvent.extension) + + await this.buildExtensions(extensions) + + const deletedExtensions = appEvent.extensionEvents.filter((extEvent) => extEvent.type === EventType.Deleted) + await this.deleteExtensionsBuildOutput(deletedExtensions.map((extEvent) => extEvent.extension)) + this.emit('all', appEvent) }) .catch((error) => { @@ -132,11 +176,61 @@ export class AppEventWatcher extends EventEmitter { }) } + async deleteExtensionsBuildOutput(extensions: ExtensionInstance[]) { + const promises = extensions.map(async (ext) => { + const outputPath = joinPath(this.buildOutputPath, ext.getOutputFolderId()) + return rmdir(outputPath, {force: true}) + }) + await Promise.all(promises) + } + onEvent(listener: (appEvent: AppEvent) => Promise | void) { // eslint-disable-next-line @typescript-eslint/no-misused-promises this.addListener('all', listener) return this } + + /** + * Builds all given extensions. + * ESBuild extensions will be built using their own ESBuild context, other extensions will be built using the default + * buildForBundle method. + */ + private async buildExtensions(extensions: ExtensionInstance[]): Promise { + const promises = extensions.map(async (ext) => { + try { + if (this.esbuildManager.contexts[ext.handle]) { + const result = await this.esbuildManager.contexts[ext.handle]?.rebuild() + if (result?.errors?.length) throw new Error(result?.errors.map((err) => err.text).join('\n')) + } else { + await this.buildExtension(ext) + } + return {status: 'ok', handle: ext.handle} as const + // eslint-disable-next-line no-catch-all/no-catch-all, @typescript-eslint/no-explicit-any + } catch (error: any) { + return {status: 'error', error: error.message, handle: ext.handle} as const + } + }) + const output = await Promise.all(promises) + // For now, do nothing with the output, but we could log the errors or something + // ESBuild errors are already logged by the ESBuild bundler + return output + } + + /** + * Build a single non-esbuild extension using the default buildForBundle method. + * @param extension - The extension to build + */ + private async buildExtension(extension: ExtensionInstance): Promise { + const buildOptions: ExtensionBuildOptions = { + app: this.app, + stdout: this.options.stdout, + stderr: this.options.stderr, + useTasks: false, + environment: 'development', + appURL: this.appURL, + } + await extension.buildForBundle(buildOptions, this.buildOutputPath) + } } /** @@ -156,55 +250,30 @@ async function ExtensionFolderDeletedHandler({event, app, extensions}: HandlerIn /** * When a file is created, updated or deleted: * Return the same app and the updated extension(s) in the event. - * Is the responsibility of the consumer of the event to build the extension if necessary * * A file can be shared between multiple extensions in the same folder. The event will include all of the affected ones. */ async function FileChangeHandler({event, app, extensions}: HandlerInput): Promise { - const events: ExtensionEvent[] = extensions.map((ext) => { - const buildPaths = ext.watchBuildPaths ?? [] - const type = micromatch.isMatch(event.path, buildPaths) ? EventType.UpdatedSourceFile : EventType.Updated - return {type, extension: ext} - }) + const events: ExtensionEvent[] = extensions.map((ext) => ({type: EventType.Updated, extension: ext})) return {app, extensionEvents: events, startTime: event.startTime, path: event.path} } /** - * When an extension folder is created: - * Reload the app and return the new app and the created extensions in the event. - */ -async function ExtensionFolderCreatedHandler({event, app, options}: HandlerInput): Promise { - const newApp = await reloadApp(app, options) - const extensionEvents = mapAppDiffToEvents(appDiff(app, newApp, false)) - return {app: newApp, extensionEvents, startTime: event.startTime, path: event.path} -} - -/** - * When any config file (toml) is updated, including the app.toml and any extension toml: - * Reload the app and find which extensions were created, deleted or updated. - * Is the responsibility of the consumer of the event to build the extension if necessary - * - * Since a toml can contain multiple extensions, this could trigger Create, Delete and Update events. - * The toml is considered a SourceFile because changes in the configuration can affect the build. + * Handler for events that requiere a full reload of the app: + * - When a new extension folder is created + * - When the app.toml is updated + * - When an extension toml is updated */ -async function TomlChangeHandler({event, app, options}: HandlerInput): Promise { +async function ReloadAppHandler({event, app, options}: HandlerInput): Promise { const newApp = await reloadApp(app, options) - const extensionEvents = mapAppDiffToEvents(appDiff(app, newApp)) + const diff = appDiff(app, newApp, true) + const createdEvents = diff.created.map((ext) => ({type: EventType.Created, extension: ext})) + const deletedEvents = diff.deleted.map((ext) => ({type: EventType.Deleted, extension: ext})) + const updatedEvents = diff.updated.map((ext) => ({type: EventType.Updated, extension: ext})) + const extensionEvents = [...createdEvents, ...deletedEvents, ...updatedEvents] return {app: newApp, extensionEvents, startTime: event.startTime, path: event.path} } -/** - * Map the AppExtensionsDiff to ExtensionEvents - * @param appDiff - The diff between the old and new app - * @returns An array of ExtensionEvents - */ -function mapAppDiffToEvents(appDiff: AppExtensionsDiff): ExtensionEvent[] { - const createdEvents = appDiff.created.map((ext) => ({type: EventType.Created, extension: ext})) - const deletedEvents = appDiff.deleted.map((ext) => ({type: EventType.Deleted, extension: ext})) - const updatedEvents = appDiff.updated.map((ext) => ({type: EventType.UpdatedSourceFile, extension: ext})) - return [...createdEvents, ...deletedEvents, ...updatedEvents] -} - /** * When the app.toml is deleted: * Throw an error to exit the process. diff --git a/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.test.ts b/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.test.ts new file mode 100644 index 0000000000..4f432186cc --- /dev/null +++ b/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.test.ts @@ -0,0 +1,82 @@ +import {DevAppWatcherOptions, ESBuildContextManager} from './app-watcher-esbuild.js' +import {AppEvent, EventType} from './app-event-watcher.js' +import {testApp, testUIExtension} from '../../../models/app/app.test-data.js' +import {describe, expect, test, vi} from 'vitest' + +vi.mock('@luckycatfactory/esbuild-graphql-loader', () => ({ + default: { + default: () => { + return {name: 'graphql-loader', setup: vi.fn()} + }, + }, +})) + +const extension1 = await testUIExtension({type: 'ui_extension', handle: 'h1', directory: '/extensions/ui_extension_1'}) +const extension2 = await testUIExtension({type: 'ui_extension', directory: '/extensions/ui_extension_2'}) + +describe('app-watcher-esbuild', () => { + test('creating contexts', async () => { + // Given + const options: DevAppWatcherOptions = { + dotEnvVariables: {key: 'value'}, + url: 'http://localhost:3000', + outputPath: '/path/to/output', + } + const manager = new ESBuildContextManager(options) + const extensions = [extension1, extension2] + + // When + await manager.createContexts(extensions) + + // Then + expect(manager.contexts).toHaveProperty('h1') + expect(manager.contexts).toHaveProperty('test-ui-extension') + }) + + test('deleting contexts', async () => { + // Given + const options: DevAppWatcherOptions = { + dotEnvVariables: {key: 'value'}, + url: 'http://localhost:3000', + outputPath: '/path/to/output', + } + const manager = new ESBuildContextManager(options) + const extensions = [extension1, extension2] + await manager.createContexts(extensions) + + // When + await manager.deleteContexts([extension1]) + + // Then + expect(manager.contexts).not.toHaveProperty('h1') + expect(manager.contexts).toHaveProperty('test-ui-extension') + }) + + test('updating contexts with an app event', async () => { + // Given + const options: DevAppWatcherOptions = { + dotEnvVariables: {key: 'value'}, + url: 'http://localhost:3000', + outputPath: '/path/to/output', + } + const manager = new ESBuildContextManager(options) + await manager.createContexts([extension2]) + + const appEvent: AppEvent = { + app: testApp(), + path: '', + startTime: [0, 0], + extensionEvents: [ + {type: EventType.Created, extension: extension1}, + {type: EventType.Deleted, extension: extension2}, + ], + } + + // When + await manager.updateContexts(appEvent) + + // Then + expect(manager.contexts).toHaveProperty('h1') + expect(manager.contexts).not.toHaveProperty('test-ui-extension') + }) +}) diff --git a/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.ts b/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.ts new file mode 100644 index 0000000000..d9a20fc094 --- /dev/null +++ b/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.ts @@ -0,0 +1,90 @@ +import {AppEvent, EventType} from './app-event-watcher.js' +import {ExtensionInstance} from '../../../models/extensions/extension-instance.js' +import {getESBuildOptions} from '../../extensions/bundle.js' +import {BuildContext, BuildOptions, context as esContext} from 'esbuild' +import {AbortSignal} from '@shopify/cli-kit/node/abort' +import {Writable} from 'stream' + +export interface DevAppWatcherOptions { + dotEnvVariables: {[key: string]: string} + url: string + outputPath: string + stderr?: Writable + stdout?: Writable + signal?: AbortSignal +} + +/** + * Class to manage the ESBuild contexts for the app watcher. + * Has a list of all active contexts and methods to create, update and delete them. + */ +export class ESBuildContextManager { + contexts: {[key: string]: BuildContext} + outputPath: string + dotEnvVariables: {[key: string]: string} + url: string + stderr?: Writable + stdout?: Writable + signal?: AbortSignal + + constructor(options: DevAppWatcherOptions) { + this.dotEnvVariables = options.dotEnvVariables + this.url = options.url + this.outputPath = options.outputPath + this.stderr = options.stderr + this.stdout = options.stdout + this.signal = options.signal + this.contexts = {} + + options.signal?.addEventListener('abort', async () => { + const allDispose = Object.values(this.contexts).map((context) => context.dispose()) + await Promise.all(allDispose) + }) + } + + async createContexts(extensions: ExtensionInstance[]) { + const promises = extensions.map(async (extension) => { + const esbuildOptions = getESBuildOptions({ + minify: false, + outputPath: extension.getOutputPathForDirectory(this.outputPath), + environment: 'development', + env: { + ...this.dotEnvVariables, + APP_URL: this.url, + }, + stdin: { + contents: extension.getBundleExtensionStdinContent(), + resolveDir: extension.directory, + loader: 'tsx', + }, + stderr: this.stderr ?? process.stderr, + stdout: this.stdout ?? process.stdout, + sourceMaps: true, + }) + + const context = await esContext(esbuildOptions) + this.contexts[extension.handle] = context + }) + + await Promise.all(promises) + } + + async updateContexts(appEvent: AppEvent) { + this.dotEnvVariables = appEvent.app.dotenv?.variables ?? {} + const createdEsBuild = appEvent.extensionEvents + .filter((extEvent) => extEvent.type === EventType.Created && extEvent.extension.isESBuildExtension) + .map((extEvent) => extEvent.extension) + await this.createContexts(createdEsBuild) + + const deletedEsBuild = appEvent.extensionEvents + .filter((extEvent) => extEvent.type === EventType.Deleted && extEvent.extension.isESBuildExtension) + .map((extEvent) => extEvent.extension) + await this.deleteContexts(deletedEsBuild) + } + + async deleteContexts(extensions: ExtensionInstance[]) { + const promises = extensions.map((ext) => this.contexts[ext.handle]?.dispose()) + await Promise.all(promises) + extensions.forEach((ext) => delete this.contexts[ext.handle]) + } +} diff --git a/packages/app/src/cli/services/dev/processes/dev-session.ts b/packages/app/src/cli/services/dev/processes/dev-session.ts index 13f4ba9d2b..9e19d992fc 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session.ts @@ -1,11 +1,10 @@ -/* eslint-disable no-case-declarations */ import {BaseProcess, DevProcessFunction} from './types.js' import {DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js' import {AppInterface} from '../../../models/app/app.js' import {getExtensionUploadURL} from '../../deploy/upload.js' import {AppEventWatcher, EventType} from '../app-events/app-event-watcher.js' import {performActionWithRetryAfterRecovery} from '@shopify/cli-kit/common/retry' -import {fileExistsSync, mkdir, readFileSync, rmdir, writeFile} from '@shopify/cli-kit/node/fs' +import {readFileSync, writeFile} from '@shopify/cli-kit/node/fs' import {dirname, joinPath} from '@shopify/cli-kit/node/path' import {AbortSignal} from '@shopify/cli-kit/node/abort' import {zip} from '@shopify/cli-kit/node/archiver' @@ -66,17 +65,13 @@ export const pushUpdatesForDevSession: DevProcessFunction = a return developerPlatformClient.refreshToken() } - const bundlePath = joinPath(app.directory, '.shopify', 'bundle') - if (fileExistsSync(bundlePath)) await rmdir(bundlePath, {force: true}) - await mkdir(bundlePath) + const appWatcher = new AppEventWatcher(app, options.url, {stderr, stdout, signal}) - const processOptions = {...options, stderr, stdout, signal, bundlePath} - const appWatcher = new AppEventWatcher(app, processOptions) + const processOptions = {...options, stderr, stdout, signal, bundlePath: appWatcher.buildOutputPath} outputWarn('-----> Using DEV SESSIONS <-----') processOptions.stdout.write('Preparing dev session...') - await initialBuild(processOptions) await bundleExtensionsAndUpload(processOptions, false) appWatcher.onEvent(async (event) => { @@ -85,32 +80,19 @@ export const pushUpdatesForDevSession: DevProcessFunction = a // Remove aborted controllers from array: bundleControllers = bundleControllers.filter((controller) => !controller.signal.aborted) - const promises = event.extensionEvents.map(async (eve) => { + event.extensionEvents.map((eve) => { switch (eve.type) { case EventType.Created: - case EventType.UpdatedSourceFile: - const message = eve.type === EventType.Created ? '✅ Extension created ' : '🔄 Extension Updated' - processOptions.stdout.write(`${message} ->> ${eve.extension.handle}`) - return eve.extension.buildForBundle( - {...processOptions, app: event.app, environment: 'development'}, - processOptions.bundlePath, - undefined, - ) + processOptions.stdout.write(`✅ Extension created ->> ${eve.extension.handle}`) + break case EventType.Deleted: processOptions.stdout.write(`❌ Extension deleted ->> ${eve.extension.handle}`) - return rmdir(joinPath(processOptions.bundlePath, eve.extension.handle), {force: true}) + break case EventType.Updated: processOptions.stdout.write(`🔄 Extension Updated ->> ${eve.extension.handle}`) break } }) - try { - await Promise.all(promises) - // eslint-disable-next-line no-catch-all/no-catch-all, @typescript-eslint/no-explicit-any - } catch (error: any) { - processOptions.stderr.write('Error building extensions') - processOptions.stderr.write(error.message) - } const networkStartTime = startHRTime() await performActionWithRetryAfterRecovery(async () => { @@ -132,23 +114,6 @@ export const pushUpdatesForDevSession: DevProcessFunction = a processOptions.stdout.write(`Dev session ready, watching for changes in your app`) } -/** - * Build all extensions for the initial bundle - * All subsequent changes in extensions will trigger individual builds - * - * @param options - The options for the process - */ -async function initialBuild(options: DevSessionProcessOptions) { - const allPromises = options.app.realExtensions.map((extension) => { - return extension.buildForBundle( - {...options, app: options.app, environment: 'development'}, - options.bundlePath, - undefined, - ) - }) - await Promise.all(allPromises) -} - /** * Bundle all extensions and upload them to the developer platform * Generate a new manifest in the bundle folder, zip it and upload it to GCS. diff --git a/packages/app/src/cli/services/extensions/bundle.ts b/packages/app/src/cli/services/extensions/bundle.ts index acd584b8b6..9f1b93ab22 100644 --- a/packages/app/src/cli/services/extensions/bundle.ts +++ b/packages/app/src/cli/services/extensions/bundle.ts @@ -120,7 +120,7 @@ function onResult(result: Awaited> | null, options: B } } -function getESBuildOptions(options: BundleOptions, processEnv = process.env): Parameters[0] { +export function getESBuildOptions(options: BundleOptions, processEnv = process.env): Parameters[0] { const validEnvs = pickBy(processEnv, (value, key) => EsbuildEnvVarRegex.test(key) && value) const env: {[variable: string]: string | undefined} = {...options.env, ...validEnvs}