UNPKG

@nstudio/angular

Version:

Angular Plugin for xplat

618 lines (617 loc) 30.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generate = generate; exports.getFeatureName = getFeatureName; exports.getNxFeaturePath = getNxFeaturePath; exports.addToFeature = addToFeature; exports.isFeatureInGeneralBarrel = isFeatureInGeneralBarrel; exports.shouldTargetCoreBarrel = shouldTargetCoreBarrel; exports.adjustBarrel = adjustBarrel; exports.adjustBarrelIndex = adjustBarrelIndex; exports.adjustBarrelIndexForType = adjustBarrelIndexForType; exports.adjustModule = adjustModule; exports.adjustFeatureModule = adjustFeatureModule; exports.adjustFeatureModuleForState = adjustFeatureModuleForState; exports.adjustRouting = adjustRouting; exports.adjustSandbox = adjustSandbox; const schematics_1 = require("@angular-devkit/schematics"); const xplat_1 = require("@nstudio/xplat"); const xplat_utils_1 = require("@nstudio/xplat-utils"); const ast_1 = require("./ast"); const ts = require("typescript"); function generate(type, options) { if (!options.name) { throw new Error((0, xplat_1.generateOptionError)(type)); } let featureName = getFeatureName(options); let platforms = []; const externalChains = []; if (options.projects) { // building in projects const projects = (0, xplat_utils_1.sanitizeCommaDelimitedArg)(options.projects); for (const name of projects) { const nameParts = name.split('-'); const platPrefix = nameParts[0]; const platSuffix = nameParts[nameParts.length - 1]; if (xplat_utils_1.supportedPlatforms.includes(platPrefix) && !platforms.includes(platPrefix)) { // if project name is prefixed with supported platform and not already added platforms.push(platPrefix); } else if (xplat_utils_1.supportedPlatforms.includes(platSuffix) && !platforms.includes(platSuffix)) { // if project name is suffixed with supported platform and not already added platforms.push(platSuffix); } } } else if (options.platforms) { // building in shared code only platforms = (0, xplat_utils_1.sanitizeCommaDelimitedArg)(options.platforms); } const projectChains = []; if (options.projects) { for (const fullProjectPath of options.projects.split(',')) { const projectName = (0, xplat_utils_1.parseProjectNameFromPath)(fullProjectPath); const projectParts = projectName.split('-'); const platform = xplat_utils_1.supportedPlatforms.includes(projectParts[0]) ? projectParts[0] : projectParts.length > 1 ? projectParts[1] : projectParts[0]; let appDir = platform === 'web' ? '/app' : ''; const prefixPath = `apps/${fullProjectPath}/src${appDir}`; let featurePath; if (shouldTargetCoreBarrel(type, featureName)) { featureName = 'core'; featurePath = `${prefixPath}/${featureName}`; } else { featurePath = `${prefixPath}/features/${featureName}`; } const featureModulePath = `${featurePath}/${featureName}.module.ts`; let barrelIndex; if (type === 'state') { barrelIndex = `${featurePath}/index.ts`; } else { barrelIndex = `${featurePath}/${type}s/index.ts`; } // console.log('will adjustProject:', projectName); projectChains.push((tree, context) => { // console.log('featureModulePath:', featureModulePath); // console.log('projectName:', projectName); if (!tree.exists(featureModulePath)) { throw new Error((0, xplat_1.needFeatureModuleError)(featureModulePath, featureName, projectName, true)); } return addToFeature(platform, type, options, prefixPath, tree)(tree, context); }); if (type === 'state') { // ngrx handling projectChains.push((tree, context) => { return adjustBarrelIndexForType(type, options, barrelIndex)(tree, context); }); projectChains.push((tree, context) => { return addToFeature(platform, type, options, prefixPath, tree, '_index')(tree, context); }); projectChains.push((tree, context) => { return adjustFeatureModuleForState(options, featureModulePath)(tree, context); }); projectChains.push((tree, context) => { return (0, xplat_1.updatePackageForNgrx)(tree, `apps/${fullProjectPath}/package.json`); }); } else { projectChains.push((tree, context) => { return adjustBarrelIndex(type, options, barrelIndex)(tree, context); }); projectChains.push((tree, context) => { return addToFeature(platform, type, options, prefixPath, tree, '_index')(tree, context); }); projectChains.push((tree, context) => { return adjustFeatureModule(type, options, featureModulePath)(tree, context); }); } } } else { projectChains.push((0, schematics_1.noop)()); for (const platform of platforms) { if (xplat_utils_1.supportedPlatforms.includes(platform)) { // externalChains.push(externalSchematic(`@nstudio/${platform}-angular`, type, options, { // interactive: false // })); externalChains.push((tree, context) => { const xplatFolderName = xplat_1.XplatHelpers.getXplatFoldername(platform, 'angular'); return addToFeature(xplatFolderName, type, options, `libs/xplat/${xplatFolderName}`, tree); }); // adjust barrel externalChains.push((tree, context) => { const xplatFolderName = xplat_1.XplatHelpers.getXplatFoldername(platform, 'angular'); return adjustBarrel(type, options, `libs/xplat/${xplatFolderName}`); }); // add index barrel if needed externalChains.push((tree, context) => { const xplatFolderName = xplat_1.XplatHelpers.getXplatFoldername(platform, 'angular'); return options.needsIndex ? addToFeature(xplatFolderName, type, options, `libs/xplat/${xplatFolderName}`, tree, '_index')(tree, context) : (0, schematics_1.noop)()(tree, context); }); // adjust feature module metadata if needed externalChains.push((tree, context) => { const xplatFolderName = xplat_1.XplatHelpers.getXplatFoldername(platform, 'angular'); return adjustModule(tree, type, options, `libs/xplat/${xplatFolderName}`); }); } else { throw new Error((0, xplat_1.unsupportedPlatformError)(platform)); } } } return (0, schematics_1.chain)([ (0, xplat_utils_1.prerun)(), (tree, context) => // for entire workspace usage // no projects and no specific platforms specified !options.projects && platforms.length === 0 ? addToFeature('', type, options, 'libs/xplat', tree)(tree, context) : (0, schematics_1.noop)()(tree, context), // adjust libs barrel (tree, context) => !options.projects && platforms.length === 0 ? adjustBarrel(type, options, 'libs/xplat')(tree, context) : (0, schematics_1.noop)()(tree, context), // add index barrel if needed (tree, context) => options.needsIndex ? addToFeature('', type, options, 'libs/xplat', tree, '_index')(tree, context) : (0, schematics_1.noop)()(tree, context), // adjust feature module metadata if needed (tree, context) => !options.projects && platforms.length === 0 ? adjustModule(tree, type, options, 'libs/xplat')(tree, context) : (0, schematics_1.noop)()(tree, context), // project handling (tree, context) => (0, schematics_1.chain)(projectChains), (tree, context) => (0, schematics_1.chain)(externalChains), // dependency updates (tree, context) => !options.projects && type === 'state' ? // ensure ngrx dependencies are added to root package (0, xplat_1.updatePackageForNgrx)(tree) : (0, schematics_1.noop)()(tree, context), ]); } function getFeatureName(options) { let featureName; const isNxLib = xplat_1.XplatHelpers.isFeatureNxLib(options.feature); if (options.feature) { if (isNxLib) { // use lib name "as-is" featureName = options.feature.toLowerCase(); } else { // otherwise, user is wanting to target a feature nested in a folder (within features) const dirParts = options.feature.split('/'); if (dirParts.length) { featureName = dirParts.pop().toLowerCase(); } } } if (!featureName) { if (options.projects) { // default to shared barrel featureName = 'shared'; } else { // default to ui barrel featureName = 'ui'; } } return featureName; } function getNxFeaturePath(tree, featureName) { const tsConfig = (0, xplat_utils_1.getJsonFromFile)(tree, 'tsconfig.base.json'); if (tsConfig) { if (tsConfig.compilerOptions && tsConfig.compilerOptions.paths && tsConfig.compilerOptions.paths[featureName]) { let libPath = tsConfig.compilerOptions.paths[featureName][0]; return libPath.replace('index.ts', 'lib'); } else { throw new Error(`No lib barrel path found in tsconfig.base.json matching "${featureName}"`); } } else { throw new Error('Workspace must have tsconfig.base.json.'); } return null; } function addToFeature(xplatFolderName, type, options, prefixPath, tree, extra = '', forSubFolder) { let featureName = getFeatureName(options); const isNxLib = xplat_1.XplatHelpers.isFeatureNxLib(featureName); let directory = ''; let relativeDirectory = ''; if (!isNxLib && options.feature) { const dirParts = options.feature.split('/'); if (dirParts.length) { dirParts.pop(); directory = dirParts.join('/'); const relative = []; dirParts.forEach(() => relative.push('..')); relativeDirectory = relative.join('/') + '/'; } } options.needsIndex = false; // reset const srcSubFolderPath = options.projects ? '' : '/src/lib'; let featurePath; // support targeting Nx libs with feature argument using the lib barrel (ie, @scope/mylib) if (isNxLib) { featurePath = getNxFeaturePath(tree, featureName); } else if (shouldTargetCoreBarrel(type, featureName)) { // services and/or state should never be generated in shared or ui features // therefore place in core (since they are service level) featureName = 'core'; featurePath = `${prefixPath}/${featureName}${srcSubFolderPath}`; } else { featurePath = `${prefixPath}/features${srcSubFolderPath}/${directory ? directory + '/' : ''}${featureName}`; } const featureModulePath = `${featurePath}/${featureName}.module.ts`; let moveTo; if (extra === '_base' || extra === '_base_index') { // always in libs moveTo = `libs/xplat/features/src/lib/${featureName}/base`; } else { moveTo = `${featurePath}/${type}${type === 'state' ? '' : 's'}`; if (!isNxLib && !tree.exists(featureModulePath)) { let optionName; if (prefixPath !== 'libs') { // parse platform from prefix const parts = prefixPath.split('/'); if (parts.length > 1) { optionName = parts[2]; } } throw new Error((0, xplat_1.needFeatureModuleError)(featureModulePath, featureName, optionName)); } } if (forSubFolder && options.subFolder) { moveTo += `/${options.subFolder}`; } // console.log('moveTo:', moveTo); const indexPath = `${moveTo}/index.ts`; if ((extra === '_index' || extra === '_base_index') && tree.exists(indexPath)) { // already has an index barrel return (0, schematics_1.noop)(); } else { return (0, schematics_1.branchAndMerge)((0, schematics_1.mergeWith)((0, schematics_1.apply)((0, schematics_1.url)(`./${extra}_files`), [ (0, schematics_1.template)(Object.assign(Object.assign(Object.assign({}, options), (0, xplat_1.getDefaultTemplateOptions)()), { name: options.name.toLowerCase(), relativeDirectory, xplatFolderName, // feature: featureName, forSubFolder })), (0, schematics_1.move)(moveTo), ]))); } } function isFeatureInGeneralBarrel(featureName) { // 'shared' barrel is for app specific shared components, pipes, directives (not service level features) // 'ui' barrel is for entire workspace ui related sharing of components, pipes, directives (not service level features) return featureName === 'shared' || featureName === 'ui'; } function shouldTargetCoreBarrel(type, featureName) { // when service or state is being generated with no options, it falls back to shared/ui // services and state should never be generated in shared or ui features // therefore target core barrel return ((type === 'service' || type === 'state') && isFeatureInGeneralBarrel(featureName)); } function adjustBarrel(type, options, prefix) { let featureName = getFeatureName(options); const srcSubFolderPath = options.projects ? '' : '/src/lib'; let barrelIndexPath; if (shouldTargetCoreBarrel(type, featureName)) { if (type === 'state') { barrelIndexPath = `${prefix}/core${srcSubFolderPath}/index.ts`; } else { barrelIndexPath = `${prefix}/core${srcSubFolderPath}/${type}s/index.ts`; } } else { if (type === 'state') { barrelIndexPath = `${prefix}/features${srcSubFolderPath}/${featureName}/index.ts`; } else { barrelIndexPath = `${prefix}/features${srcSubFolderPath}/${featureName}/${type}s/index.ts`; } } if (type === 'state') { return adjustBarrelIndexForType(type, options, barrelIndexPath); } else { return adjustBarrelIndex(type, options, barrelIndexPath); } } function adjustBarrelIndex(type, options, indexFilePath, inSubFolder, isBase, importIfSubFolder) { return (host, context) => { const devKitTree = (0, xplat_1.convertNgTreeToDevKit)(host, context); // console.log('adjustBarrelIndex:', indexFilePath); // console.log('host.exists(indexFilePath):', host.exists(indexFilePath)); if (host.exists(indexFilePath)) { const indexSource = host.read(indexFilePath).toString('utf-8'); let indexSourceFile = ts.createSourceFile(indexFilePath, indexSource, ts.ScriptTarget.Latest, true); const changes = []; const name = options.name.toLowerCase(); if (!isBase && type !== 'service') { // add to barrel collection if (importIfSubFolder && options.subFolder) { // import collection from subfolder const symbolName = `${xplat_1.stringUtils .sanitize(options.subFolder) .toUpperCase()}_${type.toUpperCase()}S`; indexSourceFile = (0, xplat_1.addGlobal)(devKitTree, indexSourceFile, indexFilePath, `import { ${symbolName} } from './${options.subFolder}';`, false); indexSourceFile = (0, ast_1.addToCollection)(devKitTree, indexSourceFile, indexFilePath, `...${symbolName}`, ' '); changes.push(indexSourceFile); } else { const symbolName = `${xplat_1.stringUtils.classify(name)}${xplat_1.stringUtils.capitalize(type)}`; indexSourceFile = (0, xplat_1.addGlobal)(devKitTree, indexSourceFile, indexFilePath, `import { ${symbolName} } from './${inSubFolder ? `${name}/` : ''}${name}.${type}';`); indexSourceFile = (0, ast_1.addToCollection)(devKitTree, indexSourceFile, indexFilePath, symbolName, ' '); changes.push(indexSourceFile); } } if (type === 'component' || type === 'service' || type === 'pipe') { // export symbol from barrel if ((isBase || importIfSubFolder) && options.subFolder) { indexSourceFile = (0, xplat_1.addGlobal)(devKitTree, indexSourceFile, indexFilePath, `export * from './${options.subFolder}';`, true); changes.push(indexSourceFile); } else { const subFolder = inSubFolder ? `${name}/` : ''; indexSourceFile = (0, xplat_1.addGlobal)(devKitTree, indexSourceFile, indexFilePath, `export * from './${subFolder}${name}.${isBase ? 'base-' : ''}${type}';`, true); changes.push(indexSourceFile); } } // insert(devKitTree.tree, indexFilePath, changes); } else { options.needsIndex = true; } return devKitTree.tree; }; } function adjustBarrelIndexForType(type, options, indexFilePath) { return (host, context) => { const devKitTree = (0, xplat_1.convertNgTreeToDevKit)(host, context); if (host.exists(indexFilePath)) { const indexSource = host.read(indexFilePath).toString('utf-8'); const indexSourceFile = ts.createSourceFile(indexFilePath, indexSource, ts.ScriptTarget.Latest, true); const changes = []; changes.push((0, xplat_1.addGlobal)(devKitTree, indexSourceFile, indexFilePath, `export * from './${type}';`, true)); // insert(devKitTree.tree, indexFilePath, changes); } else { options.needsIndex = true; } return devKitTree.tree; }; } function adjustModule(tree, type, options, prefixPath) { let featureName = getFeatureName(options); const isNxLib = xplat_1.XplatHelpers.isFeatureNxLib(featureName); let featurePath; if (isNxLib) { featurePath = getNxFeaturePath(tree, featureName); featureName = featureName.split('/').pop(); } else if (shouldTargetCoreBarrel(type, featureName)) { featureName = 'core'; featurePath = `${prefixPath}/${featureName}/src/lib`; } else { featurePath = `${prefixPath}/features/src/lib/${featureName}`; } const featureModulePath = `${featurePath}/${featureName}.module.ts`; if (type === 'state') { return adjustFeatureModuleForState(options, featureModulePath); } else { return adjustFeatureModule(type, options, featureModulePath); } } function adjustFeatureModule(type, options, modulePath) { return (host, context) => { const devKitTree = (0, xplat_1.convertNgTreeToDevKit)(host, context); // console.log('adjustFeatureModule:', modulePath); if (host.exists(modulePath)) { const moduleSource = host.read(modulePath).toString('utf-8'); const moduleSourceFile = (0, ast_1.getTsSourceFile)(devKitTree, modulePath); const changes = []; let featureName; if (options.feature) { featureName = xplat_1.stringUtils.sanitize(options.feature).toUpperCase(); } else { // default collections if (type === 'service') { featureName = 'CORE'; } else { if (modulePath.indexOf('apps') > -1) { // app specific shared featureName = 'SHARED'; } else { // workspace cross platform ui libraries featureName = 'UI'; } } } let collectionName; switch (type) { case 'component': collectionName = `${featureName}_COMPONENTS`; break; case 'directive': collectionName = `${featureName}_DIRECTIVES`; break; case 'pipe': collectionName = `${featureName}_PIPES`; break; case 'service': collectionName = `${featureName}_PROVIDERS`; break; } // console.log('collectionName:', collectionName); // console.log('moduleSource:', moduleSource); if (moduleSource.indexOf(collectionName) > -1) { // already handled return host; } else { if (type !== 'service') { // add to module changes.push((0, ast_1._addSymbolToNgModuleMetadata)(devKitTree, (0, ast_1.addDeclarationToModule)(devKitTree, (0, xplat_1.addGlobal)(devKitTree, moduleSourceFile, modulePath, `import { ${collectionName} } from './${type}s';`), modulePath, `...${collectionName}`), modulePath, 'exports', `...${collectionName}`)); } // insert(devKitTree.tree, modulePath, changes); } } return devKitTree.tree; }; } function adjustFeatureModuleForState(options, modulePath) { return (host, context) => { const devKitTree = (0, xplat_1.convertNgTreeToDevKit)(host, context); // console.log('adjustFeatureModuleForState:', modulePath); if (host.exists(modulePath)) { const moduleSource = host.read(modulePath).toString('utf-8'); let moduleSourceFile = ts.createSourceFile(modulePath, moduleSource, ts.ScriptTarget.Latest, true); // console.log('moduleSource:', moduleSource); const isInLibs = modulePath.indexOf('libs/xplat/core') === 0; const name = options.name.toLowerCase(); const changes = []; if (moduleSource.indexOf('StoreModule') === -1) { moduleSourceFile = (0, xplat_1.addGlobal)(devKitTree, moduleSourceFile, modulePath, `import { StoreModule } from '@ngrx/store';`); changes.push(moduleSourceFile); } if (moduleSource.indexOf('EffectsModule') === -1) { moduleSourceFile = (0, xplat_1.addGlobal)(devKitTree, moduleSourceFile, modulePath, `import { EffectsModule } from '@ngrx/effects';`); changes.push(moduleSourceFile); } moduleSourceFile = (0, xplat_1.addGlobal)(devKitTree, moduleSourceFile, modulePath, `import { ${xplat_1.stringUtils.classify(name)}Effects } from './state/${name}.effects';`); moduleSourceFile = (0, xplat_1.addGlobal)(devKitTree, moduleSourceFile, modulePath, `import { ${xplat_1.stringUtils.camelize(name)}Reducer } from './state/${name}.reducer';`); moduleSourceFile = (0, xplat_1.addGlobal)(devKitTree, moduleSourceFile, modulePath, `import { ${xplat_1.stringUtils.classify(name)}State } from './state/${name}.state';`); changes.push(moduleSourceFile); if (options.root) { if (moduleSource.indexOf('environments/environment') === -1) { const envFrom = isInLibs ? './environments/environment' : `@${(0, xplat_utils_1.getNpmScope)()}/xplat/core`; moduleSourceFile = (0, xplat_1.addGlobal)(devKitTree, moduleSourceFile, modulePath, `import { environment } from '${envFrom}';`); changes.push(moduleSourceFile); } moduleSourceFile = (0, ast_1.addImportToModule)(devKitTree, moduleSourceFile, modulePath, `StoreModule.forRoot( { ${xplat_1.stringUtils.camelize(name)}: ${xplat_1.stringUtils.camelize(name)}Reducer }, { initialState: { ${xplat_1.stringUtils.camelize(name)}: ${xplat_1.stringUtils.classify(name)}State.initialState } } ), EffectsModule.forRoot([${xplat_1.stringUtils.classify(name)}Effects])`); moduleSourceFile = (0, ast_1.addProviderToModule)(devKitTree, moduleSourceFile, modulePath, `${xplat_1.stringUtils.classify(name)}Effects`); changes.push(moduleSourceFile); } else { moduleSourceFile = (0, ast_1.addImportToModule)(devKitTree, moduleSourceFile, modulePath, `StoreModule.forFeature('${xplat_1.stringUtils.camelize(name)}', ${xplat_1.stringUtils.camelize(name)}Reducer, { initialState: ${xplat_1.stringUtils.classify(name)}State.initialState }), EffectsModule.forFeature([${xplat_1.stringUtils.classify(name)}Effects])`); moduleSourceFile = (0, ast_1.addProviderToModule)(devKitTree, moduleSourceFile, modulePath, `${xplat_1.stringUtils.classify(name)}Effects`); // feature state changes.push(moduleSourceFile); } // insert(devKitTree.tree, modulePath, changes); } return devKitTree.tree; }; } function adjustRouting(options, routingModulePaths, platform) { return (host, context) => { const devKitTree = (0, xplat_1.convertNgTreeToDevKit)(host, context); const featureName = options.name.toLowerCase(); let routingModulePath; // check which routing naming convention might be in use // app.routing.ts or app-routing.module.ts for (const modulePath of routingModulePaths) { if (host.exists(modulePath)) { routingModulePath = modulePath; break; } } // console.log('routingModulePath:',routingModulePath); // console.log('host.exists(routingModulePath):',host.exists(routingModulePath)); if (routingModulePath) { const routingSource = host.read(routingModulePath).toString('utf-8'); const routingSourceFile = ts.createSourceFile(routingModulePath, routingSource, ts.ScriptTarget.Latest, true); const changes = []; // add component to route config changes.push((0, ast_1.addToCollection)(devKitTree, routingSourceFile, routingModulePath, `{ path: '${featureName}', loadChildren: () => import('./features/${options.directory ? options.directory + '/' : ''}${featureName}/${featureName}.module').then(m => m.${xplat_1.stringUtils.classify(featureName)}Module) }`)); // insert(devKitTree.tree, routingModulePath, changes); } return devKitTree.tree; }; } function adjustSandbox(options, platform, appDirectory) { return (tree) => { if (xplat_1.supportedSandboxPlatforms.includes(platform)) { const homeCmpPath = `${appDirectory}/features/home/components/home.component.html`; let homeTemplate = tree.get(homeCmpPath).content.toString(); switch (platform) { case 'nativescript': let buttonTag = 'Button'; let buttonEndIndex = homeTemplate.lastIndexOf(`</${buttonTag}>`); if (buttonEndIndex === -1) { // check for lowercase buttonEndIndex = homeTemplate.lastIndexOf(`</${buttonTag.toLowerCase()}>`); if (buttonEndIndex > -1) { buttonTag = buttonTag.toLowerCase(); } } let customBtnClass = ''; if (buttonEndIndex === -1) { // if no buttons were found this is a fresh sandbox app setup // it should have a label as placeholder buttonEndIndex = homeTemplate.lastIndexOf('</Label>'); if (buttonEndIndex === -1) { buttonEndIndex = homeTemplate.lastIndexOf(`</label>`); } } else { const buttonClassStartIndex = homeTemplate.lastIndexOf('class="btn '); if (buttonClassStartIndex > -1) { // using custom button class customBtnClass = ' ' + homeTemplate.substring(buttonClassStartIndex + 11, homeTemplate.lastIndexOf(`"></${buttonTag}>`)); } } const featureName = options.name.toLowerCase(); const featureNameParts = featureName.split('-'); let routeName = featureName; if (featureNameParts.length > 1) { routeName = xplat_1.stringUtils.capitalize(featureNameParts[featureNameParts.length - 1]); } homeTemplate = homeTemplate.slice(0, buttonEndIndex + 9) + `<${buttonTag} text="${routeName}" (tap)="goTo('/${featureName}')" class="btn${customBtnClass}"></${buttonTag}>` + homeTemplate.slice(buttonEndIndex + 9); break; } (0, xplat_utils_1.updateFile)(tree, homeCmpPath, homeTemplate); } else { throw new schematics_1.SchematicsException(`The --adjustSandbox option is only supported on the following at the moment: ${xplat_1.supportedSandboxPlatforms}`); } return tree; }; }