mirror of https://github.com/abpframework/abp.git
152 changed files with 2971 additions and 648 deletions
|
After Width: | Height: | Size: 1.6 MiB |
@ -0,0 +1,44 @@ |
|||||
|
{ |
||||
|
"extends": "../../.eslintrc.json", |
||||
|
"ignorePatterns": [ |
||||
|
"!**/*" |
||||
|
], |
||||
|
"overrides": [ |
||||
|
{ |
||||
|
"files": [ |
||||
|
"*.ts" |
||||
|
], |
||||
|
"parserOptions": { |
||||
|
"project": [ |
||||
|
"projects/<%= kebab(libraryName) %>/tsconfig.lib.json", |
||||
|
"projects/<%= kebab(libraryName) %>/tsconfig.spec.json" |
||||
|
], |
||||
|
"createDefaultProgram": true |
||||
|
}, |
||||
|
"rules": { |
||||
|
"@angular-eslint/directive-selector": [ |
||||
|
"error", |
||||
|
{ |
||||
|
"type": "attribute", |
||||
|
"prefix": "lib", |
||||
|
"style": "camelCase" |
||||
|
} |
||||
|
], |
||||
|
"@angular-eslint/component-selector": [ |
||||
|
"error", |
||||
|
{ |
||||
|
"type": "element", |
||||
|
"prefix": "lib", |
||||
|
"style": "kebab-case" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"files": [ |
||||
|
"*.html" |
||||
|
], |
||||
|
"rules": {} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json", |
||||
|
"dest": "../../dist/<%= kebab(libraryName) %>/config", |
||||
|
"lib": { |
||||
|
"entryFile": "src/public-api.ts" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './route-names'; |
||||
@ -0,0 +1,3 @@ |
|||||
|
export const enum e<%= pascal(libraryName) %>RouteNames { |
||||
|
<%= pascal(libraryName) %> = '<%= pascal(libraryName) %>', |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './route.provider'; |
||||
@ -0,0 +1,30 @@ |
|||||
|
import { eLayoutType, RoutesService } from '@abp/ng.core'; |
||||
|
import { e<%= pascal(libraryName) %>RouteNames } from '../enums/route-names'; |
||||
|
import { makeEnvironmentProviders, provideAppInitializer, inject, EnvironmentProviders } from '@angular/core'; |
||||
|
|
||||
|
export const <%= macro(libraryName) %>_ROUTE_PROVIDERS = [ |
||||
|
provideAppInitializer(() => { |
||||
|
configureRoutes(); |
||||
|
}), |
||||
|
]; |
||||
|
|
||||
|
export function configureRoutes() { |
||||
|
const routes = inject(RoutesService); |
||||
|
routes.add([ |
||||
|
{ |
||||
|
path: '/<%= kebab(libraryName) %>', |
||||
|
name: e<%= pascal(libraryName) %>RouteNames.<%= pascal(libraryName) %>, |
||||
|
iconClass: 'fas fa-book', |
||||
|
layout: eLayoutType.application, |
||||
|
order: 3, |
||||
|
}, |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
export const <%= macro(libraryName) %>_PROVIDERS: EnvironmentProviders[] = [ |
||||
|
...<%= macro(libraryName) %>_ROUTE_PROVIDERS, |
||||
|
]; |
||||
|
|
||||
|
export function provide<%= pascal(libraryName) %>Config() { |
||||
|
return makeEnvironmentProviders(<%= macro(libraryName) %>_PROVIDERS); |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
export * from './enums'; |
||||
|
export * from './providers'; |
||||
@ -0,0 +1,44 @@ |
|||||
|
// Karma configuration file, see link for more information |
||||
|
// https://karma-runner.github.io/1.0/config/configuration-file.html |
||||
|
|
||||
|
module.exports = function (config) { |
||||
|
config.set({ |
||||
|
basePath: '', |
||||
|
frameworks: ['jasmine', '@angular-devkit/build-angular'], |
||||
|
plugins: [ |
||||
|
require('karma-jasmine'), |
||||
|
require('karma-chrome-launcher'), |
||||
|
require('karma-jasmine-html-reporter'), |
||||
|
require('karma-coverage'), |
||||
|
require('@angular-devkit/build-angular/plugins/karma') |
||||
|
], |
||||
|
client: { |
||||
|
jasmine: { |
||||
|
// you can add configuration options for Jasmine here |
||||
|
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html |
||||
|
// for example, you can disable the random execution with `random: false` |
||||
|
// or set a specific seed with `seed: 4321` |
||||
|
}, |
||||
|
clearContext: false // leave Jasmine Spec Runner output visible in browser |
||||
|
}, |
||||
|
jasmineHtmlReporter: { |
||||
|
suppressAll: true // removes the duplicated traces |
||||
|
}, |
||||
|
coverageReporter: { |
||||
|
dir: require('path').join(__dirname, '../../coverage/<%= kebab(libraryName) %>'), |
||||
|
subdir: '.', |
||||
|
reporters: [ |
||||
|
{ type: 'html' }, |
||||
|
{ type: 'text-summary' } |
||||
|
] |
||||
|
}, |
||||
|
reporters: ['progress', 'kjhtml'], |
||||
|
port: 9876, |
||||
|
colors: true, |
||||
|
logLevel: config.LOG_INFO, |
||||
|
autoWatch: true, |
||||
|
browsers: ['Chrome'], |
||||
|
singleRun: false, |
||||
|
restartOnFileChange: true |
||||
|
}); |
||||
|
}; |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json", |
||||
|
"dest": "../../dist/<%= kebab(libraryName) %>", |
||||
|
"lib": { |
||||
|
"entryFile": "src/public-api.ts" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
{ |
||||
|
"name": "@<%= kebab(libraryName) %>/<%= kebab(libraryName) %>", |
||||
|
"version": "0.0.1", |
||||
|
"peerDependencies": { |
||||
|
"@abp/ng.core": "<%= abpVersion %>", |
||||
|
"@abp/ng.theme.shared": "<%= abpVersion %>" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"tslib": "^2.1.0" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import { Component } from '@angular/core'; |
||||
|
import {CoreModule} from "@abp/ng.core"; |
||||
|
import {ThemeSharedModule} from "@abp/ng.theme.shared"; |
||||
|
|
||||
|
@Component({ |
||||
|
standalone: true, |
||||
|
selector: '<%= camel(libraryName) %>-home', |
||||
|
template: `<h1>Lazy Loaded Test Component</h1>`, |
||||
|
imports: [CoreModule, ThemeSharedModule], |
||||
|
}) |
||||
|
export class <%= pascal(libraryName) %>Component {} |
||||
@ -0,0 +1,9 @@ |
|||||
|
import { Routes } from '@angular/router'; |
||||
|
|
||||
|
export const <%= macro(libraryName) %>_ROUTES: Routes = [ |
||||
|
{ |
||||
|
path: '', |
||||
|
loadComponent: () => |
||||
|
import('./<%= kebab(libraryName) %>.component').then(m => m.<%= pascal(libraryName) %>Component), |
||||
|
}, |
||||
|
]; |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './<%= kebab(libraryName) %>.routes'; |
||||
@ -0,0 +1,4 @@ |
|||||
|
/* |
||||
|
* Public API Surface of my-project-name |
||||
|
*/ |
||||
|
export * from './lib'; |
||||
@ -0,0 +1,26 @@ |
|||||
|
// This file is required by karma.conf.js and loads recursively all the .spec and framework files |
||||
|
|
||||
|
import 'zone.js'; |
||||
|
import 'zone.js/testing'; |
||||
|
import { getTestBed } from '@angular/core/testing'; |
||||
|
import { |
||||
|
BrowserDynamicTestingModule, |
||||
|
platformBrowserDynamicTesting |
||||
|
} from '@angular/platform-browser-dynamic/testing'; |
||||
|
|
||||
|
declare const require: { |
||||
|
context(path: string, deep?: boolean, filter?: RegExp): { |
||||
|
keys(): string[]; |
||||
|
<T>(id: string): T; |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
// First, initialize the Angular testing environment. |
||||
|
getTestBed().initTestEnvironment( |
||||
|
BrowserDynamicTestingModule, |
||||
|
platformBrowserDynamicTesting() |
||||
|
); |
||||
|
// Then we find all the tests. |
||||
|
const context = require.context('./', true, /\.spec\.ts$/); |
||||
|
// And load the modules. |
||||
|
context.keys().map(context); |
||||
@ -0,0 +1,20 @@ |
|||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */ |
||||
|
{ |
||||
|
"extends": "../../tsconfig.json", |
||||
|
"compilerOptions": { |
||||
|
"outDir": "../../out-tsc/lib", |
||||
|
"target": "es2020", |
||||
|
"declaration": true, |
||||
|
"declarationMap": true, |
||||
|
"inlineSources": true, |
||||
|
"types": [], |
||||
|
"lib": [ |
||||
|
"dom", |
||||
|
"es2018" |
||||
|
] |
||||
|
}, |
||||
|
"exclude": [ |
||||
|
"src/test.ts", |
||||
|
"**/*.spec.ts" |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */ |
||||
|
{ |
||||
|
"extends": "./tsconfig.lib.json", |
||||
|
"compilerOptions": { |
||||
|
"declarationMap": false |
||||
|
}, |
||||
|
"angularCompilerOptions": { |
||||
|
"compilationMode": "partial" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */ |
||||
|
{ |
||||
|
"extends": "../../tsconfig.json", |
||||
|
"compilerOptions": { |
||||
|
"outDir": "../../out-tsc/spec", |
||||
|
"types": [ |
||||
|
"jasmine" |
||||
|
] |
||||
|
}, |
||||
|
"files": [ |
||||
|
"src/test.ts" |
||||
|
], |
||||
|
"include": [ |
||||
|
"**/*.spec.ts", |
||||
|
"**/*.d.ts" |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json", |
||||
|
"lib": { |
||||
|
"entryFile": "src/public-api.ts" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
import { Provider } from '@angular/core'; |
||||
|
|
||||
|
export function provide<%= pascal(target) %><%= pascal(libraryName) %>(): Provider[] { |
||||
|
return [ |
||||
|
// Add your providers here |
||||
|
]; |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './<%= kebab(target) %>-<%= kebab(libraryName) %>'; |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './lib/index'; |
||||
@ -0,0 +1,25 @@ |
|||||
|
/** |
||||
|
* @license |
||||
|
* Copyright Google LLC All Rights Reserved. |
||||
|
* |
||||
|
* Use of this source code is governed by an MIT-style license that can be |
||||
|
* found in the LICENSE file at https://angular.dev/license
|
||||
|
*/ |
||||
|
|
||||
|
import { EOL } from 'node:os'; |
||||
|
|
||||
|
const CRLF = '\r\n'; |
||||
|
const LF = '\n'; |
||||
|
|
||||
|
export function getEOL(content: string): string { |
||||
|
const newlines = content.match(/(?:\r?\n)/g); |
||||
|
|
||||
|
if (newlines?.length) { |
||||
|
const crlf = newlines.filter(l => l === CRLF).length; |
||||
|
const lf = newlines.length - crlf; |
||||
|
|
||||
|
return crlf > lf ? CRLF : LF; |
||||
|
} |
||||
|
|
||||
|
return EOL; |
||||
|
} |
||||
@ -0,0 +1,148 @@ |
|||||
|
/** |
||||
|
* @license |
||||
|
* Copyright Google LLC All Rights Reserved. |
||||
|
* |
||||
|
* Use of this source code is governed by an MIT-style license that can be |
||||
|
* found in the LICENSE file at https://angular.dev/license
|
||||
|
*/ |
||||
|
|
||||
|
import { SchematicsException, Tree } from '@angular-devkit/schematics'; |
||||
|
import * as ts from 'typescript'; |
||||
|
import { getDecoratorMetadata, getMetadataField } from '../ast-utils'; |
||||
|
import { findBootstrapModuleCall, getAppModulePath } from '../ng-ast-utils'; |
||||
|
import { findBootstrapApplicationCall, getSourceFile } from './util'; |
||||
|
|
||||
|
/** Data resolved for a bootstrapped component. */ |
||||
|
interface BootstrappedComponentData { |
||||
|
/** Original name of the component class. */ |
||||
|
componentName: string; |
||||
|
|
||||
|
/** Path under which the component was imported in the main entrypoint. */ |
||||
|
componentImportPathInSameFile: string; |
||||
|
|
||||
|
/** Original name of the NgModule being bootstrapped, null if the app isn't module-based. */ |
||||
|
moduleName: string | null; |
||||
|
|
||||
|
/** |
||||
|
* Path under which the module was imported in the main entrypoint, |
||||
|
* null if the app isn't module-based. |
||||
|
*/ |
||||
|
moduleImportPathInSameFile: string | null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Finds the original name and path relative to the `main.ts` of the bootrstrapped app component. |
||||
|
* @param tree File tree in which to look for the component. |
||||
|
* @param mainFilePath Path of the `main` file. |
||||
|
*/ |
||||
|
export function resolveBootstrappedComponentData( |
||||
|
tree: Tree, |
||||
|
mainFilePath: string, |
||||
|
): BootstrappedComponentData | null { |
||||
|
// First try to resolve for a standalone app.
|
||||
|
try { |
||||
|
const call = findBootstrapApplicationCall(tree, mainFilePath); |
||||
|
|
||||
|
if (call.arguments.length > 0 && ts.isIdentifier(call.arguments[0])) { |
||||
|
const resolved = resolveIdentifier(call.arguments[0]); |
||||
|
|
||||
|
if (resolved) { |
||||
|
return { |
||||
|
componentName: resolved.name, |
||||
|
componentImportPathInSameFile: resolved.path, |
||||
|
moduleName: null, |
||||
|
moduleImportPathInSameFile: null, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} catch (e) { |
||||
|
// `findBootstrapApplicationCall` will throw if it can't find the `bootrstrapApplication` call.
|
||||
|
// Catch so we can continue to the fallback logic.
|
||||
|
if (!(e instanceof SchematicsException)) { |
||||
|
throw e; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Otherwise fall back to resolving an NgModule-based app.
|
||||
|
return resolveNgModuleBasedData(tree, mainFilePath); |
||||
|
} |
||||
|
|
||||
|
/** Resolves the bootstrap data for a NgModule-based app. */ |
||||
|
function resolveNgModuleBasedData( |
||||
|
tree: Tree, |
||||
|
mainFilePath: string, |
||||
|
): BootstrappedComponentData | null { |
||||
|
const appModulePath = getAppModulePath(tree, mainFilePath); |
||||
|
const appModuleFile = getSourceFile(tree, appModulePath); |
||||
|
const metadataNodes = getDecoratorMetadata(appModuleFile, 'NgModule', '@angular/core'); |
||||
|
|
||||
|
for (const node of metadataNodes) { |
||||
|
if (!ts.isObjectLiteralExpression(node)) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
const bootstrapProp = getMetadataField(node, 'bootstrap').find(prop => { |
||||
|
return ( |
||||
|
ts.isArrayLiteralExpression(prop.initializer) && |
||||
|
prop.initializer.elements.length > 0 && |
||||
|
ts.isIdentifier(prop.initializer.elements[0]) |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
const componentIdentifier = (bootstrapProp?.initializer as ts.ArrayLiteralExpression) |
||||
|
.elements[0] as ts.Identifier | undefined; |
||||
|
const componentResult = componentIdentifier ? resolveIdentifier(componentIdentifier) : null; |
||||
|
const bootstrapCall = findBootstrapModuleCall(tree, mainFilePath); |
||||
|
|
||||
|
if ( |
||||
|
componentResult && |
||||
|
bootstrapCall && |
||||
|
bootstrapCall.arguments.length > 0 && |
||||
|
ts.isIdentifier(bootstrapCall.arguments[0]) |
||||
|
) { |
||||
|
const moduleResult = resolveIdentifier(bootstrapCall.arguments[0]); |
||||
|
|
||||
|
if (moduleResult) { |
||||
|
return { |
||||
|
componentName: componentResult.name, |
||||
|
componentImportPathInSameFile: componentResult.path, |
||||
|
moduleName: moduleResult.name, |
||||
|
moduleImportPathInSameFile: moduleResult.path, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** Resolves an identifier to its original name and path that it was imported from. */ |
||||
|
function resolveIdentifier(identifier: ts.Identifier): { name: string; path: string } | null { |
||||
|
const sourceFile = identifier.getSourceFile(); |
||||
|
|
||||
|
// Try to resolve the import path by looking at the top-level named imports of the file.
|
||||
|
for (const node of sourceFile.statements) { |
||||
|
if ( |
||||
|
!ts.isImportDeclaration(node) || |
||||
|
!ts.isStringLiteral(node.moduleSpecifier) || |
||||
|
!node.importClause || |
||||
|
!node.importClause.namedBindings || |
||||
|
!ts.isNamedImports(node.importClause.namedBindings) |
||||
|
) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
for (const element of node.importClause.namedBindings.elements) { |
||||
|
if (element.name.text === identifier.text) { |
||||
|
return { |
||||
|
// Note that we use `propertyName` if available, because it contains
|
||||
|
// the real name in the case where the import is aliased.
|
||||
|
name: (element.propertyName || element.name).text, |
||||
|
path: node.moduleSpecifier.text, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
@ -0,0 +1,127 @@ |
|||||
|
/** |
||||
|
* @license |
||||
|
* Copyright Google LLC All Rights Reserved. |
||||
|
* |
||||
|
* Use of this source code is governed by an MIT-style license that can be |
||||
|
* found in the LICENSE file at https://angular.dev/license
|
||||
|
*/ |
||||
|
|
||||
|
import { Tree } from '@angular-devkit/schematics'; |
||||
|
import { dirname, join } from 'node:path'; |
||||
|
import * as ts from 'typescript'; |
||||
|
import { getSourceFile } from './util'; |
||||
|
|
||||
|
/** App config that was resolved to its source node. */ |
||||
|
export interface ResolvedAppConfig { |
||||
|
/** Tree-relative path of the file containing the app config. */ |
||||
|
filePath: string; |
||||
|
|
||||
|
/** Node defining the app config. */ |
||||
|
node: ts.ObjectLiteralExpression; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Resolves the node that defines the app config from a bootstrap call. |
||||
|
* @param bootstrapCall Call for which to resolve the config. |
||||
|
* @param tree File tree of the project. |
||||
|
* @param filePath File path of the bootstrap call. |
||||
|
*/ |
||||
|
export function findAppConfig( |
||||
|
bootstrapCall: ts.CallExpression, |
||||
|
tree: Tree, |
||||
|
filePath: string, |
||||
|
): ResolvedAppConfig | null { |
||||
|
if (bootstrapCall.arguments.length > 1) { |
||||
|
const config = bootstrapCall.arguments[1]; |
||||
|
|
||||
|
if (ts.isObjectLiteralExpression(config)) { |
||||
|
return { filePath, node: config }; |
||||
|
} |
||||
|
|
||||
|
if (ts.isIdentifier(config)) { |
||||
|
return resolveAppConfigFromIdentifier(config, tree, filePath); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Resolves the app config from an identifier referring to it. |
||||
|
* @param identifier Identifier referring to the app config. |
||||
|
* @param tree File tree of the project. |
||||
|
* @param bootstapFilePath Path of the bootstrap call. |
||||
|
*/ |
||||
|
function resolveAppConfigFromIdentifier( |
||||
|
identifier: ts.Identifier, |
||||
|
tree: Tree, |
||||
|
bootstapFilePath: string, |
||||
|
): ResolvedAppConfig | null { |
||||
|
const sourceFile = identifier.getSourceFile(); |
||||
|
|
||||
|
for (const node of sourceFile.statements) { |
||||
|
// Only look at relative imports. This will break if the app uses a path
|
||||
|
// mapping to refer to the import, but in order to resolve those, we would
|
||||
|
// need knowledge about the entire program.
|
||||
|
if ( |
||||
|
!ts.isImportDeclaration(node) || |
||||
|
!node.importClause?.namedBindings || |
||||
|
!ts.isNamedImports(node.importClause.namedBindings) || |
||||
|
!ts.isStringLiteralLike(node.moduleSpecifier) || |
||||
|
!node.moduleSpecifier.text.startsWith('.') |
||||
|
) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
for (const specifier of node.importClause.namedBindings.elements) { |
||||
|
if (specifier.name.text !== identifier.text) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// Look for a variable with the imported name in the file. Note that ideally we would use
|
||||
|
// the type checker to resolve this, but we can't because these utilities are set up to
|
||||
|
// operate on individual files, not the entire program.
|
||||
|
const filePath = join(dirname(bootstapFilePath), node.moduleSpecifier.text + '.ts'); |
||||
|
const importedSourceFile = getSourceFile(tree, filePath); |
||||
|
const resolvedVariable = findAppConfigFromVariableName( |
||||
|
importedSourceFile, |
||||
|
(specifier.propertyName || specifier.name).text, |
||||
|
); |
||||
|
|
||||
|
if (resolvedVariable) { |
||||
|
return { filePath, node: resolvedVariable }; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const variableInSameFile = findAppConfigFromVariableName(sourceFile, identifier.text); |
||||
|
|
||||
|
return variableInSameFile ? { filePath: bootstapFilePath, node: variableInSameFile } : null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Finds an app config within the top-level variables of a file. |
||||
|
* @param sourceFile File in which to search for the config. |
||||
|
* @param variableName Name of the variable containing the config. |
||||
|
*/ |
||||
|
function findAppConfigFromVariableName( |
||||
|
sourceFile: ts.SourceFile, |
||||
|
variableName: string, |
||||
|
): ts.ObjectLiteralExpression | null { |
||||
|
for (const node of sourceFile.statements) { |
||||
|
if (ts.isVariableStatement(node)) { |
||||
|
for (const decl of node.declarationList.declarations) { |
||||
|
if ( |
||||
|
ts.isIdentifier(decl.name) && |
||||
|
decl.name.text === variableName && |
||||
|
decl.initializer && |
||||
|
ts.isObjectLiteralExpression(decl.initializer) |
||||
|
) { |
||||
|
return decl.initializer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
@ -0,0 +1,115 @@ |
|||||
|
/** |
||||
|
* @license |
||||
|
* Copyright Google LLC All Rights Reserved. |
||||
|
* |
||||
|
* Use of this source code is governed by an MIT-style license that can be |
||||
|
* found in the LICENSE file at https://angular.dev/license
|
||||
|
*/ |
||||
|
|
||||
|
import { Rule, Tree } from '@angular-devkit/schematics'; |
||||
|
import * as ts from 'typescript'; |
||||
|
import { hasTopLevelIdentifier, insertImport } from '../ast-utils'; |
||||
|
import { applyToUpdateRecorder } from '../change'; |
||||
|
|
||||
|
/** Generated code that hasn't been interpolated yet. */ |
||||
|
export interface PendingCode { |
||||
|
/** Code that will be inserted. */ |
||||
|
expression: string; |
||||
|
|
||||
|
/** Imports that need to be added to the file in which the code is inserted. */ |
||||
|
imports: PendingImports; |
||||
|
} |
||||
|
|
||||
|
/** Map keeping track of imports and aliases under which they're referred to in an expression. */ |
||||
|
type PendingImports = Map<string, Map<string, string>>; |
||||
|
|
||||
|
/** Counter used to generate unique IDs. */ |
||||
|
let uniqueIdCounter = 0; |
||||
|
|
||||
|
/** |
||||
|
* Callback invoked by a Rule that produces the code |
||||
|
* that needs to be inserted somewhere in the app. |
||||
|
*/ |
||||
|
export type CodeBlockCallback = (block: CodeBlock) => PendingCode; |
||||
|
|
||||
|
/** |
||||
|
* Utility class used to generate blocks of code that |
||||
|
* can be inserted by the devkit into a user's app. |
||||
|
*/ |
||||
|
export class CodeBlock { |
||||
|
private _imports: PendingImports = new Map<string, Map<string, string>>(); |
||||
|
|
||||
|
// Note: the methods here are defined as arrow function so that they can be destructured by
|
||||
|
// consumers without losing their context. This makes the API more concise.
|
||||
|
|
||||
|
/** Function used to tag a code block in order to produce a `PendingCode` object. */ |
||||
|
code = (strings: TemplateStringsArray, ...params: unknown[]): PendingCode => { |
||||
|
return { |
||||
|
expression: strings.map((part, index) => part + (params[index] || '')).join(''), |
||||
|
imports: this._imports, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* Used inside of a code block to mark external symbols and which module they should be imported |
||||
|
* from. When the code is inserted, the required import statements will be produced automatically. |
||||
|
* @param symbolName Name of the external symbol. |
||||
|
* @param moduleName Module from which the symbol should be imported. |
||||
|
*/ |
||||
|
external = (symbolName: string, moduleName: string): string => { |
||||
|
if (!this._imports.has(moduleName)) { |
||||
|
this._imports.set(moduleName, new Map()); |
||||
|
} |
||||
|
|
||||
|
const symbolsPerModule = this._imports.get(moduleName) as Map<string, string>; |
||||
|
|
||||
|
if (!symbolsPerModule.has(symbolName)) { |
||||
|
symbolsPerModule.set(symbolName, `@@__SCHEMATIC_PLACEHOLDER_${uniqueIdCounter++}__@@`); |
||||
|
} |
||||
|
|
||||
|
return symbolsPerModule.get(symbolName) as string; |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* Produces the necessary rules to transform a `PendingCode` object into valid code. |
||||
|
* @param initialCode Code pending transformed. |
||||
|
* @param filePath Path of the file in which the code will be inserted. |
||||
|
*/ |
||||
|
static transformPendingCode(initialCode: PendingCode, filePath: string) { |
||||
|
const code = { ...initialCode }; |
||||
|
const rules: Rule[] = []; |
||||
|
|
||||
|
code.imports.forEach((symbols, moduleName) => { |
||||
|
symbols.forEach((placeholder, symbolName) => { |
||||
|
rules.push((tree: Tree) => { |
||||
|
const recorder = tree.beginUpdate(filePath); |
||||
|
const sourceFile = ts.createSourceFile( |
||||
|
filePath, |
||||
|
tree.readText(filePath), |
||||
|
ts.ScriptTarget.Latest, |
||||
|
true, |
||||
|
); |
||||
|
|
||||
|
// Note that this could still technically clash if there's a top-level symbol called
|
||||
|
// `${symbolName}_alias`, however this is unlikely. We can revisit this if it becomes
|
||||
|
// a problem.
|
||||
|
const alias = hasTopLevelIdentifier(sourceFile, symbolName, moduleName) |
||||
|
? symbolName + '_alias' |
||||
|
: undefined; |
||||
|
|
||||
|
code.expression = code.expression.replace( |
||||
|
new RegExp(placeholder, 'g'), |
||||
|
alias || symbolName, |
||||
|
); |
||||
|
|
||||
|
applyToUpdateRecorder(recorder, [ |
||||
|
insertImport(sourceFile, filePath, symbolName, moduleName, false, alias), |
||||
|
]); |
||||
|
tree.commitUpdate(recorder); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
return { code, rules }; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
/** |
||||
|
* @license |
||||
|
* Copyright Google LLC All Rights Reserved. |
||||
|
* |
||||
|
* Use of this source code is governed by an MIT-style license that can be |
||||
|
* found in the LICENSE file at https://angular.dev/license
|
||||
|
*/ |
||||
|
|
||||
|
export { addRootImport, addRootProvider } from './rules'; |
||||
|
export type { PendingCode, CodeBlockCallback, CodeBlock } from './code_block'; |
||||
@ -0,0 +1,258 @@ |
|||||
|
/** |
||||
|
* @license |
||||
|
* Copyright Google LLC All Rights Reserved. |
||||
|
* |
||||
|
* Use of this source code is governed by an MIT-style license that can be |
||||
|
* found in the LICENSE file at https://angular.dev/license
|
||||
|
*/ |
||||
|
|
||||
|
import { tags } from '@angular-devkit/core'; |
||||
|
import { Rule, SchematicsException, Tree, chain } from '@angular-devkit/schematics'; |
||||
|
import * as ts from 'typescript'; |
||||
|
import { addSymbolToNgModuleMetadata, insertAfterLastOccurrence } from '../ast-utils'; |
||||
|
import { InsertChange } from '../change'; |
||||
|
import { getAppModulePath, isStandaloneApp } from '../ng-ast-utils'; |
||||
|
import { ResolvedAppConfig, findAppConfig } from './app_config'; |
||||
|
import { CodeBlock, CodeBlockCallback, PendingCode } from './code_block'; |
||||
|
import { |
||||
|
applyChangesToFile, |
||||
|
findBootstrapApplicationCall, |
||||
|
findProvidersLiteral, |
||||
|
getMainFilePath, |
||||
|
getSourceFile, |
||||
|
isMergeAppConfigCall, |
||||
|
} from './util'; |
||||
|
|
||||
|
/** |
||||
|
* Adds an import to the root of the project. |
||||
|
* @param project Name of the project to which to add the import. |
||||
|
* @param callback Function that generates the code block which should be inserted. |
||||
|
* @example |
||||
|
* |
||||
|
* ```ts
|
||||
|
* import { Rule } from '@angular-devkit/schematics'; |
||||
|
* import { addRootImport } from '@schematics/angular/utility'; |
||||
|
* |
||||
|
* export default function(): Rule { |
||||
|
* return addRootImport('default', ({code, external}) => { |
||||
|
* return code`${external('MyModule', '@my/module')}.forRoot({})`; |
||||
|
* }); |
||||
|
* } |
||||
|
* ``` |
||||
|
*/ |
||||
|
export function addRootImport(project: string, callback: CodeBlockCallback): Rule { |
||||
|
return getRootInsertionRule(project, callback, 'imports', { |
||||
|
name: 'importProvidersFrom', |
||||
|
module: '@angular/core', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Adds a provider to the root of the project. |
||||
|
* @param project Name of the project to which to add the import. |
||||
|
* @param callback Function that generates the code block which should be inserted. |
||||
|
* @example |
||||
|
* |
||||
|
* ```ts
|
||||
|
* import { Rule } from '@angular-devkit/schematics'; |
||||
|
* import { addRootProvider } from '@schematics/angular/utility'; |
||||
|
* |
||||
|
* export default function(): Rule { |
||||
|
* return addRootProvider('default', ({code, external}) => { |
||||
|
* return code`${external('provideLibrary', '@my/library')}({})`; |
||||
|
* }); |
||||
|
* } |
||||
|
* ``` |
||||
|
*/ |
||||
|
export function addRootProvider(project: string, callback: CodeBlockCallback): Rule { |
||||
|
return getRootInsertionRule(project, callback, 'providers'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Creates a rule that inserts code at the root of either a standalone or NgModule-based project. |
||||
|
* @param project Name of the project into which to inser tthe code. |
||||
|
* @param callback Function that generates the code block which should be inserted. |
||||
|
* @param ngModuleField Field of the root NgModule into which the code should be inserted, if the |
||||
|
* app is based on NgModule |
||||
|
* @param standaloneWrapperFunction Function with which to wrap the code if the app is standalone. |
||||
|
*/ |
||||
|
function getRootInsertionRule( |
||||
|
project: string, |
||||
|
callback: CodeBlockCallback, |
||||
|
ngModuleField: string, |
||||
|
standaloneWrapperFunction?: { name: string; module: string }, |
||||
|
): Rule { |
||||
|
return async host => { |
||||
|
const mainFilePath = await getMainFilePath(host, project); |
||||
|
const codeBlock = new CodeBlock(); |
||||
|
|
||||
|
if (isStandaloneApp(host, mainFilePath)) { |
||||
|
return tree => |
||||
|
addProviderToStandaloneBootstrap( |
||||
|
tree, |
||||
|
callback(codeBlock), |
||||
|
mainFilePath, |
||||
|
standaloneWrapperFunction, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const modulePath = getAppModulePath(host, mainFilePath); |
||||
|
const pendingCode = CodeBlock.transformPendingCode(callback(codeBlock), modulePath); |
||||
|
|
||||
|
return chain([ |
||||
|
...pendingCode.rules, |
||||
|
tree => { |
||||
|
const changes = addSymbolToNgModuleMetadata( |
||||
|
getSourceFile(tree, modulePath), |
||||
|
modulePath, |
||||
|
ngModuleField, |
||||
|
pendingCode.code.expression, |
||||
|
// Explicitly set the import path to null since we deal with imports here separately.
|
||||
|
null, |
||||
|
); |
||||
|
|
||||
|
applyChangesToFile(tree, modulePath, changes); |
||||
|
}, |
||||
|
]); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Adds a provider to the root of a standalone project. |
||||
|
* @param host Tree of the root rule. |
||||
|
* @param pendingCode Code that should be inserted. |
||||
|
* @param mainFilePath Path to the project's main file. |
||||
|
* @param wrapperFunction Optional function with which to wrap the provider. |
||||
|
*/ |
||||
|
function addProviderToStandaloneBootstrap( |
||||
|
host: Tree, |
||||
|
pendingCode: PendingCode, |
||||
|
mainFilePath: string, |
||||
|
wrapperFunction?: { name: string; module: string }, |
||||
|
): Rule { |
||||
|
const bootstrapCall = findBootstrapApplicationCall(host, mainFilePath); |
||||
|
const fileToEdit = findAppConfig(bootstrapCall, host, mainFilePath)?.filePath || mainFilePath; |
||||
|
const { code, rules } = CodeBlock.transformPendingCode(pendingCode, fileToEdit); |
||||
|
|
||||
|
return chain([ |
||||
|
...rules, |
||||
|
() => { |
||||
|
let wrapped: PendingCode; |
||||
|
let additionalRules: Rule[]; |
||||
|
|
||||
|
if (wrapperFunction) { |
||||
|
const block = new CodeBlock(); |
||||
|
const result = CodeBlock.transformPendingCode( |
||||
|
block.code`${block.external(wrapperFunction.name, wrapperFunction.module)}(${ |
||||
|
code.expression |
||||
|
})`,
|
||||
|
fileToEdit, |
||||
|
); |
||||
|
|
||||
|
wrapped = result.code; |
||||
|
additionalRules = result.rules; |
||||
|
} else { |
||||
|
wrapped = code; |
||||
|
additionalRules = []; |
||||
|
} |
||||
|
|
||||
|
return chain([ |
||||
|
...additionalRules, |
||||
|
tree => insertStandaloneRootProvider(tree, mainFilePath, wrapped.expression), |
||||
|
]); |
||||
|
}, |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Inserts a string expression into the root of a standalone project. |
||||
|
* @param tree File tree used to modify the project. |
||||
|
* @param mainFilePath Path to the main file of the project. |
||||
|
* @param expression Code expression to be inserted. |
||||
|
*/ |
||||
|
function insertStandaloneRootProvider(tree: Tree, mainFilePath: string, expression: string): void { |
||||
|
const bootstrapCall = findBootstrapApplicationCall(tree, mainFilePath); |
||||
|
const appConfig = findAppConfig(bootstrapCall, tree, mainFilePath); |
||||
|
|
||||
|
if (bootstrapCall.arguments.length === 0) { |
||||
|
throw new SchematicsException( |
||||
|
`Cannot add provider to invalid bootstrapApplication call in ${ |
||||
|
bootstrapCall.getSourceFile().fileName |
||||
|
}`,
|
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (appConfig) { |
||||
|
addProvidersExpressionToAppConfig(tree, appConfig, expression); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const newAppConfig = `, {\n${tags.indentBy(2)`providers: [${expression}]`}\n}`; |
||||
|
let targetCall: ts.CallExpression; |
||||
|
|
||||
|
if (bootstrapCall.arguments.length === 1) { |
||||
|
targetCall = bootstrapCall; |
||||
|
} else if (isMergeAppConfigCall(bootstrapCall.arguments[1])) { |
||||
|
targetCall = bootstrapCall.arguments[1]; |
||||
|
} else { |
||||
|
throw new SchematicsException( |
||||
|
`Cannot statically analyze bootstrapApplication call in ${ |
||||
|
bootstrapCall.getSourceFile().fileName |
||||
|
}`,
|
||||
|
); |
||||
|
} |
||||
|
|
||||
|
applyChangesToFile(tree, mainFilePath, [ |
||||
|
insertAfterLastOccurrence( |
||||
|
targetCall.arguments, |
||||
|
newAppConfig, |
||||
|
mainFilePath, |
||||
|
targetCall.getEnd() - 1, |
||||
|
), |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Adds a string expression to an app config object. |
||||
|
* @param tree File tree used to modify the project. |
||||
|
* @param appConfig Resolved configuration object of the project. |
||||
|
* @param expression Code expression to be inserted. |
||||
|
*/ |
||||
|
function addProvidersExpressionToAppConfig( |
||||
|
tree: Tree, |
||||
|
appConfig: ResolvedAppConfig, |
||||
|
expression: string, |
||||
|
): void { |
||||
|
const { node, filePath } = appConfig; |
||||
|
const configProps = node.properties; |
||||
|
const providersLiteral = findProvidersLiteral(node); |
||||
|
|
||||
|
// If there's a `providers` property, we can add the provider
|
||||
|
// to it, otherwise we need to declare it ourselves.
|
||||
|
if (providersLiteral) { |
||||
|
applyChangesToFile(tree, filePath, [ |
||||
|
insertAfterLastOccurrence( |
||||
|
providersLiteral.elements, |
||||
|
(providersLiteral.elements.length === 0 ? '' : ', ') + expression, |
||||
|
filePath, |
||||
|
providersLiteral.getStart() + 1, |
||||
|
), |
||||
|
]); |
||||
|
} else { |
||||
|
const prop = tags.indentBy(2)`providers: [${expression}]`; |
||||
|
let toInsert: string; |
||||
|
let insertPosition: number; |
||||
|
|
||||
|
if (configProps.length === 0) { |
||||
|
toInsert = '\n' + prop + '\n'; |
||||
|
insertPosition = node.getEnd() - 1; |
||||
|
} else { |
||||
|
const hasTrailingComma = configProps.hasTrailingComma; |
||||
|
toInsert = (hasTrailingComma ? '' : ',') + '\n' + prop; |
||||
|
insertPosition = configProps[configProps.length - 1].getEnd() + (hasTrailingComma ? 1 : 0); |
||||
|
} |
||||
|
|
||||
|
applyChangesToFile(tree, filePath, [new InsertChange(filePath, insertPosition, toInsert)]); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,171 @@ |
|||||
|
/** |
||||
|
* @license |
||||
|
* Copyright Google LLC All Rights Reserved. |
||||
|
* |
||||
|
* Use of this source code is governed by an MIT-style license that can be |
||||
|
* found in the LICENSE file at https://angular.dev/license
|
||||
|
*/ |
||||
|
|
||||
|
import { SchematicsException, Tree } from '@angular-devkit/schematics'; |
||||
|
import * as ts from 'typescript'; |
||||
|
import { Change, applyToUpdateRecorder } from '../change'; |
||||
|
import { targetBuildNotFoundError } from '../project-targets'; |
||||
|
import { getWorkspace } from '../workspace'; |
||||
|
import { Builders } from '../workspace-models'; |
||||
|
|
||||
|
/** |
||||
|
* Finds the main file of a project. |
||||
|
* @param tree File tree for the project. |
||||
|
* @param projectName Name of the project in which to search. |
||||
|
*/ |
||||
|
export async function getMainFilePath(tree: Tree, projectName: string): Promise<string> { |
||||
|
const workspace = await getWorkspace(tree); |
||||
|
const project = workspace.projects.get(projectName); |
||||
|
const buildTarget = project?.targets.get('build'); |
||||
|
|
||||
|
if (!buildTarget) { |
||||
|
throw targetBuildNotFoundError(); |
||||
|
} |
||||
|
|
||||
|
const options = buildTarget.options as Record<string, string>; |
||||
|
|
||||
|
return buildTarget.builder === Builders.Application || |
||||
|
buildTarget.builder === Builders.BuildApplication |
||||
|
? options.browser |
||||
|
: options.main; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Gets a TypeScript source file at a specific path. |
||||
|
* @param tree File tree of a project. |
||||
|
* @param path Path to the file. |
||||
|
*/ |
||||
|
export function getSourceFile(tree: Tree, path: string): ts.SourceFile { |
||||
|
const content = tree.readText(path); |
||||
|
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); |
||||
|
|
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
/** Finds the call to `bootstrapApplication` within a file. */ |
||||
|
export function findBootstrapApplicationCall(tree: Tree, mainFilePath: string): ts.CallExpression { |
||||
|
const sourceFile = getSourceFile(tree, mainFilePath); |
||||
|
const localName = findImportLocalName( |
||||
|
sourceFile, |
||||
|
'bootstrapApplication', |
||||
|
'@angular/platform-browser', |
||||
|
); |
||||
|
|
||||
|
if (localName) { |
||||
|
let result: ts.CallExpression | null = null; |
||||
|
|
||||
|
sourceFile.forEachChild(function walk(node) { |
||||
|
if ( |
||||
|
ts.isCallExpression(node) && |
||||
|
ts.isIdentifier(node.expression) && |
||||
|
node.expression.text === localName |
||||
|
) { |
||||
|
result = node; |
||||
|
} |
||||
|
|
||||
|
if (!result) { |
||||
|
node.forEachChild(walk); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (result) { |
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
throw new SchematicsException(`Could not find bootstrapApplication call in ${mainFilePath}`); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Finds the local name of an imported symbol. Could be the symbol name itself or its alias. |
||||
|
* @param sourceFile File within which to search for the import. |
||||
|
* @param name Actual name of the import, not its local alias. |
||||
|
* @param moduleName Name of the module from which the symbol is imported. |
||||
|
*/ |
||||
|
function findImportLocalName( |
||||
|
sourceFile: ts.SourceFile, |
||||
|
name: string, |
||||
|
moduleName: string, |
||||
|
): string | null { |
||||
|
for (const node of sourceFile.statements) { |
||||
|
// Only look for top-level imports.
|
||||
|
if ( |
||||
|
!ts.isImportDeclaration(node) || |
||||
|
!ts.isStringLiteral(node.moduleSpecifier) || |
||||
|
node.moduleSpecifier.text !== moduleName |
||||
|
) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// Filter out imports that don't have the right shape.
|
||||
|
if ( |
||||
|
!node.importClause || |
||||
|
!node.importClause.namedBindings || |
||||
|
!ts.isNamedImports(node.importClause.namedBindings) |
||||
|
) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// Look through the elements of the declaration for the specific import.
|
||||
|
for (const element of node.importClause.namedBindings.elements) { |
||||
|
if ((element.propertyName || element.name).text === name) { |
||||
|
// The local name is always in `name`.
|
||||
|
return element.name.text; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Applies a set of changes to a file. |
||||
|
* @param tree File tree of the project. |
||||
|
* @param path Path to the file that is being changed. |
||||
|
* @param changes Changes that should be applied to the file. |
||||
|
*/ |
||||
|
export function applyChangesToFile(tree: Tree, path: string, changes: Change[]) { |
||||
|
if (changes.length > 0) { |
||||
|
const recorder = tree.beginUpdate(path); |
||||
|
applyToUpdateRecorder(recorder, changes); |
||||
|
tree.commitUpdate(recorder); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** Checks whether a node is a call to `mergeApplicationConfig`. */ |
||||
|
export function isMergeAppConfigCall(node: ts.Node): node is ts.CallExpression { |
||||
|
if (!ts.isCallExpression(node)) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
const localName = findImportLocalName( |
||||
|
node.getSourceFile(), |
||||
|
'mergeApplicationConfig', |
||||
|
'@angular/core', |
||||
|
); |
||||
|
|
||||
|
return !!localName && ts.isIdentifier(node.expression) && node.expression.text === localName; |
||||
|
} |
||||
|
|
||||
|
/** Finds the `providers` array literal within an application config. */ |
||||
|
export function findProvidersLiteral( |
||||
|
config: ts.ObjectLiteralExpression, |
||||
|
): ts.ArrayLiteralExpression | null { |
||||
|
for (const prop of config.properties) { |
||||
|
if ( |
||||
|
ts.isPropertyAssignment(prop) && |
||||
|
ts.isIdentifier(prop.name) && |
||||
|
prop.name.text === 'providers' && |
||||
|
ts.isArrayLiteralExpression(prop.initializer) |
||||
|
) { |
||||
|
return prop.initializer; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue