diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index 6da9a76ebf7..948d68bb7e8 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -324,10 +324,6 @@ export class ExtensionInstance { stdout = {write: vi.fn()} stderr = {write: vi.fn()} abortController = new AbortController() + + // Mock buildForBundle on all test extensions so the watcher doesn't attempt real builds + const allExtensions = [ + extension1, + extension1B, + extension2, + extension1Updated, + extension1BUpdated, + flowExtension, + posExtension, + posExtensionUpdated, + appAccessExtension, + webhookExtension, + ] + for (const ext of allExtensions) { + vi.spyOn(ext, 'buildForBundle').mockResolvedValue() + vi.spyOn(ext, 'rescanImports').mockResolvedValue(false) + } }) afterEach(() => { @@ -360,9 +376,8 @@ describe('app-event-watcher', () => { configuration: testAppConfiguration, }) - const mockManager = new MockESBuildContextManager() const mockFileWatcher = new MockFileWatcher(app, outputOptions, [fileWatchEvent]) - const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockManager, mockFileWatcher) + const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockFileWatcher) const emitSpy = vi.spyOn(watcher, 'emit') await watcher.start({stdout, stderr, signal: abortController.signal}) @@ -433,9 +448,8 @@ describe('app-event-watcher', () => { }) const generateTypesSpy = vi.spyOn(app, 'generateExtensionTypes') - const mockManager = new MockESBuildContextManager() const mockFileWatcher = new MockFileWatcher(app, outputOptions, [fileWatchEvent]) - const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockManager, mockFileWatcher) + const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockFileWatcher) const emitSpy = vi.spyOn(watcher, 'emit') // When @@ -468,9 +482,8 @@ describe('app-event-watcher', () => { configuration: testAppConfiguration, }) - const mockManager = new MockESBuildContextManager() const mockFileWatcher = new MockFileWatcher(app, outputOptions, [fileWatchEvent]) - const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockManager, mockFileWatcher) + const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockFileWatcher) const emitSpy = vi.spyOn(watcher, 'emit') // When @@ -503,9 +516,8 @@ describe('app-event-watcher', () => { configuration: testAppConfiguration, }) - const mockManager = new MockESBuildContextManager() const mockFileWatcher = new MockFileWatcher(app, outputOptions, [fileWatchEvent]) - const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockManager, mockFileWatcher) + const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockFileWatcher) const emitSpy = vi.spyOn(watcher, 'emit') // When @@ -535,9 +547,8 @@ describe('app-event-watcher', () => { }) const generateTypesSpy = vi.spyOn(app, 'generateExtensionTypes') - const mockManager = new MockESBuildContextManager() const mockFileWatcher = new MockFileWatcher(app, outputOptions, [fileWatchEvent]) - const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockManager, mockFileWatcher) + const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockFileWatcher) const emitSpy = vi.spyOn(watcher, 'emit') // When @@ -571,19 +582,18 @@ describe('app-event-watcher', () => { ], } - const mockManager = new MockESBuildContextManager() - mockManager.rebuildContext = vi.fn().mockRejectedValueOnce(esbuildError) - const buildOutputPath = joinPath(tmpDir, '.shopify', 'bundle') const app = testAppLinked({ allExtensions: [extension1], configPath: 'shopify.app.custom.toml', configuration: testAppConfiguration, }) + // First call succeeds (initial build on start), second call fails (file watcher triggered build) + vi.spyOn(extension1, 'buildForBundle').mockResolvedValueOnce().mockRejectedValueOnce(esbuildError) const mockFileWatcher = new MockFileWatcher(app, outputOptions, [fileWatchEvent]) // When - const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockManager, mockFileWatcher) + const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockFileWatcher) const emitSpy = vi.spyOn(watcher, 'emit') const stderr = {write: vi.fn()} as unknown as Writable const stdout = {write: vi.fn()} as unknown as Writable @@ -627,9 +637,9 @@ describe('app-event-watcher', () => { }) // When - const mockManager = new MockESBuildContextManager() + const mockFileWatcher = new MockFileWatcher(app, outputOptions, [fileWatchEvent]) - const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockManager, mockFileWatcher) + const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockFileWatcher) const emitSpy = vi.spyOn(watcher, 'emit') const stderr = {write: vi.fn()} as unknown as Writable const stdout = {write: vi.fn()} as unknown as Writable @@ -661,13 +671,14 @@ describe('app-event-watcher', () => { configPath: 'shopify.app.custom.toml', configuration: testAppConfiguration, }) + + // Make rescanImports throw to simulate an uncaught error in the watcher pipeline + vi.spyOn(extension1, 'rescanImports').mockRejectedValueOnce(uncaughtError) + const mockFileWatcher = new MockFileWatcher(app, outputOptions, [fileWatchEvent]) // When - const mockManager = new MockESBuildContextManager() - mockManager.updateContexts = vi.fn().mockRejectedValueOnce(uncaughtError) - - const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockManager, mockFileWatcher) + const watcher = new AppEventWatcher(app, 'url', buildOutputPath, mockFileWatcher) const emitSpy = vi.spyOn(watcher, 'emit') const stderr = {write: vi.fn()} as unknown as Writable const stdout = {write: vi.fn()} as unknown as Writable @@ -684,26 +695,6 @@ describe('app-event-watcher', () => { }) }) }) -// Mock class for ESBuildContextManager -// It handles the ESBuild contexts for the extensions that are being watched -class MockESBuildContextManager extends ESBuildContextManager { - contexts = { - // The keys are the extension handles, the values are the ESBuild contexts mocked - uid1: [{rebuild: vi.fn(), watch: vi.fn(), serve: vi.fn(), cancel: vi.fn(), dispose: vi.fn()}], - uid1B: [{rebuild: vi.fn(), watch: vi.fn(), serve: vi.fn(), cancel: vi.fn(), dispose: vi.fn()}], - uid2: [{rebuild: vi.fn(), watch: vi.fn(), serve: vi.fn(), cancel: vi.fn(), dispose: vi.fn()}], - 'test-ui-extension': [{rebuild: vi.fn(), watch: vi.fn(), serve: vi.fn(), cancel: vi.fn(), dispose: vi.fn()}], - } - - constructor() { - super({dotEnvVariables: {}, url: 'url', outputPath: 'outputPath'}) - } - - async createContexts(extensions: ExtensionInstance[]) {} - async updateContexts(appEvent: AppEvent) {} - async deleteContexts(extensions: ExtensionInstance[]) {} -} - // Mock class for FileWatcher // Used to trigger mocked file system events immediately after the watcher is started. class MockFileWatcher extends FileWatcher { 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 686cfb17bdb..5bda637f460 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,11 +1,9 @@ /* eslint-disable tsdoc/syntax */ import {FileWatcher, OutputContextOptions} from './file-watcher.js' -import {ESBuildContextManager} from './app-watcher-esbuild.js' import {handleWatcherEvents} from './app-event-watcher-handler.js' import {AppLinkedInterface} from '../../../models/app/app.js' import {ExtensionInstance} from '../../../models/extensions/extension-instance.js' import {ExtensionBuildOptions} from '../../build/extension.js' -import {formatBundleSize} from '../../build/bundle-size.js' import {outputDebug} from '@shopify/cli-kit/node/output' import {AbortSignal} from '@shopify/cli-kit/node/abort' import {joinPath} from '@shopify/cli-kit/node/path' @@ -95,33 +93,18 @@ export class AppEventWatcher extends EventEmitter { private app: AppLinkedInterface private options: OutputContextOptions private readonly appURL?: string - private readonly esbuildManager: ESBuildContextManager private started = false private ready = false private initialEvents: ExtensionEvent[] = [] private fileWatcher?: FileWatcher - constructor( - app: AppLinkedInterface, - appURL?: string, - buildOutputPath?: string, - esbuildManager?: ESBuildContextManager, - fileWatcher?: FileWatcher, - ) { + constructor(app: AppLinkedInterface, appURL?: string, buildOutputPath?: string, fileWatcher?: FileWatcher) { super() this.app = app this.appURL = appURL this.buildOutputPath = buildOutputPath ?? joinPath(app.directory, '.shopify', 'dev-bundle') // Default options, to be overwritten by the start method this.options = {stdout: process.stdout, stderr: process.stderr, signal: new AbortSignal()} - this.esbuildManager = - esbuildManager ?? - new ESBuildContextManager({ - outputPath: this.buildOutputPath, - dotEnvVariables: this.app.dotenv?.variables ?? {}, - url: this.appURL ?? '', - ...this.options, - }) this.fileWatcher = fileWatcher } @@ -130,15 +113,11 @@ export class AppEventWatcher extends EventEmitter { this.started = true this.options = options ?? this.options - this.esbuildManager.setAbortSignal(this.options.signal) // If there is a previous build folder, delete it if (await fileExists(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 if (buildExtensionsFirst) { this.initialEvents = this.app.realExtensions.map((ext) => ({type: EventType.Updated, extension: ext})) @@ -154,8 +133,6 @@ export class AppEventWatcher extends EventEmitter { this.app = appEvent.app if (appEvent.appWasReloaded) this.fileWatcher?.updateApp(this.app) - await this.esbuildManager.updateContexts(appEvent) - await this.rescanImports(appEvent) // Find affected created/updated extensions and build them @@ -237,9 +214,7 @@ export class AppEventWatcher extends EventEmitter { } /** - * Builds all given extensions. - * ESBuild extensions will be built using their own ESBuild context, other extensions will be built using the default - * buildForBundle method. + * Builds all given extensions using the default buildForBundle method. */ private async buildExtensions(extensionEvents: ExtensionEvent[]) { // Group events by extension to handle multiple events for the same extension @@ -258,13 +233,7 @@ export class AppEventWatcher extends EventEmitter { const promises = groupedExtensionEvents.map(async ({extension: ext, events}) => { return useConcurrentOutputContext({outputPrefix: ext.handle, stripAnsi: false}, async () => { try { - if (this.esbuildManager.contexts?.[ext.uid]?.length) { - await this.esbuildManager.rebuildContext(ext) - const sizeInfo = await formatBundleSize(ext.outputPath) - this.options.stdout.write(`Build successful${sizeInfo}`) - } else { - await this.buildExtension(ext) - } + await this.buildExtension(ext) // Update all events for this extension with success result const buildResult = {status: 'ok' as const, uid: ext.uid} events.forEach((event) => { @@ -309,7 +278,7 @@ export class AppEventWatcher extends EventEmitter { } /** - * Build a single non-esbuild extension using the default buildForBundle method. + * Build a single extension using the default buildForBundle method. * @param extension - The extension to build */ private async buildExtension(extension: ExtensionInstance): Promise { 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 deleted file mode 100644 index 0a1a7256f02..00000000000 --- a/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import {DevAppWatcherOptions, ESBuildContextManager} from './app-watcher-esbuild.js' -import {AppEvent, EventType} from './app-event-watcher.js' -import {testAppLinked, testUIExtension} from '../../../models/app/app.test-data.js' -import {environmentVariableNames} from '../../../constants.js' -import {describe, expect, test, vi} from 'vitest' -import * as fs from '@shopify/cli-kit/node/fs' -import {joinPath} from '@shopify/cli-kit/node/path' - -vi.mock('@luckycatfactory/esbuild-graphql-loader', () => ({ - default: { - default: () => { - return {name: 'graphql-loader', setup: vi.fn()} - }, - }, -})) - -const extension1 = await testUIExtension({ - type: 'ui_extension', - directory: '/extensions/ui_extension_1', - uid: 'uid1', -}) -const extension2 = await testUIExtension({ - type: 'ui_extension', - directory: '/extensions/ui_extension_2', - uid: 'uid2', -}) - -describe('app-watcher-esbuild', () => { - const options: DevAppWatcherOptions = { - dotEnvVariables: {key: 'value'}, - url: 'http://localhost:3000', - outputPath: '/path/to/output', - } - - test('creating contexts', async () => { - // Given - const manager = new ESBuildContextManager(options) - const extensions = [extension1, extension2] - - // When - await manager.createContexts(extensions) - - // Then - expect(manager.contexts).toHaveProperty('uid1') - expect(manager.contexts).toHaveProperty('uid2') - }) - - test('creating multiple contexts for the same extension', async () => { - // Given - const options: DevAppWatcherOptions = { - dotEnvVariables: {key: 'value'}, - url: 'http://localhost:3000', - outputPath: '/path/to/output', - } - const manager = new ESBuildContextManager(options) - const extension = await testUIExtension({ - uid: 'conditional-extension-uid', - configuration: { - ...extension2.configuration, - handle: 'conditional-extension', - extension_points: [ - { - target: 'target1', - module: 'module1', - should_render: { - module: 'shouldRenderModule1', - }, - build_manifest: { - assets: { - main: { - module: 'module1', - filepath: '/conditional-extension.js', - }, - should_render: { - module: 'shouldRenderModule1', - filepath: '/conditional-extension-conditions.js', - }, - }, - }, - }, - ], - }, - }) - - // When - await manager.createContexts([extension]) - - // Then - expect(manager.contexts).toHaveProperty('conditional-extension-uid') - expect(manager.contexts['conditional-extension-uid']).toHaveLength(2) - }) - - test('deleting contexts', async () => { - // Given - const manager = new ESBuildContextManager(options) - const extensions = [extension1, extension2] - await manager.createContexts(extensions) - - // When - await manager.deleteContexts([extension1]) - - // Then - expect(manager.contexts).not.toHaveProperty('uid1') - expect(manager.contexts).toHaveProperty('uid2') - }) - - test('updating contexts with an app event', async () => { - // Given - const manager = new ESBuildContextManager(options) - await manager.createContexts([extension2]) - - const appEvent: AppEvent = { - app: testAppLinked(), - 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('uid1') - expect(manager.contexts).not.toHaveProperty('uid2') - }) - - test('updating contexts with an app event when the app was reloaded', async () => { - // Given - const manager = new ESBuildContextManager(options) - await manager.createContexts([extension1]) - const originalContext = manager.contexts.uid1![0]! - const appEvent: AppEvent = { - app: testAppLinked(), - path: '', - startTime: [0, 0], - extensionEvents: [{type: EventType.Updated, extension: extension1}], - appWasReloaded: true, - } - - // When - await manager.updateContexts(appEvent) - - // Then - expect(manager.contexts).toHaveProperty('uid1') - expect(manager.contexts.uid1!.length).toBe(1) - expect(manager.contexts.uid1![0]).not.toBe(originalContext) - }) - - test('rebuilding contexts', async () => { - // Given - const manager = new ESBuildContextManager(options) - await manager.createContexts([extension1]) - const spyContext = vi.spyOn(manager.contexts.uid1![0]!, 'rebuild').mockResolvedValue({} as any) - const spyCopyStaticAssets = vi.spyOn(extension1, 'copyStaticAssets').mockResolvedValue() - const spyCopy = vi.spyOn(fs, 'copyFile').mockResolvedValue() - - // When - await manager.rebuildContext(extension1) - - // Then - expect(spyContext).toHaveBeenCalled() - expect(spyCopyStaticAssets).toHaveBeenCalledWith('/path/to/output/uid1/dist/test-ui-extension.js') - expect(spyCopy).toHaveBeenCalledWith('/path/to/output/uid1/dist', '/extensions/ui_extension_1/dist') - }) - - /** - * Creates and builds an extension with a simple JS file - * @param tmpDir - The temporary directory to use - * @returns The built file content and the manager instance - */ - async function buildExtension(tmpDir: string) { - // Create source directory and simple test file - const extSrcDir = joinPath(tmpDir, 'src') - await fs.mkdir(extSrcDir) - const entryFile = joinPath(extSrcDir, 'index.js') - const simpleJS = 'function hello(){console.log("hi")};hello();' - await fs.writeFile(entryFile, simpleJS) - - // Create test extension - const ext = await testUIExtension({ - directory: extSrcDir, - entrySourceFilePath: entryFile, - }) - - // Configure stdin content - ext.getBundleExtensionStdinContent = () => ({main: simpleJS, assets: []}) - - // Set up manager and build - const outputRoot = joinPath(tmpDir, 'out') - const mgr = new ESBuildContextManager({dotEnvVariables: {}, url: 'http://localhost', outputPath: outputRoot}) - - await mgr.createContexts([ext]) - await mgr.rebuildContext(ext) - - // Get the built file - const builtFile = joinPath(outputRoot, ext.getOutputFolderId(), 'dist', `${ext.handle}.js`) - await expect(fs.fileExists(builtFile)).resolves.toBe(true) - const content = await fs.readFile(builtFile, {encoding: 'utf8'}) - - return content - } - - test('minification is enabled by default', async () => { - await fs.inTemporaryDirectory(async (tmpDir) => { - // Given / When - const content = await buildExtension(tmpDir) - - // Then - expect(content).not.toContain('hello') - }) - }) - - test('minification is disabled when SHOPIFY_CLI_APP_DEV_DISABLE_MINIFICATION is truthy', async () => { - await fs.inTemporaryDirectory(async (tmpDir) => { - // Given - vi.stubEnv(environmentVariableNames.disableMinificationOnDev, '1') - - // When - const content = await buildExtension(tmpDir) - - // Then - expect(content).toContain('hello') - }) - }) -}) 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 deleted file mode 100644 index edf39052082..00000000000 --- a/packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.ts +++ /dev/null @@ -1,147 +0,0 @@ -import {AppEvent, EventType} from './app-event-watcher.js' -import {ExtensionInstance} from '../../../models/extensions/extension-instance.js' -import {getESBuildOptions} from '../../extensions/bundle.js' -import {environmentVariableNames} from '../../../constants.js' -import {BuildContext, context as esContext, StdinOptions} from 'esbuild' -import {AbortSignal} from '@shopify/cli-kit/node/abort' -import {copyFile} from '@shopify/cli-kit/node/fs' -import {dirname, joinPath} from '@shopify/cli-kit/node/path' -import {isTruthy} from '@shopify/cli-kit/node/context/utilities' -import {getEnvironmentVariables} from '@shopify/cli-kit/node/environment' - -export interface DevAppWatcherOptions { - dotEnvVariables: {[key: string]: string} - url: string - outputPath: string -} - -/** - * 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 - signal?: AbortSignal - - constructor(options: DevAppWatcherOptions) { - this.dotEnvVariables = options.dotEnvVariables - this.url = options.url - this.outputPath = options.outputPath - this.contexts = {} - } - - setAbortSignal(signal: AbortSignal) { - this.signal = signal - this.signal?.addEventListener('abort', async () => { - const allDispose = Object.values(this.contexts) - .map((context) => context.map((ctxt) => ctxt.dispose())) - .flat() - await Promise.all(allDispose) - }) - } - - async createContexts(extensions: ExtensionInstance[]) { - const promises = extensions.map(async (extension) => { - const {main, assets} = extension.getBundleExtensionStdinContent() - const mainOutputPath = extension.getOutputPathForDirectory(this.outputPath) - const esbuildOptions = await this.extensionEsBuildOptions( - { - contents: main, - resolveDir: extension.directory, - loader: 'tsx', - }, - mainOutputPath, - ) - const mainContextPromise = esContext(esbuildOptions) - - const assetContextPromises = (assets ?? []).map(async (asset) => { - const esbuildOptions = await this.extensionEsBuildOptions( - { - contents: asset.content, - resolveDir: extension.directory, - loader: 'ts', - }, - joinPath(dirname(mainOutputPath), asset.outputFileName), - ) - return esContext(esbuildOptions) - }) - - this.contexts[extension.uid] = await Promise.all(assetContextPromises.concat(mainContextPromise)) - }) - - await Promise.all(promises) - } - - async rebuildContext(extension: ExtensionInstance) { - const context = this.contexts[extension.uid] - if (!context) return - await Promise.all(context.map((ctxt) => ctxt.rebuild())) - const devBundleOutputPath = extension.getOutputPathForDirectory(this.outputPath) - - // Copy static assets after build completes - // Pass in an explicit output path because the extension.outputPath is not the same as the dev bundle output path. - await extension.copyStaticAssets(devBundleOutputPath) - - // The default output path for a extension is now inside `.shopify/bundle//dist`, - // all extensions output need to be under the same directory so that it can all be zipped together. - - // But historically the output was inside each extension's directory. - // To avoid breaking flows that depend on this, we copy the output to the old location. - // This also makes it easier to access sourcemaps or other built artifacts. - const outputPath = dirname(devBundleOutputPath) - const copyPath = dirname(extension.outputPath) - await copyFile(outputPath, copyPath) - } - - /** - * New contexts are created for new extensions that were added during a dev session. - * We also need to recreate contexts for UI extensions whose TOML files were updated. - * This is because some changes in the TOML invalidate the current context (changing targets or handle for example) - */ - async updateContexts(appEvent: AppEvent) { - this.dotEnvVariables = appEvent.app.dotenv?.variables ?? {} - const createdEsBuild = appEvent.extensionEvents - .filter((extEvent) => extEvent.extension.isESBuildExtension) - .filter((extEvent) => extEvent.type === 'created' || (extEvent.type === 'changed' && appEvent.appWasReloaded)) - .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.uid]?.map((context) => context.dispose())).flat() - await Promise.all(promises) - extensions.forEach((ext) => { - const {[ext.uid]: _, ...rest} = this.contexts - this.contexts = rest - }) - } - - private async extensionEsBuildOptions(stdin: StdinOptions, outputPath: string) { - const env = getEnvironmentVariables() - const disableMinificationOnDev = env[environmentVariableNames.disableMinificationOnDev] - const minify = !isTruthy(disableMinificationOnDev) - return getESBuildOptions({ - minify, - outputPath, - environment: 'development', - env: { - ...this.dotEnvVariables, - APP_URL: this.url, - }, - stdin, - logLevel: 'silent', - // stdout and stderr are mandatory, but not actually used - stderr: process.stderr, - stdout: process.stdout, - sourceMaps: true, - }) - } -} diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts index 1e9602ce520..6d99f1049cd 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts @@ -162,10 +162,14 @@ export class DevSession { const errors = this.parseBuildErrors(event) if (errors.length) { await this.logger.logMultipleErrors(errors) - throw new AbortError('Dev preview aborted, build errors detected in extensions') + await setImmediate(() => { + const affected = errors.map((err) => err.prefix) + throw new AbortError(`Dev preview aborted, build errors detected in extensions: ${affected}`) + }) + } else { + const result = await this.bundleExtensionsAndUpload(event) + await this.handleDevSessionResult(result, event) } - const result = await this.bundleExtensionsAndUpload(event) - await this.handleDevSessionResult(result, event) } /** diff --git a/packages/app/src/cli/services/extensions/bundle.ts b/packages/app/src/cli/services/extensions/bundle.ts index 67a4ad0cc2b..b41ab0c9e28 100644 --- a/packages/app/src/cli/services/extensions/bundle.ts +++ b/packages/app/src/cli/services/extensions/bundle.ts @@ -139,7 +139,7 @@ function onResult(result: EsbuildResult | null, options: BundleOptions) { } } -export function getESBuildOptions(options: BundleOptions, processEnv = process.env): Parameters[0] { +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} @@ -150,6 +150,7 @@ export function getESBuildOptions(options: BundleOptions, processEnv = process.e }), {'process.env.NODE_ENV': JSON.stringify(options.environment)}, ) + const esbuildOptions: Parameters[0] = { outfile: options.outputPath, stdin: options.stdin, @@ -175,6 +176,11 @@ export function getESBuildOptions(options: BundleOptions, processEnv = process.e if (options.environment === 'production') { esbuildOptions.metafile = true } + if (options.environment === 'development') { + const disableMinificationOnDev = env[environmentVariableNames.disableMinificationOnDev] + esbuildOptions.minify = !isTruthy(disableMinificationOnDev) + esbuildOptions.logLevel = 'silent' + } return esbuildOptions }