mirror of https://github.com/abpframework/abp.git
89 changed files with 9980 additions and 4 deletions
@ -0,0 +1,36 @@ |
|||
{ |
|||
"extends": ["../../.eslintrc.json"], |
|||
"ignorePatterns": ["!**/*"], |
|||
"overrides": [ |
|||
{ |
|||
"files": ["*.ts"], |
|||
"extends": [ |
|||
"plugin:@nrwl/nx/angular", |
|||
"plugin:@angular-eslint/template/process-inline-templates" |
|||
], |
|||
"rules": { |
|||
"@angular-eslint/directive-selector": [ |
|||
"error", |
|||
{ |
|||
"type": "attribute", |
|||
"prefix": "abp", |
|||
"style": "camelCase" |
|||
} |
|||
], |
|||
"@angular-eslint/component-selector": [ |
|||
"error", |
|||
{ |
|||
"type": "element", |
|||
"prefix": "abp", |
|||
"style": "kebab-case" |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
"files": ["*.html"], |
|||
"extends": ["plugin:@nrwl/nx/angular-template"], |
|||
"rules": {} |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
# Outputs |
|||
src/**/*.js |
|||
src/**/*.js.map |
|||
src/**/*.d.ts |
|||
|
|||
# IDEs |
|||
.idea/ |
|||
jsconfig.json |
|||
.vscode/ |
|||
|
|||
# Misc |
|||
node_modules/ |
|||
npm-debug.log* |
|||
yarn-error.log* |
|||
|
|||
# Mac OSX Finder files. |
|||
**/.DS_Store |
|||
.DS_Store |
|||
@ -0,0 +1,3 @@ |
|||
# Ignores TypeScript files, but keeps definitions. |
|||
*.ts |
|||
!*.d.ts |
|||
@ -0,0 +1,3 @@ |
|||
# ABP Suite Schematics |
|||
|
|||
TODO: Add usage and development information |
|||
@ -0,0 +1,20 @@ |
|||
module.exports = { |
|||
displayName: 'schematics', |
|||
preset: '../../jest.preset.js', |
|||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], |
|||
globals: { |
|||
'ts-jest': { |
|||
tsconfig: '<rootDir>/tsconfig.spec.json', |
|||
stringifyContentPathRegex: '\\.(html|svg)$', |
|||
}, |
|||
}, |
|||
coverageDirectory: '../../coverage/packages/schematics', |
|||
transform: { |
|||
'^.+\\.(ts|js|html)$': 'jest-preset-angular', |
|||
}, |
|||
snapshotSerializers: [ |
|||
'jest-preset-angular/build/serializers/no-ng-attributes', |
|||
'jest-preset-angular/build/serializers/ng-snapshot', |
|||
'jest-preset-angular/build/serializers/html-comment', |
|||
], |
|||
}; |
|||
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "@abp/ng.schematics", |
|||
"version": "4.4.0", |
|||
"description": "Schematics that works with ABP Backend", |
|||
"keywords": [ |
|||
"schematics" |
|||
], |
|||
"author": "", |
|||
"license": "MIT", |
|||
"schematics": "./collection.json", |
|||
"dependencies": { |
|||
"@angular-devkit/core": "~11.0.2", |
|||
"@angular-devkit/schematics": "~11.0.2", |
|||
"got": "^11.5.2", |
|||
"jsonc-parser": "^2.3.0", |
|||
"should-quote": "^1.0.0", |
|||
"typescript": "~3.9.2" |
|||
}, |
|||
"devDependencies": { |
|||
"@schematics/angular": "~11.0.2", |
|||
"@types/jest": "^26.0.0", |
|||
"@types/node": "^12.11.1", |
|||
"jest": "^26.0.0", |
|||
"jest-preset-angular": "^8.2.0" |
|||
}, |
|||
"publishConfig": { |
|||
"access": "public" |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
{ |
|||
"schematics": { |
|||
"proxy-add": { |
|||
"description": "ABP Proxy Generator Add Schematics", |
|||
"factory": "./commands/proxy-add", |
|||
"schema": "./commands/proxy-add/schema.json" |
|||
}, |
|||
"proxy-index": { |
|||
"description": "ABP Proxy Generator Index Schematics", |
|||
"factory": "./commands/proxy-index", |
|||
"schema": "./commands/proxy-index/schema.json" |
|||
}, |
|||
"proxy-refresh": { |
|||
"description": "ABP Proxy Generator Refresh Schematics", |
|||
"factory": "./commands/proxy-refresh", |
|||
"schema": "./commands/proxy-refresh/schema.json" |
|||
}, |
|||
"proxy-remove": { |
|||
"description": "ABP Proxy Generator Remove Schematics", |
|||
"factory": "./commands/proxy-remove", |
|||
"schema": "./commands/proxy-remove/schema.json" |
|||
}, |
|||
"api": { |
|||
"description": "ABP API Generator Schematics", |
|||
"factory": "./commands/api", |
|||
"schema": "./commands/api/schema.json" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
import { mapEnumToOptions } from '@abp/ng.core'; |
|||
|
|||
export enum <%= name %> {<% |
|||
for (let member of members) { %> |
|||
<%= member.key %> = <%= quote(member.value) %>,<% } %> |
|||
} |
|||
|
|||
export const <%= camel(name) %>Options = mapEnumToOptions(<%= name %>); |
|||
@ -0,0 +1,10 @@ |
|||
<% |
|||
for (const {keyword, specifiers, path} of imports) { |
|||
%><%= keyword %> { <%= specifiers.join(', ') %> } from '<%= path %>'; |
|||
<% } |
|||
for (let {base, identifier, properties} of interfaces) { %> |
|||
export interface <%= identifier %> <%= base ? `extends ${base} ` : '' %>{<% |
|||
for (let {name, optional, type} of properties) { %> |
|||
<%= name + optional %>: <%= type %>;<% } %> |
|||
} |
|||
<% } %> |
|||
@ -0,0 +1,25 @@ |
|||
<% for (const {keyword, specifiers, path} of imports) { |
|||
%><%= keyword %> { <%= specifiers.join(', ') %> } from '<%= path %>'; |
|||
<% } %> |
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class <%= name %>Service { |
|||
apiName = '<%= apiName %>';<% |
|||
for (let {body, signature} of methods) { %> |
|||
|
|||
<%= camel(signature.name) %> = (<%= serializeParameters(signature.parameters) %>) => |
|||
this.restService.request<<%= body.requestType %>, <%= body.responseType %>>({ |
|||
method: '<%= body.method %>',<% |
|||
if (body.responseType === 'string') { %> |
|||
responseType: 'text',<% } %> |
|||
url: <%= body.url %>,<% |
|||
if (body.params.length) { %> |
|||
params: { <%= body.params.join(', ') %> },<% } |
|||
if (body.body) { %> |
|||
body: <%= body.body %>,<% } %> |
|||
}, |
|||
{ apiName: this.apiName });<% } %> |
|||
|
|||
constructor(private restService: RestService) {} |
|||
} |
|||
@ -0,0 +1,172 @@ |
|||
import { normalize } from '@angular-devkit/core'; |
|||
import { |
|||
applyTemplates, |
|||
branchAndMerge, |
|||
chain, |
|||
move, |
|||
SchematicContext, |
|||
SchematicsException, |
|||
Tree, |
|||
url, |
|||
} from '@angular-devkit/schematics'; |
|||
import { Exception } from '../../enums'; |
|||
import { GenerateProxySchema, ServiceGeneratorParams } from '../../models'; |
|||
import { |
|||
applyWithOverwrite, |
|||
buildDefaultPath, |
|||
createControllerToServiceMapper, |
|||
createImportRefsToModelReducer, |
|||
createImportRefToEnumMapper, |
|||
createProxyConfigReader, |
|||
createProxyConfigWriterCreator, |
|||
createRootNamespaceGetter, |
|||
EnumGeneratorParams, |
|||
generateProxyConfigJson, |
|||
getEnumNamesFromImports, |
|||
interpolate, |
|||
ModelGeneratorParams, |
|||
removeDefaultPlaceholders, |
|||
resolveProject, |
|||
serializeParameters, |
|||
} from '../../utils'; |
|||
import * as cases from '../../utils/text'; |
|||
|
|||
export default function(schema: GenerateProxySchema) { |
|||
const params = removeDefaultPlaceholders(schema); |
|||
const moduleName = params.module || 'app'; |
|||
|
|||
return chain([ |
|||
async (tree: Tree, _context: SchematicContext) => { |
|||
const getRootNamespace = createRootNamespaceGetter(params); |
|||
const solution = await getRootNamespace(tree); |
|||
|
|||
const target = await resolveProject(tree, params.target!); |
|||
const targetPath = buildDefaultPath(target.definition); |
|||
const readProxyConfig = createProxyConfigReader(targetPath); |
|||
const createProxyConfigWriter = createProxyConfigWriterCreator(targetPath); |
|||
const data = readProxyConfig(tree); |
|||
const types = data.types; |
|||
const modules = data.modules; |
|||
if (!types || !modules) throw new SchematicsException(Exception.InvalidApiDefinition); |
|||
|
|||
const definition = data.modules[moduleName]; |
|||
if (!definition) |
|||
throw new SchematicsException(interpolate(Exception.InvalidModule, moduleName)); |
|||
|
|||
const apiName = definition.remoteServiceName; |
|||
const controllers = Object.values(definition.controllers || {}); |
|||
const serviceImports: Record<string, string[]> = {}; |
|||
const generateServices = createServiceGenerator({ |
|||
targetPath, |
|||
solution, |
|||
types, |
|||
apiName, |
|||
controllers, |
|||
serviceImports, |
|||
}); |
|||
|
|||
const modelImports: Record<string, string[]> = {}; |
|||
const generateModels = createModelGenerator({ |
|||
targetPath, |
|||
solution, |
|||
types, |
|||
serviceImports, |
|||
modelImports, |
|||
}); |
|||
|
|||
const generateEnums = createEnumGenerator({ |
|||
targetPath, |
|||
solution, |
|||
types, |
|||
serviceImports, |
|||
modelImports, |
|||
}); |
|||
|
|||
if (!data.generated.includes(moduleName)) data.generated.push(moduleName); |
|||
data.generated.sort(); |
|||
const json = generateProxyConfigJson(data); |
|||
const overwriteProxyConfig = createProxyConfigWriter('overwrite', json); |
|||
|
|||
return branchAndMerge( |
|||
chain([generateServices, generateModels, generateEnums, overwriteProxyConfig]), |
|||
); |
|||
}, |
|||
]); |
|||
} |
|||
|
|||
function createEnumGenerator(params: EnumGeneratorParams) { |
|||
const { targetPath, serviceImports, modelImports } = params; |
|||
const mapImportRefToEnum = createImportRefToEnumMapper(params); |
|||
const enumRefs = [ |
|||
...new Set([ |
|||
...getEnumNamesFromImports(serviceImports), |
|||
...getEnumNamesFromImports(modelImports), |
|||
]), |
|||
]; |
|||
|
|||
return chain( |
|||
enumRefs.map(ref => { |
|||
return applyWithOverwrite(url('./files-enum'), [ |
|||
applyTemplates({ |
|||
...cases, |
|||
...mapImportRefToEnum(ref), |
|||
}), |
|||
move(normalize(targetPath)), |
|||
]); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
function createModelGenerator(params: ModelGeneratorParams) { |
|||
const { targetPath, serviceImports, modelImports } = params; |
|||
const reduceImportRefsToModels = createImportRefsToModelReducer(params); |
|||
const models = Object.values(serviceImports).reduce(reduceImportRefsToModels, []); |
|||
models.forEach(({ imports }) => |
|||
imports.forEach(({ refs, path }) => |
|||
refs.forEach(ref => { |
|||
if (path === '@abp/ng.core') return; |
|||
if (!modelImports[path]) return (modelImports[path] = [ref]); |
|||
modelImports[path] = [...new Set([...modelImports[path], ref])]; |
|||
}), |
|||
), |
|||
); |
|||
|
|||
return chain( |
|||
models.map(model => |
|||
applyWithOverwrite(url('./files-model'), [ |
|||
applyTemplates({ |
|||
...cases, |
|||
...model, |
|||
}), |
|||
move(normalize(targetPath)), |
|||
]), |
|||
), |
|||
); |
|||
} |
|||
|
|||
function createServiceGenerator(params: ServiceGeneratorParams) { |
|||
const { targetPath, controllers, serviceImports } = params; |
|||
const mapControllerToService = createControllerToServiceMapper(params); |
|||
|
|||
return chain( |
|||
controllers.map(controller => { |
|||
const service = mapControllerToService(controller); |
|||
service.imports.forEach(({ refs, path }) => |
|||
refs.forEach(ref => { |
|||
if (path === '@abp/ng.core') return; |
|||
if (!serviceImports[path]) return (serviceImports[path] = [ref]); |
|||
serviceImports[path] = [...new Set([...serviceImports[path], ref])]; |
|||
}), |
|||
); |
|||
|
|||
return applyWithOverwrite(url('./files-service'), [ |
|||
applyTemplates({ |
|||
...cases, |
|||
serializeParameters, |
|||
...service, |
|||
}), |
|||
move(normalize(targetPath)), |
|||
]); |
|||
}), |
|||
); |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
{ |
|||
"$schema": "http://json-schema.org/schema", |
|||
"id": "SchematicsAbpGenerateAPI", |
|||
"title": "ABP Generate API Schema", |
|||
"type": "object", |
|||
"properties": { |
|||
"module": { |
|||
"description": "Backend module name", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 0 |
|||
}, |
|||
"x-prompt": "Please enter backend module name. (default: \"app\")" |
|||
}, |
|||
"api-name": { |
|||
"description": "Backend api name, a.k.a. remoteServiceName", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 1 |
|||
}, |
|||
"x-prompt": "Please enter backend api name, a.k.a. remoteServiceName. (default: \"default\")" |
|||
}, |
|||
"source": { |
|||
"description": "Source Angular project for API definition URL & root namespace resolution", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 2 |
|||
}, |
|||
"x-prompt": "Please enter source Angular project for API definition URL & root namespace resolution. (default: workspace \"defaultProject\")" |
|||
}, |
|||
"target": { |
|||
"description": "Target Angular project to place the generated code", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 3 |
|||
}, |
|||
"x-prompt": "Please enter target Angular project to place the generated code. (default: workspace \"defaultProject\")" |
|||
} |
|||
}, |
|||
"required": [] |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
import { chain, SchematicContext, Tree } from '@angular-devkit/schematics'; |
|||
import { GenerateProxySchema } from '../../models'; |
|||
import { |
|||
buildDefaultPath, |
|||
createApiDefinitionGetter, |
|||
createApisGenerator, |
|||
createProxyClearer, |
|||
createProxyConfigReader, |
|||
createProxyConfigSaver, |
|||
createProxyIndexGenerator, |
|||
createProxyWarningSaver, |
|||
mergeAndAllowDelete, |
|||
removeDefaultPlaceholders, |
|||
resolveProject, |
|||
} from '../../utils'; |
|||
|
|||
export default function(schema: GenerateProxySchema) { |
|||
const params = removeDefaultPlaceholders(schema); |
|||
const moduleName = params.module || 'app'; |
|||
|
|||
return chain([ |
|||
async (host: Tree, _context: SchematicContext) => { |
|||
const target = await resolveProject(host, params.target!); |
|||
const targetPath = buildDefaultPath(target.definition); |
|||
const readProxyConfig = createProxyConfigReader(targetPath); |
|||
let generated: string[] = []; |
|||
|
|||
try { |
|||
generated = readProxyConfig(host).generated; |
|||
const index = generated.findIndex(m => m === moduleName); |
|||
if (index < 0) generated.push(moduleName); |
|||
} catch (_) { |
|||
generated.push(moduleName); |
|||
} |
|||
|
|||
const getApiDefinition = createApiDefinitionGetter(params); |
|||
const data = { generated, ...(await getApiDefinition(host)) }; |
|||
data.generated = []; |
|||
|
|||
const clearProxy = createProxyClearer(targetPath); |
|||
|
|||
const saveProxyConfig = createProxyConfigSaver(data, targetPath); |
|||
|
|||
const saveProxyWarning = createProxyWarningSaver(targetPath); |
|||
|
|||
const generateApis = createApisGenerator(schema, generated); |
|||
|
|||
const generateIndex = createProxyIndexGenerator(targetPath); |
|||
|
|||
return chain([ |
|||
mergeAndAllowDelete(host, clearProxy), |
|||
saveProxyConfig, |
|||
saveProxyWarning, |
|||
generateApis, |
|||
generateIndex, |
|||
]); |
|||
}, |
|||
]); |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
{ |
|||
"$schema": "http://json-schema.org/schema", |
|||
"id": "SchematicsAbpGenerateProxy", |
|||
"title": "ABP Generate Proxy Schema", |
|||
"type": "object", |
|||
"properties": { |
|||
"module": { |
|||
"description": "Backend module name", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 0 |
|||
}, |
|||
"x-prompt": "Please enter backend module name. (default: \"app\")" |
|||
}, |
|||
"api-name": { |
|||
"description": "Backend api name, a.k.a. remoteServiceName", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 1 |
|||
}, |
|||
"x-prompt": "Please enter backend api name, a.k.a. remoteServiceName. (default: \"default\")" |
|||
}, |
|||
"source": { |
|||
"description": "Source Angular project for API definition URL & root namespace resolution", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 2 |
|||
}, |
|||
"x-prompt": "Please enter source Angular project for API definition URL & root namespace resolution. (default: workspace \"defaultProject\")" |
|||
}, |
|||
"target": { |
|||
"description": "Target Angular project to place the generated code", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 3 |
|||
}, |
|||
"x-prompt": "Please enter target Angular project to place the generated code. (default: workspace \"defaultProject\")" |
|||
} |
|||
}, |
|||
"required": [] |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
import { SchematicContext, Tree } from '@angular-devkit/schematics'; |
|||
import { |
|||
buildDefaultPath, |
|||
createProxyIndexGenerator, |
|||
removeDefaultPlaceholders, |
|||
resolveProject, |
|||
} from '../../utils'; |
|||
|
|||
export default function(schema: { target?: string }) { |
|||
const params = removeDefaultPlaceholders(schema); |
|||
|
|||
return async (host: Tree, _context: SchematicContext) => { |
|||
const target = await resolveProject(host, params.target!); |
|||
const targetPath = buildDefaultPath(target.definition); |
|||
|
|||
const generateIndex = createProxyIndexGenerator(targetPath); |
|||
|
|||
return generateIndex(host); |
|||
}; |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
{ |
|||
"$schema": "http://json-schema.org/schema", |
|||
"id": "SchematicsAbpIndexProxy", |
|||
"title": "ABP Index Proxy Schema", |
|||
"type": "object", |
|||
"properties": { |
|||
"target": { |
|||
"description": "Target Angular project to place the generated code", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 0 |
|||
}, |
|||
"x-prompt": "Please enter target Angular project to place the generated code. (default: workspace \"defaultProject\")" |
|||
} |
|||
}, |
|||
"required": [] |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
import { chain, SchematicContext, Tree } from '@angular-devkit/schematics'; |
|||
import { GenerateProxySchema } from '../../models'; |
|||
import { |
|||
buildDefaultPath, |
|||
createApiDefinitionGetter, |
|||
createApisGenerator, |
|||
createProxyClearer, |
|||
createProxyConfigReader, |
|||
createProxyConfigSaver, |
|||
createProxyIndexGenerator, |
|||
mergeAndAllowDelete, |
|||
removeDefaultPlaceholders, |
|||
resolveProject, |
|||
} from '../../utils'; |
|||
|
|||
export default function(schema: GenerateProxySchema) { |
|||
const params = removeDefaultPlaceholders(schema); |
|||
|
|||
return async (host: Tree, _context: SchematicContext) => { |
|||
const target = await resolveProject(host, params.target!); |
|||
const targetPath = buildDefaultPath(target.definition); |
|||
|
|||
const readProxyConfig = createProxyConfigReader(targetPath); |
|||
const { generated } = readProxyConfig(host); |
|||
|
|||
const getApiDefinition = createApiDefinitionGetter(params); |
|||
const data = { generated, ...(await getApiDefinition(host)) }; |
|||
data.generated = []; |
|||
|
|||
const clearProxy = createProxyClearer(targetPath); |
|||
|
|||
const saveProxyConfig = createProxyConfigSaver(data, targetPath); |
|||
|
|||
const generateApis = createApisGenerator(schema, generated); |
|||
|
|||
const generateIndex = createProxyIndexGenerator(targetPath); |
|||
|
|||
return chain([ |
|||
mergeAndAllowDelete(host, clearProxy), |
|||
saveProxyConfig, |
|||
generateApis, |
|||
generateIndex, |
|||
]); |
|||
}; |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
{ |
|||
"$schema": "http://json-schema.org/schema", |
|||
"id": "SchematicsAbpGenerateProxy", |
|||
"title": "ABP Generate Proxy Schema", |
|||
"type": "object", |
|||
"properties": { |
|||
"module": { |
|||
"description": "Backend module name", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 0 |
|||
}, |
|||
"x-prompt": "Please enter backend module name. (default: \"app\")" |
|||
}, |
|||
"api-name": { |
|||
"description": "Backend api name, a.k.a. remoteServiceName", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 1 |
|||
}, |
|||
"x-prompt": "Please enter backend api name, a.k.a. remoteServiceName. (default: \"default\")" |
|||
}, |
|||
"source": { |
|||
"description": "Source Angular project for API definition URL & root namespace resolution", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 2 |
|||
}, |
|||
"x-prompt": "Please enter source Angular project for API definition URL & root namespace resolution. (default: workspace \"defaultProject\")" |
|||
}, |
|||
"target": { |
|||
"description": "Target Angular project to place the generated code", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 3 |
|||
}, |
|||
"x-prompt": "Please enter target Angular project to place the generated code. (default: workspace \"defaultProject\")" |
|||
} |
|||
}, |
|||
"required": [] |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
import { chain, SchematicContext, Tree } from '@angular-devkit/schematics'; |
|||
import { GenerateProxySchema } from '../../models'; |
|||
import { |
|||
buildDefaultPath, |
|||
createApiDefinitionGetter, |
|||
createApisGenerator, |
|||
createProxyClearer, |
|||
createProxyConfigReader, |
|||
createProxyConfigSaver, |
|||
createProxyIndexGenerator, |
|||
mergeAndAllowDelete, |
|||
removeDefaultPlaceholders, |
|||
resolveProject, |
|||
} from '../../utils'; |
|||
|
|||
export default function(schema: GenerateProxySchema) { |
|||
const params = removeDefaultPlaceholders(schema); |
|||
const moduleName = params.module || 'app'; |
|||
|
|||
return async (host: Tree, _context: SchematicContext) => { |
|||
const target = await resolveProject(host, params.target!); |
|||
const targetPath = buildDefaultPath(target.definition); |
|||
|
|||
const readProxyConfig = createProxyConfigReader(targetPath); |
|||
const { generated } = readProxyConfig(host); |
|||
|
|||
const index = generated.findIndex(m => m === moduleName); |
|||
if (index < 0) return host; |
|||
generated.splice(index, 1); |
|||
|
|||
const getApiDefinition = createApiDefinitionGetter(params); |
|||
const data = { generated, ...(await getApiDefinition(host)) }; |
|||
data.generated = []; |
|||
|
|||
const clearProxy = createProxyClearer(targetPath); |
|||
|
|||
const saveProxyConfig = createProxyConfigSaver(data, targetPath); |
|||
|
|||
const generateApis = createApisGenerator(schema, generated); |
|||
|
|||
const generateIndex = createProxyIndexGenerator(targetPath); |
|||
|
|||
return chain([ |
|||
mergeAndAllowDelete(host, clearProxy), |
|||
saveProxyConfig, |
|||
generateApis, |
|||
generateIndex, |
|||
]); |
|||
}; |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
{ |
|||
"$schema": "http://json-schema.org/schema", |
|||
"id": "SchematicsAbpGenerateProxy", |
|||
"title": "ABP Generate Proxy Schema", |
|||
"type": "object", |
|||
"properties": { |
|||
"module": { |
|||
"description": "Backend module name", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 0 |
|||
}, |
|||
"x-prompt": "Please enter backend module name. (default: \"app\")" |
|||
}, |
|||
"api-name": { |
|||
"description": "Backend api name, a.k.a. remoteServiceName", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 1 |
|||
}, |
|||
"x-prompt": "Please enter backend api name, a.k.a. remoteServiceName. (default: \"default\")" |
|||
}, |
|||
"source": { |
|||
"description": "Source Angular project for API definition URL & root namespace resolution", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 2 |
|||
}, |
|||
"x-prompt": "Please enter source Angular project for API definition URL & root namespace resolution. (default: workspace \"defaultProject\")" |
|||
}, |
|||
"target": { |
|||
"description": "Target Angular project to place the generated code", |
|||
"type": "string", |
|||
"$default": { |
|||
"$source": "argv", |
|||
"index": 3 |
|||
}, |
|||
"x-prompt": "Please enter target Angular project to place the generated code. (default: workspace \"defaultProject\")" |
|||
} |
|||
}, |
|||
"required": [] |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export const API_DEFINITION_ENDPOINT = '/api/abp/api-definition'; |
|||
@ -0,0 +1,4 @@ |
|||
export * from './api'; |
|||
export * from './proxy'; |
|||
export * from './system-types'; |
|||
export * from './volo'; |
|||
@ -0,0 +1,22 @@ |
|||
export const PROXY_PATH = '/proxy'; |
|||
export const PROXY_CONFIG_PATH = `${PROXY_PATH}/generate-proxy.json`; |
|||
export const PROXY_WARNING_PATH = `${PROXY_PATH}/README.md`; |
|||
|
|||
export const PROXY_WARNING = `# Proxy Generation Output
|
|||
|
|||
This directory includes the output of the latest proxy generation. |
|||
The files and folders in it will be overwritten when proxy generation is run again. |
|||
Therefore, please do not place your own content in this folder. |
|||
|
|||
In addition, \`generate-proxy.json\` works like a lock file.
|
|||
It includes information used by the proxy generator, so please do not delete or modify it. |
|||
|
|||
Finally, the name of the files and folders should not be changed for two reasons: |
|||
- Proxy generator will keep creating them at those paths and you will have multiple copies of the same content. |
|||
- ABP Suite generates files which include imports from this folder. |
|||
|
|||
> **Important Notice:** If you are building a module and are planning to publish to npm, |
|||
> some of the generated proxies are likely to be exported from public-api.ts file. In such a case, |
|||
> please make sure you export files directly and not from barrel exports. In other words, |
|||
> do not include index.ts exports in your public-api.ts exports. |
|||
`;
|
|||
@ -0,0 +1,24 @@ |
|||
export const SYSTEM_TYPES = new Map([ |
|||
['Bool', 'boolean'], |
|||
['Byte', 'number'], |
|||
['Char', 'string'], |
|||
['Collections.Generic.Dictionary', 'Record'], |
|||
['DateTime', 'string'], |
|||
['DateTimeOffset', 'string'], |
|||
['Decimal', 'number'], |
|||
['Double', 'number'], |
|||
['Guid', 'string'], |
|||
['Int16', 'number'], |
|||
['Int32', 'number'], |
|||
['Int64', 'number'], |
|||
['Net.HttpStatusCode', 'number'], |
|||
['Object', 'object'], |
|||
['Sbyte', 'number'], |
|||
['Single', 'number'], |
|||
['String', 'string'], |
|||
['TimeSpan', 'string'], |
|||
['UInt16', 'number'], |
|||
['UInt32', 'number'], |
|||
['UInt64', 'number'], |
|||
['Void', 'void'], |
|||
]); |
|||
@ -0,0 +1 @@ |
|||
export const VOLO_REGEX = /^Volo\.Abp\.(Application\.Dtos|ObjectExtending)/; |
|||
@ -0,0 +1,6 @@ |
|||
export enum eBindingSourceId { |
|||
Body = 'Body', |
|||
Model = 'ModelBinding', |
|||
Path = 'Path', |
|||
Query = 'Query', |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
export const enum Exception { |
|||
DirRemoveFailed = '[Directory Remove Failed] Cannot remove "{0}".', |
|||
FileNotFound = '[File Not Found] There is no file at "{0}" path.', |
|||
FileWriteFailed = '[File Write Failed] Cannot write file at "{0}".', |
|||
InvalidModule = '[Invalid Module] Backend module "{0}" does not exist in API definition.', |
|||
InvalidApiDefinition = '[Invalid API Definition] The provided API definition is invalid.', |
|||
InvalidWorkspace = '[Invalid Workspace] The angular.json should be a valid JSON file.', |
|||
NoApi = '[API Not Available] Request to {0} is unsuccessful. Please double-check the URL in the source project environment and make sure your application is up and running.', |
|||
NoProject = '[Project Not Found] Either define a default project in your workspace or specify the project name in schematics options.', |
|||
NoProxyConfig = '[Proxy Config Not Found] There is no JSON file at "{0}".', |
|||
NoTypeDefinition = '[Type Definition Not Found] There is no type definition for "{0}".', |
|||
NoWorkspace = '[Workspace Not Found] Make sure you are running schematics at the root directory of your workspace and it has an angular.json file.', |
|||
NoEnvironment = '[Environment Not Found] An environment file cannot be located in "{0}" project.', |
|||
NoApiUrl = '[API URL Not Found] Cannot resolve API URL for "{1}" remote service name from "{0}" project.', |
|||
NoRootNamespace = '[Root Namespace Not Found] Cannot resolve root namespace for "{1}" api from "{0}" project.', |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
export enum eImportKeyword { |
|||
Default = 'import', |
|||
Type = 'import type', |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
export * from './binding-source-id'; |
|||
export * from './exception'; |
|||
export * from './import-keyword'; |
|||
export * from './method-modifier'; |
|||
@ -0,0 +1,6 @@ |
|||
export enum eMethodModifier { |
|||
Public = '', |
|||
Private = 'private ', |
|||
Async = 'async ', |
|||
PrivateAsync = 'private async ', |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export {}; |
|||
@ -0,0 +1,7 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
|
|||
@NgModule({ |
|||
imports: [CommonModule], |
|||
}) |
|||
export class SchematicsModule {} |
|||
File diff suppressed because it is too large
@ -0,0 +1,83 @@ |
|||
import { eBindingSourceId } from '../enums'; |
|||
|
|||
export interface ApiDefinition { |
|||
modules: Record<string, Module>; |
|||
types: Record<string, Type>; |
|||
} |
|||
|
|||
export interface Type { |
|||
baseType: string | null; |
|||
isEnum: boolean; |
|||
enumNames: string[] | null; |
|||
enumValues: number[] | null; |
|||
genericArguments: string[] | null; |
|||
properties: PropertyDef[] | null; |
|||
} |
|||
|
|||
export interface PropertyDef { |
|||
name: string; |
|||
jsonName: string | null; |
|||
type: string; |
|||
typeSimple: string; |
|||
isRequired: boolean; |
|||
} |
|||
|
|||
export interface Module { |
|||
rootPath: string; |
|||
remoteServiceName: string; |
|||
controllers: Record<string, Controller>; |
|||
} |
|||
|
|||
export interface Controller { |
|||
controllerName: string; |
|||
type: string; |
|||
interfaces: InterfaceDef[]; |
|||
actions: Record<string, Action>; |
|||
} |
|||
|
|||
export interface InterfaceDef { |
|||
type: string; |
|||
} |
|||
|
|||
export interface Action { |
|||
uniqueName: string; |
|||
name: string; |
|||
httpMethod: string; |
|||
url: string; |
|||
supportedVersions: string[]; |
|||
parametersOnMethod: ParameterInSignature[]; |
|||
parameters: ParameterInBody[]; |
|||
returnValue: TypeDef; |
|||
} |
|||
|
|||
export interface ParameterInSignature { |
|||
name: string; |
|||
typeAsString: string; |
|||
type: string; |
|||
typeSimple: string; |
|||
isOptional: boolean; |
|||
defaultValue: any; |
|||
} |
|||
|
|||
export interface ParameterInBody { |
|||
nameOnMethod: string; |
|||
name: string; |
|||
jsonName: string | null; |
|||
type: string; |
|||
typeSimple: string; |
|||
isOptional: boolean; |
|||
defaultValue: any; |
|||
constraintTypes: string[] | null; |
|||
bindingSourceId: eBindingSourceId; |
|||
descriptorName: string; |
|||
} |
|||
|
|||
export interface TypeDef { |
|||
type: string; |
|||
typeSimple: string; |
|||
} |
|||
|
|||
export interface TypeWithEnum { |
|||
isEnum: boolean; |
|||
type: string; |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
export interface GenerateProxySchema { |
|||
/** |
|||
* Backend module name |
|||
*/ |
|||
module?: string; |
|||
|
|||
/** |
|||
* Backend api name, a.k.a. remoteServiceName |
|||
*/ |
|||
['api-name']?: string; |
|||
|
|||
/** |
|||
* Source Angular project for API definition URL & root namespace resolution |
|||
*/ |
|||
source?: string; |
|||
|
|||
/** |
|||
* Target Angular project to place the generated code |
|||
*/ |
|||
target?: string; |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
import { eImportKeyword } from '../enums'; |
|||
import { Omissible } from './util'; |
|||
|
|||
export class Import { |
|||
alias?: string; |
|||
keyword = eImportKeyword.Default; |
|||
path: string; |
|||
refs: string[] = []; |
|||
specifiers: string[] = []; |
|||
|
|||
constructor(options: ImportOptions) { |
|||
Object.assign(this, options); |
|||
} |
|||
} |
|||
|
|||
export type ImportOptions = Omissible<Import, 'keyword' | 'refs' | 'specifiers'>; |
|||
@ -0,0 +1,10 @@ |
|||
export * from './api-definition'; |
|||
export * from './generate-proxy-schema'; |
|||
export * from './import'; |
|||
export * from './method'; |
|||
export * from './model'; |
|||
export * from './project'; |
|||
export * from './proxy-config'; |
|||
export * from './service'; |
|||
export * from './tree'; |
|||
export * from './util'; |
|||
@ -0,0 +1,84 @@ |
|||
import { eBindingSourceId, eMethodModifier } from '../enums'; |
|||
import { camel } from '../utils/text'; |
|||
import { ParameterInBody } from './api-definition'; |
|||
import { Property } from './model'; |
|||
import { Omissible } from './util'; |
|||
const shouldQuote = require('should-quote'); |
|||
|
|||
export class Method { |
|||
body: Body; |
|||
signature: Signature; |
|||
|
|||
constructor(options: MethodOptions) { |
|||
Object.assign(this, options); |
|||
} |
|||
} |
|||
|
|||
export type MethodOptions = Method; |
|||
|
|||
export class Signature { |
|||
generics = ''; |
|||
modifier = eMethodModifier.Public; |
|||
name: string; |
|||
parameters: Property[] = []; |
|||
returnType = ''; |
|||
|
|||
constructor(options: SignatureOptions) { |
|||
Object.assign(this, options); |
|||
} |
|||
} |
|||
|
|||
export type SignatureOptions = Omissible< |
|||
Signature, |
|||
'generics' | 'modifier' | 'parameters' | 'returnType' |
|||
>; |
|||
|
|||
export class Body { |
|||
body?: string; |
|||
method: string; |
|||
params: string[] = []; |
|||
requestType = 'any'; |
|||
responseType: string; |
|||
url: string; |
|||
|
|||
registerActionParameter = (param: ParameterInBody) => { |
|||
const { bindingSourceId, descriptorName, jsonName, name, nameOnMethod } = param; |
|||
const camelName = camel(name); |
|||
const paramName = jsonName || camelName; |
|||
const value = descriptorName |
|||
? shouldQuote(paramName) |
|||
? `${descriptorName}['${paramName}']` |
|||
: `${descriptorName}.${paramName}` |
|||
: nameOnMethod; |
|||
|
|||
switch (bindingSourceId) { |
|||
case eBindingSourceId.Model: |
|||
case eBindingSourceId.Query: |
|||
this.params.push(paramName === value ? value : `${paramName}: ${value}`); |
|||
break; |
|||
case eBindingSourceId.Body: |
|||
this.body = value; |
|||
break; |
|||
case eBindingSourceId.Path: |
|||
const regex = new RegExp('{(' + paramName + '|' + camelName + '|' + name + ')}', 'g'); |
|||
this.url = this.url.replace(regex, '${' + value + '}'); |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
}; |
|||
|
|||
constructor(options: BodyOptions) { |
|||
Object.assign(this, options); |
|||
this.setUrlQuotes(); |
|||
} |
|||
|
|||
private setUrlQuotes() { |
|||
this.url = /{/.test(this.url) ? `\`/${this.url}\`` : `'/${this.url}'`; |
|||
} |
|||
} |
|||
|
|||
export type BodyOptions = Omissible< |
|||
Omit<Body, 'registerActionParameter'>, |
|||
'params' | 'requestType' |
|||
>; |
|||
@ -0,0 +1,97 @@ |
|||
import { Import } from './import'; |
|||
import { Options } from './util'; |
|||
|
|||
export class Model { |
|||
readonly imports: Import[] = []; |
|||
readonly interfaces: Interface[] = []; |
|||
readonly namespace: string; |
|||
readonly path: string; |
|||
|
|||
constructor(options: ModelOptions) { |
|||
Object.assign(this, options); |
|||
} |
|||
} |
|||
|
|||
export type ModelOptions = Options<Model, 'imports' | 'interfaces'>; |
|||
|
|||
export class Interface { |
|||
readonly base: string | null; |
|||
readonly identifier: string; |
|||
readonly namespace: string; |
|||
readonly generics: Generic[] = []; |
|||
readonly properties: Property[] = []; |
|||
readonly ref: string; |
|||
|
|||
constructor(options: InterfaceOptions) { |
|||
Object.assign(this, options); |
|||
} |
|||
} |
|||
|
|||
export type InterfaceOptions = Options<Interface, 'generics' | 'properties'>; |
|||
|
|||
abstract class TypeRef { |
|||
readonly refs: string[] = []; |
|||
|
|||
protected _type = ''; |
|||
get type() { |
|||
return this._type; |
|||
} |
|||
set type(value: string) { |
|||
if (!value) return; |
|||
this._type = value; |
|||
} |
|||
|
|||
protected _default = ''; |
|||
get default() { |
|||
return this._default; |
|||
} |
|||
set default(value: string) { |
|||
if (!value) return; |
|||
this._default = ` = ${value}`; |
|||
} |
|||
|
|||
constructor(options: TypeRefOptions) { |
|||
Object.assign(this, options); |
|||
} |
|||
|
|||
setDefault(value: string) { |
|||
this.default = value; |
|||
} |
|||
|
|||
setType(value: string) { |
|||
this.type = value; |
|||
} |
|||
} |
|||
|
|||
type TypeRefOptionalKeys = 'default' | 'refs'; |
|||
type TypeRefOptions = Options<TypeRef, TypeRefOptionalKeys>; |
|||
|
|||
export class Generic extends TypeRef { |
|||
constructor(options: GenericOptions) { |
|||
super(options); |
|||
} |
|||
} |
|||
|
|||
export type GenericOptions = Options<Generic, TypeRefOptionalKeys>; |
|||
|
|||
export class Property extends TypeRef { |
|||
readonly name: string; |
|||
private _optional: '' | '?' = ''; |
|||
get optional() { |
|||
return this.default ? '' : this._optional; |
|||
} |
|||
|
|||
set optional(value: '' | '?') { |
|||
this._optional = value; |
|||
} |
|||
|
|||
constructor(options: PropertyOptions) { |
|||
super(options); |
|||
} |
|||
|
|||
setOptional(isOptional: boolean) { |
|||
this.optional = isOptional ? '?' : ''; |
|||
} |
|||
} |
|||
|
|||
export type PropertyOptions = Options<Property, TypeRefOptionalKeys | 'optional'>; |
|||
@ -0,0 +1,6 @@ |
|||
import { workspaces } from '@angular-devkit/core'; |
|||
|
|||
export interface Project { |
|||
name: string; |
|||
definition: workspaces.ProjectDefinition; |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
import { ApiDefinition } from './api-definition'; |
|||
|
|||
export interface ProxyConfig extends ApiDefinition { |
|||
generated: string[]; |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
import { Controller, Type } from './api-definition'; |
|||
import { Import } from './import'; |
|||
import { Method } from './method'; |
|||
import { Omissible } from './util'; |
|||
|
|||
export interface ServiceGeneratorParams { |
|||
targetPath: string; |
|||
solution: string; |
|||
types: Record<string, Type>; |
|||
apiName: string; |
|||
controllers: Controller[]; |
|||
serviceImports: Record<string, string[]>; |
|||
} |
|||
|
|||
export class Service { |
|||
apiName: string; |
|||
imports: Import[] = []; |
|||
methods: Method[] = []; |
|||
name: string; |
|||
namespace: string; |
|||
|
|||
constructor(options: ServiceOptions) { |
|||
Object.assign(this, options); |
|||
} |
|||
} |
|||
|
|||
export type ServiceOptions = Omissible<Service, 'imports' | 'methods'>; |
|||
@ -0,0 +1 @@ |
|||
export type WriteOp = 'create' | 'overwrite'; |
|||
@ -0,0 +1,16 @@ |
|||
// Omissible (given keys will become optional)
|
|||
export type Omissible<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>; |
|||
|
|||
// ExcludeKeys (keys will be excluded based on their type)
|
|||
type ExcludeKeys<Type, Excluded> = Exclude< |
|||
{ |
|||
[Key in keyof Type]: Type[Key] extends Excluded ? never : Key; |
|||
}[keyof Type], |
|||
never |
|||
>; |
|||
|
|||
// tslint:disable-next-line: ban-types
|
|||
type ExcludeMethods<Type> = Pick<Type, ExcludeKeys<Type, Function>>; |
|||
|
|||
// Options (methods will be omitted, given keys will become optional)
|
|||
export type Options<T, K extends keyof ExcludeMethods<T>> = Omissible<ExcludeMethods<T>, K>; |
|||
@ -0,0 +1 @@ |
|||
import 'jest-preset-angular/setup-jest'; |
|||
@ -0,0 +1,5 @@ |
|||
**DISCLAIMER** |
|||
|
|||
This directory is a direct copy of https://github.com/angular/angular-cli/tree/master/packages/schematics/angular/utility and is used under terms and permissions by the MIT license granted by Google, Inc. |
|||
|
|||
All credits go to Angular team for building these utilities. |
|||
@ -0,0 +1,753 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
import * as ts from 'typescript'; |
|||
import { Change, InsertChange, NoopChange } from './change'; |
|||
|
|||
|
|||
/** |
|||
* Add Import `import { symbolName } from fileName` if the import doesn't exit |
|||
* already. Assumes fileToEdit can be resolved and accessed. |
|||
* @param fileToEdit (file we want to add import to) |
|||
* @param symbolName (item to import) |
|||
* @param fileName (path to the file) |
|||
* @param isDefault (if true, import follows style for importing default exports) |
|||
* @return Change |
|||
*/ |
|||
export function insertImport(source: ts.SourceFile, fileToEdit: string, symbolName: string, |
|||
fileName: string, isDefault = false): Change { |
|||
const rootNode = source; |
|||
const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); |
|||
|
|||
// get nodes that map to import statements from the file fileName
|
|||
const relevantImports = allImports.filter(node => { |
|||
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
|
|||
const importFiles = node.getChildren() |
|||
.filter(ts.isStringLiteral) |
|||
.map(n => n.text); |
|||
|
|||
return importFiles.filter(file => file === fileName).length === 1; |
|||
}); |
|||
|
|||
if (relevantImports.length > 0) { |
|||
let importsAsterisk = false; |
|||
// imports from import file
|
|||
const imports: ts.Node[] = []; |
|||
relevantImports.forEach(n => { |
|||
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier)); |
|||
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { |
|||
importsAsterisk = true; |
|||
} |
|||
}); |
|||
|
|||
// if imports * from fileName, don't add symbolName
|
|||
if (importsAsterisk) { |
|||
return new NoopChange(); |
|||
} |
|||
|
|||
const importTextNodes = imports.filter(n => (n as ts.Identifier).text === symbolName); |
|||
|
|||
// insert import if it's not there
|
|||
if (importTextNodes.length === 0) { |
|||
const fallbackPos = |
|||
findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].getStart() || |
|||
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart(); |
|||
|
|||
return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos); |
|||
} |
|||
|
|||
return new NoopChange(); |
|||
} |
|||
|
|||
// no such import declaration exists
|
|||
const useStrict = findNodes(rootNode, ts.isStringLiteral) |
|||
.filter((n) => n.text === 'use strict'); |
|||
let fallbackPos = 0; |
|||
if (useStrict.length > 0) { |
|||
fallbackPos = useStrict[0].end; |
|||
} |
|||
const open = isDefault ? '' : '{ '; |
|||
const close = isDefault ? '' : ' }'; |
|||
// if there are no imports or 'use strict' statement, insert import at beginning of file
|
|||
const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; |
|||
const separator = insertAtBeginning ? '' : ';\n'; |
|||
const toInsert = `${separator}import ${open}${symbolName}${close}` + |
|||
` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; |
|||
|
|||
return insertAfterLastOccurrence( |
|||
allImports, |
|||
toInsert, |
|||
fileToEdit, |
|||
fallbackPos, |
|||
ts.SyntaxKind.StringLiteral, |
|||
); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Find all nodes from the AST in the subtree of node of SyntaxKind kind. |
|||
* @param node |
|||
* @param kind |
|||
* @param max The maximum number of items to return. |
|||
* @param recursive Continue looking for nodes of kind recursive until end |
|||
* the last child even when node of kind has been found. |
|||
* @return all nodes of kind, or [] if none is found |
|||
*/ |
|||
export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max?: number, recursive?: boolean): ts.Node[]; |
|||
|
|||
/** |
|||
* Find all nodes from the AST in the subtree that satisfy a type guard. |
|||
* @param node |
|||
* @param guard |
|||
* @param max The maximum number of items to return. |
|||
* @param recursive Continue looking for nodes of kind recursive until end |
|||
* the last child even when node of kind has been found. |
|||
* @return all nodes that satisfy the type guard, or [] if none is found |
|||
*/ |
|||
export function findNodes<T extends ts.Node>(node: ts.Node, guard: (node: ts.Node) => node is T, max?: number, recursive?: boolean): T[]; |
|||
|
|||
export function findNodes<T extends ts.Node>( |
|||
node: ts.Node, |
|||
kindOrGuard: ts.SyntaxKind | ((node: ts.Node) => node is T), |
|||
max = Infinity, |
|||
recursive = false, |
|||
): T[] { |
|||
if (!node || max == 0) { |
|||
return []; |
|||
} |
|||
|
|||
const test = |
|||
typeof kindOrGuard === 'function' |
|||
? kindOrGuard |
|||
: (node: ts.Node): node is T => node.kind === kindOrGuard; |
|||
|
|||
const arr: T[] = []; |
|||
if (test(node)) { |
|||
arr.push(node); |
|||
max--; |
|||
} |
|||
if (max > 0 && (recursive || !test(node))) { |
|||
for (const child of node.getChildren()) { |
|||
findNodes(child, test, max).forEach((node) => { |
|||
if (max > 0) { |
|||
arr.push(node); |
|||
} |
|||
max--; |
|||
}); |
|||
|
|||
if (max <= 0) { |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return arr; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Get all the nodes from a source. |
|||
* @param sourceFile The source file object. |
|||
* @returns {Array<ts.Node>} An array of all the nodes in the source. |
|||
*/ |
|||
export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] { |
|||
const nodes: ts.Node[] = [sourceFile]; |
|||
const result = []; |
|||
|
|||
while (nodes.length > 0) { |
|||
const node = nodes.shift(); |
|||
|
|||
if (node) { |
|||
result.push(node); |
|||
if (node.getChildCount(sourceFile) >= 0) { |
|||
nodes.unshift(...node.getChildren()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
export function findNode(node: ts.Node, kind: ts.SyntaxKind, text: string): ts.Node | null { |
|||
if (node.kind === kind && node.getText() === text) { |
|||
// throw new Error(node.getText());
|
|||
return node; |
|||
} |
|||
|
|||
let foundNode: ts.Node | null = null; |
|||
ts.forEachChild(node, childNode => { |
|||
foundNode = foundNode || findNode(childNode, kind, text); |
|||
}); |
|||
|
|||
return foundNode; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Helper for sorting nodes. |
|||
* @return function to sort nodes in increasing order of position in sourceFile |
|||
*/ |
|||
function nodesByPosition(first: ts.Node, second: ts.Node): number { |
|||
return first.getStart() - second.getStart(); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` |
|||
* or after the last of occurence of `syntaxKind` if the last occurence is a sub child |
|||
* of ts.SyntaxKind[nodes[i].kind] and save the changes in file. |
|||
* |
|||
* @param nodes insert after the last occurence of nodes |
|||
* @param toInsert string to insert |
|||
* @param file file to insert changes into |
|||
* @param fallbackPos position to insert if toInsert happens to be the first occurence |
|||
* @param syntaxKind the ts.SyntaxKind of the subchildren to insert after |
|||
* @return Change instance |
|||
* @throw Error if toInsert is first occurence but fall back is not set |
|||
*/ |
|||
export function insertAfterLastOccurrence(nodes: ts.Node[], |
|||
toInsert: string, |
|||
file: string, |
|||
fallbackPos: number, |
|||
syntaxKind?: ts.SyntaxKind): Change { |
|||
let lastItem: ts.Node | undefined; |
|||
for (const node of nodes) { |
|||
if (!lastItem || lastItem.getStart() < node.getStart()) { |
|||
lastItem = node; |
|||
} |
|||
} |
|||
if (syntaxKind && lastItem) { |
|||
lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop(); |
|||
} |
|||
if (!lastItem && fallbackPos == undefined) { |
|||
throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`); |
|||
} |
|||
const lastItemPosition: number = lastItem ? lastItem.getEnd() : fallbackPos; |
|||
|
|||
return new InsertChange(file, lastItemPosition, toInsert); |
|||
} |
|||
|
|||
|
|||
export function getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string | null { |
|||
if (node.kind == ts.SyntaxKind.Identifier) { |
|||
return (node as ts.Identifier).text; |
|||
} else if (node.kind == ts.SyntaxKind.StringLiteral) { |
|||
return (node as ts.StringLiteral).text; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
|
|||
function _angularImportsFromNode(node: ts.ImportDeclaration, |
|||
_sourceFile: ts.SourceFile): {[name: string]: string} { |
|||
const ms = node.moduleSpecifier; |
|||
let modulePath: string; |
|||
switch (ms.kind) { |
|||
case ts.SyntaxKind.StringLiteral: |
|||
modulePath = (ms as ts.StringLiteral).text; |
|||
break; |
|||
default: |
|||
return {}; |
|||
} |
|||
|
|||
if (!modulePath.startsWith('@angular/')) { |
|||
return {}; |
|||
} |
|||
|
|||
if (node.importClause) { |
|||
if (node.importClause.name) { |
|||
// This is of the form `import Name from 'path'`. Ignore.
|
|||
return {}; |
|||
} else if (node.importClause.namedBindings) { |
|||
const nb = node.importClause.namedBindings; |
|||
if (nb.kind == ts.SyntaxKind.NamespaceImport) { |
|||
// This is of the form `import * as name from 'path'`. Return `name.`.
|
|||
return { |
|||
[(nb as ts.NamespaceImport).name.text + '.']: modulePath, |
|||
}; |
|||
} else { |
|||
// This is of the form `import {a,b,c} from 'path'`
|
|||
const namedImports = nb as ts.NamedImports; |
|||
|
|||
return namedImports.elements |
|||
.map((is: ts.ImportSpecifier) => is.propertyName ? is.propertyName.text : is.name.text) |
|||
.reduce((acc: {[name: string]: string}, curr: string) => { |
|||
acc[curr] = modulePath; |
|||
|
|||
return acc; |
|||
}, {}); |
|||
} |
|||
} |
|||
|
|||
return {}; |
|||
} else { |
|||
// This is of the form `import 'path';`. Nothing to do.
|
|||
return {}; |
|||
} |
|||
} |
|||
|
|||
|
|||
export function getDecoratorMetadata(source: ts.SourceFile, identifier: string, |
|||
module: string): ts.Node[] { |
|||
const angularImports = findNodes(source, ts.isImportDeclaration) |
|||
.map((node) => _angularImportsFromNode(node, source)) |
|||
.reduce((acc, current) => { |
|||
for (const key of Object.keys(current)) { |
|||
acc[key] = current[key]; |
|||
} |
|||
|
|||
return acc; |
|||
}, {}); |
|||
|
|||
return getSourceNodes(source) |
|||
.filter(node => { |
|||
return node.kind == ts.SyntaxKind.Decorator |
|||
&& (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression; |
|||
}) |
|||
.map(node => (node as ts.Decorator).expression as ts.CallExpression) |
|||
.filter(expr => { |
|||
if (expr.expression.kind == ts.SyntaxKind.Identifier) { |
|||
const id = expr.expression as ts.Identifier; |
|||
|
|||
return id.text == identifier && angularImports[id.text] === module; |
|||
} else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) { |
|||
// This covers foo.NgModule when importing * as foo.
|
|||
const paExpr = expr.expression as ts.PropertyAccessExpression; |
|||
// If the left expression is not an identifier, just give up at that point.
|
|||
if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { |
|||
return false; |
|||
} |
|||
|
|||
const id = paExpr.name.text; |
|||
const moduleId = (paExpr.expression as ts.Identifier).text; |
|||
|
|||
return id === identifier && (angularImports[moduleId + '.'] === module); |
|||
} |
|||
|
|||
return false; |
|||
}) |
|||
.filter(expr => expr.arguments[0] |
|||
&& expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression) |
|||
.map(expr => expr.arguments[0] as ts.ObjectLiteralExpression); |
|||
} |
|||
|
|||
function findClassDeclarationParent(node: ts.Node): ts.ClassDeclaration|undefined { |
|||
if (ts.isClassDeclaration(node)) { |
|||
return node; |
|||
} |
|||
|
|||
return node.parent && findClassDeclarationParent(node.parent); |
|||
} |
|||
|
|||
/** |
|||
* Given a source file with @NgModule class(es), find the name of the first @NgModule class. |
|||
* |
|||
* @param source source file containing one or more @NgModule |
|||
* @returns the name of the first @NgModule, or `undefined` if none is found |
|||
*/ |
|||
export function getFirstNgModuleName(source: ts.SourceFile): string|undefined { |
|||
// First, find the @NgModule decorators.
|
|||
const ngModulesMetadata = getDecoratorMetadata(source, 'NgModule', '@angular/core'); |
|||
if (ngModulesMetadata.length === 0) { |
|||
return undefined; |
|||
} |
|||
|
|||
// Then walk parent pointers up the AST, looking for the ClassDeclaration parent of the NgModule
|
|||
// metadata.
|
|||
const moduleClass = findClassDeclarationParent(ngModulesMetadata[0]); |
|||
if (!moduleClass || !moduleClass.name) { |
|||
return undefined; |
|||
} |
|||
|
|||
// Get the class name of the module ClassDeclaration.
|
|||
return moduleClass.name.text; |
|||
} |
|||
|
|||
export function getMetadataField( |
|||
node: ts.ObjectLiteralExpression, |
|||
metadataField: string, |
|||
): ts.ObjectLiteralElement[] { |
|||
return node.properties |
|||
.filter(ts.isPropertyAssignment) |
|||
// Filter out every fields that's not "metadataField". Also handles string literals
|
|||
// (but not expressions).
|
|||
.filter(({ name }) => { |
|||
return (ts.isIdentifier(name) || ts.isStringLiteral(name)) |
|||
&& name.getText() === metadataField; |
|||
}); |
|||
} |
|||
|
|||
export function addSymbolToNgModuleMetadata( |
|||
source: ts.SourceFile, |
|||
ngModulePath: string, |
|||
metadataField: string, |
|||
symbolName: string, |
|||
importPath: string | null = null, |
|||
): Change[] { |
|||
const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core'); |
|||
let node: any = nodes[0]; // tslint:disable-line:no-any
|
|||
|
|||
// Find the decorator declaration.
|
|||
if (!node) { |
|||
return []; |
|||
} |
|||
|
|||
// Get all the children property assignment of object literals.
|
|||
const matchingProperties = getMetadataField( |
|||
node as ts.ObjectLiteralExpression, |
|||
metadataField, |
|||
); |
|||
|
|||
// Get the last node of the array literal.
|
|||
if (!matchingProperties) { |
|||
return []; |
|||
} |
|||
if (matchingProperties.length == 0) { |
|||
// We haven't found the field in the metadata declaration. Insert a new field.
|
|||
const expr = node as ts.ObjectLiteralExpression; |
|||
let position: number; |
|||
let toInsert: string; |
|||
if (expr.properties.length == 0) { |
|||
position = expr.getEnd() - 1; |
|||
toInsert = ` ${metadataField}: [${symbolName}]\n`; |
|||
} else { |
|||
node = expr.properties[expr.properties.length - 1]; |
|||
position = node.getEnd(); |
|||
// Get the indentation of the last element, if any.
|
|||
const text = node.getFullText(source); |
|||
const matches = text.match(/^\r?\n\s*/); |
|||
if (matches && matches.length > 0) { |
|||
toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`; |
|||
} else { |
|||
toInsert = `, ${metadataField}: [${symbolName}]`; |
|||
} |
|||
} |
|||
if (importPath !== null) { |
|||
return [ |
|||
new InsertChange(ngModulePath, position, toInsert), |
|||
insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath), |
|||
]; |
|||
} else { |
|||
return [new InsertChange(ngModulePath, position, toInsert)]; |
|||
} |
|||
} |
|||
const assignment = matchingProperties[0] as ts.PropertyAssignment; |
|||
|
|||
// If it's not an array, nothing we can do really.
|
|||
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { |
|||
return []; |
|||
} |
|||
|
|||
const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; |
|||
if (arrLiteral.elements.length == 0) { |
|||
// Forward the property.
|
|||
node = arrLiteral; |
|||
} else { |
|||
node = arrLiteral.elements; |
|||
} |
|||
|
|||
if (!node) { |
|||
// tslint:disable-next-line: no-console
|
|||
console.error('No app module found. Please add your new class to your component.'); |
|||
|
|||
return []; |
|||
} |
|||
|
|||
if (Array.isArray(node)) { |
|||
const nodeArray = node as {} as Array<ts.Node>; |
|||
const symbolsArray = nodeArray.map(node => node.getText()); |
|||
if (symbolsArray.includes(symbolName)) { |
|||
return []; |
|||
} |
|||
|
|||
node = node[node.length - 1]; |
|||
} |
|||
|
|||
let toInsert: string; |
|||
let position = node.getEnd(); |
|||
if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) { |
|||
// We haven't found the field in the metadata declaration. Insert a new
|
|||
// field.
|
|||
const expr = node as ts.ObjectLiteralExpression; |
|||
if (expr.properties.length == 0) { |
|||
position = expr.getEnd() - 1; |
|||
toInsert = ` ${symbolName}\n`; |
|||
} else { |
|||
// Get the indentation of the last element, if any.
|
|||
const text = node.getFullText(source); |
|||
if (text.match(/^\r?\r?\n/)) { |
|||
toInsert = `,${text.match(/^\r?\n\s*/)[0]}${symbolName}`; |
|||
} else { |
|||
toInsert = `, ${symbolName}`; |
|||
} |
|||
} |
|||
} else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) { |
|||
// We found the field but it's empty. Insert it just before the `]`.
|
|||
position--; |
|||
toInsert = `${symbolName}`; |
|||
} else { |
|||
// Get the indentation of the last element, if any.
|
|||
const text = node.getFullText(source); |
|||
if (text.match(/^\r?\n/)) { |
|||
toInsert = `,${text.match(/^\r?\n(\r?)\s*/)[0]}${symbolName}`; |
|||
} else { |
|||
toInsert = `, ${symbolName}`; |
|||
} |
|||
} |
|||
if (importPath !== null) { |
|||
return [ |
|||
new InsertChange(ngModulePath, position, toInsert), |
|||
insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath), |
|||
]; |
|||
} |
|||
|
|||
return [new InsertChange(ngModulePath, position, toInsert)]; |
|||
} |
|||
|
|||
/** |
|||
* Custom function to insert a declaration (component, pipe, directive) |
|||
* into NgModule declarations. It also imports the component. |
|||
*/ |
|||
export function addDeclarationToModule(source: ts.SourceFile, |
|||
modulePath: string, classifiedName: string, |
|||
importPath: string): Change[] { |
|||
return addSymbolToNgModuleMetadata( |
|||
source, modulePath, 'declarations', classifiedName, importPath); |
|||
} |
|||
|
|||
/** |
|||
* Custom function to insert an NgModule into NgModule imports. It also imports the module. |
|||
*/ |
|||
export function addImportToModule(source: ts.SourceFile, |
|||
modulePath: string, classifiedName: string, |
|||
importPath: string): Change[] { |
|||
|
|||
return addSymbolToNgModuleMetadata(source, modulePath, 'imports', classifiedName, importPath); |
|||
} |
|||
|
|||
/** |
|||
* Custom function to insert a provider into NgModule. It also imports it. |
|||
*/ |
|||
export function addProviderToModule(source: ts.SourceFile, |
|||
modulePath: string, classifiedName: string, |
|||
importPath: string): Change[] { |
|||
return addSymbolToNgModuleMetadata(source, modulePath, 'providers', classifiedName, importPath); |
|||
} |
|||
|
|||
/** |
|||
* Custom function to insert an export into NgModule. It also imports it. |
|||
*/ |
|||
export function addExportToModule(source: ts.SourceFile, |
|||
modulePath: string, classifiedName: string, |
|||
importPath: string): Change[] { |
|||
return addSymbolToNgModuleMetadata(source, modulePath, 'exports', classifiedName, importPath); |
|||
} |
|||
|
|||
/** |
|||
* Custom function to insert an export into NgModule. It also imports it. |
|||
*/ |
|||
export function addBootstrapToModule(source: ts.SourceFile, |
|||
modulePath: string, classifiedName: string, |
|||
importPath: string): Change[] { |
|||
return addSymbolToNgModuleMetadata(source, modulePath, 'bootstrap', classifiedName, importPath); |
|||
} |
|||
|
|||
/** |
|||
* Custom function to insert an entryComponent into NgModule. It also imports it. |
|||
* @deprecated - Since version 9.0.0 with Ivy, entryComponents is no longer necessary. |
|||
*/ |
|||
export function addEntryComponentToModule(source: ts.SourceFile, |
|||
modulePath: string, classifiedName: string, |
|||
importPath: string): Change[] { |
|||
return addSymbolToNgModuleMetadata( |
|||
source, modulePath, |
|||
'entryComponents', classifiedName, importPath, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Determine if an import already exists. |
|||
*/ |
|||
export function isImported(source: ts.SourceFile, |
|||
classifiedName: string, |
|||
importPath: string): boolean { |
|||
const allNodes = getSourceNodes(source); |
|||
const matchingNodes = allNodes |
|||
.filter(ts.isImportDeclaration) |
|||
.filter( |
|||
(imp) => ts.isStringLiteral(imp.moduleSpecifier) && imp.moduleSpecifier.text === importPath, |
|||
) |
|||
.filter((imp) => { |
|||
if (!imp.importClause) { |
|||
return false; |
|||
} |
|||
const nodes = findNodes(imp.importClause, ts.isImportSpecifier).filter( |
|||
(n) => n.getText() === classifiedName, |
|||
); |
|||
|
|||
return nodes.length > 0; |
|||
}); |
|||
|
|||
return matchingNodes.length > 0; |
|||
} |
|||
|
|||
/** |
|||
* This function returns the name of the environment export |
|||
* whether this export is aliased or not. If the environment file |
|||
* is not imported, then it will return `null`. |
|||
*/ |
|||
export function getEnvironmentExportName(source: ts.SourceFile): string | null { |
|||
// Initial value is `null` as we don't know yet if the user
|
|||
// has imported `environment` into the root module or not.
|
|||
let environmentExportName: string | null = null; |
|||
|
|||
const allNodes = getSourceNodes(source); |
|||
|
|||
allNodes |
|||
.filter(ts.isImportDeclaration) |
|||
.filter( |
|||
(declaration) => |
|||
declaration.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral && |
|||
declaration.importClause !== undefined, |
|||
) |
|||
.map((declaration) => |
|||
// If `importClause` property is defined then the first
|
|||
// child will be `NamedImports` object (or `namedBindings`).
|
|||
(declaration.importClause as ts.ImportClause).getChildAt(0), |
|||
) |
|||
// Find those `NamedImports` object that contains `environment` keyword
|
|||
// in its text. E.g. `{ environment as env }`.
|
|||
.filter(ts.isNamedImports) |
|||
.filter((namedImports) => namedImports.getText().includes('environment')) |
|||
.forEach((namedImports) => { |
|||
for (const specifier of namedImports.elements) { |
|||
// `propertyName` is defined if the specifier
|
|||
// has an aliased import.
|
|||
const name = specifier.propertyName || specifier.name; |
|||
|
|||
// Find specifier that contains `environment` keyword in its text.
|
|||
// Whether it's `environment` or `environment as env`.
|
|||
if (name.text.includes('environment')) { |
|||
environmentExportName = specifier.name.text; |
|||
} |
|||
} |
|||
}); |
|||
|
|||
return environmentExportName; |
|||
} |
|||
|
|||
/** |
|||
* Returns the RouterModule declaration from NgModule metadata, if any. |
|||
*/ |
|||
export function getRouterModuleDeclaration(source: ts.SourceFile): ts.Expression | undefined { |
|||
const result = getDecoratorMetadata(source, 'NgModule', '@angular/core') as ts.Node[]; |
|||
const node = result[0] as ts.ObjectLiteralExpression; |
|||
const matchingProperties = getMetadataField(node, 'imports'); |
|||
|
|||
if (!matchingProperties) { |
|||
return; |
|||
} |
|||
|
|||
const assignment = matchingProperties[0] as ts.PropertyAssignment; |
|||
|
|||
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { |
|||
return; |
|||
} |
|||
|
|||
const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; |
|||
|
|||
return arrLiteral.elements |
|||
.filter(el => el.kind === ts.SyntaxKind.CallExpression) |
|||
.find(el => (el as ts.Identifier).getText().startsWith('RouterModule')); |
|||
} |
|||
|
|||
/** |
|||
* Adds a new route declaration to a router module (i.e. has a RouterModule declaration) |
|||
*/ |
|||
export function addRouteDeclarationToModule( |
|||
source: ts.SourceFile, |
|||
fileToAdd: string, |
|||
routeLiteral: string, |
|||
): Change { |
|||
const routerModuleExpr = getRouterModuleDeclaration(source); |
|||
if (!routerModuleExpr) { |
|||
throw new Error(`Couldn't find a route declaration in ${fileToAdd}.`); |
|||
} |
|||
const scopeConfigMethodArgs = (routerModuleExpr as ts.CallExpression).arguments; |
|||
if (!scopeConfigMethodArgs.length) { |
|||
const { line } = source.getLineAndCharacterOfPosition(routerModuleExpr.getStart()); |
|||
throw new Error( |
|||
`The router module method doesn't have arguments ` + |
|||
`at line ${line} in ${fileToAdd}`, |
|||
); |
|||
} |
|||
|
|||
let routesArr: ts.ArrayLiteralExpression | undefined; |
|||
const routesArg = scopeConfigMethodArgs[0]; |
|||
|
|||
// Check if the route declarations array is
|
|||
// an inlined argument of RouterModule or a standalone variable
|
|||
if (ts.isArrayLiteralExpression(routesArg)) { |
|||
routesArr = routesArg; |
|||
} else { |
|||
const routesVarName = routesArg.getText(); |
|||
let routesVar; |
|||
if (routesArg.kind === ts.SyntaxKind.Identifier) { |
|||
routesVar = source.statements |
|||
.filter(ts.isVariableStatement) |
|||
.find((v) => { |
|||
return v.declarationList.declarations[0].name.getText() === routesVarName; |
|||
}); |
|||
} |
|||
|
|||
if (!routesVar) { |
|||
const { line } = source.getLineAndCharacterOfPosition(routesArg.getStart()); |
|||
throw new Error( |
|||
`No route declaration array was found that corresponds ` + |
|||
`to router module at line ${line} in ${fileToAdd}`, |
|||
); |
|||
} |
|||
|
|||
routesArr = findNodes(routesVar, ts.SyntaxKind.ArrayLiteralExpression, 1)[0] as ts.ArrayLiteralExpression; |
|||
} |
|||
|
|||
const occurrencesCount = routesArr.elements.length; |
|||
const text = routesArr.getFullText(source); |
|||
|
|||
let route: string = routeLiteral; |
|||
let insertPos = routesArr.elements.pos; |
|||
|
|||
if (occurrencesCount > 0) { |
|||
const lastRouteLiteral = [...routesArr.elements].pop() as ts.Expression; |
|||
const lastRouteIsWildcard = ts.isObjectLiteralExpression(lastRouteLiteral) |
|||
&& lastRouteLiteral |
|||
.properties |
|||
.some(n => ( |
|||
ts.isPropertyAssignment(n) |
|||
&& ts.isIdentifier(n.name) |
|||
&& n.name.text === 'path' |
|||
&& ts.isStringLiteral(n.initializer) |
|||
&& n.initializer.text === '**' |
|||
)); |
|||
|
|||
const indentation = text.match(/\r?\n(\r?)\s*/) || []; |
|||
const routeText = `${indentation[0] || ' '}${routeLiteral}`; |
|||
|
|||
// Add the new route before the wildcard route
|
|||
// otherwise we'll always redirect to the wildcard route
|
|||
if (lastRouteIsWildcard) { |
|||
insertPos = lastRouteLiteral.pos; |
|||
route = `${routeText},`; |
|||
} else { |
|||
insertPos = lastRouteLiteral.end; |
|||
route = `,${routeText}`; |
|||
} |
|||
} |
|||
|
|||
return new InsertChange(fileToAdd, insertPos, route); |
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
export interface Host { |
|||
write(path: string, content: string): Promise<void>; |
|||
read(path: string): Promise<string>; |
|||
} |
|||
|
|||
|
|||
export interface Change { |
|||
apply(host: Host): Promise<void>; |
|||
|
|||
// The file this change should be applied to. Some changes might not apply to
|
|||
// a file (maybe the config).
|
|||
readonly path: string | null; |
|||
|
|||
// The order this change should be applied. Normally the position inside the file.
|
|||
// Changes are applied from the bottom of a file to the top.
|
|||
readonly order: number; |
|||
|
|||
// The description of this change. This will be outputted in a dry or verbose run.
|
|||
readonly description: string; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* An operation that does nothing. |
|||
*/ |
|||
export class NoopChange implements Change { |
|||
description = 'No operation.'; |
|||
order = Infinity; |
|||
path = null; |
|||
apply() { return Promise.resolve(); } |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Will add text to the source code. |
|||
*/ |
|||
export class InsertChange implements Change { |
|||
|
|||
order: number; |
|||
description: string; |
|||
|
|||
constructor(public path: string, public pos: number, public toAdd: string) { |
|||
if (pos < 0) { |
|||
throw new Error('Negative positions are invalid'); |
|||
} |
|||
this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; |
|||
this.order = pos; |
|||
} |
|||
|
|||
/** |
|||
* This method does not insert spaces if there is none in the original string. |
|||
*/ |
|||
apply(host: Host) { |
|||
return host.read(this.path).then(content => { |
|||
const prefix = content.substring(0, this.pos); |
|||
const suffix = content.substring(this.pos); |
|||
|
|||
return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Will remove text from the source code. |
|||
*/ |
|||
export class RemoveChange implements Change { |
|||
|
|||
order: number; |
|||
description: string; |
|||
|
|||
constructor(public path: string, private pos: number, private toRemove: string) { |
|||
if (pos < 0) { |
|||
throw new Error('Negative positions are invalid'); |
|||
} |
|||
this.description = `Removed ${toRemove} into position ${pos} of ${path}`; |
|||
this.order = pos; |
|||
} |
|||
|
|||
apply(host: Host): Promise<void> { |
|||
return host.read(this.path).then(content => { |
|||
const prefix = content.substring(0, this.pos); |
|||
const suffix = content.substring(this.pos + this.toRemove.length); |
|||
|
|||
// TODO: throw error if toRemove doesn't match removed string.
|
|||
return host.write(this.path, `${prefix}${suffix}`); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Will replace text from the source code. |
|||
*/ |
|||
export class ReplaceChange implements Change { |
|||
order: number; |
|||
description: string; |
|||
|
|||
constructor(public path: string, private pos: number, private oldText: string, |
|||
private newText: string) { |
|||
if (pos < 0) { |
|||
throw new Error('Negative positions are invalid'); |
|||
} |
|||
this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`; |
|||
this.order = pos; |
|||
} |
|||
|
|||
apply(host: Host): Promise<void> { |
|||
return host.read(this.path).then(content => { |
|||
const prefix = content.substring(0, this.pos); |
|||
const suffix = content.substring(this.pos + this.oldText.length); |
|||
const text = content.substring(this.pos, this.pos + this.oldText.length); |
|||
|
|||
if (text !== this.oldText) { |
|||
return Promise.reject(new Error(`Invalid replace: "${text}" != "${this.oldText}".`)); |
|||
} |
|||
|
|||
// TODO: throw error if oldText doesn't match removed string.
|
|||
return host.write(this.path, `${prefix}${this.newText}${suffix}`); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,532 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
import { JsonParseMode, parseJson } from '@angular-devkit/core'; |
|||
import { Rule, SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics'; |
|||
import { ProjectType, WorkspaceProject, WorkspaceSchema } from './workspace-models'; |
|||
|
|||
// The interfaces below are generated from the Angular CLI configuration schema
|
|||
// https://github.com/angular/angular-cli/blob/master/packages/@angular/cli/lib/config/schema.json
|
|||
export interface AppConfig { |
|||
/** |
|||
* Name of the app. |
|||
*/ |
|||
name?: string; |
|||
/** |
|||
* Directory where app files are placed. |
|||
*/ |
|||
appRoot?: string; |
|||
/** |
|||
* The root directory of the app. |
|||
*/ |
|||
root?: string; |
|||
/** |
|||
* The output directory for build results. |
|||
*/ |
|||
outDir?: string; |
|||
/** |
|||
* List of application assets. |
|||
*/ |
|||
assets?: (string | { |
|||
/** |
|||
* The pattern to match. |
|||
*/ |
|||
glob?: string; |
|||
/** |
|||
* The dir to search within. |
|||
*/ |
|||
input?: string; |
|||
/** |
|||
* The output path (relative to the outDir). |
|||
*/ |
|||
output?: string; |
|||
})[]; |
|||
/** |
|||
* URL where files will be deployed. |
|||
*/ |
|||
deployUrl?: string; |
|||
/** |
|||
* Base url for the application being built. |
|||
*/ |
|||
baseHref?: string; |
|||
/** |
|||
* The runtime platform of the app. |
|||
*/ |
|||
platform?: ('browser' | 'server'); |
|||
/** |
|||
* The name of the start HTML file. |
|||
*/ |
|||
index?: string; |
|||
/** |
|||
* The name of the main entry-point file. |
|||
*/ |
|||
main?: string; |
|||
/** |
|||
* The name of the polyfills file. |
|||
*/ |
|||
polyfills?: string; |
|||
/** |
|||
* The name of the test entry-point file. |
|||
*/ |
|||
test?: string; |
|||
/** |
|||
* The name of the TypeScript configuration file. |
|||
*/ |
|||
tsconfig?: string; |
|||
/** |
|||
* The name of the TypeScript configuration file for unit tests. |
|||
*/ |
|||
testTsconfig?: string; |
|||
/** |
|||
* The prefix to apply to generated selectors. |
|||
*/ |
|||
prefix?: string; |
|||
/** |
|||
* Experimental support for a service worker from @angular/service-worker. |
|||
*/ |
|||
serviceWorker?: boolean; |
|||
/** |
|||
* Global styles to be included in the build. |
|||
*/ |
|||
styles?: (string | { |
|||
input?: string; |
|||
[name: string]: any; // tslint:disable-line:no-any
|
|||
})[]; |
|||
/** |
|||
* Options to pass to style preprocessors |
|||
*/ |
|||
stylePreprocessorOptions?: { |
|||
/** |
|||
* Paths to include. Paths will be resolved to project root. |
|||
*/ |
|||
includePaths?: string[]; |
|||
}; |
|||
/** |
|||
* Global scripts to be included in the build. |
|||
*/ |
|||
scripts?: (string | { |
|||
input: string; |
|||
[name: string]: any; // tslint:disable-line:no-any
|
|||
})[]; |
|||
/** |
|||
* Source file for environment config. |
|||
*/ |
|||
environmentSource?: string; |
|||
/** |
|||
* Name and corresponding file for environment config. |
|||
*/ |
|||
environments?: { |
|||
[name: string]: any; // tslint:disable-line:no-any
|
|||
}; |
|||
appShell?: { |
|||
app: string; |
|||
route: string; |
|||
}; |
|||
budgets?: { |
|||
/** |
|||
* The type of budget |
|||
*/ |
|||
type?: ('bundle' | 'initial' | 'allScript' | 'all' | 'anyScript' | 'any' | 'anyComponentStyle'); |
|||
/** |
|||
* The name of the bundle |
|||
*/ |
|||
name?: string; |
|||
/** |
|||
* The baseline size for comparison. |
|||
*/ |
|||
baseline?: string; |
|||
/** |
|||
* The maximum threshold for warning relative to the baseline. |
|||
*/ |
|||
maximumWarning?: string; |
|||
/** |
|||
* The maximum threshold for error relative to the baseline. |
|||
*/ |
|||
maximumError?: string; |
|||
/** |
|||
* The minimum threshold for warning relative to the baseline. |
|||
*/ |
|||
minimumWarning?: string; |
|||
/** |
|||
* The minimum threshold for error relative to the baseline. |
|||
*/ |
|||
minimumError?: string; |
|||
/** |
|||
* The threshold for warning relative to the baseline (min & max). |
|||
*/ |
|||
warning?: string; |
|||
/** |
|||
* The threshold for error relative to the baseline (min & max). |
|||
*/ |
|||
error?: string; |
|||
}[]; |
|||
} |
|||
|
|||
export interface CliConfig { |
|||
$schema?: string; |
|||
/** |
|||
* The global configuration of the project. |
|||
*/ |
|||
project?: { |
|||
/** |
|||
* The name of the project. |
|||
*/ |
|||
name?: string; |
|||
/** |
|||
* Whether or not this project was ejected. |
|||
*/ |
|||
ejected?: boolean; |
|||
}; |
|||
/** |
|||
* Properties of the different applications in this project. |
|||
*/ |
|||
apps?: AppConfig[]; |
|||
/** |
|||
* Configuration for end-to-end tests. |
|||
*/ |
|||
e2e?: { |
|||
protractor?: { |
|||
/** |
|||
* Path to the config file. |
|||
*/ |
|||
config?: string; |
|||
}; |
|||
}; |
|||
/** |
|||
* Properties to be passed to TSLint. |
|||
*/ |
|||
lint?: { |
|||
/** |
|||
* File glob(s) to lint. |
|||
*/ |
|||
files?: (string | string[]); |
|||
/** |
|||
* Location of the tsconfig.json project file. |
|||
* Will also use as files to lint if 'files' property not present. |
|||
*/ |
|||
project: string; |
|||
/** |
|||
* Location of the tslint.json configuration. |
|||
*/ |
|||
tslintConfig?: string; |
|||
/** |
|||
* File glob(s) to ignore. |
|||
*/ |
|||
exclude?: (string | string[]); |
|||
}[]; |
|||
/** |
|||
* Configuration for unit tests. |
|||
*/ |
|||
test?: { |
|||
karma?: { |
|||
/** |
|||
* Path to the karma config file. |
|||
*/ |
|||
config?: string; |
|||
}; |
|||
codeCoverage?: { |
|||
/** |
|||
* Globs to exclude from code coverage. |
|||
*/ |
|||
exclude?: string[]; |
|||
}; |
|||
}; |
|||
/** |
|||
* Specify the default values for generating. |
|||
*/ |
|||
defaults?: { |
|||
/** |
|||
* The file extension to be used for style files. |
|||
*/ |
|||
styleExt?: string; |
|||
/** |
|||
* How often to check for file updates. |
|||
*/ |
|||
poll?: number; |
|||
/** |
|||
* Use lint to fix files after generation |
|||
*/ |
|||
lintFix?: boolean; |
|||
/** |
|||
* Options for generating a class. |
|||
*/ |
|||
class?: { |
|||
/** |
|||
* Specifies if a spec file is generated. |
|||
*/ |
|||
spec?: boolean; |
|||
}; |
|||
/** |
|||
* Options for generating a component. |
|||
*/ |
|||
component?: { |
|||
/** |
|||
* Flag to indicate if a directory is created. |
|||
*/ |
|||
flat?: boolean; |
|||
/** |
|||
* Specifies if a spec file is generated. |
|||
*/ |
|||
spec?: boolean; |
|||
/** |
|||
* Specifies if the style will be in the ts file. |
|||
*/ |
|||
inlineStyle?: boolean; |
|||
/** |
|||
* Specifies if the template will be in the ts file. |
|||
*/ |
|||
inlineTemplate?: boolean; |
|||
/** |
|||
* Specifies the view encapsulation strategy. |
|||
*/ |
|||
viewEncapsulation?: ('Emulated' | 'Native' | 'None'); |
|||
/** |
|||
* Specifies the change detection strategy. |
|||
*/ |
|||
changeDetection?: ('Default' | 'OnPush'); |
|||
}; |
|||
/** |
|||
* Options for generating a directive. |
|||
*/ |
|||
directive?: { |
|||
/** |
|||
* Flag to indicate if a directory is created. |
|||
*/ |
|||
flat?: boolean; |
|||
/** |
|||
* Specifies if a spec file is generated. |
|||
*/ |
|||
spec?: boolean; |
|||
}; |
|||
/** |
|||
* Options for generating a guard. |
|||
*/ |
|||
guard?: { |
|||
/** |
|||
* Flag to indicate if a directory is created. |
|||
*/ |
|||
flat?: boolean; |
|||
/** |
|||
* Specifies if a spec file is generated. |
|||
*/ |
|||
spec?: boolean; |
|||
}; |
|||
/** |
|||
* Options for generating an interface. |
|||
*/ |
|||
interface?: { |
|||
/** |
|||
* Prefix to apply to interface names. (i.e. I) |
|||
*/ |
|||
prefix?: string; |
|||
}; |
|||
/** |
|||
* Options for generating a module. |
|||
*/ |
|||
module?: { |
|||
/** |
|||
* Flag to indicate if a directory is created. |
|||
*/ |
|||
flat?: boolean; |
|||
/** |
|||
* Specifies if a spec file is generated. |
|||
*/ |
|||
spec?: boolean; |
|||
}; |
|||
/** |
|||
* Options for generating a pipe. |
|||
*/ |
|||
pipe?: { |
|||
/** |
|||
* Flag to indicate if a directory is created. |
|||
*/ |
|||
flat?: boolean; |
|||
/** |
|||
* Specifies if a spec file is generated. |
|||
*/ |
|||
spec?: boolean; |
|||
}; |
|||
/** |
|||
* Options for generating a service. |
|||
*/ |
|||
service?: { |
|||
/** |
|||
* Flag to indicate if a directory is created. |
|||
*/ |
|||
flat?: boolean; |
|||
/** |
|||
* Specifies if a spec file is generated. |
|||
*/ |
|||
spec?: boolean; |
|||
}; |
|||
/** |
|||
* Properties to be passed to the build command. |
|||
*/ |
|||
build?: { |
|||
/** |
|||
* Output sourcemaps. |
|||
*/ |
|||
sourcemaps?: boolean; |
|||
/** |
|||
* Base url for the application being built. |
|||
*/ |
|||
baseHref?: string; |
|||
/** |
|||
* The ssl key used by the server. |
|||
*/ |
|||
progress?: boolean; |
|||
/** |
|||
* Enable and define the file watching poll time period (milliseconds). |
|||
*/ |
|||
poll?: number; |
|||
/** |
|||
* Delete output path before build. |
|||
*/ |
|||
deleteOutputPath?: boolean; |
|||
/** |
|||
* Do not use the real path when resolving modules. |
|||
*/ |
|||
preserveSymlinks?: boolean; |
|||
/** |
|||
* Show circular dependency warnings on builds. |
|||
*/ |
|||
showCircularDependencies?: boolean; |
|||
/** |
|||
* Use a separate bundle containing code used across multiple bundles. |
|||
*/ |
|||
commonChunk?: boolean; |
|||
/** |
|||
* Use file name for lazy loaded chunks. |
|||
*/ |
|||
namedChunks?: boolean; |
|||
}; |
|||
/** |
|||
* Properties to be passed to the serve command. |
|||
*/ |
|||
serve?: { |
|||
/** |
|||
* The port the application will be served on. |
|||
*/ |
|||
port?: number; |
|||
/** |
|||
* The host the application will be served on. |
|||
*/ |
|||
host?: string; |
|||
/** |
|||
* Enables ssl for the application. |
|||
*/ |
|||
ssl?: boolean; |
|||
/** |
|||
* The ssl key used by the server. |
|||
*/ |
|||
sslKey?: string; |
|||
/** |
|||
* The ssl certificate used by the server. |
|||
*/ |
|||
sslCert?: string; |
|||
/** |
|||
* Proxy configuration file. |
|||
*/ |
|||
proxyConfig?: string; |
|||
}; |
|||
/** |
|||
* Properties about schematics. |
|||
*/ |
|||
schematics?: { |
|||
/** |
|||
* The schematics collection to use. |
|||
*/ |
|||
collection?: string; |
|||
/** |
|||
* The new app schematic. |
|||
*/ |
|||
newApp?: string; |
|||
}; |
|||
}; |
|||
/** |
|||
* Specify which package manager tool to use. |
|||
*/ |
|||
packageManager?: ('npm' | 'cnpm' | 'yarn' | 'default'); |
|||
/** |
|||
* Allow people to disable console warnings. |
|||
*/ |
|||
warnings?: { |
|||
versionMismatch?: boolean; |
|||
}; |
|||
} |
|||
|
|||
export function getWorkspacePath(host: Tree): string { |
|||
const possibleFiles = [ '/angular.json', '/.angular.json' ]; |
|||
const path = possibleFiles.filter(path => host.exists(path))[0]; |
|||
|
|||
return path; |
|||
} |
|||
|
|||
export function getWorkspaceSchema(host: Tree): WorkspaceSchema { |
|||
const path = getWorkspacePath(host); |
|||
const configBuffer = host.read(path); |
|||
if (configBuffer === null) { |
|||
throw new SchematicsException(`Could not find (${path})`); |
|||
} |
|||
const content = configBuffer.toString(); |
|||
|
|||
return parseJson(content, JsonParseMode.Loose) as {} as WorkspaceSchema; |
|||
} |
|||
|
|||
export function addProjectToWorkspace<TProjectType extends ProjectType = ProjectType.Application>( |
|||
workspace: WorkspaceSchema, |
|||
name: string, |
|||
project: WorkspaceProject<TProjectType>, |
|||
): Rule { |
|||
return (_host: Tree, _context: SchematicContext) => { |
|||
|
|||
if (workspace.projects[name]) { |
|||
throw new Error(`Project '${name}' already exists in workspace.`); |
|||
} |
|||
|
|||
// Add project to workspace.
|
|||
workspace.projects[name] = project; |
|||
|
|||
if (!workspace.defaultProject && Object.keys(workspace.projects).length === 1) { |
|||
// Make the new project the default one.
|
|||
workspace.defaultProject = name; |
|||
} |
|||
|
|||
return updateWorkspaceSchema(workspace); |
|||
}; |
|||
} |
|||
|
|||
export function updateWorkspaceSchema(workspace: WorkspaceSchema): Rule { |
|||
return (host: Tree, _context: SchematicContext) => { |
|||
host.overwrite(getWorkspacePath(host), JSON.stringify(workspace, null, 2)); |
|||
}; |
|||
} |
|||
|
|||
export const configPath = '/.angular-cli.json'; |
|||
|
|||
export function getConfig(host: Tree): CliConfig { |
|||
const configBuffer = host.read(configPath); |
|||
if (configBuffer === null) { |
|||
throw new SchematicsException('Could not find .angular-cli.json'); |
|||
} |
|||
|
|||
const config = parseJson(configBuffer.toString(), JsonParseMode.Loose) as {} as CliConfig; |
|||
|
|||
return config; |
|||
} |
|||
|
|||
export function getAppFromConfig(config: CliConfig, appIndexOrName: string): AppConfig | null { |
|||
if (!config.apps) { |
|||
return null; |
|||
} |
|||
|
|||
if (parseInt(appIndexOrName) >= 0) { |
|||
return config.apps[parseInt(appIndexOrName)]; |
|||
} |
|||
|
|||
return config.apps.filter((app) => app.name === appIndexOrName)[0]; |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
import { Tree } from '@angular-devkit/schematics'; |
|||
import { JSONFile } from './json-file'; |
|||
|
|||
const PKG_JSON_PATH = '/package.json'; |
|||
export enum NodeDependencyType { |
|||
Default = 'dependencies', |
|||
Dev = 'devDependencies', |
|||
Peer = 'peerDependencies', |
|||
Optional = 'optionalDependencies', |
|||
} |
|||
|
|||
export interface NodeDependency { |
|||
type: NodeDependencyType; |
|||
name: string; |
|||
version: string; |
|||
overwrite?: boolean; |
|||
} |
|||
|
|||
const ALL_DEPENDENCY_TYPE = [ |
|||
NodeDependencyType.Default, |
|||
NodeDependencyType.Dev, |
|||
NodeDependencyType.Optional, |
|||
NodeDependencyType.Peer, |
|||
]; |
|||
|
|||
export function addPackageJsonDependency(tree: Tree, dependency: NodeDependency, pkgJsonPath = PKG_JSON_PATH): void { |
|||
const json = new JSONFile(tree, pkgJsonPath); |
|||
if (json.error) { |
|||
throw json.error; |
|||
} |
|||
|
|||
const { overwrite, type, name, version } = dependency; |
|||
const path = [type, name]; |
|||
if (overwrite || !json.get(path)) { |
|||
json.modify(path, version); |
|||
} |
|||
} |
|||
|
|||
export function removePackageJsonDependency(tree: Tree, name: string, pkgJsonPath = PKG_JSON_PATH): void { |
|||
const json = new JSONFile(tree, pkgJsonPath); |
|||
if (json.error) { |
|||
throw json.error; |
|||
} |
|||
|
|||
for (const depType of ALL_DEPENDENCY_TYPE) { |
|||
json.remove([depType, name]); |
|||
} |
|||
} |
|||
|
|||
export function getPackageJsonDependency(tree: Tree, name: string, pkgJsonPath = PKG_JSON_PATH): NodeDependency | null { |
|||
const json = new JSONFile(tree, pkgJsonPath); |
|||
if (json.error) { |
|||
throw json.error; |
|||
} |
|||
|
|||
for (const depType of ALL_DEPENDENCY_TYPE) { |
|||
const version = json.get([depType, name]); |
|||
|
|||
if (typeof version === 'string') { |
|||
return { |
|||
type: depType, |
|||
name: name, |
|||
version, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
@ -0,0 +1,151 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
import { |
|||
dirname, |
|||
join, |
|||
normalize, NormalizedRoot, |
|||
Path, |
|||
relative |
|||
} from '@angular-devkit/core'; |
|||
import { DirEntry, Tree } from '@angular-devkit/schematics'; |
|||
|
|||
|
|||
export interface ModuleOptions { |
|||
project?: string; // added this
|
|||
module?: string; |
|||
name: string; |
|||
flat?: boolean; |
|||
path?: string; |
|||
route?: string; // added this
|
|||
selector?: string; // added this
|
|||
skipImport?: boolean; |
|||
moduleExt?: string; |
|||
routingModuleExt?: string; |
|||
} |
|||
|
|||
export const MODULE_EXT = '.module.ts'; |
|||
export const ROUTING_MODULE_EXT = '-routing.module.ts'; |
|||
|
|||
/** |
|||
* Find the module referred by a set of options passed to the schematics. |
|||
*/ |
|||
export function findModuleFromOptions(host: Tree, options: ModuleOptions): Path | undefined { |
|||
if (options.hasOwnProperty('skipImport') && options.skipImport) { |
|||
return undefined; |
|||
} |
|||
|
|||
const moduleExt = options.moduleExt || MODULE_EXT; |
|||
const routingModuleExt = options.routingModuleExt || ROUTING_MODULE_EXT; |
|||
|
|||
if (!options.module) { |
|||
const pathToCheck = (options.path || '') + '/' + options.name; |
|||
|
|||
return normalize(findModule(host, pathToCheck, moduleExt, routingModuleExt)); |
|||
} else { |
|||
const modulePath = normalize(`/${options.path}/${options.module}`); |
|||
const componentPath = normalize(`/${options.path}/${options.name}`); |
|||
const moduleBaseName = normalize(modulePath).split('/').pop(); |
|||
|
|||
const candidateSet = new Set<Path>([ |
|||
normalize(options.path || '/'), |
|||
]); |
|||
|
|||
for (let dir = modulePath; dir != NormalizedRoot; dir = dirname(dir)) { |
|||
candidateSet.add(dir); |
|||
} |
|||
for (let dir = componentPath; dir != NormalizedRoot; dir = dirname(dir)) { |
|||
candidateSet.add(dir); |
|||
} |
|||
|
|||
const candidatesDirs = [...candidateSet].sort((a, b) => b.length - a.length); |
|||
for (const c of candidatesDirs) { |
|||
const candidateFiles = [ |
|||
'', |
|||
`${moduleBaseName}.ts`, |
|||
`${moduleBaseName}${moduleExt}`, |
|||
].map(x => join(c, x)); |
|||
|
|||
for (const sc of candidateFiles) { |
|||
if (host.exists(sc)) { |
|||
return normalize(sc); |
|||
} |
|||
} |
|||
} |
|||
|
|||
throw new Error( |
|||
`Specified module '${options.module}' does not exist.\n` |
|||
+ `Looked in the following directories:\n ${candidatesDirs.join('\n ')}`, |
|||
); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Function to find the "closest" module to a generated file's path. |
|||
*/ |
|||
export function findModule(host: Tree, generateDir: string, |
|||
moduleExt = MODULE_EXT, routingModuleExt = ROUTING_MODULE_EXT): Path { |
|||
|
|||
let dir: DirEntry | null = host.getDir('/' + generateDir); |
|||
let foundRoutingModule = false; |
|||
|
|||
while (dir) { |
|||
const allMatches = dir.subfiles.filter(p => p.endsWith(moduleExt)); |
|||
const filteredMatches = allMatches.filter(p => !p.endsWith(routingModuleExt)); |
|||
|
|||
foundRoutingModule = foundRoutingModule || allMatches.length !== filteredMatches.length; |
|||
|
|||
if (filteredMatches.length == 1) { |
|||
return join(dir.path, filteredMatches[0]); |
|||
} else if (filteredMatches.length > 1) { |
|||
throw new Error( |
|||
'More than one module matches. Use the skip-import option to skip importing ' + |
|||
'the component into the closest module or use the module option to specify a module.'); |
|||
} |
|||
|
|||
dir = dir.parent; |
|||
} |
|||
|
|||
const errorMsg = foundRoutingModule ? 'Could not find a non Routing NgModule.' |
|||
+ `\nModules with suffix '${routingModuleExt}' are strictly reserved for routing.` |
|||
+ '\nUse the skip-import option to skip importing in NgModule.' |
|||
: 'Could not find an NgModule. Use the skip-import option to skip importing in NgModule.'; |
|||
|
|||
throw new Error(errorMsg); |
|||
} |
|||
|
|||
/** |
|||
* Build a relative path from one file path to another file path. |
|||
*/ |
|||
export function buildRelativePath(from: string, to: string): string { |
|||
from = normalize(from); |
|||
to = normalize(to); |
|||
|
|||
// Convert to arrays.
|
|||
const fromParts = from.split('/'); |
|||
const toParts = to.split('/'); |
|||
|
|||
// Remove file names (preserving destination)
|
|||
fromParts.pop(); |
|||
const toFileName = toParts.pop(); |
|||
|
|||
const relativePath = relative(normalize(fromParts.join('/') || '/'), |
|||
normalize(toParts.join('/') || '/')); |
|||
let pathPrefix = ''; |
|||
|
|||
// Set the path prefix for same dir or child dir, parent dir starts with `..`
|
|||
if (!relativePath) { |
|||
pathPrefix = '.'; |
|||
} else if (!relativePath.startsWith('.')) { |
|||
pathPrefix = `./`; |
|||
} |
|||
if (pathPrefix && !pathPrefix.endsWith('/')) { |
|||
pathPrefix += '/'; |
|||
} |
|||
|
|||
return pathPrefix + (relativePath ? relativePath + '/' : '') + toFileName; |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
export * from './ast-utils'; |
|||
export * from './change'; |
|||
export * from './config'; |
|||
export * from './dependencies'; |
|||
export * from './find-module'; |
|||
export * from './json-file'; |
|||
export * from './json-utils'; |
|||
export * from './latest-versions'; |
|||
export * from './lint-fix'; |
|||
export * from './ng-ast-utils'; |
|||
export * from './parse-name'; |
|||
export * from './paths'; |
|||
export * from './project-targets'; |
|||
export * from './tsconfig'; |
|||
export * from './validation'; |
|||
export * from './workspace'; |
|||
export * from './workspace-models'; |
|||
@ -0,0 +1,82 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
|
|||
import { JsonValue } from '@angular-devkit/core'; |
|||
import { Tree } from '@angular-devkit/schematics'; |
|||
import { Node, applyEdits, findNodeAtLocation, getNodeValue, modify, parseTree } from 'jsonc-parser'; |
|||
|
|||
export type JSONPath = (string | number)[]; |
|||
|
|||
/** @internal */ |
|||
export class JSONFile { |
|||
private content: string; |
|||
error: undefined | Error; |
|||
|
|||
constructor( |
|||
private readonly host: Tree, |
|||
private readonly path: string, |
|||
) { |
|||
const buffer = this.host.read(this.path); |
|||
if (buffer) { |
|||
this.content = buffer.toString(); |
|||
} else { |
|||
this.error = new Error(`Could not read ${path}.`); |
|||
} |
|||
} |
|||
|
|||
private _jsonAst: Node | undefined; |
|||
private get JsonAst(): Node { |
|||
if (this._jsonAst) { |
|||
return this._jsonAst; |
|||
} |
|||
|
|||
this._jsonAst = parseTree(this.content); |
|||
|
|||
return this._jsonAst; |
|||
} |
|||
|
|||
get(jsonPath: JSONPath): unknown { |
|||
if (jsonPath.length === 0) { |
|||
return getNodeValue(this.JsonAst); |
|||
} |
|||
|
|||
const node = findNodeAtLocation(this.JsonAst, jsonPath); |
|||
|
|||
return node === undefined ? undefined : getNodeValue(node); |
|||
} |
|||
|
|||
modify(jsonPath: JSONPath, value: JsonValue | undefined, getInsertionIndex?: (properties: string[]) => number): void { |
|||
if (!getInsertionIndex) { |
|||
const property = jsonPath.slice(-1)[0]; |
|||
getInsertionIndex = properties => [...properties, property].sort().findIndex(p => p === property); |
|||
} |
|||
|
|||
const edits = modify( |
|||
this.content, |
|||
jsonPath, |
|||
value, |
|||
{ |
|||
getInsertionIndex, |
|||
formattingOptions: { |
|||
insertSpaces: true, |
|||
tabSize: 2, |
|||
}, |
|||
}, |
|||
); |
|||
|
|||
this.content = applyEdits(this.content, edits); |
|||
this.host.overwrite(this.path, this.content); |
|||
this._jsonAst = undefined; |
|||
} |
|||
|
|||
remove(jsonPath: JSONPath): void { |
|||
if (this.get(jsonPath) !== undefined) { |
|||
this.modify(jsonPath, undefined); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,231 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
import { |
|||
JsonAstArray, |
|||
JsonAstKeyValue, |
|||
JsonAstNode, |
|||
JsonAstObject, |
|||
JsonValue, |
|||
} from '@angular-devkit/core'; |
|||
import { UpdateRecorder } from '@angular-devkit/schematics'; |
|||
|
|||
export function appendPropertyInAstObject( |
|||
recorder: UpdateRecorder, |
|||
node: JsonAstObject, |
|||
propertyName: string, |
|||
value: JsonValue, |
|||
indent: number, |
|||
) { |
|||
const indentStr = _buildIndent(indent); |
|||
let index = node.start.offset + 1; |
|||
if (node.properties.length > 0) { |
|||
// Insert comma.
|
|||
const last = node.properties[node.properties.length - 1]; |
|||
const { text, end } = last; |
|||
const commaIndex = text.endsWith('\n') ? end.offset - 1 : end.offset; |
|||
recorder.insertRight(commaIndex, ','); |
|||
index = end.offset; |
|||
} |
|||
const content = _stringifyContent(value, indentStr); |
|||
recorder.insertRight( |
|||
index, |
|||
(node.properties.length === 0 && indent ? '\n' : '') |
|||
+ ' '.repeat(indent) |
|||
+ `"${propertyName}":${indent ? ' ' : ''}${content}` |
|||
+ indentStr.slice(0, -indent), |
|||
); |
|||
} |
|||
|
|||
export function insertPropertyInAstObjectInOrder( |
|||
recorder: UpdateRecorder, |
|||
node: JsonAstObject, |
|||
propertyName: string, |
|||
value: JsonValue, |
|||
indent: number, |
|||
) { |
|||
|
|||
if (node.properties.length === 0) { |
|||
appendPropertyInAstObject(recorder, node, propertyName, value, indent); |
|||
|
|||
return; |
|||
} |
|||
|
|||
// Find insertion info.
|
|||
let insertAfterProp: JsonAstKeyValue | null = null; |
|||
let prev: JsonAstKeyValue | null = null; |
|||
let isLastProp = false; |
|||
const last = node.properties[node.properties.length - 1]; |
|||
for (const prop of node.properties) { |
|||
if (prop.key.value > propertyName) { |
|||
if (prev) { |
|||
insertAfterProp = prev; |
|||
} |
|||
break; |
|||
} |
|||
if (prop === last) { |
|||
isLastProp = true; |
|||
insertAfterProp = last; |
|||
} |
|||
prev = prop; |
|||
} |
|||
|
|||
if (isLastProp) { |
|||
appendPropertyInAstObject(recorder, node, propertyName, value, indent); |
|||
|
|||
return; |
|||
} |
|||
|
|||
const indentStr = _buildIndent(indent); |
|||
const insertIndex = insertAfterProp === null |
|||
? node.start.offset + 1 |
|||
: insertAfterProp.end.offset + 1; |
|||
const content = _stringifyContent(value, indentStr); |
|||
recorder.insertRight( |
|||
insertIndex, |
|||
indentStr |
|||
+ `"${propertyName}":${indent ? ' ' : ''}${content}` |
|||
+ ',', |
|||
); |
|||
} |
|||
|
|||
export function removePropertyInAstObject( |
|||
recorder: UpdateRecorder, |
|||
node: JsonAstObject, |
|||
propertyName: string, |
|||
) { |
|||
// Find the property inside the object.
|
|||
const propIdx = node.properties.findIndex(prop => prop.key.value === propertyName); |
|||
|
|||
if (propIdx === -1) { |
|||
// There's nothing to remove.
|
|||
return; |
|||
} |
|||
|
|||
if (node.properties.length === 1) { |
|||
// This is a special case. Everything should be removed, including indentation.
|
|||
recorder.remove(node.start.offset, node.end.offset - node.start.offset); |
|||
recorder.insertRight(node.start.offset, '{}'); |
|||
|
|||
return; |
|||
} |
|||
|
|||
// The AST considers commas and indentation to be part of the preceding property.
|
|||
// To get around messy comma and identation management, we can work over the range between
|
|||
// two properties instead.
|
|||
const previousProp = node.properties[propIdx - 1]; |
|||
const targetProp = node.properties[propIdx]; |
|||
const nextProp = node.properties[propIdx + 1]; |
|||
|
|||
let start, end; |
|||
if (previousProp) { |
|||
// Given the object below, and intending to remove the `m` property:
|
|||
// "{\n \"a\": \"a\",\n \"m\": \"m\",\n \"z\": \"z\"\n}"
|
|||
// ^---------------^
|
|||
// Removing the range above results in:
|
|||
// "{\n \"a\": \"a\",\n \"z\": \"z\"\n}"
|
|||
start = previousProp.end; |
|||
end = targetProp.end; |
|||
} else { |
|||
// If there's no previousProp there is a nextProp, since we've specialcased the 1 length case.
|
|||
// Given the object below, and intending to remove the `a` property:
|
|||
// "{\n \"a\": \"a\",\n \"m\": \"m\",\n \"z\": \"z\"\n}"
|
|||
// ^---------------^
|
|||
// Removing the range above results in:
|
|||
// "{\n \"m\": \"m\",\n \"z\": \"z\"\n}"
|
|||
start = targetProp.start; |
|||
end = nextProp.start; |
|||
} |
|||
|
|||
recorder.remove(start.offset, end.offset - start.offset); |
|||
if (!nextProp) { |
|||
recorder.insertRight(start.offset, '\n'); |
|||
} |
|||
} |
|||
|
|||
|
|||
export function appendValueInAstArray( |
|||
recorder: UpdateRecorder, |
|||
node: JsonAstArray, |
|||
value: JsonValue, |
|||
indent = 4, |
|||
) { |
|||
let indentStr = _buildIndent(indent); |
|||
let index = node.start.offset + 1; |
|||
// tslint:disable-next-line: no-any
|
|||
let newNodes: any[] | undefined; |
|||
|
|||
if (node.elements.length > 0) { |
|||
// Insert comma.
|
|||
const { end } = node.elements[node.elements.length - 1]; |
|||
const isClosingOnSameLine = node.end.offset - end.offset === 1; |
|||
|
|||
if (isClosingOnSameLine && indent) { |
|||
// Reformat the entire array
|
|||
recorder.remove(node.start.offset, node.end.offset - node.start.offset); |
|||
newNodes = [ |
|||
...node.elements.map(({ value }) => value), |
|||
value, |
|||
]; |
|||
index = node.start.offset; |
|||
// In case we are generating the entire node we need to reduce the spacing as
|
|||
// otherwise we'd end up having incorrect double spacing
|
|||
indent = indent - 2; |
|||
indentStr = _buildIndent(indent); |
|||
} else { |
|||
recorder.insertRight(end.offset, ','); |
|||
index = end.offset; |
|||
} |
|||
} |
|||
|
|||
recorder.insertRight( |
|||
index, |
|||
(newNodes ? '' : indentStr) |
|||
+ _stringifyContent(newNodes || value, indentStr) |
|||
+ (node.elements.length === 0 && indent ? indentStr.substr(0, -indent) + '\n' : ''), |
|||
); |
|||
} |
|||
|
|||
|
|||
export function findPropertyInAstObject( |
|||
node: JsonAstObject, |
|||
propertyName: string, |
|||
): JsonAstNode | null { |
|||
let maybeNode: JsonAstNode | null = null; |
|||
for (const property of node.properties) { |
|||
if (property.key.value == propertyName) { |
|||
maybeNode = property.value; |
|||
} |
|||
} |
|||
|
|||
return maybeNode; |
|||
} |
|||
|
|||
function _buildIndent(count: number): string { |
|||
return count ? '\n' + ' '.repeat(count) : ''; |
|||
} |
|||
|
|||
function _stringifyContent(value: JsonValue, indentStr: string): string { |
|||
// TODO: Add snapshot tests
|
|||
|
|||
// The 'space' value is 2, because we want to add 2 additional
|
|||
// indents from the 'key' node.
|
|||
|
|||
// If we use the indent provided we will have double indents:
|
|||
// "budgets": [
|
|||
// {
|
|||
// "type": "initial",
|
|||
// "maximumWarning": "2mb",
|
|||
// "maximumError": "5mb"
|
|||
// },
|
|||
// {
|
|||
// "type": "anyComponentStyle",
|
|||
// 'maximumWarning": "5kb"
|
|||
// }
|
|||
// ]
|
|||
return JSON.stringify(value, null, 2).replace(/\n/g, indentStr); |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
|
|||
export const latestVersions = { |
|||
// These versions should be kept up to date with latest Angular peer dependencies.
|
|||
Angular: '~10.0.0-rc.0', |
|||
RxJs: '~6.6.0', |
|||
ZoneJs: '~0.10.2', |
|||
TypeScript: '~3.9.5', |
|||
TsLib: '^2.0.0', |
|||
|
|||
// The versions below must be manually updated when making a new devkit release.
|
|||
// For our e2e tests, these versions must match the latest tag present on the branch.
|
|||
// During RC periods they will not match the latest RC until there's a new git tag, and
|
|||
// should not be updated.
|
|||
DevkitBuildAngular: '~0.1000.0-rc.0', |
|||
DevkitBuildNgPackagr: '~0.1000.0-rc.0', |
|||
DevkitBuildWebpack: '~0.1000.0-rc.0', |
|||
|
|||
ngPackagr: '^10.0.0', |
|||
}; |
|||
@ -0,0 +1,51 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
import { |
|||
DirEntry, |
|||
Rule, |
|||
SchematicContext, |
|||
SchematicsException, |
|||
Tree, |
|||
} from '@angular-devkit/schematics'; |
|||
import { TslintFixTask } from '@angular-devkit/schematics/tasks'; |
|||
|
|||
export function applyLintFix(path = '/'): Rule { |
|||
return (tree: Tree, context: SchematicContext) => { |
|||
// Find the closest tslint.json or tslint.yaml
|
|||
let dir: DirEntry | null = tree.getDir(path.substr(0, path.lastIndexOf('/'))); |
|||
|
|||
do { |
|||
if ((dir.subfiles as string[]).some(f => f === 'tslint.json' || f === 'tslint.yaml')) { |
|||
break; |
|||
} |
|||
|
|||
dir = dir.parent; |
|||
} while (dir !== null); |
|||
|
|||
if (dir === null) { |
|||
throw new SchematicsException( |
|||
'Asked to run lint fixes, but could not find a tslint.json or tslint.yaml config file.'); |
|||
} |
|||
|
|||
// Only include files that have been touched.
|
|||
const files = tree.actions.reduce((acc: Set<string>, action) => { |
|||
const path = action.path.substr(1); // Remove the starting '/'.
|
|||
if (path.endsWith('.ts') && dir && action.path.startsWith(dir.path)) { |
|||
acc.add(path); |
|||
} |
|||
|
|||
return acc; |
|||
}, new Set<string>()); |
|||
|
|||
context.addTask(new TslintFixTask({ |
|||
ignoreErrors: true, |
|||
tsConfigPath: 'tsconfig.json', |
|||
files: [...files], |
|||
})); |
|||
}; |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
import { normalize } from '@angular-devkit/core'; |
|||
import { SchematicsException, Tree } from '@angular-devkit/schematics'; |
|||
import { dirname } from 'path'; |
|||
import * as ts from 'typescript'; |
|||
import { findNode, getSourceNodes } from './ast-utils'; |
|||
|
|||
export function findBootstrapModuleCall(host: Tree, mainPath: string): ts.CallExpression | null { |
|||
const mainBuffer = host.read(mainPath); |
|||
if (!mainBuffer) { |
|||
throw new SchematicsException(`Main file (${mainPath}) not found`); |
|||
} |
|||
const mainText = mainBuffer.toString('utf-8'); |
|||
const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true); |
|||
|
|||
const allNodes = getSourceNodes(source); |
|||
|
|||
let bootstrapCall: ts.CallExpression | null = null; |
|||
|
|||
for (const node of allNodes) { |
|||
let bootstrapCallNode: ts.Node | null = null; |
|||
bootstrapCallNode = findNode(node, ts.SyntaxKind.Identifier, 'bootstrapModule'); |
|||
|
|||
// Walk up the parent until CallExpression is found.
|
|||
while ( |
|||
bootstrapCallNode && |
|||
bootstrapCallNode.parent && |
|||
bootstrapCallNode.parent.kind !== ts.SyntaxKind.CallExpression |
|||
) { |
|||
bootstrapCallNode = bootstrapCallNode.parent; |
|||
} |
|||
|
|||
if ( |
|||
bootstrapCallNode !== null && |
|||
bootstrapCallNode.parent !== undefined && |
|||
bootstrapCallNode.parent.kind === ts.SyntaxKind.CallExpression |
|||
) { |
|||
bootstrapCall = bootstrapCallNode.parent as ts.CallExpression; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return bootstrapCall; |
|||
} |
|||
|
|||
export function findBootstrapModulePath(host: Tree, mainPath: string): string { |
|||
const bootstrapCall = findBootstrapModuleCall(host, mainPath); |
|||
if (!bootstrapCall) { |
|||
throw new SchematicsException('Bootstrap call not found'); |
|||
} |
|||
|
|||
const bootstrapModule = bootstrapCall.arguments[0]; |
|||
|
|||
const mainBuffer = host.read(mainPath); |
|||
if (!mainBuffer) { |
|||
throw new SchematicsException(`Client app main file (${mainPath}) not found`); |
|||
} |
|||
const mainText = mainBuffer.toString('utf-8'); |
|||
const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true); |
|||
const allNodes = getSourceNodes(source); |
|||
const bootstrapModuleRelativePath = allNodes |
|||
.filter(node => node.kind === ts.SyntaxKind.ImportDeclaration) |
|||
.filter(imp => { |
|||
return findNode(imp, ts.SyntaxKind.Identifier, bootstrapModule.getText()); |
|||
}) |
|||
.map((imp: ts.ImportDeclaration) => { |
|||
const modulePathStringLiteral = imp.moduleSpecifier as ts.StringLiteral; |
|||
|
|||
return modulePathStringLiteral.text; |
|||
})[0]; |
|||
|
|||
return bootstrapModuleRelativePath; |
|||
} |
|||
|
|||
export function getAppModulePath(host: Tree, mainPath: string): string { |
|||
const moduleRelativePath = findBootstrapModulePath(host, mainPath); |
|||
const mainDir = dirname(mainPath); |
|||
const modulePath = normalize(`/${mainDir}/${moduleRelativePath}.ts`); |
|||
|
|||
return modulePath; |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
|
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
// import { relative, Path } from "../../../angular_devkit/core/src/virtual-fs";
|
|||
import { Path, basename, dirname, join, normalize } from '@angular-devkit/core'; |
|||
|
|||
export interface Location { |
|||
name: string; |
|||
path: Path; |
|||
} |
|||
|
|||
export function parseName(path: string, name: string): Location { |
|||
const nameWithoutPath = basename(normalize(name)); |
|||
const namePath = dirname(join(normalize(path), name) as Path); |
|||
|
|||
return { |
|||
name: nameWithoutPath, |
|||
path: normalize('/' + namePath), |
|||
}; |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
|
|||
import { normalize, split } from '@angular-devkit/core'; |
|||
|
|||
export function relativePathToWorkspaceRoot(projectRoot: string | undefined): string { |
|||
const normalizedPath = split(normalize(projectRoot || '')); |
|||
|
|||
if (normalizedPath.length === 0 || !normalizedPath[0]) { |
|||
return '.'; |
|||
} else { |
|||
return normalizedPath.map(() => '..').join('/'); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
|
|||
import { SchematicsException } from '@angular-devkit/schematics'; |
|||
|
|||
export function targetBuildNotFoundError(): SchematicsException { |
|||
return new SchematicsException(`Project target "build" not found.`); |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
|
|||
import { JsonParseMode, parseJsonAst } from '@angular-devkit/core'; |
|||
import { Rule, SchematicsException, Tree } from '@angular-devkit/schematics'; |
|||
import { appendValueInAstArray, findPropertyInAstObject } from './json-utils'; |
|||
|
|||
const SOLUTION_TSCONFIG_PATH = 'tsconfig.json'; |
|||
|
|||
/** |
|||
* Add project references in "Solution Style" tsconfig. |
|||
*/ |
|||
export function addTsConfigProjectReferences(paths: string[]): Rule { |
|||
return (host, context) => { |
|||
const logger = context.logger; |
|||
|
|||
// We need to read after each write to avoid missing `,` when appending multiple items.
|
|||
for (const path of paths) { |
|||
const source = host.read(SOLUTION_TSCONFIG_PATH); |
|||
if (!source) { |
|||
// Solution tsconfig doesn't exist.
|
|||
logger.warn(`Cannot add reference '${path}' in '${SOLUTION_TSCONFIG_PATH}'. File doesn't exists.`); |
|||
|
|||
return; |
|||
} |
|||
|
|||
const jsonAst = parseJsonAst(source.toString(), JsonParseMode.Loose); |
|||
if (jsonAst?.kind !== 'object') { |
|||
// Invalid JSON
|
|||
throw new SchematicsException(`Invalid JSON AST Object '${SOLUTION_TSCONFIG_PATH}'.`); |
|||
} |
|||
|
|||
// Solutions style tsconfig can contain 2 properties:
|
|||
// - 'files' with a value of empty array
|
|||
// - 'references'
|
|||
const filesAst = findPropertyInAstObject(jsonAst, 'files'); |
|||
const referencesAst = findPropertyInAstObject(jsonAst, 'references'); |
|||
if ( |
|||
filesAst?.kind !== 'array' || |
|||
filesAst.elements.length !== 0 || |
|||
referencesAst?.kind !== 'array' |
|||
) { |
|||
logger.warn(`Cannot add reference '${path}' in '${SOLUTION_TSCONFIG_PATH}'. It appears to be an invalid solution style tsconfig.`); |
|||
|
|||
return; |
|||
} |
|||
|
|||
// Append new paths
|
|||
const recorder = host.beginUpdate(SOLUTION_TSCONFIG_PATH); |
|||
appendValueInAstArray(recorder, referencesAst, { 'path': `./${path}` }, 4); |
|||
host.commitUpdate(recorder); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Throws an exception when the base tsconfig doesn't exists. |
|||
*/ |
|||
export function verifyBaseTsConfigExists(host: Tree): void { |
|||
if (host.exists('tsconfig.base.json')) { |
|||
return; |
|||
} |
|||
|
|||
throw new SchematicsException(`Cannot find base TypeScript configuration file 'tsconfig.base.json'.`); |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
import { tags } from '@angular-devkit/core'; |
|||
import { SchematicsException } from '@angular-devkit/schematics'; |
|||
|
|||
export function validateName(name: string): void { |
|||
if (name && /^\d/.test(name)) { |
|||
throw new SchematicsException(tags.oneLine`name (${name})
|
|||
can not start with a digit.`);
|
|||
} |
|||
} |
|||
|
|||
// Must start with a letter, and must contain only alphanumeric characters or dashes.
|
|||
// When adding a dash the segment after the dash must also start with a letter.
|
|||
export const htmlSelectorRe = /^[a-zA-Z][.0-9a-zA-Z]*(:?-[a-zA-Z][.0-9a-zA-Z]*)*$/; |
|||
|
|||
export function validateHtmlSelector(selector: string): void { |
|||
if (selector && !htmlSelectorRe.test(selector)) { |
|||
throw new SchematicsException(tags.oneLine`Selector (${selector})
|
|||
is invalid.`);
|
|||
} |
|||
} |
|||
|
|||
|
|||
export function validateProjectName(projectName: string) { |
|||
const errorIndex = getRegExpFailPosition(projectName); |
|||
const unsupportedProjectNames: string[] = []; |
|||
const packageNameRegex = /^(?:@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9_-]+$/; |
|||
if (errorIndex !== null) { |
|||
const firstMessage = tags.oneLine` |
|||
Project name "${projectName}" is not valid. New project names must |
|||
start with a letter, and must contain only alphanumeric characters or dashes. |
|||
When adding a dash the segment after the dash must also start with a letter. |
|||
`;
|
|||
const msg = tags.stripIndent` |
|||
${firstMessage} |
|||
${projectName} |
|||
${Array(errorIndex + 1).join(' ') + '^'} |
|||
`;
|
|||
throw new SchematicsException(msg); |
|||
} else if (unsupportedProjectNames.indexOf(projectName) !== -1) { |
|||
throw new SchematicsException( |
|||
`Project name ${JSON.stringify(projectName)} is not a supported name.`); |
|||
} else if (!packageNameRegex.test(projectName)) { |
|||
throw new SchematicsException(`Project name ${JSON.stringify(projectName)} is invalid.`); |
|||
} |
|||
} |
|||
|
|||
function getRegExpFailPosition(str: string): number | null { |
|||
const isScope = /^@.*\/.*/.test(str); |
|||
if (isScope) { |
|||
// Remove starting @
|
|||
str = str.replace(/^@/, ''); |
|||
// Change / to - for validation
|
|||
str = str.replace(/\//g, '-'); |
|||
} |
|||
|
|||
const parts = str.indexOf('-') >= 0 ? str.split('-') : [str]; |
|||
const matched: string[] = []; |
|||
|
|||
const projectNameRegexp = /^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$/; |
|||
|
|||
parts.forEach(part => { |
|||
if (part.match(projectNameRegexp)) { |
|||
matched.push(part); |
|||
} |
|||
}); |
|||
|
|||
const compare = matched.join('-'); |
|||
|
|||
return (str !== compare) ? compare.length : null; |
|||
} |
|||
@ -0,0 +1,179 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
export enum ProjectType { |
|||
Application = 'application', |
|||
Library = 'library', |
|||
} |
|||
|
|||
export enum Builders { |
|||
AppShell = '@angular-devkit/build-angular:app-shell', |
|||
Server = '@angular-devkit/build-angular:server', |
|||
Browser = '@angular-devkit/build-angular:browser', |
|||
Karma = '@angular-devkit/build-angular:karma', |
|||
TsLint = '@angular-devkit/build-angular:tslint', |
|||
DeprecatedNgPackagr = '@angular-devkit/build-ng-packagr:build', |
|||
NgPackagr = '@angular-devkit/build-angular:ng-packagr', |
|||
DevServer = '@angular-devkit/build-angular:dev-server', |
|||
ExtractI18n = '@angular-devkit/build-angular:extract-i18n', |
|||
Protractor = '@angular-devkit/build-angular:protractor', |
|||
} |
|||
|
|||
export interface FileReplacements { |
|||
replace: string; |
|||
with: string; |
|||
} |
|||
|
|||
export interface BrowserBuilderBaseOptions { |
|||
main: string; |
|||
tsConfig: string; |
|||
fileReplacements?: FileReplacements[]; |
|||
outputPath?: string; |
|||
index?: string; |
|||
polyfills: string; |
|||
assets?: (object | string)[]; |
|||
styles?: (object | string)[]; |
|||
scripts?: (object | string)[]; |
|||
sourceMap?: boolean; |
|||
} |
|||
|
|||
export type OutputHashing = 'all' | 'media' | 'none' | 'bundles'; |
|||
|
|||
export interface BrowserBuilderOptions extends BrowserBuilderBaseOptions { |
|||
serviceWorker?: boolean; |
|||
optimization?: boolean; |
|||
outputHashing?: OutputHashing; |
|||
resourcesOutputPath?: string; |
|||
extractCss?: boolean; |
|||
namedChunks?: boolean; |
|||
aot?: boolean; |
|||
extractLicenses?: boolean; |
|||
vendorChunk?: boolean; |
|||
buildOptimizer?: boolean; |
|||
ngswConfigPath?: string; |
|||
budgets?: { |
|||
type: string; |
|||
maximumWarning?: string; |
|||
maximumError?: string; |
|||
}[]; |
|||
webWorkerTsConfig?: string; |
|||
} |
|||
|
|||
export interface ServeBuilderOptions { |
|||
browserTarget: string; |
|||
} |
|||
export interface LibraryBuilderOptions { |
|||
tsConfig: string; |
|||
project: string; |
|||
} |
|||
|
|||
export interface ServerBuilderOptions { |
|||
outputPath: string; |
|||
tsConfig: string; |
|||
main: string; |
|||
fileReplacements?: FileReplacements[]; |
|||
optimization?: { |
|||
scripts?: boolean; |
|||
styles?: boolean; |
|||
}; |
|||
sourceMap?: |
|||
| boolean |
|||
| { |
|||
scripts?: boolean; |
|||
styles?: boolean; |
|||
hidden?: boolean; |
|||
vendor?: boolean; |
|||
}; |
|||
} |
|||
|
|||
export interface AppShellBuilderOptions { |
|||
browserTarget: string; |
|||
serverTarget: string; |
|||
route: string; |
|||
} |
|||
|
|||
export interface TestBuilderOptions extends Partial<BrowserBuilderBaseOptions> { |
|||
karmaConfig: string; |
|||
} |
|||
|
|||
export interface LintBuilderOptions { |
|||
tsConfig: string[] | string; |
|||
exclude?: string[]; |
|||
} |
|||
|
|||
export interface ExtractI18nOptions { |
|||
browserTarget: string; |
|||
} |
|||
|
|||
export interface E2EOptions { |
|||
protractorConfig: string; |
|||
devServerTarget: string; |
|||
} |
|||
|
|||
export interface BuilderTarget<TBuilder extends Builders, TOptions> { |
|||
builder: TBuilder; |
|||
options: TOptions; |
|||
configurations?: { |
|||
production: Partial<TOptions>; |
|||
[key: string]: Partial<TOptions>; |
|||
}; |
|||
} |
|||
|
|||
export type LibraryBuilderTarget = BuilderTarget<Builders.NgPackagr, LibraryBuilderOptions>; |
|||
export type BrowserBuilderTarget = BuilderTarget<Builders.Browser, BrowserBuilderOptions>; |
|||
export type ServerBuilderTarget = BuilderTarget<Builders.Server, ServerBuilderOptions>; |
|||
export type AppShellBuilderTarget = BuilderTarget<Builders.AppShell, AppShellBuilderOptions>; |
|||
export type LintBuilderTarget = BuilderTarget<Builders.TsLint, LintBuilderOptions>; |
|||
export type TestBuilderTarget = BuilderTarget<Builders.Karma, TestBuilderOptions>; |
|||
export type ServeBuilderTarget = BuilderTarget<Builders.DevServer, ServeBuilderOptions>; |
|||
export type ExtractI18nBuilderTarget = BuilderTarget<Builders.ExtractI18n, ExtractI18nOptions>; |
|||
export type E2EBuilderTarget = BuilderTarget<Builders.Protractor, E2EOptions>; |
|||
|
|||
export interface WorkspaceSchema { |
|||
version: 1; |
|||
defaultProject?: string; |
|||
cli?: { warnings?: Record<string, boolean> }; |
|||
projects: { |
|||
[key: string]: WorkspaceProject<ProjectType.Application | ProjectType.Library>; |
|||
}; |
|||
} |
|||
|
|||
export interface WorkspaceProject<TProjectType extends ProjectType = ProjectType.Application> { |
|||
/** |
|||
* Project type. |
|||
*/ |
|||
projectType: ProjectType; |
|||
|
|||
root: string; |
|||
sourceRoot: string; |
|||
prefix: string; |
|||
|
|||
cli?: { warnings?: Record<string, boolean> }; |
|||
|
|||
/** |
|||
* Tool options. |
|||
*/ |
|||
architect?: WorkspaceTargets<TProjectType>; |
|||
/** |
|||
* Tool options. |
|||
*/ |
|||
targets?: WorkspaceTargets<TProjectType>; |
|||
} |
|||
|
|||
export interface WorkspaceTargets<TProjectType extends ProjectType = ProjectType.Application> { |
|||
build?: TProjectType extends ProjectType.Library ? LibraryBuilderTarget : BrowserBuilderTarget; |
|||
server?: ServerBuilderTarget; |
|||
lint?: LintBuilderTarget; |
|||
test?: TestBuilderTarget; |
|||
serve?: ServeBuilderTarget; |
|||
e2e?: E2EBuilderTarget; |
|||
'app-shell'?: AppShellBuilderTarget; |
|||
'extract-i18n'?: ExtractI18nBuilderTarget; |
|||
// TODO(hans): change this any to unknown when google3 supports TypeScript 3.0.
|
|||
// tslint:disable-next-line:no-any
|
|||
[key: string]: any; |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
/** |
|||
* @license |
|||
* Copyright Google Inc. 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.io/license
|
|||
*/ |
|||
import { virtualFs, workspaces } from '@angular-devkit/core'; |
|||
import { Rule, Tree } from '@angular-devkit/schematics'; |
|||
import { ProjectType } from './workspace-models'; |
|||
|
|||
function createHost(tree: Tree): workspaces.WorkspaceHost { |
|||
return { |
|||
async readFile(path: string): Promise<string> { |
|||
const data = tree.read(path); |
|||
if (!data) { |
|||
throw new Error('File not found.'); |
|||
} |
|||
|
|||
return virtualFs.fileBufferToString(data); |
|||
}, |
|||
async writeFile(path: string, data: string): Promise<void> { |
|||
return tree.overwrite(path, data); |
|||
}, |
|||
async isDirectory(path: string): Promise<boolean> { |
|||
// approximate a directory check
|
|||
return !tree.exists(path) && tree.getDir(path).subfiles.length > 0; |
|||
}, |
|||
async isFile(path: string): Promise<boolean> { |
|||
return tree.exists(path); |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
export function updateWorkspace( |
|||
updater: (workspace: workspaces.WorkspaceDefinition) => void | PromiseLike<void>, |
|||
): Rule; |
|||
export function updateWorkspace( |
|||
workspace: workspaces.WorkspaceDefinition, |
|||
): Rule; |
|||
export function updateWorkspace( |
|||
updaterOrWorkspace: workspaces.WorkspaceDefinition |
|||
| ((workspace: workspaces.WorkspaceDefinition) => void | PromiseLike<void>), |
|||
): Rule { |
|||
return async (tree: Tree) => { |
|||
const host = createHost(tree); |
|||
|
|||
if (typeof updaterOrWorkspace === 'function') { |
|||
|
|||
const { workspace } = await workspaces.readWorkspace('/', host); |
|||
|
|||
const result = updaterOrWorkspace(workspace); |
|||
if (result !== undefined) { |
|||
await result; |
|||
} |
|||
|
|||
await workspaces.writeWorkspace(workspace, host); |
|||
} else { |
|||
await workspaces.writeWorkspace(updaterOrWorkspace, host); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
export async function getWorkspace(tree: Tree, path = '/') { |
|||
const host = createHost(tree); |
|||
|
|||
const { workspace } = await workspaces.readWorkspace(path, host); |
|||
|
|||
return workspace; |
|||
} |
|||
|
|||
/** |
|||
* Build a default project path for generating. |
|||
* @param project The project which will have its default path generated. |
|||
*/ |
|||
export function buildDefaultPath(project: workspaces.ProjectDefinition): string { |
|||
const root = project.sourceRoot ? `/${project.sourceRoot}/` : `/${project.root}/src/`; |
|||
const projectDirName = project.extensions['projectType'] === ProjectType.Application ? 'app' : 'lib'; |
|||
|
|||
return `${root}${projectDirName}`; |
|||
} |
|||
|
|||
export async function createDefaultPath(tree: Tree, projectName: string): Promise<string> { |
|||
const workspace = await getWorkspace(tree); |
|||
const project = workspace.projects.get(projectName); |
|||
if (!project) { |
|||
throw new Error('Specified project does not exist.'); |
|||
} |
|||
|
|||
return buildDefaultPath(project); |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
import { chain, schematic } from '@angular-devkit/schematics'; |
|||
import { GenerateProxySchema } from '../models'; |
|||
|
|||
export function createApisGenerator(schema: GenerateProxySchema, generated: string[]) { |
|||
return chain(generated.map(m => schematic('api', { ...schema, module: m }))); |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
import * as ts from 'typescript'; |
|||
import { findNodes } from './angular/ast-utils'; |
|||
|
|||
export function findEnvironmentExpression(source: ts.SourceFile) { |
|||
const expressions = findNodes(source, ts.isObjectLiteralExpression); |
|||
return expressions.find(expr => expr.getText().includes('production')); |
|||
} |
|||
|
|||
export function getAssignedPropertyFromObjectliteral( |
|||
expression: ts.ObjectLiteralExpression, |
|||
variableSelector: string[], |
|||
) { |
|||
const expressions = findNodes(expression, isBooleanStringOrNumberLiteral); |
|||
|
|||
const literal = expressions.find(node => |
|||
Boolean( |
|||
variableSelector.reduceRight( |
|||
(acc: ts.PropertyAssignment, key) => |
|||
acc?.name?.getText() === key ? acc.parent.parent : undefined, |
|||
node.parent, |
|||
), |
|||
), |
|||
); |
|||
|
|||
return literal ? literal.getText() : undefined; |
|||
} |
|||
|
|||
export function isBooleanStringOrNumberLiteral( |
|||
node: ts.Node, |
|||
): node is ts.StringLiteral | ts.NumericLiteral | ts.BooleanLiteral { |
|||
return ( |
|||
ts.isStringLiteral(node) || |
|||
ts.isNumericLiteral(node) || |
|||
node.kind === ts.SyntaxKind.TrueKeyword || |
|||
node.kind === ts.SyntaxKind.FalseKeyword |
|||
); |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
import { strings } from '@angular-devkit/core'; |
|||
import { Tree } from '@angular-devkit/schematics'; |
|||
import { PROXY_PATH } from '../constants'; |
|||
import { createFileSaver } from './file'; |
|||
|
|||
export function createProxyIndexGenerator(targetPath: string) { |
|||
return createBarrelsGenerator(targetPath + PROXY_PATH); |
|||
} |
|||
|
|||
export function createBarrelsGenerator(rootPath: string) { |
|||
return (tree: Tree) => { |
|||
generateBarrelFromPath(tree, rootPath); |
|||
return tree; |
|||
}; |
|||
} |
|||
|
|||
export function generateBarrelFromPath(tree: Tree, indexPath: string) { |
|||
const saveFile = createFileSaver(tree); |
|||
|
|||
const asterisk = collectAsteriskBarrel(tree, indexPath); |
|||
const named = collectNamedBarrel(tree, indexPath); |
|||
|
|||
if (asterisk.exports.length + named.exports.length) |
|||
saveFile(indexPath + '/index.ts', generateBarrelContent(asterisk, named)); |
|||
} |
|||
|
|||
function generateBarrelContent(asterisk: AsteriskBarrel, named: NamedBarrel): string { |
|||
const namedImports = !named.imports.length |
|||
? '' |
|||
: named.imports.join(` |
|||
`) +
|
|||
` |
|||
`;
|
|||
|
|||
const namedExports = !named.exports.length |
|||
? '' |
|||
: `export { ${named.exports.join(', ')} };
|
|||
`;
|
|||
|
|||
const asteriskExports = !asterisk.exports.length |
|||
? '' |
|||
: asterisk.exports.join(` |
|||
`) +
|
|||
` |
|||
`;
|
|||
|
|||
return namedImports + asteriskExports + namedExports; |
|||
} |
|||
|
|||
function collectNamedBarrel(tree: Tree, indexPath: string) { |
|||
const dir = tree.getDir(indexPath); |
|||
const barrel = new NamedBarrel(); |
|||
|
|||
dir.subdirs.forEach(fragment => { |
|||
const subDirPath = indexPath + '/' + fragment; |
|||
const subDir = tree.getDir(subDirPath); |
|||
let hasFiles = false; |
|||
subDir.visit(() => (hasFiles = true)); |
|||
if (!hasFiles) return; |
|||
|
|||
const namespaceFragment = strings.classify(fragment); |
|||
barrel.imports.push(`import * as ${namespaceFragment} from './${fragment}';`); |
|||
barrel.exports.push(namespaceFragment); |
|||
generateBarrelFromPath(tree, subDirPath); |
|||
}); |
|||
|
|||
barrel.imports.sort(); |
|||
barrel.exports.sort(); |
|||
|
|||
return barrel; |
|||
} |
|||
|
|||
function collectAsteriskBarrel(tree: Tree, indexPath: string) { |
|||
const dir = tree.getDir(indexPath); |
|||
const barrel = new AsteriskBarrel(); |
|||
|
|||
dir.subfiles.forEach(fragment => { |
|||
if (!fragment.endsWith('.ts') || fragment === 'index.ts') return; |
|||
|
|||
barrel.exports.push(`export * from './${fragment.replace(/\.ts$/, '')}';`); |
|||
}); |
|||
|
|||
barrel.exports.sort(); |
|||
|
|||
return barrel; |
|||
} |
|||
|
|||
abstract class Barrel { |
|||
imports: string[] = []; |
|||
exports: string[] = []; |
|||
} |
|||
|
|||
class AsteriskBarrel extends Barrel { |
|||
type = 'Asterisk' as const; |
|||
} |
|||
|
|||
class NamedBarrel extends Barrel { |
|||
type = 'Named' as const; |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
import { SchematicsException, Tree } from '@angular-devkit/schematics'; |
|||
import * as ts from 'typescript'; |
|||
import { Exception } from '../enums'; |
|||
|
|||
export function interpolate(text: string, ...params: (string | number | boolean)[]) { |
|||
params.forEach((param, i) => { |
|||
const pattern = new RegExp('{\\s*' + i + '\\s*}'); |
|||
text = text.replace(pattern, String(param)); |
|||
}); |
|||
|
|||
return text; |
|||
} |
|||
|
|||
export function isNullOrUndefined(value: any): value is null | undefined { |
|||
return value === null || value === undefined; |
|||
} |
|||
|
|||
export function readFileInTree(tree: Tree, filePath: string): ts.SourceFile { |
|||
const buffer = tree.read(filePath); |
|||
if (isNullOrUndefined(buffer)) |
|||
throw new SchematicsException(interpolate(Exception.FileNotFound, filePath)); |
|||
|
|||
const text = buffer.toString('utf-8'); |
|||
return ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true); |
|||
} |
|||
|
|||
export function removeDefaultPlaceholders<T>(oldParams: T) { |
|||
const newParams: Record<string, any> = {}; |
|||
|
|||
Object.entries(oldParams).forEach(([key, value]) => { |
|||
newParams[key] = value === '__default' ? undefined : value; |
|||
}); |
|||
|
|||
return newParams as T; |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
import { SchematicsException } from '@angular-devkit/schematics'; |
|||
import { Exception } from '../enums'; |
|||
import { Type } from '../models'; |
|||
import { interpolate } from './common'; |
|||
import { parseNamespace } from './namespace'; |
|||
const shouldQuote = require('should-quote'); |
|||
|
|||
export interface EnumGeneratorParams { |
|||
targetPath: string; |
|||
solution: string; |
|||
types: Record<string, Type>; |
|||
serviceImports: Record<string, string[]>; |
|||
modelImports: Record<string, string[]>; |
|||
} |
|||
|
|||
export function isEnumImport(path: string) { |
|||
return path.endsWith('.enum'); |
|||
} |
|||
|
|||
export function getEnumNamesFromImports(serviceImports: Record<string, string[]>) { |
|||
return Object.keys(serviceImports) |
|||
.filter(isEnumImport) |
|||
.reduce((acc: string[], path) => { |
|||
serviceImports[path].forEach(_import => acc.push(_import)); |
|||
return acc; |
|||
}, []); |
|||
} |
|||
|
|||
export function createImportRefToEnumMapper({ solution, types }: EnumGeneratorParams) { |
|||
return (ref: string) => { |
|||
const { enumNames, enumValues } = types[ref]; |
|||
if (!enumNames || !enumValues) |
|||
throw new SchematicsException(interpolate(Exception.NoTypeDefinition, ref)); |
|||
|
|||
const namespace = parseNamespace(solution, ref); |
|||
const members = enumNames!.map((key, i) => ({ |
|||
key: shouldQuote(key) ? `'${key}'` : key, |
|||
value: enumValues[i], |
|||
})); |
|||
|
|||
return { |
|||
namespace, |
|||
name: ref.split('.').pop()!, |
|||
members, |
|||
}; |
|||
}; |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
import { Tree } from '@angular-devkit/schematics'; |
|||
|
|||
export function createFileSaver(tree: Tree) { |
|||
return (filePath: string, fileContent: string) => |
|||
tree.exists(filePath) |
|||
? tree.overwrite(filePath, fileContent) |
|||
: tree.create(filePath, fileContent); |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
import { Generic } from '../models'; |
|||
|
|||
export class GenericsCollector { |
|||
private _generics: Generic[] = []; |
|||
get generics() { |
|||
return this._generics; |
|||
} |
|||
|
|||
apply = (value: string, index: number) => { |
|||
const generic = this.get(index); |
|||
if (generic) { |
|||
if (!generic.type) generic.setType(value); |
|||
return value + generic.default; |
|||
} |
|||
|
|||
return value; |
|||
}; |
|||
|
|||
constructor(private getTypeIdentifier = (type: string) => type) {} |
|||
|
|||
private createGeneric(type: string, ref: string, defaultValue: string) { |
|||
const _default = this.getTypeIdentifier(defaultValue); |
|||
const refs = [generateRefWithPlaceholders(ref)]; |
|||
const generic = new Generic({ type, default: _default, refs }); |
|||
return generic; |
|||
} |
|||
|
|||
private register(index: number, generic: Generic) { |
|||
const existing = this.get(index); |
|||
if (existing) { |
|||
existing.setDefault(generic.default); |
|||
existing.setType(generic.type); |
|||
} else this.set(index, generic); |
|||
} |
|||
|
|||
collect(generics: string[], genericArguments: string[]) { |
|||
generics.forEach((ref, i) => { |
|||
const generic = this.createGeneric( |
|||
genericArguments[i], |
|||
ref, |
|||
genericArguments.includes(ref) ? '' : ref, |
|||
); |
|||
this.register(i, generic); |
|||
}); |
|||
} |
|||
|
|||
get(index: number) { |
|||
return this.generics[index]; |
|||
} |
|||
|
|||
set(index: number, value: Generic) { |
|||
this.generics[index] = value; |
|||
} |
|||
|
|||
reset() { |
|||
this._generics = []; |
|||
} |
|||
} |
|||
|
|||
export function generateRefWithPlaceholders(sourceType: string) { |
|||
let { identifier, generics } = extractGenerics(sourceType); |
|||
|
|||
identifier = identifier; |
|||
generics = generics.map((_, i) => `T${i}`); |
|||
|
|||
return generics.length ? `${identifier}<${generics}>` : identifier; |
|||
} |
|||
|
|||
export function extractSimpleGenerics(sourceType: string) { |
|||
const { identifier, generics } = extractGenerics(sourceType); |
|||
|
|||
return { |
|||
identifier: getLastSegment(identifier), |
|||
generics: generics.map(getLastSegment), |
|||
}; |
|||
} |
|||
|
|||
export function extractGenerics(sourceType: string) { |
|||
const regex = /(?<identifier>[^<]+)(<(?<generics>.+)>)?/g; |
|||
const { identifier = '', generics = '' } = regex.exec(sourceType)?.groups ?? {}; |
|||
|
|||
return { |
|||
identifier, |
|||
generics: generics.split(/,\s*/).filter(Boolean), |
|||
}; |
|||
} |
|||
|
|||
function getLastSegment(str: string) { |
|||
return str.split('.').pop()!; |
|||
} |
|||
|
|||
export function replacePlaceholdersWithGenerics( |
|||
type: string, |
|||
generics: string[], |
|||
genericsCollector: GenericsCollector, |
|||
) { |
|||
return generics |
|||
.map(genericsCollector.apply) |
|||
.reduce((acc, v, i) => acc.replace(new RegExp(`([<, ])T${i}([,>])`, 'g'), `$1${v}$2`), type); |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
import { Import } from '../models'; |
|||
|
|||
export function sortImports(imports: Import[]) { |
|||
imports.sort((a, b) => |
|||
removeRelative(a) > removeRelative(b) ? 1 : a.keyword > b.keyword ? 1 : -1, |
|||
); |
|||
} |
|||
|
|||
export function removeRelative(importDef: Import) { |
|||
return importDef.path.replace(/\.\.\//g, ''); |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
export * from './angular'; |
|||
export * from './api'; |
|||
export * from './ast'; |
|||
export * from './barrel'; |
|||
export * from './common'; |
|||
export * from './enum'; |
|||
export * from './file'; |
|||
export * from './generics'; |
|||
export * from './import'; |
|||
export * from './model'; |
|||
export * from './namespace'; |
|||
export * from './path'; |
|||
export * from './rule'; |
|||
export * from './service'; |
|||
export * from './source'; |
|||
export * from './text'; |
|||
export * from './tree'; |
|||
export * from './type'; |
|||
export * from './workspace'; |
|||
@ -0,0 +1,177 @@ |
|||
import { VOLO_REGEX } from '../constants'; |
|||
import { Interface, Model, Property, PropertyDef, Type, TypeWithEnum } from '../models'; |
|||
import { |
|||
extractGenerics, |
|||
generateRefWithPlaceholders, |
|||
GenericsCollector, |
|||
replacePlaceholdersWithGenerics, |
|||
} from './generics'; |
|||
import { parseNamespace } from './namespace'; |
|||
import { relativePathToModel } from './path'; |
|||
import { camel } from './text'; |
|||
import { parseGenerics } from './tree'; |
|||
import { |
|||
createTypeParser, |
|||
createTypeSimplifier, |
|||
createTypesToImportsReducer, |
|||
extendsSelf, |
|||
removeTypeModifiers, |
|||
} from './type'; |
|||
const shouldQuote = require('should-quote'); |
|||
|
|||
export interface ModelGeneratorParams { |
|||
targetPath: string; |
|||
solution: string; |
|||
types: Record<string, Type>; |
|||
serviceImports: Record<string, string[]>; |
|||
modelImports: Record<string, string[]>; |
|||
} |
|||
|
|||
export function createImportRefsToModelReducer(params: ModelGeneratorParams) { |
|||
const reduceImportRefsToInterfaces = createImportRefToInterfaceReducerCreator(params); |
|||
const createRefToImportReducer = createRefToImportReducerCreator(params); |
|||
const { solution, types } = params; |
|||
|
|||
return (models: Model[], importRefs: string[]) => { |
|||
const enums: string[] = []; |
|||
const interfaces = importRefs.reduce(reduceImportRefsToInterfaces, []); |
|||
|
|||
sortInterfaces(interfaces); |
|||
|
|||
interfaces.forEach(_interface => { |
|||
if (VOLO_REGEX.test(_interface.ref)) return; |
|||
|
|||
if (types[_interface.ref]!.isEnum) { |
|||
if (!enums.includes(_interface.ref)) enums.push(_interface.ref); |
|||
return; |
|||
} |
|||
|
|||
const index = models.findIndex(m => m.namespace === _interface.namespace); |
|||
if (index > -1) { |
|||
if (models[index].interfaces.some(i => i.identifier === _interface.identifier)) return; |
|||
|
|||
models[index].interfaces.push(_interface); |
|||
} else { |
|||
const { namespace } = _interface; |
|||
|
|||
models.push( |
|||
new Model({ |
|||
interfaces: [_interface], |
|||
namespace, |
|||
path: relativePathToModel(namespace, namespace), |
|||
}), |
|||
); |
|||
} |
|||
}); |
|||
|
|||
models.forEach(model => { |
|||
const toBeImported: TypeWithEnum[] = []; |
|||
|
|||
model.interfaces.forEach(_interface => { |
|||
const { baseType } = types[_interface.ref]; |
|||
|
|||
if (baseType && parseNamespace(solution, baseType) !== model.namespace) |
|||
toBeImported.push({ |
|||
type: baseType.split('<')[0], |
|||
isEnum: false, |
|||
}); |
|||
|
|||
[..._interface.properties, ..._interface.generics].forEach(prop => { |
|||
prop.refs.forEach(ref => { |
|||
const propType = types[ref]; |
|||
if (!propType) return; |
|||
if (propType.isEnum) toBeImported.push({ type: ref, isEnum: true }); |
|||
else if (parseNamespace(solution, ref) !== model.namespace) |
|||
toBeImported.push({ type: ref, isEnum: false }); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
if (!toBeImported.length) return; |
|||
|
|||
const reduceRefToImport = createRefToImportReducer(model.namespace); |
|||
reduceRefToImport(model.imports, toBeImported); |
|||
}); |
|||
|
|||
return models; |
|||
}; |
|||
} |
|||
|
|||
function sortInterfaces(interfaces: Interface[]) { |
|||
interfaces.sort((a, b) => (a.identifier > b.identifier ? 1 : -1)); |
|||
} |
|||
|
|||
export function createImportRefToInterfaceReducerCreator(params: ModelGeneratorParams) { |
|||
const { solution, types } = params; |
|||
const parseType = createTypeParser(removeTypeModifiers); |
|||
const simplifyType = createTypeSimplifier(); |
|||
const getIdentifier = (type: string) => removeTypeModifiers(simplifyType(type)); |
|||
const genericsCollector = new GenericsCollector(getIdentifier); |
|||
|
|||
return reduceRefsToInterfaces; |
|||
|
|||
function reduceRefsToInterfaces(interfaces: Interface[], ref: string): Interface[] { |
|||
const typeDef = types[ref]; |
|||
if (!typeDef) return interfaces; |
|||
|
|||
const namespace = parseNamespace(solution, ref); |
|||
|
|||
let { baseType: base, genericArguments } = typeDef; |
|||
genericArguments = genericArguments || []; |
|||
let identifier = getIdentifier(ref); |
|||
identifier = replacePlaceholdersWithGenerics(identifier, genericArguments, genericsCollector); |
|||
|
|||
if (base) { |
|||
if (extendsSelf(ref, base)) { |
|||
genericsCollector.collect(extractGenerics(base).generics, genericArguments); |
|||
return reduceRefsToInterfaces(interfaces, generateRefWithPlaceholders(base)); |
|||
} else { |
|||
base = getIdentifier(base); |
|||
} |
|||
} |
|||
|
|||
const { generics } = genericsCollector; |
|||
const _interface = new Interface({ identifier, base, namespace, ref, generics }); |
|||
genericsCollector.reset(); |
|||
|
|||
typeDef.properties?.forEach(prop => { |
|||
let name = prop.jsonName || camel(prop.name); |
|||
name = shouldQuote(name) ? `'${name}'` : name; |
|||
const type = simplifyType(prop.typeSimple); |
|||
const refs = parseType(prop.type).reduce( |
|||
(acc: string[], r) => acc.concat(parseGenerics(r).toGenerics()), |
|||
[], |
|||
); |
|||
const property = new Property({ name, type, refs }); |
|||
property.setOptional(isOptionalProperty(prop)); |
|||
|
|||
_interface.properties.push(property); |
|||
}); |
|||
|
|||
interfaces.push(_interface); |
|||
|
|||
return [..._interface.properties, ..._interface.generics] |
|||
.reduce<string[]>((refs, prop) => { |
|||
prop.refs.forEach(type => { |
|||
if (types[type]?.isEnum) return; |
|||
if (interfaces.some(i => i.ref === type)) return; |
|||
refs.push(type); |
|||
}); |
|||
|
|||
return refs; |
|||
}, []) |
|||
.concat(base ? parseGenerics(typeDef.baseType!).toGenerics() : []) |
|||
.reduce(reduceRefsToInterfaces, interfaces); |
|||
} |
|||
} |
|||
|
|||
export function createRefToImportReducerCreator(params: ModelGeneratorParams) { |
|||
const { solution } = params; |
|||
return (namespace: string) => createTypesToImportsReducer(solution, namespace); |
|||
} |
|||
|
|||
function isOptionalProperty(prop: PropertyDef) { |
|||
return ( |
|||
prop.typeSimple.endsWith('?') || (prop.typeSimple === 'string' && prop.isRequired === false) |
|||
); |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
import { createTypeParser, removeGenerics } from './type'; |
|||
|
|||
export function parseNamespace(solution: string, type: string) { |
|||
const parseType = createTypeParser(removeGenerics); |
|||
let namespace = parseType(type)[0] |
|||
.split('.') |
|||
.slice(0, -1) |
|||
.join('.'); |
|||
|
|||
if (solution === namespace) return ''; |
|||
|
|||
solution.split('.').reduceRight((acc, part) => { |
|||
acc = `${part}\\.${acc}`; |
|||
const regex = new RegExp(`^${acc}(Controllers\\.)?`); |
|||
namespace = namespace.replace(regex, ''); |
|||
return acc; |
|||
}, ''); |
|||
|
|||
return namespace; |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
import { strings } from '@angular-devkit/core'; |
|||
import { kebab } from './text'; |
|||
|
|||
export function relativePathToEnum(namespace: string, enumNamespace: string, enumName: string) { |
|||
const path = calculateRelativePath(namespace, enumNamespace); |
|||
return path + `/${kebab(enumName)}.enum`; |
|||
} |
|||
|
|||
export function relativePathToModel(namespace: string, modelNamespace: string) { |
|||
const path = calculateRelativePath(namespace, modelNamespace); |
|||
return path + '/models'; |
|||
} |
|||
|
|||
function calculateRelativePath(ns1: string, ns2: string) { |
|||
if (ns1 === ns2) return '.'; |
|||
|
|||
const parts1 = ns1 ? ns1.split('.') : []; |
|||
const parts2 = ns2 ? ns2.split('.') : []; |
|||
|
|||
while (parts1.length && parts2.length) { |
|||
if (parts1[0] !== parts2[0]) break; |
|||
|
|||
parts1.shift(); |
|||
parts2.shift(); |
|||
} |
|||
|
|||
const up = '../'.repeat(parts1.length) || '.'; |
|||
const down = parts2.reduce((acc, p) => acc + '/' + strings.dasherize(p), ''); |
|||
|
|||
return removeTrailingSlash(removeDoubleSlash(up + down)); |
|||
} |
|||
|
|||
function removeDoubleSlash(path: string) { |
|||
return path.replace(/\/{2,}/g, '/'); |
|||
} |
|||
|
|||
function removeTrailingSlash(path: string) { |
|||
return path.replace(/\/+$/, ''); |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
import { |
|||
apply, |
|||
callRule, |
|||
forEach, |
|||
MergeStrategy, |
|||
mergeWith, |
|||
Rule, |
|||
SchematicContext, |
|||
Source, |
|||
Tree, |
|||
} from '@angular-devkit/schematics'; |
|||
|
|||
export function applyWithOverwrite(source: Source, rules: Rule[]): Rule { |
|||
return (tree: Tree, _context: SchematicContext) => { |
|||
const rule = mergeWith(apply(source, [...rules, overwriteFileIfExists(tree)])); |
|||
|
|||
return rule(tree, _context); |
|||
}; |
|||
} |
|||
|
|||
export function mergeAndAllowDelete(host: Tree, rule: Rule) { |
|||
return async (tree: Tree, context: SchematicContext) => { |
|||
const nextTree = await callRule(rule, tree, context).toPromise(); |
|||
host.merge(nextTree, MergeStrategy.AllowDeleteConflict); |
|||
}; |
|||
} |
|||
|
|||
export function overwriteFileIfExists(tree: Tree): Rule { |
|||
return forEach(fileEntry => { |
|||
if (!tree.exists(fileEntry.path)) return fileEntry; |
|||
|
|||
tree.overwrite(fileEntry.path, fileEntry.content); |
|||
return null; |
|||
}); |
|||
} |
|||
@ -0,0 +1,122 @@ |
|||
import { |
|||
Action, |
|||
Body, |
|||
Controller, |
|||
Import, |
|||
Method, |
|||
Property, |
|||
Service, |
|||
ServiceGeneratorParams, |
|||
Signature, |
|||
Type, |
|||
TypeWithEnum, |
|||
} from '../models'; |
|||
import { sortImports } from './import'; |
|||
import { parseNamespace } from './namespace'; |
|||
import { parseGenerics } from './tree'; |
|||
import { |
|||
createTypeAdapter, |
|||
createTypeParser, |
|||
createTypesToImportsReducer, |
|||
removeTypeModifiers, |
|||
} from './type'; |
|||
|
|||
export function serializeParameters(parameters: Property[]) { |
|||
return parameters.map(p => p.name + p.optional + ': ' + p.type + p.default, '').join(', '); |
|||
} |
|||
|
|||
export function createControllerToServiceMapper({ |
|||
solution, |
|||
types, |
|||
apiName, |
|||
}: ServiceGeneratorParams) { |
|||
const mapActionToMethod = createActionToMethodMapper(); |
|||
|
|||
return (controller: Controller) => { |
|||
const name = controller.controllerName; |
|||
const namespace = parseNamespace(solution, controller.type); |
|||
const actions = Object.values(controller.actions); |
|||
const imports = actions.reduce(createActionToImportsReducer(solution, types, namespace), []); |
|||
imports.push(new Import({ path: '@abp/ng.core', specifiers: ['RestService'] })); |
|||
imports.push(new Import({ path: '@angular/core', specifiers: ['Injectable'] })); |
|||
sortImports(imports); |
|||
const methods = actions.map(mapActionToMethod); |
|||
sortMethods(methods); |
|||
return new Service({ apiName, imports, methods, name, namespace }); |
|||
}; |
|||
} |
|||
|
|||
function sortMethods(methods: Method[]) { |
|||
methods.sort((a, b) => (a.signature.name > b.signature.name ? 1 : -1)); |
|||
} |
|||
|
|||
export function createActionToMethodMapper() { |
|||
const mapActionToBody = createActionToBodyMapper(); |
|||
const mapActionToSignature = createActionToSignatureMapper(); |
|||
|
|||
return (action: Action) => { |
|||
const body = mapActionToBody(action); |
|||
const signature = mapActionToSignature(action); |
|||
return new Method({ body, signature }); |
|||
}; |
|||
} |
|||
|
|||
export function createActionToBodyMapper() { |
|||
const adaptType = createTypeAdapter(); |
|||
|
|||
return ({ httpMethod, parameters, returnValue, url }: Action) => { |
|||
const responseType = adaptType(returnValue.typeSimple); |
|||
const body = new Body({ method: httpMethod, responseType, url }); |
|||
|
|||
parameters.forEach(body.registerActionParameter); |
|||
|
|||
return body; |
|||
}; |
|||
} |
|||
|
|||
export function createActionToSignatureMapper() { |
|||
const adaptType = createTypeAdapter(); |
|||
|
|||
return (action: Action) => { |
|||
const signature = new Signature({ name: getMethodNameFromAction(action) }); |
|||
|
|||
signature.parameters = action.parametersOnMethod.map(p => { |
|||
const type = adaptType(p.typeSimple); |
|||
const parameter = new Property({ name: p.name, type }); |
|||
parameter.setDefault(p.defaultValue); |
|||
parameter.setOptional(p.isOptional); |
|||
return parameter; |
|||
}); |
|||
|
|||
return signature; |
|||
}; |
|||
} |
|||
|
|||
function getMethodNameFromAction(action: Action): string { |
|||
return action.uniqueName.split('Async')[0]; |
|||
} |
|||
|
|||
function createActionToImportsReducer( |
|||
solution: string, |
|||
types: Record<string, Type>, |
|||
namespace: string, |
|||
) { |
|||
const mapTypesToImports = createTypesToImportsReducer(solution, namespace); |
|||
const parseType = createTypeParser(removeTypeModifiers); |
|||
|
|||
return (imports: Import[], { parametersOnMethod, returnValue }: Action) => |
|||
mapTypesToImports( |
|||
imports, |
|||
[returnValue, ...parametersOnMethod].reduce((acc: TypeWithEnum[], param) => { |
|||
parseType(param.type).forEach(paramType => |
|||
parseGenerics(paramType) |
|||
.toGenerics() |
|||
.forEach(type => { |
|||
if (types[type]) acc.push({ type, isEnum: types[type].isEnum }); |
|||
}), |
|||
); |
|||
|
|||
return acc; |
|||
}, []), |
|||
); |
|||
} |
|||
@ -0,0 +1,190 @@ |
|||
import { SchematicsException, Tree } from '@angular-devkit/schematics'; |
|||
import got from 'got'; |
|||
import { |
|||
API_DEFINITION_ENDPOINT, |
|||
PROXY_CONFIG_PATH, |
|||
PROXY_PATH, |
|||
PROXY_WARNING, |
|||
PROXY_WARNING_PATH, |
|||
} from '../constants'; |
|||
import { Exception } from '../enums'; |
|||
import { ApiDefinition, GenerateProxySchema, Project, ProxyConfig, WriteOp } from '../models'; |
|||
import { getAssignedPropertyFromObjectliteral } from './ast'; |
|||
import { interpolate } from './common'; |
|||
import { readEnvironment, resolveProject } from './workspace'; |
|||
|
|||
export function createApiDefinitionGetter(params: GenerateProxySchema) { |
|||
const apiName = params['api-name'] || 'default'; |
|||
|
|||
return async (host: Tree) => { |
|||
const source = await resolveProject(host, params.source!); |
|||
const sourceUrl = getSourceUrl(host, source, apiName); |
|||
return await getApiDefinition(sourceUrl); |
|||
}; |
|||
} |
|||
|
|||
async function getApiDefinition(sourceUrl: string) { |
|||
const url = sourceUrl + API_DEFINITION_ENDPOINT; |
|||
let body: ApiDefinition; |
|||
|
|||
try { |
|||
({ body } = await got(url, { |
|||
responseType: 'json', |
|||
searchParams: { includeTypes: true }, |
|||
https: { rejectUnauthorized: false }, |
|||
})); |
|||
} catch ({ response }) { |
|||
// handle redirects
|
|||
if (!response?.body || response.statusCode >= 400) |
|||
throw new SchematicsException(interpolate(Exception.NoApi, url)); |
|||
|
|||
body = response.body; |
|||
} |
|||
|
|||
return body; |
|||
} |
|||
|
|||
export function createRootNamespaceGetter(params: GenerateProxySchema) { |
|||
const apiName = params['api-name'] || 'default'; |
|||
|
|||
return async (tree: Tree) => { |
|||
const project = await resolveProject(tree, params.source!); |
|||
const environmentExpr = readEnvironment(tree, project.definition); |
|||
|
|||
if (!environmentExpr) |
|||
throw new SchematicsException(interpolate(Exception.NoEnvironment, project.name)); |
|||
|
|||
let assignment = getAssignedPropertyFromObjectliteral(environmentExpr, [ |
|||
'apis', |
|||
apiName, |
|||
'rootNamespace', |
|||
]); |
|||
|
|||
if (!assignment) |
|||
assignment = getAssignedPropertyFromObjectliteral(environmentExpr, [ |
|||
'apis', |
|||
'default', |
|||
'rootNamespace', |
|||
]); |
|||
|
|||
if (!assignment) |
|||
throw new SchematicsException(interpolate(Exception.NoRootNamespace, project.name, apiName)); |
|||
|
|||
return assignment.replace(/[`'"]/g, ''); |
|||
}; |
|||
} |
|||
|
|||
export function getSourceUrl(tree: Tree, project: Project, apiName: string) { |
|||
const environmentExpr = readEnvironment(tree, project.definition); |
|||
|
|||
if (!environmentExpr) |
|||
throw new SchematicsException(interpolate(Exception.NoEnvironment, project.name)); |
|||
|
|||
let assignment = getAssignedPropertyFromObjectliteral(environmentExpr, ['apis', apiName, 'url']); |
|||
|
|||
if (!assignment) |
|||
assignment = getAssignedPropertyFromObjectliteral(environmentExpr, ['apis', 'default', 'url']); |
|||
|
|||
if (!assignment) |
|||
throw new SchematicsException(interpolate(Exception.NoApiUrl, project.name, apiName)); |
|||
|
|||
return assignment.replace(/[`'"]/g, ''); |
|||
} |
|||
|
|||
export function createProxyConfigReader(targetPath: string) { |
|||
targetPath += PROXY_CONFIG_PATH; |
|||
|
|||
return (tree: Tree) => { |
|||
try { |
|||
const buffer = tree.read(targetPath); |
|||
return JSON.parse(buffer!.toString()) as ProxyConfig; |
|||
} catch (_) {} |
|||
|
|||
throw new SchematicsException(interpolate(Exception.NoProxyConfig, targetPath)); |
|||
}; |
|||
} |
|||
|
|||
export function createProxyClearer(targetPath: string) { |
|||
targetPath += PROXY_PATH; |
|||
const proxyIndexPath = `${targetPath}/index.ts`; |
|||
|
|||
return (tree: Tree) => { |
|||
try { |
|||
tree.getDir(targetPath).subdirs.forEach(dirName => { |
|||
const dirPath = `${targetPath}/${dirName}`; |
|||
tree.getDir(dirPath).visit(filePath => tree.delete(filePath)); |
|||
tree.delete(dirPath); |
|||
}); |
|||
|
|||
if (tree.exists(proxyIndexPath)) tree.delete(proxyIndexPath); |
|||
|
|||
return tree; |
|||
} catch (_) { |
|||
throw new SchematicsException(interpolate(Exception.DirRemoveFailed, targetPath)); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
export function createProxyWarningSaver(targetPath: string) { |
|||
targetPath += PROXY_WARNING_PATH; |
|||
const createFileWriter = createFileWriterCreator(targetPath); |
|||
|
|||
return (tree: Tree) => { |
|||
const op = tree.exists(targetPath) ? 'overwrite' : 'create'; |
|||
const writeWarningMD = createFileWriter(op, PROXY_WARNING); |
|||
writeWarningMD(tree); |
|||
|
|||
return tree; |
|||
}; |
|||
} |
|||
|
|||
export function createProxyConfigSaver(apiDefinition: ApiDefinition, targetPath: string) { |
|||
const createProxyConfigJson = createProxyConfigJsonCreator(apiDefinition); |
|||
const readPreviousConfig = createProxyConfigReader(targetPath); |
|||
const createProxyConfigWriter = createProxyConfigWriterCreator(targetPath); |
|||
targetPath += PROXY_CONFIG_PATH; |
|||
|
|||
return (tree: Tree) => { |
|||
const generated: string[] = []; |
|||
let op: WriteOp = 'create'; |
|||
|
|||
if (tree.exists(targetPath)) { |
|||
op = 'overwrite'; |
|||
|
|||
try { |
|||
readPreviousConfig(tree).generated.forEach(m => generated.push(m)); |
|||
} catch (_) {} |
|||
} |
|||
|
|||
const json = createProxyConfigJson(generated); |
|||
const writeProxyConfig = createProxyConfigWriter(op, json); |
|||
writeProxyConfig(tree); |
|||
|
|||
return tree; |
|||
}; |
|||
} |
|||
|
|||
export function createProxyConfigWriterCreator(targetPath: string) { |
|||
targetPath += PROXY_CONFIG_PATH; |
|||
|
|||
return createFileWriterCreator(targetPath); |
|||
} |
|||
|
|||
export function createFileWriterCreator(targetPath: string) { |
|||
return (op: WriteOp, data: string) => (tree: Tree) => { |
|||
try { |
|||
tree[op](targetPath, data); |
|||
return tree; |
|||
} catch (_) {} |
|||
|
|||
throw new SchematicsException(interpolate(Exception.FileWriteFailed, targetPath)); |
|||
}; |
|||
} |
|||
|
|||
export function createProxyConfigJsonCreator(apiDefinition: ApiDefinition) { |
|||
return (generated: string[]) => generateProxyConfigJson({ generated, ...apiDefinition }); |
|||
} |
|||
|
|||
export function generateProxyConfigJson(proxyConfig: ProxyConfig) { |
|||
return JSON.stringify(proxyConfig, null, 2); |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
import { strings } from '@angular-devkit/core'; |
|||
|
|||
export const lower = (text: string) => text.toLowerCase(); |
|||
export const upper = (text: string) => text.toUpperCase(); |
|||
export const camel = (text: string) => toCamelCase(_(text)); |
|||
export const pascal = (text: string) => strings.classify(_(text)); |
|||
export const kebab = (text: string) => strings.dasherize(_(text)); |
|||
export const snake = (text: string) => strings.underscore(_(text)); |
|||
export const macro = (text: string) => upper(snake(text)); |
|||
export const dir = (text: string) => |
|||
strings.dasherize(text.replace(/\./g, '/').replace(/\/\//g, '/')); |
|||
|
|||
export const quote = (value: number | string) => |
|||
typeof value === 'string' ? `'${value.replace(/'/g, '\\\'')}'` : value; |
|||
|
|||
function _(text: string): string { |
|||
return text.replace(/\./g, '_'); |
|||
} |
|||
|
|||
// https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/StringUtils.cs#L155
|
|||
function toCamelCase(str: string) { |
|||
if (!str || !isUpperCase(str[0])) return str; |
|||
|
|||
const chars = str.split(''); |
|||
const { length } = chars; |
|||
|
|||
for (let i = 0; i < length; i++) { |
|||
if (i === 1 && !isUpperCase(chars[i])) break; |
|||
|
|||
const hasNext = i + 1 < length; |
|||
|
|||
if (i > 0 && hasNext && !isUpperCase(chars[i + 1])) { |
|||
if (isSeparator(chars[i + 1])) { |
|||
chars[i] = toLowerCase(chars[i]); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
chars[i] = toLowerCase(chars[i]); |
|||
} |
|||
|
|||
return chars.join(''); |
|||
} |
|||
|
|||
function isSeparator(str = '') { |
|||
return /[\s\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]+/.test(str); |
|||
} |
|||
|
|||
function isUpperCase(str = '') { |
|||
return /[A-Z]+/.test(str); |
|||
} |
|||
|
|||
function toLowerCase(str = '') { |
|||
return str.toLowerCase(); |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
export class TypeNode { |
|||
children: TypeNode[] = []; |
|||
|
|||
index = 0; |
|||
|
|||
constructor( |
|||
public data: string, |
|||
public parent: TypeNode | null, |
|||
public mapperFn = (node: TypeNode) => node.data, |
|||
) {} |
|||
|
|||
toGenerics(): string[] { |
|||
const generics = this.children.length ? `<${this.children.map(n => `T${n.index}`)}>` : ''; |
|||
return [this.data + generics].concat( |
|||
this.children.reduce((acc: string[], node) => acc.concat(node.toGenerics()), []), |
|||
); |
|||
} |
|||
|
|||
toString() { |
|||
const self = this.mapperFn(this); |
|||
|
|||
if (!self) return ''; |
|||
|
|||
const representation = self + this.children.filter(String).join(', '); |
|||
|
|||
if (!this.parent) return representation; |
|||
|
|||
const siblings = this.parent.children; |
|||
|
|||
return ( |
|||
(siblings[0] === this ? '<' : '') + |
|||
representation + |
|||
(siblings[siblings.length - 1] === this ? '>' : '') |
|||
); |
|||
} |
|||
|
|||
valueOf() { |
|||
return this.toString(); |
|||
} |
|||
} |
|||
|
|||
export function parseGenerics(type: string, mapperFn?: TypeNodeMapperFn) { |
|||
const [rootType, ...types] = type.split('<'); |
|||
const root = new TypeNode(rootType, null, mapperFn); |
|||
|
|||
types.reduce((parent, t) => { |
|||
const [left, right] = t.split(/>+,?\s*/); |
|||
|
|||
const leftNode = new TypeNode(left, parent, mapperFn); |
|||
leftNode.index = parent.children.length; |
|||
parent.children.push(leftNode); |
|||
parent = leftNode; |
|||
|
|||
let { length } = t.match(/>/g) || []; |
|||
while (length--) parent = parent.parent!; |
|||
|
|||
if (right) { |
|||
parent = parent.parent!; |
|||
const rightNode = new TypeNode(right, parent, mapperFn); |
|||
rightNode.index = parent.children.length; |
|||
parent.children.push(rightNode); |
|||
parent = rightNode; |
|||
} |
|||
|
|||
return parent; |
|||
}, root); |
|||
|
|||
return root; |
|||
} |
|||
|
|||
export type TypeNodeMapperFn = (node: TypeNode) => string; |
|||
@ -0,0 +1,114 @@ |
|||
import { SYSTEM_TYPES, VOLO_REGEX } from '../constants'; |
|||
import { eImportKeyword } from '../enums'; |
|||
import { Import, TypeWithEnum } from '../models'; |
|||
import { extractSimpleGenerics } from './generics'; |
|||
import { parseNamespace } from './namespace'; |
|||
import { relativePathToEnum, relativePathToModel } from './path'; |
|||
import { parseGenerics } from './tree'; |
|||
|
|||
export function createTypeSimplifier() { |
|||
const parseType = createTypeParser(t => { |
|||
let type = t.replace( |
|||
/(?<![^<, ])System\.([0-9A-Za-z.]+)/g, |
|||
(_, match) => SYSTEM_TYPES.get(match) ?? 'any', |
|||
); |
|||
|
|||
type = /any</.test(type) ? 'any' : type; |
|||
|
|||
const { identifier, generics } = extractSimpleGenerics(type); |
|||
|
|||
return generics.length ? `${identifier}<${generics.join(', ')}>` : identifier; |
|||
}); |
|||
|
|||
return (type: string) => { |
|||
const parsed = parseType(type); |
|||
const last = parsed.pop()!; |
|||
return parsed.reduceRight((record, tKey) => `Record<${tKey}, ${record}>`, last); |
|||
}; |
|||
} |
|||
|
|||
export function createTypeParser(replacerFn = (t: string) => t) { |
|||
const normalizeType = createTypeNormalizer(replacerFn); |
|||
|
|||
return (originalType: string) => flattenDictionaryTypes([], originalType).map(normalizeType); |
|||
} |
|||
|
|||
export function createTypeNormalizer(replacerFn = (t: string) => t) { |
|||
return (type: string) => { |
|||
return replacerFn(normalizeTypeAnnotations(type)); |
|||
}; |
|||
} |
|||
|
|||
export function flattenDictionaryTypes(types: string[], type: string) { |
|||
type |
|||
.replace(/[}{]/g, '') |
|||
.split(':') |
|||
.forEach(t => types.push(t)); |
|||
|
|||
return types; |
|||
} |
|||
|
|||
export function normalizeTypeAnnotations(type: string) { |
|||
return type.replace(/\[(.+)+\]/g, '$1[]').replace(/\?/g, ''); |
|||
} |
|||
|
|||
export function removeGenerics(type: string) { |
|||
return type.replace(/<.+>/g, ''); |
|||
} |
|||
|
|||
export function removeTypeModifiers(type: string) { |
|||
return type.replace(/\[\]/g, ''); |
|||
} |
|||
|
|||
export function createTypesToImportsReducer(solution: string, namespace: string) { |
|||
const mapTypeToImport = createTypeToImportMapper(solution, namespace); |
|||
|
|||
return (imports: Import[], types: TypeWithEnum[]) => { |
|||
types.forEach(({ type, isEnum }) => { |
|||
const newImport = mapTypeToImport(type, isEnum); |
|||
if (!newImport) return; |
|||
|
|||
const existingImport = imports.find( |
|||
({ keyword, path }) => keyword === newImport.keyword && path === newImport.path, |
|||
); |
|||
if (!existingImport) return imports.push(newImport); |
|||
|
|||
existingImport.refs = [...new Set([...existingImport.refs, ...newImport.refs])]; |
|||
existingImport.specifiers = [ |
|||
...new Set([...existingImport.specifiers, ...newImport.specifiers]), |
|||
].sort(); |
|||
}); |
|||
|
|||
return imports; |
|||
}; |
|||
} |
|||
|
|||
export function createTypeToImportMapper(solution: string, namespace: string) { |
|||
const adaptType = createTypeAdapter(); |
|||
const simplifyType = createTypeSimplifier(); |
|||
|
|||
return (type: string, isEnum: boolean) => { |
|||
if (!type || type.startsWith('System')) return; |
|||
|
|||
const modelNamespace = parseNamespace(solution, type); |
|||
const refs = [removeTypeModifiers(type)]; |
|||
const specifiers = [adaptType(simplifyType(refs[0]).split('<')[0])]; |
|||
const path = VOLO_REGEX.test(type) |
|||
? '@abp/ng.core' |
|||
: isEnum |
|||
? relativePathToEnum(namespace, modelNamespace, specifiers[0]) |
|||
: relativePathToModel(namespace, modelNamespace); |
|||
|
|||
return new Import({ keyword: eImportKeyword.Type, path, refs, specifiers }); |
|||
}; |
|||
} |
|||
|
|||
export function createTypeAdapter() { |
|||
const simplifyType = createTypeSimplifier(); |
|||
return (type: string) => parseGenerics(type, node => simplifyType(node.data)).toString(); |
|||
} |
|||
|
|||
// naming here is depictive only
|
|||
export function extendsSelf(type: string, base: string) { |
|||
return removeGenerics(base) === removeGenerics(type); |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
import { strings, workspaces } from '@angular-devkit/core'; |
|||
import { SchematicsException, Tree } from '@angular-devkit/schematics'; |
|||
import { Exception } from '../enums'; |
|||
import { Project } from '../models'; |
|||
import { getWorkspace, getWorkspaceSchema, ProjectType, WorkspaceSchema } from './angular'; |
|||
import { findEnvironmentExpression } from './ast'; |
|||
import { readFileInTree } from './common'; |
|||
|
|||
export function isLibrary(project: workspaces.ProjectDefinition): boolean { |
|||
return project.extensions['projectType'] === ProjectType.Library; |
|||
} |
|||
|
|||
export function readEnvironment(tree: Tree, project: workspaces.ProjectDefinition) { |
|||
if (isLibrary(project)) return undefined; |
|||
|
|||
const srcPath = project.sourceRoot || `${project.root}/src`; |
|||
const envPath = srcPath + '/environments/environment.ts'; |
|||
const source = readFileInTree(tree, envPath); |
|||
return findEnvironmentExpression(source); |
|||
} |
|||
|
|||
export function readWorkspaceSchema(tree: Tree) { |
|||
if (!tree.exists('/angular.json')) throw new SchematicsException(Exception.NoWorkspace); |
|||
|
|||
let workspaceSchema: WorkspaceSchema; |
|||
|
|||
try { |
|||
workspaceSchema = getWorkspaceSchema(tree); |
|||
} catch (_) { |
|||
throw new SchematicsException(Exception.InvalidWorkspace); |
|||
} |
|||
|
|||
return workspaceSchema; |
|||
} |
|||
|
|||
export async function resolveProject(tree: Tree, name: string): Promise<Project> { |
|||
name = name || readWorkspaceSchema(tree).defaultProject!; |
|||
const workspace = await getWorkspace(tree); |
|||
let definition: Project['definition'] | undefined; |
|||
|
|||
try { |
|||
definition = workspace.projects.get(name); |
|||
} catch (_) {} |
|||
|
|||
if (!definition) |
|||
try { |
|||
name = strings.dasherize(name); |
|||
definition = workspace.projects.get(name); |
|||
} catch (_) {} |
|||
|
|||
if (!definition) |
|||
try { |
|||
name = strings.camelize(name); |
|||
definition = workspace.projects.get(name); |
|||
} catch (_) {} |
|||
|
|||
if (!definition) |
|||
try { |
|||
name = strings.classify(name); |
|||
definition = workspace.projects.get(name); |
|||
} catch (_) {} |
|||
|
|||
if (!definition) throw new SchematicsException(Exception.NoProject); |
|||
|
|||
return { name, definition }; |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"baseUrl": "tsconfig", |
|||
"lib": ["es2018", "dom"], |
|||
"declaration": true, |
|||
"module": "commonjs", |
|||
"moduleResolution": "node", |
|||
"noEmitOnError": true, |
|||
"noFallthroughCasesInSwitch": true, |
|||
"noImplicitAny": true, |
|||
"noImplicitThis": true, |
|||
"noUnusedParameters": true, |
|||
"noUnusedLocals": true, |
|||
"rootDir": "src/", |
|||
"skipDefaultLibCheck": true, |
|||
"skipLibCheck": true, |
|||
"sourceMap": true, |
|||
"strictNullChecks": true, |
|||
"target": "es2017", |
|||
"types": ["jest", "node"] |
|||
}, |
|||
"include": ["src/**/*"], |
|||
"exclude": ["node_modules", "dist", "src/*/files/**/*", "**/*.spec.ts"] |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"extends": "./tsconfig.json", |
|||
"compilerOptions": { |
|||
"outDir": "../../dist/out-tsc", |
|||
"module": "commonjs", |
|||
"types": ["jest", "node"] |
|||
}, |
|||
"files": ["src/test-setup.ts"], |
|||
"include": ["**/*.spec.ts", "**/*.d.ts"] |
|||
} |
|||
Loading…
Reference in new issue