fix(misc): fix misc issues in project generators for the ts solution setup (#30111)

The following are the main changes in the context of the TS solution
setup:

- Ensure `name` in `package.json` files is set to the import path for
all projects
- Set `nx.name` in `package.json` files when the user provides a name
different than the package name (import path)
- Clean up project generators so they don't set the `nx` property in
`package.json` files unless strictly needed
- Fix `@nx/vue:application` generator so it creates the Nx config in a
`package.json` file for e2e projects
- Ensure `@types/node` is installed in `vitest` generator
- Fix generated Vite config typing error (surfaced with Vite 6)
- Ensure `jsonc-eslint-parser` is installed when the
`@nx/dependency-checks` rule is added to the ESLint config
- Misc minor alignment changes

## Current Behavior

## Expected Behavior

## Related Issue(s)

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2025-03-06 02:08:10 +01:00 committed by GitHub
parent 121d9973a8
commit ada8be473d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
129 changed files with 2050 additions and 897 deletions

View File

@ -169,7 +169,6 @@ describe('EsBuild Plugin', () => {
expect( expect(
readJson(`dist/libs/${parentLib}/package.json`).dependencies readJson(`dist/libs/${parentLib}/package.json`).dependencies
).toEqual({ ).toEqual({
'jsonc-eslint-parser': expect.any(String),
// Don't care about the versions, just that they exist // Don't care about the versions, just that they exist
rambda: expect.any(String), rambda: expect.any(String),
lodash: expect.any(String), lodash: expect.any(String),

View File

@ -3,6 +3,7 @@ import {
getPackageManagerCommand, getPackageManagerCommand,
getSelectedPackageManager, getSelectedPackageManager,
newProject, newProject,
readJson,
runCLI, runCLI,
runCommand, runCommand,
uniq, uniq,
@ -177,4 +178,28 @@ ${content}`
`Successfully ran target test for project @proj/${viteParentLib}` `Successfully ran target test for project @proj/${viteParentLib}`
); );
}, 300_000); }, 300_000);
it('should respect and support generating libraries with a name different than the import path', () => {
const lib1 = uniq('lib1');
runCLI(
`generate @nx/js:lib packages/${lib1} --name=${lib1} --bundler=vite --linter=eslint --unitTestRunner=jest`
);
const packageJson = readJson(`packages/${lib1}/package.json`);
expect(packageJson.nx.name).toBe(lib1);
expect(runCLI(`build ${lib1}`)).toContain(
`Successfully ran target build for project ${lib1}`
);
expect(runCLI(`typecheck ${lib1}`)).toContain(
`Successfully ran target typecheck for project ${lib1}`
);
expect(runCLI(`lint ${lib1}`)).toContain(
`Successfully ran target lint for project ${lib1}`
);
expect(runCLI(`test ${lib1}`)).toContain(
`Successfully ran target test for project ${lib1}`
);
}, 300_000);
}); });

View File

@ -6,6 +6,7 @@ import {
killPorts, killPorts,
newProject, newProject,
promisifiedTreeKill, promisifiedTreeKill,
readJson,
runCLI, runCLI,
runCommand, runCommand,
runCommandUntil, runCommandUntil,
@ -169,6 +170,30 @@ describe('Node Applications', () => {
expect(err).toBeFalsy(); expect(err).toBeFalsy();
} }
}, 300_000); }, 300_000);
it('should respect and support generating libraries with a name different than the import path', () => {
const nodeLib = uniq('node-lib');
const nestLib = uniq('nest-lib');
runCLI(
`generate @nx/node:lib packages/${nodeLib} --name=${nodeLib} --buildable`
);
runCLI(
`generate @nx/nest:lib packages/${nestLib} --name=${nestLib} --buildable`
);
const packageJson = readJson(`packages/${nodeLib}/package.json`);
expect(packageJson.nx.name).toBe(nodeLib);
const nestPackageJson = readJson(`packages/${nestLib}/package.json`);
expect(nestPackageJson.nx.name).toBe(nestLib);
expect(runCLI(`build ${nodeLib}`)).toContain(
`Successfully ran target build for project ${nodeLib}`
);
expect(runCLI(`build ${nestLib}`)).toContain(
`Successfully ran target build for project ${nestLib}`
);
}, 300_000);
}); });
function getRandomPort() { function getRandomPort() {

View File

@ -3,8 +3,10 @@ import {
cleanupProject, cleanupProject,
createFile, createFile,
newProject, newProject,
readJson,
renameFile, renameFile,
runCLI, runCLI,
runCommand,
uniq, uniq,
updateFile, updateFile,
updateJson, updateJson,
@ -103,12 +105,10 @@ describe('Nx Plugin (TS solution)', () => {
// Register plugin in nx.json (required for inference) // Register plugin in nx.json (required for inference)
updateJson(`nx.json`, (nxJson) => { updateJson(`nx.json`, (nxJson) => {
nxJson.plugins = [ nxJson.plugins.push({
{
plugin: `@${workspaceName}/${plugin}`, plugin: `@${workspaceName}/${plugin}`,
options: { inferredTags: ['my-tag'] }, options: { inferredTags: ['my-tag'] },
}, });
];
return nxJson; return nxJson;
}); });
@ -262,4 +262,22 @@ describe('Nx Plugin (TS solution)', () => {
expect(() => checkFilesExist(`libs/${generatedProject}`)).not.toThrow(); expect(() => checkFilesExist(`libs/${generatedProject}`)).not.toThrow();
expect(() => runCLI(`execute ${generatedProject}`)).not.toThrow(); expect(() => runCLI(`execute ${generatedProject}`)).not.toThrow();
}); });
it('should respect and support generating plugins with a name different than the import path', async () => {
const plugin = uniq('plugin');
runCLI(
`generate @nx/plugin:plugin packages/${plugin} --name=${plugin} --linter=eslint --publishable`
);
const packageJson = readJson(`packages/${plugin}/package.json`);
expect(packageJson.nx.name).toBe(plugin);
expect(runCLI(`build ${plugin}`)).toContain(
`Successfully ran target build for project ${plugin}`
);
expect(runCLI(`lint ${plugin}`)).toContain(
`Successfully ran target lint for project ${plugin}`
);
}, 90000);
}); });

View File

@ -0,0 +1,41 @@
import {
cleanupProject,
newProject,
readJson,
runCLI,
uniq,
} from '@nx/e2e/utils';
describe('React (TS solution)', () => {
let workspaceName: string;
beforeAll(() => {
workspaceName = newProject({ preset: 'ts', packages: ['@nx/react'] });
});
afterAll(() => cleanupProject());
it('should respect and support generating libraries with a name different than the import path', async () => {
const lib = uniq('lib');
runCLI(
`generate @nx/react:library packages/${lib} --name=${lib} --bundler=vite --linter=eslint --unitTestRunner=vitest`
);
const packageJson = readJson(`packages/${lib}/package.json`);
expect(packageJson.nx.name).toBe(lib);
expect(runCLI(`build ${lib}`)).toContain(
`Successfully ran target build for project ${lib}`
);
expect(runCLI(`typecheck ${lib}`)).toContain(
`Successfully ran target typecheck for project ${lib}`
);
expect(runCLI(`lint ${lib}`)).toContain(
`Successfully ran target lint for project ${lib}`
);
expect(runCLI(`test ${lib}`)).toContain(
`Successfully ran target test for project ${lib}`
);
}, 90000);
});

View File

@ -1,4 +1,10 @@
import { cleanupProject, newProject, runCLI, uniq } from '@nx/e2e/utils'; import {
cleanupProject,
newProject,
readJson,
runCLI,
uniq,
} from '@nx/e2e/utils';
describe('Remix - TS solution setup', () => { describe('Remix - TS solution setup', () => {
beforeAll(() => { beforeAll(() => {
@ -113,4 +119,28 @@ describe('Remix - TS solution setup', () => {
`Successfully ran target test for project @proj/${buildableLibJest}` `Successfully ran target test for project @proj/${buildableLibJest}`
); );
}, 120_000); }, 120_000);
it('should respect and support generating libraries with a name different than the import path', async () => {
const lib = uniq('lib');
runCLI(
`generate @nx/remix:library packages/${lib} --name=${lib} --linter=eslint --unitTestRunner=vitest --buildable`
);
const packageJson = readJson(`packages/${lib}/package.json`);
expect(packageJson.nx.name).toBe(lib);
expect(runCLI(`build ${lib}`)).toContain(
`Successfully ran target build for project ${lib}`
);
expect(runCLI(`typecheck ${lib}`)).toContain(
`Successfully ran target typecheck for project ${lib}`
);
expect(runCLI(`lint ${lib}`)).toContain(
`Successfully ran target lint for project ${lib}`
);
expect(runCLI(`test ${lib}`)).toContain(
`Successfully ran target test for project ${lib}`
);
}, 120_000);
}); });

View File

@ -1,18 +1,15 @@
import { import {
cleanupProject, cleanupProject,
getSelectedPackageManager,
newProject, newProject,
readJson,
runCLI, runCLI,
uniq, uniq,
updateFile, updateFile,
updateJson,
} from '@nx/e2e/utils'; } from '@nx/e2e/utils';
describe('Vue Plugin', () => { describe('Vue (TS solution)', () => {
let proj: string; let proj: string;
const pm = getSelectedPackageManager();
beforeAll(() => { beforeAll(() => {
proj = newProject({ proj = newProject({
packages: ['@nx/vue'], packages: ['@nx/vue'],
@ -57,4 +54,32 @@ describe('Vue Plugin', () => {
expect(() => runCLI(`test @proj/${lib}`)).not.toThrow(); expect(() => runCLI(`test @proj/${lib}`)).not.toThrow();
expect(() => runCLI(`build @proj/${lib}`)).not.toThrow(); expect(() => runCLI(`build @proj/${lib}`)).not.toThrow();
}, 300_000); }, 300_000);
it('should respect and support generating libraries with a name different than the import path', async () => {
const lib = uniq('lib');
runCLI(
`generate @nx/vue:library packages/${lib} --name=${lib} --bundler=vite --unitTestRunner=vitest`
);
// lib generator doesn't generate specs, add one
updateFile(
`packages/${lib}/src/foo.spec.ts`,
`test('it should run', () => {
expect(true).toBeTruthy();
});`
);
const packageJson = readJson(`packages/${lib}/package.json`);
expect(packageJson.nx.name).toBe(lib);
expect(runCLI(`build ${lib}`)).toContain(
`Successfully ran target build for project ${lib}`
);
expect(runCLI(`typecheck ${lib}`)).toContain(
`Successfully ran target typecheck for project ${lib}`
);
expect(runCLI(`test ${lib}`)).toContain(
`Successfully ran target test for project ${lib}`
);
}, 300_000);
}); });

View File

@ -1,7 +1,7 @@
import { joinPathFragments, type Tree } from '@nx/devkit'; import { joinPathFragments, type Tree } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Linter } from '@nx/eslint'; import { Linter } from '@nx/eslint';
import { E2eTestRunner, UnitTestRunner } from '../../../utils/test-runners'; import { E2eTestRunner, UnitTestRunner } from '../../../utils/test-runners';
@ -12,7 +12,7 @@ export async function normalizeOptions(
host: Tree, host: Tree,
options: Partial<Schema> options: Partial<Schema>
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
await ensureProjectName(host, options as Schema, 'application'); await ensureRootProjectName(options as Schema, 'application');
const { projectName: appProjectName, projectRoot: appProjectRoot } = const { projectName: appProjectName, projectRoot: appProjectRoot } =
await determineProjectNameAndRootOptions(host, { await determineProjectNameAndRootOptions(host, {
name: options.name, name: options.name,

View File

@ -7,7 +7,7 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { isValidVariable } from '@nx/js'; import { isValidVariable } from '@nx/js';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
@ -53,7 +53,7 @@ export async function host(tree: Tree, schema: Schema) {
}); });
} }
await ensureProjectName(tree, options, 'application'); await ensureRootProjectName(options, 'application');
const { projectName: hostProjectName, projectRoot: appRoot } = const { projectName: hostProjectName, projectRoot: appRoot } =
await determineProjectNameAndRootOptions(tree, { await determineProjectNameAndRootOptions(tree, {
name: options.name, name: options.name,

View File

@ -1,7 +1,7 @@
import { names, Tree } from '@nx/devkit'; import { names, Tree } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Linter } from '@nx/eslint'; import { Linter } from '@nx/eslint';
import { UnitTestRunner } from '../../../utils/test-runners'; import { UnitTestRunner } from '../../../utils/test-runners';
@ -29,7 +29,7 @@ export async function normalizeOptions(
...schema, ...schema,
}; };
await ensureProjectName(host, options, 'library'); await ensureRootProjectName(options, 'library');
const { const {
projectName, projectName,
names: projectNames, names: projectNames,

View File

@ -8,7 +8,7 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { swcHelpersVersion } from '@nx/js/src/utils/versions'; import { swcHelpersVersion } from '@nx/js/src/utils/versions';
@ -32,7 +32,7 @@ export async function remote(tree: Tree, schema: Schema) {
); );
} }
await ensureProjectName(tree, options, 'application'); await ensureRootProjectName(options, 'application');
const { projectName: remoteProjectName } = const { projectName: remoteProjectName } =
await determineProjectNameAndRootOptions(tree, { await determineProjectNameAndRootOptions(tree, {
name: options.name, name: options.name,

View File

@ -474,10 +474,10 @@ function createPackageJson(tree: Tree, options: NormalizedSchema) {
name: importPath, name: importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: {
name: options.project,
},
}; };
if (options.project !== importPath) {
packageJson.nx = { name: options.project };
}
writeJson(tree, packageJsonPath, packageJson); writeJson(tree, packageJsonPath, packageJson);
} }

View File

@ -548,12 +548,10 @@ describe('detox application generator', () => {
expect(tree.read('apps/my-app-e2e/package.json', 'utf-8')) expect(tree.read('apps/my-app-e2e/package.json', 'utf-8'))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"{ "{
"name": "my-app-e2e", "name": "@proj/my-app-e2e",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"nx": { "nx": {
"sourceRoot": "apps/my-app-e2e/src",
"projectType": "application",
"implicitDependencies": [ "implicitDependencies": [
"my-app" "my-app"
] ]
@ -660,5 +658,34 @@ describe('detox application generator', () => {
" "
`); `);
}); });
it('should respect the provided e2e name', async () => {
writeJson(tree, 'apps/my-app/package.json', {
name: 'my-app',
});
await detoxApplicationGenerator(tree, {
e2eDirectory: 'apps/my-app-e2e',
appProject: 'my-app',
e2eName: 'my-app-e2e',
linter: Linter.None,
framework: 'react-native',
addPlugin: true,
skipFormat: true,
});
const packageJson = readJson(tree, 'apps/my-app-e2e/package.json');
expect(packageJson.name).toBe('@proj/my-app-e2e');
expect(packageJson.nx.name).toBe('my-app-e2e');
// Make sure keys are in idiomatic order
expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
[
"name",
"version",
"private",
"nx",
]
`);
});
}); });
}); });

View File

@ -13,6 +13,7 @@ describe('Add Linting', () => {
e2eDirectory: 'my-app-e2e', e2eDirectory: 'my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
importPath: '@proj/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',
appClassName: 'MyApp', appClassName: 'MyApp',
@ -30,6 +31,7 @@ describe('Add Linting', () => {
e2eDirectory: 'my-app-e2e', e2eDirectory: 'my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
importPath: '@proj/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',
appClassName: 'MyApp', appClassName: 'MyApp',
@ -49,6 +51,7 @@ describe('Add Linting', () => {
e2eDirectory: 'my-app-e2e', e2eDirectory: 'my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
importPath: '@proj/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',
appClassName: 'MyApp', appClassName: 'MyApp',

View File

@ -32,6 +32,7 @@ describe('Add Project', () => {
e2eDirectory: 'my-app-e2e', e2eDirectory: 'my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
importPath: '@proj/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',
appClassName: 'MyApp', appClassName: 'MyApp',
@ -81,6 +82,7 @@ describe('Add Project', () => {
e2eDirectory: 'my-dir-my-app-e2e', e2eDirectory: 'my-dir-my-app-e2e',
e2eProjectName: 'my-dir-my-app-e2e', e2eProjectName: 'my-dir-my-app-e2e',
e2eProjectRoot: 'apps/my-dir/my-app-e2e', e2eProjectRoot: 'apps/my-dir/my-app-e2e',
importPath: '@proj/my-dir-my-app-e2e',
appProject: 'my-dir-my-app', appProject: 'my-dir-my-app',
appFileName: 'my-app', appFileName: 'my-app',
appClassName: 'MyApp', appClassName: 'MyApp',

View File

@ -25,12 +25,14 @@ export function addProject(host: Tree, options: NormalizedSchema) {
if (isUsingTsSolutionSetup(host)) { if (isUsingTsSolutionSetup(host)) {
writeJson(host, joinPathFragments(options.e2eProjectRoot, 'package.json'), { writeJson(host, joinPathFragments(options.e2eProjectRoot, 'package.json'), {
name: options.e2eProjectName, name: options.importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
sourceRoot: `${options.e2eProjectRoot}/src`, name:
projectType: 'application', options.e2eProjectName !== options.importPath
? options.e2eProjectName
: undefined,
targets: hasPlugin ? undefined : getTargets(options), targets: hasPlugin ? undefined : getTargets(options),
implicitDependencies: [options.appProject], implicitDependencies: [options.appProject],
}, },

View File

@ -15,6 +15,7 @@ describe('Create Files', () => {
e2eDirectory: 'my-app-e2e', e2eDirectory: 'my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
importPath: '@proj/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',
appClassName: 'MyApp', appClassName: 'MyApp',

View File

@ -27,10 +27,10 @@ describe('Normalize Options', () => {
expect(options).toEqual({ expect(options).toEqual({
addPlugin: true, addPlugin: true,
framework: 'react-native', framework: 'react-native',
e2eName: 'my-app-e2e',
e2eDirectory: 'apps/my-app-e2e', e2eDirectory: 'apps/my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
importPath: '@proj/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',
appClassName: 'MyApp', appClassName: 'MyApp',
@ -62,11 +62,11 @@ describe('Normalize Options', () => {
appClassName: 'MyApp', appClassName: 'MyApp',
appFileName: 'my-app', appFileName: 'my-app',
appRoot: 'apps/my-app', appRoot: 'apps/my-app',
e2eName: 'my-app-e2e',
e2eDirectory: 'apps/my-app-e2e', e2eDirectory: 'apps/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
importPath: '@proj/my-app-e2e',
framework: 'react-native', framework: 'react-native',
isUsingTsSolutionConfig: false, isUsingTsSolutionConfig: false,
js: false, js: false,
@ -94,6 +94,7 @@ describe('Normalize Options', () => {
appFileName: 'my-app', appFileName: 'my-app',
appRoot: 'apps/my-app', appRoot: 'apps/my-app',
e2eProjectRoot: 'directory', e2eProjectRoot: 'directory',
importPath: '@proj/directory-my-app-e2e',
e2eName: 'directory-my-app-e2e', e2eName: 'directory-my-app-e2e',
e2eDirectory: 'directory', e2eDirectory: 'directory',
e2eProjectName: 'directory-my-app-e2e', e2eProjectName: 'directory-my-app-e2e',

View File

@ -1,18 +1,16 @@
import { names, readNxJson, readProjectConfiguration, Tree } from '@nx/devkit'; import { names, readNxJson, readProjectConfiguration, Tree } from '@nx/devkit';
import { import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
determineProjectNameAndRootOptions,
ensureProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Omit<Schema, 'e2eName'> {
appFileName: string; // the file name of app to be tested in kebab case appFileName: string; // the file name of app to be tested in kebab case
appClassName: string; // the class name of app to be tested in pascal case appClassName: string; // the class name of app to be tested in pascal case
appExpoName: string; // the expo name of app to be tested in class case appExpoName: string; // the expo name of app to be tested in class case
appRoot: string; // the root path of e2e project. e.g. apps/app-directory/app appRoot: string; // the root path of e2e project. e.g. apps/app-directory/app
e2eProjectName: string; // the name of e2e project e2eProjectName: string; // the name of e2e project
e2eProjectRoot: string; // the root path of e2e project. e.g. apps/e2e-directory/e2e-app e2eProjectRoot: string; // the root path of e2e project. e.g. apps/e2e-directory/e2e-app
importPath: string;
isUsingTsSolutionConfig?: boolean; isUsingTsSolutionConfig?: boolean;
} }
@ -20,8 +18,11 @@ export async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
const { projectName: e2eProjectName, projectRoot: e2eProjectRoot } = const {
await determineProjectNameAndRootOptions(host, { projectName,
projectRoot: e2eProjectRoot,
importPath,
} = await determineProjectNameAndRootOptions(host, {
name: options.e2eName, name: options.e2eName,
projectType: 'application', projectType: 'application',
directory: options.e2eDirectory, directory: options.e2eDirectory,
@ -37,6 +38,10 @@ export async function normalizeOptions(
); );
const { root: appRoot } = readProjectConfiguration(host, options.appProject); const { root: appRoot } = readProjectConfiguration(host, options.appProject);
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
const e2eProjectName =
!isUsingTsSolutionConfig || options.e2eName ? projectName : importPath;
return { return {
...options, ...options,
appFileName, appFileName,
@ -44,10 +49,10 @@ export async function normalizeOptions(
appDisplayName: options.appDisplayName || appClassName, appDisplayName: options.appDisplayName || appClassName,
appExpoName: options.appDisplayName?.replace(/\s/g, '') || appClassName, appExpoName: options.appDisplayName?.replace(/\s/g, '') || appClassName,
appRoot, appRoot,
e2eName: e2eProjectName,
e2eProjectName, e2eProjectName,
e2eProjectRoot, e2eProjectRoot,
isUsingTsSolutionConfig: isUsingTsSolutionSetup(host), importPath,
isUsingTsSolutionConfig,
js: options.js ?? false, js: options.js ?? false,
}; };
} }

View File

@ -9,7 +9,6 @@ import { determineProjectNameAndRootOptions } from './project-name-and-root-util
describe('determineProjectNameAndRootOptions', () => { describe('determineProjectNameAndRootOptions', () => {
let tree: Tree; let tree: Tree;
describe('no layout', () => {
beforeEach(() => { beforeEach(() => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
@ -266,4 +265,3 @@ describe('determineProjectNameAndRootOptions', () => {
}); });
}); });
}); });
});

View File

@ -42,7 +42,7 @@ export type ProjectNameAndRootOptions = {
/** /**
* Normalized import path for the project. * Normalized import path for the project.
*/ */
importPath?: string; importPath: string;
}; };
export async function determineProjectNameAndRootOptions( export async function determineProjectNameAndRootOptions(
@ -88,11 +88,8 @@ export async function determineProjectNameAndRootOptions(
} }
} }
let importPath: string | undefined = undefined; const importPath =
if (options.projectType === 'library') {
importPath =
options.importPath ?? resolveImportPath(tree, name, projectRoot); options.importPath ?? resolveImportPath(tree, name, projectRoot);
}
return { return {
projectName: name, projectName: name,
@ -125,24 +122,17 @@ export function resolveImportPath(
return importPath; return importPath;
} }
export async function ensureProjectName( export async function ensureRootProjectName(
tree: Tree, options: { directory: string; name?: string },
options: Omit<ProjectGenerationOptions, 'projectType'>,
projectType: 'application' | 'library' projectType: 'application' | 'library'
): Promise<void> { ): Promise<void> {
if (!options.name) { if (!options.name && options.directory === '.' && getRelativeCwd() === '') {
if (options.directory === '.' && getRelativeCwd() === '') {
const result = await prompt<{ name: string }>({ const result = await prompt<{ name: string }>({
type: 'input', type: 'input',
name: 'name', name: 'name',
message: `What do you want to name the ${projectType}?`, message: `What do you want to name the ${projectType}?`,
}).then(({ name }) => (options.name = name));
}
const { projectName } = await determineProjectNameAndRootOptions(tree, {
...options,
projectType,
}); });
options.name = projectName; options.name = result.name;
} }
} }

View File

@ -11,7 +11,10 @@ import {
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { getOutputDir, getUpdatedPackageJsonContent } from '@nx/js'; import { getOutputDir, getUpdatedPackageJsonContent } from '@nx/js';
import { getImportPath } from '@nx/js/src/utils/get-import-path'; import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import {
getProjectSourceRoot,
isUsingTsSolutionSetup,
} from '@nx/js/src/utils/typescript/ts-solution-setup';
import { basename, dirname, join } from 'node:path/posix'; import { basename, dirname, join } from 'node:path/posix';
import { mergeTargetConfigurations } from 'nx/src/devkit-internals'; import { mergeTargetConfigurations } from 'nx/src/devkit-internals';
import { PackageJson } from 'nx/src/utils/package-json'; import { PackageJson } from 'nx/src/utils/package-json';
@ -78,10 +81,11 @@ function addBuildTarget(
}; };
if (isTsSolutionSetup) { if (isTsSolutionSetup) {
buildOptions.declarationRootDir = buildOptions.declarationRootDir = getProjectSourceRoot(
project.sourceRoot ?? tree.exists(`${project.root}/src`) tree,
? `${project.root}/src` project.sourceRoot,
: project.root; project.root
);
} else { } else {
buildOptions.assets = []; buildOptions.assets = [];

View File

@ -505,6 +505,9 @@ describe('@nx/eslint:lint-project', () => {
} }
" "
`); `);
expect(
readJson(tree, 'package.json').devDependencies['jsonc-eslint-parser']
).toBeDefined();
}); });
it('should generate a project config for buildable lib with lintFilePatterns if provided', async () => { it('should generate a project config for buildable lib with lintFilePatterns if provided', async () => {

View File

@ -1,4 +1,5 @@
import { import {
addDependenciesToPackageJson,
createProjectGraphAsync, createProjectGraphAsync,
formatFiles, formatFiles,
GeneratorCallback, GeneratorCallback,
@ -38,6 +39,7 @@ import {
BASE_ESLINT_CONFIG_FILENAMES, BASE_ESLINT_CONFIG_FILENAMES,
} from '../../utils/config-file'; } from '../../utils/config-file';
import { hasEslintPlugin } from '../utils/plugin'; import { hasEslintPlugin } from '../utils/plugin';
import { jsoncEslintParserVersion } from '../../utils/versions';
import { setupRootEsLint } from './setup-root-eslint'; import { setupRootEsLint } from './setup-root-eslint';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
@ -165,13 +167,29 @@ export async function lintProjectGeneratorInternal(
// additionally, the companion e2e app would have `rootProject: true` // additionally, the companion e2e app would have `rootProject: true`
// so we need to check for the root path as well // so we need to check for the root path as well
if (!options.rootProject || projectConfig.root !== '.') { if (!options.rootProject || projectConfig.root !== '.') {
const addDependencyChecks =
options.addPackageJsonDependencyChecks ||
isBuildableLibraryProject(tree, projectConfig);
createEsLintConfiguration( createEsLintConfiguration(
tree, tree,
options, options,
projectConfig, projectConfig,
options.setParserOptionsProject, options.setParserOptionsProject,
options.rootProject options.rootProject,
addDependencyChecks
); );
if (addDependencyChecks) {
tasks.push(
addDependenciesToPackageJson(
tree,
{},
{ 'jsonc-eslint-parser': jsoncEslintParserVersion },
undefined,
true
)
);
}
} }
// Buildable libs need source analysis enabled for linting `package.json`. // Buildable libs need source analysis enabled for linting `package.json`.
@ -201,7 +219,8 @@ function createEsLintConfiguration(
options: LintProjectOptions, options: LintProjectOptions,
projectConfig: ProjectConfiguration, projectConfig: ProjectConfiguration,
setParserOptionsProject: boolean, setParserOptionsProject: boolean,
rootProject: boolean rootProject: boolean,
addDependencyChecks: boolean
) { ) {
// we are only extending root for non-standalone projects or their complementary e2e apps // we are only extending root for non-standalone projects or their complementary e2e apps
const extendedRootConfig = rootProject ? undefined : findEslintFile(tree); const extendedRootConfig = rootProject ? undefined : findEslintFile(tree);
@ -224,10 +243,6 @@ function createEsLintConfiguration(
} }
} }
const addDependencyChecks =
options.addPackageJsonDependencyChecks ||
isBuildableLibraryProject(tree, projectConfig);
const overrides: Linter.ConfigOverride<Linter.RulesRecord>[] = useFlatConfig( const overrides: Linter.ConfigOverride<Linter.RulesRecord>[] = useFlatConfig(
tree tree
) )

View File

@ -4,6 +4,7 @@ export const eslintVersion = '~8.57.0';
export const eslintrcVersion = '^2.1.1'; export const eslintrcVersion = '^2.1.1';
export const eslintConfigPrettierVersion = '^9.0.0'; export const eslintConfigPrettierVersion = '^9.0.0';
export const typescriptESLintVersion = '^7.16.0'; export const typescriptESLintVersion = '^7.16.0';
export const jsoncEslintParserVersion = '^2.1.0';
// Updated linting stack for ESLint v9, typescript-eslint v8 // Updated linting stack for ESLint v9, typescript-eslint v8
export const eslint9__typescriptESLintVersion = '^8.19.0'; export const eslint9__typescriptESLintVersion = '^8.19.0';

View File

@ -331,8 +331,10 @@ describe('app', () => {
}); });
describe('TS solution setup', () => { describe('TS solution setup', () => {
it('should add project references when using TS solution', async () => { let tree: Tree;
const tree = createTreeWithEmptyWorkspace();
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
tree.write('.gitignore', ''); tree.write('.gitignore', '');
updateJson(tree, 'package.json', (json) => { updateJson(tree, 'package.json', (json) => {
json.workspaces = ['packages/*', 'apps/*']; json.workspaces = ['packages/*', 'apps/*'];
@ -349,7 +351,9 @@ describe('app', () => {
files: [], files: [],
references: [], references: [],
}); });
});
it('should add project references when using TS solution', async () => {
await expoApplicationGenerator(tree, { await expoApplicationGenerator(tree, {
directory: 'my-app', directory: 'my-app',
displayName: 'myApp', displayName: 'myApp',
@ -368,14 +372,15 @@ describe('app', () => {
}, },
] ]
`); `);
const packageJson = readJson(tree, 'my-app/package.json');
expect(packageJson.name).toBe('@proj/my-app');
expect(packageJson.nx).toBeUndefined();
// Make sure keys are in idiomatic order // Make sure keys are in idiomatic order
expect(Object.keys(readJson(tree, 'my-app/package.json'))) expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
.toMatchInlineSnapshot(`
[ [
"name", "name",
"version", "version",
"private", "private",
"nx",
] ]
`); `);
expect(readJson(tree, 'my-app/tsconfig.json')).toMatchInlineSnapshot(` expect(readJson(tree, 'my-app/tsconfig.json')).toMatchInlineSnapshot(`
@ -476,5 +481,32 @@ describe('app', () => {
} }
`); `);
}); });
it('should respect provided name', async () => {
await expoApplicationGenerator(tree, {
directory: 'my-app',
name: 'my-app',
displayName: 'myApp',
linter: Linter.EsLint,
e2eTestRunner: 'none',
skipFormat: false,
js: false,
unitTestRunner: 'jest',
addPlugin: true,
});
const packageJson = readJson(tree, 'my-app/package.json');
expect(packageJson.name).toBe('@proj/my-app');
expect(packageJson.nx.name).toBe('my-app');
// Make sure keys are in idiomatic order
expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
[
"name",
"version",
"private",
"nx",
]
`);
});
}); });
}); });

View File

@ -62,6 +62,12 @@ export async function expoApplicationGeneratorInternal(
await createApplicationFiles(host, options); await createApplicationFiles(host, options);
addProject(host, options); addProject(host, options);
// 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.isTsSolutionSetup) {
addProjectToTsSolutionWorkspace(host, options.appProjectRoot);
}
const lintTask = await addLinting(host, { const lintTask = await addLinting(host, {
...options, ...options,
projectRoot: options.appProjectRoot, projectRoot: options.appProjectRoot,
@ -100,12 +106,6 @@ export async function expoApplicationGeneratorInternal(
: undefined : undefined
); );
// 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.useTsSolution) {
addProjectToTsSolutionWorkspace(host, options.appProjectRoot);
}
sortPackageJsonFields(host, options.appProjectRoot); sortPackageJsonFields(host, options.appProjectRoot);
if (!options.skipFormat) { if (!options.skipFormat) {

View File

@ -51,8 +51,6 @@ export async function addE2e(
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName], implicitDependencies: [options.projectName],
}, },
} }
@ -134,8 +132,6 @@ export async function addE2e(
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName], implicitDependencies: [options.projectName],
}, },
} }
@ -205,7 +201,7 @@ export async function addE2e(
e2eDirectory: options.e2eProjectRoot, e2eDirectory: options.e2eProjectRoot,
appProject: options.projectName, appProject: options.projectName,
appDisplayName: options.displayName, appDisplayName: options.displayName,
appName: options.name, appName: options.simpleName,
framework: 'expo', framework: 'expo',
setParserOptionsProject: options.setParserOptionsProject, setParserOptionsProject: options.setParserOptionsProject,
skipFormat: true, skipFormat: true,

View File

@ -2,7 +2,6 @@ import {
addProjectConfiguration, addProjectConfiguration,
joinPathFragments, joinPathFragments,
ProjectConfiguration, ProjectConfiguration,
readNxJson,
TargetConfiguration, TargetConfiguration,
Tree, Tree,
writeJson, writeJson,
@ -12,10 +11,9 @@ import { hasExpoPlugin } from '../../../utils/has-expo-plugin';
import { NormalizedSchema } from './normalize-options'; import { NormalizedSchema } from './normalize-options';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path'; import type { PackageJson } from 'nx/src/utils/package-json';
export function addProject(host: Tree, options: NormalizedSchema) { export function addProject(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host);
const hasPlugin = hasExpoPlugin(host); const hasPlugin = hasExpoPlugin(host);
if (!hasPlugin) { if (!hasPlugin) {
@ -31,19 +29,29 @@ export function addProject(host: Tree, options: NormalizedSchema) {
}; };
if (isUsingTsSolutionSetup(host)) { if (isUsingTsSolutionSetup(host)) {
const packageName = getImportPath(host, options.name); const packageJson: PackageJson = {
writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), { name: options.importPath,
name: packageName,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { };
name: packageName === options.name ? undefined : options.name,
projectType: 'application', if (options.importPath !== options.projectName) {
sourceRoot: `${options.appProjectRoot}/src`, packageJson.nx = { name: options.projectName };
targets: hasPlugin ? undefined : getTargets(options), }
tags: options.parsedTags?.length ? options.parsedTags : undefined, if (!hasPlugin) {
}, packageJson.nx ??= {};
}); packageJson.nx.targets = getTargets(options);
}
if (options.parsedTags?.length) {
packageJson.nx ??= {};
packageJson.nx.tags = options.parsedTags;
}
writeJson(
host,
joinPathFragments(options.appProjectRoot, 'package.json'),
packageJson
);
} else { } else {
addProjectConfiguration( addProjectConfiguration(
host, host,

View File

@ -28,11 +28,12 @@ describe('Normalize Options', () => {
directory: 'my-app', directory: 'my-app',
displayName: 'MyApp', displayName: 'MyApp',
lowerCaseName: 'myapp', lowerCaseName: 'myapp',
name: 'my-app', simpleName: 'my-app',
parsedTags: [], parsedTags: [],
projectName: 'my-app', projectName: 'my-app',
linter: Linter.EsLint, linter: Linter.EsLint,
e2eTestRunner: 'none', e2eTestRunner: 'none',
importPath: '@proj/my-app',
unitTestRunner: 'jest', unitTestRunner: 'jest',
skipFormat: false, skipFormat: false,
js: true, js: true,
@ -60,11 +61,12 @@ describe('Normalize Options', () => {
directory: 'myApp', directory: 'myApp',
displayName: 'MyApp', displayName: 'MyApp',
lowerCaseName: 'myapp', lowerCaseName: 'myapp',
name: 'myApp', simpleName: 'myApp',
parsedTags: [], parsedTags: [],
projectName: 'myApp', projectName: 'myApp',
linter: Linter.EsLint, linter: Linter.EsLint,
e2eTestRunner: 'none', e2eTestRunner: 'none',
importPath: '@proj/myApp',
skipFormat: false, skipFormat: false,
js: true, js: true,
unitTestRunner: 'jest', unitTestRunner: 'jest',
@ -93,10 +95,12 @@ describe('Normalize Options', () => {
displayName: 'MyApp', displayName: 'MyApp',
lowerCaseName: 'myapp', lowerCaseName: 'myapp',
name: 'my-app', name: 'my-app',
simpleName: 'my-app',
directory: 'directory', directory: 'directory',
parsedTags: [], parsedTags: [],
projectName: 'my-app', projectName: 'my-app',
e2eTestRunner: 'none', e2eTestRunner: 'none',
importPath: '@proj/my-app',
unitTestRunner: 'jest', unitTestRunner: 'jest',
linter: Linter.EsLint, linter: Linter.EsLint,
skipFormat: false, skipFormat: false,
@ -125,10 +129,11 @@ describe('Normalize Options', () => {
directory: 'directory/my-app', directory: 'directory/my-app',
displayName: 'MyApp', displayName: 'MyApp',
lowerCaseName: 'myapp', lowerCaseName: 'myapp',
name: 'my-app', simpleName: 'my-app',
parsedTags: [], parsedTags: [],
projectName: 'my-app', projectName: 'my-app',
e2eTestRunner: 'none', e2eTestRunner: 'none',
importPath: '@proj/my-app',
unitTestRunner: 'jest', unitTestRunner: 'jest',
linter: Linter.EsLint, linter: Linter.EsLint,
skipFormat: false, skipFormat: false,
@ -158,10 +163,11 @@ describe('Normalize Options', () => {
className: 'MyApp', className: 'MyApp',
displayName: 'My App', displayName: 'My App',
lowerCaseName: 'myapp', lowerCaseName: 'myapp',
name: 'my-app', simpleName: 'my-app',
parsedTags: [], parsedTags: [],
projectName: 'my-app', projectName: 'my-app',
e2eTestRunner: 'none', e2eTestRunner: 'none',
importPath: '@proj/my-app',
unitTestRunner: 'jest', unitTestRunner: 'jest',
linter: Linter.EsLint, linter: Linter.EsLint,
skipFormat: false, skipFormat: false,

View File

@ -1,15 +1,18 @@
import { names, readNxJson, Tree } from '@nx/devkit'; import { names, readNxJson, Tree } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { Schema } from '../schema'; import { Schema } from '../schema';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema
extends Omit<Schema, 'name' | 'useTsSolution'> {
className: string; className: string;
simpleName: string;
projectName: string; projectName: string;
appProjectRoot: string; appProjectRoot: string;
importPath: string;
lowerCaseName: string; lowerCaseName: string;
parsedTags: string[]; parsedTags: string[];
rootProject: boolean; rootProject: boolean;
@ -22,11 +25,12 @@ export async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
await ensureProjectName(host, options, 'application'); await ensureRootProjectName(options, 'application');
const { const {
projectName: appProjectName, projectName,
names: projectNames, names: projectNames,
projectRoot: appProjectRoot, projectRoot: appProjectRoot,
importPath,
} = await determineProjectNameAndRootOptions(host, { } = await determineProjectNameAndRootOptions(host, {
name: options.name, name: options.name,
projectType: 'application', projectType: 'application',
@ -38,12 +42,16 @@ export async function normalizeOptions(
nxJson.useInferencePlugins !== false; nxJson.useInferencePlugins !== false;
options.addPlugin ??= addPluginDefault; options.addPlugin ??= addPluginDefault;
const { className } = names(options.name); const { className } = names(projectName);
const parsedTags = options.tags const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim()) ? options.tags.split(',').map((s) => s.trim())
: []; : [];
const rootProject = appProjectRoot === '.'; const rootProject = appProjectRoot === '.';
const isTsSolutionSetup = isUsingTsSolutionSetup(host);
const appProjectName =
!isTsSolutionSetup || options.name ? projectName : importPath;
const e2eProjectName = rootProject ? 'e2e' : `${appProjectName}-e2e`; const e2eProjectName = rootProject ? 'e2e' : `${appProjectName}-e2e`;
const e2eProjectRoot = rootProject ? 'e2e' : `${appProjectRoot}-e2e`; const e2eProjectRoot = rootProject ? 'e2e' : `${appProjectRoot}-e2e`;
@ -51,16 +59,17 @@ export async function normalizeOptions(
...options, ...options,
unitTestRunner: options.unitTestRunner || 'jest', unitTestRunner: options.unitTestRunner || 'jest',
e2eTestRunner: options.e2eTestRunner || 'none', e2eTestRunner: options.e2eTestRunner || 'none',
name: projectNames.projectSimpleName, simpleName: projectNames.projectSimpleName,
className, className,
lowerCaseName: className.toLowerCase(), lowerCaseName: className.toLowerCase(),
displayName: options.displayName || className, displayName: options.displayName || className,
projectName: appProjectName, projectName: appProjectName,
appProjectRoot, appProjectRoot,
importPath,
parsedTags, parsedTags,
rootProject, rootProject,
e2eProjectName, e2eProjectName,
e2eProjectRoot, e2eProjectRoot,
isTsSolutionSetup: isUsingTsSolutionSetup(host), isTsSolutionSetup,
}; };
} }

View File

@ -1,17 +1,17 @@
import { readNxJson, Tree } from '@nx/devkit'; import { readNxJson, Tree } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
name: string; name: string;
fileName: string; fileName: string;
projectName: string; projectName: string;
projectRoot: string; projectRoot: string;
importPath: string;
routePath: string; routePath: string;
parsedTags: string[]; parsedTags: string[];
isUsingTsSolutionConfig: boolean; isUsingTsSolutionConfig: boolean;
@ -21,7 +21,7 @@ export async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
await ensureProjectName(host, options, 'library'); await ensureRootProjectName(options, 'library');
const { const {
projectName, projectName,
names: projectNames, names: projectNames,
@ -49,9 +49,8 @@ export async function normalizeOptions(
fileName: projectName, fileName: projectName,
routePath: `/${projectNames.projectSimpleName}`, routePath: `/${projectNames.projectSimpleName}`,
name: projectName, name: projectName,
projectName: isUsingTsSolutionConfig projectName:
? importPath ?? getImportPath(host, projectName) isUsingTsSolutionConfig && !options.name ? importPath : projectName,
: projectName,
projectRoot, projectRoot,
parsedTags, parsedTags,
importPath, importPath,

View File

@ -503,7 +503,6 @@ describe('lib', () => {
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"name": "@proj/my-lib", "name": "@proj/my-lib",
"nx": {},
"peerDependencies": { "peerDependencies": {
"react": "~18.3.1", "react": "~18.3.1",
"react-native": "0.76.3", "react-native": "0.76.3",
@ -520,7 +519,6 @@ describe('lib', () => {
"main", "main",
"types", "types",
"exports", "exports",
"nx",
"peerDependencies", "peerDependencies",
] ]
`); `);
@ -639,7 +637,6 @@ describe('lib', () => {
"main": "./src/index.ts", "main": "./src/index.ts",
"module": "./dist/index.esm.js", "module": "./dist/index.esm.js",
"name": "@proj/my-lib", "name": "@proj/my-lib",
"nx": {},
"peerDependencies": { "peerDependencies": {
"react": "~18.3.1", "react": "~18.3.1",
"react-native": "0.76.3", "react-native": "0.76.3",
@ -649,5 +646,38 @@ describe('lib', () => {
} }
`); `);
}); });
it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => {
await expoLibraryGenerator(appTree, {
...defaultSchema,
directory: 'my-lib',
name: 'my-lib', // import path contains the npm scope, so it would be different
skipFormat: true,
});
expect(readJson(appTree, 'my-lib/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 expoLibraryGenerator(appTree, {
...defaultSchema,
directory: 'my-lib',
name: '@proj/my-lib',
skipFormat: true,
});
expect(readJson(appTree, 'my-lib/package.json').nx).toBeUndefined();
});
it('should not set "nx.name" in package.json when the user does not provide a name', async () => {
await expoLibraryGenerator(appTree, {
...defaultSchema, // defaultSchema has no name
skipFormat: true,
});
expect(readJson(appTree, 'my-lib/package.json').nx).toBeUndefined();
});
}); });
}); });

View File

@ -176,19 +176,30 @@ async function addProject(
}; };
if (options.isUsingTsSolutionConfig) { if (options.isUsingTsSolutionConfig) {
writeJson(host, joinPathFragments(options.projectRoot, 'package.json'), { const packageJson: PackageJson = {
name: options.projectName, name: options.projectName,
version: '0.0.1', version: '0.0.1',
...determineEntryFields(options), ...determineEntryFields(options),
nx: {
tags: options.parsedTags?.length ? options.parsedTags : undefined,
},
files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined, files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined,
peerDependencies: { peerDependencies: {
react: reactVersion, react: reactVersion,
'react-native': reactNativeVersion, 'react-native': reactNativeVersion,
}, },
}); };
if (options.projectName !== options.importPath) {
packageJson.nx = { name: options.projectName };
}
if (options.parsedTags?.length) {
packageJson.nx ??= {};
packageJson.nx.tags = options.parsedTags;
}
writeJson(
host,
joinPathFragments(options.projectRoot, 'package.json'),
packageJson
);
} else { } else {
addProjectConfiguration(host, options.name, project); addProjectConfiguration(host, options.name, project);
} }

View File

@ -185,9 +185,6 @@ describe('app', () => {
{ {
"name": "@proj/myapp", "name": "@proj/myapp",
"nx": { "nx": {
"name": "myapp",
"projectType": "application",
"sourceRoot": "myapp/src",
"targets": { "targets": {
"build": { "build": {
"configurations": { "configurations": {
@ -217,10 +214,10 @@ describe('app', () => {
"serve": { "serve": {
"configurations": { "configurations": {
"development": { "development": {
"buildTarget": "myapp:build:development", "buildTarget": "@proj/myapp:build:development",
}, },
"production": { "production": {
"buildTarget": "myapp:build:production", "buildTarget": "@proj/myapp:build:production",
}, },
}, },
"defaultConfiguration": "development", "defaultConfiguration": "development",
@ -229,7 +226,7 @@ describe('app', () => {
], ],
"executor": "@nx/js:node", "executor": "@nx/js:node",
"options": { "options": {
"buildTarget": "myapp:build", "buildTarget": "@proj/myapp:build",
"runBuildTargetDependencies": false, "runBuildTargetDependencies": false,
}, },
}, },

View File

@ -9,7 +9,7 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { applicationGenerator as nodeApplicationGenerator } from '@nx/node'; import { applicationGenerator as nodeApplicationGenerator } from '@nx/node';
import { tslibVersion } from '@nx/node/src/utils/versions'; import { tslibVersion } from '@nx/node/src/utils/versions';
@ -106,7 +106,7 @@ async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
await ensureProjectName(host, options, 'application'); await ensureRootProjectName(options, 'application');
const { projectName: appProjectName, projectRoot: appProjectRoot } = const { projectName: appProjectName, projectRoot: appProjectRoot } =
await determineProjectNameAndRootOptions(host, { await determineProjectNameAndRootOptions(host, {
name: options.name, name: options.name,

View File

@ -2126,6 +2126,7 @@ describe('lib', () => {
directory: 'my-lib', directory: 'my-lib',
unitTestRunner: 'jest', unitTestRunner: 'jest',
bundler, bundler,
useProjectJson: false,
}); });
expect(tree.exists('my-lib/tsconfig.spec.json')).toBeTruthy(); expect(tree.exists('my-lib/tsconfig.spec.json')).toBeTruthy();
@ -2338,5 +2339,72 @@ describe('lib', () => {
} }
`); `);
}); });
it('should set "nx.name" in package.json when the user provides a name that is different than the package name and "useProjectJson" is "false"', async () => {
await libraryGenerator(tree, {
...defaultOptions,
directory: 'my-lib',
name: 'my-lib',
useProjectJson: false,
bundler: 'none',
addPlugin: true,
});
expect(readJson(tree, 'my-lib/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, {
...defaultOptions,
directory: 'my-lib',
name: '@proj/my-lib',
useProjectJson: false,
bundler: 'none',
addPlugin: true,
});
expect(readJson(tree, 'my-lib/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, {
...defaultOptions,
directory: 'my-lib',
useProjectJson: false,
bundler: 'none',
addPlugin: true,
});
expect(readJson(tree, 'my-lib/package.json').nx).toBeUndefined();
});
it('should set "name" in project.json to the import path when "useProjectJson" is "true"', async () => {
await libraryGenerator(tree, {
...defaultOptions,
directory: 'my-lib',
useProjectJson: true,
bundler: 'none',
addPlugin: true,
});
expect(readJson(tree, 'my-lib/project.json').name).toBe('@proj/my-lib');
expect(readJson(tree, 'my-lib/package.json').nx).toBeUndefined();
});
it('should set "name" in project.json to the user-provided name when "useProjectJson" is "true"', async () => {
await libraryGenerator(tree, {
...defaultOptions,
directory: 'my-lib',
name: 'my-lib',
useProjectJson: true,
bundler: 'none',
addPlugin: true,
});
expect(readJson(tree, 'my-lib/project.json').name).toBe('my-lib');
expect(readJson(tree, 'my-lib/package.json').nx).toBeUndefined();
});
}); });
}); });

View File

@ -22,7 +22,7 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt'; import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
@ -63,7 +63,6 @@ import type {
NormalizedLibraryGeneratorOptions, NormalizedLibraryGeneratorOptions,
} from './schema'; } from './schema';
import { sortPackageJsonFields } from '../../utils/package-json/sort-fields'; import { sortPackageJsonFields } from '../../utils/package-json/sort-fields';
import { getImportPath } from '../../utils/get-import-path';
import { import {
addReleaseConfigForNonTsSolution, addReleaseConfigForNonTsSolution,
addReleaseConfigForTsSolution, addReleaseConfigForTsSolution,
@ -368,13 +367,7 @@ async function configureProject(
} }
// empty targets are cleaned up automatically by `updateProjectConfiguration` // empty targets are cleaned up automatically by `updateProjectConfiguration`
updateProjectConfiguration( updateProjectConfiguration(tree, options.name, projectConfiguration);
tree,
options.isUsingTsSolutionConfig
? options.importPath ?? options.name
: options.name,
projectConfiguration
);
} else if (options.config === 'workspace' || options.config === 'project') { } else if (options.config === 'workspace' || options.config === 'project') {
addProjectConfiguration(tree, options.name, projectConfiguration); addProjectConfiguration(tree, options.name, projectConfiguration);
} else { } else {
@ -682,6 +675,12 @@ function createFiles(tree: Tree, options: NormalizedLibraryGeneratorOptions) {
}); });
} }
if (!options.useProjectJson && options.name !== options.importPath) {
packageJson.nx = {
name: options.name,
};
}
writeJson<PackageJson>(tree, packageJsonPath, packageJson); writeJson<PackageJson>(tree, packageJsonPath, packageJson);
} }
@ -760,7 +759,7 @@ async function normalizeOptions(
tree: Tree, tree: Tree,
options: LibraryGeneratorSchema options: LibraryGeneratorSchema
): Promise<NormalizedLibraryGeneratorOptions> { ): Promise<NormalizedLibraryGeneratorOptions> {
await ensureProjectName(tree, options, 'library'); await ensureRootProjectName(options, 'library');
const nxJson = readNxJson(tree); const nxJson = readNxJson(tree);
options.addPlugin ??= options.addPlugin ??=
process.env.NX_ADD_PLUGINS !== 'false' && process.env.NX_ADD_PLUGINS !== 'false' &&
@ -901,9 +900,7 @@ async function normalizeOptions(
return { return {
...options, ...options,
fileName, fileName,
name: isUsingTsSolutionConfig name: isUsingTsSolutionConfig && !options.name ? importPath : projectName,
? getImportPath(tree, projectName)
: projectName,
projectNames, projectNames,
projectRoot, projectRoot,
parsedTags, parsedTags,

View File

@ -270,3 +270,16 @@ export function getProjectType(
if (!packageJson?.exports) return 'application'; if (!packageJson?.exports) return 'application';
return 'library'; return 'library';
} }
export function getProjectSourceRoot(
tree: Tree,
projectSourceRoot: string | undefined,
projectRoot: string
): string | undefined {
return (
projectSourceRoot ??
(tree.exists(joinPathFragments(projectRoot, 'src'))
? joinPathFragments(projectRoot, 'src')
: projectRoot)
);
}

View File

@ -209,9 +209,6 @@ describe('application generator', () => {
{ {
"name": "@proj/myapp", "name": "@proj/myapp",
"nx": { "nx": {
"name": "myapp",
"projectType": "application",
"sourceRoot": "myapp/src",
"targets": { "targets": {
"build": { "build": {
"configurations": { "configurations": {
@ -232,10 +229,10 @@ describe('application generator', () => {
"serve": { "serve": {
"configurations": { "configurations": {
"development": { "development": {
"buildTarget": "myapp:build:development", "buildTarget": "@proj/myapp:build:development",
}, },
"production": { "production": {
"buildTarget": "myapp:build:production", "buildTarget": "@proj/myapp:build:production",
}, },
}, },
"defaultConfiguration": "development", "defaultConfiguration": "development",
@ -244,7 +241,7 @@ describe('application generator', () => {
], ],
"executor": "@nx/js:node", "executor": "@nx/js:node",
"options": { "options": {
"buildTarget": "myapp:build", "buildTarget": "@proj/myapp:build",
"runBuildTargetDependencies": false, "runBuildTargetDependencies": false,
}, },
}, },

View File

@ -1,7 +1,7 @@
import { Tree, readNxJson } from '@nx/devkit'; import { Tree, readNxJson } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Linter } from '@nx/eslint'; import { Linter } from '@nx/eslint';
import type { Schema as NodeApplicationGeneratorOptions } from '@nx/node/src/generators/application/schema'; import type { Schema as NodeApplicationGeneratorOptions } from '@nx/node/src/generators/application/schema';
@ -11,7 +11,7 @@ export async function normalizeOptions(
tree: Tree, tree: Tree,
options: ApplicationGeneratorOptions options: ApplicationGeneratorOptions
): Promise<NormalizedOptions> { ): Promise<NormalizedOptions> {
await ensureProjectName(tree, options, 'application'); await ensureRootProjectName(options, 'application');
const { projectName: appProjectName, projectRoot: appProjectRoot } = const { projectName: appProjectName, projectRoot: appProjectRoot } =
await determineProjectNameAndRootOptions(tree, { await determineProjectNameAndRootOptions(tree, {
name: options.name, name: options.name,

View File

@ -9,8 +9,8 @@ import type { NormalizedOptions } from '../schema';
export function createFiles(tree: Tree, options: NormalizedOptions): void { export function createFiles(tree: Tree, options: NormalizedOptions): void {
const substitutions = { const substitutions = {
...options,
...names(options.projectName), ...names(options.projectName),
...options,
tmpl: '', tmpl: '',
offsetFromRoot: offsetFromRoot(options.projectRoot), offsetFromRoot: offsetFromRoot(options.projectRoot),
fileName: options.fileName, fileName: options.fileName,

View File

@ -1,20 +1,19 @@
import { Tree, readNxJson } from '@nx/devkit'; import { Tree, readNxJson } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'; import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope';
import type { LibraryGeneratorSchema as JsLibraryGeneratorSchema } from '@nx/js/src/generators/library/schema'; import type { LibraryGeneratorSchema as JsLibraryGeneratorSchema } from '@nx/js/src/generators/library/schema';
import { Linter } from '@nx/eslint'; import { Linter } from '@nx/eslint';
import type { LibraryGeneratorOptions, NormalizedOptions } from '../schema'; import type { LibraryGeneratorOptions, NormalizedOptions } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export async function normalizeOptions( export async function normalizeOptions(
tree: Tree, tree: Tree,
options: LibraryGeneratorOptions options: LibraryGeneratorOptions
): Promise<NormalizedOptions> { ): Promise<NormalizedOptions> {
await ensureProjectName(tree, options, 'library'); await ensureRootProjectName(options, 'library');
const { const {
projectName, projectName,
names: projectNames, names: projectNames,
@ -50,9 +49,8 @@ export async function normalizeOptions(
linter: options.linter ?? Linter.EsLint, linter: options.linter ?? Linter.EsLint,
parsedTags, parsedTags,
prefix: getNpmScope(tree), // we could also allow customizing this prefix: getNpmScope(tree), // we could also allow customizing this
projectName: isUsingTsSolutionsConfig projectName:
? getImportPath(tree, projectName) isUsingTsSolutionsConfig && !options.name ? importPath : projectName,
: projectName,
projectRoot, projectRoot,
importPath, importPath,
service: options.service ?? false, service: options.service ?? false,

View File

@ -409,7 +409,6 @@ describe('lib', () => {
}, },
}, },
"private": true, "private": true,
"type": "module",
"types": "./src/index.ts", "types": "./src/index.ts",
"version": "0.0.1", "version": "0.0.1",
} }
@ -497,5 +496,42 @@ describe('lib', () => {
} }
`); `);
}); });
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', // import path contains the npm scope, so it would be different
linter: 'none',
unitTestRunner: 'none',
skipFormat: true,
});
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',
skipFormat: true,
});
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',
skipFormat: true,
});
expect(readJson(tree, 'mylib/package.json').nx).toBeUndefined();
});
}); });
}); });

View File

@ -1,6 +1,15 @@
import type { GeneratorCallback, Tree } from '@nx/devkit'; import type { GeneratorCallback, Tree } from '@nx/devkit';
import { formatFiles, runTasksInSerial } from '@nx/devkit'; import {
formatFiles,
joinPathFragments,
readJson,
runTasksInSerial,
writeJson,
} from '@nx/devkit';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { libraryGenerator as jsLibraryGenerator } from '@nx/js'; import { libraryGenerator as jsLibraryGenerator } from '@nx/js';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import initGenerator from '../init/init';
import { import {
addExportsToBarrelFile, addExportsToBarrelFile,
addProject, addProject,
@ -10,10 +19,7 @@ import {
toJsLibraryGeneratorOptions, toJsLibraryGeneratorOptions,
updateTsConfig, updateTsConfig,
} from './lib'; } from './lib';
import type { LibraryGeneratorOptions } from './schema'; import type { LibraryGeneratorOptions, NormalizedOptions } from './schema';
import initGenerator from '../init/init';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { ensureDependencies } from '../../utils/ensure-dependencies';
export async function libraryGenerator( export async function libraryGenerator(
tree: Tree, tree: Tree,
@ -34,6 +40,7 @@ export async function libraryGeneratorInternal(
tree, tree,
toJsLibraryGeneratorOptions(options) toJsLibraryGeneratorOptions(options)
); );
updatePackageJson(tree, options);
const initTask = await initGenerator(tree, rawOptions); const initTask = await initGenerator(tree, rawOptions);
const depsTask = ensureDependencies(tree); const depsTask = ensureDependencies(tree);
deleteFiles(tree, options); deleteFiles(tree, options);
@ -59,3 +66,23 @@ export async function libraryGeneratorInternal(
} }
export default libraryGenerator; export default libraryGenerator;
function updatePackageJson(tree: Tree, options: NormalizedOptions) {
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);
}

View File

@ -938,7 +938,6 @@ describe('app (legacy)', () => {
...schema, ...schema,
addPlugin: true, addPlugin: true,
directory: 'myapp', directory: 'myapp',
name: 'myapp',
}); });
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(`
@ -951,14 +950,15 @@ describe('app (legacy)', () => {
}, },
] ]
`); `);
const packageJson = readJson(tree, 'myapp/package.json');
expect(packageJson.name).toBe('@proj/myapp');
expect(packageJson.nx).toBeUndefined();
// Make sure keys are in idiomatic order // Make sure keys are in idiomatic order
expect(Object.keys(readJson(tree, 'myapp/package.json'))) expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
.toMatchInlineSnapshot(`
[ [
"name", "name",
"version", "version",
"private", "private",
"nx",
"dependencies", "dependencies",
] ]
`); `);
@ -1086,6 +1086,29 @@ describe('app (legacy)', () => {
} }
`); `);
}); });
it('should respect the provided name', async () => {
await applicationGenerator(tree, {
...schema,
addPlugin: true,
directory: 'myapp',
name: 'myapp',
});
const packageJson = readJson(tree, 'myapp/package.json');
expect(packageJson.name).toBe('@proj/myapp');
expect(packageJson.nx.name).toBe('myapp');
// Make sure keys are in idiomatic order
expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
[
"name",
"version",
"private",
"nx",
"dependencies",
]
`);
});
}); });
}); });

View File

@ -54,8 +54,8 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
js: options.js, js: options.js,
skipPackageJson: options.skipPackageJson, skipPackageJson: options.skipPackageJson,
skipFormat: true, skipFormat: true,
addTsPlugin: schema.useTsSolution, addTsPlugin: options.isTsSolutionSetup,
formatter: schema.formatter, formatter: options.formatter,
platform: 'web', platform: 'web',
}); });
tasks.push(jsInitTask); tasks.push(jsInitTask);
@ -70,6 +70,12 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
addProject(host, options); addProject(host, options);
// 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.isTsSolutionSetup) {
addProjectToTsSolutionWorkspace(host, options.appProjectRoot);
}
const e2eTask = await addE2e(host, options); const e2eTask = await addE2e(host, options);
tasks.push(e2eTask); tasks.push(e2eTask);
@ -145,12 +151,6 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
options.src ? 'src' : '.' options.src ? 'src' : '.'
); );
// 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.useTsSolution) {
addProjectToTsSolutionWorkspace(host, options.appProjectRoot);
}
sortPackageJsonFields(host, options.appProjectRoot); sortPackageJsonFields(host, options.appProjectRoot);
if (!options.skipFormat) { if (!options.skipFormat) {

View File

@ -54,8 +54,6 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName], implicitDependencies: [options.projectName],
}, },
} }
@ -135,8 +133,6 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName], implicitDependencies: [options.projectName],
}, },
} }

View File

@ -16,18 +16,20 @@ describe('updateEslint', () => {
beforeEach(async () => { beforeEach(async () => {
schema = { schema = {
projectName: 'my-app', projectName: 'my-app',
projectSimpleName: 'my-app',
appProjectRoot: 'my-app', appProjectRoot: 'my-app',
directory: 'my-app', directory: 'my-app',
importPath: '@proj/my-app',
linter: Linter.EsLint, linter: Linter.EsLint,
unitTestRunner: 'jest', unitTestRunner: 'jest',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectRoot: 'my-app-e2e', e2eProjectRoot: 'my-app-e2e',
outputPath: 'dist/my-app', outputPath: 'dist/my-app',
name: 'my-app',
parsedTags: [], parsedTags: [],
fileName: 'index', fileName: 'index',
e2eTestRunner: 'cypress', e2eTestRunner: 'cypress',
styledModule: null, styledModule: null,
isTsSolutionSetup: false,
}; };
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
const project: ProjectConfiguration = { const project: ProjectConfiguration = {

View File

@ -9,9 +9,9 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { nextVersion } from '../../../utils/versions'; import { nextVersion } from '../../../utils/versions';
import { reactDomVersion, reactVersion } from '@nx/react'; import { reactDomVersion, reactVersion } from '@nx/react';
import type { PackageJson } from 'nx/src/utils/package-json';
export function addProject(host: Tree, options: NormalizedSchema) { export function addProject(host: Tree, options: NormalizedSchema) {
const targets: Record<string, any> = {}; const targets: Record<string, any> = {};
@ -73,8 +73,8 @@ export function addProject(host: Tree, options: NormalizedSchema) {
}; };
if (isUsingTsSolutionSetup(host)) { if (isUsingTsSolutionSetup(host)) {
writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), { const packageJson: PackageJson = {
name: getImportPath(host, options.name), name: options.importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
dependencies: { dependencies: {
@ -82,13 +82,21 @@ export function addProject(host: Tree, options: NormalizedSchema) {
react: reactVersion, react: reactVersion,
'react-dom': reactDomVersion, 'react-dom': reactDomVersion,
}, },
nx: { };
name: options.name,
projectType: 'application', if (options.projectName !== options.importPath) {
sourceRoot: options.appProjectRoot, packageJson.nx = { name: options.projectName };
tags: options.parsedTags?.length ? options.parsedTags : undefined, }
}, if (options.parsedTags?.length) {
}); packageJson.nx ??= {};
packageJson.nx.tags = options.parsedTags;
}
writeJson(
host,
joinPathFragments(options.appProjectRoot, 'package.json'),
packageJson
);
} else { } else {
addProjectConfiguration(host, options.projectName, { addProjectConfiguration(host, options.projectName, {
...project, ...project,

View File

@ -41,7 +41,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
: ''; : '';
const templateVariables = { const templateVariables = {
...names(options.name), ...names(options.projectSimpleName),
...options, ...options,
dot: '.', dot: '.',
tmpl: '', tmpl: '',

View File

@ -1,21 +1,26 @@
import { joinPathFragments, names, readNxJson, Tree } from '@nx/devkit'; import { joinPathFragments, names, readNxJson, Tree } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Linter } from '@nx/eslint'; import { Linter } from '@nx/eslint';
import { assertValidStyle } from '@nx/react/src/utils/assertion'; import { assertValidStyle } from '@nx/react/src/utils/assertion';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema
extends Omit<Schema, 'name' | 'useTsSolution'> {
projectName: string; projectName: string;
projectSimpleName: string;
appProjectRoot: string; appProjectRoot: string;
importPath: string;
outputPath: string; outputPath: string;
e2eProjectName: string; e2eProjectName: string;
e2eProjectRoot: string; e2eProjectRoot: string;
parsedTags: string[]; parsedTags: string[];
fileName: string; fileName: string;
styledModule: null | string; styledModule: null | string;
isTsSolutionSetup: boolean;
js?: boolean; js?: boolean;
} }
@ -23,9 +28,13 @@ export async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
await ensureProjectName(host, options, 'application'); await ensureRootProjectName(options, 'application');
const { projectName: appProjectName, projectRoot: appProjectRoot } = const {
await determineProjectNameAndRootOptions(host, { projectName,
names: projectNames,
projectRoot: appProjectRoot,
importPath,
} = await determineProjectNameAndRootOptions(host, {
name: options.name, name: options.name,
projectType: 'application', projectType: 'application',
directory: options.directory, directory: options.directory,
@ -40,15 +49,18 @@ export async function normalizeOptions(
options.addPlugin ??= addPlugin; options.addPlugin ??= addPlugin;
const isTsSolutionSetup =
options.useTsSolution || isUsingTsSolutionSetup(host);
const appProjectName =
!isTsSolutionSetup || options.name ? projectName : importPath;
const e2eProjectName = options.rootProject ? 'e2e' : `${appProjectName}-e2e`; const e2eProjectName = options.rootProject ? 'e2e' : `${appProjectName}-e2e`;
const e2eProjectRoot = options.rootProject ? 'e2e' : `${appProjectRoot}-e2e`; const e2eProjectRoot = options.rootProject ? 'e2e' : `${appProjectRoot}-e2e`;
const name = names(options.name).fileName;
const outputPath = joinPathFragments( const outputPath = joinPathFragments(
'dist', 'dist',
appProjectRoot, appProjectRoot,
...(options.rootProject ? [name] : []) ...(options.rootProject ? [projectNames.projectFileName] : [])
); );
const parsedTags = options.tags const parsedTags = options.tags
@ -76,12 +88,14 @@ export async function normalizeOptions(
e2eTestRunner: options.e2eTestRunner || 'playwright', e2eTestRunner: options.e2eTestRunner || 'playwright',
fileName, fileName,
linter: options.linter || Linter.EsLint, linter: options.linter || Linter.EsLint,
name,
outputPath, outputPath,
parsedTags, parsedTags,
projectName: appProjectName, projectName: appProjectName,
projectSimpleName: projectNames.projectSimpleName,
style: options.style || 'css', style: options.style || 'css',
styledModule, styledModule,
unitTestRunner: options.unitTestRunner || 'jest', unitTestRunner: options.unitTestRunner || 'jest',
importPath,
isTsSolutionSetup,
}; };
} }

View File

@ -1,7 +1,7 @@
import { readNxJson, Tree } from '@nx/devkit'; import { readNxJson, Tree } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
@ -16,7 +16,7 @@ export async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
await ensureProjectName(host, options, 'library'); await ensureRootProjectName(options, 'library');
const { projectRoot, importPath } = await determineProjectNameAndRootOptions( const { projectRoot, importPath } = await determineProjectNameAndRootOptions(
host, host,
{ {

View File

@ -256,5 +256,45 @@ describe('next library', () => {
} }
`); `);
}); });
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', // import path contains the npm scope, so it would be different
linter: 'none',
unitTestRunner: 'none',
style: 'css',
skipFormat: true,
});
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',
style: 'css',
skipFormat: true,
});
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',
style: 'css',
skipFormat: true,
});
expect(readJson(tree, 'mylib/package.json').nx).toBeUndefined();
});
}); });
}); });

View File

@ -599,9 +599,11 @@ describe('app', () => {
}, },
] ]
`); `);
const packageJson = readJson(tree, 'myapp/package.json');
expect(packageJson.name).toBe('@proj/myapp');
expect(packageJson.nx.name).toBeUndefined();
// Make sure keys are in idiomatic order // Make sure keys are in idiomatic order
expect(Object.keys(readJson(tree, 'myapp/package.json'))) expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
.toMatchInlineSnapshot(`
[ [
"name", "name",
"version", "version",
@ -613,17 +615,14 @@ describe('app', () => {
{ {
"name": "@proj/myapp", "name": "@proj/myapp",
"nx": { "nx": {
"name": "myapp",
"projectType": "application",
"sourceRoot": "myapp/src",
"targets": { "targets": {
"serve": { "serve": {
"configurations": { "configurations": {
"development": { "development": {
"buildTarget": "myapp:build:development", "buildTarget": "@proj/myapp:build:development",
}, },
"production": { "production": {
"buildTarget": "myapp:build:production", "buildTarget": "@proj/myapp:build:production",
}, },
}, },
"defaultConfiguration": "development", "defaultConfiguration": "development",
@ -632,7 +631,7 @@ describe('app', () => {
], ],
"executor": "@nx/js:node", "executor": "@nx/js:node",
"options": { "options": {
"buildTarget": "myapp:build", "buildTarget": "@proj/myapp:build",
"runBuildTargetDependencies": false, "runBuildTargetDependencies": false,
}, },
}, },
@ -717,6 +716,29 @@ describe('app', () => {
`); `);
}); });
it('should respect the provided name', async () => {
await applicationGenerator(tree, {
directory: 'myapp',
name: 'myapp',
bundler: 'webpack',
unitTestRunner: 'jest',
addPlugin: true,
});
const packageJson = readJson(tree, 'myapp/package.json');
expect(packageJson.name).toBe('@proj/myapp');
expect(packageJson.nx.name).toBe('myapp');
// Make sure keys are in idiomatic order
expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
[
"name",
"version",
"private",
"nx",
]
`);
});
it('should use @swc/jest for jest', async () => { it('should use @swc/jest for jest', async () => {
await applicationGenerator(tree, { await applicationGenerator(tree, {
directory: 'apps/my-app', directory: 'apps/my-app',
@ -737,7 +759,7 @@ describe('app', () => {
swcJestConfig.swcrc = false; swcJestConfig.swcrc = false;
export default { export default {
displayName: 'my-app', displayName: '@proj/my-app',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
testEnvironment: 'node', testEnvironment: 'node',
transform: { transform: {
@ -819,7 +841,7 @@ describe('app', () => {
}); });
expect( expect(
readProjectConfiguration(tree, 'my-app').targets.build.options readProjectConfiguration(tree, '@proj/my-app').targets.build.options
.outputPath .outputPath
).toBe('apps/my-app/dist'); ).toBe('apps/my-app/dist');
}); });
@ -833,7 +855,7 @@ describe('app', () => {
}); });
expect( expect(
readProjectConfiguration(tree, 'my-app').targets.build.options readProjectConfiguration(tree, '@proj/my-app').targets.build.options
.outputPath .outputPath
).toBe('apps/my-app/dist'); ).toBe('apps/my-app/dist');
}); });

View File

@ -1,4 +1,3 @@
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { import {
addDependenciesToPackageJson, addDependenciesToPackageJson,
addProjectConfiguration, addProjectConfiguration,
@ -24,7 +23,7 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { configurationGenerator } from '@nx/jest'; import { configurationGenerator } from '@nx/jest';
import { import {
@ -62,10 +61,11 @@ import {
} from '@nx/js/src/utils/typescript/ts-solution-setup'; } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { sortPackageJsonFields } from '@nx/js/src/utils/package-json/sort-fields'; import { sortPackageJsonFields } from '@nx/js/src/utils/package-json/sort-fields';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Omit<Schema, 'useTsSolution'> {
appProjectRoot: string; appProjectRoot: string;
parsedTags: string[]; parsedTags: string[];
outputPath: string; outputPath: string;
importPath: string;
isUsingTsSolutionConfig: boolean; isUsingTsSolutionConfig: boolean;
} }
@ -208,13 +208,11 @@ function addProject(tree: Tree, options: NormalizedSchema) {
if (options.isUsingTsSolutionConfig) { if (options.isUsingTsSolutionConfig) {
writeJson(tree, joinPathFragments(options.appProjectRoot, 'package.json'), { writeJson(tree, joinPathFragments(options.appProjectRoot, 'package.json'), {
name: getImportPath(tree, options.name), name: options.importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
name: options.name, name: options.name !== options.importPath ? options.name : undefined,
projectType: 'application',
sourceRoot: project.sourceRoot,
targets: project.targets, targets: project.targets,
tags: project.tags?.length ? project.tags : undefined, tags: project.tags?.length ? project.tags : undefined,
}, },
@ -515,6 +513,12 @@ export async function applicationGeneratorInternal(tree: Tree, schema: Schema) {
addAppFiles(tree, options); addAppFiles(tree, options);
addProject(tree, options); addProject(tree, options);
// 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) {
addProjectToTsSolutionWorkspace(tree, options.appProjectRoot);
}
updateTsConfigOptions(tree, options); updateTsConfigOptions(tree, options);
if (options.linter === Linter.EsLint) { if (options.linter === Linter.EsLint) {
@ -595,12 +599,6 @@ export async function applicationGeneratorInternal(tree: Tree, schema: 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) {
addProjectToTsSolutionWorkspace(tree, options.appProjectRoot);
}
sortPackageJsonFields(tree, options.appProjectRoot); sortPackageJsonFields(tree, options.appProjectRoot);
if (!options.skipFormat) { if (!options.skipFormat) {
@ -618,9 +616,12 @@ async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
await ensureProjectName(host, options, 'application'); await ensureRootProjectName(options, 'application');
const { projectName: appProjectName, projectRoot: appProjectRoot } = const {
await determineProjectNameAndRootOptions(host, { projectName,
projectRoot: appProjectRoot,
importPath,
} = await determineProjectNameAndRootOptions(host, {
name: options.name, name: options.name,
projectType: 'application', projectType: 'application',
directory: options.directory, directory: options.directory,
@ -643,6 +644,9 @@ async function normalizeOptions(
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host); const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
const swcJest = options.swcJest ?? isUsingTsSolutionConfig; const swcJest = options.swcJest ?? isUsingTsSolutionConfig;
const appProjectName =
!isUsingTsSolutionConfig || options.name ? projectName : importPath;
return { return {
addPlugin, addPlugin,
...options, ...options,
@ -651,6 +655,7 @@ async function normalizeOptions(
? names(options.frontendProject).fileName ? names(options.frontendProject).fileName
: undefined, : undefined,
appProjectRoot, appProjectRoot,
importPath,
parsedTags, parsedTags,
linter: options.linter ?? Linter.EsLint, linter: options.linter ?? Linter.EsLint,
unitTestRunner: options.unitTestRunner ?? 'jest', unitTestRunner: options.unitTestRunner ?? 'jest',
@ -660,7 +665,7 @@ async function normalizeOptions(
? joinPathFragments(appProjectRoot, 'dist') ? joinPathFragments(appProjectRoot, 'dist')
: joinPathFragments( : joinPathFragments(
'dist', 'dist',
options.rootProject ? options.name : appProjectRoot options.rootProject ? appProjectName : appProjectRoot
), ),
isUsingTsSolutionConfig, isUsingTsSolutionConfig,
swcJest, swcJest,

View File

@ -176,7 +176,7 @@ describe('e2eProjectGenerator', () => {
}); });
await e2eProjectGenerator(tree, { await e2eProjectGenerator(tree, {
projectType: 'server', projectType: 'server',
project: 'api', project: '@proj/api',
addPlugin: true, addPlugin: true,
}); });
@ -217,7 +217,7 @@ describe('e2eProjectGenerator', () => {
}); });
await e2eProjectGenerator(tree, { await e2eProjectGenerator(tree, {
projectType: 'server', projectType: 'server',
project: 'api', project: '@proj/api',
addPlugin: true, addPlugin: true,
}); });
@ -285,7 +285,7 @@ describe('e2eProjectGenerator', () => {
}); });
await e2eProjectGenerator(tree, { await e2eProjectGenerator(tree, {
projectType: 'cli', projectType: 'cli',
project: 'cli', project: '@proj/cli',
addPlugin: true, addPlugin: true,
}); });

View File

@ -34,7 +34,6 @@ import {
addProjectToTsSolutionWorkspace, addProjectToTsSolutionWorkspace,
isUsingTsSolutionSetup, isUsingTsSolutionSetup,
} from '@nx/js/src/utils/typescript/ts-solution-setup'; } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { relative } from 'node:path/posix'; import { relative } from 'node:path/posix';
import { addSwcTestConfig } from '@nx/js/src/utils/swc/add-swc-config'; import { addSwcTestConfig } from '@nx/js/src/utils/swc/add-swc-config';
@ -57,17 +56,19 @@ export async function e2eProjectGeneratorInternal(
// TODO(@ndcunningham): This is broken.. the outputs are wrong.. and this isn't using the jest generator // TODO(@ndcunningham): This is broken.. the outputs are wrong.. and this isn't using the jest generator
if (isUsingTsSolutionConfig) { if (isUsingTsSolutionConfig) {
writeJson(host, joinPathFragments(options.e2eProjectRoot, 'package.json'), { writeJson(host, joinPathFragments(options.e2eProjectRoot, 'package.json'), {
name: getImportPath(host, options.e2eProjectName), name: options.importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
name: options.e2eProjectName, name:
projectType: 'application', options.e2eProjectName !== options.importPath
? options.e2eProjectName
: undefined,
implicitDependencies: [options.project], implicitDependencies: [options.project],
targets: { targets: {
e2e: { e2e: {
executor: '@nx/jest:jest', executor: '@nx/jest:jest',
outputs: ['{workspaceRoot}/coverage/{e2eProjectRoot}'], outputs: ['{projectRoot}/test-output/jest/coverage'],
options: { options: {
jestConfig: `${options.e2eProjectRoot}/jest.config.ts`, jestConfig: `${options.e2eProjectRoot}/jest.config.ts`,
passWithNoTests: true, passWithNoTests: true,
@ -131,6 +132,7 @@ export async function e2eProjectGeneratorInternal(
const coverageDirectory = isUsingTsSolutionConfig const coverageDirectory = isUsingTsSolutionConfig
? 'test-output/jest/coverage' ? 'test-output/jest/coverage'
: joinPathFragments(rootOffset, 'coverage', options.e2eProjectName); : joinPathFragments(rootOffset, 'coverage', options.e2eProjectName);
const projectSimpleName = options.project.split('/').pop();
if (options.projectType === 'server') { if (options.projectType === 'server') {
generateFiles( generateFiles(
host, host,
@ -138,7 +140,7 @@ export async function e2eProjectGeneratorInternal(
options.e2eProjectRoot, options.e2eProjectRoot,
{ {
...options, ...options,
...names(options.rootProject ? 'server' : options.project), ...names(options.rootProject ? 'server' : projectSimpleName),
tsConfigFile, tsConfigFile,
offsetFromRoot: rootOffset, offsetFromRoot: rootOffset,
jestPreset, jestPreset,
@ -155,7 +157,7 @@ export async function e2eProjectGeneratorInternal(
options.e2eProjectRoot, options.e2eProjectRoot,
{ {
...options, ...options,
...names(options.rootProject ? 'server' : options.project), ...names(options.rootProject ? 'server' : projectSimpleName),
tsConfigFile, tsConfigFile,
offsetFromRoot: rootOffset, offsetFromRoot: rootOffset,
tmpl: '', tmpl: '',
@ -170,7 +172,7 @@ export async function e2eProjectGeneratorInternal(
options.e2eProjectRoot, options.e2eProjectRoot,
{ {
...options, ...options,
...names(options.rootProject ? 'cli' : options.project), ...names(options.rootProject ? 'cli' : projectSimpleName),
mainFile, mainFile,
tsConfigFile, tsConfigFile,
offsetFromRoot: rootOffset, offsetFromRoot: rootOffset,
@ -275,14 +277,25 @@ async function normalizeOptions(
tree: Tree, tree: Tree,
options: Schema options: Schema
): Promise< ): Promise<
Omit<Schema, 'name'> & { e2eProjectRoot: string; e2eProjectName: string } Omit<Schema, 'name'> & {
e2eProjectRoot: string;
e2eProjectName: string;
importPath: string;
}
> { > {
options.directory = options.directory ?? `${options.project}-e2e`; let directory = options.rootProject ? 'e2e' : options.directory;
const { projectName: e2eProjectName, projectRoot: e2eProjectRoot } = if (!directory) {
await determineProjectNameAndRootOptions(tree, { const projectConfig = readProjectConfiguration(tree, options.project);
directory = `${projectConfig.root}-e2e`;
}
const {
projectName: e2eProjectName,
projectRoot: e2eProjectRoot,
importPath,
} = await determineProjectNameAndRootOptions(tree, {
name: options.name, name: options.name,
projectType: 'library', projectType: 'application',
directory: options.rootProject ? 'e2e' : options.directory, directory,
}); });
const nxJson = readNxJson(tree); const nxJson = readNxJson(tree);
@ -295,6 +308,7 @@ async function normalizeOptions(
...options, ...options,
e2eProjectRoot, e2eProjectRoot,
e2eProjectName, e2eProjectName,
importPath,
port: options.port ?? 3000, port: options.port ?? 3000,
rootProject: !!options.rootProject, rootProject: !!options.rootProject,
}; };

View File

@ -519,6 +519,7 @@ describe('lib', () => {
expect(tree.exists('my-dir/my-lib/src/lib/my-lib.spec.js')).toBeTruthy(); expect(tree.exists('my-dir/my-lib/src/lib/my-lib.spec.js')).toBeTruthy();
}); });
}); });
describe('TS solution setup', () => { describe('TS solution setup', () => {
beforeEach(() => { beforeEach(() => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
@ -700,5 +701,45 @@ describe('lib', () => {
} }
`); `);
}); });
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,
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,
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,
skipFormat: true,
} as Schema);
expect(readJson(tree, 'mylib/package.json').nx).toBeUndefined();
});
}); });
}); });

View File

@ -19,7 +19,7 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { libraryGenerator as jsLibraryGenerator } from '@nx/js'; import { libraryGenerator as jsLibraryGenerator } from '@nx/js';
import { addSwcConfig } from '@nx/js/src/utils/swc/add-swc-config'; import { addSwcConfig } from '@nx/js/src/utils/swc/add-swc-config';
@ -33,7 +33,6 @@ import {
addProjectToTsSolutionWorkspace, addProjectToTsSolutionWorkspace,
isUsingTsSolutionSetup, isUsingTsSolutionSetup,
} from '@nx/js/src/utils/typescript/ts-solution-setup'; } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { sortPackageJsonFields } from '@nx/js/src/utils/package-json/sort-fields'; import { sortPackageJsonFields } from '@nx/js/src/utils/package-json/sort-fields';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
@ -76,7 +75,7 @@ export async function libraryGeneratorInternal(tree: Tree, schema: Schema) {
options.buildable options.buildable
) { ) {
writeJson(tree, joinPathFragments(options.projectRoot, 'package.json'), { writeJson(tree, joinPathFragments(options.projectRoot, 'package.json'), {
name: getImportPath(tree, options.name), name: options.importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined, files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined,
@ -134,7 +133,7 @@ async function normalizeOptions(
tree: Tree, tree: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
await ensureProjectName(tree, options, 'library'); await ensureRootProjectName(options, 'library');
const { const {
projectName, projectName,
names: projectNames, names: projectNames,
@ -168,9 +167,8 @@ async function normalizeOptions(
return { return {
...options, ...options,
fileName, fileName,
projectName: isUsingTsSolutionConfig projectName:
? getImportPath(tree, projectName) isUsingTsSolutionConfig && !options.name ? importPath : projectName,
: projectName,
projectRoot, projectRoot,
parsedTags, parsedTags,
importPath, importPath,

View File

@ -255,14 +255,15 @@ describe('app', () => {
}, },
] ]
`); `);
const packageJson = readJson(tree, 'myapp/package.json');
expect(packageJson.name).toBe('@proj/myapp');
expect(packageJson.nx).toBeUndefined();
// Make sure keys are in idiomatic order // Make sure keys are in idiomatic order
expect(Object.keys(readJson(tree, 'myapp/package.json'))) expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
.toMatchInlineSnapshot(`
[ [
"name", "name",
"version", "version",
"private", "private",
"nx",
] ]
`); `);
expect(readJson(tree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(` expect(readJson(tree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(`
@ -386,5 +387,28 @@ describe('app', () => {
} }
`); `);
}); });
it('should respect the provided name', async () => {
await applicationGenerator(tree, {
directory: 'myapp',
name: 'myapp',
e2eTestRunner: 'playwright',
unitTestRunner: 'vitest',
linter: 'eslint',
});
const packageJson = readJson(tree, 'myapp/package.json');
expect(packageJson.name).toBe('@proj/myapp');
expect(packageJson.nx.name).toBe('myapp');
// Make sure keys are in idiomatic order
expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
[
"name",
"version",
"private",
"nx",
]
`);
});
}); });
}); });

View File

@ -32,12 +32,12 @@ import {
getNxCloudAppOnBoardingUrl, getNxCloudAppOnBoardingUrl,
createNxCloudOnboardingURLForWelcomeApp, createNxCloudOnboardingURLForWelcomeApp,
} from 'nx/src/nx-cloud/utilities/onboarding'; } from 'nx/src/nx-cloud/utilities/onboarding';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { import {
addProjectToTsSolutionWorkspace, addProjectToTsSolutionWorkspace,
updateTsconfigFiles, updateTsconfigFiles,
} from '@nx/js/src/utils/typescript/ts-solution-setup'; } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { sortPackageJsonFields } from '@nx/js/src/utils/package-json/sort-fields'; import { sortPackageJsonFields } from '@nx/js/src/utils/package-json/sort-fields';
import type { PackageJson } from 'nx/src/utils/package-json';
export async function applicationGenerator(tree: Tree, schema: Schema) { export async function applicationGenerator(tree: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
@ -67,17 +67,25 @@ export async function applicationGenerator(tree: Tree, schema: Schema) {
tasks.push(ensureDependencies(tree, options)); tasks.push(ensureDependencies(tree, options));
if (options.isUsingTsSolutionConfig) { if (options.isUsingTsSolutionConfig) {
writeJson(tree, joinPathFragments(options.appProjectRoot, 'package.json'), { const packageJson: PackageJson = {
name: getImportPath(tree, options.name), name: options.importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { };
name: options.name,
projectType: 'application', if (options.projectName !== options.importPath) {
sourceRoot: `${options.appProjectRoot}/src`, packageJson.nx = { name: options.projectName };
tags: options.parsedTags?.length ? options.parsedTags : undefined, }
}, if (options.parsedTags?.length) {
}); packageJson.nx ??= {};
packageJson.nx.tags = options.parsedTags;
}
writeJson(
tree,
joinPathFragments(options.appProjectRoot, 'package.json'),
packageJson
);
} else { } else {
addProjectConfiguration(tree, options.projectName, { addProjectConfiguration(tree, options.projectName, {
root: options.appProjectRoot, root: options.appProjectRoot,
@ -140,6 +148,12 @@ export async function applicationGenerator(tree: Tree, schema: Schema) {
updateGitIgnore(tree); updateGitIgnore(tree);
// 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) {
addProjectToTsSolutionWorkspace(tree, options.appProjectRoot);
}
tasks.push( tasks.push(
await addLinting(tree, { await addLinting(tree, {
projectName: options.projectName, projectName: options.projectName,
@ -193,12 +207,6 @@ export async function applicationGenerator(tree: Tree, schema: 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) {
addProjectToTsSolutionWorkspace(tree, options.appProjectRoot);
}
sortPackageJsonFields(tree, options.appProjectRoot); sortPackageJsonFields(tree, options.appProjectRoot);
if (!options.skipFormat) await formatFiles(tree); if (!options.skipFormat) await formatFiles(tree);

View File

@ -34,8 +34,6 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName], implicitDependencies: [options.projectName],
}, },
} }
@ -106,8 +104,6 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName], implicitDependencies: [options.projectName],
}, },
} }

View File

@ -1,7 +1,7 @@
import { names, Tree } from '@nx/devkit'; import { names, Tree } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { NormalizedSchema, Schema } from '../schema'; import { NormalizedSchema, Schema } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
@ -10,9 +10,13 @@ export async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
await ensureProjectName(host, options, 'application'); await ensureRootProjectName(options, 'application');
const { projectName: appProjectName, projectRoot: appProjectRoot } = const {
await determineProjectNameAndRootOptions(host, { projectName,
names: projectNames,
projectRoot: appProjectRoot,
importPath,
} = await determineProjectNameAndRootOptions(host, {
name: options.name, name: options.name,
projectType: 'application', projectType: 'application',
directory: options.directory, directory: options.directory,
@ -20,6 +24,10 @@ export async function normalizeOptions(
}); });
options.rootProject = appProjectRoot === '.'; options.rootProject = appProjectRoot === '.';
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
const appProjectName =
!isUsingTsSolutionConfig || options.name ? projectName : importPath;
const e2eProjectName = options.rootProject ? 'e2e' : `${appProjectName}-e2e`; const e2eProjectName = options.rootProject ? 'e2e' : `${appProjectName}-e2e`;
const e2eProjectRoot = options.rootProject ? 'e2e' : `${appProjectRoot}-e2e`; const e2eProjectRoot = options.rootProject ? 'e2e' : `${appProjectRoot}-e2e`;
@ -29,14 +37,15 @@ export async function normalizeOptions(
const normalized = { const normalized = {
...options, ...options,
name: names(options.name).fileName, name: projectNames.projectFileName,
projectName: appProjectName, projectName: appProjectName,
appProjectRoot, appProjectRoot,
importPath,
e2eProjectName, e2eProjectName,
e2eProjectRoot, e2eProjectRoot,
parsedTags, parsedTags,
style: options.style ?? 'none', style: options.style ?? 'none',
isUsingTsSolutionConfig: isUsingTsSolutionSetup(host), isUsingTsSolutionConfig,
} as NormalizedSchema; } as NormalizedSchema;
normalized.unitTestRunner ??= 'vitest'; normalized.unitTestRunner ??= 'vitest';

View File

@ -18,9 +18,10 @@ export interface Schema {
useTsSolution?: boolean; useTsSolution?: boolean;
} }
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Omit<Schema, 'useTsSolution'> {
projectName: string; projectName: string;
appProjectRoot: string; appProjectRoot: string;
importPath: string;
e2eProjectName: string; e2eProjectName: string;
e2eProjectRoot: string; e2eProjectRoot: string;
parsedTags: string[]; parsedTags: string[];

View File

@ -65,7 +65,6 @@ describe('Workspaces', () => {
}, },
"name": "my-package", "name": "my-package",
"root": "packages/my-package", "root": "packages/my-package",
"sourceRoot": "packages/my-package",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],

View File

@ -69,7 +69,6 @@ describe('nx package.json workspaces plugin', () => {
}, },
"name": "root", "name": "root",
"root": ".", "root": ".",
"sourceRoot": ".",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],
@ -123,7 +122,6 @@ describe('nx package.json workspaces plugin', () => {
}, },
"name": "lib-a", "name": "lib-a",
"root": "packages/lib-a", "root": "packages/lib-a",
"sourceRoot": "packages/lib-a",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],
@ -185,7 +183,6 @@ describe('nx package.json workspaces plugin', () => {
}, },
"name": "lib-b", "name": "lib-b",
"root": "packages/lib-b", "root": "packages/lib-b",
"sourceRoot": "packages/lib-b",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],
@ -290,7 +287,6 @@ describe('nx package.json workspaces plugin', () => {
}, },
"name": "vite", "name": "vite",
"root": "packages/vite", "root": "packages/vite",
"sourceRoot": "packages/vite",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],
@ -394,7 +390,6 @@ describe('nx package.json workspaces plugin', () => {
}, },
"name": "vite", "name": "vite",
"root": "packages/vite", "root": "packages/vite",
"sourceRoot": "packages/vite",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],
@ -494,7 +489,6 @@ describe('nx package.json workspaces plugin', () => {
}, },
"name": "vite", "name": "vite",
"root": "packages/vite", "root": "packages/vite",
"sourceRoot": "packages/vite",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],
@ -582,7 +576,6 @@ describe('nx package.json workspaces plugin', () => {
}, },
"name": "root", "name": "root",
"root": "packages/a", "root": "packages/a",
"sourceRoot": "packages/a",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],
@ -667,7 +660,6 @@ describe('nx package.json workspaces plugin', () => {
}, },
"name": "root", "name": "root",
"root": "packages/a", "root": "packages/a",
"sourceRoot": "packages/a",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],
@ -753,7 +745,6 @@ describe('nx package.json workspaces plugin', () => {
}, },
"name": "root", "name": "root",
"root": "packages/a", "root": "packages/a",
"sourceRoot": "packages/a",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],
@ -939,7 +930,6 @@ describe('nx package.json workspaces plugin', () => {
}, },
"name": "lib-a", "name": "lib-a",
"root": "packages/lib-a", "root": "packages/lib-a",
"sourceRoot": "packages/lib-a",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],
@ -990,7 +980,6 @@ describe('nx package.json workspaces plugin', () => {
}, },
"name": "lib-b", "name": "lib-b",
"root": "libs/lib-b", "root": "libs/lib-b",
"sourceRoot": "libs/lib-b",
"tags": [ "tags": [
"npm:public", "npm:public",
], ],

View File

@ -143,6 +143,9 @@ export function createNodeFromPackageJson(
...json, ...json,
root: projectRoot, root: projectRoot,
isInPackageManagerWorkspaces, isInPackageManagerWorkspaces,
// change this to bust the cache when making changes that result in different
// results for the same hash
bust: 1,
}); });
const cached = cache[hash]; const cached = cache[hash];
@ -209,7 +212,6 @@ export function buildProjectConfigurationFromPackageJson(
const projectConfiguration: ProjectConfiguration & { name: string } = { const projectConfiguration: ProjectConfiguration & { name: string } = {
root: projectRoot, root: projectRoot,
sourceRoot: projectRoot,
name, name,
...packageJson.nx, ...packageJson.nx,
targets: readTargetsFromPackageJson(packageJson, nxJson), targets: readTargetsFromPackageJson(packageJson, nxJson),

View File

@ -179,10 +179,10 @@ export async function configurationGeneratorInternal(
name: importPath, name: importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: {
name: options.project,
},
}; };
if (options.project !== importPath) {
packageJson.nx = { name: options.project };
}
writeJson(tree, packageJsonPath, packageJson); writeJson(tree, packageJsonPath, packageJson);
} }

View File

@ -17,10 +17,7 @@ import {
type ProjectConfiguration, type ProjectConfiguration,
type Tree, type Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import { import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
determineProjectNameAndRootOptions,
resolveImportPath,
} from '@nx/devkit/src/generators/project-name-and-root-utils';
import { LinterType, lintProjectGenerator } from '@nx/eslint'; import { LinterType, lintProjectGenerator } from '@nx/eslint';
import { addPropertyToJestConfig, configurationGenerator } from '@nx/jest'; import { addPropertyToJestConfig, configurationGenerator } from '@nx/jest';
import { getRelativePathToRootTsConfig } from '@nx/js'; import { getRelativePathToRootTsConfig } from '@nx/js';
@ -106,12 +103,14 @@ function addFiles(host: Tree, options: NormalizedSchema) {
join(projectConfiguration.root, 'package.json') join(projectConfiguration.root, 'package.json')
); );
const simplePluginName = options.pluginName.split('/').pop();
generateFiles(host, join(__dirname, './files'), options.projectRoot, { generateFiles(host, join(__dirname, './files'), options.projectRoot, {
...options, ...options,
tmpl: '', tmpl: '',
rootTsConfigPath: getRelativePathToRootTsConfig(host, options.projectRoot), rootTsConfigPath: getRelativePathToRootTsConfig(host, options.projectRoot),
packageManagerCommands: getPackageManagerCommand(), packageManagerCommands: getPackageManagerCommand(),
pluginPackageName, pluginPackageName,
simplePluginName,
}); });
} }
@ -129,7 +128,7 @@ async function addJest(host: Tree, options: NormalizedSchema) {
host, host,
joinPathFragments(options.projectRoot, 'package.json'), joinPathFragments(options.projectRoot, 'package.json'),
{ {
name: resolveImportPath(host, options.projectName, options.projectRoot), name: options.projectName,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
} }

View File

@ -354,6 +354,7 @@ describe('NxPlugin Plugin Generator', () => {
getSchema({ getSchema({
directory: 'my-plugin', directory: 'my-plugin',
unitTestRunner: 'jest', unitTestRunner: 'jest',
useProjectJson: false,
}) })
); );
@ -530,5 +531,50 @@ describe('NxPlugin Plugin Generator', () => {
} }
`); `);
}); });
it('should set "nx.name" in package.json when the user provides a name that is different than the package name and "useProjectJson" is "false"', async () => {
await pluginGenerator(tree, {
directory: 'my-plugin',
name: 'my-plugin', // import path contains the npm scope, so it would be different
useProjectJson: false,
skipFormat: true,
});
expect(readJson(tree, 'my-plugin/package.json').nx.name).toBe(
'my-plugin'
);
});
it('should not set "nx.name" in package.json when the provided name matches the package name', async () => {
await pluginGenerator(tree, {
directory: 'my-plugin',
name: '@proj/my-plugin',
useProjectJson: false,
skipFormat: true,
});
expect(readJson(tree, 'my-plugin/package.json').nx.name).toBeUndefined();
});
it('should not set "nx" in package.json when "useProjectJson" is "true"', async () => {
await pluginGenerator(tree, {
directory: 'my-plugin',
name: '@proj/my-plugin',
useProjectJson: true,
skipFormat: true,
});
expect(readJson(tree, 'my-plugin/package.json').nx).toBeUndefined();
});
it('should not set "nx.name" in package.json when the user does not provide a name', async () => {
await pluginGenerator(tree, {
directory: 'my-plugin',
useProjectJson: false,
skipFormat: true,
});
expect(readJson(tree, 'my-plugin/package.json').nx.name).toBeUndefined();
});
}); });
}); });

View File

@ -94,7 +94,7 @@ export async function pluginGeneratorInternal(host: Tree, schema: Schema) {
config: 'project', config: 'project',
bundler: options.bundler, bundler: options.bundler,
publishable: options.publishable, publishable: options.publishable,
importPath: options.npmPackageName, importPath: options.importPath,
linter: options.linter, linter: options.linter,
unitTestRunner: options.unitTestRunner, unitTestRunner: options.unitTestRunner,
useProjectJson: options.useProjectJson, useProjectJson: options.useProjectJson,
@ -149,9 +149,9 @@ export async function pluginGeneratorInternal(host: Tree, schema: Schema) {
projectDirectory: options.projectDirectory, projectDirectory: options.projectDirectory,
pluginOutputPath: joinPathFragments( pluginOutputPath: joinPathFragments(
'dist', 'dist',
options.rootProject ? options.name : options.projectRoot options.rootProject ? options.projectName : options.projectRoot
), ),
npmPackageName: options.npmPackageName, npmPackageName: options.importPath,
skipFormat: true, skipFormat: true,
rootProject: options.rootProject, rootProject: options.rootProject,
linter: options.linter, linter: options.linter,

View File

@ -1,7 +1,7 @@
import { readNxJson, type Tree } from '@nx/devkit'; import { readNxJson, type Tree } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { LinterType } from '@nx/eslint'; import type { LinterType } from '@nx/eslint';
import { import {
@ -10,16 +10,14 @@ import {
} from '@nx/js/src/utils/generator-prompts'; } from '@nx/js/src/utils/generator-prompts';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import type { Schema } from '../schema'; import type { Schema } from '../schema';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
name: string;
projectName: string; projectName: string;
fileName: string; fileName: string;
projectRoot: string; projectRoot: string;
projectDirectory: string; projectDirectory: string;
parsedTags: string[]; parsedTags: string[];
npmPackageName: string; importPath: string;
bundler: 'swc' | 'tsc'; bundler: 'swc' | 'tsc';
publishable: boolean; publishable: boolean;
unitTestRunner: 'jest' | 'vitest' | 'none'; unitTestRunner: 'jest' | 'vitest' | 'none';
@ -48,12 +46,9 @@ export async function normalizeOptions(
process.env.NX_ADD_PLUGINS !== 'false' && process.env.NX_ADD_PLUGINS !== 'false' &&
nxJson.useInferencePlugins !== false); nxJson.useInferencePlugins !== false);
await ensureProjectName(host, options, 'application'); await ensureRootProjectName(options, 'library');
const { const { projectName, projectRoot, importPath } =
projectName, await determineProjectNameAndRootOptions(host, {
projectRoot,
importPath: npmPackageName,
} = await determineProjectNameAndRootOptions(host, {
name: options.name, name: options.name,
projectType: 'library', projectType: 'library',
directory: options.directory, directory: options.directory,
@ -72,14 +67,11 @@ export async function normalizeOptions(
...options, ...options,
bundler: options.compiler ?? 'tsc', bundler: options.compiler ?? 'tsc',
fileName: projectName, fileName: projectName,
name: projectName, projectName: isTsSolutionSetup && !options.name ? importPath : projectName,
projectName: isTsSolutionSetup
? getImportPath(host, projectName)
: projectName,
projectRoot, projectRoot,
projectDirectory, projectDirectory,
parsedTags, parsedTags,
npmPackageName, importPath,
publishable: options.publishable ?? false, publishable: options.publishable ?? false,
linter, linter,
unitTestRunner, unitTestRunner,

View File

@ -253,8 +253,10 @@ describe('app', () => {
}); });
describe('TS solution setup', () => { describe('TS solution setup', () => {
it('should add project references when using TS solution', async () => { let tree: Tree;
const tree = createTreeWithEmptyWorkspace();
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
tree.write('.gitignore', ''); tree.write('.gitignore', '');
updateJson(tree, 'package.json', (json) => { updateJson(tree, 'package.json', (json) => {
json.workspaces = ['packages/*', 'apps/*']; json.workspaces = ['packages/*', 'apps/*'];
@ -271,7 +273,9 @@ describe('app', () => {
files: [], files: [],
references: [], references: [],
}); });
});
it('should add project references when using TS solution', async () => {
await reactNativeApplicationGenerator(tree, { await reactNativeApplicationGenerator(tree, {
directory: 'my-app', directory: 'my-app',
displayName: 'myApp', displayName: 'myApp',
@ -291,9 +295,11 @@ describe('app', () => {
}, },
] ]
`); `);
const packageJson = readJson(tree, 'my-app/package.json');
expect(packageJson.name).toBe('@proj/my-app');
expect(packageJson.nx.name).toBeUndefined();
// Make sure keys are in idiomatic order // Make sure keys are in idiomatic order
expect(Object.keys(readJson(tree, 'my-app/package.json'))) expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
.toMatchInlineSnapshot(`
[ [
"name", "name",
"version", "version",
@ -403,5 +409,33 @@ describe('app', () => {
} }
`); `);
}); });
it('should respect the provided name', async () => {
await reactNativeApplicationGenerator(tree, {
directory: 'my-app',
name: 'my-app',
displayName: 'myApp',
tags: 'one,two',
linter: Linter.EsLint,
e2eTestRunner: 'none',
install: false,
unitTestRunner: 'jest',
bundler: 'vite',
addPlugin: true,
});
const packageJson = readJson(tree, 'my-app/package.json');
expect(packageJson.name).toBe('@proj/my-app');
expect(packageJson.nx.name).toBe('my-app');
// Make sure keys are in idiomatic order
expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
[
"name",
"version",
"private",
"nx",
]
`);
});
}); });
}); });

View File

@ -66,6 +66,12 @@ export async function reactNativeApplicationGeneratorInternal(
await createApplicationFiles(host, options); await createApplicationFiles(host, options);
addProject(host, options); addProject(host, options);
// 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.isTsSolutionSetup) {
addProjectToTsSolutionWorkspace(host, options.appProjectRoot);
}
const lintTask = await addLinting(host, { const lintTask = await addLinting(host, {
...options, ...options,
projectRoot: options.appProjectRoot, projectRoot: options.appProjectRoot,
@ -149,12 +155,6 @@ export async function reactNativeApplicationGeneratorInternal(
: undefined : undefined
); );
// 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.useTsSolution) {
addProjectToTsSolutionWorkspace(host, options.appProjectRoot);
}
sortPackageJsonFields(host, options.appProjectRoot); sortPackageJsonFields(host, options.appProjectRoot);
if (!options.skipFormat) { if (!options.skipFormat) {

View File

@ -1,5 +1,5 @@
import { addE2e as addE2eReact } from '@nx/react/src/generators/application/lib/add-e2e'; import { addE2e as addE2eReact } from '@nx/react/src/generators/application/lib/add-e2e';
import { GeneratorCallback, Tree, ensurePackage } from '@nx/devkit'; import { GeneratorCallback, Tree, ensurePackage, names } from '@nx/devkit';
import { nxVersion } from '../../../utils/versions'; import { nxVersion } from '../../../utils/versions';
@ -18,6 +18,7 @@ export async function addE2e(
styledModule: null, styledModule: null,
hasStyles: false, hasStyles: false,
unitTestRunner: 'none', unitTestRunner: 'none',
names: names(options.name),
}); });
case 'playwright': case 'playwright':
return addE2eReact(host, { return addE2eReact(host, {
@ -27,6 +28,7 @@ export async function addE2e(
styledModule: null, styledModule: null,
hasStyles: false, hasStyles: false,
unitTestRunner: 'none', unitTestRunner: 'none',
names: names(options.name),
}); });
case 'detox': case 'detox':
const { detoxApplicationGenerator } = ensurePackage< const { detoxApplicationGenerator } = ensurePackage<

View File

@ -9,7 +9,7 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { NormalizedSchema } from './normalize-options'; import { NormalizedSchema } from './normalize-options';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path'; import type { PackageJson } from 'nx/src/utils/package-json';
export function addProject(host: Tree, options: NormalizedSchema) { export function addProject(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host); const nxJson = readNxJson(host);
@ -28,15 +28,29 @@ export function addProject(host: Tree, options: NormalizedSchema) {
}; };
if (isUsingTsSolutionSetup(host)) { if (isUsingTsSolutionSetup(host)) {
writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), { const packageJson: PackageJson = {
name: options.projectName, name: options.importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { };
targets: hasPlugin ? {} : getTargets(options),
tags: options.parsedTags?.length ? options.parsedTags : undefined, if (options.projectName !== options.importPath) {
}, packageJson.nx = { name: options.projectName };
}); }
if (!hasPlugin) {
packageJson.nx ??= {};
packageJson.nx.targets = getTargets(options);
}
if (options.parsedTags?.length) {
packageJson.nx ??= {};
packageJson.nx.tags = options.parsedTags;
}
writeJson(
host,
joinPathFragments(options.appProjectRoot, 'package.json'),
packageJson
);
} else { } else {
addProjectConfiguration(host, options.projectName, { addProjectConfiguration(host, options.projectName, {
...project, ...project,

View File

@ -25,6 +25,7 @@ describe('Normalize Options', () => {
addPlugin: true, addPlugin: true,
androidProjectRoot: 'my-app/android', androidProjectRoot: 'my-app/android',
appProjectRoot: 'my-app', appProjectRoot: 'my-app',
importPath: '@proj/my-app',
fileName: 'my-app', fileName: 'my-app',
className: 'MyApp', className: 'MyApp',
directory: 'my-app', directory: 'my-app',
@ -61,6 +62,7 @@ describe('Normalize Options', () => {
addPlugin: true, addPlugin: true,
androidProjectRoot: 'myApp/android', androidProjectRoot: 'myApp/android',
appProjectRoot: 'myApp', appProjectRoot: 'myApp',
importPath: '@proj/myApp',
className: 'MyApp', className: 'MyApp',
fileName: 'my-app', fileName: 'my-app',
directory: 'myApp', directory: 'myApp',
@ -98,6 +100,7 @@ describe('Normalize Options', () => {
addPlugin: true, addPlugin: true,
androidProjectRoot: 'directory/my-app/android', androidProjectRoot: 'directory/my-app/android',
appProjectRoot: 'directory/my-app', appProjectRoot: 'directory/my-app',
importPath: '@proj/my-app',
className: 'MyApp', className: 'MyApp',
fileName: 'my-app', fileName: 'my-app',
directory: 'directory/my-app', directory: 'directory/my-app',
@ -134,6 +137,7 @@ describe('Normalize Options', () => {
addPlugin: true, addPlugin: true,
androidProjectRoot: 'directory/my-app/android', androidProjectRoot: 'directory/my-app/android',
appProjectRoot: 'directory/my-app', appProjectRoot: 'directory/my-app',
importPath: '@proj/my-app',
className: 'MyApp', className: 'MyApp',
directory: 'directory/my-app', directory: 'directory/my-app',
fileName: 'my-app', fileName: 'my-app',
@ -171,6 +175,7 @@ describe('Normalize Options', () => {
addPlugin: true, addPlugin: true,
androidProjectRoot: 'my-app/android', androidProjectRoot: 'my-app/android',
appProjectRoot: 'my-app', appProjectRoot: 'my-app',
importPath: '@proj/my-app',
className: 'MyApp', className: 'MyApp',
fileName: 'my-app', fileName: 'my-app',
directory: 'my-app', directory: 'my-app',

View File

@ -1,13 +1,12 @@
import { joinPathFragments, names, readNxJson, Tree } from '@nx/devkit'; import { joinPathFragments, names, readNxJson, Tree } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Omit<Schema, 'useTsSolution'> {
className: string; // app name in class case className: string; // app name in class case
fileName: string; // app name in file class fileName: string; // app name in file class
projectName: string; // directory + app name, case based on user input projectName: string; // directory + app name, case based on user input
@ -20,6 +19,7 @@ export interface NormalizedSchema extends Schema {
rootProject: boolean; rootProject: boolean;
e2eProjectName: string; e2eProjectName: string;
e2eProjectRoot: string; e2eProjectRoot: string;
importPath: string;
isTsSolutionSetup: boolean; isTsSolutionSetup: boolean;
} }
@ -27,11 +27,12 @@ export async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
await ensureProjectName(host, options, 'application'); await ensureRootProjectName(options, 'application');
const { const {
projectName: appProjectName, projectName,
names: projectNames, names: projectNames,
projectRoot: appProjectRoot, projectRoot: appProjectRoot,
importPath,
} = await determineProjectNameAndRootOptions(host, { } = await determineProjectNameAndRootOptions(host, {
name: options.name, name: options.name,
projectType: 'application', projectType: 'application',
@ -43,11 +44,15 @@ export async function normalizeOptions(
nxJson.useInferencePlugins !== false; nxJson.useInferencePlugins !== false;
options.addPlugin ??= addPluginDefault; options.addPlugin ??= addPluginDefault;
const { className, fileName } = names(options.name); const { className, fileName } = names(projectNames.projectSimpleName);
const iosProjectRoot = joinPathFragments(appProjectRoot, 'ios'); const iosProjectRoot = joinPathFragments(appProjectRoot, 'ios');
const androidProjectRoot = joinPathFragments(appProjectRoot, 'android'); const androidProjectRoot = joinPathFragments(appProjectRoot, 'android');
const rootProject = appProjectRoot === '.'; const rootProject = appProjectRoot === '.';
const isTsSolutionSetup = isUsingTsSolutionSetup(host);
const appProjectName =
!isTsSolutionSetup || options.name ? projectName : importPath;
const e2eProjectName = rootProject ? 'e2e' : `${appProjectName}-e2e`; const e2eProjectName = rootProject ? 'e2e' : `${appProjectName}-e2e`;
const e2eProjectRoot = rootProject ? 'e2e' : `${appProjectRoot}-e2e`; const e2eProjectRoot = rootProject ? 'e2e' : `${appProjectRoot}-e2e`;
@ -57,8 +62,6 @@ export async function normalizeOptions(
const entryFile = options.js ? 'src/main.js' : 'src/main.tsx'; const entryFile = options.js ? 'src/main.js' : 'src/main.tsx';
const isTsSolutionSetup = isUsingTsSolutionSetup(host);
return { return {
...options, ...options,
name: projectNames.projectSimpleName, name: projectNames.projectSimpleName,
@ -66,10 +69,9 @@ export async function normalizeOptions(
fileName, fileName,
lowerCaseName: className.toLowerCase(), lowerCaseName: className.toLowerCase(),
displayName: options.displayName || className, displayName: options.displayName || className,
projectName: isTsSolutionSetup projectName: appProjectName,
? getImportPath(host, appProjectName)
: appProjectName,
appProjectRoot, appProjectRoot,
importPath,
iosProjectRoot, iosProjectRoot,
androidProjectRoot, androidProjectRoot,
parsedTags, parsedTags,

View File

@ -1,16 +1,16 @@
import { readNxJson, Tree } from '@nx/devkit'; import { readNxJson, Tree } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
name: string; name: string;
fileName: string; fileName: string;
projectRoot: string; projectRoot: string;
importPath: string;
routePath: string; routePath: string;
parsedTags: string[]; parsedTags: string[];
appMain?: string; appMain?: string;
@ -22,7 +22,7 @@ export async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
await ensureProjectName(host, options, 'library'); await ensureRootProjectName(options, 'library');
const { const {
projectName, projectName,
names: projectNames, names: projectNames,
@ -50,9 +50,7 @@ export async function normalizeOptions(
...options, ...options,
fileName: projectName, fileName: projectName,
routePath: `/${projectNames.projectSimpleName}`, routePath: `/${projectNames.projectSimpleName}`,
name: isUsingTsSolutionConfig name: isUsingTsSolutionConfig && !options.name ? importPath : projectName,
? options.importPath ?? getImportPath(host, projectName)
: projectName,
projectRoot, projectRoot,
parsedTags, parsedTags,
importPath, importPath,

View File

@ -450,7 +450,7 @@ describe('lib', () => {
}); });
describe('TS solution setup', () => { describe('TS solution setup', () => {
it('should add project references when using TS solution', async () => { beforeEach(() => {
updateJson(appTree, 'package.json', (json) => { updateJson(appTree, 'package.json', (json) => {
json.workspaces = ['packages/*', 'apps/*']; json.workspaces = ['packages/*', 'apps/*'];
return json; return json;
@ -466,7 +466,9 @@ describe('lib', () => {
files: [], files: [],
references: [], references: [],
}); });
});
it('should add project references when using TS solution', async () => {
await libraryGenerator(appTree, { await libraryGenerator(appTree, {
...defaultSchema, ...defaultSchema,
}); });
@ -492,7 +494,6 @@ describe('lib', () => {
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"name": "@proj/my-lib", "name": "@proj/my-lib",
"nx": {},
"peerDependencies": { "peerDependencies": {
"react": "~18.3.1", "react": "~18.3.1",
"react-native": "~0.76.3", "react-native": "~0.76.3",
@ -593,5 +594,39 @@ describe('lib', () => {
} }
`); `);
}); });
it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
directory: 'my-lib',
name: 'my-lib', // import path contains the npm scope, so it would be different
skipFormat: true,
});
expect(readJson(appTree, 'my-lib/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(appTree, {
...defaultSchema,
directory: 'my-lib',
name: '@proj/my-lib',
skipFormat: true,
});
expect(readJson(appTree, 'my-lib/package.json').nx).toBeUndefined();
});
it('should not set "nx.name" in package.json when the user does not provide a name', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
directory: 'my-lib',
skipFormat: true,
});
expect(readJson(appTree, 'my-lib/package.json').nx).toBeUndefined();
});
}); });
}); });

View File

@ -87,6 +87,10 @@ export async function reactNativeLibraryGeneratorInternal(
tasks.push(addProjectTask); tasks.push(addProjectTask);
} }
if (options.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(host, options.projectRoot);
}
const lintTask = await addLinting(host, { const lintTask = await addLinting(host, {
...options, ...options,
projectName: options.name, projectName: options.name,
@ -146,10 +150,6 @@ export async function reactNativeLibraryGeneratorInternal(
: undefined : undefined
); );
if (options.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(host, options.projectRoot);
}
sortPackageJsonFields(host, options.projectRoot); sortPackageJsonFields(host, options.projectRoot);
if (!options.skipFormat) { if (!options.skipFormat) {
@ -181,19 +181,30 @@ async function addProject(
}; };
if (options.isUsingTsSolutionConfig) { if (options.isUsingTsSolutionConfig) {
writeJson(host, joinPathFragments(options.projectRoot, 'package.json'), { const packageJson: PackageJson = {
name: options.name, name: options.importPath,
version: '0.0.1', version: '0.0.1',
...determineEntryFields(options), ...determineEntryFields(options),
nx: {
tags: options.parsedTags?.length ? options.parsedTags : undefined,
},
files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined, files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined,
peerDependencies: { peerDependencies: {
react: reactVersion, react: reactVersion,
'react-native': reactNativeVersion, 'react-native': reactNativeVersion,
}, },
}); };
if (options.name !== options.importPath) {
packageJson.nx = { name: options.name };
}
if (options.parsedTags?.length) {
packageJson.nx ??= {};
packageJson.nx.tags = options.parsedTags;
}
writeJson(
host,
joinPathFragments(options.projectRoot, 'package.json'),
packageJson
);
} else { } else {
addProjectConfiguration(host, options.name, project); addProjectConfiguration(host, options.name, project);
} }

View File

@ -1330,9 +1330,11 @@ describe('app', () => {
}, },
] ]
`); `);
const packageJson = readJson(appTree, 'myapp/package.json');
expect(packageJson.name).toBe('@proj/myapp');
expect(packageJson.nx).toBeUndefined();
// Make sure keys are in idiomatic order // Make sure keys are in idiomatic order
expect(Object.keys(readJson(appTree, 'myapp/package.json'))) expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
.toMatchInlineSnapshot(`
[ [
"name", "name",
"version", "version",
@ -1473,6 +1475,32 @@ describe('app', () => {
`); `);
}); });
it('should respect the provided name', async () => {
await applicationGenerator(appTree, {
directory: 'myapp',
name: 'myapp',
addPlugin: true,
linter: Linter.EsLint,
style: 'none',
bundler: 'vite',
unitTestRunner: 'vitest',
e2eTestRunner: 'playwright',
});
const packageJson = readJson(appTree, 'myapp/package.json');
expect(packageJson.name).toBe('@proj/myapp');
expect(packageJson.nx.name).toBe('myapp');
// Make sure keys are in idiomatic order
expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
[
"name",
"version",
"private",
"nx",
]
`);
});
it('should add project to workspaces when using TS solution (npm, yarn, bun)', async () => { it('should add project to workspaces when using TS solution (npm, yarn, bun)', async () => {
await applicationGenerator(appTree, { await applicationGenerator(appTree, {
directory: 'myapp', directory: 'myapp',

View File

@ -111,8 +111,6 @@ export async function addE2e(
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName], implicitDependencies: [options.projectName],
}, },
} }
@ -209,8 +207,6 @@ export async function addE2e(
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName], implicitDependencies: [options.projectName],
}, },
} }

View File

@ -11,6 +11,7 @@ import {
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin'; import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
import { maybeJs } from '../../../utils/maybe-js'; import { maybeJs } from '../../../utils/maybe-js';
import { hasRspackPlugin } from '../../../utils/has-rspack-plugin'; import { hasRspackPlugin } from '../../../utils/has-rspack-plugin';
import type { PackageJson } from 'nx/src/utils/package-json';
export function addProject(host: Tree, options: NormalizedSchema) { export function addProject(host: Tree, options: NormalizedSchema) {
const project: ProjectConfiguration = { const project: ProjectConfiguration = {
@ -39,11 +40,29 @@ export function addProject(host: Tree, options: NormalizedSchema) {
} }
if (options.isUsingTsSolutionConfig) { if (options.isUsingTsSolutionConfig) {
writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), { const packageJson: PackageJson = {
name: options.projectName, name: options.importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
}); };
if (options.projectName !== options.importPath) {
packageJson.nx = { name: options.projectName };
}
if (Object.keys(project.targets).length) {
packageJson.nx ??= {};
packageJson.nx.targets = project.targets;
}
if (options.parsedTags?.length) {
packageJson.nx ??= {};
packageJson.nx.tags = options.parsedTags;
}
writeJson(
host,
joinPathFragments(options.appProjectRoot, 'package.json'),
packageJson
);
} }
if (!options.isUsingTsSolutionConfig || options.alwaysGenerateProjectJson) { if (!options.isUsingTsSolutionConfig || options.alwaysGenerateProjectJson) {

View File

@ -1,7 +1,6 @@
import { import {
generateFiles, generateFiles,
joinPathFragments, joinPathFragments,
names,
offsetFromRoot, offsetFromRoot,
toJS, toJS,
Tree, Tree,
@ -59,7 +58,7 @@ export async function createApplicationFiles(
); );
const appTests = getAppTests(options); const appTests = getAppTests(options);
const templateVariables = { const templateVariables = {
...names(options.name), ...options.names,
...options, ...options,
js: !!options.js, // Ensure this is defined in template js: !!options.js, // Ensure this is defined in template
tmpl: '', tmpl: '',

View File

@ -1,33 +1,24 @@
import { Tree, extractLayoutDirectory, names, readNxJson } from '@nx/devkit'; import { Tree, names, readNxJson } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { assertValidStyle } from '../../../utils/assertion'; import { assertValidStyle } from '../../../utils/assertion';
import { NormalizedSchema, Schema } from '../schema'; import { NormalizedSchema, Schema } from '../schema';
import { findFreePort } from './find-free-port'; import { findFreePort } from './find-free-port';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export function normalizeDirectory(options: Schema) {
options.directory = options.directory?.replace(/\\{1,2}/g, '/');
const { projectDirectory } = extractLayoutDirectory(options.directory);
return projectDirectory
? `${names(projectDirectory).fileName}/${names(options.name).fileName}`
: names(options.name).fileName;
}
export function normalizeProjectName(options: Schema) {
return normalizeDirectory(options).replace(new RegExp('/', 'g'), '-');
}
export async function normalizeOptions<T extends Schema = Schema>( export async function normalizeOptions<T extends Schema = Schema>(
host: Tree, host: Tree,
options: Schema options: Schema
): Promise<NormalizedSchema<T>> { ): Promise<NormalizedSchema<T>> {
await ensureProjectName(host, options, 'application'); await ensureRootProjectName(options, 'application');
const { projectName: appProjectName, projectRoot: appProjectRoot } = const {
await determineProjectNameAndRootOptions(host, { projectName,
names: projectNames,
projectRoot: appProjectRoot,
importPath,
} = await determineProjectNameAndRootOptions(host, {
name: options.name, name: options.name,
projectType: 'application', projectType: 'application',
directory: options.directory, directory: options.directory,
@ -43,6 +34,10 @@ export async function normalizeOptions<T extends Schema = Schema>(
options.rootProject = appProjectRoot === '.'; options.rootProject = appProjectRoot === '.';
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
const appProjectName =
!isUsingTsSolutionConfig || options.name ? projectName : importPath;
const e2eProjectName = options.rootProject ? 'e2e' : `${appProjectName}-e2e`; const e2eProjectName = options.rootProject ? 'e2e' : `${appProjectName}-e2e`;
const e2eProjectRoot = options.rootProject ? 'e2e' : `${appProjectRoot}-e2e`; const e2eProjectRoot = options.rootProject ? 'e2e' : `${appProjectRoot}-e2e`;
@ -58,20 +53,18 @@ export async function normalizeOptions<T extends Schema = Schema>(
assertValidStyle(options.style); assertValidStyle(options.style);
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
const normalized = { const normalized = {
...options, ...options,
name: appProjectName, projectName: appProjectName,
projectName: isUsingTsSolutionConfig
? getImportPath(host, appProjectName)
: appProjectName,
appProjectRoot, appProjectRoot,
importPath,
e2eProjectName, e2eProjectName,
e2eProjectRoot, e2eProjectRoot,
parsedTags, parsedTags,
fileName, fileName,
styledModule, styledModule,
hasStyles: options.style !== 'none', hasStyles: options.style !== 'none',
names: names(projectNames.projectSimpleName),
isUsingTsSolutionConfig, isUsingTsSolutionConfig,
} as NormalizedSchema; } as NormalizedSchema;

View File

@ -1,3 +1,4 @@
import type { names } from '@nx/devkit';
import type { Linter, LinterType } from '@nx/eslint'; import type { Linter, LinterType } from '@nx/eslint';
import type { SupportedStyles } from '../../../typings/style'; import type { SupportedStyles } from '../../../typings/style';
@ -38,11 +39,13 @@ export interface NormalizedSchema<T extends Schema = Schema> extends T {
appProjectRoot: string; appProjectRoot: string;
e2eProjectName: string; e2eProjectName: string;
e2eProjectRoot: string; e2eProjectRoot: string;
importPath: string;
parsedTags: string[]; parsedTags: string[];
fileName: string; fileName: string;
styledModule: null | SupportedStyles; styledModule: null | SupportedStyles;
hasStyles: boolean; hasStyles: boolean;
unitTestRunner: 'jest' | 'vitest' | 'none'; unitTestRunner: 'jest' | 'vitest' | 'none';
addPlugin?: boolean; addPlugin?: boolean;
names: ReturnType<typeof names>;
isUsingTsSolutionConfig?: boolean; isUsingTsSolutionConfig?: boolean;
} }

View File

@ -13,10 +13,7 @@ import { Schema } from './schema';
import { remoteGenerator } from '../remote/remote'; import { remoteGenerator } from '../remote/remote';
import { addPathToExposes, checkRemoteExists } from './lib/utils'; import { addPathToExposes, checkRemoteExists } from './lib/utils';
import { import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
determineProjectNameAndRootOptions,
ensureProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils';
import { addTsConfigPath, getRootTsConfigPathInTree } from '@nx/js'; import { addTsConfigPath, getRootTsConfigPathInTree } from '@nx/js';
export async function federateModuleGenerator(tree: Tree, schema: Schema) { export async function federateModuleGenerator(tree: Tree, schema: Schema) {

View File

@ -27,7 +27,7 @@ import {
moduleFederationEnhancedVersion, moduleFederationEnhancedVersion,
nxVersion, nxVersion,
} from '../../utils/versions'; } from '../../utils/versions';
import { ensureProjectName } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { ensureRootProjectName } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { updateModuleFederationTsconfig } from './lib/update-module-federation-tsconfig'; import { updateModuleFederationTsconfig } from './lib/update-module-federation-tsconfig';
export async function hostGenerator( export async function hostGenerator(
@ -36,7 +36,10 @@ export async function hostGenerator(
): Promise<GeneratorCallback> { ): Promise<GeneratorCallback> {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const options: NormalizedSchema = { const options: NormalizedSchema = {
...(await normalizeOptions<Schema>(host, schema)), ...(await normalizeOptions<Schema>(host, {
...schema,
alwaysGenerateProjectJson: true,
})),
js: schema.js ?? false, js: schema.js ?? false,
typescriptConfiguration: schema.js typescriptConfiguration: schema.js
? false ? false
@ -60,7 +63,7 @@ export async function hostGenerator(
}); });
} }
await ensureProjectName(host, options, 'application'); await ensureRootProjectName(options, 'application');
const initTask = await applicationGenerator(host, { const initTask = await applicationGenerator(host, {
...options, ...options,
directory: options.appProjectRoot, directory: options.appProjectRoot,

View File

@ -126,7 +126,7 @@ export default defineConfig(() => ({
fileName: 'index', fileName: 'index',
// Change this to the formats you want to support. // Change this to the formats you want to support.
// Don't forget to update your package.json as well. // Don't forget to update your package.json as well.
formats: ['es'], formats: ['es' as const],
}, },
rollupOptions: { rollupOptions: {
// External packages that should not be bundled into your library. // External packages that should not be bundled into your library.

View File

@ -8,11 +8,15 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { import {
determineProjectNameAndRootOptions, determineProjectNameAndRootOptions,
ensureProjectName, ensureRootProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { assertValidStyle } from '../../../utils/assertion'; import { assertValidStyle } from '../../../utils/assertion';
import { NormalizedSchema, Schema } from '../schema'; import { NormalizedSchema, Schema } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import {
getProjectSourceRoot,
getProjectType,
isUsingTsSolutionSetup,
} from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function normalizeOptions( export async function normalizeOptions(
host: Tree, host: Tree,
@ -20,7 +24,7 @@ export async function normalizeOptions(
): Promise<NormalizedSchema> { ): Promise<NormalizedSchema> {
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host); const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
await ensureProjectName(host, options, 'library'); await ensureRootProjectName(options, 'library');
const { const {
projectName, projectName,
names: projectNames, names: projectNames,
@ -70,7 +74,7 @@ export async function normalizeOptions(
bundler, bundler,
fileName, fileName,
routePath: `/${projectNames.projectSimpleName}`, routePath: `/${projectNames.projectSimpleName}`,
name: isUsingTsSolutionConfig ? importPath : projectName, name: isUsingTsSolutionConfig && !options.name ? importPath : projectName,
projectRoot, projectRoot,
parsedTags, parsedTags,
importPath, importPath,
@ -85,17 +89,28 @@ export async function normalizeOptions(
if (options.appProject) { if (options.appProject) {
const appProjectConfig = getProjects(host).get(options.appProject); const appProjectConfig = getProjects(host).get(options.appProject);
const appProjectType = getProjectType(
host,
appProjectConfig.root,
appProjectConfig.projectType
);
if (appProjectConfig.projectType !== 'application') { if (appProjectType !== 'application') {
throw new Error( throw new Error(
`appProject expected type of "application" but got "${appProjectConfig.projectType}"` `appProject expected type of "application" but got "${appProjectType}"`
); );
} }
const appSourceRoot = getProjectSourceRoot(
host,
appProjectConfig.sourceRoot,
appProjectConfig.root
);
normalized.appMain = normalized.appMain =
appProjectConfig.targets.build?.options?.main ?? appProjectConfig.targets.build?.options?.main ??
findMainEntry(host, appProjectConfig.root); findMainEntry(host, appProjectConfig.root);
normalized.appSourceRoot = normalizePath(appProjectConfig.sourceRoot); normalized.appSourceRoot = normalizePath(appSourceRoot);
// TODO(jack): We should use appEntryFile instead of appProject so users can directly set it rather than us inferring it. // TODO(jack): We should use appEntryFile instead of appProject so users can directly set it rather than us inferring it.
if (!normalized.appMain) { if (!normalized.appMain) {

View File

@ -988,7 +988,7 @@ module.exports = withNx(
fileName: 'index', fileName: 'index',
// Change this to the formats you want to support. // Change this to the formats you want to support.
// Don't forget to update your package.json as well. // Don't forget to update your package.json as well.
formats: ['es'] formats: ['es' as const]
}, },
rollupOptions: { rollupOptions: {
// External packages that should not be bundled into your library. // External packages that should not be bundled into your library.
@ -1275,5 +1275,39 @@ module.exports = withNx(
expect(pnpmWorkspaceFile.packages).toEqual(['mylib']); expect(pnpmWorkspaceFile.packages).toEqual(['mylib']);
}); });
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, {
...defaultSchema,
directory: 'libs/my-lib',
name: 'my-lib', // import path contains the npm scope, so it would be different
skipFormat: true,
});
expect(readJson(tree, 'libs/my-lib/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, {
...defaultSchema,
directory: 'libs/my-lib',
name: '@proj/my-lib',
skipFormat: true,
});
expect(readJson(tree, 'libs/my-lib/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, {
...defaultSchema,
directory: 'libs/my-lib',
skipFormat: true,
});
expect(readJson(tree, 'libs/my-lib/package.json').nx).toBeUndefined();
});
}); });
}); });

View File

@ -41,6 +41,7 @@ import {
addReleaseConfigForTsSolution, addReleaseConfigForTsSolution,
releaseTasks, releaseTasks,
} from '@nx/js/src/generators/library/utils/add-release-config'; } from '@nx/js/src/generators/library/utils/add-release-config';
import type { PackageJson } from 'nx/src/utils/package-json';
export async function libraryGenerator(host: Tree, schema: Schema) { export async function libraryGenerator(host: Tree, schema: Schema) {
return await libraryGeneratorInternal(host, { return await libraryGeneratorInternal(host, {
@ -80,17 +81,22 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) {
tasks.push(initTask); tasks.push(initTask);
if (options.isUsingTsSolutionConfig) { if (options.isUsingTsSolutionConfig) {
writeJson(host, `${options.projectRoot}/package.json`, { const packageJson: PackageJson = {
name: options.importPath ?? options.name, name: options.importPath,
version: '0.0.1', version: '0.0.1',
...determineEntryFields(options), ...determineEntryFields(options),
nx: options.parsedTags?.length
? {
tags: options.parsedTags,
}
: undefined,
files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined, files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined,
}); };
if (options.name !== options.importPath) {
packageJson.nx = { name: options.name };
}
if (options.parsedTags?.length) {
packageJson.nx ??= {};
packageJson.nx.tags = options.parsedTags;
}
writeJson(host, `${options.projectRoot}/package.json`, packageJson);
} else { } else {
addProjectConfiguration(host, options.name, { addProjectConfiguration(host, options.name, {
root: options.projectRoot, root: options.projectRoot,

View File

@ -36,6 +36,7 @@ export interface NormalizedSchema extends Schema {
projectRoot: string; projectRoot: string;
routePath: string; routePath: string;
parsedTags: string[]; parsedTags: string[];
importPath: string;
appMain?: string; appMain?: string;
appSourceRoot?: string; appSourceRoot?: string;
unitTestRunner: 'jest' | 'vitest' | 'none'; unitTestRunner: 'jest' | 'vitest' | 'none';

View File

@ -30,7 +30,7 @@ import {
moduleFederationEnhancedVersion, moduleFederationEnhancedVersion,
nxVersion, nxVersion,
} from '../../utils/versions'; } from '../../utils/versions';
import { ensureProjectName } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { ensureRootProjectName } from '@nx/devkit/src/generators/project-name-and-root-utils';
export function addModuleFederationFiles( export function addModuleFederationFiles(
host: Tree, host: Tree,
@ -96,7 +96,10 @@ export function addModuleFederationFiles(
export async function remoteGenerator(host: Tree, schema: Schema) { export async function remoteGenerator(host: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const options: NormalizedSchema<Schema> = { const options: NormalizedSchema<Schema> = {
...(await normalizeOptions<Schema>(host, schema)), ...(await normalizeOptions<Schema>(host, {
...schema,
alwaysGenerateProjectJson: true,
})),
// when js is set to true, we want to use the js configuration // when js is set to true, we want to use the js configuration
js: schema.js ?? false, js: schema.js ?? false,
typescriptConfiguration: schema.js typescriptConfiguration: schema.js
@ -111,20 +114,20 @@ export async function remoteGenerator(host: Tree, schema: Schema) {
if (options.dynamic) { if (options.dynamic) {
// Dynamic remotes generate with library { type: 'var' } by default. // Dynamic remotes generate with library { type: 'var' } by default.
// We need to ensure that the remote name is a valid variable name. // We need to ensure that the remote name is a valid variable name.
const isValidRemote = isValidVariable(options.name); const isValidRemote = isValidVariable(options.projectName);
if (!isValidRemote.isValid) { if (!isValidRemote.isValid) {
throw new Error( throw new Error(
`Invalid remote name provided: ${options.name}. ${isValidRemote.message}` `Invalid remote name provided: ${options.projectName}. ${isValidRemote.message}`
); );
} }
} }
await ensureProjectName(host, options, 'application'); await ensureRootProjectName(options, 'application');
const REMOTE_NAME_REGEX = '^[a-zA-Z_$][a-zA-Z_$0-9]*$'; const REMOTE_NAME_REGEX = '^[a-zA-Z_$][a-zA-Z_$0-9]*$';
const remoteNameRegex = new RegExp(REMOTE_NAME_REGEX); const remoteNameRegex = new RegExp(REMOTE_NAME_REGEX);
if (!remoteNameRegex.test(options.name)) { if (!remoteNameRegex.test(options.projectName)) {
throw new Error( throw new Error(
stripIndents`Invalid remote name: ${options.name}. Remote project names must: stripIndents`Invalid remote name: ${options.projectName}. Remote project names must:
- Start with a letter, dollar sign ($) or underscore (_) - Start with a letter, dollar sign ($) or underscore (_)
- Followed by any valid character (letters, digits, underscores, or dollar signs) - Followed by any valid character (letters, digits, underscores, or dollar signs)
The regular expression used is ${REMOTE_NAME_REGEX}.` The regular expression used is ${REMOTE_NAME_REGEX}.`
@ -132,14 +135,14 @@ export async function remoteGenerator(host: Tree, schema: Schema) {
} }
const initAppTask = await applicationGenerator(host, { const initAppTask = await applicationGenerator(host, {
...options, ...options,
name: options.name, name: options.projectName,
skipFormat: true, skipFormat: true,
alwaysGenerateProjectJson: true, alwaysGenerateProjectJson: true,
}); });
tasks.push(initAppTask); tasks.push(initAppTask);
if (options.host) { if (options.host) {
updateHostWithRemote(host, options.host, options.name); updateHostWithRemote(host, options.host, options.projectName);
} }
// Module federation requires bootstrap code to be dynamically imported. // Module federation requires bootstrap code to be dynamically imported.

View File

@ -462,9 +462,9 @@ describe('Remix Application', () => {
tags: 'foo', tags: 'foo',
}); });
const packageJson = readJson(tree, 'myapp/package.json');
// Make sure keys are in idiomatic order // Make sure keys are in idiomatic order
expect(Object.keys(readJson(tree, 'myapp/package.json'))) expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
.toMatchInlineSnapshot(`
[ [
"name", "name",
"private", "private",
@ -477,7 +477,7 @@ describe('Remix Application', () => {
"devDependencies", "devDependencies",
] ]
`); `);
expect(readJson(tree, 'myapp/package.json')).toMatchInlineSnapshot(` expect(packageJson).toMatchInlineSnapshot(`
{ {
"dependencies": { "dependencies": {
"@remix-run/node": "^2.15.0", "@remix-run/node": "^2.15.0",
@ -655,6 +655,35 @@ describe('Remix Application', () => {
`); `);
}); });
it('should respect the provided name', async () => {
await applicationGenerator(tree, {
directory: 'myapp',
name: 'myapp',
e2eTestRunner: 'playwright',
unitTestRunner: 'jest',
addPlugin: true,
tags: 'foo',
});
const packageJson = readJson(tree, 'myapp/package.json');
expect(packageJson.name).toBe('@proj/myapp');
expect(packageJson.nx.name).toBe('myapp');
// Make sure keys are in idiomatic order
expect(Object.keys(packageJson)).toMatchInlineSnapshot(`
[
"name",
"private",
"type",
"scripts",
"engines",
"sideEffects",
"nx",
"dependencies",
"devDependencies",
]
`);
});
it('should skip nx property in package.json when no tags are provided', async () => { it('should skip nx property in package.json when no tags are provided', async () => {
await applicationGenerator(tree, { await applicationGenerator(tree, {
directory: 'apps/myapp', directory: 'apps/myapp',

View File

@ -161,6 +161,21 @@ export async function remixApplicationGeneratorInternal(
options.projectRoot, options.projectRoot,
vars vars
); );
updateJson(
tree,
joinPathFragments(options.projectRoot, 'package.json'),
(json) => {
if (options.projectName !== options.importPath) {
json.nx = { name: options.projectName };
}
if (options.parsedTags?.length) {
json.nx ??= {};
json.nx.tags = options.parsedTags;
}
return json;
}
);
} }
if (options.unitTestRunner !== 'none') { if (options.unitTestRunner !== 'none') {

View File

@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"name": "<%= projectName %>", "name": "<%= importPath %>",
"scripts": {}, "scripts": {},
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@ -19,8 +19,5 @@
"engines": { "engines": {
"node": ">=20" "node": ">=20"
}, },
"sideEffects": false<% if (isUsingTsSolutionConfig && parsedTags?.length) { %>, "sideEffects": false
"nx": {
"tags": <%- JSON.stringify(parsedTags) %>
}<% } %>
} }

Some files were not shown because too many files have changed in this diff Show More