UNPKG

19.7 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright Google LLC All Rights Reserved.
5 *
6 * Use of this source code is governed by an MIT-style license that can be
7 * found in the LICENSE file at https://angular.io/license
8 */
9var __importDefault = (this && this.__importDefault) || function (mod) {
10 return (mod && mod.__esModule) ? mod : { "default": mod };
11};
12Object.defineProperty(exports, "__esModule", { value: true });
13exports.findBootstrapApplicationCall = exports.addFunctionalProvidersToStandaloneBootstrap = exports.addModuleImportToStandaloneBootstrap = exports.callsProvidersFunction = exports.importsProvidersFrom = void 0;
14const schematics_1 = require("@angular-devkit/schematics");
15const path_1 = require("path");
16const typescript_1 = __importDefault(require("../third_party/github.com/Microsoft/TypeScript/lib/typescript"));
17const ast_utils_1 = require("../utility/ast-utils");
18const change_1 = require("../utility/change");
19/**
20 * Checks whether the providers from a module are being imported in a `bootstrapApplication` call.
21 * @param tree File tree of the project.
22 * @param filePath Path of the file in which to check.
23 * @param className Class name of the module to search for.
24 * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
25 * `@schematics/angular/utility` instead.
26 */
27function importsProvidersFrom(tree, filePath, className) {
28 const sourceFile = createSourceFile(tree, filePath);
29 const bootstrapCall = findBootstrapApplicationCall(sourceFile);
30 const appConfig = bootstrapCall ? findAppConfig(bootstrapCall, tree, filePath) : null;
31 const importProvidersFromCall = appConfig ? findImportProvidersFromCall(appConfig.node) : null;
32 return !!importProvidersFromCall?.arguments.some((arg) => typescript_1.default.isIdentifier(arg) && arg.text === className);
33}
34exports.importsProvidersFrom = importsProvidersFrom;
35/**
36 * Checks whether a providers function is being called in a `bootstrapApplication` call.
37 * @param tree File tree of the project.
38 * @param filePath Path of the file in which to check.
39 * @param functionName Name of the function to search for.
40 * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
41 * `@schematics/angular/utility` instead.
42 */
43function callsProvidersFunction(tree, filePath, functionName) {
44 const sourceFile = createSourceFile(tree, filePath);
45 const bootstrapCall = findBootstrapApplicationCall(sourceFile);
46 const appConfig = bootstrapCall ? findAppConfig(bootstrapCall, tree, filePath) : null;
47 const providersLiteral = appConfig ? findProvidersLiteral(appConfig.node) : null;
48 return !!providersLiteral?.elements.some((el) => typescript_1.default.isCallExpression(el) &&
49 typescript_1.default.isIdentifier(el.expression) &&
50 el.expression.text === functionName);
51}
52exports.callsProvidersFunction = callsProvidersFunction;
53/**
54 * Adds an `importProvidersFrom` call to the `bootstrapApplication` call.
55 * @param tree File tree of the project.
56 * @param filePath Path to the file that should be updated.
57 * @param moduleName Name of the module that should be imported.
58 * @param modulePath Path from which to import the module.
59 * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
60 * `@schematics/angular/utility` instead.
61 */
62function addModuleImportToStandaloneBootstrap(tree, filePath, moduleName, modulePath) {
63 const sourceFile = createSourceFile(tree, filePath);
64 const bootstrapCall = findBootstrapApplicationCall(sourceFile);
65 const addImports = (file, recorder) => {
66 const sourceText = file.getText();
67 [
68 (0, ast_utils_1.insertImport)(file, sourceText, moduleName, modulePath),
69 (0, ast_utils_1.insertImport)(file, sourceText, 'importProvidersFrom', '@angular/core'),
70 ].forEach((change) => {
71 if (change instanceof change_1.InsertChange) {
72 recorder.insertLeft(change.pos, change.toAdd);
73 }
74 });
75 };
76 if (!bootstrapCall) {
77 throw new schematics_1.SchematicsException(`Could not find bootstrapApplication call in ${filePath}`);
78 }
79 const importProvidersCall = typescript_1.default.factory.createCallExpression(typescript_1.default.factory.createIdentifier('importProvidersFrom'), [], [typescript_1.default.factory.createIdentifier(moduleName)]);
80 // If there's only one argument, we have to create a new object literal.
81 if (bootstrapCall.arguments.length === 1) {
82 const recorder = tree.beginUpdate(filePath);
83 addNewAppConfigToCall(bootstrapCall, importProvidersCall, recorder);
84 addImports(sourceFile, recorder);
85 tree.commitUpdate(recorder);
86 return;
87 }
88 // If the config is a `mergeApplicationProviders` call, add another config to it.
89 if (isMergeAppConfigCall(bootstrapCall.arguments[1])) {
90 const recorder = tree.beginUpdate(filePath);
91 addNewAppConfigToCall(bootstrapCall.arguments[1], importProvidersCall, recorder);
92 addImports(sourceFile, recorder);
93 tree.commitUpdate(recorder);
94 return;
95 }
96 // Otherwise attempt to merge into the current config.
97 const appConfig = findAppConfig(bootstrapCall, tree, filePath);
98 if (!appConfig) {
99 throw new schematics_1.SchematicsException(`Could not statically analyze config in bootstrapApplication call in ${filePath}`);
100 }
101 const { filePath: configFilePath, node: config } = appConfig;
102 const recorder = tree.beginUpdate(configFilePath);
103 const importCall = findImportProvidersFromCall(config);
104 addImports(config.getSourceFile(), recorder);
105 if (importCall) {
106 // If there's an `importProvidersFrom` call already, add the module to it.
107 recorder.insertRight(importCall.arguments[importCall.arguments.length - 1].getEnd(), `, ${moduleName}`);
108 }
109 else {
110 const providersLiteral = findProvidersLiteral(config);
111 if (providersLiteral) {
112 // If there's a `providers` array, add the import to it.
113 addElementToArray(providersLiteral, importProvidersCall, recorder);
114 }
115 else {
116 // Otherwise add a `providers` array to the existing object literal.
117 addProvidersToObjectLiteral(config, importProvidersCall, recorder);
118 }
119 }
120 tree.commitUpdate(recorder);
121}
122exports.addModuleImportToStandaloneBootstrap = addModuleImportToStandaloneBootstrap;
123/**
124 * Adds a providers function call to the `bootstrapApplication` call.
125 * @param tree File tree of the project.
126 * @param filePath Path to the file that should be updated.
127 * @param functionName Name of the function that should be called.
128 * @param importPath Path from which to import the function.
129 * @param args Arguments to use when calling the function.
130 * @returns The file path that the provider was added to.
131 * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
132 * `@schematics/angular/utility` instead.
133 */
134function addFunctionalProvidersToStandaloneBootstrap(tree, filePath, functionName, importPath, args = []) {
135 const sourceFile = createSourceFile(tree, filePath);
136 const bootstrapCall = findBootstrapApplicationCall(sourceFile);
137 const addImports = (file, recorder) => {
138 const change = (0, ast_utils_1.insertImport)(file, file.getText(), functionName, importPath);
139 if (change instanceof change_1.InsertChange) {
140 recorder.insertLeft(change.pos, change.toAdd);
141 }
142 };
143 if (!bootstrapCall) {
144 throw new schematics_1.SchematicsException(`Could not find bootstrapApplication call in ${filePath}`);
145 }
146 const providersCall = typescript_1.default.factory.createCallExpression(typescript_1.default.factory.createIdentifier(functionName), undefined, args);
147 // If there's only one argument, we have to create a new object literal.
148 if (bootstrapCall.arguments.length === 1) {
149 const recorder = tree.beginUpdate(filePath);
150 addNewAppConfigToCall(bootstrapCall, providersCall, recorder);
151 addImports(sourceFile, recorder);
152 tree.commitUpdate(recorder);
153 return filePath;
154 }
155 // If the config is a `mergeApplicationProviders` call, add another config to it.
156 if (isMergeAppConfigCall(bootstrapCall.arguments[1])) {
157 const recorder = tree.beginUpdate(filePath);
158 addNewAppConfigToCall(bootstrapCall.arguments[1], providersCall, recorder);
159 addImports(sourceFile, recorder);
160 tree.commitUpdate(recorder);
161 return filePath;
162 }
163 // Otherwise attempt to merge into the current config.
164 const appConfig = findAppConfig(bootstrapCall, tree, filePath);
165 if (!appConfig) {
166 throw new schematics_1.SchematicsException(`Could not statically analyze config in bootstrapApplication call in ${filePath}`);
167 }
168 const { filePath: configFilePath, node: config } = appConfig;
169 const recorder = tree.beginUpdate(configFilePath);
170 const providersLiteral = findProvidersLiteral(config);
171 addImports(config.getSourceFile(), recorder);
172 if (providersLiteral) {
173 // If there's a `providers` array, add the import to it.
174 addElementToArray(providersLiteral, providersCall, recorder);
175 }
176 else {
177 // Otherwise add a `providers` array to the existing object literal.
178 addProvidersToObjectLiteral(config, providersCall, recorder);
179 }
180 tree.commitUpdate(recorder);
181 return configFilePath;
182}
183exports.addFunctionalProvidersToStandaloneBootstrap = addFunctionalProvidersToStandaloneBootstrap;
184/**
185 * Finds the call to `bootstrapApplication` within a file.
186 * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
187 * `@schematics/angular/utility` instead.
188 */
189function findBootstrapApplicationCall(sourceFile) {
190 const localName = findImportLocalName(sourceFile, 'bootstrapApplication', '@angular/platform-browser');
191 if (!localName) {
192 return null;
193 }
194 let result = null;
195 sourceFile.forEachChild(function walk(node) {
196 if (typescript_1.default.isCallExpression(node) &&
197 typescript_1.default.isIdentifier(node.expression) &&
198 node.expression.text === localName) {
199 result = node;
200 }
201 if (!result) {
202 node.forEachChild(walk);
203 }
204 });
205 return result;
206}
207exports.findBootstrapApplicationCall = findBootstrapApplicationCall;
208/** Find a call to `importProvidersFrom` within an application config. */
209function findImportProvidersFromCall(config) {
210 const importProvidersName = findImportLocalName(config.getSourceFile(), 'importProvidersFrom', '@angular/core');
211 const providersLiteral = findProvidersLiteral(config);
212 if (providersLiteral && importProvidersName) {
213 for (const element of providersLiteral.elements) {
214 // Look for an array element that calls the `importProvidersFrom` function.
215 if (typescript_1.default.isCallExpression(element) &&
216 typescript_1.default.isIdentifier(element.expression) &&
217 element.expression.text === importProvidersName) {
218 return element;
219 }
220 }
221 }
222 return null;
223}
224/** Finds the `providers` array literal within an application config. */
225function findProvidersLiteral(config) {
226 for (const prop of config.properties) {
227 if (typescript_1.default.isPropertyAssignment(prop) &&
228 typescript_1.default.isIdentifier(prop.name) &&
229 prop.name.text === 'providers' &&
230 typescript_1.default.isArrayLiteralExpression(prop.initializer)) {
231 return prop.initializer;
232 }
233 }
234 return null;
235}
236/**
237 * Resolves the node that defines the app config from a bootstrap call.
238 * @param bootstrapCall Call for which to resolve the config.
239 * @param tree File tree of the project.
240 * @param filePath File path of the bootstrap call.
241 */
242function findAppConfig(bootstrapCall, tree, filePath) {
243 if (bootstrapCall.arguments.length > 1) {
244 const config = bootstrapCall.arguments[1];
245 if (typescript_1.default.isObjectLiteralExpression(config)) {
246 return { filePath, node: config };
247 }
248 if (typescript_1.default.isIdentifier(config)) {
249 return resolveAppConfigFromIdentifier(config, tree, filePath);
250 }
251 }
252 return null;
253}
254/**
255 * Resolves the app config from an identifier referring to it.
256 * @param identifier Identifier referring to the app config.
257 * @param tree File tree of the project.
258 * @param bootstapFilePath Path of the bootstrap call.
259 */
260function resolveAppConfigFromIdentifier(identifier, tree, bootstapFilePath) {
261 const sourceFile = identifier.getSourceFile();
262 for (const node of sourceFile.statements) {
263 // Only look at relative imports. This will break if the app uses a path
264 // mapping to refer to the import, but in order to resolve those, we would
265 // need knowledge about the entire program.
266 if (!typescript_1.default.isImportDeclaration(node) ||
267 !node.importClause?.namedBindings ||
268 !typescript_1.default.isNamedImports(node.importClause.namedBindings) ||
269 !typescript_1.default.isStringLiteralLike(node.moduleSpecifier) ||
270 !node.moduleSpecifier.text.startsWith('.')) {
271 continue;
272 }
273 for (const specifier of node.importClause.namedBindings.elements) {
274 if (specifier.name.text !== identifier.text) {
275 continue;
276 }
277 // Look for a variable with the imported name in the file. Note that ideally we would use
278 // the type checker to resolve this, but we can't because these utilities are set up to
279 // operate on individual files, not the entire program.
280 const filePath = (0, path_1.join)((0, path_1.dirname)(bootstapFilePath), node.moduleSpecifier.text + '.ts');
281 const importedSourceFile = createSourceFile(tree, filePath);
282 const resolvedVariable = findAppConfigFromVariableName(importedSourceFile, (specifier.propertyName || specifier.name).text);
283 if (resolvedVariable) {
284 return { filePath, node: resolvedVariable };
285 }
286 }
287 }
288 const variableInSameFile = findAppConfigFromVariableName(sourceFile, identifier.text);
289 return variableInSameFile ? { filePath: bootstapFilePath, node: variableInSameFile } : null;
290}
291/**
292 * Finds an app config within the top-level variables of a file.
293 * @param sourceFile File in which to search for the config.
294 * @param variableName Name of the variable containing the config.
295 */
296function findAppConfigFromVariableName(sourceFile, variableName) {
297 for (const node of sourceFile.statements) {
298 if (typescript_1.default.isVariableStatement(node)) {
299 for (const decl of node.declarationList.declarations) {
300 if (typescript_1.default.isIdentifier(decl.name) &&
301 decl.name.text === variableName &&
302 decl.initializer &&
303 typescript_1.default.isObjectLiteralExpression(decl.initializer)) {
304 return decl.initializer;
305 }
306 }
307 }
308 }
309 return null;
310}
311/**
312 * Finds the local name of an imported symbol. Could be the symbol name itself or its alias.
313 * @param sourceFile File within which to search for the import.
314 * @param name Actual name of the import, not its local alias.
315 * @param moduleName Name of the module from which the symbol is imported.
316 */
317function findImportLocalName(sourceFile, name, moduleName) {
318 for (const node of sourceFile.statements) {
319 // Only look for top-level imports.
320 if (!typescript_1.default.isImportDeclaration(node) ||
321 !typescript_1.default.isStringLiteral(node.moduleSpecifier) ||
322 node.moduleSpecifier.text !== moduleName) {
323 continue;
324 }
325 // Filter out imports that don't have the right shape.
326 if (!node.importClause ||
327 !node.importClause.namedBindings ||
328 !typescript_1.default.isNamedImports(node.importClause.namedBindings)) {
329 continue;
330 }
331 // Look through the elements of the declaration for the specific import.
332 for (const element of node.importClause.namedBindings.elements) {
333 if ((element.propertyName || element.name).text === name) {
334 // The local name is always in `name`.
335 return element.name.text;
336 }
337 }
338 }
339 return null;
340}
341/** Creates a source file from a file path within a project. */
342function createSourceFile(tree, filePath) {
343 return typescript_1.default.createSourceFile(filePath, tree.readText(filePath), typescript_1.default.ScriptTarget.Latest, true);
344}
345/**
346 * Creates a new app config object literal and adds it to a call expression as an argument.
347 * @param call Call to which to add the config.
348 * @param expression Expression that should inserted into the new config.
349 * @param recorder Recorder to which to log the change.
350 */
351function addNewAppConfigToCall(call, expression, recorder) {
352 const newCall = typescript_1.default.factory.updateCallExpression(call, call.expression, call.typeArguments, [
353 ...call.arguments,
354 typescript_1.default.factory.createObjectLiteralExpression([
355 typescript_1.default.factory.createPropertyAssignment('providers', typescript_1.default.factory.createArrayLiteralExpression([expression])),
356 ], true),
357 ]);
358 recorder.remove(call.getStart(), call.getWidth());
359 recorder.insertRight(call.getStart(), typescript_1.default.createPrinter().printNode(typescript_1.default.EmitHint.Unspecified, newCall, call.getSourceFile()));
360}
361/**
362 * Adds an element to an array literal expression.
363 * @param node Array to which to add the element.
364 * @param element Element to be added.
365 * @param recorder Recorder to which to log the change.
366 */
367function addElementToArray(node, element, recorder) {
368 const newLiteral = typescript_1.default.factory.updateArrayLiteralExpression(node, [...node.elements, element]);
369 recorder.remove(node.getStart(), node.getWidth());
370 recorder.insertRight(node.getStart(), typescript_1.default.createPrinter().printNode(typescript_1.default.EmitHint.Unspecified, newLiteral, node.getSourceFile()));
371}
372/**
373 * Adds a `providers` property to an object literal.
374 * @param node Literal to which to add the `providers`.
375 * @param expression Provider that should be part of the generated `providers` array.
376 * @param recorder Recorder to which to log the change.
377 */
378function addProvidersToObjectLiteral(node, expression, recorder) {
379 const newOptionsLiteral = typescript_1.default.factory.updateObjectLiteralExpression(node, [
380 ...node.properties,
381 typescript_1.default.factory.createPropertyAssignment('providers', typescript_1.default.factory.createArrayLiteralExpression([expression])),
382 ]);
383 recorder.remove(node.getStart(), node.getWidth());
384 recorder.insertRight(node.getStart(), typescript_1.default.createPrinter().printNode(typescript_1.default.EmitHint.Unspecified, newOptionsLiteral, node.getSourceFile()));
385}
386/** Checks whether a node is a call to `mergeApplicationConfig`. */
387function isMergeAppConfigCall(node) {
388 if (!typescript_1.default.isCallExpression(node)) {
389 return false;
390 }
391 const localName = findImportLocalName(node.getSourceFile(), 'mergeApplicationConfig', '@angular/core');
392 return !!localName && typescript_1.default.isIdentifier(node.expression) && node.expression.text === localName;
393}