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:
parent
97d7ebea4c
commit
a2fbc47c13
@ -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`
|
||||||
|
|||||||
@ -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 }, {})
|
||||||
])
|
])
|
||||||
|
|||||||
@ -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"/);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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 (!isRouterPresent) {
|
if (!componentImportPath) {
|
||||||
insert(
|
throw new Error(
|
||||||
host,
|
`Could not find App component in ${
|
||||||
options.parentRoute,
|
options.appMain
|
||||||
addRouter(options.parentRoute, source)
|
} (Hint: you can omit --appProject, or make sure App exists)`
|
||||||
);
|
);
|
||||||
return addDepsToPackageJson(
|
}
|
||||||
{ 'react-router-dom': reactRouterVersion },
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function addRouteToComponent(host: Tree) {
|
|
||||||
const npmScope = getNpmScope(host);
|
|
||||||
const { source: componentSource } = readComponent(
|
|
||||||
host,
|
|
||||||
options.parentRoute
|
|
||||||
);
|
|
||||||
|
|
||||||
|
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) {
|
||||||
insert(
|
insert(
|
||||||
host,
|
host,
|
||||||
options.parentRoute,
|
options.appMain,
|
||||||
addRoute(options.parentRoute, componentSource, {
|
addBrowserRouter(options.appMain, source, context)
|
||||||
libName: options.name,
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 { source: componentSource } = readComponent(
|
||||||
|
host,
|
||||||
|
appComponentPath
|
||||||
|
);
|
||||||
|
insert(
|
||||||
|
host,
|
||||||
|
appComponentPath,
|
||||||
|
addRoute(
|
||||||
|
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 }, {})
|
);
|
||||||
])
|
},
|
||||||
: noop();
|
addDepsToPackageJson({ 'react-router-dom': reactRouterVersion }, {})
|
||||||
|
]);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
@ -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>');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (routes.length > 0) {
|
|
||||||
const lastRoute = routes[0];
|
|
||||||
|
|
||||||
const addImport = addGlobal(
|
|
||||||
source,
|
|
||||||
sourcePath,
|
|
||||||
`import { ${options.componentName} } from '${options.moduleName}';`
|
|
||||||
);
|
);
|
||||||
|
return [];
|
||||||
const insertRoute = new InsertChange(
|
|
||||||
sourcePath,
|
|
||||||
lastRoute.getEnd(),
|
|
||||||
`<Route path="/${options.libName}" component={${
|
|
||||||
options.componentName
|
|
||||||
}} />`
|
|
||||||
);
|
|
||||||
|
|
||||||
return [...addImport, insertRoute];
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Could not find routes in ${sourcePath}`);
|
const changes: Change[] = [];
|
||||||
|
const firstRoute = routes[0];
|
||||||
|
const firstLink = links[0];
|
||||||
|
|
||||||
|
changes.push(
|
||||||
|
...addGlobal(
|
||||||
|
source,
|
||||||
|
sourcePath,
|
||||||
|
`import { ${options.componentName} } from '${options.moduleName}';`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
changes.push(
|
||||||
|
new InsertChange(
|
||||||
|
sourcePath,
|
||||||
|
firstRoute.getEnd(),
|
||||||
|
`<Route path="${options.routePath}" component={${
|
||||||
|
options.componentName
|
||||||
|
}} />`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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--;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user