Skip to content

Commit 16428fc

Browse files
atcastlealan-agius4
authored andcommitted
fix(@ngtools/webpack): adjust static scan to find image domains in standlone components
add a new path in the static scan to enable image domain identification to work on standalone components (cherry picked from commit cc055f7)
1 parent 3ec651d commit 16428fc

File tree

2 files changed

+134
-72
lines changed

2 files changed

+134
-72
lines changed

packages/ngtools/webpack/src/transformers/find_image_domains.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function findImageDomains(imageDomains: Set<string>): ts.TransformerFacto
8484
return node;
8585
}
8686

87-
function findPropertyAssignment(node: ts.Node) {
87+
function findProvidersAssignment(node: ts.Node) {
8888
if (ts.isPropertyAssignment(node)) {
8989
if (ts.isIdentifier(node.name) && node.name.escapedText === 'providers') {
9090
ts.visitEachChild(node.initializer, findImageLoaders, context);
@@ -94,22 +94,56 @@ export function findImageDomains(imageDomains: Set<string>): ts.TransformerFacto
9494
return node;
9595
}
9696

97+
function findFeaturesAssignment(node: ts.Node) {
98+
if (ts.isPropertyAssignment(node)) {
99+
if (
100+
ts.isIdentifier(node.name) &&
101+
node.name.escapedText === 'features' &&
102+
ts.isArrayLiteralExpression(node.initializer)
103+
) {
104+
const providerElement = node.initializer.elements.find(isProvidersFeatureElement);
105+
if (
106+
providerElement &&
107+
ts.isCallExpression(providerElement) &&
108+
providerElement.arguments[0]
109+
) {
110+
ts.visitEachChild(providerElement.arguments[0], findImageLoaders, context);
111+
}
112+
}
113+
}
114+
115+
return node;
116+
}
117+
118+
function isProvidersFeatureElement(node: ts.Node): Boolean {
119+
return (
120+
ts.isCallExpression(node) &&
121+
ts.isPropertyAccessExpression(node.expression) &&
122+
ts.isIdentifier(node.expression.expression) &&
123+
node.expression.expression.escapedText === 'i0' &&
124+
ts.isIdentifier(node.expression.name) &&
125+
node.expression.name.escapedText === 'ɵɵProvidersFeature'
126+
);
127+
}
128+
97129
function findPropertyDeclaration(node: ts.Node) {
98130
if (
99131
ts.isPropertyDeclaration(node) &&
100132
ts.isIdentifier(node.name) &&
101-
node.name.escapedText === 'ɵinj' &&
102133
node.initializer &&
103134
ts.isCallExpression(node.initializer) &&
104135
node.initializer.arguments[0]
105136
) {
106-
ts.visitEachChild(node.initializer.arguments[0], findPropertyAssignment, context);
137+
if (node.name.escapedText === 'ɵinj') {
138+
ts.visitEachChild(node.initializer.arguments[0], findProvidersAssignment, context);
139+
} else if (node.name.escapedText === 'ɵcmp') {
140+
ts.visitEachChild(node.initializer.arguments[0], findFeaturesAssignment, context);
141+
}
107142
}
108143

109144
return node;
110145
}
111146

112-
// Continue traversal if node is ClassDeclaration and has name "AppModule"
113147
function findClassDeclaration(node: ts.Node) {
114148
if (ts.isClassDeclaration(node)) {
115149
ts.visitEachChild(node, findPropertyDeclaration, context);

packages/ngtools/webpack/src/transformers/find_image_domains_spec.ts

Lines changed: 96 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function findDomains(
2828
return domains;
2929
}
3030

31-
function inputTemplate(provider: string) {
31+
function inputTemplateAppModule(provider: string) {
3232
/* eslint-disable max-len */
3333
return tags.stripIndent`
3434
export class AppModule {
@@ -56,72 +56,127 @@ function inputTemplate(provider: string) {
5656
}], null, null); })();
5757
(function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(AppModule, { declarations: [AppComponent], imports: [BrowserModule,
5858
NgOptimizedImage] }); })();
59-
`;
59+
`;
60+
}
61+
62+
function inputTemplateComponent(provider: string) {
63+
/* eslint-disable max-len */
64+
return tags.stripIndent`
65+
export class AppComponent {
66+
title = 'angular-cli-testbed';
67+
static ɵfac = function AppComponent_Factory(t) { return new (t || AppComponent)(); };
68+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: AppComponent, selectors: [["app-root"]], standalone: true, features: [i0.ɵɵProvidersFeature([
69+
${provider}
70+
]), i0.ɵɵStandaloneFeature], decls: 2, vars: 0, template: function AppComponent_Template(rf, ctx) { if (rf & 1) {
71+
i0.ɵɵelementStart(0, "div");
72+
i0.ɵɵtext(1, "Hello world");
73+
i0.ɵɵelementEnd();
74+
} } });
75+
}
76+
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AppComponent, [{
77+
type: Component,
78+
args: [{ selector: 'app-root', imports: [NgOptimizedImage, NgSwitchCase, NgSwitchDefault, NgSwitch], standalone: true, providers: [
79+
${provider}
80+
], template: "<div>Hello world</div>\n\n" }]
81+
}], null, null); })();
82+
`;
83+
}
84+
85+
function runSharedTests(template: (povider: string) => string) {
86+
it('should find a domain when a built-in loader is used with a string-literal-like argument', () => {
87+
// Intentionally inconsistent use of quote styles in this data structure:
88+
const builtInLoaders: Array<[string, string]> = [
89+
['provideCloudflareLoader("www.cloudflaredomain.com")', 'www.cloudflaredomain.com'],
90+
[
91+
"provideCloudinaryLoader('https://www.cloudinarydomain.net')",
92+
'https://www.cloudinarydomain.net',
93+
],
94+
['provideImageKitLoader("www.imageKitdomain.com")', 'www.imageKitdomain.com'],
95+
['provideImgixLoader(`www.imgixdomain.com/images/`)', 'www.imgixdomain.com/images/'],
96+
];
97+
for (const loader of builtInLoaders) {
98+
const input = template(loader[0]);
99+
const result = Array.from(findDomains(input));
100+
expect(result.length).toBe(1);
101+
expect(result[0]).toBe(loader[1]);
102+
}
103+
});
104+
105+
it('should find a domain in a custom loader function with a template literal', () => {
106+
const customLoader = tags.stripIndent`
107+
{
108+
provide: IMAGE_LOADER,
109+
useValue: (config: ImageLoaderConfig) => {
110+
return ${'`https://customLoaderTemplate.com/images?src=${config.src}&width=${config.width}`'};
111+
},
112+
},`;
113+
const input = template(customLoader);
114+
const result = Array.from(findDomains(input));
115+
expect(result.length).toBe(1);
116+
expect(result[0]).toBe('https://customLoaderTemplate.com/');
117+
});
118+
119+
it('should find a domain when provider is alongside other providers', () => {
120+
const customLoader = tags.stripIndent`
121+
{
122+
provide: SOME_OTHER_PROVIDER,
123+
useValue: (config: ImageLoaderConfig) => {
124+
return "https://notacustomloaderstring.com/images?src=" + config.src + "&width=" + config.width;
125+
},
126+
},
127+
provideNotARealLoader("https://www.foo.com"),
128+
{
129+
provide: IMAGE_LOADER,
130+
useValue: (config: ImageLoaderConfig) => {
131+
return ${'`https://customloadertemplate.com/images?src=${config.src}&width=${config.width}`'};
132+
},
133+
},
134+
{
135+
provide: YET_ANOTHER_PROVIDER,
136+
useValue: (config: ImageLoaderConfig) => {
137+
return ${'`https://notacustomloadertemplate.com/images?src=${config.src}&width=${config.width}`'};
138+
},
139+
},`;
140+
const input = template(customLoader);
141+
const result = Array.from(findDomains(input));
142+
expect(result.length).toBe(1);
143+
expect(result[0]).toBe('https://customloadertemplate.com/');
144+
});
60145
}
61146

62147
describe('@ngtools/webpack transformers', () => {
63-
describe('find_image_domains', () => {
64-
it('should find a domain when a built-in loader is used with a string-literal-like argument', () => {
65-
// Intentionally inconsistent use of quote styles in this data structure:
66-
const builtInLoaders: Array<[string, string]> = [
67-
['provideCloudflareLoader("www.cloudflaredomain.com")', 'www.cloudflaredomain.com'],
68-
[
69-
"provideCloudinaryLoader('https://www.cloudinarydomain.net')",
70-
'https://www.cloudinarydomain.net',
71-
],
72-
['provideImageKitLoader("www.imageKitdomain.com")', 'www.imageKitdomain.com'],
73-
['provideImgixLoader(`www.imgixdomain.com/images/`)', 'www.imgixdomain.com/images/'],
74-
];
75-
for (const loader of builtInLoaders) {
76-
const input = inputTemplate(loader[0]);
77-
const result = Array.from(findDomains(input));
78-
expect(result.length).toBe(1);
79-
expect(result[0]).toBe(loader[1]);
80-
}
81-
});
148+
describe('find_image_domains (app module)', () => {
149+
runSharedTests(inputTemplateAppModule);
150+
runSharedTests(inputTemplateComponent);
82151

83152
it('should not find a domain when a built-in loader is used with a variable', () => {
84-
const input = inputTemplate(`provideCloudflareLoader(myImageCDN)`);
153+
const input = inputTemplateAppModule(`provideCloudflareLoader(myImageCDN)`);
85154
const result = Array.from(findDomains(input));
86155
expect(result.length).toBe(0);
87156
});
88157

89158
it('should not find a domain when a built-in loader is used with an expression', () => {
90-
const input = inputTemplate(
159+
const input = inputTemplateAppModule(
91160
`provideCloudflareLoader("https://www." + (dev ? "dev." : "") + "cloudinarydomain.net")`,
92161
);
93162
const result = Array.from(findDomains(input));
94163
expect(result.length).toBe(0);
95164
});
96165

97166
it('should not find a domain when a built-in loader is used with a template literal', () => {
98-
const input = inputTemplate(
167+
const input = inputTemplateAppModule(
99168
'provideCloudflareLoader(`https://www.${dev ? "dev." : ""}cloudinarydomain.net`)',
100169
);
101170
const result = Array.from(findDomains(input));
102171
expect(result.length).toBe(0);
103172
});
104173

105174
it('should not find a domain in a function that is not a built-in loader', () => {
106-
const input = inputTemplate('provideNotARealLoader("https://www.foo.com")');
175+
const input = inputTemplateAppModule('provideNotARealLoader("https://www.foo.com")');
107176
const result = Array.from(findDomains(input));
108177
expect(result.length).toBe(0);
109178
});
110179

111-
it('should find a domain in a custom loader function with a template literal', () => {
112-
const customLoader = tags.stripIndent`
113-
{
114-
provide: IMAGE_LOADER,
115-
useValue: (config: ImageLoaderConfig) => {
116-
return ${'`https://customLoaderTemplate.com/images?src=${config.src}&width=${config.width}`'};
117-
},
118-
},`;
119-
const input = inputTemplate(customLoader);
120-
const result = Array.from(findDomains(input));
121-
expect(result.length).toBe(1);
122-
expect(result[0]).toBe('https://customLoaderTemplate.com/');
123-
});
124-
125180
it('should find a domain in a custom loader function with string concatenation', () => {
126181
const customLoader = tags.stripIndent`
127182
{
@@ -130,7 +185,7 @@ describe('@ngtools/webpack transformers', () => {
130185
return "https://customLoaderString.com/images?src=" + config.src + "&width=" + config.width;
131186
},
132187
},`;
133-
const input = inputTemplate(customLoader);
188+
const input = inputTemplateAppModule(customLoader);
134189
const result = Array.from(findDomains(input));
135190
expect(result.length).toBe(1);
136191
expect(result[0]).toBe('https://customLoaderString.com/');
@@ -144,36 +199,9 @@ describe('@ngtools/webpack transformers', () => {
144199
return "https://customLoaderString.com/images?src=" + config.src + "&width=" + config.width;
145200
},
146201
},`;
147-
const input = inputTemplate(customLoader);
202+
const input = inputTemplateAppModule(customLoader);
148203
const result = Array.from(findDomains(input));
149204
expect(result.length).toBe(0);
150205
});
151-
152-
it('should find a domain when provider is alongside other providers', () => {
153-
const customLoader = tags.stripIndent`
154-
{
155-
provide: SOME_OTHER_PROVIDER,
156-
useValue: (config: ImageLoaderConfig) => {
157-
return "https://notacustomloaderstring.com/images?src=" + config.src + "&width=" + config.width;
158-
},
159-
},
160-
provideNotARealLoader("https://www.foo.com"),
161-
{
162-
provide: IMAGE_LOADER,
163-
useValue: (config: ImageLoaderConfig) => {
164-
return ${'`https://customloadertemplate.com/images?src=${config.src}&width=${config.width}`'};
165-
},
166-
},
167-
{
168-
provide: YET_ANOTHER_PROVIDER,
169-
useValue: (config: ImageLoaderConfig) => {
170-
return ${'`https://notacustomloadertemplate.com/images?src=${config.src}&width=${config.width}`'};
171-
},
172-
},`;
173-
const input = inputTemplate(customLoader);
174-
const result = Array.from(findDomains(input));
175-
expect(result.length).toBe(1);
176-
expect(result[0]).toBe('https://customloadertemplate.com/');
177-
});
178206
});
179207
});

0 commit comments

Comments
 (0)