diff --git a/packages/ngtools/webpack/src/transformers/find_image_domains.ts b/packages/ngtools/webpack/src/transformers/find_image_domains.ts index 9f9a7e7ef9be..bdce8dd6cd7e 100644 --- a/packages/ngtools/webpack/src/transformers/find_image_domains.ts +++ b/packages/ngtools/webpack/src/transformers/find_image_domains.ts @@ -84,7 +84,7 @@ export function findImageDomains(imageDomains: Set): ts.TransformerFacto return node; } - function findPropertyAssignment(node: ts.Node) { + function findProvidersAssignment(node: ts.Node) { if (ts.isPropertyAssignment(node)) { if (ts.isIdentifier(node.name) && node.name.escapedText === 'providers') { ts.visitEachChild(node.initializer, findImageLoaders, context); @@ -94,22 +94,56 @@ export function findImageDomains(imageDomains: Set): ts.TransformerFacto return node; } + function findFeaturesAssignment(node: ts.Node) { + if (ts.isPropertyAssignment(node)) { + if ( + ts.isIdentifier(node.name) && + node.name.escapedText === 'features' && + ts.isArrayLiteralExpression(node.initializer) + ) { + const providerElement = node.initializer.elements.find(isProvidersFeatureElement); + if ( + providerElement && + ts.isCallExpression(providerElement) && + providerElement.arguments[0] + ) { + ts.visitEachChild(providerElement.arguments[0], findImageLoaders, context); + } + } + } + + return node; + } + + function isProvidersFeatureElement(node: ts.Node): Boolean { + return ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && + node.expression.expression.escapedText === 'i0' && + ts.isIdentifier(node.expression.name) && + node.expression.name.escapedText === 'ɵɵProvidersFeature' + ); + } + function findPropertyDeclaration(node: ts.Node) { if ( ts.isPropertyDeclaration(node) && ts.isIdentifier(node.name) && - node.name.escapedText === 'ɵinj' && node.initializer && ts.isCallExpression(node.initializer) && node.initializer.arguments[0] ) { - ts.visitEachChild(node.initializer.arguments[0], findPropertyAssignment, context); + if (node.name.escapedText === 'ɵinj') { + ts.visitEachChild(node.initializer.arguments[0], findProvidersAssignment, context); + } else if (node.name.escapedText === 'ɵcmp') { + ts.visitEachChild(node.initializer.arguments[0], findFeaturesAssignment, context); + } } return node; } - // Continue traversal if node is ClassDeclaration and has name "AppModule" function findClassDeclaration(node: ts.Node) { if (ts.isClassDeclaration(node)) { ts.visitEachChild(node, findPropertyDeclaration, context); diff --git a/packages/ngtools/webpack/src/transformers/find_image_domains_spec.ts b/packages/ngtools/webpack/src/transformers/find_image_domains_spec.ts index 30876c59ef2d..2e9322d447d4 100644 --- a/packages/ngtools/webpack/src/transformers/find_image_domains_spec.ts +++ b/packages/ngtools/webpack/src/transformers/find_image_domains_spec.ts @@ -28,7 +28,7 @@ function findDomains( return domains; } -function inputTemplate(provider: string) { +function inputTemplateAppModule(provider: string) { /* eslint-disable max-len */ return tags.stripIndent` export class AppModule { @@ -56,38 +56,107 @@ function inputTemplate(provider: string) { }], null, null); })(); (function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(AppModule, { declarations: [AppComponent], imports: [BrowserModule, NgOptimizedImage] }); })(); - `; + `; +} + +function inputTemplateComponent(provider: string) { + /* eslint-disable max-len */ + return tags.stripIndent` + export class AppComponent { + title = 'angular-cli-testbed'; + static ɵfac = function AppComponent_Factory(t) { return new (t || AppComponent)(); }; + static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: AppComponent, selectors: [["app-root"]], standalone: true, features: [i0.ɵɵProvidersFeature([ + ${provider} + ]), i0.ɵɵStandaloneFeature], decls: 2, vars: 0, template: function AppComponent_Template(rf, ctx) { if (rf & 1) { + i0.ɵɵelementStart(0, "div"); + i0.ɵɵtext(1, "Hello world"); + i0.ɵɵelementEnd(); + } } }); + } + (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AppComponent, [{ + type: Component, + args: [{ selector: 'app-root', imports: [NgOptimizedImage, NgSwitchCase, NgSwitchDefault, NgSwitch], standalone: true, providers: [ + ${provider} + ], template: "
Hello world
\n\n" }] + }], null, null); })(); + `; +} + +function runSharedTests(template: (povider: string) => string) { + it('should find a domain when a built-in loader is used with a string-literal-like argument', () => { + // Intentionally inconsistent use of quote styles in this data structure: + const builtInLoaders: Array<[string, string]> = [ + ['provideCloudflareLoader("www.cloudflaredomain.com")', 'www.cloudflaredomain.com'], + [ + "provideCloudinaryLoader('https://www.cloudinarydomain.net')", + 'https://www.cloudinarydomain.net', + ], + ['provideImageKitLoader("www.imageKitdomain.com")', 'www.imageKitdomain.com'], + ['provideImgixLoader(`www.imgixdomain.com/images/`)', 'www.imgixdomain.com/images/'], + ]; + for (const loader of builtInLoaders) { + const input = template(loader[0]); + const result = Array.from(findDomains(input)); + expect(result.length).toBe(1); + expect(result[0]).toBe(loader[1]); + } + }); + + it('should find a domain in a custom loader function with a template literal', () => { + const customLoader = tags.stripIndent` + { + provide: IMAGE_LOADER, + useValue: (config: ImageLoaderConfig) => { + return ${'`https://customLoaderTemplate.com/images?src=${config.src}&width=${config.width}`'}; + }, + },`; + const input = template(customLoader); + const result = Array.from(findDomains(input)); + expect(result.length).toBe(1); + expect(result[0]).toBe('https://customLoaderTemplate.com/'); + }); + + it('should find a domain when provider is alongside other providers', () => { + const customLoader = tags.stripIndent` + { + provide: SOME_OTHER_PROVIDER, + useValue: (config: ImageLoaderConfig) => { + return "https://notacustomloaderstring.com/images?src=" + config.src + "&width=" + config.width; + }, + }, + provideNotARealLoader("https://www.foo.com"), + { + provide: IMAGE_LOADER, + useValue: (config: ImageLoaderConfig) => { + return ${'`https://customloadertemplate.com/images?src=${config.src}&width=${config.width}`'}; + }, + }, + { + provide: YET_ANOTHER_PROVIDER, + useValue: (config: ImageLoaderConfig) => { + return ${'`https://notacustomloadertemplate.com/images?src=${config.src}&width=${config.width}`'}; + }, + },`; + const input = template(customLoader); + const result = Array.from(findDomains(input)); + expect(result.length).toBe(1); + expect(result[0]).toBe('https://customloadertemplate.com/'); + }); } describe('@ngtools/webpack transformers', () => { - describe('find_image_domains', () => { - it('should find a domain when a built-in loader is used with a string-literal-like argument', () => { - // Intentionally inconsistent use of quote styles in this data structure: - const builtInLoaders: Array<[string, string]> = [ - ['provideCloudflareLoader("www.cloudflaredomain.com")', 'www.cloudflaredomain.com'], - [ - "provideCloudinaryLoader('https://www.cloudinarydomain.net')", - 'https://www.cloudinarydomain.net', - ], - ['provideImageKitLoader("www.imageKitdomain.com")', 'www.imageKitdomain.com'], - ['provideImgixLoader(`www.imgixdomain.com/images/`)', 'www.imgixdomain.com/images/'], - ]; - for (const loader of builtInLoaders) { - const input = inputTemplate(loader[0]); - const result = Array.from(findDomains(input)); - expect(result.length).toBe(1); - expect(result[0]).toBe(loader[1]); - } - }); + describe('find_image_domains (app module)', () => { + runSharedTests(inputTemplateAppModule); + runSharedTests(inputTemplateComponent); it('should not find a domain when a built-in loader is used with a variable', () => { - const input = inputTemplate(`provideCloudflareLoader(myImageCDN)`); + const input = inputTemplateAppModule(`provideCloudflareLoader(myImageCDN)`); const result = Array.from(findDomains(input)); expect(result.length).toBe(0); }); it('should not find a domain when a built-in loader is used with an expression', () => { - const input = inputTemplate( + const input = inputTemplateAppModule( `provideCloudflareLoader("https://www." + (dev ? "dev." : "") + "cloudinarydomain.net")`, ); const result = Array.from(findDomains(input)); @@ -95,7 +164,7 @@ describe('@ngtools/webpack transformers', () => { }); it('should not find a domain when a built-in loader is used with a template literal', () => { - const input = inputTemplate( + const input = inputTemplateAppModule( 'provideCloudflareLoader(`https://www.${dev ? "dev." : ""}cloudinarydomain.net`)', ); const result = Array.from(findDomains(input)); @@ -103,25 +172,11 @@ describe('@ngtools/webpack transformers', () => { }); it('should not find a domain in a function that is not a built-in loader', () => { - const input = inputTemplate('provideNotARealLoader("https://www.foo.com")'); + const input = inputTemplateAppModule('provideNotARealLoader("https://www.foo.com")'); const result = Array.from(findDomains(input)); expect(result.length).toBe(0); }); - it('should find a domain in a custom loader function with a template literal', () => { - const customLoader = tags.stripIndent` - { - provide: IMAGE_LOADER, - useValue: (config: ImageLoaderConfig) => { - return ${'`https://customLoaderTemplate.com/images?src=${config.src}&width=${config.width}`'}; - }, - },`; - const input = inputTemplate(customLoader); - const result = Array.from(findDomains(input)); - expect(result.length).toBe(1); - expect(result[0]).toBe('https://customLoaderTemplate.com/'); - }); - it('should find a domain in a custom loader function with string concatenation', () => { const customLoader = tags.stripIndent` { @@ -130,7 +185,7 @@ describe('@ngtools/webpack transformers', () => { return "https://customLoaderString.com/images?src=" + config.src + "&width=" + config.width; }, },`; - const input = inputTemplate(customLoader); + const input = inputTemplateAppModule(customLoader); const result = Array.from(findDomains(input)); expect(result.length).toBe(1); expect(result[0]).toBe('https://customLoaderString.com/'); @@ -144,36 +199,9 @@ describe('@ngtools/webpack transformers', () => { return "https://customLoaderString.com/images?src=" + config.src + "&width=" + config.width; }, },`; - const input = inputTemplate(customLoader); + const input = inputTemplateAppModule(customLoader); const result = Array.from(findDomains(input)); expect(result.length).toBe(0); }); - - it('should find a domain when provider is alongside other providers', () => { - const customLoader = tags.stripIndent` - { - provide: SOME_OTHER_PROVIDER, - useValue: (config: ImageLoaderConfig) => { - return "https://notacustomloaderstring.com/images?src=" + config.src + "&width=" + config.width; - }, - }, - provideNotARealLoader("https://www.foo.com"), - { - provide: IMAGE_LOADER, - useValue: (config: ImageLoaderConfig) => { - return ${'`https://customloadertemplate.com/images?src=${config.src}&width=${config.width}`'}; - }, - }, - { - provide: YET_ANOTHER_PROVIDER, - useValue: (config: ImageLoaderConfig) => { - return ${'`https://notacustomloadertemplate.com/images?src=${config.src}&width=${config.width}`'}; - }, - },`; - const input = inputTemplate(customLoader); - const result = Array.from(findDomains(input)); - expect(result.length).toBe(1); - expect(result[0]).toBe('https://customloadertemplate.com/'); - }); }); });