373 lines
11 KiB
TypeScript
373 lines
11 KiB
TypeScript
import { join } from 'path';
|
|
import {
|
|
addDependenciesToPackageJson,
|
|
addProjectConfiguration,
|
|
convertNxGenerator,
|
|
ensurePackage,
|
|
extractLayoutDirectory,
|
|
formatFiles,
|
|
generateFiles,
|
|
GeneratorCallback,
|
|
getWorkspaceLayout,
|
|
joinPathFragments,
|
|
names,
|
|
offsetFromRoot,
|
|
readNxJson,
|
|
readProjectConfiguration,
|
|
runTasksInSerial,
|
|
TargetConfiguration,
|
|
Tree,
|
|
updateNxJson,
|
|
updateProjectConfiguration,
|
|
} from '@nx/devkit';
|
|
import { swcCoreVersion } from '@nx/js/src/utils/versions';
|
|
import type { Linter } from '@nx/linter';
|
|
|
|
import { getRelativePathToRootTsConfig } from '@nx/js';
|
|
|
|
import { nxVersion, swcLoaderVersion } from '../../utils/versions';
|
|
import { webInitGenerator } from '../init/init';
|
|
import { Schema } from './schema';
|
|
|
|
interface NormalizedSchema extends Schema {
|
|
projectName: string;
|
|
appProjectRoot: string;
|
|
e2eProjectName: string;
|
|
e2eProjectRoot: string;
|
|
parsedTags: string[];
|
|
}
|
|
|
|
function createApplicationFiles(tree: Tree, options: NormalizedSchema) {
|
|
generateFiles(
|
|
tree,
|
|
join(
|
|
__dirname,
|
|
options.bundler === 'vite' ? './files/app-vite' : './files/app-webpack'
|
|
),
|
|
options.appProjectRoot,
|
|
{
|
|
...options,
|
|
...names(options.name),
|
|
tmpl: '',
|
|
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
|
|
rootTsConfigPath: getRelativePathToRootTsConfig(
|
|
tree,
|
|
options.appProjectRoot
|
|
),
|
|
}
|
|
);
|
|
if (options.unitTestRunner === 'none') {
|
|
tree.delete(join(options.appProjectRoot, './src/app/app.element.spec.ts'));
|
|
}
|
|
}
|
|
|
|
async function setupBundler(tree: Tree, options: NormalizedSchema) {
|
|
const main = joinPathFragments(options.appProjectRoot, 'src/main.ts');
|
|
const tsConfig = joinPathFragments(
|
|
options.appProjectRoot,
|
|
'tsconfig.app.json'
|
|
);
|
|
const assets = [
|
|
joinPathFragments(options.appProjectRoot, 'src/favicon.ico'),
|
|
joinPathFragments(options.appProjectRoot, 'src/assets'),
|
|
];
|
|
|
|
if (options.bundler === 'webpack') {
|
|
const { configurationGenerator } = ensurePackage<
|
|
typeof import('@nx/webpack')
|
|
>('@nx/webpack', nxVersion);
|
|
await configurationGenerator(tree, {
|
|
project: options.projectName,
|
|
main,
|
|
tsConfig,
|
|
compiler: options.compiler ?? 'babel',
|
|
devServer: true,
|
|
webpackConfig: joinPathFragments(
|
|
options.appProjectRoot,
|
|
'webpack.config.js'
|
|
),
|
|
skipFormat: true,
|
|
});
|
|
const project = readProjectConfiguration(tree, options.projectName);
|
|
const prodConfig = project.targets.build.configurations.production;
|
|
const buildOptions = project.targets.build.options;
|
|
buildOptions.assets = assets;
|
|
buildOptions.index = joinPathFragments(
|
|
options.appProjectRoot,
|
|
'src/index.html'
|
|
);
|
|
buildOptions.baseHref = '/';
|
|
buildOptions.styles = [
|
|
joinPathFragments(options.appProjectRoot, `src/styles.${options.style}`),
|
|
];
|
|
// We can delete that, because this projest is an application
|
|
// and applications have a .babelrc file in their root dir.
|
|
// So Nx will find it and use it
|
|
delete buildOptions.babelUpwardRootMode;
|
|
buildOptions.scripts = [];
|
|
prodConfig.fileReplacements = [
|
|
{
|
|
replace: joinPathFragments(
|
|
options.appProjectRoot,
|
|
`src/environments/environment.ts`
|
|
),
|
|
with: joinPathFragments(
|
|
options.appProjectRoot,
|
|
`src/environments/environment.prod.ts`
|
|
),
|
|
},
|
|
];
|
|
prodConfig.optimization = true;
|
|
prodConfig.outputHashing = 'all';
|
|
prodConfig.sourceMap = false;
|
|
prodConfig.namedChunks = false;
|
|
prodConfig.extractLicenses = true;
|
|
prodConfig.vendorChunk = false;
|
|
updateProjectConfiguration(tree, options.projectName, project);
|
|
} else if (options.bundler === 'none') {
|
|
// TODO(jack): Flush this out... no bundler should be possible for web but the experience isn't holistic due to missing features (e.g. writing index.html).
|
|
const project = readProjectConfiguration(tree, options.projectName);
|
|
project.targets.build = {
|
|
executor: `@nx/js:${options.compiler}`,
|
|
outputs: ['{options.outputPath}'],
|
|
options: {
|
|
main,
|
|
outputPath: joinPathFragments('dist', options.appProjectRoot),
|
|
tsConfig,
|
|
assets,
|
|
},
|
|
};
|
|
updateProjectConfiguration(tree, options.projectName, project);
|
|
} else {
|
|
throw new Error('Unsupported bundler type');
|
|
}
|
|
}
|
|
|
|
async function addProject(tree: Tree, options: NormalizedSchema) {
|
|
const targets: Record<string, TargetConfiguration> = {};
|
|
|
|
addProjectConfiguration(
|
|
tree,
|
|
options.projectName,
|
|
{
|
|
projectType: 'application',
|
|
root: options.appProjectRoot,
|
|
sourceRoot: joinPathFragments(options.appProjectRoot, 'src'),
|
|
tags: options.parsedTags,
|
|
targets,
|
|
},
|
|
options.standaloneConfig
|
|
);
|
|
|
|
if (options.bundler !== 'vite') {
|
|
await setupBundler(tree, options);
|
|
}
|
|
}
|
|
|
|
function setDefaults(tree: Tree, options: NormalizedSchema) {
|
|
const nxJson = readNxJson(tree);
|
|
nxJson.generators = nxJson.generators || {};
|
|
nxJson.generators['@nx/web:application'] = {
|
|
style: options.style,
|
|
linter: options.linter,
|
|
unitTestRunner: options.unitTestRunner,
|
|
e2eTestRunner: options.e2eTestRunner,
|
|
...nxJson.generators['@nx/web:application'],
|
|
};
|
|
updateNxJson(tree, nxJson);
|
|
}
|
|
|
|
export async function applicationGenerator(host: Tree, schema: Schema) {
|
|
const options = normalizeOptions(host, schema);
|
|
|
|
const tasks: GeneratorCallback[] = [];
|
|
|
|
const webTask = await webInitGenerator(host, {
|
|
...options,
|
|
skipFormat: true,
|
|
});
|
|
tasks.push(webTask);
|
|
|
|
createApplicationFiles(host, options);
|
|
await addProject(host, options);
|
|
|
|
if (options.bundler === 'vite') {
|
|
const { viteConfigurationGenerator } = ensurePackage<
|
|
typeof import('@nx/vite')
|
|
>('@nx/vite', nxVersion);
|
|
// We recommend users use `import.meta.env.MODE` and other variables in their code to differentiate between production and development.
|
|
// See: https://vitejs.dev/guide/env-and-mode.html
|
|
if (
|
|
host.exists(joinPathFragments(options.appProjectRoot, 'src/environments'))
|
|
) {
|
|
host.delete(
|
|
joinPathFragments(options.appProjectRoot, 'src/environments')
|
|
);
|
|
}
|
|
|
|
const viteTask = await viteConfigurationGenerator(host, {
|
|
uiFramework: 'none',
|
|
project: options.projectName,
|
|
newProject: true,
|
|
includeVitest: options.unitTestRunner === 'vitest',
|
|
inSourceTests: options.inSourceTests,
|
|
skipFormat: true,
|
|
});
|
|
tasks.push(viteTask);
|
|
}
|
|
|
|
if (options.bundler !== 'vite' && options.unitTestRunner === 'vitest') {
|
|
const { vitestGenerator } = ensurePackage<typeof import('@nx/vite')>(
|
|
'@nx/vite',
|
|
nxVersion
|
|
);
|
|
const vitestTask = await vitestGenerator(host, {
|
|
uiFramework: 'none',
|
|
project: options.projectName,
|
|
coverageProvider: 'c8',
|
|
inSourceTests: options.inSourceTests,
|
|
skipFormat: true,
|
|
});
|
|
tasks.push(vitestTask);
|
|
}
|
|
|
|
if (
|
|
(options.bundler === 'vite' || options.unitTestRunner === 'vitest') &&
|
|
options.inSourceTests
|
|
) {
|
|
host.delete(
|
|
joinPathFragments(options.appProjectRoot, `src/app/app.element.spec.ts`)
|
|
);
|
|
}
|
|
|
|
if (options.linter === 'eslint') {
|
|
const { lintProjectGenerator } = ensurePackage('@nx/linter', nxVersion);
|
|
const lintTask = await lintProjectGenerator(host, {
|
|
linter: options.linter,
|
|
project: options.projectName,
|
|
tsConfigPaths: [
|
|
joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
|
|
],
|
|
unitTestRunner: options.unitTestRunner,
|
|
eslintFilePatterns: [`${options.appProjectRoot}/**/*.ts`],
|
|
skipFormat: true,
|
|
setParserOptionsProject: options.setParserOptionsProject,
|
|
});
|
|
tasks.push(lintTask);
|
|
}
|
|
|
|
if (options.e2eTestRunner === 'cypress') {
|
|
const { cypressProjectGenerator } = ensurePackage<
|
|
typeof import('@nx/cypress')
|
|
>('@nx/cypress', nxVersion);
|
|
const cypressTask = await cypressProjectGenerator(host, {
|
|
...options,
|
|
name: `${options.name}-e2e`,
|
|
directory: options.directory,
|
|
project: options.projectName,
|
|
skipFormat: true,
|
|
});
|
|
tasks.push(cypressTask);
|
|
} else if (options.e2eTestRunner === 'playwright') {
|
|
const { configurationGenerator: playwrightConfigGenerator } = ensurePackage<
|
|
typeof import('@nx/playwright')
|
|
>('@nx/playwright', nxVersion);
|
|
|
|
addProjectConfiguration(host, options.e2eProjectName, {
|
|
root: options.e2eProjectRoot,
|
|
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
|
|
projectType: 'application',
|
|
targets: {},
|
|
implicitDependencies: [options.projectName],
|
|
});
|
|
const playwrightTask = await playwrightConfigGenerator(host, {
|
|
project: options.e2eProjectName,
|
|
skipFormat: true,
|
|
skipPackageJson: false,
|
|
directory: 'src',
|
|
js: false,
|
|
linter: options.linter,
|
|
setParserOptionsProject: options.setParserOptionsProject,
|
|
});
|
|
tasks.push(playwrightTask);
|
|
}
|
|
if (options.unitTestRunner === 'jest') {
|
|
const { configurationGenerator } = ensurePackage<typeof import('@nx/jest')>(
|
|
'@nx/jest',
|
|
nxVersion
|
|
);
|
|
const jestTask = await configurationGenerator(host, {
|
|
project: options.projectName,
|
|
skipSerializers: true,
|
|
setupFile: 'web-components',
|
|
compiler: options.compiler,
|
|
skipFormat: true,
|
|
});
|
|
tasks.push(jestTask);
|
|
}
|
|
|
|
if (options.compiler === 'swc') {
|
|
const installTask = addDependenciesToPackageJson(
|
|
host,
|
|
{},
|
|
{ '@swc/core': swcCoreVersion, 'swc-loader': swcLoaderVersion }
|
|
);
|
|
tasks.push(installTask);
|
|
}
|
|
|
|
setDefaults(host, options);
|
|
|
|
if (!schema.skipFormat) {
|
|
await formatFiles(host);
|
|
}
|
|
return runTasksInSerial(...tasks);
|
|
}
|
|
|
|
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
|
|
const { layoutDirectory, projectDirectory } = extractLayoutDirectory(
|
|
options.directory
|
|
);
|
|
|
|
const appDirectory = projectDirectory
|
|
? `${names(projectDirectory).fileName}/${names(options.name).fileName}`
|
|
: names(options.name).fileName;
|
|
|
|
const { appsDir: defaultAppsDir, npmScope } = getWorkspaceLayout(host);
|
|
const appsDir = layoutDirectory ?? defaultAppsDir;
|
|
|
|
const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-');
|
|
const e2eProjectName = `${appProjectName}-e2e`;
|
|
|
|
const appProjectRoot = joinPathFragments(appsDir, appDirectory);
|
|
const e2eProjectRoot = joinPathFragments(appsDir, `${appDirectory}-e2e`);
|
|
|
|
const parsedTags = options.tags
|
|
? options.tags.split(',').map((s) => s.trim())
|
|
: [];
|
|
|
|
if (options.bundler === 'vite' && !options.unitTestRunner) {
|
|
options.unitTestRunner = 'vitest';
|
|
}
|
|
|
|
options.style = options.style || 'css';
|
|
options.linter = options.linter || ('eslint' as Linter.EsLint);
|
|
options.unitTestRunner = options.unitTestRunner || 'jest';
|
|
options.e2eTestRunner = options.e2eTestRunner || 'cypress';
|
|
|
|
return {
|
|
...options,
|
|
prefix: options.prefix ?? npmScope ?? 'app',
|
|
name: names(options.name).fileName,
|
|
compiler: options.compiler ?? 'babel',
|
|
bundler: options.bundler ?? 'webpack',
|
|
projectName: appProjectName,
|
|
appProjectRoot,
|
|
e2eProjectRoot,
|
|
e2eProjectName,
|
|
parsedTags,
|
|
};
|
|
}
|
|
|
|
export default applicationGenerator;
|
|
export const applicationSchematic = convertNxGenerator(applicationGenerator);
|