chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"outDir": "packages/node/dist",
|
||||
"assets": [
|
||||
{ "glob": "**/files/**" },
|
||||
{ "glob": "**/files/**/.gitkeep" },
|
||||
{ "glob": "src/**/schema.json" },
|
||||
{ "glob": "src/**/schema.d.ts" },
|
||||
"LICENSE"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { baseConfig } from '../../eslint.config.mjs';
|
||||
import * as jsoncEslintParser from 'jsonc-eslint-parser';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
'@nx/workspace',
|
||||
'@angular-devkit/core',
|
||||
'@angular-devkit/architect',
|
||||
'@angular-devkit/schematics',
|
||||
],
|
||||
},
|
||||
ignores: ['./src/migrations/**'],
|
||||
},
|
||||
{
|
||||
files: ['./package.json', './generators.json', './migrations.json'],
|
||||
rules: {
|
||||
'@nx/nx-plugin-checks': 'error',
|
||||
},
|
||||
languageOptions: {
|
||||
parser: jsoncEslintParser,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./package.json'],
|
||||
rules: {
|
||||
'@nx/dependency-checks': [
|
||||
'error',
|
||||
{
|
||||
buildTargets: ['build-base'],
|
||||
ignoredDependencies: [
|
||||
'nx',
|
||||
'typescript',
|
||||
'@nx/webpack',
|
||||
'express',
|
||||
'koa',
|
||||
'fastify',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
languageOptions: {
|
||||
parser: jsoncEslintParser,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "nx/node",
|
||||
"version": "0.1",
|
||||
"extends": ["@nx/workspace"],
|
||||
"generators": {
|
||||
"init": {
|
||||
"factory": "./dist/src/generators/init/init",
|
||||
"schema": "./dist/src/generators/init/schema.json",
|
||||
"description": "Initialize the `@nx/node` plugin.",
|
||||
"aliases": ["ng-add"],
|
||||
"hidden": true
|
||||
},
|
||||
"application": {
|
||||
"factory": "./dist/src/generators/application/application#applicationGeneratorInternal",
|
||||
"schema": "./dist/src/generators/application/schema.json",
|
||||
"aliases": ["app"],
|
||||
"x-type": "application",
|
||||
"description": "Create a node application."
|
||||
},
|
||||
"library": {
|
||||
"factory": "./dist/src/generators/library/library#libraryGeneratorInternal",
|
||||
"schema": "./dist/src/generators/library/schema.json",
|
||||
"aliases": ["lib"],
|
||||
"x-type": "library",
|
||||
"description": "Create a node library."
|
||||
},
|
||||
"setup-docker": {
|
||||
"factory": "./dist/src/generators/setup-docker/setup-docker",
|
||||
"schema": "./dist/src/generators/setup-docker/schema.json",
|
||||
"description": "Set up Docker configuration for a project.",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { applicationGenerator } from './src/generators/application/application';
|
||||
export { libraryGenerator } from './src/generators/library/library';
|
||||
export { initGenerator } from './src/generators/init/init';
|
||||
@@ -0,0 +1,5 @@
|
||||
// Curated internal API surface for other first-party Nx plugins (e.g. @nx/nest,
|
||||
// @nx/express). Prefer this over deep `@nx/node/src/*` imports — those are not
|
||||
// part of the public API.
|
||||
export type { Schema } from './src/generators/application/schema';
|
||||
export { tslibVersion } from './src/utils/versions';
|
||||
@@ -0,0 +1,7 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
|
||||
globals: {},
|
||||
displayName: 'node',
|
||||
preset: '../../jest.preset.js',
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"generators": {
|
||||
"rewrite-node-internal-subpath-imports": {
|
||||
"version": "23.0.0-beta.25",
|
||||
"description": "Rewrites `@nx/node/src/*` subpath imports now that the `./src/*` subpath is no longer exposed by `@nx/node`'s exports map. Named imports/exports of public symbols are routed to `@nx/node` and the rest to the new `@nx/node/internal` entry; `require`, dynamic `import` and `jest.mock` calls reference the whole module and are routed to `@nx/node/internal`.",
|
||||
"implementation": "./dist/src/migrations/update-23-0-0/rewrite-internal-subpath-imports"
|
||||
}
|
||||
},
|
||||
"packageJsonUpdates": {
|
||||
"22.0.2": {
|
||||
"version": "22.0.2-beta.0",
|
||||
"requires": {
|
||||
"koa": ">=2.0.0 <3.0.0"
|
||||
},
|
||||
"packages": {
|
||||
"koa": {
|
||||
"version": "^3.0.3",
|
||||
"alwaysAddToPackageJson": false
|
||||
},
|
||||
"@types/koa": {
|
||||
"version": "^3.0.0",
|
||||
"alwaysAddToPackageJson": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"22.6.0": {
|
||||
"version": "22.6.0-beta.10",
|
||||
"x-prompt": "Bump koa to ^3.1.2 to resolve CVE-2026-27959 (Host Header Injection)",
|
||||
"requires": {
|
||||
"koa": ">=3.0.0 <4.0.0"
|
||||
},
|
||||
"packages": {
|
||||
"koa": {
|
||||
"version": "^3.1.2",
|
||||
"alwaysAddToPackageJson": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import json = require('./migrations.json');
|
||||
|
||||
import { assertValidMigrationPaths } from '@nx/devkit/internal-testing-utils';
|
||||
import { MigrationsJson } from '@nx/devkit';
|
||||
|
||||
describe('node migrations', () => {
|
||||
assertValidMigrationPaths(json as MigrationsJson, __dirname);
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"name": "@nx/node",
|
||||
"version": "0.0.1",
|
||||
"private": false,
|
||||
"type": "commonjs",
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/tsconfig.tsbuildinfo",
|
||||
"migrations.json",
|
||||
"generators.json"
|
||||
],
|
||||
"description": "The Node Plugin for Nx contains generators to manage Node applications within an Nx workspace.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nrwl/nx.git",
|
||||
"directory": "packages/node"
|
||||
},
|
||||
"keywords": [
|
||||
"Monorepo",
|
||||
"Node",
|
||||
"Nest",
|
||||
"Jest",
|
||||
"Cypress",
|
||||
"CLI",
|
||||
"Backend"
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"utils": [
|
||||
"dist/utils.d.ts"
|
||||
],
|
||||
"internal": [
|
||||
"dist/internal.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"author": "Victor Savkin",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/nrwl/nx/issues"
|
||||
},
|
||||
"homepage": "https://nx.dev",
|
||||
"generators": "./generators.json",
|
||||
"ng-update": {
|
||||
"requirements": {},
|
||||
"migrations": "./migrations.json",
|
||||
"supportsOptionalMigrations": true
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"@nx/nx-source": "./index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./migrations.json": "./migrations.json",
|
||||
"./generators.json": "./generators.json",
|
||||
"./utils": {
|
||||
"@nx/nx-source": "./utils.ts",
|
||||
"types": "./dist/utils.d.ts",
|
||||
"default": "./dist/utils.js"
|
||||
},
|
||||
"./internal": {
|
||||
"@nx/nx-source": "./internal.ts",
|
||||
"types": "./dist/internal.d.ts",
|
||||
"default": "./dist/internal.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "catalog:typescript",
|
||||
"@nx/devkit": "workspace:*",
|
||||
"@nx/jest": "workspace:*",
|
||||
"@nx/js": "workspace:*",
|
||||
"@nx/eslint": "workspace:*",
|
||||
"@nx/docker": "workspace:*",
|
||||
"semver": "catalog:",
|
||||
"tcp-port-used": "^1.0.2",
|
||||
"kill-port": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nx": "workspace:*",
|
||||
"@nx/webpack": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">=4.0.0 <6.0.0",
|
||||
"koa": ">=2.0.0 <4.0.0",
|
||||
"fastify": ">=4.0.0 <6.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"express": {
|
||||
"optional": true
|
||||
},
|
||||
"koa": {
|
||||
"optional": true
|
||||
},
|
||||
"fastify": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "node",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/node",
|
||||
"projectType": "library",
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": ["build-base"],
|
||||
"outputs": ["{projectRoot}/README.md"],
|
||||
"command": "node ./scripts/copy-readme.js node packages/node/readme-template.md packages/node/README.md",
|
||||
"inputs": ["copyReadme"]
|
||||
},
|
||||
"build-base": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"nx",
|
||||
"devkit",
|
||||
"workspace",
|
||||
"js",
|
||||
"cypress",
|
||||
"module-federation",
|
||||
"playwright",
|
||||
"webpack",
|
||||
"jest",
|
||||
"eslint",
|
||||
"docker"
|
||||
],
|
||||
"target": "build-base"
|
||||
},
|
||||
"build-native",
|
||||
"copy-assets"
|
||||
]
|
||||
},
|
||||
"nx-release-publish": {
|
||||
"dependsOn": []
|
||||
}
|
||||
},
|
||||
"implicitDependencies": ["angular", "nest", "webpack"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<p style="text-align: center;">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-dark.svg">
|
||||
<img alt="Nx - Smart Monorepos · Fast Builds" src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-light.svg" width="100%">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
{{links}}
|
||||
|
||||
<hr>
|
||||
|
||||
# Nx: Smart Monorepos · Fast Builds
|
||||
|
||||
Get to green PRs in half the time. Nx optimizes your builds, scales your CI, and fixes failed PRs. Built for developers and AI agents.
|
||||
|
||||
{{content}}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`app debug support should generate a debug config for vscode by default 1`] = `
|
||||
[
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect=9229",
|
||||
},
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"name": "Debug api-fastify with Nx",
|
||||
"outFiles": [
|
||||
"\${workspaceFolder}/api-fastify/dist/**/*.(m|c|)js",
|
||||
"!**/node_modules/**",
|
||||
],
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"serve",
|
||||
"api-fastify",
|
||||
],
|
||||
"runtimeExecutable": "npx",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"type": "node",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`app debug support should generate a debug config for vscode by default 2`] = `
|
||||
[
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect=9229",
|
||||
},
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"name": "Debug api-express with Nx",
|
||||
"outFiles": [
|
||||
"\${workspaceFolder}/api-express/dist/**/*.(m|c|)js",
|
||||
"!**/node_modules/**",
|
||||
],
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"serve",
|
||||
"api-express",
|
||||
],
|
||||
"runtimeExecutable": "npx",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"type": "node",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`app debug support should generate a debug config for vscode by default 3`] = `
|
||||
[
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect=9229",
|
||||
},
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"name": "Debug api-koa with Nx",
|
||||
"outFiles": [
|
||||
"\${workspaceFolder}/api-koa/dist/**/*.(m|c|)js",
|
||||
"!**/node_modules/**",
|
||||
],
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"serve",
|
||||
"api-koa",
|
||||
],
|
||||
"runtimeExecutable": "npx",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"type": "node",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`app debug support should generate a debug config for vscode by default 4`] = `
|
||||
[
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect=9229",
|
||||
},
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"name": "Debug api-nest with Nx",
|
||||
"outFiles": [
|
||||
"\${workspaceFolder}/api-nest/dist/**/*.(m|c|)js",
|
||||
"!**/node_modules/**",
|
||||
],
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"serve",
|
||||
"api-nest",
|
||||
],
|
||||
"runtimeExecutable": "npx",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"type": "node",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
import {
|
||||
readNxJson,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateNxJson,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
|
||||
// nx-ignore-next-line
|
||||
import { applicationGenerator } from './application';
|
||||
|
||||
describe('node app generator (legacy)', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
const nxJson = readNxJson(tree);
|
||||
updateNxJson(tree, nxJson);
|
||||
});
|
||||
|
||||
it('should not skip the build target', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'my-node-app',
|
||||
bundler: 'webpack',
|
||||
addPlugin: false,
|
||||
});
|
||||
const project = readProjectConfiguration(tree, 'my-node-app');
|
||||
expect(project.root).toEqual('my-node-app');
|
||||
expect(project.targets.build).toMatchInlineSnapshot(`
|
||||
{
|
||||
"configurations": {
|
||||
"development": {
|
||||
"outputHashing": "none",
|
||||
},
|
||||
"production": {},
|
||||
},
|
||||
"defaultConfiguration": "production",
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"options": {
|
||||
"assets": [
|
||||
"my-node-app/src/assets",
|
||||
],
|
||||
"compiler": "tsc",
|
||||
"generatePackageJson": true,
|
||||
"main": "my-node-app/src/main.ts",
|
||||
"outputPath": "dist/my-node-app",
|
||||
"target": "node",
|
||||
"tsConfig": "my-node-app/tsconfig.app.json",
|
||||
"webpackConfig": "my-node-app/webpack.config.js",
|
||||
},
|
||||
"outputs": [
|
||||
"{options.outputPath}",
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
const webpackConfig = tree.read('my-node-app/webpack.config.js', 'utf-8');
|
||||
expect(webpackConfig).toContain(`composePlugins`);
|
||||
expect(webpackConfig).toContain(`target: 'node'`);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,269 @@
|
||||
import { logShowProjectCommand } from '@nx/devkit/internal';
|
||||
import {
|
||||
ensurePackage,
|
||||
formatFiles,
|
||||
GeneratorCallback,
|
||||
joinPathFragments,
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
updateProjectConfiguration,
|
||||
updateTsConfigsToJs,
|
||||
} from '@nx/devkit';
|
||||
import { configurationGenerator } from '@nx/jest';
|
||||
import { initGenerator as jsInitGenerator } from '@nx/js';
|
||||
import {
|
||||
addProjectToTsSolutionWorkspace,
|
||||
getTsConfigBaseOptions,
|
||||
shouldConfigureTsSolutionSetup,
|
||||
updateTsconfigFiles,
|
||||
sortPackageJsonFields,
|
||||
} from '@nx/js/internal';
|
||||
import { assertSupportedFrameworkVersion } from '../../utils/assert-supported-framework-version';
|
||||
import { nxVersion } from '../../utils/versions';
|
||||
import { e2eProjectGenerator } from '../e2e-project/e2e-project';
|
||||
import { initGenerator } from '../init/init';
|
||||
import { setupDockerGenerator } from '../setup-docker/setup-docker';
|
||||
import { Schema } from './schema';
|
||||
import {
|
||||
addAppFiles,
|
||||
addLintingToApplication,
|
||||
addProject,
|
||||
addProjectDependencies,
|
||||
addProxy,
|
||||
normalizeOptions,
|
||||
NormalizedSchema,
|
||||
} from './lib';
|
||||
|
||||
function updateTsConfigOptions(tree: Tree, options: NormalizedSchema) {
|
||||
if (options.isUsingTsSolutionConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateJson(tree, `${options.appProjectRoot}/tsconfig.json`, (json) => {
|
||||
if (options.rootProject) {
|
||||
return {
|
||||
compilerOptions: {
|
||||
...getTsConfigBaseOptions(tree),
|
||||
...json.compilerOptions,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
...json,
|
||||
extends: undefined,
|
||||
exclude: ['node_modules', 'tmp'],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...json,
|
||||
compilerOptions: {
|
||||
...json.compilerOptions,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function applicationGenerator(tree: Tree, schema: Schema) {
|
||||
return await applicationGeneratorInternal(tree, {
|
||||
addPlugin: false,
|
||||
useProjectJson: true,
|
||||
...schema,
|
||||
});
|
||||
}
|
||||
|
||||
export async function applicationGeneratorInternal(tree: Tree, schema: Schema) {
|
||||
assertSupportedFrameworkVersion(tree, schema.framework);
|
||||
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
|
||||
const addTsPlugin = shouldConfigureTsSolutionSetup(
|
||||
tree,
|
||||
schema.addPlugin,
|
||||
schema.useTsSolution
|
||||
);
|
||||
const jsInitTask = await jsInitGenerator(tree, {
|
||||
...schema,
|
||||
tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json',
|
||||
skipFormat: true,
|
||||
addTsPlugin,
|
||||
});
|
||||
tasks.push(jsInitTask);
|
||||
|
||||
const options = await normalizeOptions(tree, schema);
|
||||
|
||||
if (options.framework === 'nest') {
|
||||
// nx-ignore-next-line
|
||||
const { applicationGenerator } = ensurePackage('@nx/nest', nxVersion);
|
||||
const nestTasks = await applicationGenerator(tree, {
|
||||
...options,
|
||||
skipFormat: true,
|
||||
});
|
||||
tasks.push(nestTasks);
|
||||
|
||||
if (options.docker) {
|
||||
const dockerTask = await setupDockerGenerator(tree, {
|
||||
...options,
|
||||
project: options.name,
|
||||
skipFormat: true,
|
||||
});
|
||||
tasks.push(dockerTask);
|
||||
}
|
||||
return runTasksInSerial(
|
||||
...[
|
||||
...tasks,
|
||||
() => {
|
||||
logShowProjectCommand(options.name);
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
const initTask = await initGenerator(tree, {
|
||||
...schema,
|
||||
skipFormat: true,
|
||||
});
|
||||
tasks.push(initTask);
|
||||
|
||||
const { installTask, frameworkDependencies } = addProjectDependencies(
|
||||
tree,
|
||||
options
|
||||
);
|
||||
tasks.push(installTask);
|
||||
|
||||
if (options.bundler === 'webpack') {
|
||||
const { webpackInitGenerator } = ensurePackage<
|
||||
typeof import('@nx/webpack')
|
||||
>('@nx/webpack', nxVersion);
|
||||
const webpackInitTask = await webpackInitGenerator(tree, {
|
||||
skipPackageJson: options.skipPackageJson,
|
||||
skipFormat: true,
|
||||
addPlugin: options.addPlugin,
|
||||
});
|
||||
tasks.push(webpackInitTask);
|
||||
if (!options.skipPackageJson) {
|
||||
// Use CommonJS `require` rather than a dynamic ESM `import`:
|
||||
// `ensurePackage` makes the on-demand-installed package available via
|
||||
// `Module._initPaths`, which `require()` honors but ESM resolution does
|
||||
// not. Under nodenext, a dynamic `import()` is preserved as a true ESM
|
||||
// dynamic import, so it can't see the temp install.
|
||||
const {
|
||||
ensureDependencies,
|
||||
}: typeof import('@nx/webpack/internal') = require('@nx/webpack/internal');
|
||||
tasks.push(
|
||||
ensureDependencies(tree, {
|
||||
uiFramework: options.isNest ? 'none' : 'react',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
addAppFiles(tree, options);
|
||||
addProject(tree, options, frameworkDependencies);
|
||||
|
||||
// If we are using the new TS solution
|
||||
// We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project
|
||||
if (options.isUsingTsSolutionConfig) {
|
||||
await addProjectToTsSolutionWorkspace(tree, options.appProjectRoot);
|
||||
}
|
||||
|
||||
updateTsConfigOptions(tree, options);
|
||||
|
||||
if (options.linter === 'eslint') {
|
||||
const lintTask = await addLintingToApplication(tree, options);
|
||||
tasks.push(lintTask);
|
||||
}
|
||||
|
||||
if (options.unitTestRunner === 'jest') {
|
||||
const jestTask = await configurationGenerator(tree, {
|
||||
...options,
|
||||
project: options.name,
|
||||
setupFile: 'none',
|
||||
skipSerializers: true,
|
||||
supportTsx: options.js,
|
||||
testEnvironment: 'node',
|
||||
compiler: options.swcJest ? 'swc' : 'tsc',
|
||||
skipFormat: true,
|
||||
});
|
||||
tasks.push(jestTask);
|
||||
// There are no tests by default, so set `--passWithNoTests` to avoid test failure on new project.
|
||||
const projectConfig = readProjectConfiguration(tree, options.name);
|
||||
projectConfig.targets ??= {};
|
||||
projectConfig.targets.test = {
|
||||
...projectConfig.targets.test,
|
||||
options: {
|
||||
...projectConfig.targets.test?.options,
|
||||
passWithNoTests: true,
|
||||
},
|
||||
};
|
||||
updateProjectConfiguration(tree, options.name, projectConfig);
|
||||
} else {
|
||||
// No need for default spec file if unit testing is not setup.
|
||||
tree.delete(
|
||||
joinPathFragments(options.appProjectRoot, 'src/app/app.spec.ts')
|
||||
);
|
||||
}
|
||||
|
||||
if (options.e2eTestRunner === 'jest') {
|
||||
const e2eTask = await e2eProjectGenerator(tree, {
|
||||
...options,
|
||||
projectType: options.framework === 'none' ? 'cli' : 'server',
|
||||
name: options.rootProject ? 'e2e' : `${options.name}-e2e`,
|
||||
directory: options.rootProject ? 'e2e' : `${options.appProjectRoot}-e2e`,
|
||||
project: options.name,
|
||||
port: options.port,
|
||||
isNest: options.isNest,
|
||||
skipFormat: true,
|
||||
});
|
||||
tasks.push(e2eTask);
|
||||
}
|
||||
|
||||
if (options.js) {
|
||||
updateTsConfigsToJs(tree, { projectRoot: options.appProjectRoot });
|
||||
}
|
||||
|
||||
if (options.frontendProject) {
|
||||
addProxy(tree, options);
|
||||
}
|
||||
|
||||
if (options.docker) {
|
||||
const dockerTask = await setupDockerGenerator(tree, {
|
||||
...options,
|
||||
project: options.name,
|
||||
skipFormat: true,
|
||||
skipDockerPlugin: options.skipDockerPlugin ?? false,
|
||||
});
|
||||
|
||||
tasks.push(dockerTask);
|
||||
}
|
||||
|
||||
if (options.isUsingTsSolutionConfig) {
|
||||
updateTsconfigFiles(
|
||||
tree,
|
||||
options.appProjectRoot,
|
||||
'tsconfig.app.json',
|
||||
{
|
||||
module: 'nodenext',
|
||||
moduleResolution: 'nodenext',
|
||||
},
|
||||
options.linter === 'eslint'
|
||||
? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs']
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
sortPackageJsonFields(tree, options.appProjectRoot);
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
tasks.push(() => {
|
||||
logShowProjectCommand(options.name);
|
||||
});
|
||||
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
export default applicationGenerator;
|
||||
@@ -0,0 +1 @@
|
||||
console.log('Hello World');
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "<%= offset %>dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "<%= rootTsConfigPath %>",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<%_ if (webpackPluginOptions) { _%>
|
||||
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = {
|
||||
output: {
|
||||
path: join(__dirname, '<%= webpackPluginOptions.outputPath %>'),
|
||||
clean: true,
|
||||
...(process.env.NODE_ENV !== 'production' && {
|
||||
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
|
||||
}),
|
||||
},
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin({
|
||||
target: 'node',
|
||||
compiler: 'tsc',
|
||||
main: '<%= webpackPluginOptions.main %>',
|
||||
tsConfig: '<%= webpackPluginOptions.tsConfig %>',
|
||||
assets: <%- JSON.stringify(webpackPluginOptions.assets) %>,
|
||||
optimization: false,
|
||||
outputHashing: 'none',
|
||||
generatePackageJson: <%= webpackPluginOptions.generatePackageJson %>,
|
||||
sourceMap: true,
|
||||
})
|
||||
],
|
||||
};
|
||||
<%_ } else { _%>
|
||||
const { composePlugins, withNx} = require('@nx/webpack');
|
||||
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins(
|
||||
withNx({
|
||||
target: 'node',
|
||||
}),
|
||||
(config) => {
|
||||
config.output = {
|
||||
...config.output,
|
||||
...(process.env.NODE_ENV !== 'production' && {
|
||||
clean: true,
|
||||
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
|
||||
}),
|
||||
};
|
||||
config.devtool = 'source-map';
|
||||
// Update the webpack config as needed here.
|
||||
// e.g. `config.plugins.push(new MyPlugin())`
|
||||
return config;
|
||||
}
|
||||
);
|
||||
<%_ } _%>
|
||||
@@ -0,0 +1,14 @@
|
||||
import express from 'express';
|
||||
|
||||
const host = process.env.HOST ?? 'localhost';
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : <%= port %>;
|
||||
|
||||
const app = express();
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send({ 'message': 'Hello API'});
|
||||
});
|
||||
|
||||
app.listen(port, host, () => {
|
||||
console.log(`[ ready ] http://${host}:${port}`);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import Fastify, { FastifyInstance } from 'fastify';
|
||||
import { app } from './app';
|
||||
|
||||
describe('GET /', () => {
|
||||
let server: FastifyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
server = Fastify();
|
||||
server.register(app);
|
||||
});
|
||||
|
||||
it('should respond with a message', async () => {
|
||||
const response = await server.inject({
|
||||
method: 'GET',
|
||||
url: '/'
|
||||
});
|
||||
|
||||
expect(response.json()).toEqual({ message: 'Hello API' })
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as path from 'path';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import AutoLoad from '@fastify/autoload';
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
export interface AppOptions { }
|
||||
|
||||
export async function app(fastify: FastifyInstance, opts: AppOptions) {
|
||||
// Place here your custom code!
|
||||
|
||||
// Do not touch the following lines
|
||||
|
||||
// This loads all plugins defined in plugins
|
||||
// those should be support plugins that are reused
|
||||
// through your application
|
||||
fastify.register(AutoLoad, {
|
||||
dir: path.join(__dirname, 'plugins'),
|
||||
options: { ...opts },
|
||||
});
|
||||
|
||||
// This loads all plugins defined in routes
|
||||
// define your routes in one of these
|
||||
fastify.register(AutoLoad, {
|
||||
dir: path.join(__dirname, 'routes'),
|
||||
options: { ...opts },
|
||||
});
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import fp from 'fastify-plugin';
|
||||
import sensible from '@fastify/sensible';
|
||||
|
||||
/**
|
||||
* This plugins adds some utilities to handle http errors
|
||||
*
|
||||
* @see https://github.com/fastify/fastify-sensible
|
||||
*/
|
||||
export default fp(async function(fastify: FastifyInstance) {
|
||||
fastify.register(sensible);
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
|
||||
export default async function(fastify: FastifyInstance) {
|
||||
fastify.get('/', async function() {
|
||||
return { message: 'Hello API' };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import Fastify from 'fastify';
|
||||
import { app } from './app/app';
|
||||
|
||||
const host = process.env.HOST ?? 'localhost';
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : <%= port %>;
|
||||
|
||||
// Instantiate Fastify with some config
|
||||
const server = Fastify({
|
||||
logger: true,
|
||||
});
|
||||
|
||||
// Register your application as a normal plugin.
|
||||
server.register(app);
|
||||
|
||||
// Start listening.
|
||||
server.listen({ port, host }, (err) => {
|
||||
if (err) {
|
||||
server.log.error(err);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(`[ ready ] http://${host}:${port}`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import koa from 'koa';
|
||||
|
||||
const host = process.env.HOST ?? 'localhost';
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : <%= port %>;
|
||||
|
||||
const app = new koa();
|
||||
|
||||
app.use(async ctx =>{
|
||||
ctx.body = { 'message': 'Hello API'};
|
||||
});
|
||||
|
||||
app.listen(port, host, () => {
|
||||
console.log(`[ ready ] http://${host}:${port}`);
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
GeneratorCallback,
|
||||
Tree,
|
||||
} from '@nx/devkit';
|
||||
import { esbuildVersion } from '@nx/js/internal';
|
||||
import {
|
||||
expressVersions,
|
||||
fastifyVersions,
|
||||
koaVersions,
|
||||
nodeTypesVersions,
|
||||
nxVersion,
|
||||
tslibVersion,
|
||||
} from '../../../utils/versions';
|
||||
import { NormalizedSchema } from './normalized-schema';
|
||||
|
||||
export function addProjectDependencies(
|
||||
tree: Tree,
|
||||
options: NormalizedSchema
|
||||
): {
|
||||
installTask: GeneratorCallback;
|
||||
frameworkDependencies: Record<string, string>;
|
||||
} {
|
||||
const bundlers = {
|
||||
webpack: {
|
||||
'@nx/webpack': nxVersion,
|
||||
},
|
||||
esbuild: {
|
||||
'@nx/esbuild': nxVersion,
|
||||
esbuild: esbuildVersion,
|
||||
},
|
||||
};
|
||||
|
||||
const exprPkgVersions = expressVersions(tree);
|
||||
const koaPkgVersions = koaVersions(tree);
|
||||
const fastifyPkgVersions = fastifyVersions(tree);
|
||||
|
||||
const frameworkDependencies = {
|
||||
express: {
|
||||
express: exprPkgVersions.expressVersion,
|
||||
},
|
||||
koa: {
|
||||
koa: koaPkgVersions.koaVersion,
|
||||
},
|
||||
fastify: {
|
||||
fastify: fastifyPkgVersions.fastifyVersion,
|
||||
'fastify-plugin': fastifyPkgVersions.fastifyPluginVersion,
|
||||
'@fastify/autoload': fastifyPkgVersions.fastifyAutoloadVersion,
|
||||
'@fastify/sensible': fastifyPkgVersions.fastifySensibleVersion,
|
||||
},
|
||||
};
|
||||
const frameworkDevDependencies = {
|
||||
express: {
|
||||
'@types/express': exprPkgVersions.expressTypingsVersion,
|
||||
},
|
||||
koa: {
|
||||
'@types/koa': koaPkgVersions.koaTypingsVersion,
|
||||
},
|
||||
fastify: {},
|
||||
};
|
||||
|
||||
const typesNodeVersion = nodeTypesVersions(tree).typesNodeVersion;
|
||||
|
||||
return {
|
||||
installTask: addDependenciesToPackageJson(
|
||||
tree,
|
||||
{
|
||||
...frameworkDependencies[options.framework],
|
||||
tslib: tslibVersion,
|
||||
},
|
||||
{
|
||||
...frameworkDevDependencies[options.framework],
|
||||
...bundlers[options.bundler],
|
||||
'@types/node': typesNodeVersion,
|
||||
},
|
||||
undefined,
|
||||
options.keepExistingVersions ?? true
|
||||
),
|
||||
frameworkDependencies: frameworkDependencies[options.framework],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { GeneratorCallback, joinPathFragments, Tree } from '@nx/devkit';
|
||||
import { lintProjectGenerator } from '@nx/eslint';
|
||||
import { NormalizedSchema } from './normalized-schema';
|
||||
|
||||
export async function addLintingToApplication(
|
||||
tree: Tree,
|
||||
options: NormalizedSchema
|
||||
): Promise<GeneratorCallback> {
|
||||
const lintTask = await lintProjectGenerator(tree, {
|
||||
linter: options.linter,
|
||||
project: options.name,
|
||||
tsConfigPaths: [
|
||||
joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
|
||||
],
|
||||
unitTestRunner: options.unitTestRunner,
|
||||
skipFormat: true,
|
||||
setParserOptionsProject: options.setParserOptionsProject,
|
||||
rootProject: options.rootProject,
|
||||
addPlugin: options.addPlugin,
|
||||
});
|
||||
|
||||
return lintTask;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
logger,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { NormalizedSchema } from './normalized-schema';
|
||||
|
||||
export function addProxy(tree: Tree, options: NormalizedSchema) {
|
||||
const projectConfig = readProjectConfiguration(tree, options.frontendProject);
|
||||
if (
|
||||
projectConfig.targets &&
|
||||
['serve', 'dev'].find((t) => !!projectConfig.targets[t])
|
||||
) {
|
||||
const targetName = ['serve', 'dev'].find((t) => !!projectConfig.targets[t]);
|
||||
projectConfig.targets[targetName].dependsOn = [
|
||||
...(projectConfig.targets[targetName].dependsOn ?? []),
|
||||
`${options.name}:serve`,
|
||||
];
|
||||
const pathToProxyFile = `${projectConfig.root}/proxy.conf.json`;
|
||||
projectConfig.targets[targetName].options = {
|
||||
...projectConfig.targets[targetName].options,
|
||||
proxyConfig: pathToProxyFile,
|
||||
};
|
||||
|
||||
if (!tree.exists(pathToProxyFile)) {
|
||||
tree.write(
|
||||
pathToProxyFile,
|
||||
JSON.stringify(
|
||||
{
|
||||
'/api': {
|
||||
target: `http://localhost:${options.port}`,
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
} else {
|
||||
//add new entry to existing config
|
||||
const proxyFileContent = tree.read(pathToProxyFile).toString();
|
||||
|
||||
const proxyModified = {
|
||||
...JSON.parse(proxyFileContent),
|
||||
[`/${options.name}-api`]: {
|
||||
target: `http://localhost:${options.port}`,
|
||||
secure: false,
|
||||
},
|
||||
};
|
||||
|
||||
tree.write(pathToProxyFile, JSON.stringify(proxyModified, null, 2));
|
||||
}
|
||||
|
||||
updateProjectConfiguration(tree, options.frontendProject, projectConfig);
|
||||
} else {
|
||||
logger.warn(
|
||||
`Skip updating proxy for frontend project "${options.frontendProject}" since "serve" target is not found in project.json. For more information, see: https://nx.dev/recipes/node/application-proxies.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
generateFiles,
|
||||
joinPathFragments,
|
||||
offsetFromRoot,
|
||||
toJS,
|
||||
Tree,
|
||||
} from '@nx/devkit';
|
||||
import { getRelativePathToRootTsConfig } from '@nx/js';
|
||||
import { join } from 'path';
|
||||
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
|
||||
import { addVSCodeDebugConfiguration } from '../../../utils/vscode-debug-config';
|
||||
import { NormalizedSchema } from './normalized-schema';
|
||||
|
||||
export function addAppFiles(tree: Tree, options: NormalizedSchema) {
|
||||
generateFiles(
|
||||
tree,
|
||||
join(__dirname, '../files/common'),
|
||||
options.appProjectRoot,
|
||||
{
|
||||
...options,
|
||||
tmpl: '',
|
||||
name: options.name,
|
||||
root: options.appProjectRoot,
|
||||
offset: offsetFromRoot(options.appProjectRoot),
|
||||
rootTsConfigPath: getRelativePathToRootTsConfig(
|
||||
tree,
|
||||
options.appProjectRoot
|
||||
),
|
||||
webpackPluginOptions:
|
||||
hasWebpackPlugin(tree) && options.addPlugin !== false
|
||||
? {
|
||||
outputPath: options.isUsingTsSolutionConfig
|
||||
? 'dist'
|
||||
: joinPathFragments(
|
||||
offsetFromRoot(options.appProjectRoot),
|
||||
'dist',
|
||||
options.rootProject ? options.name : options.appProjectRoot
|
||||
),
|
||||
main: './src/main' + (options.js ? '.js' : '.ts'),
|
||||
tsConfig: './tsconfig.app.json',
|
||||
assets: ['./src/assets'],
|
||||
generatePackageJson: !options.isUsingTsSolutionConfig,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
);
|
||||
|
||||
if (options.bundler !== 'webpack') {
|
||||
tree.delete(joinPathFragments(options.appProjectRoot, 'webpack.config.js'));
|
||||
}
|
||||
|
||||
if (options.framework && options.framework !== 'none') {
|
||||
generateFiles(
|
||||
tree,
|
||||
join(__dirname, `../files/${options.framework}`),
|
||||
options.appProjectRoot,
|
||||
{
|
||||
...options,
|
||||
tmpl: '',
|
||||
name: options.name,
|
||||
root: options.appProjectRoot,
|
||||
offset: offsetFromRoot(options.appProjectRoot),
|
||||
rootTsConfigPath: getRelativePathToRootTsConfig(
|
||||
tree,
|
||||
options.appProjectRoot
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
if (options.js) {
|
||||
toJS(tree);
|
||||
}
|
||||
|
||||
// Generate a debug config for VS Code so that users can easily debug their application
|
||||
addVSCodeDebugConfiguration(tree, {
|
||||
projectName: options.name,
|
||||
projectRoot: options.appProjectRoot,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { addBuildTargetDefaults } from '@nx/devkit/internal';
|
||||
import {
|
||||
addProjectConfiguration,
|
||||
joinPathFragments,
|
||||
ProjectConfiguration,
|
||||
Tree,
|
||||
writeJson,
|
||||
} from '@nx/devkit';
|
||||
import { TS_SOLUTION_SETUP_TSCONFIG_INPUT } from '@nx/js/internal';
|
||||
import type { PackageJson } from 'nx/src/utils/package-json';
|
||||
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
|
||||
import { NormalizedSchema } from './normalized-schema';
|
||||
import {
|
||||
getEsBuildConfig,
|
||||
getNestWebpackBuildConfig,
|
||||
getServeConfig,
|
||||
getWebpackBuildConfig,
|
||||
getPruneTargets,
|
||||
} from './create-targets';
|
||||
|
||||
export function addProject(
|
||||
tree: Tree,
|
||||
options: NormalizedSchema,
|
||||
frameworkDependencies: Record<string, string>
|
||||
) {
|
||||
const project: ProjectConfiguration = {
|
||||
root: options.appProjectRoot,
|
||||
sourceRoot: joinPathFragments(options.appProjectRoot, 'src'),
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
tags: options.parsedTags,
|
||||
};
|
||||
|
||||
if (options.bundler === 'esbuild') {
|
||||
addBuildTargetDefaults(tree, '@nx/esbuild:esbuild', 'build', [
|
||||
TS_SOLUTION_SETUP_TSCONFIG_INPUT,
|
||||
]);
|
||||
project.targets.build = getEsBuildConfig(tree, project, options);
|
||||
} else if (options.bundler === 'webpack') {
|
||||
if (!hasWebpackPlugin(tree) && options.addPlugin === false) {
|
||||
addBuildTargetDefaults(tree, `@nx/webpack:webpack`, 'build', [
|
||||
TS_SOLUTION_SETUP_TSCONFIG_INPUT,
|
||||
]);
|
||||
project.targets.build = getWebpackBuildConfig(tree, project, options);
|
||||
} else if (options.isNest) {
|
||||
// If we are using Nest that has the webpack plugin we need to override the
|
||||
// build target so that NODE_ENV can be set to production or development so the serve target can be run in development mode
|
||||
project.targets.build = getNestWebpackBuildConfig(project);
|
||||
}
|
||||
}
|
||||
project.targets = {
|
||||
...project.targets,
|
||||
...getPruneTargets('build', options.outputPath),
|
||||
};
|
||||
project.targets.serve = getServeConfig(options);
|
||||
|
||||
const packageJson: PackageJson = {
|
||||
name: options.importPath,
|
||||
version: '0.0.1',
|
||||
private: true,
|
||||
dependencies: { ...frameworkDependencies },
|
||||
};
|
||||
|
||||
if (!options.useProjectJson) {
|
||||
packageJson.nx = {
|
||||
name: options.name !== options.importPath ? options.name : undefined,
|
||||
targets: project.targets,
|
||||
tags: project.tags?.length ? project.tags : undefined,
|
||||
};
|
||||
} else {
|
||||
addProjectConfiguration(tree, options.name, project);
|
||||
}
|
||||
|
||||
if (!options.useProjectJson || options.isUsingTsSolutionConfig) {
|
||||
writeJson(
|
||||
tree,
|
||||
joinPathFragments(options.appProjectRoot, 'package.json'),
|
||||
packageJson
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import {
|
||||
joinPathFragments,
|
||||
ProjectConfiguration,
|
||||
Tree,
|
||||
TargetConfiguration,
|
||||
detectPackageManager,
|
||||
} from '@nx/devkit';
|
||||
import { getProjectSourceRoot } from '@nx/js/internal';
|
||||
import { NormalizedSchema } from './normalized-schema';
|
||||
import { getLockFileName } from '@nx/js';
|
||||
|
||||
export function getWebpackBuildConfig(
|
||||
tree: Tree,
|
||||
project: ProjectConfiguration,
|
||||
options: NormalizedSchema
|
||||
): TargetConfiguration {
|
||||
const sourceRoot = getProjectSourceRoot(project, tree);
|
||||
return {
|
||||
executor: `@nx/webpack:webpack`,
|
||||
outputs: ['{options.outputPath}'],
|
||||
defaultConfiguration: 'production',
|
||||
options: {
|
||||
target: 'node',
|
||||
compiler: 'tsc',
|
||||
outputPath: options.outputPath,
|
||||
main: joinPathFragments(
|
||||
sourceRoot,
|
||||
'main' + (options.js ? '.js' : '.ts')
|
||||
),
|
||||
tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
|
||||
assets: [joinPathFragments(sourceRoot, 'assets')],
|
||||
webpackConfig: joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
'webpack.config.js'
|
||||
),
|
||||
generatePackageJson: options.isUsingTsSolutionConfig ? undefined : true,
|
||||
},
|
||||
configurations: {
|
||||
development: {
|
||||
outputHashing: 'none',
|
||||
},
|
||||
production: {
|
||||
...(options.docker && { generateLockfile: true }),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getEsBuildConfig(
|
||||
tree: Tree,
|
||||
project: ProjectConfiguration,
|
||||
options: NormalizedSchema
|
||||
): TargetConfiguration {
|
||||
const sourceRoot = getProjectSourceRoot(project, tree);
|
||||
return {
|
||||
executor: '@nx/esbuild:esbuild',
|
||||
outputs: ['{options.outputPath}'],
|
||||
defaultConfiguration: 'production',
|
||||
options: {
|
||||
platform: 'node',
|
||||
outputPath: options.outputPath,
|
||||
// Use CJS for Node apps for widest compatibility.
|
||||
format: ['cjs'],
|
||||
bundle: false,
|
||||
main: joinPathFragments(
|
||||
sourceRoot,
|
||||
'main' + (options.js ? '.js' : '.ts')
|
||||
),
|
||||
tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
|
||||
assets: [joinPathFragments(sourceRoot, 'assets')],
|
||||
generatePackageJson: options.isUsingTsSolutionConfig ? undefined : true,
|
||||
esbuildOptions: {
|
||||
sourcemap: true,
|
||||
// Generate CJS files as .js so imports can be './foo' rather than './foo.cjs'.
|
||||
outExtension: { '.js': '.js' },
|
||||
},
|
||||
},
|
||||
configurations: {
|
||||
development: {},
|
||||
production: {
|
||||
...(options.docker && { generateLockfile: true }),
|
||||
esbuildOptions: {
|
||||
sourcemap: false,
|
||||
// Generate CJS files as .js so imports can be './foo' rather than './foo.cjs'.
|
||||
outExtension: { '.js': '.js' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getServeConfig(options: NormalizedSchema): TargetConfiguration {
|
||||
return {
|
||||
continuous: true,
|
||||
executor: '@nx/js:node',
|
||||
defaultConfiguration: 'development',
|
||||
// Run build, which includes dependency on "^build" by default, so the first run
|
||||
// won't error out due to missing build artifacts.
|
||||
dependsOn: ['build'],
|
||||
options: {
|
||||
buildTarget: `${options.name}:build`,
|
||||
// Even though `false` is the default, set this option so users know it
|
||||
// exists if they want to always run dependencies during each rebuild.
|
||||
runBuildTargetDependencies: false,
|
||||
},
|
||||
configurations: {
|
||||
development: {
|
||||
buildTarget: `${options.name}:build:development`,
|
||||
},
|
||||
production: {
|
||||
buildTarget: `${options.name}:build:production`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getNestWebpackBuildConfig(
|
||||
project: ProjectConfiguration
|
||||
): TargetConfiguration {
|
||||
return {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'webpack-cli build',
|
||||
env: { NODE_ENV: 'production' },
|
||||
cwd: project.root,
|
||||
},
|
||||
configurations: {
|
||||
development: {
|
||||
env: { NODE_ENV: 'development' },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getPruneTargets(
|
||||
buildTarget: string,
|
||||
outputPath: string
|
||||
): {
|
||||
prune: TargetConfiguration;
|
||||
'prune-lockfile': TargetConfiguration;
|
||||
'copy-workspace-modules': TargetConfiguration;
|
||||
} {
|
||||
const lockFileName =
|
||||
getLockFileName(detectPackageManager() ?? 'npm') ?? 'package-lock.json';
|
||||
return {
|
||||
'prune-lockfile': {
|
||||
dependsOn: ['build'],
|
||||
cache: true,
|
||||
executor: '@nx/js:prune-lockfile',
|
||||
outputs: [
|
||||
`{workspaceRoot}/${joinPathFragments(outputPath, 'package.json')}`,
|
||||
`{workspaceRoot}/${joinPathFragments(outputPath, lockFileName)}`,
|
||||
],
|
||||
options: {
|
||||
buildTarget,
|
||||
},
|
||||
},
|
||||
'copy-workspace-modules': {
|
||||
dependsOn: ['build'],
|
||||
cache: true,
|
||||
outputs: [
|
||||
`{workspaceRoot}/${joinPathFragments(outputPath, 'workspace_modules')}`,
|
||||
],
|
||||
executor: '@nx/js:copy-workspace-modules',
|
||||
options: {
|
||||
buildTarget,
|
||||
},
|
||||
},
|
||||
prune: {
|
||||
dependsOn: ['prune-lockfile', 'copy-workspace-modules'],
|
||||
executor: 'nx:noop',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './normalized-schema';
|
||||
export * from './normalize-options';
|
||||
export * from './create-targets';
|
||||
export * from './create-project';
|
||||
export * from './create-files';
|
||||
export * from './add-dependencies';
|
||||
export * from './add-linting';
|
||||
export * from './add-proxy';
|
||||
@@ -0,0 +1,71 @@
|
||||
import { joinPathFragments, names, readNxJson, Tree } from '@nx/devkit';
|
||||
import {
|
||||
determineProjectNameAndRootOptions,
|
||||
ensureRootProjectName,
|
||||
} from '@nx/devkit/internal';
|
||||
import { isUsingTsSolutionSetup } from '@nx/js/internal';
|
||||
import { Schema } from '../schema';
|
||||
import { NormalizedSchema } from './normalized-schema';
|
||||
|
||||
export async function normalizeOptions(
|
||||
host: Tree,
|
||||
options: Schema
|
||||
): Promise<NormalizedSchema> {
|
||||
await ensureRootProjectName(options, 'application');
|
||||
const {
|
||||
projectName,
|
||||
projectRoot: appProjectRoot,
|
||||
importPath,
|
||||
} = await determineProjectNameAndRootOptions(host, {
|
||||
name: options.name,
|
||||
projectType: 'application',
|
||||
directory: options.directory,
|
||||
rootProject: options.rootProject,
|
||||
});
|
||||
options.rootProject = appProjectRoot === '.';
|
||||
|
||||
options.bundler = options.bundler ?? 'esbuild';
|
||||
options.e2eTestRunner = options.e2eTestRunner ?? 'jest';
|
||||
|
||||
const parsedTags = options.tags
|
||||
? options.tags.split(',').map((s) => s.trim())
|
||||
: [];
|
||||
|
||||
const nxJson = readNxJson(host);
|
||||
const addPlugin =
|
||||
options.addPlugin ??
|
||||
(process.env.NX_ADD_PLUGINS !== 'false' &&
|
||||
nxJson.useInferencePlugins !== false);
|
||||
|
||||
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
|
||||
const swcJest = options.swcJest ?? isUsingTsSolutionConfig;
|
||||
|
||||
const appProjectName =
|
||||
!isUsingTsSolutionConfig || options.name ? projectName : importPath;
|
||||
const useProjectJson = options.useProjectJson ?? !isUsingTsSolutionConfig;
|
||||
|
||||
return {
|
||||
...options,
|
||||
addPlugin,
|
||||
name: appProjectName,
|
||||
frontendProject: options.frontendProject
|
||||
? names(options.frontendProject).fileName
|
||||
: undefined,
|
||||
appProjectRoot,
|
||||
importPath,
|
||||
parsedTags,
|
||||
linter: options.linter ?? 'eslint',
|
||||
unitTestRunner: options.unitTestRunner ?? 'jest',
|
||||
rootProject: options.rootProject ?? false,
|
||||
port: options.port ?? 3000,
|
||||
outputPath: isUsingTsSolutionConfig
|
||||
? joinPathFragments(appProjectRoot, 'dist')
|
||||
: joinPathFragments(
|
||||
'dist',
|
||||
options.rootProject ? appProjectName : appProjectRoot
|
||||
),
|
||||
isUsingTsSolutionConfig,
|
||||
swcJest,
|
||||
useProjectJson,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Schema } from '../schema';
|
||||
|
||||
export interface NormalizedSchema extends Omit<Schema, 'useTsSolution'> {
|
||||
appProjectRoot: string;
|
||||
parsedTags: string[];
|
||||
outputPath: string;
|
||||
importPath: string;
|
||||
isUsingTsSolutionConfig: boolean;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Linter, LinterType } from '@nx/eslint';
|
||||
|
||||
export interface Schema {
|
||||
directory: string;
|
||||
name?: string;
|
||||
skipFormat?: boolean;
|
||||
skipPackageJson?: boolean;
|
||||
unitTestRunner?: 'jest' | 'none';
|
||||
e2eTestRunner?: 'jest' | 'none';
|
||||
linter?: Linter | LinterType;
|
||||
formatter?: 'none' | 'prettier';
|
||||
tags?: string;
|
||||
frontendProject?: string;
|
||||
swcJest?: boolean;
|
||||
/** @deprecated use `swcJest` instead */
|
||||
babelJest?: boolean;
|
||||
js?: boolean;
|
||||
setParserOptionsProject?: boolean;
|
||||
bundler?: 'esbuild' | 'webpack';
|
||||
framework?: NodeJsFrameWorks;
|
||||
port?: number;
|
||||
rootProject?: boolean;
|
||||
docker?: boolean;
|
||||
skipDockerPlugin?: boolean;
|
||||
isNest?: boolean;
|
||||
addPlugin?: boolean;
|
||||
useTsSolution?: boolean;
|
||||
useProjectJson?: boolean;
|
||||
keepExistingVersions?: boolean;
|
||||
}
|
||||
|
||||
export type NodeJsFrameWorks = 'express' | 'koa' | 'fastify' | 'nest' | 'none';
|
||||
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"cli": "nx",
|
||||
"$id": "SchematicsNxNodeApp",
|
||||
"title": "Nx Application Options Schema",
|
||||
"description": "Nx Application Options Schema.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"description": "The directory of the new application.",
|
||||
"type": "string",
|
||||
"$default": {
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "Which directory do you want to create the application in?"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the application.",
|
||||
"type": "string",
|
||||
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"skipFormat": {
|
||||
"description": "Skip formatting files",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"skipPackageJson": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Do not add dependencies to `package.json`.",
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"linter": {
|
||||
"description": "The tool to use for running lint checks.",
|
||||
"type": "string",
|
||||
"enum": ["eslint", "none"],
|
||||
"default": "none",
|
||||
"x-prompt": "Which linter would you like to use?",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"unitTestRunner": {
|
||||
"type": "string",
|
||||
"enum": ["jest", "none"],
|
||||
"description": "Test runner to use for unit tests.",
|
||||
"default": "none",
|
||||
"x-priority": "important",
|
||||
"x-prompt": "Which unit test runner would you like to use?"
|
||||
},
|
||||
"e2eTestRunner": {
|
||||
"type": "string",
|
||||
"enum": ["jest", "none"],
|
||||
"description": "Test runner to use for end-to-end tests",
|
||||
"default": "none",
|
||||
"x-priority": "important",
|
||||
"x-prompt": "Which end-to-end test runner would you like to use?"
|
||||
},
|
||||
"tags": {
|
||||
"type": "string",
|
||||
"description": "Add tags to the application (used for linting)."
|
||||
},
|
||||
"frontendProject": {
|
||||
"type": "string",
|
||||
"description": "Frontend project that needs to access this application. This sets up proxy configuration.",
|
||||
"x-priority": "important",
|
||||
"x-dropdown": "projects"
|
||||
},
|
||||
"swcJest": {
|
||||
"type": "boolean",
|
||||
"description": "Use `@swc/jest` instead `ts-jest` for faster test compilation."
|
||||
},
|
||||
"babelJest": {
|
||||
"type": "boolean",
|
||||
"description": "Use `babel` instead `ts-jest`.",
|
||||
"default": false,
|
||||
"x-deprecated": "Use --swcJest instead for faster compilation"
|
||||
},
|
||||
"js": {
|
||||
"type": "boolean",
|
||||
"description": "Generate JavaScript files rather than TypeScript files.",
|
||||
"default": false
|
||||
},
|
||||
"setParserOptionsProject": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.",
|
||||
"default": false
|
||||
},
|
||||
"bundler": {
|
||||
"description": "Bundler which is used to package the application",
|
||||
"type": "string",
|
||||
"enum": ["esbuild", "webpack"],
|
||||
"default": "esbuild",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"framework": {
|
||||
"description": "Generate the node application using a framework",
|
||||
"type": "string",
|
||||
"enum": ["express", "fastify", "koa", "nest", "none"],
|
||||
"default": "none",
|
||||
"x-prompt": "Which framework do you want to use?",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"port": {
|
||||
"description": "The port which the server will be run on",
|
||||
"type": "number",
|
||||
"default": 3000
|
||||
},
|
||||
"rootProject": {
|
||||
"description": "Create node application at the root of the workspace",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"hidden": true,
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"docker": {
|
||||
"type": "boolean",
|
||||
"description": "Add a docker build target"
|
||||
},
|
||||
"skipDockerPlugin": {
|
||||
"type": "boolean",
|
||||
"description": "Skip the @nx/docker plugin and use the legacy docker build target instead.",
|
||||
"default": false
|
||||
},
|
||||
"useProjectJson": {
|
||||
"type": "boolean",
|
||||
"description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file."
|
||||
},
|
||||
"keepExistingVersions": {
|
||||
"type": "boolean",
|
||||
"x-priority": "internal",
|
||||
"description": "Keep existing dependencies versions",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": ["directory"]
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
import { readJson, Tree, updateJson, writeJson } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { applicationGenerator } from '../application/application';
|
||||
import { e2eProjectGenerator } from './e2e-project';
|
||||
|
||||
describe('e2eProjectGenerator', () => {
|
||||
let tree: Tree;
|
||||
beforeEach(async () => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should generate default spec for server app (integrated)', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'api',
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
});
|
||||
await e2eProjectGenerator(tree, {
|
||||
projectType: 'server',
|
||||
project: 'api',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
expect(tree.exists(`api-e2e/src/api/api.spec.ts`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should generate default spec for server app (standalone)', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'api',
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
rootProject: true,
|
||||
addPlugin: true,
|
||||
});
|
||||
await e2eProjectGenerator(tree, {
|
||||
projectType: 'server',
|
||||
project: 'api',
|
||||
rootProject: true,
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
expect(tree.exists(`e2e/src/server/server.spec.ts`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should generate jest test config with ts-jest for server app', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'api',
|
||||
framework: 'none',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
});
|
||||
await e2eProjectGenerator(tree, {
|
||||
projectType: 'server',
|
||||
project: 'api',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
expect(tree.read('api-e2e/jest.config.cts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"export default {
|
||||
displayName: 'api-e2e',
|
||||
preset: '../jest.preset.js',
|
||||
globalSetup: '<rootDir>/src/support/global-setup.ts',
|
||||
globalTeardown: '<rootDir>/src/support/global-teardown.ts',
|
||||
setupFiles: ['<rootDir>/src/support/test-setup.ts'],
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\\\.[tj]s$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../coverage/api-e2e',
|
||||
};
|
||||
"
|
||||
`);
|
||||
expect(tree.exists('api-e2e/.spec.swcrc')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should generate cli project', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'api',
|
||||
framework: 'none',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
});
|
||||
await e2eProjectGenerator(tree, {
|
||||
projectType: 'cli',
|
||||
project: 'api',
|
||||
addPlugin: true,
|
||||
});
|
||||
expect(tree.read('api-e2e/src/api/api.spec.ts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"import { execSync } from 'child_process';
|
||||
import { join } from 'path';
|
||||
|
||||
describe('CLI tests', () => {
|
||||
it('should print a message', () => {
|
||||
const cliPath = join(process.cwd(), 'dist/api');
|
||||
|
||||
const output = execSync(\`node \${cliPath}\`).toString();
|
||||
|
||||
expect(output).toMatch(/Hello World/);
|
||||
});
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate jest test config with ts-jest for cli project', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'cli',
|
||||
framework: 'none',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
});
|
||||
await e2eProjectGenerator(tree, {
|
||||
projectType: 'cli',
|
||||
project: 'cli',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
expect(tree.read('cli-e2e/jest.config.cts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"export default {
|
||||
displayName: 'cli-e2e',
|
||||
preset: '../jest.preset.js',
|
||||
setupFiles: ['<rootDir>/src/test-setup.ts'],
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\\\.[tj]s$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../coverage/cli-e2e',
|
||||
};
|
||||
"
|
||||
`);
|
||||
expect(tree.exists('cli-e2e/.spec.swcrc')).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('TS solution setup', () => {
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
updateJson(tree, 'package.json', (json) => {
|
||||
json.workspaces = ['packages/*', 'apps/*'];
|
||||
return json;
|
||||
});
|
||||
writeJson(tree, 'tsconfig.base.json', {
|
||||
compilerOptions: {
|
||||
composite: true,
|
||||
declaration: true,
|
||||
},
|
||||
});
|
||||
writeJson(tree, 'tsconfig.json', {
|
||||
extends: './tsconfig.base.json',
|
||||
files: [],
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add project references when using TS solution', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'api',
|
||||
framework: 'none',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
useProjectJson: false,
|
||||
});
|
||||
await e2eProjectGenerator(tree, {
|
||||
projectType: 'server',
|
||||
project: '@proj/api',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"path": "./api",
|
||||
},
|
||||
{
|
||||
"path": "./api-e2e",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(readJson(tree, 'api-e2e/tsconfig.json')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": false,
|
||||
"outDir": "out-tsc/api-e2e",
|
||||
},
|
||||
"extends": "../tsconfig.base.json",
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.ts",
|
||||
],
|
||||
"references": [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate jest test config with @swc/jest for server app', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'api',
|
||||
framework: 'none',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
useProjectJson: false,
|
||||
});
|
||||
await e2eProjectGenerator(tree, {
|
||||
projectType: 'server',
|
||||
project: '@proj/api',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
expect(tree.read('api-e2e/jest.config.cts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"/* eslint-disable */
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
// Reading the SWC compilation config for the spec files
|
||||
const swcJestConfig = JSON.parse(
|
||||
readFileSync(\`\${__dirname}/.spec.swcrc\`, 'utf-8'),
|
||||
);
|
||||
|
||||
// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves
|
||||
swcJestConfig.swcrc = false;
|
||||
|
||||
export default {
|
||||
displayName: 'api-e2e',
|
||||
preset: '../jest.preset.js',
|
||||
globalSetup: '<rootDir>/src/support/global-setup.ts',
|
||||
globalTeardown: '<rootDir>/src/support/global-teardown.ts',
|
||||
setupFiles: ['<rootDir>/src/support/test-setup.ts'],
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\\\.[tj]s$': ['@swc/jest', swcJestConfig],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: 'test-output/jest/coverage',
|
||||
};
|
||||
"
|
||||
`);
|
||||
expect(tree.read('api-e2e/.spec.swcrc', 'utf-8')).toMatchInlineSnapshot(`
|
||||
"{
|
||||
"jsc": {
|
||||
"target": "es2017",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"transform": {
|
||||
"decoratorMetadata": true,
|
||||
"legacyDecorator": true
|
||||
},
|
||||
"keepClassNames": true,
|
||||
"externalHelpers": true,
|
||||
"loose": true
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"exclude": []
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate jest test config with @swc/jest for cli project', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'cli',
|
||||
framework: 'none',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
useProjectJson: false,
|
||||
});
|
||||
await e2eProjectGenerator(tree, {
|
||||
projectType: 'cli',
|
||||
project: '@proj/cli',
|
||||
addPlugin: true,
|
||||
useProjectJson: false,
|
||||
});
|
||||
|
||||
expect(tree.read('cli-e2e/jest.config.cts', 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"/* eslint-disable */
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
// Reading the SWC compilation config for the spec files
|
||||
const swcJestConfig = JSON.parse(
|
||||
readFileSync(\`\${__dirname}/.spec.swcrc\`, 'utf-8'),
|
||||
);
|
||||
|
||||
// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves
|
||||
swcJestConfig.swcrc = false;
|
||||
|
||||
export default {
|
||||
displayName: 'cli-e2e',
|
||||
preset: '../jest.preset.js',
|
||||
setupFiles: ['<rootDir>/src/test-setup.ts'],
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\\\.[tj]s$': ['@swc/jest', swcJestConfig],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: 'test-output/jest/coverage',
|
||||
};
|
||||
"
|
||||
`);
|
||||
expect(tree.read('cli-e2e/.spec.swcrc', 'utf-8')).toMatchInlineSnapshot(`
|
||||
"{
|
||||
"jsc": {
|
||||
"target": "es2017",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"transform": {
|
||||
"decoratorMetadata": true,
|
||||
"legacyDecorator": true
|
||||
},
|
||||
"keepClassNames": true,
|
||||
"externalHelpers": true,
|
||||
"loose": true
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"exclude": []
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,351 @@
|
||||
import {
|
||||
determineProjectNameAndRootOptions,
|
||||
logShowProjectCommand,
|
||||
} from '@nx/devkit/internal';
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
addProjectConfiguration,
|
||||
formatFiles,
|
||||
generateFiles,
|
||||
GeneratorCallback,
|
||||
joinPathFragments,
|
||||
names,
|
||||
offsetFromRoot,
|
||||
readNxJson,
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
writeJson,
|
||||
} from '@nx/devkit';
|
||||
import { lintProjectGenerator } from '@nx/eslint';
|
||||
import {
|
||||
javaScriptOverride,
|
||||
typeScriptOverride,
|
||||
addPluginsToLintConfig,
|
||||
isEslintConfigSupported,
|
||||
replaceOverridesInLintConfig,
|
||||
} from '@nx/eslint/internal';
|
||||
import * as path from 'path';
|
||||
import { axiosVersion } from '../../utils/versions';
|
||||
import { Schema } from './schema';
|
||||
import {
|
||||
findRootJestPreset,
|
||||
getInstalledJestMajorVersion,
|
||||
} from '@nx/jest/internal';
|
||||
import {
|
||||
addProjectToTsSolutionWorkspace,
|
||||
isUsingTsSolutionSetup,
|
||||
addSwcTestConfig,
|
||||
} from '@nx/js/internal';
|
||||
import { relative } from 'node:path/posix';
|
||||
import type { PackageJson } from 'nx/src/utils/package-json';
|
||||
|
||||
export async function e2eProjectGenerator(host: Tree, options: Schema) {
|
||||
return await e2eProjectGeneratorInternal(host, {
|
||||
addPlugin: false,
|
||||
useProjectJson: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export async function e2eProjectGeneratorInternal(
|
||||
host: Tree,
|
||||
_options: Schema
|
||||
) {
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
const options = await normalizeOptions(host, _options);
|
||||
const appProject = readProjectConfiguration(host, options.project);
|
||||
|
||||
// Detect Jest 30+ to use .cts config files (CommonJS TypeScript)
|
||||
const jestMajorVersion = getInstalledJestMajorVersion(host);
|
||||
const useCommonJsConfig = jestMajorVersion === null || jestMajorVersion >= 30;
|
||||
const jestConfigExt = useCommonJsConfig ? 'cts' : 'ts';
|
||||
|
||||
// TODO(@ndcunningham): This is broken.. the outputs are wrong.. and this isn't using the jest generator
|
||||
const packageJson: PackageJson = {
|
||||
name: options.importPath,
|
||||
version: '0.0.1',
|
||||
private: true,
|
||||
};
|
||||
|
||||
if (!options.useProjectJson) {
|
||||
packageJson.nx = {
|
||||
name:
|
||||
options.e2eProjectName !== options.importPath
|
||||
? options.e2eProjectName
|
||||
: undefined,
|
||||
implicitDependencies: [options.project],
|
||||
targets: {
|
||||
e2e: {
|
||||
executor: '@nx/jest:jest',
|
||||
outputs: ['{projectRoot}/test-output/jest/coverage'],
|
||||
options: {
|
||||
jestConfig: `${options.e2eProjectRoot}/jest.config.${jestConfigExt}`,
|
||||
passWithNoTests: true,
|
||||
},
|
||||
dependsOn: [`${options.project}:build`, `${options.project}:serve`],
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
addProjectConfiguration(host, options.e2eProjectName, {
|
||||
root: options.e2eProjectRoot,
|
||||
implicitDependencies: [options.project],
|
||||
projectType: 'application',
|
||||
targets: {
|
||||
e2e: {
|
||||
executor: '@nx/jest:jest',
|
||||
outputs: ['{workspaceRoot}/coverage/{e2eProjectRoot}'],
|
||||
options: {
|
||||
jestConfig: `${options.e2eProjectRoot}/jest.config.${jestConfigExt}`,
|
||||
passWithNoTests: true,
|
||||
},
|
||||
dependsOn: [`${options.project}:build`, `${options.project}:serve`],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.useProjectJson || options.isUsingTsSolutionConfig) {
|
||||
writeJson(
|
||||
host,
|
||||
joinPathFragments(options.e2eProjectRoot, 'package.json'),
|
||||
packageJson
|
||||
);
|
||||
}
|
||||
|
||||
// TODO(@nicholas): Find a better way to get build target
|
||||
|
||||
// We remove the 'test' target from the e2e project because it is not needed
|
||||
// The 'e2e' target is the one that should run the tests for the e2e project
|
||||
const nxJson = readNxJson(host);
|
||||
const hasPlugin = nxJson.plugins?.some((p) => {
|
||||
if (typeof p !== 'string' && p.plugin === '@nx/jest/plugin') {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasPlugin) {
|
||||
updateJson(host, 'nx.json', (json) => {
|
||||
return {
|
||||
...json,
|
||||
plugins: json.plugins?.map((p) => {
|
||||
if (typeof p !== 'string' && p.plugin === '@nx/jest/plugin') {
|
||||
return {
|
||||
...p,
|
||||
exclude: [...(p.exclude || []), `${options.e2eProjectRoot}/**/*`],
|
||||
};
|
||||
}
|
||||
return p;
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const jestPreset = findRootJestPreset(host) ?? 'jest.preset.js';
|
||||
const tsConfigFile = options.isUsingTsSolutionConfig
|
||||
? 'tsconfig.json'
|
||||
: 'tsconfig.spec.json';
|
||||
const rootOffset = offsetFromRoot(options.e2eProjectRoot);
|
||||
const coverageDirectory = options.isUsingTsSolutionConfig
|
||||
? 'test-output/jest/coverage'
|
||||
: joinPathFragments(rootOffset, 'coverage', options.e2eProjectName);
|
||||
const projectSimpleName = options.project.split('/').pop();
|
||||
if (options.projectType === 'server') {
|
||||
generateFiles(
|
||||
host,
|
||||
path.join(__dirname, 'files/server/common'),
|
||||
options.e2eProjectRoot,
|
||||
{
|
||||
...options,
|
||||
...names(options.rootProject ? 'server' : projectSimpleName),
|
||||
tsConfigFile,
|
||||
offsetFromRoot: rootOffset,
|
||||
jestPreset,
|
||||
coverageDirectory,
|
||||
tmpl: '',
|
||||
}
|
||||
);
|
||||
|
||||
if (options.isNest) {
|
||||
generateFiles(
|
||||
host,
|
||||
path.join(__dirname, 'files/server/nest'),
|
||||
options.e2eProjectRoot,
|
||||
{
|
||||
...options,
|
||||
...names(options.rootProject ? 'server' : projectSimpleName),
|
||||
tsConfigFile,
|
||||
offsetFromRoot: rootOffset,
|
||||
tmpl: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
} else if (options.projectType === 'cli') {
|
||||
const mainFile = appProject.targets.build?.options?.outputPath;
|
||||
generateFiles(
|
||||
host,
|
||||
path.join(__dirname, 'files/cli'),
|
||||
options.e2eProjectRoot,
|
||||
{
|
||||
...options,
|
||||
...names(options.rootProject ? 'cli' : projectSimpleName),
|
||||
mainFile,
|
||||
tsConfigFile,
|
||||
offsetFromRoot: rootOffset,
|
||||
jestPreset,
|
||||
coverageDirectory,
|
||||
tmpl: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Rename jest.config.ts to jest.config.cts for Jest 30+
|
||||
const jestConfigPath = joinPathFragments(
|
||||
options.e2eProjectRoot,
|
||||
'jest.config.ts'
|
||||
);
|
||||
if (host.exists(jestConfigPath) && useCommonJsConfig) {
|
||||
host.rename(
|
||||
jestConfigPath,
|
||||
joinPathFragments(options.e2eProjectRoot, 'jest.config.cts')
|
||||
);
|
||||
}
|
||||
|
||||
if (options.isUsingTsSolutionConfig) {
|
||||
addSwcTestConfig(host, options.e2eProjectRoot, 'es6');
|
||||
generateFiles(
|
||||
host,
|
||||
path.join(__dirname, 'files/ts-solution'),
|
||||
options.e2eProjectRoot,
|
||||
{
|
||||
...options,
|
||||
relativeProjectReferencePath: relative(
|
||||
options.e2eProjectRoot,
|
||||
appProject.root
|
||||
),
|
||||
offsetFromRoot: rootOffset,
|
||||
tmpl: '',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
generateFiles(
|
||||
host,
|
||||
path.join(__dirname, 'files/non-ts-solution'),
|
||||
options.e2eProjectRoot,
|
||||
{
|
||||
...options,
|
||||
offsetFromRoot: rootOffset,
|
||||
tmpl: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// axios is more than likely used in the application code, so install it as a regular dependency.
|
||||
const installTask = addDependenciesToPackageJson(
|
||||
host,
|
||||
{ axios: axiosVersion },
|
||||
{}
|
||||
);
|
||||
tasks.push(installTask);
|
||||
|
||||
if (options.linter === 'eslint') {
|
||||
const linterTask = await lintProjectGenerator(host, {
|
||||
project: options.e2eProjectName,
|
||||
linter: 'eslint',
|
||||
skipFormat: true,
|
||||
tsConfigPaths: [
|
||||
joinPathFragments(options.e2eProjectRoot, 'tsconfig.json'),
|
||||
],
|
||||
setParserOptionsProject: false,
|
||||
skipPackageJson: false,
|
||||
rootProject: options.rootProject,
|
||||
addPlugin: options.addPlugin,
|
||||
});
|
||||
tasks.push(linterTask);
|
||||
|
||||
if (options.rootProject && isEslintConfigSupported(host)) {
|
||||
addPluginsToLintConfig(host, options.e2eProjectRoot, '@nx');
|
||||
replaceOverridesInLintConfig(host, options.e2eProjectRoot, [
|
||||
typeScriptOverride,
|
||||
javaScriptOverride,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.isUsingTsSolutionConfig) {
|
||||
updateJson(host, 'tsconfig.json', (json) => {
|
||||
json.references ??= [];
|
||||
const e2eRef = `./${options.e2eProjectRoot}`;
|
||||
if (!json.references.find((ref) => ref.path === e2eRef)) {
|
||||
json.references.push({ path: e2eRef });
|
||||
}
|
||||
return json;
|
||||
});
|
||||
}
|
||||
|
||||
// If we are using the new TS solution
|
||||
// We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project
|
||||
if (options.isUsingTsSolutionConfig) {
|
||||
await addProjectToTsSolutionWorkspace(host, options.e2eProjectRoot);
|
||||
}
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(host);
|
||||
}
|
||||
|
||||
tasks.push(() => {
|
||||
logShowProjectCommand(options.e2eProjectName);
|
||||
});
|
||||
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
async function normalizeOptions(
|
||||
tree: Tree,
|
||||
options: Schema
|
||||
): Promise<
|
||||
Omit<Schema, 'name'> & {
|
||||
e2eProjectRoot: string;
|
||||
e2eProjectName: string;
|
||||
importPath: string;
|
||||
isUsingTsSolutionConfig: boolean;
|
||||
}
|
||||
> {
|
||||
let directory = options.rootProject ? 'e2e' : options.directory;
|
||||
if (!directory) {
|
||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||
directory = `${projectConfig.root}-e2e`;
|
||||
}
|
||||
const {
|
||||
projectName: e2eProjectName,
|
||||
projectRoot: e2eProjectRoot,
|
||||
importPath,
|
||||
} = await determineProjectNameAndRootOptions(tree, {
|
||||
name: options.name,
|
||||
projectType: 'application',
|
||||
directory,
|
||||
});
|
||||
|
||||
const nxJson = readNxJson(tree);
|
||||
const addPlugin =
|
||||
process.env.NX_ADD_PLUGINS !== 'false' &&
|
||||
nxJson.useInferencePlugins !== false;
|
||||
|
||||
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(tree);
|
||||
|
||||
return {
|
||||
addPlugin,
|
||||
...options,
|
||||
e2eProjectRoot,
|
||||
e2eProjectName,
|
||||
importPath,
|
||||
port: options.port ?? 3000,
|
||||
rootProject: !!options.rootProject,
|
||||
isUsingTsSolutionConfig,
|
||||
useProjectJson: options.useProjectJson ?? !isUsingTsSolutionConfig,
|
||||
};
|
||||
}
|
||||
|
||||
export default e2eProjectGenerator;
|
||||
@@ -0,0 +1,26 @@
|
||||
<%_ if (isUsingTsSolutionConfig) { _%>
|
||||
/* eslint-disable */
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
// Reading the SWC compilation config for the spec files
|
||||
const swcJestConfig = JSON.parse(
|
||||
readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8')
|
||||
);
|
||||
|
||||
// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves
|
||||
swcJestConfig.swcrc = false;
|
||||
|
||||
<%_ } _%>
|
||||
export default {
|
||||
displayName: '<%= e2eProjectName %>',
|
||||
preset: '<%= offsetFromRoot %><%= jestPreset %>',
|
||||
setupFiles: ['<rootDir>/src/test-setup.ts'],
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': <% if (isUsingTsSolutionConfig) { %>['@swc/jest', swcJestConfig]<% } else { %>['ts-jest', {
|
||||
tsconfig: '<rootDir>/<%= tsConfigFile %>',
|
||||
}]<% } %>,
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '<%= coverageDirectory %>',
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { join } from 'path';
|
||||
|
||||
describe('CLI tests', () => {
|
||||
it('should print a message', () => {
|
||||
const cliPath = join(process.cwd(), "<%= mainFile %>");
|
||||
|
||||
const output = execSync(`node ${cliPath}`).toString();
|
||||
|
||||
expect(output).toMatch(/Hello World/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
//
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "<%= offsetFromRoot %><% if (rootProject) { %>tsconfig.json<% } else { %>tsconfig.base.json<% } %>",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<%_ if (isUsingTsSolutionConfig) { _%>
|
||||
/* eslint-disable */
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
// Reading the SWC compilation config for the spec files
|
||||
const swcJestConfig = JSON.parse(
|
||||
readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8')
|
||||
);
|
||||
|
||||
// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves
|
||||
swcJestConfig.swcrc = false;
|
||||
|
||||
<%_ } _%>
|
||||
export default {
|
||||
displayName: '<%= e2eProjectName %>',
|
||||
preset: '<%= offsetFromRoot %><%= jestPreset %>',
|
||||
globalSetup: '<rootDir>/src/support/global-setup.ts',
|
||||
globalTeardown: '<rootDir>/src/support/global-teardown.ts',
|
||||
setupFiles: ['<rootDir>/src/support/test-setup.ts'],
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': <% if (isUsingTsSolutionConfig) { %>['@swc/jest', swcJestConfig]<% } else { %>['ts-jest', {
|
||||
tsconfig: '<rootDir>/<%= tsConfigFile %>',
|
||||
}]<% } %>,
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '<%= coverageDirectory %>',
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import axios from 'axios';
|
||||
|
||||
describe('GET /', () => {
|
||||
it('should return a message', async () => {
|
||||
const res = await axios.get(`/`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.data).toEqual({ message: 'Hello API' });
|
||||
});
|
||||
})
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { waitForPortOpen } from '@nx/node/utils';
|
||||
|
||||
/* eslint-disable */
|
||||
var __TEARDOWN_MESSAGE__: string;
|
||||
|
||||
module.exports = async function() {
|
||||
// Start services that that the app needs to run (e.g. database, docker-compose, etc.).
|
||||
console.log('\nSetting up...\n');
|
||||
|
||||
const host = process.env.HOST ?? 'localhost';
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : <%= port %>;
|
||||
await waitForPortOpen(port, { host });
|
||||
|
||||
// Hint: Use `globalThis` to pass variables to global teardown.
|
||||
globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n';
|
||||
};
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { killPort } from '@nx/node/utils';
|
||||
/* eslint-disable */
|
||||
|
||||
module.exports = async function() {
|
||||
// Put clean up logic here (e.g. stopping services, docker-compose, etc.).
|
||||
// Hint: `globalThis` is shared between setup and teardown.
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : <%= port %>;
|
||||
await killPort(port);
|
||||
console.log(globalThis.__TEARDOWN_MESSAGE__);
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/* eslint-disable */
|
||||
import axios from 'axios';
|
||||
|
||||
module.exports = async function() {
|
||||
// Configure axios for tests to use.
|
||||
const host = process.env.HOST ?? 'localhost';
|
||||
const port = process.env.PORT ?? '<%= port %>';
|
||||
axios.defaults.baseURL = `http://${host}:${port}`;
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import axios from 'axios';
|
||||
|
||||
describe('GET /api', () => {
|
||||
it('should return a message', async () => {
|
||||
const res = await axios.get(`/api`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.data).toEqual({ message: 'Hello API' });
|
||||
});
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "<%= offsetFromRoot %>tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "out-tsc/<%= e2eProjectName %>",
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": false,
|
||||
"noImplicitAny": false
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface Schema {
|
||||
project: string;
|
||||
projectType: 'server' | 'cli';
|
||||
directory?: string;
|
||||
name?: string;
|
||||
port?: number;
|
||||
linter?: 'eslint' | 'none';
|
||||
rootProject?: boolean;
|
||||
isNest?: boolean;
|
||||
skipFormat?: boolean;
|
||||
addPlugin?: boolean;
|
||||
useProjectJson?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"cli": "nx",
|
||||
"$id": "SchematicsNxNodeE2eProject",
|
||||
"title": "Node E2E Project Generator",
|
||||
"description": "Generate an E2E project for a Node server application.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The name of the project.",
|
||||
"$default": { "$source": "argv", "index": 0 },
|
||||
"x-dropdown": "project",
|
||||
"x-prompt": "What project is this for?"
|
||||
},
|
||||
"directory": {
|
||||
"description": "The directory of the e2e project.",
|
||||
"type": "string",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the e2e project. Defaults to the project name with '-e2e' suffix.",
|
||||
"type": "string"
|
||||
},
|
||||
"projectType": {
|
||||
"description": "The type of application that is being tested.",
|
||||
"type": "string",
|
||||
"enum": ["server", "cli"],
|
||||
"default": "server"
|
||||
},
|
||||
"port": {
|
||||
"description": "The port that the server runs on. Only application for server application.",
|
||||
"type": "number",
|
||||
"default": 3000
|
||||
},
|
||||
"linter": {
|
||||
"description": "Linter to be used for the E2E project",
|
||||
"type": "string",
|
||||
"enum": ["eslint", "none"],
|
||||
"default": "eslint"
|
||||
},
|
||||
"isNest": {
|
||||
"description": "Is the E2E server project nest",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"hidden": true,
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"rootProject": {
|
||||
"description": "Create node application at the root of the workspace.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"hidden": true,
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"skipFormat": {
|
||||
"description": "Skip formatting files.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"hidden": true,
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"useProjectJson": {
|
||||
"type": "boolean",
|
||||
"description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file."
|
||||
}
|
||||
},
|
||||
"required": ["project"]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
readJson,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
|
||||
import { nxVersion } from '../../utils/versions';
|
||||
import { initGenerator } from './init';
|
||||
|
||||
describe('init', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should add dependencies', async () => {
|
||||
const existing = 'existing';
|
||||
const existingVersion = '1.0.0';
|
||||
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{
|
||||
'@nx/node': nxVersion,
|
||||
[existing]: existingVersion,
|
||||
},
|
||||
{
|
||||
[existing]: existingVersion,
|
||||
}
|
||||
);
|
||||
await initGenerator(tree, {});
|
||||
|
||||
const packageJson = readJson(tree, 'package.json');
|
||||
expect(packageJson.dependencies['@nx/node']).toBeUndefined();
|
||||
expect(packageJson.dependencies[existing]).toBeDefined();
|
||||
expect(packageJson.devDependencies['@nx/node']).toBeDefined();
|
||||
expect(packageJson.devDependencies[existing]).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not fail when dependencies is missing from package.json and no other init generators are invoked', async () => {
|
||||
updateJson(tree, 'package.json', (json) => {
|
||||
delete json.dependencies;
|
||||
return json;
|
||||
});
|
||||
|
||||
await expect(initGenerator(tree, {})).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
formatFiles,
|
||||
GeneratorCallback,
|
||||
removeDependenciesFromPackageJson,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
} from '@nx/devkit';
|
||||
import { nxVersion } from '../../utils/versions';
|
||||
import { Schema } from './schema';
|
||||
|
||||
function updateDependencies(tree: Tree, options: Schema) {
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
tasks.push(removeDependenciesFromPackageJson(tree, ['@nx/node'], []));
|
||||
tasks.push(
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{ '@nx/node': nxVersion },
|
||||
undefined,
|
||||
options.keepExistingVersions ?? true
|
||||
)
|
||||
);
|
||||
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
export async function initGenerator(tree: Tree, options: Schema) {
|
||||
let installTask: GeneratorCallback = () => {};
|
||||
if (!options.skipPackageJson) {
|
||||
installTask = updateDependencies(tree, options);
|
||||
}
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
return installTask;
|
||||
}
|
||||
|
||||
export default initGenerator;
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface Schema {
|
||||
skipFormat?: boolean;
|
||||
skipPackageJson?: boolean;
|
||||
keepExistingVersions?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"cli": "nx",
|
||||
"$id": "NxNodeInit",
|
||||
"title": "Init Node Plugin",
|
||||
"description": "Init Node Plugin.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"skipFormat": {
|
||||
"description": "Skip formatting files.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"skipPackageJson": {
|
||||
"description": "Do not add dependencies to `package.json`.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"keepExistingVersions": {
|
||||
"type": "boolean",
|
||||
"x-priority": "internal",
|
||||
"description": "Keep existing dependencies versions",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`lib not nested should update configuration 1`] = `
|
||||
"module.exports = {
|
||||
displayName: 'my-lib',
|
||||
preset: '../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../coverage/my-lib',
|
||||
};
|
||||
"
|
||||
`;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { <%= propertyName %> } from './<%= fileName %>';
|
||||
|
||||
describe('<%= propertyName %>', () => {
|
||||
it('should work', () => {
|
||||
expect(<%= propertyName %>()).toEqual('<%= name %>');
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export function <%= propertyName %>(): string {
|
||||
return '<%= name %>';
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": [
|
||||
"jest.config.ts",
|
||||
"jest.config.cts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
|
||||
"emitDeclarationOnly": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": [
|
||||
"jest.config.ts",
|
||||
"jest.config.cts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,821 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
import {
|
||||
getProjects,
|
||||
readJson,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateJson,
|
||||
writeJson,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
|
||||
import { Schema } from './schema.d';
|
||||
import { libraryGenerator } from './library';
|
||||
|
||||
const baseLibraryConfig = {
|
||||
directory: 'my-lib',
|
||||
compiler: 'tsc' as const,
|
||||
addPlugin: true,
|
||||
};
|
||||
|
||||
describe('lib', () => {
|
||||
let tree: Tree;
|
||||
let envBackup: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
envBackup = process.env.ESLINT_USE_FLAT_CONFIG;
|
||||
delete process.env.ESLINT_USE_FLAT_CONFIG;
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (envBackup === undefined) {
|
||||
delete process.env.ESLINT_USE_FLAT_CONFIG;
|
||||
} else {
|
||||
process.env.ESLINT_USE_FLAT_CONFIG = envBackup;
|
||||
}
|
||||
});
|
||||
|
||||
describe('not nested', () => {
|
||||
it('should update configuration', async () => {
|
||||
await libraryGenerator(tree, baseLibraryConfig);
|
||||
const configuration = readProjectConfiguration(tree, 'my-lib');
|
||||
expect(configuration.root).toEqual('my-lib');
|
||||
expect(configuration.targets.build).toBeUndefined();
|
||||
expect(tree.read('my-lib/jest.config.cts', 'utf-8')).toMatchSnapshot();
|
||||
expect(
|
||||
readJson(tree, 'package.json').devDependencies['jest-environment-jsdom']
|
||||
).not.toBeDefined();
|
||||
expect(
|
||||
readJson(tree, 'package.json').devDependencies['jest-environment-node']
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('adds srcRootForCompilationRoot', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
rootDir: './src',
|
||||
buildable: true,
|
||||
});
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'my-lib').targets.build.options
|
||||
.srcRootForCompilationRoot
|
||||
).toEqual('./src');
|
||||
});
|
||||
|
||||
it('should update tags', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
tags: 'one,two',
|
||||
});
|
||||
const projects = Object.fromEntries(getProjects(tree));
|
||||
expect(projects).toMatchObject({
|
||||
'my-lib': {
|
||||
tags: ['one', 'two'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should update root tsconfig.base.json', async () => {
|
||||
await libraryGenerator(tree, baseLibraryConfig);
|
||||
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
|
||||
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
|
||||
'./my-lib/src/index.ts',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should create a local tsconfig.json', async () => {
|
||||
await libraryGenerator(tree, baseLibraryConfig);
|
||||
const tsconfigJson = readJson(tree, 'my-lib/tsconfig.json');
|
||||
expect(tsconfigJson).toMatchInlineSnapshot(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
},
|
||||
"extends": "../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json",
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should extend the local tsconfig.json with tsconfig.spec.json', async () => {
|
||||
await libraryGenerator(tree, baseLibraryConfig);
|
||||
const tsconfigJson = readJson(tree, 'my-lib/tsconfig.spec.json');
|
||||
expect(tsconfigJson.extends).toEqual('./tsconfig.json');
|
||||
});
|
||||
|
||||
it('should extend the local tsconfig.json with tsconfig.lib.json', async () => {
|
||||
await libraryGenerator(tree, baseLibraryConfig);
|
||||
const tsconfigJson = readJson(tree, 'my-lib/tsconfig.lib.json');
|
||||
expect(tsconfigJson.compilerOptions.types).toContain('node');
|
||||
expect(tsconfigJson.extends).toEqual('./tsconfig.json');
|
||||
});
|
||||
|
||||
it('should exclude test files from tsconfig.lib.json', async () => {
|
||||
await libraryGenerator(tree, baseLibraryConfig);
|
||||
const tsconfigJson = readJson(tree, 'my-lib/tsconfig.lib.json');
|
||||
expect(tsconfigJson.exclude).toEqual([
|
||||
'jest.config.ts',
|
||||
'jest.config.cts',
|
||||
'src/**/*.spec.ts',
|
||||
'src/**/*.test.ts',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate files', async () => {
|
||||
await libraryGenerator(tree, baseLibraryConfig);
|
||||
expect(tree.exists(`my-lib/jest.config.cts`)).toBeTruthy();
|
||||
expect(tree.exists('my-lib/src/index.ts')).toBeTruthy();
|
||||
|
||||
expect(tree.exists('my-lib/eslint.config.mjs')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested', () => {
|
||||
it('should update tags', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
directory: 'my-dir/my-lib',
|
||||
tags: 'one',
|
||||
});
|
||||
let projects = Object.fromEntries(getProjects(tree));
|
||||
expect(projects).toMatchObject({
|
||||
'my-lib': {
|
||||
tags: ['one'],
|
||||
},
|
||||
});
|
||||
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
name: 'my-lib2',
|
||||
directory: 'my-dir/my-lib-2',
|
||||
tags: 'one,two',
|
||||
});
|
||||
projects = Object.fromEntries(getProjects(tree));
|
||||
expect(projects).toMatchObject({
|
||||
'my-lib': {
|
||||
tags: ['one'],
|
||||
},
|
||||
'my-lib2': {
|
||||
tags: ['one', 'two'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate files', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
directory: 'my-dir/my-lib',
|
||||
});
|
||||
expect(tree.exists(`my-dir/my-lib/jest.config.cts`)).toBeTruthy();
|
||||
expect(tree.exists('my-dir/my-lib/src/index.ts')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should update project.json', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
directory: 'my-dir/my-lib',
|
||||
});
|
||||
|
||||
const project = readProjectConfiguration(tree, 'my-lib');
|
||||
expect(project).toMatchInlineSnapshot(`
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"name": "my-lib",
|
||||
"projectType": "library",
|
||||
"root": "my-dir/my-lib",
|
||||
"sourceRoot": "my-dir/my-lib/src",
|
||||
"tags": [],
|
||||
"targets": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should update tsconfig.json', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
directory: 'my-dir/my-lib',
|
||||
});
|
||||
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
|
||||
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
|
||||
'./my-dir/my-lib/src/index.ts',
|
||||
]);
|
||||
expect(tsconfigJson.compilerOptions.paths['my-lib/*']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw an exception when not passing importPath when using --publishable', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
try {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
directory: 'my-dir/my-lib',
|
||||
publishable: true,
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e.message).toContain(
|
||||
'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)'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should create a local tsconfig.json', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
directory: 'my-dir/my-lib',
|
||||
});
|
||||
|
||||
const tsconfigJson = readJson(tree, 'my-dir/my-lib/tsconfig.json');
|
||||
expect(tsconfigJson.extends).toEqual('../../tsconfig.base.json');
|
||||
expect(tsconfigJson.references).toEqual([
|
||||
{
|
||||
path: './tsconfig.lib.json',
|
||||
},
|
||||
{
|
||||
path: './tsconfig.spec.json',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate filenames that do not contain directory with --simpleModuleName', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
directory: 'my-dir/my-lib',
|
||||
simpleModuleName: true,
|
||||
});
|
||||
expect(tree.exists(`my-dir/my-lib/jest.config.cts`)).toBeTruthy();
|
||||
expect(tree.exists('my-dir/my-lib/src/index.ts')).toBeTruthy();
|
||||
expect(tree.exists('my-dir/my-lib/src/lib/my-lib.ts')).toBeTruthy();
|
||||
expect(tree.exists('my-dir/my-lib/src/lib/my-lib.spec.ts')).toBeTruthy();
|
||||
expect(tree.exists('my-dir/my-lib/src/index.ts')).toBeTruthy();
|
||||
expect(tree.exists(`my-dir/my-lib/eslint.config.mjs`)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('--compiler', () => {
|
||||
it('should specify tsc as compiler', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
compiler: 'tsc',
|
||||
buildable: true,
|
||||
});
|
||||
|
||||
const { build } = readProjectConfiguration(tree, 'my-lib').targets;
|
||||
|
||||
expect(build.executor).toEqual('@nx/js:tsc');
|
||||
});
|
||||
|
||||
it('should specify swc as compiler', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
compiler: 'swc',
|
||||
buildable: true,
|
||||
});
|
||||
|
||||
const { build } = readProjectConfiguration(tree, 'my-lib').targets;
|
||||
|
||||
expect(build.executor).toEqual('@nx/js:swc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('--unit-test-runner none', () => {
|
||||
it('should not generate test configuration', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
unitTestRunner: 'none',
|
||||
});
|
||||
expect(tree.exists('my-lib/tsconfig.spec.json')).toBeFalsy();
|
||||
expect(tree.exists('my-lib/jest.config.cts')).toBeFalsy();
|
||||
expect(tree.exists('my-lib/lib/my-lib.spec.ts')).toBeFalsy();
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'my-lib').targets.test
|
||||
).toBeUndefined();
|
||||
const tsconfigJson = readJson(tree, 'my-lib/tsconfig.json');
|
||||
expect(tsconfigJson.extends).toEqual('../tsconfig.base.json');
|
||||
expect(tsconfigJson.references).toEqual([
|
||||
{
|
||||
path: './tsconfig.lib.json',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildable package', () => {
|
||||
it('should have a builder defined', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
buildable: true,
|
||||
});
|
||||
|
||||
const projectConfiguration = readProjectConfiguration(tree, 'my-lib');
|
||||
expect(projectConfiguration.root).toEqual('my-lib');
|
||||
|
||||
expect(projectConfiguration.targets.build).toMatchInlineSnapshot(`
|
||||
{
|
||||
"executor": "@nx/js:tsc",
|
||||
"options": {
|
||||
"assets": [
|
||||
"my-lib/*.md",
|
||||
],
|
||||
"main": "my-lib/src/index.ts",
|
||||
"outputPath": "dist/my-lib",
|
||||
"packageJson": "my-lib/package.json",
|
||||
"tsConfig": "my-lib/tsconfig.lib.json",
|
||||
},
|
||||
"outputs": [
|
||||
"{options.outputPath}",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('publishable package', () => {
|
||||
it('should have a builder defined', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
publishable: true,
|
||||
importPath: '@proj/mylib',
|
||||
});
|
||||
|
||||
const projectConfiguration = readProjectConfiguration(tree, 'my-lib');
|
||||
expect(projectConfiguration.root).toEqual('my-lib');
|
||||
expect(projectConfiguration.targets.build).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update package.json', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
directory: 'mylib',
|
||||
publishable: true,
|
||||
importPath: '@proj/mylib',
|
||||
});
|
||||
|
||||
let packageJsonContent = readJson(tree, 'mylib/package.json');
|
||||
|
||||
expect(packageJsonContent.name).toEqual('@proj/mylib');
|
||||
});
|
||||
});
|
||||
|
||||
describe('--importPath', () => {
|
||||
it('should update the package.json & tsconfig with the given import path', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
publishable: true,
|
||||
directory: 'my-dir/my-lib',
|
||||
importPath: '@myorg/lib',
|
||||
});
|
||||
const packageJson = readJson(tree, 'my-dir/my-lib/package.json');
|
||||
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
|
||||
|
||||
expect(packageJson.name).toBe('@myorg/lib');
|
||||
expect(
|
||||
tsconfigJson.compilerOptions.paths[packageJson.name]
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail if the same importPath has already been used', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
directory: 'my-lib1',
|
||||
publishable: true,
|
||||
importPath: '@myorg/lib',
|
||||
});
|
||||
|
||||
try {
|
||||
await libraryGenerator(tree, {
|
||||
...baseLibraryConfig,
|
||||
directory: 'my-lib2',
|
||||
publishable: true,
|
||||
importPath: '@myorg/lib',
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e.message).toContain(
|
||||
'You already have a library using the import path'
|
||||
);
|
||||
}
|
||||
|
||||
expect.assertions(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`--babelJest`, () => {
|
||||
it('should use babel for jest', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'my-lib',
|
||||
babelJest: true,
|
||||
} as Schema);
|
||||
|
||||
expect(tree.read(`my-lib/jest.config.cts`, 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"module.exports = {
|
||||
displayName: 'my-lib',
|
||||
preset: '../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\\\.[tj]s$': 'babel-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../coverage/my-lib',
|
||||
};
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('eslintrc (legacy)', () => {
|
||||
it('should generate .eslintrc.json when ESLINT_USE_FLAT_CONFIG=false', async () => {
|
||||
process.env.ESLINT_USE_FLAT_CONFIG = 'false';
|
||||
await libraryGenerator(tree, baseLibraryConfig);
|
||||
const eslintrc = readJson(tree, 'my-lib/.eslintrc.json');
|
||||
expect(eslintrc.extends).toEqual(['../.eslintrc.json']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('--js flag', () => {
|
||||
it('should generate js files instead of ts files', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'my-lib',
|
||||
js: true,
|
||||
} as Schema);
|
||||
|
||||
expect(tree.exists(`my-lib/jest.config.js`)).toBeTruthy();
|
||||
expect(tree.exists('my-lib/src/index.js')).toBeTruthy();
|
||||
expect(tree.exists('my-lib/src/lib/my-lib.js')).toBeTruthy();
|
||||
expect(tree.exists('my-lib/src/lib/my-lib.spec.js')).toBeTruthy();
|
||||
|
||||
expect(readJson(tree, 'my-lib/tsconfig.json').compilerOptions).toEqual({
|
||||
allowJs: true,
|
||||
module: 'commonjs',
|
||||
});
|
||||
expect(readJson(tree, 'my-lib/tsconfig.lib.json').include).toEqual([
|
||||
'src/**/*.ts',
|
||||
'src/**/*.js',
|
||||
]);
|
||||
expect(readJson(tree, 'my-lib/tsconfig.lib.json').exclude).toEqual([
|
||||
'jest.config.js',
|
||||
'src/**/*.spec.ts',
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.spec.js',
|
||||
'src/**/*.test.js',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should update root tsconfig.json with a js file path', async () => {
|
||||
await libraryGenerator(tree, { directory: 'my-lib', js: true } as Schema);
|
||||
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
|
||||
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
|
||||
'./my-lib/src/index.js',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should update architect builder when --buildable', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'my-lib',
|
||||
buildable: true,
|
||||
js: true,
|
||||
} as Schema);
|
||||
|
||||
const projectConfiguration = readProjectConfiguration(tree, 'my-lib');
|
||||
expect(projectConfiguration.root).toEqual('my-lib');
|
||||
|
||||
expect(projectConfiguration.targets.build.options.main).toEqual(
|
||||
'my-lib/src/index.js'
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate js files for nested libs as well', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
name: 'my-lib',
|
||||
directory: 'my-dir/my-lib',
|
||||
js: true,
|
||||
} as Schema);
|
||||
expect(tree.exists(`my-dir/my-lib/jest.config.js`)).toBeTruthy();
|
||||
expect(tree.exists('my-dir/my-lib/src/index.js')).toBeTruthy();
|
||||
expect(tree.exists('my-dir/my-lib/src/lib/my-lib.js')).toBeTruthy();
|
||||
expect(tree.exists('my-dir/my-lib/src/lib/my-lib.spec.js')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TS solution setup', () => {
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
updateJson(tree, 'package.json', (json) => {
|
||||
json.workspaces = ['packages/*', 'apps/*'];
|
||||
return json;
|
||||
});
|
||||
writeJson(tree, 'tsconfig.base.json', {
|
||||
compilerOptions: {
|
||||
composite: true,
|
||||
declaration: true,
|
||||
customConditions: ['@proj/source'],
|
||||
},
|
||||
});
|
||||
writeJson(tree, 'tsconfig.json', {
|
||||
extends: './tsconfig.base.json',
|
||||
files: [],
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add project references when using TS solution', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'mylib',
|
||||
unitTestRunner: 'jest',
|
||||
addPlugin: true,
|
||||
useProjectJson: false,
|
||||
} as Schema);
|
||||
|
||||
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"path": "./mylib",
|
||||
},
|
||||
]
|
||||
`);
|
||||
// Make sure keys are in idiomatic order
|
||||
expect(Object.keys(readJson(tree, 'mylib/package.json')))
|
||||
.toMatchInlineSnapshot(`
|
||||
[
|
||||
"name",
|
||||
"version",
|
||||
"private",
|
||||
"main",
|
||||
"types",
|
||||
"exports",
|
||||
"dependencies",
|
||||
]
|
||||
`);
|
||||
expect(readJson(tree, 'mylib/package.json')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"dependencies": {},
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./src/index.ts",
|
||||
"import": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"name": "@proj/mylib",
|
||||
"private": true,
|
||||
"types": "./src/index.ts",
|
||||
"version": "0.0.1",
|
||||
}
|
||||
`);
|
||||
expect(readJson(tree, 'mylib/tsconfig.json')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json",
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(readJson(tree, 'mylib/tsconfig.lib.json')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": true,
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
|
||||
"types": [
|
||||
"node",
|
||||
],
|
||||
},
|
||||
"exclude": [
|
||||
"jest.config.ts",
|
||||
"jest.config.cts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts",
|
||||
],
|
||||
"extends": "../tsconfig.base.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
],
|
||||
"references": [],
|
||||
}
|
||||
`);
|
||||
expect(readJson(tree, 'mylib/tsconfig.spec.json')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"outDir": "./out-tsc/jest",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
],
|
||||
},
|
||||
"extends": "../tsconfig.base.json",
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"jest.config.cts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts",
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should create a correct package.json for buildable libraries', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'mylib',
|
||||
unitTestRunner: 'jest',
|
||||
addPlugin: true,
|
||||
useProjectJson: false,
|
||||
buildable: true,
|
||||
skipFormat: true,
|
||||
} as Schema);
|
||||
|
||||
expect(tree.read('mylib/package.json', 'utf-8')).toMatchInlineSnapshot(`
|
||||
"{
|
||||
"name": "@proj/mylib",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"@proj/source": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not set the custom condition in exports when it does not exist in tsconfig.base.json', async () => {
|
||||
updateJson(tree, 'tsconfig.base.json', (json) => {
|
||||
delete json.compilerOptions.customConditions;
|
||||
return json;
|
||||
});
|
||||
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'mylib',
|
||||
unitTestRunner: 'jest',
|
||||
addPlugin: true,
|
||||
useProjectJson: false,
|
||||
buildable: true,
|
||||
skipFormat: true,
|
||||
} as Schema);
|
||||
|
||||
expect(
|
||||
readJson(tree, 'mylib/package.json').exports['.']
|
||||
).not.toHaveProperty('development');
|
||||
});
|
||||
|
||||
it('should set correct options for swc', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'mylib',
|
||||
buildable: true,
|
||||
compiler: 'swc',
|
||||
unitTestRunner: 'jest',
|
||||
addPlugin: true,
|
||||
useProjectJson: false,
|
||||
} as Schema);
|
||||
|
||||
expect(readJson(tree, 'mylib/package.json')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"dependencies": {
|
||||
"@swc/helpers": "~0.5.18",
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"@proj/source": "./src/index.ts",
|
||||
"default": "./dist/index.js",
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"name": "@proj/mylib",
|
||||
"nx": {
|
||||
"sourceRoot": "mylib/src",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:swc",
|
||||
"options": {
|
||||
"main": "mylib/src/index.ts",
|
||||
"outputPath": "mylib/dist",
|
||||
"packageJson": "mylib/package.json",
|
||||
"stripLeadingPaths": true,
|
||||
"tsConfig": "mylib/tsconfig.lib.json",
|
||||
},
|
||||
"outputs": [
|
||||
"{options.outputPath}",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"private": true,
|
||||
"types": "./dist/index.d.ts",
|
||||
"version": "0.0.1",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'mylib',
|
||||
name: 'my-lib',
|
||||
linter: 'none',
|
||||
unitTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
useProjectJson: false,
|
||||
skipFormat: true,
|
||||
} as Schema);
|
||||
|
||||
expect(readJson(tree, 'mylib/package.json').nx).toStrictEqual({
|
||||
name: 'my-lib',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set "nx.name" in package.json when the provided name matches the package name', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'mylib',
|
||||
name: '@proj/my-lib',
|
||||
linter: 'none',
|
||||
unitTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
useProjectJson: false,
|
||||
skipFormat: true,
|
||||
} as Schema);
|
||||
|
||||
expect(readJson(tree, 'mylib/package.json').nx).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not set "nx.name" in package.json when the user does not provide a name', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'mylib',
|
||||
linter: 'none',
|
||||
unitTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
useProjectJson: false,
|
||||
skipFormat: true,
|
||||
} as Schema);
|
||||
|
||||
expect(readJson(tree, 'mylib/package.json').nx).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should generate project.json if useProjectJson is true', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'mylib',
|
||||
unitTestRunner: 'jest',
|
||||
addPlugin: true,
|
||||
useProjectJson: true,
|
||||
skipFormat: true,
|
||||
} as Schema);
|
||||
|
||||
expect(tree.exists('mylib/project.json')).toBeTruthy();
|
||||
expect(readProjectConfiguration(tree, '@proj/mylib'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"$schema": "../node_modules/nx/schemas/project-schema.json",
|
||||
"name": "@proj/mylib",
|
||||
"projectType": "library",
|
||||
"root": "mylib",
|
||||
"sourceRoot": "mylib/src",
|
||||
"tags": [],
|
||||
"targets": {},
|
||||
}
|
||||
`);
|
||||
expect(readJson(tree, 'mylib/package.json').nx).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,285 @@
|
||||
import {
|
||||
determineProjectNameAndRootOptions,
|
||||
ensureRootProjectName,
|
||||
addBuildTargetDefaults,
|
||||
} from '@nx/devkit/internal';
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
formatFiles,
|
||||
generateFiles,
|
||||
GeneratorCallback,
|
||||
installPackagesTask,
|
||||
joinPathFragments,
|
||||
names,
|
||||
offsetFromRoot,
|
||||
readJson,
|
||||
readNxJson,
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
toJS,
|
||||
Tree,
|
||||
updateProjectConfiguration,
|
||||
updateTsConfigsToJs,
|
||||
writeJson,
|
||||
} from '@nx/devkit';
|
||||
import { libraryGenerator as jsLibraryGenerator } from '@nx/js';
|
||||
import {
|
||||
addSwcConfig,
|
||||
addSwcDependencies,
|
||||
addProjectToTsSolutionWorkspace,
|
||||
isUsingTsSolutionSetup,
|
||||
shouldConfigureTsSolutionSetup,
|
||||
sortPackageJsonFields,
|
||||
} from '@nx/js/internal';
|
||||
import { join } from 'path';
|
||||
import { tslibVersion, typesNodeVersion } from '../../utils/versions';
|
||||
import { initGenerator } from '../init/init';
|
||||
import { Schema } from './schema';
|
||||
export interface NormalizedSchema extends Schema {
|
||||
fileName: string;
|
||||
projectName: string;
|
||||
projectRoot: string;
|
||||
parsedTags: string[];
|
||||
compiler: 'swc' | 'tsc';
|
||||
isUsingTsSolutionConfig: boolean;
|
||||
}
|
||||
|
||||
export async function libraryGenerator(tree: Tree, schema: Schema) {
|
||||
return await libraryGeneratorInternal(tree, {
|
||||
addPlugin: false,
|
||||
useProjectJson: true,
|
||||
...schema,
|
||||
});
|
||||
}
|
||||
|
||||
export async function libraryGeneratorInternal(tree: Tree, schema: Schema) {
|
||||
const options = await normalizeOptions(tree, schema);
|
||||
|
||||
// If we are using the new TS solution
|
||||
// We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project
|
||||
if (options.isUsingTsSolutionConfig) {
|
||||
await addProjectToTsSolutionWorkspace(tree, options.projectRoot);
|
||||
}
|
||||
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
|
||||
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)`
|
||||
);
|
||||
}
|
||||
|
||||
// Create `package.json` first because @nx/js:lib generator will update it.
|
||||
if (
|
||||
!options.useProjectJson ||
|
||||
options.isUsingTsSolutionConfig ||
|
||||
options.publishable ||
|
||||
options.buildable
|
||||
) {
|
||||
writeJson(tree, joinPathFragments(options.projectRoot, 'package.json'), {
|
||||
name: options.importPath,
|
||||
version: '0.0.1',
|
||||
private: true,
|
||||
});
|
||||
}
|
||||
|
||||
tasks.push(
|
||||
await jsLibraryGenerator(tree, {
|
||||
...schema,
|
||||
bundler:
|
||||
schema.buildable || schema.publishable ? schema.compiler : 'none',
|
||||
includeBabelRc: schema.babelJest,
|
||||
importPath: schema.importPath,
|
||||
testEnvironment: 'node',
|
||||
skipFormat: true,
|
||||
setParserOptionsProject: schema.setParserOptionsProject,
|
||||
useProjectJson: options.useProjectJson,
|
||||
})
|
||||
);
|
||||
|
||||
updatePackageJson(tree, options);
|
||||
|
||||
tasks.push(
|
||||
await initGenerator(tree, {
|
||||
...options,
|
||||
skipFormat: true,
|
||||
})
|
||||
);
|
||||
|
||||
createFiles(tree, options);
|
||||
|
||||
if (options.js) {
|
||||
updateTsConfigsToJs(tree, options);
|
||||
}
|
||||
updateProject(tree, options);
|
||||
|
||||
tasks.push(ensureDependencies(tree, options.compiler));
|
||||
|
||||
// Always run install to link packages.
|
||||
if (options.isUsingTsSolutionConfig) {
|
||||
tasks.push(() => installPackagesTask(tree, true));
|
||||
}
|
||||
|
||||
sortPackageJsonFields(tree, options.projectRoot);
|
||||
|
||||
if (!schema.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
export default libraryGenerator;
|
||||
|
||||
async function normalizeOptions(
|
||||
tree: Tree,
|
||||
options: Schema
|
||||
): Promise<NormalizedSchema> {
|
||||
await ensureRootProjectName(options, 'library');
|
||||
const {
|
||||
projectName,
|
||||
names: projectNames,
|
||||
projectRoot,
|
||||
importPath,
|
||||
} = await determineProjectNameAndRootOptions(tree, {
|
||||
name: options.name,
|
||||
projectType: 'library',
|
||||
directory: options.directory,
|
||||
importPath: options.importPath,
|
||||
});
|
||||
|
||||
const nxJson = readNxJson(tree);
|
||||
const addPluginDefault =
|
||||
process.env.NX_ADD_PLUGINS !== 'false' &&
|
||||
nxJson.useInferencePlugins !== false;
|
||||
|
||||
options.addPlugin ??= addPluginDefault;
|
||||
|
||||
const fileName = names(
|
||||
options.simpleModuleName
|
||||
? projectNames.projectSimpleName
|
||||
: projectNames.projectFileName
|
||||
).fileName;
|
||||
|
||||
const parsedTags = options.tags
|
||||
? options.tags.split(',').map((s) => s.trim())
|
||||
: [];
|
||||
|
||||
// this helper is called before the jsLibraryGenerator is called, so, if the
|
||||
// TS solution setup is not configured, we additionally check if the TS
|
||||
// solution setup will be configured by the jsLibraryGenerator
|
||||
const isUsingTsSolutionConfig =
|
||||
isUsingTsSolutionSetup(tree) ||
|
||||
shouldConfigureTsSolutionSetup(tree, options.addPlugin);
|
||||
return {
|
||||
...options,
|
||||
fileName,
|
||||
projectName:
|
||||
isUsingTsSolutionConfig && !options.name ? importPath : projectName,
|
||||
projectRoot,
|
||||
parsedTags,
|
||||
importPath,
|
||||
isUsingTsSolutionConfig,
|
||||
useProjectJson: options.useProjectJson ?? !isUsingTsSolutionConfig,
|
||||
};
|
||||
}
|
||||
|
||||
function createFiles(tree: Tree, options: NormalizedSchema) {
|
||||
const { className, name, propertyName } = names(options.fileName);
|
||||
|
||||
generateFiles(tree, join(__dirname, './files/lib'), options.projectRoot, {
|
||||
...options,
|
||||
className,
|
||||
name,
|
||||
propertyName,
|
||||
tmpl: '',
|
||||
offsetFromRoot: offsetFromRoot(options.projectRoot),
|
||||
});
|
||||
|
||||
if (options.unitTestRunner === 'none') {
|
||||
tree.delete(
|
||||
join(options.projectRoot, `./src/lib/${options.fileName}.spec.ts`)
|
||||
);
|
||||
}
|
||||
if (options.js) {
|
||||
toJS(tree);
|
||||
}
|
||||
}
|
||||
|
||||
function updateProject(tree: Tree, options: NormalizedSchema) {
|
||||
if (!options.publishable && !options.buildable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const project = readProjectConfiguration(tree, options.projectName);
|
||||
const rootProject = options.projectRoot === '.' || options.projectRoot === '';
|
||||
|
||||
project.targets = project.targets || {};
|
||||
addBuildTargetDefaults(tree, `@nx/js:${options.compiler}`);
|
||||
|
||||
// For TS solution, we want tsc build to be inferred by `@nx/js/typescript` plugin.
|
||||
if (!options.isUsingTsSolutionConfig || options.compiler === 'swc') {
|
||||
project.targets.build = {
|
||||
executor: `@nx/js:${options.compiler}`,
|
||||
outputs: ['{options.outputPath}'],
|
||||
options: {
|
||||
outputPath: options.isUsingTsSolutionConfig
|
||||
? joinPathFragments(options.projectRoot, 'dist')
|
||||
: joinPathFragments(
|
||||
'dist',
|
||||
rootProject ? options.projectName : options.projectRoot
|
||||
),
|
||||
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
|
||||
packageJson: `${options.projectRoot}/package.json`,
|
||||
main: `${options.projectRoot}/src/index` + (options.js ? '.js' : '.ts'),
|
||||
assets: options.isUsingTsSolutionConfig
|
||||
? undefined
|
||||
: [`${options.projectRoot}/*.md`],
|
||||
stripLeadingPaths: options.isUsingTsSolutionConfig ? true : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (options.compiler === 'swc') {
|
||||
addSwcDependencies(tree);
|
||||
addSwcConfig(tree, options.projectRoot);
|
||||
}
|
||||
|
||||
if (options.rootDir) {
|
||||
project.targets.build.options.srcRootForCompilationRoot = options.rootDir;
|
||||
}
|
||||
|
||||
updateProjectConfiguration(tree, options.projectName, project);
|
||||
}
|
||||
|
||||
function ensureDependencies(
|
||||
tree: Tree,
|
||||
compiler: 'swc' | 'tsc'
|
||||
): GeneratorCallback {
|
||||
// Only add tslib when using tsc compiler
|
||||
// When using swc, @swc/helpers is added by addSwcDependencies()
|
||||
const dependencies = compiler === 'tsc' ? { tslib: tslibVersion } : {};
|
||||
return addDependenciesToPackageJson(tree, dependencies, {
|
||||
'@types/node': typesNodeVersion,
|
||||
});
|
||||
}
|
||||
|
||||
function updatePackageJson(tree: Tree, options: NormalizedSchema) {
|
||||
const packageJsonPath = joinPathFragments(
|
||||
options.projectRoot,
|
||||
'package.json'
|
||||
);
|
||||
if (!tree.exists(packageJsonPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageJson = readJson(tree, packageJsonPath);
|
||||
|
||||
if (packageJson.type === 'module') {
|
||||
// The @nx/js:lib generator can set the type to 'module' which would
|
||||
// potentially break consumers of the library.
|
||||
delete packageJson.type;
|
||||
}
|
||||
|
||||
writeJson(tree, packageJsonPath, packageJson);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Linter, LinterType } from '@nx/eslint';
|
||||
|
||||
export interface Schema {
|
||||
directory: string;
|
||||
name?: string;
|
||||
simpleModuleName?: boolean;
|
||||
skipTsConfig?: boolean;
|
||||
skipFormat?: boolean;
|
||||
tags?: string;
|
||||
unitTestRunner?: 'jest' | 'none';
|
||||
linter?: Linter | LinterType;
|
||||
buildable?: boolean;
|
||||
publishable?: boolean;
|
||||
importPath?: string;
|
||||
testEnvironment?: 'jsdom' | 'node';
|
||||
rootDir?: string;
|
||||
babelJest?: boolean;
|
||||
js?: boolean;
|
||||
strict?: boolean;
|
||||
setParserOptionsProject?: boolean;
|
||||
compiler: 'tsc' | 'swc';
|
||||
addPlugin?: boolean;
|
||||
useProjectJson?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"cli": "nx",
|
||||
"$id": "NxNodeLibrary",
|
||||
"title": "Create a Node Library for Nx",
|
||||
"description": "Create a Node Library for an Nx workspace.",
|
||||
"type": "object",
|
||||
"examples": [
|
||||
{
|
||||
"command": "nx g lib mylib --directory=myapp",
|
||||
"description": "Generate `libs/myapp/mylib`"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "A directory where the lib is placed",
|
||||
"alias": "dir",
|
||||
"$default": {
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "Which directory do you want to create the library in?"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Library name",
|
||||
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"simpleModuleName": {
|
||||
"description": "Keep the module name simple.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"linter": {
|
||||
"description": "The tool to use for running lint checks.",
|
||||
"type": "string",
|
||||
"enum": ["eslint", "none"],
|
||||
"default": "none",
|
||||
"x-prompt": "Which linter would you like to use?",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"unitTestRunner": {
|
||||
"type": "string",
|
||||
"enum": ["jest", "none"],
|
||||
"description": "Test runner to use for unit tests.",
|
||||
"default": "none",
|
||||
"x-prompt": "Which unit test runner would you like to use?",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"tags": {
|
||||
"type": "string",
|
||||
"description": "Add tags to the library (used for linting).",
|
||||
"alias": "t"
|
||||
},
|
||||
"skipFormat": {
|
||||
"description": "Skip formatting files.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"skipTsConfig": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Do not update `tsconfig.base.json` for development experience.",
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"publishable": {
|
||||
"type": "boolean",
|
||||
"description": "Create a publishable library.",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"buildable": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Generate a buildable library.",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"compiler": {
|
||||
"type": "string",
|
||||
"enum": ["tsc", "swc"],
|
||||
"default": "tsc",
|
||||
"description": "The compiler used by the build and test targets.",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"importPath": {
|
||||
"type": "string",
|
||||
"description": "The library name used to import it, like `@myorg/my-awesome-lib`. Must be a valid npm name."
|
||||
},
|
||||
"rootDir": {
|
||||
"type": "string",
|
||||
"description": "Sets the `rootDir` for TypeScript compilation. When not defined, it uses the project's root property, or `srcRootForCompilationRoot` if it is defined."
|
||||
},
|
||||
"testEnvironment": {
|
||||
"type": "string",
|
||||
"enum": ["jsdom", "node"],
|
||||
"description": "The test environment to use if `unitTestRunner` is set to `jest`.",
|
||||
"default": "jsdom"
|
||||
},
|
||||
"babelJest": {
|
||||
"type": "boolean",
|
||||
"description": "Use `babel` instead of `ts-jest`.",
|
||||
"default": false
|
||||
},
|
||||
"js": {
|
||||
"type": "boolean",
|
||||
"description": "Generate JavaScript files rather than TypeScript files.",
|
||||
"default": false
|
||||
},
|
||||
"strict": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to enable tsconfig strict mode or not.",
|
||||
"default": false
|
||||
},
|
||||
"setParserOptionsProject": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not to configure the ESLint `parserOptions.project`. We do not do this by default for lint performance reasons.",
|
||||
"default": false
|
||||
},
|
||||
"useProjectJson": {
|
||||
"type": "boolean",
|
||||
"description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file."
|
||||
}
|
||||
},
|
||||
"required": ["directory"]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# This file is generated by Nx.
|
||||
# Build the docker image with `npx nx docker:build <%= project %>`.
|
||||
# Tip: Modify "docker:build" options in project.json to change docker build args.
|
||||
#<% if (skipDockerPlugin) { %>
|
||||
# Run the container with `docker run -p 3000:3000 -t <%= sanitizedProjectName %>`.
|
||||
#<% } else { %>
|
||||
# Run the container with `nx docker:run <%= project %> -p 3000:3000`.
|
||||
#<% } %>
|
||||
FROM docker.io/node:lts-alpine
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
<% if(packageManager === "pnpm" || packageManager === 'yarn') { %>
|
||||
RUN corepack enable
|
||||
<% } %>
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY <%= buildLocation %> .
|
||||
|
||||
# You can remove this install step if you build with `--bundle` option.
|
||||
# The bundled output will include external dependencies.
|
||||
<% if(packageManager === 'npm') { %>
|
||||
RUN npm --omit=dev -f install
|
||||
<% } else { %>
|
||||
RUN <%= packageManager %> install
|
||||
<% } %>
|
||||
CMD [ "node", "main.js" ]
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface SetUpDockerOptions {
|
||||
project?: string;
|
||||
targetName?: string;
|
||||
buildTarget?: string;
|
||||
skipFormat?: boolean;
|
||||
outputPath: string;
|
||||
skipDockerPlugin?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"cli": "nx",
|
||||
"$id": "SchematicsNxSetupDocker",
|
||||
"title": "Nx Node Docker Options Schema",
|
||||
"description": "Nx Node Docker Options Schema.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"description": "The name of the project",
|
||||
"$default": {
|
||||
"$source": "projectName"
|
||||
},
|
||||
"type": "string",
|
||||
"x-prompt": "What project would you like to add a Dockerfile to?",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"targetName": {
|
||||
"description": "The name of the target to create",
|
||||
"type": "string",
|
||||
"default": "docker:build"
|
||||
},
|
||||
"buildTargetName": {
|
||||
"description": "The name of the build target",
|
||||
"type": "string",
|
||||
"default": "build"
|
||||
},
|
||||
"outputPath": {
|
||||
"description": "The output path for the node application",
|
||||
"type": "string"
|
||||
},
|
||||
"skipDockerPlugin": {
|
||||
"type": "boolean",
|
||||
"description": "Skip the @nx/docker plugin and use the legacy docker build target instead.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
import { readNxJson, readProjectConfiguration, Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { applicationGenerator } from '../application/application';
|
||||
import { setupDockerGenerator } from './setup-docker';
|
||||
import * as process from 'node:process';
|
||||
|
||||
describe('setupDockerGenerator', () => {
|
||||
let tree: Tree;
|
||||
beforeEach(async () => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
describe('integrated', () => {
|
||||
it('should create docker assets when --docker is passed', async () => {
|
||||
const projectName = 'integreated-api';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
directory: projectName,
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
docker: true,
|
||||
skipDockerPlugin: true, // Use legacy mode for this test
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
|
||||
const dockerFile = tree.read(`${projectName}/Dockerfile`, 'utf8');
|
||||
expect(tree.exists(`${projectName}/Dockerfile`)).toBeTruthy();
|
||||
expect(dockerFile).toContain(`COPY ../dist/${projectName} .`);
|
||||
expect(project.targets).toEqual(
|
||||
expect.objectContaining({
|
||||
'docker:build': {
|
||||
dependsOn: ['build', 'prune'],
|
||||
command: `docker build . -t ${projectName}`,
|
||||
options: {
|
||||
cwd: project.root,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('standalone', () => {
|
||||
it('should create docker assets when --docker is passed', async () => {
|
||||
const projectName = 'standalone-api';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
name: projectName,
|
||||
directory: '.',
|
||||
framework: 'fastify',
|
||||
docker: true,
|
||||
skipDockerPlugin: true, // Use legacy mode for this test
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
const dockerFile = tree.read(`Dockerfile`, 'utf8');
|
||||
|
||||
expect(tree.exists(`Dockerfile`)).toBeTruthy();
|
||||
expect(dockerFile).toContain(`COPY dist/${projectName} .`);
|
||||
expect(project.targets).toEqual(
|
||||
expect.objectContaining({
|
||||
'docker:build': {
|
||||
dependsOn: ['build', 'prune'],
|
||||
command: `docker build . -t ${projectName}`,
|
||||
options: {
|
||||
cwd: project.root,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('skipDockerPlugin', () => {
|
||||
it('should create docker:build target when skipDockerPlugin is true', async () => {
|
||||
const projectName = 'api-with-legacy-docker';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
directory: projectName,
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
docker: true,
|
||||
skipDockerPlugin: true,
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
const dockerFile = tree.read(`${projectName}/Dockerfile`, 'utf8');
|
||||
|
||||
expect(tree.exists(`${projectName}/Dockerfile`)).toBeTruthy();
|
||||
expect(dockerFile).toContain(`COPY ../dist/${projectName} .`);
|
||||
expect(dockerFile).toContain(
|
||||
'Build the docker image with `npx nx docker:build'
|
||||
);
|
||||
expect(project.targets).toEqual(
|
||||
expect.objectContaining({
|
||||
'docker:build': {
|
||||
dependsOn: ['build', 'prune'],
|
||||
command: `docker build . -t ${projectName}`,
|
||||
options: {
|
||||
cwd: project.root,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should create docker:build target when skipDockerPlugin is false', async () => {
|
||||
const projectName = 'api-with-plugin-docker';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
directory: projectName,
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
docker: true,
|
||||
skipDockerPlugin: false,
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
const dockerFile = tree.read(`${projectName}/Dockerfile`, 'utf8');
|
||||
|
||||
expect(tree.exists(`${projectName}/Dockerfile`)).toBeTruthy();
|
||||
expect(dockerFile).toContain(`COPY dist .`);
|
||||
expect(dockerFile).toContain(
|
||||
'Build the docker image with `npx nx docker:build'
|
||||
);
|
||||
expect(project.targets?.['docker-build']).toBeUndefined();
|
||||
|
||||
expect(readNxJson(tree)).toEqual(
|
||||
expect.objectContaining({
|
||||
plugins: expect.arrayContaining([
|
||||
{
|
||||
plugin: '@nx/docker',
|
||||
options: {
|
||||
buildTarget: {
|
||||
name: 'docker:build',
|
||||
},
|
||||
runTarget: { name: 'docker:run' },
|
||||
},
|
||||
},
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should use project-relative paths when skipDockerPlugin is false', async () => {
|
||||
const projectName = 'nested-api';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
directory: `apps/${projectName}`,
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
docker: true,
|
||||
skipDockerPlugin: false,
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
const dockerFile = tree.read(`apps/${projectName}/Dockerfile`, 'utf8');
|
||||
|
||||
expect(dockerFile).toContain(`COPY dist .`);
|
||||
expect(dockerFile).not.toContain(`apps/${projectName}`);
|
||||
});
|
||||
|
||||
it('should use workspace-relative paths when skipDockerPlugin is true', async () => {
|
||||
const projectName = 'nested-api-legacy';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
directory: `apps/${projectName}`,
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
docker: true,
|
||||
skipDockerPlugin: true,
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
const dockerFile = tree.read(`apps/${projectName}/Dockerfile`, 'utf8');
|
||||
|
||||
expect(dockerFile).toContain(`COPY ../../dist/apps/${projectName} .`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('project name sanitization', () => {
|
||||
it('should sanitize project names with special characters for Docker commands', async () => {
|
||||
const projectName = '@myorg/my-app';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
name: projectName,
|
||||
directory: '.',
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
await setupDockerGenerator(tree, {
|
||||
project: projectName,
|
||||
outputPath: 'dist/myorg/my-app',
|
||||
skipDockerPlugin: true,
|
||||
});
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
|
||||
expect(project.targets['docker:build']).toEqual({
|
||||
dependsOn: ['build', 'prune'],
|
||||
command: `docker build . -t myorg-my-app`,
|
||||
options: {
|
||||
cwd: project.root,
|
||||
},
|
||||
});
|
||||
|
||||
expect(tree.read('Dockerfile', 'utf8')).toMatchInlineSnapshot(`
|
||||
"# This file is generated by Nx.
|
||||
# Build the docker image with \`npx nx docker:build @myorg/my-app\`.
|
||||
# Tip: Modify "docker:build" options in project.json to change docker build args.
|
||||
#
|
||||
# Run the container with \`docker run -p 3000:3000 -t myorg-my-app\`.
|
||||
#
|
||||
FROM docker.io/node:lts-alpine
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY dist/myorg/my-app .
|
||||
|
||||
# You can remove this install step if you build with \`--bundle\` option.
|
||||
# The bundled output will include external dependencies.
|
||||
|
||||
RUN npm --omit=dev -f install
|
||||
|
||||
CMD [ "node", "main.js" ]
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should sanitize project names with slashes and other special characters', async () => {
|
||||
const projectName = 'my/special@app';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
name: projectName,
|
||||
directory: '.',
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
await setupDockerGenerator(tree, {
|
||||
project: projectName,
|
||||
outputPath: 'dist/basic-app',
|
||||
skipDockerPlugin: true,
|
||||
});
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
|
||||
expect(project.targets['docker:build']).toEqual({
|
||||
dependsOn: ['build', 'prune'],
|
||||
command: `docker build . -t my-special-app`,
|
||||
options: {
|
||||
cwd: project.root,
|
||||
},
|
||||
});
|
||||
|
||||
expect(tree.read('Dockerfile', 'utf8')).toMatchInlineSnapshot(`
|
||||
"# This file is generated by Nx.
|
||||
# Build the docker image with \`npx nx docker:build my/special@app\`.
|
||||
# Tip: Modify "docker:build" options in project.json to change docker build args.
|
||||
#
|
||||
# Run the container with \`docker run -p 3000:3000 -t my-special-app\`.
|
||||
#
|
||||
FROM docker.io/node:lts-alpine
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY dist/basic-app .
|
||||
|
||||
# You can remove this install step if you build with \`--bundle\` option.
|
||||
# The bundled output will include external dependencies.
|
||||
|
||||
RUN npm --omit=dev -f install
|
||||
|
||||
CMD [ "node", "main.js" ]
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle uppercase and multiple special characters', async () => {
|
||||
const projectName = 'My_App@123/Test';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
name: projectName,
|
||||
directory: '.',
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
docker: true,
|
||||
skipDockerPlugin: true, // Use legacy mode for this test
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
|
||||
expect(project.targets['docker:build']).toEqual({
|
||||
dependsOn: ['build', 'prune'],
|
||||
command: `docker build . -t my_app-123-test`,
|
||||
options: {
|
||||
cwd: project.root,
|
||||
},
|
||||
});
|
||||
|
||||
expect(tree.read('Dockerfile', 'utf8')).toMatchInlineSnapshot(`
|
||||
"# This file is generated by Nx.
|
||||
# Build the docker image with \`npx nx docker:build My_App@123/Test\`.
|
||||
# Tip: Modify "docker:build" options in project.json to change docker build args.
|
||||
#
|
||||
# Run the container with \`docker run -p 3000:3000 -t my_app-123-test\`.
|
||||
#
|
||||
FROM docker.io/node:lts-alpine
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY dist/My_App@123/Test .
|
||||
|
||||
# You can remove this install step if you build with \`--bundle\` option.
|
||||
# The bundled output will include external dependencies.
|
||||
|
||||
RUN npm --omit=dev -f install
|
||||
|
||||
CMD [ "node", "main.js" ]
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should ensure docker:build target works with sanitized names in Dockerfile', async () => {
|
||||
const projectName = '@scope/my-app';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
name: projectName,
|
||||
directory: '.',
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
await setupDockerGenerator(tree, {
|
||||
project: projectName,
|
||||
outputPath: 'dist/scope/my-app',
|
||||
skipDockerPlugin: true,
|
||||
});
|
||||
|
||||
const dockerfileContent = tree.read('Dockerfile', 'utf8');
|
||||
|
||||
expect(dockerfileContent).toMatch(/^FROM\s+\S+/m);
|
||||
expect(dockerfileContent).toMatch(/^WORKDIR\s+\S+/m);
|
||||
expect(dockerfileContent).toMatch(/^CMD\s+\[/m);
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
expect(project.targets['docker:build'].command).toContain(
|
||||
`-t scope-my-app`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
formatFiles,
|
||||
generateFiles,
|
||||
GeneratorCallback,
|
||||
readNxJson,
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateProjectConfiguration,
|
||||
detectPackageManager,
|
||||
workspaceRoot,
|
||||
offsetFromRoot,
|
||||
joinPathFragments,
|
||||
} from '@nx/devkit';
|
||||
import { initGenerator as dockerInitGenerator } from '@nx/docker/generators';
|
||||
import { SetUpDockerOptions } from './schema';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
function normalizeOptions(
|
||||
tree: Tree,
|
||||
setupOptions: SetUpDockerOptions
|
||||
): SetUpDockerOptions {
|
||||
return {
|
||||
...setupOptions,
|
||||
project: setupOptions.project ?? readNxJson(tree).defaultProject,
|
||||
targetName: setupOptions.targetName ?? 'docker:build',
|
||||
buildTarget: setupOptions.buildTarget ?? 'build',
|
||||
skipDockerPlugin: setupOptions.skipDockerPlugin ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeProjectName(projectName: string): string {
|
||||
return projectName
|
||||
.toLowerCase()
|
||||
.replace(/[@\/]/g, '-')
|
||||
.replace(/[^a-z0-9._-]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
async function addDocker(tree: Tree, options: SetUpDockerOptions) {
|
||||
let installTask = () => {};
|
||||
if (!options.skipDockerPlugin) {
|
||||
installTask = await dockerInitGenerator(tree, { skipFormat: true });
|
||||
}
|
||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||
|
||||
const outputPath =
|
||||
projectConfig.targets[options.buildTarget]?.options['outputPath'];
|
||||
|
||||
if (!projectConfig) {
|
||||
throw new Error(`Cannot find project configuration for ${options.project}`);
|
||||
}
|
||||
|
||||
if (!outputPath && !options.outputPath) {
|
||||
throw new Error(
|
||||
`The output path for the project ${options.project} is not defined. Please provide it as an option to the generator.`
|
||||
);
|
||||
}
|
||||
|
||||
const sanitizedProjectName = sanitizeProjectName(options.project);
|
||||
const finalOutputPath = options.outputPath ?? outputPath;
|
||||
|
||||
// Calculate build location based on skipDockerPlugin flag
|
||||
let buildLocation: string;
|
||||
if (options.skipDockerPlugin) {
|
||||
// Legacy mode: use workspace-relative paths
|
||||
// docker target is set to run at project root, so ensure offset to workspace root
|
||||
buildLocation = joinPathFragments(
|
||||
offsetFromRoot(projectConfig.root),
|
||||
finalOutputPath
|
||||
);
|
||||
} else {
|
||||
// New mode: use project-relative paths
|
||||
// Remove the project root prefix from the output path
|
||||
const projectRootWithSlash = projectConfig.root + '/';
|
||||
buildLocation = finalOutputPath.startsWith(projectRootWithSlash)
|
||||
? finalOutputPath.substring(projectRootWithSlash.length)
|
||||
: finalOutputPath.startsWith(projectConfig.root)
|
||||
? finalOutputPath.substring(projectConfig.root.length)
|
||||
: 'dist';
|
||||
}
|
||||
|
||||
const packageManager = existsSync(projectConfig.root)
|
||||
? detectPackageManager(projectConfig.root)
|
||||
: detectPackageManager(workspaceRoot);
|
||||
|
||||
generateFiles(tree, join(__dirname, './files'), projectConfig.root, {
|
||||
tmpl: '',
|
||||
buildLocation,
|
||||
project: options.project,
|
||||
projectPath: projectConfig.root,
|
||||
sanitizedProjectName,
|
||||
skipDockerPlugin: options.skipDockerPlugin,
|
||||
packageManager,
|
||||
});
|
||||
|
||||
return installTask;
|
||||
}
|
||||
|
||||
export function updateProjectConfig(tree: Tree, options: SetUpDockerOptions) {
|
||||
let projectConfig = readProjectConfiguration(tree, options.project);
|
||||
|
||||
if (options.skipDockerPlugin) {
|
||||
// Use sanitized project name for Docker image tag
|
||||
const sanitizedProjectName = sanitizeProjectName(options.project);
|
||||
|
||||
projectConfig.targets[`${options.targetName}`] = {
|
||||
dependsOn: [`${options.buildTarget}`, 'prune'],
|
||||
command: `docker build . -t ${sanitizedProjectName}`,
|
||||
options: {
|
||||
cwd: projectConfig.root,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
projectConfig.targets[`${options.targetName}`] = {
|
||||
dependsOn: [`${options.buildTarget}`, 'prune'],
|
||||
};
|
||||
}
|
||||
|
||||
updateProjectConfiguration(tree, options.project, projectConfig);
|
||||
}
|
||||
|
||||
export async function setupDockerGenerator(
|
||||
tree: Tree,
|
||||
setupOptions: SetUpDockerOptions
|
||||
) {
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
const options = normalizeOptions(tree, setupOptions);
|
||||
|
||||
const installTask = await addDocker(tree, options);
|
||||
tasks.push(installTask);
|
||||
updateProjectConfig(tree, options);
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
export default setupDockerGenerator;
|
||||
@@ -0,0 +1,129 @@
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import type { Tree } from '@nx/devkit';
|
||||
import update, {
|
||||
rewriteSubpathImports,
|
||||
} from './rewrite-internal-subpath-imports';
|
||||
|
||||
describe('rewrite-@nx/node-internal-subpath-imports migration', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
describe('rewriteSubpathImports (pure)', () => {
|
||||
it('routes an internal-symbol import to @nx/node/internal', () => {
|
||||
const src = `import { tslibVersion } from '@nx/node/src/some/internal';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import { tslibVersion } from '@nx/node/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes a public-symbol import to @nx/node', () => {
|
||||
const src = `import { applicationGenerator } from '@nx/node/src/some/path';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import { applicationGenerator } from '@nx/node';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('splits a mixed public/internal import into two declarations', () => {
|
||||
const src = `import { applicationGenerator, tslibVersion } from '@nx/node/src/some/path';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import { applicationGenerator } from '@nx/node';\nimport { tslibVersion } from '@nx/node/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('classifies aliased bindings by their original name', () => {
|
||||
const src = `import { applicationGenerator as aliased } from '@nx/node/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import { applicationGenerator as aliased } from '@nx/node';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves the type modifier when splitting', () => {
|
||||
const src = `import type { applicationGenerator, tslibVersion } from '@nx/node/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import type { applicationGenerator } from '@nx/node';\nimport type { tslibVersion } from '@nx/node/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes export { ... } from by symbol', () => {
|
||||
const src = `export { applicationGenerator, tslibVersion } from '@nx/node/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`export { applicationGenerator } from '@nx/node';\nexport { tslibVersion } from '@nx/node/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes a namespace import to @nx/node/internal', () => {
|
||||
const src = `import * as ns from '@nx/node/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import * as ns from '@nx/node/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes a default import to @nx/node/internal', () => {
|
||||
const src = `import thing from '@nx/node/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import thing from '@nx/node/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes export * to @nx/node/internal', () => {
|
||||
const src = `export * from '@nx/node/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`export * from '@nx/node/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes require() to @nx/node/internal', () => {
|
||||
const src = `const m = require('@nx/node/src/x');\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`const m = require('@nx/node/internal');\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes dynamic import() to @nx/node/internal', () => {
|
||||
const src = `const m = await import('@nx/node/src/x');\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`const m = await import('@nx/node/internal');\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes typeof import() type queries to @nx/node/internal', () => {
|
||||
const src = `let m: typeof import('@nx/node/src/x');\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`let m: typeof import('@nx/node/internal');\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('rewrites both runtime and type args of a require()-with-cast', () => {
|
||||
const src = `const m = require('@nx/node/src/x') as typeof import('@nx/node/src/x');\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`const m = require('@nx/node/internal') as typeof import('@nx/node/internal');\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes jest.mock() to @nx/node/internal', () => {
|
||||
const src = `jest.mock('@nx/node/src/x');\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`jest.mock('@nx/node/internal');\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves non-@nx/node imports untouched', () => {
|
||||
const src = `import { foo } from '@nx/devkit';\nimport { bar } from '@nx/node';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(src);
|
||||
});
|
||||
});
|
||||
|
||||
it('rewrites files across the tree and formats', async () => {
|
||||
tree.write(
|
||||
'libs/app/src/x.ts',
|
||||
`import { tslibVersion } from '@nx/node/src/some/internal';\n`
|
||||
);
|
||||
await update(tree);
|
||||
expect(tree.read('libs/app/src/x.ts', 'utf-8')).toContain(
|
||||
`from '@nx/node/internal'`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,306 @@
|
||||
import {
|
||||
applyChangesToString,
|
||||
ChangeType,
|
||||
ensurePackage,
|
||||
formatFiles,
|
||||
logger,
|
||||
type StringChange,
|
||||
type Tree,
|
||||
visitNotIgnoredFiles,
|
||||
} from '@nx/devkit';
|
||||
import type {
|
||||
CallExpression,
|
||||
ExportDeclaration,
|
||||
ExportSpecifier,
|
||||
ImportDeclaration,
|
||||
ImportSpecifier,
|
||||
ImportTypeNode,
|
||||
Node,
|
||||
SourceFile,
|
||||
StringLiteral,
|
||||
} from 'typescript';
|
||||
|
||||
const TS_EXTENSIONS = ['.ts', '.tsx', '.cts', '.mts'] as const;
|
||||
const FROM_PREFIX = '@nx/node/src/';
|
||||
const TO_PUBLIC = '@nx/node';
|
||||
const TO_INTERNAL = '@nx/node/internal';
|
||||
|
||||
// Symbols exported from `@nx/node`'s public entry (packages/node/index.ts).
|
||||
// A named import/export of one of these from `@nx/node/src/*` is routed to
|
||||
// the public `@nx/node` entry; everything else goes to `@nx/node/internal`.
|
||||
const PUBLIC_SYMBOLS: ReadonlySet<string> = new Set([
|
||||
'applicationGenerator',
|
||||
'initGenerator',
|
||||
'libraryGenerator',
|
||||
]);
|
||||
|
||||
// Methods on `jest` and `vi` that take a module specifier as their first arg.
|
||||
const MOCK_HELPER_METHODS: ReadonlySet<string> = new Set([
|
||||
'mock',
|
||||
'unmock',
|
||||
'doMock',
|
||||
'dontMock',
|
||||
'requireActual',
|
||||
'requireMock',
|
||||
'importActual',
|
||||
'importMock',
|
||||
]);
|
||||
|
||||
let ts: typeof import('typescript') | undefined;
|
||||
|
||||
export default async function rewriteInternalSubpathImports(
|
||||
tree: Tree
|
||||
): Promise<void> {
|
||||
let touchedCount = 0;
|
||||
|
||||
visitNotIgnoredFiles(tree, '.', (filePath) => {
|
||||
if (!TS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) {
|
||||
return;
|
||||
}
|
||||
const original = tree.read(filePath, 'utf-8');
|
||||
if (!original || !original.includes(FROM_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
const updated = rewriteSubpathImports(original);
|
||||
if (updated !== original) {
|
||||
tree.write(filePath, updated);
|
||||
touchedCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
if (touchedCount > 0) {
|
||||
logger.info(
|
||||
`Rewrote @nx/node/src/* imports in ${touchedCount} file(s) ` +
|
||||
`(public symbols to @nx/node, internals to @nx/node/internal).`
|
||||
);
|
||||
}
|
||||
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
export function rewriteSubpathImports(source: string): string {
|
||||
ts ??= ensurePackage<typeof import('typescript')>('typescript', '*');
|
||||
const sourceFile = ts.createSourceFile(
|
||||
'tmp.ts',
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
/* setParentNodes */ true,
|
||||
ts.ScriptKind.TSX
|
||||
);
|
||||
|
||||
const changes: StringChange[] = [];
|
||||
for (const stmt of sourceFile.statements) {
|
||||
if (ts.isImportDeclaration(stmt)) {
|
||||
collectImportRewrite(sourceFile, stmt, changes);
|
||||
} else if (ts.isExportDeclaration(stmt)) {
|
||||
collectExportRewrite(sourceFile, stmt, changes);
|
||||
}
|
||||
}
|
||||
collectCallExpressionRewrites(sourceFile, changes);
|
||||
|
||||
return changes.length > 0 ? applyChangesToString(source, changes) : source;
|
||||
}
|
||||
|
||||
function isSubpathSpecifier(node: Node): node is StringLiteral {
|
||||
return ts!.isStringLiteral(node) && node.text.startsWith(FROM_PREFIX);
|
||||
}
|
||||
|
||||
function collectImportRewrite(
|
||||
sourceFile: SourceFile,
|
||||
stmt: ImportDeclaration,
|
||||
changes: StringChange[]
|
||||
): void {
|
||||
if (!isSubpathSpecifier(stmt.moduleSpecifier)) {
|
||||
return;
|
||||
}
|
||||
const clause = stmt.importClause;
|
||||
// Pure named imports (`import { a, b } from '...'`) can be split by symbol.
|
||||
// A default or namespace import grabs the whole module, so it can't be
|
||||
// split — route it wholesale to the internal entry.
|
||||
if (
|
||||
clause &&
|
||||
!clause.name &&
|
||||
clause.namedBindings &&
|
||||
ts!.isNamedImports(clause.namedBindings)
|
||||
) {
|
||||
rewriteNamedDeclaration(
|
||||
sourceFile,
|
||||
stmt,
|
||||
stmt.moduleSpecifier,
|
||||
clause.isTypeOnly,
|
||||
clause.namedBindings.elements,
|
||||
'import',
|
||||
changes
|
||||
);
|
||||
return;
|
||||
}
|
||||
replaceSpecifier(sourceFile, stmt.moduleSpecifier, TO_INTERNAL, changes);
|
||||
}
|
||||
|
||||
function collectExportRewrite(
|
||||
sourceFile: SourceFile,
|
||||
stmt: ExportDeclaration,
|
||||
changes: StringChange[]
|
||||
): void {
|
||||
if (!stmt.moduleSpecifier || !isSubpathSpecifier(stmt.moduleSpecifier)) {
|
||||
return;
|
||||
}
|
||||
// `export { a, b } from '...'` can be split; `export * from '...'` cannot.
|
||||
if (stmt.exportClause && ts!.isNamedExports(stmt.exportClause)) {
|
||||
rewriteNamedDeclaration(
|
||||
sourceFile,
|
||||
stmt,
|
||||
stmt.moduleSpecifier,
|
||||
stmt.isTypeOnly,
|
||||
stmt.exportClause.elements,
|
||||
'export',
|
||||
changes
|
||||
);
|
||||
return;
|
||||
}
|
||||
replaceSpecifier(sourceFile, stmt.moduleSpecifier, TO_INTERNAL, changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Partition the named bindings of an import/export declaration into the ones
|
||||
* that resolve to `@nx/node`'s public entry and the ones that don't. If both
|
||||
* groups are non-empty, the single declaration is split into two.
|
||||
*/
|
||||
function rewriteNamedDeclaration(
|
||||
sourceFile: SourceFile,
|
||||
decl: ImportDeclaration | ExportDeclaration,
|
||||
specifier: StringLiteral,
|
||||
isTypeOnly: boolean,
|
||||
elements: readonly (ImportSpecifier | ExportSpecifier)[],
|
||||
keyword: 'import' | 'export',
|
||||
changes: StringChange[]
|
||||
): void {
|
||||
const publicEls: (ImportSpecifier | ExportSpecifier)[] = [];
|
||||
const internalEls: (ImportSpecifier | ExportSpecifier)[] = [];
|
||||
for (const el of elements) {
|
||||
// `propertyName` is the original name in `orig as alias`; fall back to
|
||||
// `name` for the plain `orig` form.
|
||||
const importedName = (el.propertyName ?? el.name).text;
|
||||
(PUBLIC_SYMBOLS.has(importedName) ? publicEls : internalEls).push(el);
|
||||
}
|
||||
|
||||
if (publicEls.length === 0) {
|
||||
replaceSpecifier(sourceFile, specifier, TO_INTERNAL, changes);
|
||||
return;
|
||||
}
|
||||
if (internalEls.length === 0) {
|
||||
replaceSpecifier(sourceFile, specifier, TO_PUBLIC, changes);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mixed — replace the whole declaration with one statement per target.
|
||||
const quote = sourceFile.text.charAt(specifier.getStart(sourceFile));
|
||||
const start = decl.getStart(sourceFile);
|
||||
const end = decl.getEnd();
|
||||
const semicolon = sourceFile.text.charAt(end - 1) === ';' ? ';' : '';
|
||||
const prefix = isTypeOnly ? `${keyword} type` : keyword;
|
||||
const render = (
|
||||
els: (ImportSpecifier | ExportSpecifier)[],
|
||||
target: string
|
||||
): string =>
|
||||
`${prefix} { ${els
|
||||
.map((el) => el.getText(sourceFile))
|
||||
.join(', ')} } from ${quote}${target}${quote}${semicolon}`;
|
||||
|
||||
changes.push(
|
||||
{ type: ChangeType.Delete, start, length: end - start },
|
||||
{
|
||||
type: ChangeType.Insert,
|
||||
index: start,
|
||||
text: `${render(publicEls, TO_PUBLIC)}\n${render(
|
||||
internalEls,
|
||||
TO_INTERNAL
|
||||
)}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function collectCallExpressionRewrites(
|
||||
sourceFile: SourceFile,
|
||||
changes: StringChange[]
|
||||
): void {
|
||||
const visit = (node: Node): void => {
|
||||
if (
|
||||
ts!.isCallExpression(node) &&
|
||||
shouldRewriteCallExpression(node) &&
|
||||
node.arguments.length >= 1 &&
|
||||
isSubpathSpecifier(node.arguments[0])
|
||||
) {
|
||||
// `require(...)`, dynamic `import(...)` and `jest.mock(...)` reference
|
||||
// the module as a whole and can't be symbol-split, so they go to the
|
||||
// internal entry.
|
||||
replaceSpecifier(
|
||||
sourceFile,
|
||||
node.arguments[0] as StringLiteral,
|
||||
TO_INTERNAL,
|
||||
changes
|
||||
);
|
||||
} else if (ts!.isImportTypeNode(node)) {
|
||||
// `typeof import('...')` parses as an `ImportTypeNode`, not a
|
||||
// CallExpression — its argument is `LiteralTypeNode<StringLiteral>`.
|
||||
// The whole module is referenced, so it can't be symbol-split.
|
||||
const literal = getImportTypeStringLiteral(node);
|
||||
if (literal && literal.text.startsWith(FROM_PREFIX)) {
|
||||
replaceSpecifier(sourceFile, literal, TO_INTERNAL, changes);
|
||||
}
|
||||
}
|
||||
ts!.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
}
|
||||
|
||||
function getImportTypeStringLiteral(
|
||||
node: ImportTypeNode
|
||||
): StringLiteral | undefined {
|
||||
const arg = node.argument;
|
||||
if (arg && ts!.isLiteralTypeNode(arg) && ts!.isStringLiteral(arg.literal)) {
|
||||
return arg.literal;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function shouldRewriteCallExpression(call: CallExpression): boolean {
|
||||
const callee = call.expression;
|
||||
// `require('...')`
|
||||
if (ts!.isIdentifier(callee) && callee.text === 'require') return true;
|
||||
// dynamic `import('...')` (runtime form parses as a CallExpression whose
|
||||
// callee is the `import` keyword). The `typeof import('...')` type-position
|
||||
// form is an `ImportTypeNode` (handled in `collectCallExpressionRewrites`).
|
||||
if (callee.kind === ts!.SyntaxKind.ImportKeyword) return true;
|
||||
// `jest.mock(...)` / `vi.mock(...)` and friends.
|
||||
if (ts!.isPropertyAccessExpression(callee)) {
|
||||
const obj = callee.expression;
|
||||
if (
|
||||
ts!.isIdentifier(obj) &&
|
||||
(obj.text === 'jest' || obj.text === 'vi') &&
|
||||
MOCK_HELPER_METHODS.has(callee.name.text)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function replaceSpecifier(
|
||||
sourceFile: SourceFile,
|
||||
literal: StringLiteral,
|
||||
target: string,
|
||||
changes: StringChange[]
|
||||
): void {
|
||||
const start = literal.getStart(sourceFile);
|
||||
const end = literal.getEnd();
|
||||
const quote = sourceFile.text.charAt(start);
|
||||
changes.push(
|
||||
{ type: ChangeType.Delete, start, length: end - start },
|
||||
{
|
||||
type: ChangeType.Insert,
|
||||
index: start,
|
||||
text: `${quote}${target}${quote}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { type Tree } from '@nx/devkit';
|
||||
import { assertSupportedPackageVersion } from '@nx/devkit/internal';
|
||||
import {
|
||||
minSupportedExpressVersion,
|
||||
minSupportedFastifyVersion,
|
||||
minSupportedKoaVersion,
|
||||
} from './versions';
|
||||
|
||||
export function assertSupportedExpressVersion(tree: Tree): void {
|
||||
assertSupportedPackageVersion(tree, 'express', minSupportedExpressVersion);
|
||||
}
|
||||
|
||||
export function assertSupportedKoaVersion(tree: Tree): void {
|
||||
assertSupportedPackageVersion(tree, 'koa', minSupportedKoaVersion);
|
||||
}
|
||||
|
||||
export function assertSupportedFastifyVersion(tree: Tree): void {
|
||||
assertSupportedPackageVersion(tree, 'fastify', minSupportedFastifyVersion);
|
||||
}
|
||||
|
||||
export function assertSupportedFrameworkVersion(
|
||||
tree: Tree,
|
||||
framework: string | undefined
|
||||
): void {
|
||||
if (framework === 'express') {
|
||||
assertSupportedExpressVersion(tree);
|
||||
} else if (framework === 'koa') {
|
||||
assertSupportedKoaVersion(tree);
|
||||
} else if (framework === 'fastify') {
|
||||
assertSupportedFastifyVersion(tree);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { readNxJson, Tree } from '@nx/devkit';
|
||||
|
||||
export function hasWebpackPlugin(tree: Tree) {
|
||||
const nxJson = readNxJson(tree);
|
||||
return !!nxJson.plugins?.some((p) =>
|
||||
typeof p === 'string'
|
||||
? p === '@nx/webpack/plugin'
|
||||
: p.plugin === '@nx/webpack/plugin'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { logger } from '@nx/devkit';
|
||||
import { check as portCheck } from 'tcp-port-used';
|
||||
|
||||
export const kill = require('kill-port');
|
||||
|
||||
/**
|
||||
* Kills the process on the given port
|
||||
* @param port
|
||||
* @param killPortDelay
|
||||
*/
|
||||
export async function killPort(
|
||||
port: number,
|
||||
killPortDelay = 2500
|
||||
): Promise<boolean> {
|
||||
if (await portCheck(port)) {
|
||||
let killPortResult;
|
||||
try {
|
||||
logger.info(`Attempting to close port ${port}`);
|
||||
killPortResult = await kill(port);
|
||||
await new Promise<void>((resolve) =>
|
||||
setTimeout(() => resolve(), killPortDelay)
|
||||
);
|
||||
if (await portCheck(port)) {
|
||||
logger.error(
|
||||
`Port ${port} still open ${JSON.stringify(killPortResult)}`
|
||||
);
|
||||
} else {
|
||||
logger.info(`Port ${port} successfully closed`);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
logger.error(`Port ${port} closing failed`);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { type Tree } from '@nx/devkit';
|
||||
import {
|
||||
getDeclaredPackageVersion,
|
||||
getInstalledPackageVersion,
|
||||
} from '@nx/devkit/internal';
|
||||
import { major } from 'semver';
|
||||
import { join } from 'path';
|
||||
|
||||
export const nxVersion = require(join('@nx/node', 'package.json')).version;
|
||||
|
||||
export const tslibVersion = '^2.3.0';
|
||||
|
||||
// `@types/node` — supported window covers the active LTS majors (currently v22
|
||||
// and v24). Fresh installs default to the active LTS (v22).
|
||||
export const minSupportedNodeTypesVersion = '22.0.0';
|
||||
export const typesNodeVersion = '^22.0.0';
|
||||
|
||||
type NodeTypesVersions = {
|
||||
typesNodeVersion: string;
|
||||
};
|
||||
const nodeTypesLatest: NodeTypesVersions = { typesNodeVersion };
|
||||
type NodeTypesCompatMajors = 22 | 24;
|
||||
const nodeTypesVersionMap: Record<NodeTypesCompatMajors, NodeTypesVersions> = {
|
||||
22: { typesNodeVersion: '^22.0.0' },
|
||||
24: { typesNodeVersion: '^24.0.0' },
|
||||
};
|
||||
|
||||
export function nodeTypesVersions(tree: Tree): NodeTypesVersions {
|
||||
const installed = getInstalledNodeTypesVersion(tree);
|
||||
if (!installed) {
|
||||
return nodeTypesLatest;
|
||||
}
|
||||
const m = major(installed);
|
||||
return nodeTypesVersionMap[m as NodeTypesCompatMajors] ?? nodeTypesLatest;
|
||||
}
|
||||
|
||||
export function getInstalledNodeTypesVersion(tree?: Tree): string | null {
|
||||
if (!tree) {
|
||||
return getInstalledPackageVersion('@types/node');
|
||||
}
|
||||
return getDeclaredPackageVersion(tree, '@types/node');
|
||||
}
|
||||
|
||||
// Express — support window v4 + v5 (v4 in MAINT through 2026-10-01, v5 ACTIVE).
|
||||
export const minSupportedExpressVersion = '4.0.0';
|
||||
export const expressVersion = '^5.1.0';
|
||||
export const expressTypingsVersion = '^5.0.0';
|
||||
|
||||
type ExpressVersions = {
|
||||
expressVersion: string;
|
||||
expressTypingsVersion: string;
|
||||
};
|
||||
const expressLatest: ExpressVersions = {
|
||||
expressVersion,
|
||||
expressTypingsVersion,
|
||||
};
|
||||
type ExpressCompatMajors = 4;
|
||||
const expressVersionMap: Record<ExpressCompatMajors, ExpressVersions> = {
|
||||
4: {
|
||||
expressVersion: '^4.21.2',
|
||||
expressTypingsVersion: '^4.17.21',
|
||||
},
|
||||
};
|
||||
|
||||
export function expressVersions(tree: Tree): ExpressVersions {
|
||||
const installed = getInstalledExpressVersion(tree);
|
||||
if (!installed) {
|
||||
return expressLatest;
|
||||
}
|
||||
const m = major(installed);
|
||||
return expressVersionMap[m as ExpressCompatMajors] ?? expressLatest;
|
||||
}
|
||||
|
||||
export function getInstalledExpressVersion(tree?: Tree): string | null {
|
||||
if (!tree) {
|
||||
return getInstalledPackageVersion('express');
|
||||
}
|
||||
return getDeclaredPackageVersion(tree, 'express');
|
||||
}
|
||||
|
||||
// Koa — no upstream LTS policy; support window v2 + v3 (Rule 2: N & N-1).
|
||||
export const minSupportedKoaVersion = '2.0.0';
|
||||
export const koaVersion = '^3.1.2';
|
||||
export const koaTypingsVersion = '^3.0.0';
|
||||
|
||||
type KoaVersions = {
|
||||
koaVersion: string;
|
||||
koaTypingsVersion: string;
|
||||
};
|
||||
const koaLatest: KoaVersions = { koaVersion, koaTypingsVersion };
|
||||
type KoaCompatMajors = 2;
|
||||
const koaVersionMap: Record<KoaCompatMajors, KoaVersions> = {
|
||||
2: {
|
||||
koaVersion: '^2.14.1',
|
||||
koaTypingsVersion: '^2.13.5',
|
||||
},
|
||||
};
|
||||
|
||||
export function koaVersions(tree: Tree): KoaVersions {
|
||||
const installed = getInstalledKoaVersion(tree);
|
||||
if (!installed) {
|
||||
return koaLatest;
|
||||
}
|
||||
const m = major(installed);
|
||||
return koaVersionMap[m as KoaCompatMajors] ?? koaLatest;
|
||||
}
|
||||
|
||||
export function getInstalledKoaVersion(tree?: Tree): string | null {
|
||||
if (!tree) {
|
||||
return getInstalledPackageVersion('koa');
|
||||
}
|
||||
return getDeclaredPackageVersion(tree, 'koa');
|
||||
}
|
||||
|
||||
// Fastify — upstream "last 2 majors" policy. Support window v4 + v5.
|
||||
export const minSupportedFastifyVersion = '4.0.0';
|
||||
export const fastifyVersion = '~5.2.1';
|
||||
export const fastifyAutoloadVersion = '~6.0.3';
|
||||
export const fastifySensibleVersion = '~6.0.2';
|
||||
export const fastifyPluginVersion = '~5.0.1';
|
||||
|
||||
type FastifyVersions = {
|
||||
fastifyVersion: string;
|
||||
fastifyAutoloadVersion: string;
|
||||
fastifySensibleVersion: string;
|
||||
fastifyPluginVersion: string;
|
||||
};
|
||||
const fastifyLatest: FastifyVersions = {
|
||||
fastifyVersion,
|
||||
fastifyAutoloadVersion,
|
||||
fastifySensibleVersion,
|
||||
fastifyPluginVersion,
|
||||
};
|
||||
type FastifyCompatMajors = 4;
|
||||
const fastifyVersionMap: Record<FastifyCompatMajors, FastifyVersions> = {
|
||||
4: {
|
||||
fastifyVersion: '~4.28.1',
|
||||
fastifyAutoloadVersion: '~5.10.0',
|
||||
fastifySensibleVersion: '~5.6.0',
|
||||
fastifyPluginVersion: '~4.5.1',
|
||||
},
|
||||
};
|
||||
|
||||
export function fastifyVersions(tree: Tree): FastifyVersions {
|
||||
const installed = getInstalledFastifyVersion(tree);
|
||||
if (!installed) {
|
||||
return fastifyLatest;
|
||||
}
|
||||
const m = major(installed);
|
||||
return fastifyVersionMap[m as FastifyCompatMajors] ?? fastifyLatest;
|
||||
}
|
||||
|
||||
export function getInstalledFastifyVersion(tree?: Tree): string | null {
|
||||
if (!tree) {
|
||||
return getInstalledPackageVersion('fastify');
|
||||
}
|
||||
return getDeclaredPackageVersion(tree, 'fastify');
|
||||
}
|
||||
|
||||
export const axiosVersion = '^1.6.0';
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
Tree,
|
||||
writeJson,
|
||||
updateJson,
|
||||
getPackageManagerCommand,
|
||||
readProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
|
||||
function findFreeDebugPort(tree: Tree): number {
|
||||
let highestPort = 0;
|
||||
|
||||
// Check existing launch.json configurations for used debug ports
|
||||
if (tree.exists('.vscode/launch.json')) {
|
||||
try {
|
||||
const launchConfig = JSON.parse(
|
||||
tree.read('.vscode/launch.json', 'utf-8')
|
||||
);
|
||||
if (launchConfig.configurations) {
|
||||
for (const config of launchConfig.configurations) {
|
||||
if (config.env && config.env.NODE_OPTIONS) {
|
||||
const match = config.env.NODE_OPTIONS.match(/--inspect=(\d+)/);
|
||||
if (match) {
|
||||
const currentPort = parseInt(match[1]);
|
||||
highestPort = Math.max(highestPort, currentPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
// Return the next available port after the highest used port, or 9229 if none are used
|
||||
return highestPort > 0 ? highestPort + 1 : 9229;
|
||||
}
|
||||
|
||||
export interface VSCodeDebugConfigOptions {
|
||||
projectName: string;
|
||||
projectRoot: string;
|
||||
packageManager?: string;
|
||||
}
|
||||
|
||||
export function addVSCodeDebugConfiguration(
|
||||
tree: Tree,
|
||||
options: VSCodeDebugConfigOptions
|
||||
): void {
|
||||
// `getPackageManagerCommand().exec` returns the full subcommand string,
|
||||
// e.g. `pnpm exec` for pnpm or `npm exec --` for npm. VS Code's
|
||||
// `runtimeExecutable` expects an actual executable path, not a string
|
||||
// with embedded arguments — passing `"pnpm exec"` fails on Windows with
|
||||
// `Can't find Node.js binary "pnpm exec": path does not exist`. Split
|
||||
// the command and prepend any extra tokens to runtimeArgs. (#35276)
|
||||
const pmCommandTokens = getPackageManagerCommand().exec.split(' ');
|
||||
const runtimeExecutable = pmCommandTokens[0];
|
||||
const extraExecArgs = pmCommandTokens.slice(1);
|
||||
|
||||
// Determine the output path based on project configuration
|
||||
let outputPath: string;
|
||||
|
||||
try {
|
||||
const projectConfig = readProjectConfiguration(tree, options.projectName);
|
||||
outputPath = projectConfig.targets?.build?.options?.outputPath;
|
||||
} catch {
|
||||
outputPath = undefined;
|
||||
}
|
||||
|
||||
// If no outputPath is configured, assume it's in the project directory
|
||||
if (!outputPath) {
|
||||
outputPath = `${options.projectRoot}/dist`;
|
||||
}
|
||||
|
||||
// Find a free debug port to avoid conflicts
|
||||
const debugPort = findFreeDebugPort(tree);
|
||||
|
||||
const debugConfig = {
|
||||
type: 'node',
|
||||
request: 'launch',
|
||||
name: `Debug ${options.projectName} with Nx`,
|
||||
runtimeExecutable,
|
||||
runtimeArgs: [...extraExecArgs, 'nx', 'serve', options.projectName],
|
||||
env: {
|
||||
NODE_OPTIONS: `--inspect=${debugPort}`,
|
||||
},
|
||||
console: 'integratedTerminal',
|
||||
internalConsoleOptions: 'neverOpen',
|
||||
skipFiles: ['<node_internals>/**'],
|
||||
sourceMaps: true,
|
||||
outFiles: [
|
||||
`\${workspaceFolder}/${outputPath}/**/*.(m|c|)js`,
|
||||
'!**/node_modules/**',
|
||||
],
|
||||
};
|
||||
|
||||
if (!tree.exists('.vscode/launch.json')) {
|
||||
// Create launch.json with the configuration
|
||||
writeJson(tree, '.vscode/launch.json', {
|
||||
version: '0.2.0',
|
||||
configurations: [debugConfig],
|
||||
});
|
||||
} else {
|
||||
// Add configuration to existing launch.json
|
||||
updateJson(tree, '.vscode/launch.json', (json) => {
|
||||
if (!json.configurations) {
|
||||
json.configurations = [];
|
||||
}
|
||||
json.configurations.push(debugConfig);
|
||||
return json;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as net from 'net';
|
||||
import { logger } from '@nx/devkit';
|
||||
|
||||
interface WaitForPortOpenOptions {
|
||||
/**
|
||||
* The host to connect to
|
||||
* @default 'localhost'
|
||||
*/
|
||||
host?: string;
|
||||
/**
|
||||
* The number of retries to attempt
|
||||
* @default 120
|
||||
*/
|
||||
retries?: number;
|
||||
/**
|
||||
* The delay between retries
|
||||
* @default 1000
|
||||
*/
|
||||
retryDelay?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the given port to be open
|
||||
* @param port
|
||||
* @param options
|
||||
*/
|
||||
export function waitForPortOpen(
|
||||
port: number,
|
||||
options: WaitForPortOpenOptions = {}
|
||||
): Promise<void> {
|
||||
const host = options.host ?? 'localhost';
|
||||
const allowedErrorCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT'];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkPort = (retries = options.retries ?? 120) => {
|
||||
const client = new net.Socket();
|
||||
const cleanupClient = () => {
|
||||
client.removeAllListeners('connect');
|
||||
client.removeAllListeners('error');
|
||||
client.end();
|
||||
client.destroy();
|
||||
client.unref();
|
||||
};
|
||||
client.once('connect', () => {
|
||||
cleanupClient();
|
||||
resolve();
|
||||
});
|
||||
|
||||
client.once('error', (err) => {
|
||||
if (retries === 0 || !allowedErrorCodes.includes(err['code'])) {
|
||||
if (process.env['NX_VERBOSE_LOGGING'] === 'true') {
|
||||
logger.info(
|
||||
`Error connecting on ${host}:${port}: ${err['code'] || err}`
|
||||
);
|
||||
}
|
||||
cleanupClient();
|
||||
reject(err);
|
||||
} else {
|
||||
setTimeout(() => checkPort(retries - 1), options.retryDelay ?? 1000);
|
||||
}
|
||||
});
|
||||
|
||||
if (process.env['NX_VERBOSE_LOGGING'] === 'true') {
|
||||
logger.info(`Connecting on ${host}:${port}`);
|
||||
}
|
||||
client.connect({ port, host });
|
||||
};
|
||||
|
||||
checkPort();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": [],
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
],
|
||||
"nx": {
|
||||
"addTypecheckTarget": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"declarationDir": "dist",
|
||||
"declarationMap": false,
|
||||
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
|
||||
"types": ["node"],
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["*.ts", "src/**/*.ts"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*_spec.ts",
|
||||
"**/*_test.ts",
|
||||
"jest.config.ts",
|
||||
"jest.config.cts",
|
||||
"eslint.config.mjs"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../docker/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../eslint/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../js/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../jest/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../devkit/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../webpack/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../nx/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "out-tsc/spec",
|
||||
"customConditions": ["@nx/nx-source"],
|
||||
"types": ["jest", "node"],
|
||||
"composite": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*_spec.ts",
|
||||
"**/*_test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx",
|
||||
"**/*.d.ts",
|
||||
"jest.config.cts",
|
||||
"*.json"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { waitForPortOpen } from './src/utils/wait-for-port-open';
|
||||
export { killPort } from './src/utils/kill-port';
|
||||
Reference in New Issue
Block a user