feat(react): improve lib schematic by replacing parentRoute with appProject option

- Instead of providing full path to app component, just pass the project's name.
- Updates both app.tsx and main.tsx so user no longer need to do any maual updates.
This commit is contained in:
Jack Hsu 2019-08-14 10:52:17 -04:00 committed by Victor Savkin
parent 97d7ebea4c
commit a2fbc47c13
9 changed files with 460 additions and 133 deletions

View File

@ -11,6 +11,12 @@ ng generate library ...
## Options ## Options
### appProject
Type: `string`
The application project to add the library route to
### directory ### directory
Type: `string` Type: `string`
@ -31,12 +37,6 @@ Type: `string`
Library name Library name
### parentRoute
Type: `string`
Add new route to the parent component as specified by this path
### pascalCaseFiles ### pascalCaseFiles
Default: `false` Default: `false`

View File

@ -8,6 +8,7 @@ import {
move, move,
noop, noop,
Rule, Rule,
SchematicContext,
template, template,
Tree, Tree,
url url
@ -35,7 +36,7 @@ import * as ts from 'typescript';
import { Schema } from './schema'; import { Schema } from './schema';
import { CSS_IN_JS_DEPENDENCIES } from '../../utils/styled'; import { CSS_IN_JS_DEPENDENCIES } from '../../utils/styled';
import { addRouter } from '../../utils/ast-utils'; import { addRoute, addInitialRoutes } from '../../utils/ast-utils';
import { import {
babelCoreVersion, babelCoreVersion,
babelLoaderVersion, babelLoaderVersion,
@ -61,7 +62,7 @@ interface NormalizedSchema extends Schema {
} }
export default function(schema: Schema): Rule { export default function(schema: Schema): Rule {
return (host: Tree) => { return (host: Tree, context: SchematicContext) => {
const options = normalizeOptions(host, schema); const options = normalizeOptions(host, schema);
return chain([ return chain([
@ -89,7 +90,7 @@ export default function(schema: Schema): Rule {
}) })
: noop(), : noop(),
addStyledModuleDependencies(options), addStyledModuleDependencies(options),
addRouting(options), addRouting(options, context),
addBabel(options), addBabel(options),
formatFiles(options) formatFiles(options)
]); ]);
@ -221,7 +222,10 @@ function addStyledModuleDependencies(options: NormalizedSchema): Rule {
: noop(); : noop();
} }
function addRouting(options: NormalizedSchema): Rule { function addRouting(
options: NormalizedSchema,
context: SchematicContext
): Rule {
return options.routing return options.routing
? chain([ ? chain([
function addRouterToComponent(host: Tree) { function addRouterToComponent(host: Tree) {
@ -237,7 +241,7 @@ function addRouting(options: NormalizedSchema): Rule {
true true
); );
insert(host, appPath, addRouter(appPath, appSource)); insert(host, appPath, addInitialRoutes(appPath, appSource, context));
}, },
addDepsToPackageJson({ 'react-router-dom': reactRouterVersion }, {}) addDepsToPackageJson({ 'react-router-dom': reactRouterVersion }, {})
]) ])

View File

@ -263,8 +263,8 @@ describe('lib', () => {
}); });
}); });
describe('--parentRoute', () => { describe('--appProject', () => {
it('should add route to parent component', async () => { it('should add new route to existing routing code', async () => {
appTree = await runSchematic( appTree = await runSchematic(
'app', 'app',
{ name: 'myApp', routing: true }, { name: 'myApp', routing: true },
@ -275,48 +275,39 @@ describe('lib', () => {
'lib', 'lib',
{ {
name: 'myLib', name: 'myLib',
parentRoute: 'apps/my-app/src/app/app.tsx' appProject: 'my-app'
}, },
appTree appTree
); );
const appSource = tree.read('apps/my-app/src/app/app.tsx').toString(); const appSource = tree.read('apps/my-app/src/app/app.tsx').toString();
const mainSource = tree.read('apps/my-app/src/main.tsx').toString();
expect(appSource).toMatch(/<Route\s*path="\/my-lib"/); expect(mainSource).toContain('react-router-dom');
expect(mainSource).toContain('<BrowserRouter>');
expect(appSource).toContain('@proj/my-lib'); expect(appSource).toContain('@proj/my-lib');
expect(appSource).toContain('react-router-dom');
expect(appSource).toMatch(/<Route\s*path="\/my-lib"/);
}); });
it('throws error when parent component file is missing', async () => { it('should initialize routes if none were set up then add new route', async () => {
await expect( appTree = await runSchematic('app', { name: 'myApp' }, appTree);
runSchematic(
'lib',
{
name: 'myLib',
parentRoute: 'does/not/exist.tsx'
},
appTree
)
).rejects.toThrow('Cannot find');
});
it('should add routing to app if it does not exist yet', async () => {
appTree = await runSchematic(
'app',
{ name: 'myApp', routing: false },
appTree
);
const tree = await runSchematic( const tree = await runSchematic(
'lib', 'lib',
{ {
name: 'myLib', name: 'myLib',
parentRoute: 'apps/my-app/src/app/app.tsx' appProject: 'my-app'
}, },
appTree appTree
); );
const appSource = tree.read('apps/my-app/src/app/app.tsx').toString(); const appSource = tree.read('apps/my-app/src/app/app.tsx').toString();
const mainSource = tree.read('apps/my-app/src/main.tsx').toString();
expect(mainSource).toContain('react-router-dom');
expect(mainSource).toContain('<BrowserRouter>');
expect(appSource).toContain('@proj/my-lib');
expect(appSource).toContain('react-router-dom'); expect(appSource).toContain('react-router-dom');
expect(appSource).toMatch(/<Route\s*path="\/my-lib"/); expect(appSource).toMatch(/<Route\s*path="\/my-lib"/);
}); });

View File

@ -13,8 +13,11 @@ import {
} from '@angular-devkit/schematics'; } from '@angular-devkit/schematics';
import { import {
addDepsToPackageJson, addDepsToPackageJson,
addLintFiles,
formatFiles, formatFiles,
generateProjectLint,
getNpmScope, getNpmScope,
getProjectConfig,
insert, insert,
names, names,
NxJson, NxJson,
@ -23,28 +26,34 @@ import {
toClassName, toClassName,
toFileName, toFileName,
updateJsonInTree, updateJsonInTree,
updateWorkspaceInTree, updateWorkspaceInTree
addLintFiles,
generateProjectLint
} from '@nrwl/workspace'; } from '@nrwl/workspace';
import { join, normalize, Path } from '@angular-devkit/core'; import { join, normalize, Path } from '@angular-devkit/core';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { Schema } from './schema'; import { Schema } from './schema';
import { addRoute, addRouter } from '../../utils/ast-utils'; import {
addBrowserRouter,
addInitialRoutes,
addRoute,
findComponentImportPath
} from '../../utils/ast-utils';
import { reactRouterVersion } from '../../utils/versions'; import { reactRouterVersion } from '../../utils/versions';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
name: string; name: string;
fileName: string; fileName: string;
projectRoot: Path; projectRoot: Path;
routePath: string;
projectDirectory: string; projectDirectory: string;
parsedTags: string[]; parsedTags: string[];
appMain?: string;
appSourceRoot?: Path;
} }
export default function(schema: Schema): Rule { export default function(schema: Schema): Rule {
return (host: Tree, context: SchematicContext) => { return (host: Tree, context: SchematicContext) => {
const options = normalizeOptions(schema); const options = normalizeOptions(host, schema, context);
return chain([ return chain([
addLintFiles(options.projectRoot, options.linter), addLintFiles(options.projectRoot, options.linter),
@ -68,7 +77,7 @@ export default function(schema: Schema): Rule {
export: true, export: true,
routing: options.routing routing: options.routing
}), }),
updateParentRoute(options), updateAppRoutes(options, context),
formatFiles(options) formatFiles(options)
])(host, context); ])(host, context);
}; };
@ -132,45 +141,77 @@ function updateNxJson(options: NormalizedSchema): Rule {
}); });
} }
function updateParentRoute(options: NormalizedSchema): Rule { function updateAppRoutes(
return options.parentRoute options: NormalizedSchema,
? chain([ context: SchematicContext
function ensureRouterAdded(host: Tree) { ): Rule {
const { source, content } = readComponent(host, options.parentRoute); if (!options.appMain || !options.appSourceRoot) {
const isRouterPresent = content.match(/react-router-dom/); return noop();
}
return (host: Tree) => {
const { source } = readComponent(host, options.appMain);
const componentImportPath = findComponentImportPath('App', source);
if (!componentImportPath) {
throw new Error(
`Could not find App component in ${
options.appMain
} (Hint: you can omit --appProject, or make sure App exists)`
);
}
const appComponentPath = join(
options.appSourceRoot,
`${componentImportPath}.tsx`
);
return chain([
addDepsToPackageJson({ 'react-router-dom': reactRouterVersion }, {}),
function addBrowserRouterToMain(host: Tree) {
const { content, source } = readComponent(host, options.appMain);
const isRouterPresent = content.match(/react-router-dom/);
if (!isRouterPresent) { if (!isRouterPresent) {
insert( insert(
host, host,
options.parentRoute, options.appMain,
addRouter(options.parentRoute, source) addBrowserRouter(options.appMain, source, context)
);
return addDepsToPackageJson(
{ 'react-router-dom': reactRouterVersion },
{}
); );
} }
}, },
function addRouteToComponent(host: Tree) { function addInitialAppRoutes(host: Tree) {
const { content, source } = readComponent(host, appComponentPath);
const isRouterPresent = content.match(/react-router-dom/);
if (!isRouterPresent) {
insert(
host,
appComponentPath,
addInitialRoutes(appComponentPath, source, context)
);
}
},
function addNewAppRoute(host: Tree) {
const npmScope = getNpmScope(host); const npmScope = getNpmScope(host);
const { source: componentSource } = readComponent( const { source: componentSource } = readComponent(
host, host,
options.parentRoute appComponentPath
); );
insert( insert(
host, host,
options.parentRoute, appComponentPath,
addRoute(options.parentRoute, componentSource, { addRoute(
libName: options.name, appComponentPath,
componentSource,
{
routePath: options.routePath,
componentName: toClassName(options.name), componentName: toClassName(options.name),
moduleName: `@${npmScope}/${options.projectDirectory}` moduleName: `@${npmScope}/${options.projectDirectory}`
}) },
context
)
); );
}, },
addDepsToPackageJson({ 'react-router-dom': reactRouterVersion }, {}) addDepsToPackageJson({ 'react-router-dom': reactRouterVersion }, {})
]) ]);
: noop(); };
} }
function readComponent( function readComponent(
@ -193,26 +234,54 @@ function readComponent(
return { content, source }; return { content, source };
} }
function normalizeOptions(options: Schema): NormalizedSchema { function normalizeOptions(
host: Tree,
options: Schema,
context: SchematicContext
): NormalizedSchema {
const name = toFileName(options.name); const name = toFileName(options.name);
const projectDirectory = options.directory const projectDirectory = options.directory
? `${toFileName(options.directory)}/${name}` ? `${toFileName(options.directory)}/${name}`
: name; : name;
const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-');
const fileName = options.simpleModuleName ? name : projectName; const fileName = projectName;
const projectRoot = normalize(`libs/${projectDirectory}`); const projectRoot = normalize(`libs/${projectDirectory}`);
const parsedTags = options.tags const parsedTags = options.tags
? options.tags.split(',').map(s => s.trim()) ? options.tags.split(',').map(s => s.trim())
: []; : [];
return { const normalized: NormalizedSchema = {
...options, ...options,
fileName, fileName,
routePath: `/${name}`,
name: projectName, name: projectName,
projectRoot, projectRoot,
projectDirectory, projectDirectory,
parsedTags parsedTags
}; };
if (options.appProject) {
const appProjectConfig = getProjectConfig(host, options.appProject);
if (appProjectConfig.projectType !== 'application') {
throw new Error(
`appProject expected type of "application" but got "${
appProjectConfig.projectType
}"`
);
}
try {
normalized.appMain = appProjectConfig.architect.build.options.main;
normalized.appSourceRoot = normalize(appProjectConfig.sourceRoot);
} catch (e) {
throw new Error(
`Could not locate project main for ${options.appProject}`
);
}
}
return normalized;
} }

View File

@ -7,10 +7,9 @@ export interface Schema {
skipTsConfig: boolean; skipTsConfig: boolean;
skipFormat: boolean; skipFormat: boolean;
tags?: string; tags?: string;
simpleModuleName: boolean;
pascalCaseFiles?: boolean; pascalCaseFiles?: boolean;
routing?: boolean; routing?: boolean;
parentRoute?: string; appProject?: string;
unitTestRunner: 'jest' | 'none'; unitTestRunner: 'jest' | 'none';
linter: Linter; linter: Linter;
} }

View File

@ -84,9 +84,9 @@
"type": "boolean", "type": "boolean",
"description": "Generate library with routes" "description": "Generate library with routes"
}, },
"parentRoute": { "appProject": {
"type": "string", "type": "string",
"description": "Add new route to the parent component as specified by this path" "description": "The application project to add the library route to"
} }
}, },
"required": ["name"] "required": ["name"]

View File

@ -1,16 +1,18 @@
import * as utils from './ast-utils'; import * as utils from './ast-utils';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { Tree } from '@angular-devkit/schematics';
import { insert } from '@nrwl/workspace/src/utils/ast-utils';
describe('react ast-utils', () => { describe('react ast-utils', () => {
describe('findDefaultExport', () => { describe('findDefaultExport', () => {
it('should find exported variable', () => { it('should find exported variable', () => {
const text = ` const sourceCode = `
const main = () => {}; const main = () => {};
export default main; export default main;
`; `;
const source = ts.createSourceFile( const source = ts.createSourceFile(
'test.ts', 'test.ts',
text, sourceCode,
ts.ScriptTarget.Latest, ts.ScriptTarget.Latest,
true true
); );
@ -22,13 +24,13 @@ describe('react ast-utils', () => {
}); });
it('should find exported function', () => { it('should find exported function', () => {
const text = ` const sourceCode = `
function main() {} function main() {}
export default main; export default main;
`; `;
const source = ts.createSourceFile( const source = ts.createSourceFile(
'test.ts', 'test.ts',
text, sourceCode,
ts.ScriptTarget.Latest, ts.ScriptTarget.Latest,
true true
); );
@ -40,12 +42,12 @@ describe('react ast-utils', () => {
}); });
it('should find default export function', () => { it('should find default export function', () => {
const text = ` const sourceCode = `
export default function main() {}; export default function main() {};
`; `;
const source = ts.createSourceFile( const source = ts.createSourceFile(
'test.ts', 'test.ts',
text, sourceCode,
ts.ScriptTarget.Latest, ts.ScriptTarget.Latest,
true true
); );
@ -56,4 +58,133 @@ describe('react ast-utils', () => {
expect(result.name.text).toEqual('main'); expect(result.name.text).toEqual('main');
}); });
}); });
describe('addRoute', () => {
let tree: Tree;
let context: any;
beforeEach(() => {
context = {
warn: jest.fn()
};
tree = Tree.empty();
});
it('should add links and routes if they are not present', async () => {
const sourceCode = `
import React from 'react';
const App = () => (
<>
<h1>Hello</h1>
</>
);
export default App;
`;
tree.create('app.tsx', sourceCode);
const source = ts.createSourceFile(
'app.tsx',
sourceCode,
ts.ScriptTarget.Latest,
true
);
insert(
tree,
'app.tsx',
utils.addInitialRoutes('app.tsx', source, context)
);
const result = tree.read('app.tsx').toString();
expect(result).toMatch(/role="navigation"/);
expect(result).toMatch(/<Link\s+to="\/page-2"/);
expect(result).toMatch(/<Route\s+path="\/page-2"/);
});
it('should update existing routes', async () => {
const sourceCode = `
import React from 'react';
import { Home } from '@example/home';
const App = () => (
<>
<header>
<h1>Hello</h1>
<div role="navigation">
<ul>
<li><Link to="/">Home</Link></li>
</ul>
</div>
</header>
<p>Hello World!</p>
<Route path="/" component={Home}/>
</>
);
export default App;
`;
tree.create('app.tsx', sourceCode);
const source = ts.createSourceFile(
'app.tsx',
sourceCode,
ts.ScriptTarget.Latest,
true
);
insert(
tree,
'app.tsx',
utils.addRoute(
'app.tsx',
source,
{
routePath: '/about',
componentName: 'About',
moduleName: '@example/about'
},
context
)
);
const result = tree.read('app.tsx').toString();
expect(result).toMatch(/<li><Link\s+to="\/about"/);
expect(result).toMatch(/<Route\s+path="\/about"\s+component={About}/);
});
});
describe('addBrowserRouter', () => {
let tree: Tree;
let context: any;
beforeEach(() => {
context = {
warn: jest.fn()
};
tree = Tree.empty();
});
it('should wrap around App component', () => {
const sourceCode = `
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from '@example/my-app';
ReactDOM.render(<App/>, document.getElementById('root'));
`;
tree.create('app.tsx', sourceCode);
const source = ts.createSourceFile(
'app.tsx',
sourceCode,
ts.ScriptTarget.Latest,
true
);
insert(
tree,
'app.tsx',
utils.addBrowserRouter('app.tsx', source, context)
);
const result = tree.read('app.tsx').toString();
expect(result).toContain('<BrowserRouter><App/></BrowserRouter>');
});
});
}); });

View File

@ -5,17 +5,43 @@ import {
InsertChange InsertChange
} from '@nrwl/workspace/src/utils/ast-utils'; } from '@nrwl/workspace/src/utils/ast-utils';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { noop, SchematicContext } from '@angular-devkit/schematics';
import { join } from '@angular-devkit/core';
export function addRouter(sourcePath: string, source: ts.SourceFile): Change[] { export function addInitialRoutes(
const jsxClosing = findNodes(source, ts.SyntaxKind.JsxClosingElement); sourcePath: string,
source: ts.SourceFile,
context: SchematicContext
): Change[] {
const jsxClosingElements = findNodes(source, [
ts.SyntaxKind.JsxClosingElement,
ts.SyntaxKind.JsxClosingFragment
]);
const outerMostJsxClosing = jsxClosingElements[jsxClosingElements.length - 1];
const outerMostJsxClosing = jsxClosing[jsxClosing.length - 1]; if (!outerMostJsxClosing) {
context.logger.warn(
`Could not find JSX elements in ${sourcePath}; Skipping insert routes`
);
return [];
}
const insertRoute = new InsertChange( const insertRoutes = new InsertChange(
sourcePath, sourcePath,
outerMostJsxClosing.getStart(), outerMostJsxClosing.getStart(),
` `
<hr style={{ margin: '36px 0' }}/> {/* START: routes */}
{/* These routes and navigation have been generated for you */}
{/* Feel free to move and update them to fit your needs */}
<br/>
<hr/>
<br/>
<div role="navigation">
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/page-2">Page 2</Link></li>
</ul>
</div>
<Route <Route
path="/" path="/"
exact exact
@ -30,18 +56,17 @@ export function addRouter(sourcePath: string, source: ts.SourceFile): Change[] {
<div><Link to="/">Click here to go back to root page.</Link></div> <div><Link to="/">Click here to go back to root page.</Link></div>
)} )}
/> />
{/* END: routes */}
` `
); );
findDefaultExport(source);
return [ return [
...addGlobal( ...addGlobal(
source, source,
sourcePath, sourcePath,
`import { Route, Link} from 'react-router-dom';` `import { Route, Link } from 'react-router-dom';`
), ),
insertRoute insertRoutes
]; ];
} }
@ -49,47 +74,91 @@ export function addRoute(
sourcePath: string, sourcePath: string,
source: ts.SourceFile, source: ts.SourceFile,
options: { options: {
libName: string; routePath: string;
componentName: string; componentName: string;
moduleName: string; moduleName: string;
} },
context: SchematicContext
): Change[] { ): Change[] {
const defaultExport = findDefaultExport(source); const routes = findElements(source, 'Route');
const links = findElements(source, 'Link');
if (!defaultExport) { if (routes.length === 0) {
throw new Error(`Cannot find default export in ${sourcePath}`); context.logger.warn(
} `Could not find <Route/> components in ${sourcePath}; Skipping add route`
const elements = findNodes(
defaultExport,
ts.SyntaxKind.JsxSelfClosingElement
) as ts.JsxSelfClosingElement[];
const routes = elements.filter(
x =>
x.tagName.kind === ts.SyntaxKind.Identifier && x.tagName.text === 'Route'
); );
return [];
} else {
const changes: Change[] = [];
const firstRoute = routes[0];
const firstLink = links[0];
if (routes.length > 0) { changes.push(
const lastRoute = routes[0]; ...addGlobal(
const addImport = addGlobal(
source, source,
sourcePath, sourcePath,
`import { ${options.componentName} } from '${options.moduleName}';` `import { ${options.componentName} } from '${options.moduleName}';`
)
); );
const insertRoute = new InsertChange( changes.push(
new InsertChange(
sourcePath, sourcePath,
lastRoute.getEnd(), firstRoute.getEnd(),
`<Route path="/${options.libName}" component={${ `<Route path="${options.routePath}" component={${
options.componentName options.componentName
}} />` }} />`
)
); );
return [...addImport, insertRoute]; if (firstLink) {
const parentLi = findClosestOpening('li', firstLink);
if (parentLi) {
changes.push(
new InsertChange(
sourcePath,
parentLi.getEnd(),
`<li><Link to="${options.routePath}">${
options.componentName
}</Link></li>`
)
);
} else { } else {
throw new Error(`Could not find routes in ${sourcePath}`); changes.push(
new InsertChange(
sourcePath,
firstLink.parent.getEnd(),
`<Link to="${options.routePath}">${options.componentName}</Link>`
)
);
}
}
return changes;
}
}
export function addBrowserRouter(
sourcePath: string,
source: ts.SourceFile,
context: SchematicContext
): Change[] {
const app = findElements(source, 'App')[0];
if (app) {
return [
...addGlobal(
source,
sourcePath,
`import { BrowserRouter } from 'react-router-dom';`
),
new InsertChange(sourcePath, app.getStart(), `<BrowserRouter>`),
new InsertChange(sourcePath, app.getEnd(), `</BrowserRouter>`)
];
} else {
context.logger.warn(
`Could not find App component in ${sourcePath}; Skipping add <BrowserRouter>`
);
return [];
} }
} }
@ -155,6 +224,29 @@ export function findDefaultClassOrFunction(
); );
} }
export function findComponentImportPath(name: string, source: ts.SourceFile) {
const allImports = findNodes(
source,
ts.SyntaxKind.ImportDeclaration
) as ts.ImportDeclaration[];
const matching = allImports.filter((i: ts.ImportDeclaration) => {
return (
i.importClause &&
i.importClause.name &&
i.importClause.name.getText() === name
);
});
if (matching.length === 0) {
return null;
}
const appImport = matching[0];
return appImport.moduleSpecifier.getText().replace(/['"]/g, '');
}
// -----------------------------------------------------------------------------
function hasDefaultExportModifier( function hasDefaultExportModifier(
x: ts.ClassDeclaration | ts.FunctionDeclaration x: ts.ClassDeclaration | ts.FunctionDeclaration
) { ) {
@ -163,3 +255,41 @@ function hasDefaultExportModifier(
x.modifiers.some(m => m.kind === ts.SyntaxKind.DefaultKeyword) x.modifiers.some(m => m.kind === ts.SyntaxKind.DefaultKeyword)
); );
} }
function findElements(source: ts.SourceFile, tagName: string) {
const nodes = findNodes(source, [
ts.SyntaxKind.JsxSelfClosingElement,
ts.SyntaxKind.JsxOpeningElement
]);
return nodes.filter(node => isTag(tagName, node));
}
function findClosestOpening(tagName: string, node: ts.Node) {
if (!node) {
return null;
}
if (isTag(tagName, node)) {
return node;
} else {
return findClosestOpening(tagName, node.parent);
}
}
function isTag(tagName: string, node: ts.Node) {
if (ts.isJsxOpeningLikeElement(node)) {
return (
node.tagName.kind === ts.SyntaxKind.Identifier &&
node.tagName.text === tagName
);
}
if (ts.isJsxElement(node) && node.openingElement) {
return (
node.openingElement.tagName.kind === ts.SyntaxKind.Identifier &&
node.openingElement.tagName.text === tagName
);
}
return false;
}

View File

@ -45,7 +45,7 @@ function insertAfterLastOccurrence(
export function findNodes( export function findNodes(
node: ts.Node, node: ts.Node,
kind: ts.SyntaxKind, kind: ts.SyntaxKind | ts.SyntaxKind[],
max = Infinity max = Infinity
): ts.Node[] { ): ts.Node[] {
if (!node || max == 0) { if (!node || max == 0) {
@ -53,7 +53,10 @@ export function findNodes(
} }
const arr: ts.Node[] = []; const arr: ts.Node[] = [];
if (node.kind === kind) { const hasMatch = Array.isArray(kind)
? kind.includes(node.kind)
: node.kind === kind;
if (hasMatch) {
arr.push(node); arr.push(node);
max--; max--;
} }