chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:36 +08:00
commit 8e2a6eb840
10194 changed files with 1593658 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
{
"outDir": "packages/node/dist",
"assets": [
{ "glob": "**/files/**" },
{ "glob": "**/files/**/.gitkeep" },
{ "glob": "src/**/schema.json" },
{ "glob": "src/**/schema.d.ts" },
"LICENSE"
]
}
+51
View File
@@ -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,
},
},
];
+34
View File
@@ -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
}
}
}
+3
View File
@@ -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';
+5
View File
@@ -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';
+7
View File
@@ -0,0 +1,7 @@
/* eslint-disable */
module.exports = {
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: {},
displayName: 'node',
preset: '../../jest.preset.js',
};
+40
View File
@@ -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
}
}
}
}
}
+8
View File
@@ -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);
});
+105
View File
@@ -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"
}
}
+40
View File
@@ -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"]
}
+16
View File
@@ -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 },
});
}
@@ -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;
}
+32
View File
@@ -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 %>',
};
@@ -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,13 @@
{
"extends": "<%= offsetFromRoot %><% if (rootProject) { %>tsconfig.json<% } else { %>tsconfig.base.json<% } %>",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.spec.json"
}
],
"compilerOptions": {
"esModuleInterop": true
}
}
@@ -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 %>',
};
@@ -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' });
});
})
@@ -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';
};
@@ -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__);
};
@@ -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}`;
};
@@ -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": []
}
+13
View File
@@ -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();
});
});
+41
View File
@@ -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;
+5
View File
@@ -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);
}
+24
View File
@@ -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" ]
+8
View File
@@ -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'
);
}
+38
View File
@@ -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;
}
}
+160
View File
@@ -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();
});
}
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": [],
"files": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
],
"nx": {
"addTypecheckTarget": false
}
}
+49
View File
@@ -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"
}
]
}
+29
View File
@@ -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"
}
]
}
+2
View File
@@ -0,0 +1,2 @@
export { waitForPortOpen } from './src/utils/wait-for-port-open';
export { killPort } from './src/utils/kill-port';