462 lines
12 KiB
TypeScript
462 lines
12 KiB
TypeScript
import {
|
|
addDependenciesToPackageJson,
|
|
addProjectConfiguration,
|
|
applyChangesToString,
|
|
convertNxGenerator,
|
|
formatFiles,
|
|
generateFiles,
|
|
GeneratorCallback,
|
|
getProjects,
|
|
getWorkspaceLayout,
|
|
joinPathFragments,
|
|
names,
|
|
normalizePath,
|
|
offsetFromRoot,
|
|
toJS,
|
|
Tree,
|
|
updateJson,
|
|
} from '@nrwl/devkit';
|
|
import { getImportPath } from 'nx/src/utils/path';
|
|
import { jestProjectGenerator } from '@nrwl/jest';
|
|
import { swcCoreVersion } from '@nrwl/js/src/utils/versions';
|
|
import { lintProjectGenerator } from '@nrwl/linter';
|
|
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
|
|
import {
|
|
getRelativePathToRootTsConfig,
|
|
getRootTsConfigPathInTree,
|
|
} from '@nrwl/workspace/src/utilities/typescript';
|
|
import * as ts from 'typescript';
|
|
import { assertValidStyle } from '../../utils/assertion';
|
|
import {
|
|
addBrowserRouter,
|
|
addInitialRoutes,
|
|
addRoute,
|
|
findComponentImportPath,
|
|
} from '../../utils/ast-utils';
|
|
import {
|
|
createReactEslintJson,
|
|
extraEslintDependencies,
|
|
} from '../../utils/lint';
|
|
import {
|
|
reactDomVersion,
|
|
reactRouterDomVersion,
|
|
reactVersion,
|
|
typesReactRouterDomVersion,
|
|
} from '../../utils/versions';
|
|
import componentGenerator from '../component/component';
|
|
import init from '../init/init';
|
|
import { Schema } from './schema';
|
|
|
|
export interface NormalizedSchema extends Schema {
|
|
name: string;
|
|
fileName: string;
|
|
projectRoot: string;
|
|
routePath: string;
|
|
projectDirectory: string;
|
|
parsedTags: string[];
|
|
appMain?: string;
|
|
appSourceRoot?: string;
|
|
}
|
|
|
|
export async function libraryGenerator(host: Tree, schema: Schema) {
|
|
const tasks: GeneratorCallback[] = [];
|
|
|
|
const options = normalizeOptions(host, schema);
|
|
if (options.publishable === true && !schema.importPath) {
|
|
throw new Error(
|
|
`For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)`
|
|
);
|
|
}
|
|
if (!options.component) {
|
|
options.style = 'none';
|
|
}
|
|
|
|
const initTask = await init(host, {
|
|
...options,
|
|
e2eTestRunner: 'none',
|
|
skipFormat: true,
|
|
});
|
|
tasks.push(initTask);
|
|
|
|
addProject(host, options);
|
|
|
|
const lintTask = await addLinting(host, options);
|
|
tasks.push(lintTask);
|
|
|
|
createFiles(host, options);
|
|
|
|
if (!options.skipTsConfig) {
|
|
updateBaseTsConfig(host, options);
|
|
}
|
|
|
|
if (options.unitTestRunner === 'jest') {
|
|
const jestTask = await jestProjectGenerator(host, {
|
|
...options,
|
|
project: options.name,
|
|
setupFile: 'none',
|
|
supportTsx: true,
|
|
skipSerializers: true,
|
|
compiler: options.compiler,
|
|
});
|
|
tasks.push(jestTask);
|
|
}
|
|
|
|
if (options.component) {
|
|
const componentTask = await componentGenerator(host, {
|
|
name: options.name,
|
|
project: options.name,
|
|
flat: true,
|
|
style: options.style,
|
|
skipTests: options.unitTestRunner === 'none',
|
|
export: true,
|
|
routing: options.routing,
|
|
js: options.js,
|
|
pascalCaseFiles: options.pascalCaseFiles,
|
|
});
|
|
tasks.push(componentTask);
|
|
}
|
|
|
|
if (options.publishable || options.buildable) {
|
|
updateLibPackageNpmScope(host, options);
|
|
}
|
|
|
|
if (!options.skipPackageJson) {
|
|
const installTask = await addDependenciesToPackageJson(
|
|
host,
|
|
{
|
|
react: reactVersion,
|
|
'react-dom': reactDomVersion,
|
|
},
|
|
options.compiler === 'swc' ? { '@swc/core': swcCoreVersion } : {}
|
|
);
|
|
tasks.push(installTask);
|
|
}
|
|
|
|
const routeTask = updateAppRoutes(host, options);
|
|
tasks.push(routeTask);
|
|
|
|
if (!options.skipFormat) {
|
|
await formatFiles(host);
|
|
}
|
|
|
|
return runTasksInSerial(...tasks);
|
|
}
|
|
|
|
async function addLinting(host: Tree, options: NormalizedSchema) {
|
|
const lintTask = await lintProjectGenerator(host, {
|
|
linter: options.linter,
|
|
project: options.name,
|
|
tsConfigPaths: [
|
|
joinPathFragments(options.projectRoot, 'tsconfig.lib.json'),
|
|
],
|
|
unitTestRunner: options.unitTestRunner,
|
|
eslintFilePatterns: [`${options.projectRoot}/**/*.{ts,tsx,js,jsx}`],
|
|
skipFormat: true,
|
|
skipPackageJson: options.skipPackageJson,
|
|
});
|
|
|
|
const reactEslintJson = createReactEslintJson(
|
|
options.projectRoot,
|
|
options.setParserOptionsProject
|
|
);
|
|
|
|
updateJson(
|
|
host,
|
|
joinPathFragments(options.projectRoot, '.eslintrc.json'),
|
|
() => reactEslintJson
|
|
);
|
|
|
|
let installTask = () => {};
|
|
if (!options.skipPackageJson) {
|
|
installTask = await addDependenciesToPackageJson(
|
|
host,
|
|
extraEslintDependencies.dependencies,
|
|
extraEslintDependencies.devDependencies
|
|
);
|
|
}
|
|
|
|
return runTasksInSerial(lintTask, installTask);
|
|
}
|
|
|
|
function addProject(host: Tree, options: NormalizedSchema) {
|
|
const targets: { [key: string]: any } = {};
|
|
|
|
if (options.publishable || options.buildable) {
|
|
const { libsDir } = getWorkspaceLayout(host);
|
|
const external: string[] = [];
|
|
|
|
if (options.style === '@emotion/styled') {
|
|
external.push('@emotion/react/jsx-runtime');
|
|
} else {
|
|
external.push('react/jsx-runtime');
|
|
}
|
|
|
|
targets.build = {
|
|
builder: '@nrwl/web:rollup',
|
|
outputs: ['{options.outputPath}'],
|
|
options: {
|
|
outputPath: `dist/${libsDir}/${options.projectDirectory}`,
|
|
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
|
|
project: `${options.projectRoot}/package.json`,
|
|
entryFile: maybeJs(options, `${options.projectRoot}/src/index.ts`),
|
|
external,
|
|
rollupConfig: `@nrwl/react/plugins/bundle-rollup`,
|
|
compiler: options.compiler ?? 'babel',
|
|
assets: [
|
|
{
|
|
glob: `${options.projectRoot}/README.md`,
|
|
input: '.',
|
|
output: '.',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
addProjectConfiguration(
|
|
host,
|
|
options.name,
|
|
{
|
|
root: options.projectRoot,
|
|
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
|
|
projectType: 'library',
|
|
tags: options.parsedTags,
|
|
targets,
|
|
},
|
|
options.standaloneConfig
|
|
);
|
|
}
|
|
|
|
function updateTsConfig(tree: Tree, options: NormalizedSchema) {
|
|
updateJson(
|
|
tree,
|
|
joinPathFragments(options.projectRoot, 'tsconfig.json'),
|
|
(json) => {
|
|
if (options.strict) {
|
|
json.compilerOptions = {
|
|
...json.compilerOptions,
|
|
forceConsistentCasingInFileNames: true,
|
|
strict: true,
|
|
noImplicitOverride: true,
|
|
noPropertyAccessFromIndexSignature: true,
|
|
noImplicitReturns: true,
|
|
noFallthroughCasesInSwitch: true,
|
|
};
|
|
}
|
|
|
|
return json;
|
|
}
|
|
);
|
|
}
|
|
|
|
function updateBaseTsConfig(host: Tree, options: NormalizedSchema) {
|
|
updateJson(host, getRootTsConfigPathInTree(host), (json) => {
|
|
const c = json.compilerOptions;
|
|
c.paths = c.paths || {};
|
|
delete c.paths[options.name];
|
|
|
|
if (c.paths[options.importPath]) {
|
|
throw new Error(
|
|
`You already have a library using the import path "${options.importPath}". Make sure to specify a unique one.`
|
|
);
|
|
}
|
|
|
|
const { libsDir } = getWorkspaceLayout(host);
|
|
|
|
c.paths[options.importPath] = [
|
|
maybeJs(
|
|
options,
|
|
joinPathFragments(libsDir, `${options.projectDirectory}/src/index.ts`)
|
|
),
|
|
];
|
|
|
|
return json;
|
|
});
|
|
}
|
|
|
|
function createFiles(host: Tree, options: NormalizedSchema) {
|
|
generateFiles(
|
|
host,
|
|
joinPathFragments(__dirname, './files/lib'),
|
|
options.projectRoot,
|
|
{
|
|
...options,
|
|
...names(options.name),
|
|
tmpl: '',
|
|
offsetFromRoot: offsetFromRoot(options.projectRoot),
|
|
rootTsConfigPath: getRelativePathToRootTsConfig(
|
|
host,
|
|
options.projectRoot
|
|
),
|
|
}
|
|
);
|
|
|
|
if (!options.publishable && !options.buildable) {
|
|
host.delete(`${options.projectRoot}/package.json`);
|
|
}
|
|
|
|
if (options.js) {
|
|
toJS(host);
|
|
}
|
|
|
|
updateTsConfig(host, options);
|
|
}
|
|
|
|
function updateAppRoutes(host: Tree, options: NormalizedSchema) {
|
|
if (!options.appMain || !options.appSourceRoot) {
|
|
return () => {};
|
|
}
|
|
|
|
const { content, 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 = joinPathFragments(
|
|
options.appSourceRoot,
|
|
maybeJs(options, `${componentImportPath}.tsx`)
|
|
);
|
|
|
|
const routerTask = addDependenciesToPackageJson(
|
|
host,
|
|
{ 'react-router-dom': reactRouterDomVersion },
|
|
{ '@types/react-router-dom': typesReactRouterDomVersion }
|
|
);
|
|
|
|
// addBrowserRouterToMain
|
|
const isRouterPresent = content.match(/react-router-dom/);
|
|
if (!isRouterPresent) {
|
|
const changes = applyChangesToString(
|
|
content,
|
|
addBrowserRouter(options.appMain, source)
|
|
);
|
|
host.write(options.appMain, changes);
|
|
}
|
|
|
|
// addInitialAppRoutes
|
|
{
|
|
const { content: componentContent, source: componentSource } =
|
|
readComponent(host, appComponentPath);
|
|
const isComponentRouterPresent = componentContent.match(/react-router-dom/);
|
|
if (!isComponentRouterPresent) {
|
|
const changes = applyChangesToString(
|
|
componentContent,
|
|
addInitialRoutes(appComponentPath, componentSource)
|
|
);
|
|
host.write(appComponentPath, changes);
|
|
}
|
|
}
|
|
|
|
// addNewAppRoute
|
|
{
|
|
const { content: componentContent, source: componentSource } =
|
|
readComponent(host, appComponentPath);
|
|
const { npmScope } = getWorkspaceLayout(host);
|
|
const changes = applyChangesToString(
|
|
componentContent,
|
|
addRoute(appComponentPath, componentSource, {
|
|
routePath: options.routePath,
|
|
componentName: names(options.name).className,
|
|
moduleName: getImportPath(npmScope, options.projectDirectory),
|
|
})
|
|
);
|
|
host.write(appComponentPath, changes);
|
|
}
|
|
|
|
return routerTask;
|
|
}
|
|
|
|
function readComponent(
|
|
host: Tree,
|
|
path: string
|
|
): { content: string; source: ts.SourceFile } {
|
|
if (!host.exists(path)) {
|
|
throw new Error(`Cannot find ${path}`);
|
|
}
|
|
|
|
const content = host.read(path, 'utf-8');
|
|
|
|
const source = ts.createSourceFile(
|
|
path,
|
|
content,
|
|
ts.ScriptTarget.Latest,
|
|
true
|
|
);
|
|
|
|
return { content, source };
|
|
}
|
|
|
|
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
|
|
const name = names(options.name).fileName;
|
|
const projectDirectory = options.directory
|
|
? `${names(options.directory).fileName}/${name}`
|
|
: name;
|
|
|
|
const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-');
|
|
const fileName = projectName;
|
|
const { libsDir, npmScope } = getWorkspaceLayout(host);
|
|
const projectRoot = joinPathFragments(libsDir, projectDirectory);
|
|
|
|
const parsedTags = options.tags
|
|
? options.tags.split(',').map((s) => s.trim())
|
|
: [];
|
|
|
|
const importPath =
|
|
options.importPath || getImportPath(npmScope, projectDirectory);
|
|
|
|
const normalized: NormalizedSchema = {
|
|
...options,
|
|
fileName,
|
|
routePath: `/${name}`,
|
|
name: projectName,
|
|
projectRoot,
|
|
projectDirectory,
|
|
parsedTags,
|
|
importPath,
|
|
};
|
|
|
|
if (options.appProject) {
|
|
const appProjectConfig = getProjects(host).get(options.appProject);
|
|
|
|
if (appProjectConfig.projectType !== 'application') {
|
|
throw new Error(
|
|
`appProject expected type of "application" but got "${appProjectConfig.projectType}"`
|
|
);
|
|
}
|
|
|
|
try {
|
|
normalized.appMain = appProjectConfig.targets.build.options.main;
|
|
normalized.appSourceRoot = normalizePath(appProjectConfig.sourceRoot);
|
|
} catch (e) {
|
|
throw new Error(
|
|
`Could not locate project main for ${options.appProject}`
|
|
);
|
|
}
|
|
}
|
|
|
|
assertValidStyle(normalized.style);
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function updateLibPackageNpmScope(host: Tree, options: NormalizedSchema) {
|
|
return updateJson(host, `${options.projectRoot}/package.json`, (json) => {
|
|
json.name = options.importPath;
|
|
return json;
|
|
});
|
|
}
|
|
|
|
function maybeJs(options: NormalizedSchema, path: string): string {
|
|
return options.js && (path.endsWith('.ts') || path.endsWith('.tsx'))
|
|
? path.replace(/\.tsx?$/, '.js')
|
|
: path;
|
|
}
|
|
|
|
export default libraryGenerator;
|
|
export const librarySchematic = convertNxGenerator(libraryGenerator);
|