feat(js): remove nx property from generated package.json files (#29705)

This PR updates our generators to no longer generate with `nx` in
`package.json` by default. The only times it is needed is if you pass
add `tags` or `implicitDependencies` to the project config.

This PR replaces our `projectType` checks to use the `getProjectType`
util from `@nx/js` to prefer the project config, but otherwise will
check for our conventions (e.g. using `exports` for libs,
`tsconfig.lib.json` vs `tsconfig.app.json`).

## Impact
- There shouldn't be any behavioral changes to existing projects that
have explicit `projectType`, `name`, etc. in with `project.json` or
`package.json` (via `nx` property).
- For new projects created under the new TS setup, the `nx` property
will no longer be there. Generators with logic that depend on
`projectType` will now check for `tsconfig.lib.json` and
`tsconfig.app.json` (so all of our generators are covered). If none of
those tsconfig files are found, then we check `package.json`, since
libraries are required to have `exports` to be consumed.
This commit is contained in:
Jack Hsu 2025-01-23 20:03:28 -05:00 committed by GitHub
parent ae9aa5ac76
commit 45847a6754
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 652 additions and 383 deletions

View File

@ -114,70 +114,70 @@ ${content}`
// check build // check build
expect(runCLI(`build ${esbuildParentLib}`)).toContain( expect(runCLI(`build ${esbuildParentLib}`)).toContain(
`Successfully ran target build for project ${esbuildParentLib} and 5 tasks it depends on` `Successfully ran target build for project @proj/${esbuildParentLib} and 5 tasks it depends on`
); );
expect(runCLI(`build ${rollupParentLib}`)).toContain( expect(runCLI(`build ${rollupParentLib}`)).toContain(
`Successfully ran target build for project ${rollupParentLib} and 5 tasks it depends on` `Successfully ran target build for project @proj/${rollupParentLib} and 5 tasks it depends on`
); );
expect(runCLI(`build ${swcParentLib}`)).toContain( expect(runCLI(`build ${swcParentLib}`)).toContain(
`Successfully ran target build for project ${swcParentLib} and 5 tasks it depends on` `Successfully ran target build for project @proj/${swcParentLib} and 5 tasks it depends on`
); );
expect(runCLI(`build ${tscParentLib}`)).toContain( expect(runCLI(`build ${tscParentLib}`)).toContain(
`Successfully ran target build for project ${tscParentLib} and 5 tasks it depends on` `Successfully ran target build for project @proj/${tscParentLib} and 5 tasks it depends on`
); );
expect(runCLI(`build ${viteParentLib}`)).toContain( expect(runCLI(`build ${viteParentLib}`)).toContain(
`Successfully ran target build for project ${viteParentLib} and 5 tasks it depends on` `Successfully ran target build for project @proj/${viteParentLib} and 5 tasks it depends on`
); );
// check typecheck // check typecheck
expect(runCLI(`typecheck ${esbuildParentLib}`)).toContain( expect(runCLI(`typecheck ${esbuildParentLib}`)).toContain(
`Successfully ran target typecheck for project ${esbuildParentLib} and 5 tasks it depends on` `Successfully ran target typecheck for project @proj/${esbuildParentLib} and 5 tasks it depends on`
); );
expect(runCLI(`typecheck ${rollupParentLib}`)).toContain( expect(runCLI(`typecheck ${rollupParentLib}`)).toContain(
`Successfully ran target typecheck for project ${rollupParentLib} and 5 tasks it depends on` `Successfully ran target typecheck for project @proj/${rollupParentLib} and 5 tasks it depends on`
); );
expect(runCLI(`typecheck ${swcParentLib}`)).toContain( expect(runCLI(`typecheck ${swcParentLib}`)).toContain(
`Successfully ran target typecheck for project ${swcParentLib} and 5 tasks it depends on` `Successfully ran target typecheck for project @proj/${swcParentLib} and 5 tasks it depends on`
); );
expect(runCLI(`typecheck ${tscParentLib}`)).toContain( expect(runCLI(`typecheck ${tscParentLib}`)).toContain(
`Successfully ran target typecheck for project ${tscParentLib} and 5 tasks it depends on` `Successfully ran target typecheck for project @proj/${tscParentLib} and 5 tasks it depends on`
); );
expect(runCLI(`typecheck ${viteParentLib}`)).toContain( expect(runCLI(`typecheck ${viteParentLib}`)).toContain(
`Successfully ran target typecheck for project ${viteParentLib} and 5 tasks it depends on` `Successfully ran target typecheck for project @proj/${viteParentLib} and 5 tasks it depends on`
); );
// check lint // check lint
expect(runCLI(`lint ${esbuildParentLib}`)).toContain( expect(runCLI(`lint ${esbuildParentLib}`)).toContain(
`Successfully ran target lint for project ${esbuildParentLib}` `Successfully ran target lint for project @proj/${esbuildParentLib}`
); );
expect(runCLI(`lint ${rollupParentLib}`)).toContain( expect(runCLI(`lint ${rollupParentLib}`)).toContain(
`Successfully ran target lint for project ${rollupParentLib}` `Successfully ran target lint for project @proj/${rollupParentLib}`
); );
expect(runCLI(`lint ${swcParentLib}`)).toContain( expect(runCLI(`lint ${swcParentLib}`)).toContain(
`Successfully ran target lint for project ${swcParentLib}` `Successfully ran target lint for project @proj/${swcParentLib}`
); );
expect(runCLI(`lint ${tscParentLib}`)).toContain( expect(runCLI(`lint ${tscParentLib}`)).toContain(
`Successfully ran target lint for project ${tscParentLib}` `Successfully ran target lint for project @proj/${tscParentLib}`
); );
expect(runCLI(`lint ${viteParentLib}`)).toContain( expect(runCLI(`lint ${viteParentLib}`)).toContain(
`Successfully ran target lint for project ${viteParentLib}` `Successfully ran target lint for project @proj/${viteParentLib}`
); );
// check test // check test
expect(runCLI(`test ${esbuildParentLib}`)).toContain( expect(runCLI(`test ${esbuildParentLib}`)).toContain(
`Successfully ran target test for project ${esbuildParentLib}` `Successfully ran target test for project @proj/${esbuildParentLib}`
); );
expect(runCLI(`test ${rollupParentLib}`)).toContain( expect(runCLI(`test ${rollupParentLib}`)).toContain(
`Successfully ran target test for project ${rollupParentLib}` `Successfully ran target test for project @proj/${rollupParentLib}`
); );
expect(runCLI(`test ${swcParentLib}`)).toContain( expect(runCLI(`test ${swcParentLib}`)).toContain(
`Successfully ran target test for project ${swcParentLib}` `Successfully ran target test for project @proj/${swcParentLib}`
); );
expect(runCLI(`test ${tscParentLib}`)).toContain( expect(runCLI(`test ${tscParentLib}`)).toContain(
`Successfully ran target test for project ${tscParentLib}` `Successfully ran target test for project @proj/${tscParentLib}`
); );
expect(runCLI(`test ${viteParentLib}`)).toContain( expect(runCLI(`test ${viteParentLib}`)).toContain(
`Successfully ran target test for project ${viteParentLib}` `Successfully ran target test for project @proj/${viteParentLib}`
); );
}, 300_000); }, 300_000);
}); });

View File

@ -42,14 +42,14 @@ describe('Nx Plugin (TS solution)', () => {
`generate @nx/plugin:migration packages/${plugin}/src/migrations/update-${migrationVersion}/update-${migrationVersion} --packageVersion=${migrationVersion} --packageJsonUpdates=false` `generate @nx/plugin:migration packages/${plugin}/src/migrations/update-${migrationVersion}/update-${migrationVersion} --packageVersion=${migrationVersion} --packageJsonUpdates=false`
); );
expect(runCLI(`lint ${plugin}`)).toContain( expect(runCLI(`lint @proj/${plugin}`)).toContain(
`Successfully ran target lint for project ${plugin}` `Successfully ran target lint for project @proj/${plugin}`
); );
expect(runCLI(`typecheck ${plugin}`)).toContain( expect(runCLI(`typecheck @proj/${plugin}`)).toContain(
`Successfully ran target typecheck for project ${plugin}` `Successfully ran target typecheck for project @proj/${plugin}`
); );
expect(runCLI(`build ${plugin}`)).toContain( expect(runCLI(`build @proj/${plugin}`)).toContain(
`Successfully ran target build for project ${plugin}` `Successfully ran target build for project @proj/${plugin}`
); );
checkFilesExist( checkFilesExist(
// entry point // entry point
@ -71,11 +71,11 @@ describe('Nx Plugin (TS solution)', () => {
`packages/${plugin}/dist/migrations/update-${migrationVersion}/update-${migrationVersion}.js`, `packages/${plugin}/dist/migrations/update-${migrationVersion}/update-${migrationVersion}.js`,
`packages/${plugin}/dist/migrations/update-${migrationVersion}/update-${migrationVersion}.d.ts` `packages/${plugin}/dist/migrations/update-${migrationVersion}/update-${migrationVersion}.d.ts`
); );
expect(runCLI(`test ${plugin}`)).toContain( expect(runCLI(`test @proj/${plugin}`)).toContain(
`Successfully ran target test for project ${plugin}` `Successfully ran target test for project @proj/${plugin}`
); );
expect(runCLI(`e2e ${plugin}-e2e`)).toContain( expect(runCLI(`e2e @proj/${plugin}-e2e`)).toContain(
`Successfully ran target e2e for project ${plugin}-e2e` `Successfully ran target e2e for project @proj/${plugin}-e2e`
); );
}, 90000); }, 90000);

View File

@ -32,13 +32,27 @@ describe('Vite - TS solution setup', () => {
const viteLib = uniq('vite-lib'); const viteLib = uniq('vite-lib');
const noBundlerLib = uniq('no-bundler-lib'); const noBundlerLib = uniq('no-bundler-lib');
runCLI(`generate @nx/react:app apps/${reactApp} --bundler=vite`); runCLI(
runCLI(`generate @nx/js:lib packages/${esbuildLib} --bundler=esbuild`); `generate @nx/react:app apps/${reactApp} --bundler=vite --e2eTestRunner=none`
runCLI(`generate @nx/js:lib packages/${rollupLib} --bundler=rollup`); );
runCLI(`generate @nx/js:lib packages/${swcLib} --bundler=swc`); runCLI(
runCLI(`generate @nx/js:lib packages/${tscLib} --bundler=tsc`); `generate @nx/js:lib packages/${esbuildLib} --bundler=esbuild --e2eTestRunner=none`
runCLI(`generate @nx/js:lib packages/${viteLib} --bundler=vite`); );
runCLI(`generate @nx/js:lib packages/${noBundlerLib} --bundler=none`); runCLI(
`generate @nx/js:lib packages/${rollupLib} --bundler=rollup --e2eTestRunner=none`
);
runCLI(
`generate @nx/js:lib packages/${swcLib} --bundler=swc --e2eTestRunner=none`
);
runCLI(
`generate @nx/js:lib packages/${tscLib} --bundler=tsc --e2eTestRunner=none`
);
runCLI(
`generate @nx/js:lib packages/${viteLib} --bundler=vite --e2eTestRunner=none`
);
runCLI(
`generate @nx/js:lib packages/${noBundlerLib} --bundler=none --e2eTestRunner=none`
);
// import all libs from the app // import all libs from the app
updateFile( updateFile(
@ -87,12 +101,12 @@ ${content}`
// check build // check build
expect(runCLI(`build ${reactApp}`)).toContain( expect(runCLI(`build ${reactApp}`)).toContain(
`Successfully ran target build for project ${reactApp} and 5 tasks it depends on` `Successfully ran target build for project @proj/${reactApp} and 5 tasks it depends on`
); );
// check typecheck // check typecheck
expect(runCLI(`typecheck ${reactApp}`)).toContain( expect(runCLI(`typecheck ${reactApp}`)).toContain(
`Successfully ran target typecheck for project ${reactApp} and 6 tasks it depends on` `Successfully ran target typecheck for project @proj/${reactApp} and 6 tasks it depends on`
); );
}, 300_000); }, 300_000);
}); });

View File

@ -50,11 +50,11 @@ describe('Vue Plugin', () => {
` `
); );
expect(() => runCLI(`lint ${app}`)).not.toThrow(); expect(() => runCLI(`lint @proj/${app}`)).not.toThrow();
expect(() => runCLI(`test ${app}`)).not.toThrow(); expect(() => runCLI(`test @proj/${app}`)).not.toThrow();
expect(() => runCLI(`build ${app}`)).not.toThrow(); expect(() => runCLI(`build @proj/${app}`)).not.toThrow();
expect(() => runCLI(`lint ${lib}`)).not.toThrow(); expect(() => runCLI(`lint @proj/${lib}`)).not.toThrow();
expect(() => runCLI(`test ${lib}`)).not.toThrow(); expect(() => runCLI(`test @proj/${lib}`)).not.toThrow();
expect(() => runCLI(`build ${lib}`)).not.toThrow(); expect(() => runCLI(`build @proj/${lib}`)).not.toThrow();
}, 300_000); }, 300_000);
}); });

View File

@ -202,14 +202,13 @@ export function updateTsConfigForComponentTesting(
tree: Tree, tree: Tree,
projectConfig: ProjectConfiguration projectConfig: ProjectConfiguration
) { ) {
const tsConfigPath = joinPathFragments( let tsConfigPath: string | null = null;
projectConfig.root, for (const candidate of ['tsconfig.lib.json', 'tsconfig.app.json']) {
projectConfig.projectType === 'library' const p = joinPathFragments(projectConfig.root, candidate);
? 'tsconfig.lib.json' if (tree.exists(p)) tsConfigPath = p;
: 'tsconfig.app.json' }
);
if (tree.exists(tsConfigPath)) { if (tsConfigPath !== null) {
updateJson(tree, tsConfigPath, (json) => { updateJson(tree, tsConfigPath, (json) => {
const excluded = new Set([ const excluded = new Set([
...(json.exclude || []), ...(json.exclude || []),

View File

@ -4,7 +4,7 @@ export function updateTsConfigsToJs(
tree: Tree, tree: Tree,
options: { projectRoot: string } options: { projectRoot: string }
): void { ): void {
let updateConfigPath: string; let updateConfigPath: string | null = null;
const paths = { const paths = {
tsConfig: `${options.projectRoot}/tsconfig.json`, tsConfig: `${options.projectRoot}/tsconfig.json`,
@ -12,19 +12,6 @@ export function updateTsConfigsToJs(
tsConfigApp: `${options.projectRoot}/tsconfig.app.json`, tsConfigApp: `${options.projectRoot}/tsconfig.app.json`,
}; };
const getProjectType = (tree: Tree) => {
if (tree.exists(paths.tsConfigApp)) {
return 'application';
}
if (tree.exists(paths.tsConfigLib)) {
return 'library';
}
throw new Error(
`project is missing tsconfig.lib.json or tsconfig.app.json`
);
};
updateJson(tree, paths.tsConfig, (json) => { updateJson(tree, paths.tsConfig, (json) => {
if (json.compilerOptions) { if (json.compilerOptions) {
json.compilerOptions.allowJs = true; json.compilerOptions.allowJs = true;
@ -34,15 +21,15 @@ export function updateTsConfigsToJs(
return json; return json;
}); });
const projectType = getProjectType(tree); if (tree.exists(paths.tsConfigLib)) {
if (projectType === 'library') {
updateConfigPath = paths.tsConfigLib; updateConfigPath = paths.tsConfigLib;
} }
if (projectType === 'application') {
if (tree.exists(paths.tsConfigApp)) {
updateConfigPath = paths.tsConfigApp; updateConfigPath = paths.tsConfigApp;
} }
if (updateConfigPath) {
updateJson(tree, updateConfigPath, (json) => { updateJson(tree, updateConfigPath, (json) => {
json.include = uniq([...json.include, 'src/**/*.js']); json.include = uniq([...json.include, 'src/**/*.js']);
json.exclude = uniq([ json.exclude = uniq([
@ -53,6 +40,11 @@ export function updateTsConfigsToJs(
return json; return json;
}); });
} else {
throw new Error(
`project is missing tsconfig.lib.json or tsconfig.app.json`
);
}
} }
const uniq = <T extends string[]>(value: T) => [...new Set(value)] as T; const uniq = <T extends string[]>(value: T) => [...new Set(value)] as T;

View File

@ -39,6 +39,7 @@ import {
} from '../../utils/config-file'; } from '../../utils/config-file';
import { hasEslintPlugin } from '../utils/plugin'; import { hasEslintPlugin } from '../utils/plugin';
import { setupRootEsLint } from './setup-root-eslint'; import { setupRootEsLint } from './setup-root-eslint';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
interface LintProjectOptions { interface LintProjectOptions {
project: string; project: string;
@ -99,7 +100,7 @@ export async function lintProjectGeneratorInternal(
lintFilePatterns && lintFilePatterns &&
lintFilePatterns.length && lintFilePatterns.length &&
!lintFilePatterns.includes('{projectRoot}') && !lintFilePatterns.includes('{projectRoot}') &&
isBuildableLibraryProject(projectConfig) isBuildableLibraryProject(tree, projectConfig)
) { ) {
lintFilePatterns.push(`{projectRoot}/package.json`); lintFilePatterns.push(`{projectRoot}/package.json`);
} }
@ -175,7 +176,7 @@ export async function lintProjectGeneratorInternal(
// Buildable libs need source analysis enabled for linting `package.json`. // Buildable libs need source analysis enabled for linting `package.json`.
if ( if (
isBuildableLibraryProject(projectConfig) && isBuildableLibraryProject(tree, projectConfig) &&
!isJsAnalyzeSourceFilesEnabled(tree) !isJsAnalyzeSourceFilesEnabled(tree)
) { ) {
updateJson(tree, 'nx.json', (json) => { updateJson(tree, 'nx.json', (json) => {
@ -225,7 +226,7 @@ function createEsLintConfiguration(
const addDependencyChecks = const addDependencyChecks =
options.addPackageJsonDependencyChecks || options.addPackageJsonDependencyChecks ||
isBuildableLibraryProject(projectConfig); isBuildableLibraryProject(tree, projectConfig);
const overrides: Linter.ConfigOverride<Linter.RulesRecord>[] = useFlatConfig( const overrides: Linter.ConfigOverride<Linter.RulesRecord>[] = useFlatConfig(
tree tree
@ -328,10 +329,12 @@ function isJsAnalyzeSourceFilesEnabled(tree: Tree): boolean {
} }
function isBuildableLibraryProject( function isBuildableLibraryProject(
tree: Tree,
projectConfig: ProjectConfiguration projectConfig: ProjectConfiguration
): boolean { ): boolean {
return ( return (
projectConfig.projectType === 'library' && getProjectType(tree, projectConfig.root, projectConfig.projectType) ===
'library' &&
projectConfig.targets?.build && projectConfig.targets?.build &&
!!projectConfig.targets.build !!projectConfig.targets.build
); );

View File

@ -11,6 +11,7 @@ import {
import { NormalizedSchema, normalizeOptions } from './lib/normalize-options'; import { NormalizedSchema, normalizeOptions } from './lib/normalize-options';
import { addImport } from './lib/add-import'; import { addImport } from './lib/add-import';
import { dirname, join, parse, relative } from 'path'; import { dirname, join, parse, relative } from 'path';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function expoComponentGenerator(host: Tree, schema: Schema) { export async function expoComponentGenerator(host: Tree, schema: Schema) {
const options = await normalizeOptions(host, schema); const options = await normalizeOptions(host, schema);
@ -46,8 +47,9 @@ function createComponentFiles(host: Tree, options: NormalizedSchema) {
function addExportsToBarrel(host: Tree, options: NormalizedSchema) { function addExportsToBarrel(host: Tree, options: NormalizedSchema) {
const workspace = getProjects(host); const workspace = getProjects(host);
const proj = workspace.get(options.projectName);
const isApp = const isApp =
workspace.get(options.projectName).projectType === 'application'; getProjectType(host, proj.root, proj.projectType) === 'application';
if (options.export && !isApp) { if (options.export && !isApp) {
const indexFilePath = joinPathFragments( const indexFilePath = joinPathFragments(

View File

@ -4,6 +4,7 @@ import {
type FileExtensionType, type FileExtensionType,
} from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; } from '@nx/devkit/src/generators/artifact-name-and-directory-utils';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
export interface NormalizedSchema extends Omit<Schema, 'js'> { export interface NormalizedSchema extends Omit<Schema, 'js'> {
directory: string; directory: string;
@ -38,9 +39,12 @@ export async function normalizeOptions(
const { className } = names(name); const { className } = names(name);
const project = getProjects(host).get(projectName); const project = getProjects(host).get(projectName);
const { sourceRoot: projectSourceRoot, projectType } = project; const { root, sourceRoot: projectSourceRoot, projectType } = project;
if (options.export && projectType === 'application') { if (
options.export &&
getProjectType(host, root, projectType) === 'application'
) {
logger.warn( logger.warn(
`The "--export" option should not be used with applications and will do nothing.` `The "--export" option should not be used with applications and will do nothing.`
); );

View File

@ -13,6 +13,7 @@ import {
type JestPresetExtension, type JestPresetExtension,
} from '../../../utils/config/config-file'; } from '../../../utils/config/config-file';
import type { NormalizedJestProjectSchema } from '../schema'; import type { NormalizedJestProjectSchema } from '../schema';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function createJestConfig( export async function createJestConfig(
tree: Tree, tree: Tree,
@ -91,7 +92,13 @@ module.exports = { ...nxPreset };`
} }
const jestProjectConfig = `jest.config.${ const jestProjectConfig = `jest.config.${
rootProjectConfig.projectType === 'application' ? 'app' : 'lib' getProjectType(
tree,
rootProjectConfig.root,
rootProjectConfig.projectType
) === 'application'
? 'app'
: 'lib'
}.${options.js ? 'js' : 'ts'}`; }.${options.js ? 'js' : 'ts'}`;
tree.rename(rootJestPath, jestProjectConfig); tree.rename(rootJestPath, jestProjectConfig);

View File

@ -6,12 +6,16 @@ import {
type Tree, type Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import type { NormalizedJestProjectSchema } from '../schema'; import type { NormalizedJestProjectSchema } from '../schema';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
export function updateTsConfig( export function updateTsConfig(
host: Tree, host: Tree,
options: NormalizedJestProjectSchema options: NormalizedJestProjectSchema
) { ) {
const { root, projectType } = readProjectConfiguration(host, options.project); const { root, projectType: _projectType } = readProjectConfiguration(
host,
options.project
);
if (!host.exists(joinPathFragments(root, 'tsconfig.json'))) { if (!host.exists(joinPathFragments(root, 'tsconfig.json'))) {
throw new Error( throw new Error(
`Expected ${joinPathFragments( `Expected ${joinPathFragments(
@ -31,6 +35,7 @@ export function updateTsConfig(
} }
return json; return json;
}); });
const projectType = getProjectType(host, root, _projectType);
// fall-back runtime tsconfig file path in case the user didn't provide one // fall-back runtime tsconfig file path in case the user didn't provide one
let runtimeTsconfigPath = joinPathFragments( let runtimeTsconfigPath = joinPathFragments(

View File

@ -2146,7 +2146,7 @@ describe('lib', () => {
swcJestConfig.swcrc = false; swcJestConfig.swcrc = false;
export default { export default {
displayName: 'my-lib', displayName: '@proj/my-lib',
preset: '../jest.preset.js', preset: '../jest.preset.js',
transform: { transform: {
'^.+\\\\.[tj]s$': ['@swc/jest', swcJestConfig], '^.+\\\\.[tj]s$': ['@swc/jest', swcJestConfig],

View File

@ -68,6 +68,7 @@ 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';
const defaultOutputDirectory = 'dist'; const defaultOutputDirectory = 'dist';
@ -100,6 +101,12 @@ export async function libraryGeneratorInternal(
); );
const options = await normalizeOptions(tree, schema); const options = await normalizeOptions(tree, schema);
// If we are using the new TS solution
// We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project
if (options.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(tree, options.projectRoot);
}
createFiles(tree, options); createFiles(tree, options);
await configureProject(tree, options); await configureProject(tree, options);
@ -234,12 +241,6 @@ export async function libraryGeneratorInternal(
); );
} }
// 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.projectRoot);
}
sortPackageJsonFields(tree, options.projectRoot); sortPackageJsonFields(tree, options.projectRoot);
if (!options.skipFormat) { if (!options.skipFormat) {
@ -369,8 +370,24 @@ async function configureProject(
delete projectConfiguration.tags; delete projectConfiguration.tags;
} }
// We want a minimal setup, so unless targets and tags are set, just skip the `nx` property in `package.json`.
if (options.isUsingTsSolutionConfig) {
delete projectConfiguration.projectType;
// SWC executor has logic around sourceRoot and `--strip-leading-paths`. If it is not set then dist will contain the `src` folder rather than being flat.
// TODO(leo): Look at how we can remove the dependency on sourceRoot for SWC.
if (options.bundler !== 'swc') {
delete projectConfiguration.sourceRoot;
}
}
// empty targets are cleaned up automatically by `updateProjectConfiguration` // empty targets are cleaned up automatically by `updateProjectConfiguration`
updateProjectConfiguration(tree, options.name, projectConfiguration); updateProjectConfiguration(
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 {
@ -897,7 +914,9 @@ async function normalizeOptions(
return { return {
...options, ...options,
fileName, fileName,
name: projectName, name: isUsingTsSolutionConfig
? getImportPath(tree, projectName)
: projectName,
projectNames, projectNames,
projectRoot, projectRoot,
parsedTags, parsedTags,

View File

@ -244,3 +244,22 @@ export function addProjectToTsSolutionWorkspace(
} }
} }
} }
export function getProjectType(
tree: Tree,
projectRoot: string,
projectType?: 'library' | 'application'
): 'library' | 'application' {
if (projectType) return projectType;
if (tree.exists(joinPathFragments(projectRoot, 'tsconfig.lib.json')))
return 'library';
if (tree.exists(joinPathFragments(projectRoot, 'tsconfig.app.json')))
return 'application';
// If there are no exports, assume it is an application since both buildable and non-buildable libraries have exports.
const packageJsonPath = joinPathFragments(projectRoot, 'package.json');
const packageJson = tree.exists(packageJsonPath)
? readJson(tree, joinPathFragments(projectRoot, 'package.json'))
: null;
if (!packageJson?.exports) return 'application';
return 'library';
}

View File

@ -7,6 +7,8 @@ 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 { getImportPath } from '@nx/js/src/utils/get-import-path';
export async function normalizeOptions( export async function normalizeOptions(
tree: Tree, tree: Tree,
@ -47,7 +49,9 @@ 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, projectName: isUsingTsSolutionSetup(tree)
? getImportPath(tree, projectName)
: projectName,
projectRoot, projectRoot,
importPath, importPath,
service: options.service ?? false, service: options.service ?? false,

View File

@ -563,7 +563,6 @@ describe('lib', () => {
"main", "main",
"types", "types",
"exports", "exports",
"nx",
"dependencies", "dependencies",
] ]
`); `);
@ -580,11 +579,6 @@ describe('lib', () => {
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"name": "@proj/mylib", "name": "@proj/mylib",
"nx": {
"name": "mylib",
"projectType": "library",
"sourceRoot": "mylib/src",
},
"private": true, "private": true,
"types": "./src/index.ts", "types": "./src/index.ts",
"version": "0.0.1", "version": "0.0.1",
@ -684,9 +678,6 @@ describe('lib', () => {
"module": "./dist/index.js", "module": "./dist/index.js",
"name": "@proj/mylib", "name": "@proj/mylib",
"nx": { "nx": {
"name": "mylib",
"projectType": "library",
"sourceRoot": "mylib/src",
"targets": { "targets": {
"build": { "build": {
"executor": "@nx/js:swc", "executor": "@nx/js:swc",

View File

@ -54,6 +54,13 @@ export async function libraryGenerator(tree: Tree, schema: Schema) {
export async function libraryGeneratorInternal(tree: Tree, schema: Schema) { export async function libraryGeneratorInternal(tree: Tree, schema: Schema) {
const options = await normalizeOptions(tree, schema); const options = await normalizeOptions(tree, schema);
// If we are using the new TS solution
// We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project
if (options.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(tree, options.projectRoot);
}
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
if (options.publishable === true && !schema.importPath) { if (options.publishable === true && !schema.importPath) {
@ -112,12 +119,6 @@ export async function libraryGeneratorInternal(tree: Tree, schema: Schema) {
tasks.push(() => installPackagesTask(tree, true)); tasks.push(() => installPackagesTask(tree, true));
} }
// 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.projectRoot);
}
sortPackageJsonFields(tree, options.projectRoot); sortPackageJsonFields(tree, options.projectRoot);
if (!schema.skipFormat) { if (!schema.skipFormat) {
@ -163,14 +164,17 @@ async function normalizeOptions(
? options.tags.split(',').map((s) => s.trim()) ? options.tags.split(',').map((s) => s.trim())
: []; : [];
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(tree);
return { return {
...options, ...options,
fileName, fileName,
projectName, projectName: isUsingTsSolutionConfig
? getImportPath(tree, projectName)
: projectName,
projectRoot, projectRoot,
parsedTags, parsedTags,
importPath, importPath,
isUsingTsSolutionConfig: isUsingTsSolutionSetup(tree), isUsingTsSolutionConfig,
}; };
} }

View File

@ -190,7 +190,7 @@ async function normalizeOptions(
propertyName, propertyName,
description, description,
projectRoot, projectRoot,
projectSourceRoot, projectSourceRoot: projectSourceRoot ?? join(projectRoot, 'src'),
isTsSolutionSetup: isUsingTsSolutionSetup(tree), isTsSolutionSetup: isUsingTsSolutionSetup(tree),
}; };
} }

View File

@ -69,7 +69,7 @@ async function normalizeOptions(
propertyName, propertyName,
description, description,
projectRoot, projectRoot,
projectSourceRoot, projectSourceRoot: projectSourceRoot ?? join(projectRoot, 'src'),
isTsSolutionSetup: isUsingTsSolutionSetup(tree), isTsSolutionSetup: isUsingTsSolutionSetup(tree),
}; };
} }

View File

@ -57,7 +57,7 @@ async function normalizeOptions(
name, name,
description, description,
projectRoot, projectRoot,
projectSourceRoot, projectSourceRoot: projectSourceRoot ?? join(projectRoot, 'src'),
isTsSolutionSetup: isUsingTsSolutionSetup(tree), isTsSolutionSetup: isUsingTsSolutionSetup(tree),
}; };

View File

@ -330,6 +330,7 @@ describe('NxPlugin Plugin Generator', () => {
describe('TS solution setup', () => { describe('TS solution setup', () => {
beforeEach(() => { beforeEach(() => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
tree.write('.gitignore', '');
updateJson(tree, 'package.json', (json) => { updateJson(tree, 'package.json', (json) => {
json.workspaces = ['packages/*']; json.workspaces = ['packages/*'];
return json; return json;
@ -371,7 +372,7 @@ describe('NxPlugin Plugin Generator', () => {
swcJestConfig.swcrc = false; swcJestConfig.swcrc = false;
export default { export default {
displayName: 'my-plugin', displayName: '@proj/my-plugin',
preset: '../jest.preset.js', preset: '../jest.preset.js',
testEnvironment: 'node', testEnvironment: 'node',
transform: { transform: {
@ -412,11 +413,119 @@ describe('NxPlugin Plugin Generator', () => {
const projectTargets = readProjectConfiguration( const projectTargets = readProjectConfiguration(
tree, tree,
'my-plugin' '@proj/my-plugin'
).targets; ).targets;
expect(projectTargets.test).toBeDefined(); expect(projectTargets.test).toBeDefined();
expect(projectTargets.test?.executor).toEqual('@nx/jest:jest'); expect(projectTargets.test?.executor).toEqual('@nx/jest:jest');
}); });
it('should add project references when using TS solution', async () => {
await pluginGenerator(
tree,
getSchema({
e2eTestRunner: 'jest',
})
);
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(`
[
{
"path": "./my-plugin",
},
{
"path": "./my-plugin-e2e",
},
]
`);
expect(readJson(tree, 'my-plugin/package.json')).toMatchInlineSnapshot(`
{
"dependencies": {
"tslib": "^2.3.0",
},
"exports": {
".": {
"default": "./dist/index.js",
"import": "./dist/index.js",
"types": "./dist/index.d.ts",
},
"./package.json": "./package.json",
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"name": "@proj/my-plugin",
"types": "./dist/index.d.ts",
"version": "0.0.1",
}
`);
expect(readJson(tree, 'my-plugin/tsconfig.json')).toMatchInlineSnapshot(`
{
"extends": "../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json",
},
{
"path": "./tsconfig.spec.json",
},
],
}
`);
expect(readJson(tree, 'my-plugin/tsconfig.lib.json'))
.toMatchInlineSnapshot(`
{
"compilerOptions": {
"baseUrl": ".",
"emitDeclarationOnly": false,
"module": "nodenext",
"moduleResolution": "nodenext",
"outDir": "dist",
"rootDir": "src",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"types": [
"node",
],
},
"exclude": [
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts",
],
"extends": "../tsconfig.base.json",
"include": [
"src/**/*.ts",
],
"references": [],
}
`);
expect(readJson(tree, 'my-plugin/tsconfig.spec.json'))
.toMatchInlineSnapshot(`
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"outDir": "./out-tsc/jest",
"types": [
"jest",
"node",
],
},
"extends": "../tsconfig.base.json",
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts",
],
"references": [
{
"path": "./tsconfig.lib.json",
},
],
}
`);
});
}); });
}); });

View File

@ -41,11 +41,12 @@ async function addFiles(host: Tree, options: NormalizedSchema) {
} }
function updatePluginConfig(host: Tree, options: NormalizedSchema) { function updatePluginConfig(host: Tree, options: NormalizedSchema) {
const project = readProjectConfiguration(host, options.name); const project = readProjectConfiguration(host, options.projectName);
if (project.targets.build) { if (project.targets.build) {
if (options.isTsSolutionSetup && options.bundler === 'tsc') { if (options.isTsSolutionSetup && options.bundler === 'tsc') {
project.targets.build.options.rootDir = project.sourceRoot; project.targets.build.options.rootDir =
project.sourceRoot ?? joinPathFragments(project.root, 'src');
project.targets.build.options.generatePackageJson = false; project.targets.build.options.generatePackageJson = false;
} }
@ -69,7 +70,7 @@ function updatePluginConfig(host: Tree, options: NormalizedSchema) {
); );
} }
updateProjectConfiguration(host, options.name, project); updateProjectConfiguration(host, options.projectName, project);
} }
} }
@ -144,7 +145,7 @@ export async function pluginGeneratorInternal(host: Tree, schema: Schema) {
if (options.e2eTestRunner !== 'none') { if (options.e2eTestRunner !== 'none') {
tasks.push( tasks.push(
await e2eProjectGenerator(host, { await e2eProjectGenerator(host, {
pluginName: options.name, pluginName: options.projectName,
projectDirectory: options.projectDirectory, projectDirectory: options.projectDirectory,
pluginOutputPath: joinPathFragments( pluginOutputPath: joinPathFragments(
'dist', 'dist',
@ -161,7 +162,7 @@ export async function pluginGeneratorInternal(host: Tree, schema: Schema) {
} }
if (options.linter === Linter.EsLint && !options.skipLintChecks) { if (options.linter === Linter.EsLint && !options.skipLintChecks) {
await pluginLintCheckGenerator(host, { projectName: options.name }); await pluginLintCheckGenerator(host, { projectName: options.projectName });
} }
if (!options.skipFormat) { if (!options.skipFormat) {

View File

@ -10,9 +10,11 @@ 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; name: string;
projectName: string;
fileName: string; fileName: string;
projectRoot: string; projectRoot: string;
projectDirectory: string; projectDirectory: string;
@ -71,6 +73,9 @@ export async function normalizeOptions(
bundler: options.compiler ?? 'tsc', bundler: options.compiler ?? 'tsc',
fileName: projectName, fileName: projectName,
name: projectName, name: projectName,
projectName: isTsSolutionSetup
? getImportPath(host, projectName)
: projectName,
projectRoot, projectRoot,
projectDirectory, projectDirectory,
parsedTags, parsedTags,

View File

@ -82,13 +82,14 @@ export async function reactNativeApplicationGeneratorInternal(
options.appProjectRoot, options.appProjectRoot,
options.js, options.js,
options.skipPackageJson, options.skipPackageJson,
options.addPlugin options.addPlugin,
'tsconfig.app.json'
); );
tasks.push(jestTask); tasks.push(jestTask);
const webTask = await webConfigurationGenerator(host, { const webTask = await webConfigurationGenerator(host, {
...options, ...options,
project: options.name, project: options.projectName,
skipFormat: true, skipFormat: true,
}); });
tasks.push(webTask); tasks.push(webTask);

View File

@ -29,13 +29,10 @@ export function addProject(host: Tree, options: NormalizedSchema) {
if (isUsingTsSolutionSetup(host)) { if (isUsingTsSolutionSetup(host)) {
writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), { writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), {
name: getImportPath(host, options.name), name: options.projectName,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: {
name: options.name,
projectType: 'application',
sourceRoot: `${options.appProjectRoot}/src`,
targets: hasPlugin ? {} : getTargets(options), targets: hasPlugin ? {} : getTargets(options),
tags: options.parsedTags?.length ? options.parsedTags : undefined, tags: options.parsedTags?.length ? options.parsedTags : undefined,
}, },

View File

@ -5,6 +5,7 @@ import {
} 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 {
className: string; // app name in class case className: string; // app name in class case
@ -56,6 +57,8 @@ 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,
@ -63,7 +66,9 @@ export async function normalizeOptions(
fileName, fileName,
lowerCaseName: className.toLowerCase(), lowerCaseName: className.toLowerCase(),
displayName: options.displayName || className, displayName: options.displayName || className,
projectName: appProjectName, projectName: isTsSolutionSetup
? getImportPath(host, appProjectName)
: appProjectName,
appProjectRoot, appProjectRoot,
iosProjectRoot, iosProjectRoot,
androidProjectRoot, androidProjectRoot,
@ -72,6 +77,6 @@ export async function normalizeOptions(
rootProject, rootProject,
e2eProjectName, e2eProjectName,
e2eProjectRoot, e2eProjectRoot,
isTsSolutionSetup: isUsingTsSolutionSetup(host), isTsSolutionSetup,
}; };
} }

View File

@ -11,6 +11,7 @@ import { NormalizedSchema, normalizeOptions } from './lib/normalize-options';
import { addImport } from './lib/add-import'; import { addImport } from './lib/add-import';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import { dirname, join, parse, relative } from 'path'; import { dirname, join, parse, relative } from 'path';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function reactNativeComponentGenerator( export async function reactNativeComponentGenerator(
host: Tree, host: Tree,
@ -52,8 +53,9 @@ function addExportsToBarrel(host: Tree, options: NormalizedSchema) {
tsModule = ensureTypescript(); tsModule = ensureTypescript();
} }
const workspace = getProjects(host); const workspace = getProjects(host);
const proj = workspace.get(options.projectName);
const isApp = const isApp =
workspace.get(options.projectName).projectType === 'application'; getProjectType(host, proj.root, proj.projectType) === 'application';
if (options.export && !isApp) { if (options.export && !isApp) {
const indexFilePath = joinPathFragments( const indexFilePath = joinPathFragments(

View File

@ -4,6 +4,7 @@ import {
type FileExtensionType, type FileExtensionType,
} from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; } from '@nx/devkit/src/generators/artifact-name-and-directory-utils';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
export interface NormalizedSchema extends Omit<Schema, 'js'> { export interface NormalizedSchema extends Omit<Schema, 'js'> {
directory: string; directory: string;
@ -40,9 +41,12 @@ export async function normalizeOptions(
const { className } = names(name); const { className } = names(name);
const { sourceRoot: projectSourceRoot, projectType } = project; const { root, sourceRoot: projectSourceRoot, projectType } = project;
if (options.export && projectType === 'application') { if (
options.export &&
getProjectType(host, root, projectType) === 'application'
) {
logger.warn( logger.warn(
`The "--export" option should not be used with applications and will do nothing.` `The "--export" option should not be used with applications and will do nothing.`
); );

View File

@ -5,6 +5,7 @@ import {
} 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;
@ -44,15 +45,18 @@ export async function normalizeOptions(
? options.tags.split(',').map((s) => s.trim()) ? options.tags.split(',').map((s) => s.trim())
: []; : [];
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
const normalized: NormalizedSchema = { const normalized: NormalizedSchema = {
...options, ...options,
fileName: projectName, fileName: projectName,
routePath: `/${projectNames.projectSimpleName}`, routePath: `/${projectNames.projectSimpleName}`,
name: projectName, name: isUsingTsSolutionConfig
? getImportPath(host, projectName)
: projectName,
projectRoot, projectRoot,
parsedTags, parsedTags,
importPath, importPath,
isUsingTsSolutionConfig: isUsingTsSolutionSetup(host), isUsingTsSolutionConfig,
}; };
return normalized; return normalized;

View File

@ -101,7 +101,8 @@ export async function reactNativeLibraryGeneratorInternal(
options.projectRoot, options.projectRoot,
options.js, options.js,
options.skipPackageJson, options.skipPackageJson,
options.addPlugin options.addPlugin,
'tsconfig.lib.json'
); );
tasks.push(jestTask); tasks.push(jestTask);
@ -171,14 +172,11 @@ async function addProject(
'package.json' 'package.json'
); );
if (options.isUsingTsSolutionConfig) { if (options.isUsingTsSolutionConfig) {
writeJson(host, packageJsonPath, { writeJson(host, joinPathFragments(options.projectRoot, 'package.json'), {
name: getImportPath(host, options.name), name: options.name,
version: '0.0.1', version: '0.0.1',
...determineEntryFields(options), ...determineEntryFields(options),
nx: { nx: {
name: options.name,
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
projectType: 'library',
tags: options.parsedTags?.length ? options.parsedTags : undefined, tags: options.parsedTags?.length ? options.parsedTags : undefined,
}, },
}); });

View File

@ -8,7 +8,8 @@ export async function addJest(
appProjectRoot: string, appProjectRoot: string,
js: boolean, js: boolean,
skipPackageJson: boolean, skipPackageJson: boolean,
addPlugin: boolean addPlugin: boolean,
runtimeTsconfigFileName: string
) { ) {
if (unitTestRunner !== 'jest') { if (unitTestRunner !== 'jest') {
return () => {}; return () => {};
@ -24,6 +25,7 @@ export async function addJest(
skipPackageJson, skipPackageJson,
skipFormat: true, skipFormat: true,
addPlugin, addPlugin,
runtimeTsconfigFileName,
}); });
// overwrite the jest.config.ts file because react native needs to have special transform property // overwrite the jest.config.ts file because react native needs to have special transform property

View File

@ -1331,7 +1331,6 @@ describe('app', () => {
"name", "name",
"version", "version",
"private", "private",
"nx",
] ]
`); `);
expect(readJson(appTree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(` expect(readJson(appTree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(`

View File

@ -72,6 +72,13 @@ export async function applicationGeneratorInternal(
tasks.push(jsInitTask); tasks.push(jsInitTask);
const options = await normalizeOptions(tree, schema); const options = await normalizeOptions(tree, schema);
// If we are using the new TS solution
// We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project
if (options.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(tree, options.appProjectRoot);
}
showPossibleWarnings(tree, options); showPossibleWarnings(tree, options);
const initTask = await reactInitGenerator(tree, { const initTask = await reactInitGenerator(tree, {
@ -179,12 +186,6 @@ export async function applicationGeneratorInternal(
: 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.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(tree, options.appProjectRoot);
}
sortPackageJsonFields(tree, options.appProjectRoot); sortPackageJsonFields(tree, options.appProjectRoot);
if (!options.skipFormat) { if (!options.skipFormat) {

View File

@ -40,15 +40,14 @@ export function addProject(host: Tree, options: NormalizedSchema) {
if (options.isUsingTsSolutionConfig) { if (options.isUsingTsSolutionConfig) {
writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), { writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), {
name: getImportPath(host, options.name), name: options.projectName,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: options.parsedTags?.length
name: options.name, ? {
projectType: 'application', tags: options.parsedTags,
sourceRoot: `${options.appProjectRoot}/src`, }
tags: options.parsedTags?.length ? options.parsedTags : undefined, : undefined,
},
}); });
} }

View File

@ -7,6 +7,7 @@ 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) { export function normalizeDirectory(options: Schema) {
options.directory = options.directory?.replace(/\\{1,2}/g, '/'); options.directory = options.directory?.replace(/\\{1,2}/g, '/');
@ -57,10 +58,13 @@ 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: names(options.name).fileName, name: appProjectName,
projectName: appProjectName, projectName: isUsingTsSolutionConfig
? getImportPath(host, appProjectName)
: appProjectName,
appProjectRoot, appProjectRoot,
e2eProjectName, e2eProjectName,
e2eProjectRoot, e2eProjectRoot,
@ -68,7 +72,7 @@ export async function normalizeOptions<T extends Schema = Schema>(
fileName, fileName,
styledModule, styledModule,
hasStyles: options.style !== 'none', hasStyles: options.style !== 'none',
isUsingTsSolutionConfig: isUsingTsSolutionSetup(host), isUsingTsSolutionConfig,
} as NormalizedSchema; } as NormalizedSchema;
normalized.routing = normalized.routing ?? false; normalized.routing = normalized.routing ?? false;

View File

@ -4,6 +4,7 @@ import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generat
import { assertValidStyle } from '../../../utils/assertion'; import { assertValidStyle } from '../../../utils/assertion';
import { NormalizedSchema, Schema } from '../schema'; import { NormalizedSchema, Schema } from '../schema';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function normalizeOptions( export async function normalizeOptions(
tree: Tree, tree: Tree,
@ -41,7 +42,10 @@ export async function normalizeOptions(
? null ? null
: options.style; : options.style;
if (options.export && projectType === 'application') { if (
options.export &&
getProjectType(tree, projectRoot, projectType) === 'application'
) {
logger.warn( logger.warn(
`The "--export" option should not be used with applications and will do nothing.` `The "--export" option should not be used with applications and will do nothing.`
); );

View File

@ -17,6 +17,7 @@ import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'
import { join } from 'path'; import { join } from 'path';
import { addImport } from '../../utils/ast-utils'; import { addImport } from '../../utils/ast-utils';
import { Schema } from './schema'; import { Schema } from './schema';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
interface NormalizedSchema extends Omit<Schema, 'js'> { interface NormalizedSchema extends Omit<Schema, 'js'> {
projectSourceRoot: string; projectSourceRoot: string;
@ -127,9 +128,12 @@ async function normalizeOptions(
const hookTypeName = names(hookName).className; const hookTypeName = names(hookName).className;
const project = getProjects(host).get(projectName); const project = getProjects(host).get(projectName);
const { sourceRoot: projectSourceRoot, projectType } = project; const { root, sourceRoot: projectSourceRoot, projectType } = project;
if (options.export && projectType === 'application') { if (
options.export &&
getProjectType(host, root, projectType) === 'application'
) {
logger.warn( logger.warn(
`The "--export" option should not be used with applications and will do nothing.` `The "--export" option should not be used with applications and will do nothing.`
); );

View File

@ -1,4 +1,3 @@
import * as devkit from '@nx/devkit';
import { Tree, updateJson, writeJson } from '@nx/devkit'; import { Tree, updateJson, writeJson } from '@nx/devkit';
import { ProjectGraph, readJson } from '@nx/devkit'; import { ProjectGraph, readJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

View File

@ -64,7 +64,7 @@ export async function hostGenerator(
const initTask = await applicationGenerator(host, { const initTask = await applicationGenerator(host, {
...options, ...options,
directory: options.appProjectRoot, directory: options.appProjectRoot,
name: options.projectName, name: options.name,
// The target use-case is loading remotes as child routes, thus always enable routing. // The target use-case is loading remotes as child routes, thus always enable routing.
routing: true, routing: true,
skipFormat: true, skipFormat: true,

View File

@ -70,7 +70,7 @@ export async function normalizeOptions(
bundler, bundler,
fileName, fileName,
routePath: `/${projectNames.projectSimpleName}`, routePath: `/${projectNames.projectSimpleName}`,
name: projectName, name: isUsingTsSolutionConfig ? importPath : projectName,
projectRoot, projectRoot,
parsedTags, parsedTags,
importPath, importPath,

View File

@ -932,7 +932,7 @@ module.exports = withNx(
beforeEach(() => { beforeEach(() => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
updateJson(tree, 'package.json', (json) => { updateJson(tree, 'package.json', (json) => {
json.workspaces = ['packages/*', 'apps/*']; json.workspaces = ['packages/*', 'apps/*', 'libs/*'];
return json; return json;
}); });
writeJson(tree, 'tsconfig.base.json', { writeJson(tree, 'tsconfig.base.json', {
@ -953,11 +953,11 @@ module.exports = withNx(
...defaultSchema, ...defaultSchema,
bundler: 'vite', bundler: 'vite',
unitTestRunner: 'vitest', unitTestRunner: 'vitest',
directory: 'mylib', directory: 'libs/mylib',
name: 'mylib',
}); });
expect(tree.read('mylib/vite.config.ts', 'utf-8')).toMatchInlineSnapshot(` expect(tree.read('libs/mylib/vite.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"/// <reference types='vitest' /> "/// <reference types='vitest' />
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
@ -966,7 +966,7 @@ module.exports = withNx(
export default defineConfig({ export default defineConfig({
root: __dirname, root: __dirname,
cacheDir: '../node_modules/.vite/mylib', cacheDir: '../../node_modules/.vite/libs/mylib',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })], plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers. // Uncomment this if you are using workers.
// worker: { // worker: {
@ -984,7 +984,7 @@ module.exports = withNx(
lib: { lib: {
// Could also be a dictionary or array of multiple entry points. // Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts', entry: 'src/index.ts',
name: 'mylib', name: '@proj/mylib',
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.
@ -1013,12 +1013,12 @@ module.exports = withNx(
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(`
[ [
{ {
"path": "./mylib", "path": "./libs/mylib",
}, },
] ]
`); `);
// Make sure keys are in idiomatic order // Make sure keys are in idiomatic order
expect(Object.keys(readJson(tree, 'mylib/package.json'))) expect(Object.keys(readJson(tree, 'libs/mylib/package.json')))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
[ [
"name", "name",
@ -1028,12 +1028,11 @@ module.exports = withNx(
"module", "module",
"types", "types",
"exports", "exports",
"nx",
] ]
`); `);
expect(readJson(tree, 'mylib/tsconfig.json')).toMatchInlineSnapshot(` expect(readJson(tree, 'libs/mylib/tsconfig.json')).toMatchInlineSnapshot(`
{ {
"extends": "../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"files": [], "files": [],
"include": [], "include": [],
"references": [ "references": [
@ -1046,7 +1045,8 @@ module.exports = withNx(
], ],
} }
`); `);
expect(readJson(tree, 'mylib/tsconfig.lib.json')).toMatchInlineSnapshot(` expect(readJson(tree, 'libs/mylib/tsconfig.lib.json'))
.toMatchInlineSnapshot(`
{ {
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx",
@ -1089,7 +1089,7 @@ module.exports = withNx(
"eslint.config.cjs", "eslint.config.cjs",
"eslint.config.mjs", "eslint.config.mjs",
], ],
"extends": "../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"include": [ "include": [
"src/**/*.js", "src/**/*.js",
"src/**/*.jsx", "src/**/*.jsx",
@ -1098,7 +1098,8 @@ module.exports = withNx(
], ],
} }
`); `);
expect(readJson(tree, 'mylib/tsconfig.spec.json')).toMatchInlineSnapshot(` expect(readJson(tree, 'libs/mylib/tsconfig.spec.json'))
.toMatchInlineSnapshot(`
{ {
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx",
@ -1113,7 +1114,7 @@ module.exports = withNx(
"vitest", "vitest",
], ],
}, },
"extends": "../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"include": [ "include": [
"vite.config.ts", "vite.config.ts",
"vite.config.mts", "vite.config.mts",
@ -1143,20 +1144,18 @@ module.exports = withNx(
...defaultSchema, ...defaultSchema,
bundler: 'none', bundler: 'none',
unitTestRunner: 'none', unitTestRunner: 'none',
directory: 'mylib', directory: 'libs/mylib',
name: 'mylib',
}); });
await libraryGenerator(tree, { await libraryGenerator(tree, {
...defaultSchema, ...defaultSchema,
bundler: 'none', bundler: 'none',
unitTestRunner: 'none', unitTestRunner: 'none',
directory: 'myjslib', directory: 'libs/myjslib',
name: 'myjslib',
js: true, js: true,
}); });
expect(readJson(tree, 'mylib/package.json')).toMatchInlineSnapshot(` expect(readJson(tree, 'libs/mylib/package.json')).toMatchInlineSnapshot(`
{ {
"exports": { "exports": {
".": { ".": {
@ -1168,16 +1167,12 @@ module.exports = withNx(
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"name": "@proj/mylib", "name": "@proj/mylib",
"nx": {
"name": "mylib",
"projectType": "library",
"sourceRoot": "mylib/src",
},
"types": "./src/index.ts", "types": "./src/index.ts",
"version": "0.0.1", "version": "0.0.1",
} }
`); `);
expect(readJson(tree, 'myjslib/package.json')).toMatchInlineSnapshot(` expect(readJson(tree, 'libs/myjslib/package.json'))
.toMatchInlineSnapshot(`
{ {
"exports": { "exports": {
".": "./src/index.js", ".": "./src/index.js",
@ -1185,11 +1180,6 @@ module.exports = withNx(
}, },
"main": "./src/index.js", "main": "./src/index.js",
"name": "@proj/myjslib", "name": "@proj/myjslib",
"nx": {
"name": "myjslib",
"projectType": "library",
"sourceRoot": "myjslib/src",
},
"types": "./src/index.js", "types": "./src/index.js",
"version": "0.0.1", "version": "0.0.1",
} }
@ -1201,11 +1191,10 @@ module.exports = withNx(
...defaultSchema, ...defaultSchema,
bundler: 'rollup', bundler: 'rollup',
unitTestRunner: 'none', unitTestRunner: 'none',
directory: 'mylib', directory: 'libs/mylib',
name: 'mylib',
}); });
expect(tree.read('mylib/rollup.config.cjs', 'utf-8')) expect(tree.read('libs/mylib/rollup.config.cjs', 'utf-8'))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"const { withNx } = require('@nx/rollup/with-nx'); "const { withNx } = require('@nx/rollup/with-nx');
const url = require('@rollup/plugin-url'); const url = require('@rollup/plugin-url');
@ -1242,13 +1231,12 @@ module.exports = withNx(
await libraryGenerator(tree, { await libraryGenerator(tree, {
...defaultSchema, ...defaultSchema,
bundler: 'rollup', bundler: 'rollup',
directory: 'mylib', directory: 'libs/mylib',
name: 'mylib',
publishable: true, publishable: true,
importPath: '@acme/mylib', importPath: '@acme/mylib',
}); });
expect(readJson(tree, 'mylib/package.json')).toMatchInlineSnapshot(` expect(readJson(tree, 'libs/mylib/package.json')).toMatchInlineSnapshot(`
{ {
"exports": { "exports": {
".": { ".": {
@ -1265,11 +1253,6 @@ module.exports = withNx(
"main": "./dist/index.esm.js", "main": "./dist/index.esm.js",
"module": "./dist/index.esm.js", "module": "./dist/index.esm.js",
"name": "@acme/mylib", "name": "@acme/mylib",
"nx": {
"name": "mylib",
"projectType": "library",
"sourceRoot": "mylib/src",
},
"type": "module", "type": "module",
"types": "./dist/index.esm.d.ts", "types": "./dist/index.esm.d.ts",
"version": "0.0.1", "version": "0.0.1",

View File

@ -55,6 +55,11 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) {
tasks.push(jsInitTask); tasks.push(jsInitTask);
const options = await normalizeOptions(host, schema); const options = await normalizeOptions(host, schema);
if (options.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(host, options.projectRoot);
}
if (options.publishable === true && !schema.importPath) { if (options.publishable === true && !schema.importPath) {
throw new Error( throw new Error(
`For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)` `For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)`
@ -75,12 +80,11 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) {
name: options.importPath, name: options.importPath,
version: '0.0.1', version: '0.0.1',
...determineEntryFields(options), ...determineEntryFields(options),
nx: { nx: options.parsedTags?.length
name: options.importPath === options.name ? undefined : options.name, ? {
projectType: 'library', tags: options.parsedTags,
sourceRoot: `${options.projectRoot}/src`, }
tags: options.parsedTags?.length ? options.parsedTags : undefined, : undefined,
},
files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined, files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined,
}); });
} else { } else {
@ -273,10 +277,6 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) {
: undefined : undefined
); );
if (options.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(host, options.projectRoot);
}
sortPackageJsonFields(host, options.projectRoot); sortPackageJsonFields(host, options.projectRoot);
if (!options.skipFormat) { if (!options.skipFormat) {

View File

@ -20,6 +20,7 @@ import {
import { getRootTsConfigPathInTree } from '@nx/js'; import { getRootTsConfigPathInTree } from '@nx/js';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
let tsModule: typeof import('typescript'); let tsModule: typeof import('typescript');
@ -165,14 +166,14 @@ async function normalizeOptions(
const projects = getProjects(host); const projects = getProjects(host);
const project = projects.get(projectName); const project = projects.get(projectName);
const { sourceRoot, projectType } = project; const { root, sourceRoot, projectType } = project;
const tsConfigJson = readJson(host, getRootTsConfigPathInTree(host)); const tsConfigJson = readJson(host, getRootTsConfigPathInTree(host));
const tsPaths: { [module: string]: string[] } = tsConfigJson.compilerOptions const tsPaths: { [module: string]: string[] } = tsConfigJson.compilerOptions
? tsConfigJson.compilerOptions.paths || {} ? tsConfigJson.compilerOptions.paths || {}
: {}; : {};
const modulePath = const modulePath =
projectType === 'application' getProjectType(host, root, projectType) === 'application'
? options.path ? options.path
? `./app/${options.path}/${extraNames.fileName}.slice` ? `./app/${options.path}/${extraNames.fileName}.slice`
: `./app/${extraNames.fileName}.slice` : `./app/${extraNames.fileName}.slice`

View File

@ -111,10 +111,10 @@ 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.projectName); const isValidRemote = isValidVariable(options.name);
if (!isValidRemote.isValid) { if (!isValidRemote.isValid) {
throw new Error( throw new Error(
`Invalid remote name provided: ${options.projectName}. ${isValidRemote.message}` `Invalid remote name provided: ${options.name}. ${isValidRemote.message}`
); );
} }
} }
@ -122,9 +122,9 @@ export async function remoteGenerator(host: Tree, schema: Schema) {
await ensureProjectName(host, options, 'application'); await ensureProjectName(host, 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.projectName)) { if (!remoteNameRegex.test(options.name)) {
throw new Error( throw new Error(
stripIndents`Invalid remote name: ${options.projectName}. Remote project names must: stripIndents`Invalid remote name: ${options.name}. 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 +132,14 @@ export async function remoteGenerator(host: Tree, schema: Schema) {
} }
const initAppTask = await applicationGenerator(host, { const initAppTask = await applicationGenerator(host, {
...options, ...options,
name: options.projectName, name: options.name,
skipFormat: true, skipFormat: true,
alwaysGenerateProjectJson: true, alwaysGenerateProjectJson: true,
}); });
tasks.push(initAppTask); tasks.push(initAppTask);
if (schema.host) { if (options.host) {
updateHostWithRemote(host, schema.host, options.projectName); updateHostWithRemote(host, options.host, options.name);
} }
// Module federation requires bootstrap code to be dynamically imported. // Module federation requires bootstrap code to be dynamically imported.

View File

@ -19,6 +19,7 @@ import { basename, join } from 'path';
import { minimatch } from 'minimatch'; import { minimatch } from 'minimatch';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import { nxVersion } from '../../utils/versions'; import { nxVersion } from '../../utils/versions';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
let tsModule: typeof import('typescript'); let tsModule: typeof import('typescript');
@ -35,7 +36,7 @@ export async function projectRootPath(
config: ProjectConfiguration config: ProjectConfiguration
): Promise<string> { ): Promise<string> {
let projectDir: string; let projectDir: string;
if (config.projectType === 'application') { if (getProjectType(tree, config.root, config.projectType) === 'application') {
const isNextJs = await isNextJsProject(tree, config); const isNextJs = await isNextJsProject(tree, config);
if (isNextJs) { if (isNextJs) {
// Next.js apps // Next.js apps

View File

@ -379,10 +379,8 @@ describe('Remix Application', () => {
"engines": { "engines": {
"node": ">=20", "node": ">=20",
}, },
"name": "myapp", "name": "@proj/myapp",
"nx": { "nx": {
"projectType": "application",
"sourceRoot": "myapp",
"tags": [ "tags": [
"foo", "foo",
], ],
@ -541,6 +539,41 @@ describe('Remix Application', () => {
`); `);
}); });
it('should skip nx property in package.json when no tags are provided', async () => {
await applicationGenerator(tree, {
directory: 'apps/myapp',
e2eTestRunner: 'playwright',
unitTestRunner: 'jest',
addPlugin: true,
});
expect(readJson(tree, 'apps/myapp/package.json')).toMatchInlineSnapshot(`
{
"dependencies": {
"@remix-run/node": "^2.14.0",
"@remix-run/react": "^2.14.0",
"@remix-run/serve": "^2.14.0",
"isbot": "^4.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
},
"devDependencies": {
"@remix-run/dev": "^2.14.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
},
"engines": {
"node": ">=20",
},
"name": "@proj/myapp",
"private": true,
"scripts": {},
"sideEffects": false,
"type": "module",
}
`);
});
it('should generate valid package.json without formatting', async () => { it('should generate valid package.json without formatting', async () => {
await applicationGenerator(tree, { await applicationGenerator(tree, {
directory: 'myapp', directory: 'myapp',

View File

@ -12,12 +12,10 @@ import {
Tree, Tree,
updateJson, updateJson,
updateProjectConfiguration, updateProjectConfiguration,
visitNotIgnoredFiles,
} from '@nx/devkit'; } from '@nx/devkit';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { initGenerator as jsInitGenerator } from '@nx/js'; import { initGenerator as jsInitGenerator } from '@nx/js';
import { extractTsConfigBase } from '@nx/js/src/utils/typescript/create-ts-config'; import { extractTsConfigBase } from '@nx/js/src/utils/typescript/create-ts-config';
import { dirname } from 'node:path';
import { import {
createNxCloudOnboardingURLForWelcomeApp, createNxCloudOnboardingURLForWelcomeApp,
getNxCloudAppOnBoardingUrl, getNxCloudAppOnBoardingUrl,
@ -39,14 +37,13 @@ import initGenerator from '../init/init';
import { updateDependencies } from '../utils/update-dependencies'; import { updateDependencies } from '../utils/update-dependencies';
import { import {
addE2E, addE2E,
addViteTempFilesToGitIgnore,
normalizeOptions, normalizeOptions,
updateUnitTestConfig, updateUnitTestConfig,
addViteTempFilesToGitIgnore,
} from './lib'; } from './lib';
import { NxRemixGeneratorSchema } from './schema'; import { NxRemixGeneratorSchema } from './schema';
import { import {
addProjectToTsSolutionWorkspace, addProjectToTsSolutionWorkspace,
isUsingTsSolutionSetup,
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';
@ -85,9 +82,13 @@ export async function remixApplicationGeneratorInternal(
); );
} }
const isUsingTsSolution = isUsingTsSolutionSetup(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.projectRoot);
}
if (!isUsingTsSolution) { if (!options.isUsingTsSolutionConfig) {
addProjectConfiguration(tree, options.projectName, { addProjectConfiguration(tree, options.projectName, {
root: options.projectRoot, root: options.projectRoot,
sourceRoot: `${options.projectRoot}`, sourceRoot: `${options.projectRoot}`,
@ -122,7 +123,6 @@ export async function remixApplicationGeneratorInternal(
eslintVersion, eslintVersion,
typescriptVersion, typescriptVersion,
viteVersion, viteVersion,
isUsingTsSolution,
}; };
generateFiles( generateFiles(
@ -154,7 +154,7 @@ export async function remixApplicationGeneratorInternal(
); );
} }
if (isUsingTsSolution) { if (options.isUsingTsSolutionConfig) {
generateFiles( generateFiles(
tree, tree,
joinPathFragments(__dirname, 'files/ts-solution'), joinPathFragments(__dirname, 'files/ts-solution'),
@ -328,12 +328,6 @@ export default {...nxPreset};
'.' '.'
); );
// 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(tree, options.projectRoot);
}
sortPackageJsonFields(tree, options.projectRoot); sortPackageJsonFields(tree, options.projectRoot);
if (!options.skipFormat) { if (!options.skipFormat) {

View File

@ -19,13 +19,8 @@
"engines": { "engines": {
"node": ">=20" "node": ">=20"
}, },
"sideEffects": false<% if (isUsingTsSolution) { %>, "sideEffects": false<% if (isUsingTsSolutionConfig && parsedTags?.length) { %>,
"nx": { "nx": {
<%_ if (name !== projectName) { _%>
"name": "<%= name %>",<%_ } _%>
"projectType": "application",
"sourceRoot": "<%- projectRoot %>"<%_ if (parsedTags?.length) { _%>,
"tags": <%- JSON.stringify(parsedTags) %> "tags": <%- JSON.stringify(parsedTags) %>
<%_ } _%>
}<% } %> }<% } %>
} }

View File

@ -6,6 +6,7 @@ import {
import { Linter } from '@nx/eslint'; import { Linter } from '@nx/eslint';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { type NxRemixGeneratorSchema } from '../schema'; import { type NxRemixGeneratorSchema } from '../schema';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export interface NormalizedSchema extends NxRemixGeneratorSchema { export interface NormalizedSchema extends NxRemixGeneratorSchema {
projectName: string; projectName: string;
@ -13,6 +14,7 @@ export interface NormalizedSchema extends NxRemixGeneratorSchema {
e2eProjectName: string; e2eProjectName: string;
e2eProjectRoot: string; e2eProjectRoot: string;
parsedTags: string[]; parsedTags: string[];
isUsingTsSolutionConfig: boolean;
} }
export async function normalizeOptions( export async function normalizeOptions(
@ -43,14 +45,18 @@ export async function normalizeOptions(
? options.tags.split(',').map((s) => s.trim()) ? options.tags.split(',').map((s) => s.trim())
: []; : [];
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(tree);
return { return {
...options, ...options,
linter: options.linter ?? Linter.EsLint, linter: options.linter ?? Linter.EsLint,
projectName, projectName: isUsingTsSolutionConfig
? getImportPath(tree, projectName)
: projectName,
projectRoot, projectRoot,
e2eProjectName, e2eProjectName,
e2eProjectRoot, e2eProjectRoot,
parsedTags, parsedTags,
useTsSolution: options.useTsSolution ?? isUsingTsSolutionSetup(tree), useTsSolution: options.useTsSolution ?? isUsingTsSolutionConfig,
isUsingTsSolutionConfig,
}; };
} }

View File

@ -16,7 +16,7 @@ export function addTsconfigEntryPoints(
tree, tree,
options.projectName options.projectName
); );
const serverFilePath = joinPathFragments(sourceRoot, 'server.ts'); const serverFilePath = joinPathFragments(sourceRoot ?? 'src', 'server.ts');
tree.write( tree.write(
serverFilePath, serverFilePath,

View File

@ -5,6 +5,7 @@ import {
} from '@nx/devkit/src/generators/project-name-and-root-utils'; } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { NxRemixGeneratorSchema } from '../schema'; import type { NxRemixGeneratorSchema } 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 RemixLibraryOptions extends NxRemixGeneratorSchema { export interface RemixLibraryOptions extends NxRemixGeneratorSchema {
projectName: string; projectName: string;
@ -32,11 +33,14 @@ export async function normalizeOptions(
nxJson.useInferencePlugins !== false; nxJson.useInferencePlugins !== false;
options.addPlugin ??= addPluginDefault; options.addPlugin ??= addPluginDefault;
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(tree);
return { return {
...options, ...options,
unitTestRunner: options.unitTestRunner ?? 'vitest', unitTestRunner: options.unitTestRunner ?? 'vitest',
projectName, projectName: isUsingTsSolutionConfig
? getImportPath(tree, projectName)
: projectName,
projectRoot, projectRoot,
isUsingTsSolutionConfig: isUsingTsSolutionSetup(tree), isUsingTsSolutionConfig,
}; };
} }

View File

@ -173,7 +173,6 @@ describe('Remix Library Generator', () => {
"main", "main",
"types", "types",
"exports", "exports",
"nx",
] ]
`); `);
expect(readJson(tree, 'packages/foo/package.json')) expect(readJson(tree, 'packages/foo/package.json'))
@ -189,15 +188,27 @@ describe('Remix Library Generator', () => {
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"name": "@proj/foo", "name": "@proj/foo",
"nx": {
"name": "foo",
"projectType": "library",
"sourceRoot": "packages/foo/src",
},
"types": "./src/index.ts", "types": "./src/index.ts",
"version": "0.0.1", "version": "0.0.1",
} }
`); `);
}); });
it('should generate server entrypoint', async () => {
await libraryGenerator(tree, {
directory: 'test',
style: 'css',
addPlugin: true,
});
expect(tree.exists(`test/src/server.ts`));
expect(tree.children(`test/src/lib`).sort()).toMatchInlineSnapshot(`
[
"test.module.css",
"test.spec.tsx",
"test.tsx",
]
`);
}, 25_000);
}); });
}); });

View File

@ -29,6 +29,10 @@ export async function remixLibraryGeneratorInternal(
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const options = await normalizeOptions(tree, schema); const options = await normalizeOptions(tree, schema);
if (options.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(tree, options.projectRoot);
}
const jsInitTask = await jsInitGenerator(tree, { const jsInitTask = await jsInitGenerator(tree, {
js: options.js, js: options.js,
skipFormat: true, skipFormat: true,
@ -77,10 +81,6 @@ export async function remixLibraryGeneratorInternal(
: undefined : undefined
); );
if (options.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(tree, options.projectRoot);
}
sortPackageJsonFields(tree, options.projectRoot); sortPackageJsonFields(tree, options.projectRoot);
if (!options.skipFormat) { if (!options.skipFormat) {

View File

@ -20,6 +20,7 @@ import {
import { editTsConfig } from '../application/lib/create-ts-config'; import { editTsConfig } from '../application/lib/create-ts-config';
import rspackInitGenerator from '../init/init'; import rspackInitGenerator from '../init/init';
import { ConfigurationSchema } from './schema'; import { ConfigurationSchema } from './schema';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function configurationGenerator( export async function configurationGenerator(
tree: Tree, tree: Tree,
@ -73,7 +74,7 @@ export async function configurationGenerator(
if ( if (
alreadyHasNxRspackTargets.build && alreadyHasNxRspackTargets.build &&
(alreadyHasNxRspackTargets.serve || (alreadyHasNxRspackTargets.serve ||
projectType === 'library' || getProjectType(tree, options.project, projectType) === 'library' ||
options.framework === 'nest') options.framework === 'nest')
) { ) {
throw new Error( throw new Error(

View File

@ -825,7 +825,7 @@ describe('@nx/storybook:configuration for Storybook v7', () => {
}); });
await configurationGenerator(tree, { await configurationGenerator(tree, {
project: 'mylib', project: '@proj/mylib',
standaloneConfig: false, standaloneConfig: false,
uiFramework: '@storybook/react-vite', uiFramework: '@storybook/react-vite',
addPlugin: true, addPlugin: true,

View File

@ -47,6 +47,7 @@ import {
import { interactionTestsDependencies } from './lib/interaction-testing.utils'; import { interactionTestsDependencies } from './lib/interaction-testing.utils';
import { ensureDependencies } from './lib/ensure-dependencies'; import { ensureDependencies } from './lib/ensure-dependencies';
import { editRootTsConfig } from './lib/edit-root-tsconfig'; import { editRootTsConfig } from './lib/edit-root-tsconfig';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
export function configurationGenerator( export function configurationGenerator(
tree: Tree, tree: Tree,
@ -126,7 +127,8 @@ export async function configurationGeneratorInternal(
); );
const mainDir = const mainDir =
!!nextConfigFilePath && projectType === 'application' !!nextConfigFilePath &&
getProjectType(tree, root, projectType) === 'application'
? 'components' ? 'components'
: 'src'; : 'src';

View File

@ -30,6 +30,7 @@ import { findEslintFile } from '@nx/eslint/src/generators/utils/eslint-file';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config'; import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
import { import {
findRuntimeTsConfigName, findRuntimeTsConfigName,
getProjectType,
isUsingTsSolutionSetup, isUsingTsSolutionSetup,
} from '@nx/js/src/utils/typescript/ts-solution-setup'; } from '@nx/js/src/utils/typescript/ts-solution-setup';
@ -581,7 +582,8 @@ export function createProjectStorybookDir(
usesReactNative?: boolean usesReactNative?: boolean
) { ) {
let projectDirectory = let projectDirectory =
projectType === 'application' getProjectType(tree, root, projectType as 'application' | 'library') ===
'application'
? isNextJs ? isNextJs
? 'components' ? 'components'
: 'src/app' : 'src/app'
@ -616,7 +618,10 @@ export function createProjectStorybookDir(
projectType, projectType,
interactionTests, interactionTests,
mainDir, mainDir,
isNextJs: isNextJs && projectType === 'application', isNextJs:
isNextJs &&
getProjectType(tree, root, projectType as 'application' | 'library') ===
'application',
usesSwc, usesSwc,
usesVite, usesVite,
isRootProject: projectIsRootProjectInStandaloneWorkspace, isRootProject: projectIsRootProjectInStandaloneWorkspace,
@ -651,7 +656,7 @@ export function getTsConfigPath(
root, root,
path?.length > 0 path?.length > 0
? path ? path
: projectType === 'application' : getProjectType(tree, root, projectType) === 'application'
? 'tsconfig.app.json' ? 'tsconfig.app.json'
: 'tsconfig.lib.json' : 'tsconfig.lib.json'
); );

View File

@ -15,7 +15,10 @@ import {
initGenerator as jsInitGenerator, initGenerator as jsInitGenerator,
} from '@nx/js'; } 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 {
getProjectType,
isUsingTsSolutionSetup,
} from '@nx/js/src/utils/typescript/ts-solution-setup';
import { join } from 'node:path/posix'; import { join } from 'node:path/posix';
import type { PackageJson } from 'nx/src/utils/package-json'; import type { PackageJson } from 'nx/src/utils/package-json';
import { ensureDependencies } from '../../utils/ensure-dependencies'; import { ensureDependencies } from '../../utils/ensure-dependencies';
@ -50,8 +53,11 @@ export async function viteConfigurationGeneratorInternal(
const projectConfig = readProjectConfiguration(tree, schema.project); const projectConfig = readProjectConfiguration(tree, schema.project);
const { targets, root: projectRoot } = projectConfig; const { targets, root: projectRoot } = projectConfig;
const projectType = const projectType = getProjectType(
schema.projectType ?? projectConfig.projectType ?? 'library'; tree,
projectConfig.root,
schema.projectType ?? projectConfig.projectType
);
schema.includeLib ??= projectType === 'library'; schema.includeLib ??= projectType === 'library';
@ -196,7 +202,7 @@ function updatePackageJson(
name: getImportPath(tree, options.project), name: getImportPath(tree, options.project),
version: '0.0.1', version: '0.0.1',
}; };
if (projectType === 'application') { if (getProjectType(tree, project.root, projectType) === 'application') {
packageJson.private = true; packageJson.private = true;
} }
} }

View File

@ -16,12 +16,13 @@ import {
handleUnknownConfiguration, handleUnknownConfiguration,
moveAndEditIndexHtml, moveAndEditIndexHtml,
} from '../../../utils/generator-utils'; } from '../../../utils/generator-utils';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function convertNonVite( export async function convertNonVite(
tree: Tree, tree: Tree,
schema: ViteConfigurationGeneratorSchema, schema: ViteConfigurationGeneratorSchema,
projectRoot: string, projectRoot: string,
projectType: string, _projectType: string,
targets: { targets: {
[targetName: string]: TargetConfiguration<any>; [targetName: string]: TargetConfiguration<any>;
} }
@ -34,6 +35,11 @@ export async function convertNonVite(
// Check if it has webpack // Check if it has webpack
const hasWebpackConfig = findWebpackConfig(tree, projectRoot); const hasWebpackConfig = findWebpackConfig(tree, projectRoot);
const projectType = getProjectType(
tree,
projectRoot,
_projectType as 'application' | 'library'
);
if (hasWebpackConfig) { if (hasWebpackConfig) {
if (projectType === 'application') { if (projectType === 'application') {
moveAndEditIndexHtml(tree, schema); moveAndEditIndexHtml(tree, schema);

View File

@ -15,7 +15,10 @@ import {
updateNxJson, updateNxJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js'; import { initGenerator as jsInitGenerator } from '@nx/js';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import {
getProjectType,
isUsingTsSolutionSetup,
} from '@nx/js/src/utils/typescript/ts-solution-setup';
import { join } from 'path'; import { join } from 'path';
import { ensureDependencies } from '../../utils/ensure-dependencies'; import { ensureDependencies } from '../../utils/ensure-dependencies';
import { import {
@ -118,7 +121,7 @@ getTestBed().initTestEnvironment(
tree, tree,
{ {
project: schema.project, project: schema.project,
includeLib: projectType === 'library', includeLib: getProjectType(tree, root, projectType) === 'library',
includeVitest: true, includeVitest: true,
inSourceTests: schema.inSourceTests, inSourceTests: schema.inSourceTests,
rollupOptionsExternal: [ rollupOptionsExternal: [
@ -142,7 +145,7 @@ getTestBed().initTestEnvironment(
{ {
...schema, ...schema,
includeVitest: true, includeVitest: true,
includeLib: projectType === 'library', includeLib: getProjectType(tree, root, projectType) === 'library',
}, },
true true
); );
@ -265,7 +268,9 @@ function updateTsConfig(
let runtimeTsconfigPath = joinPathFragments( let runtimeTsconfigPath = joinPathFragments(
projectRoot, projectRoot,
projectType === 'application' ? 'tsconfig.app.json' : 'tsconfig.lib.json' getProjectType(tree, projectRoot, projectType) === 'application'
? 'tsconfig.app.json'
: 'tsconfig.lib.json'
); );
if (options.runtimeTsconfigFileName) { if (options.runtimeTsconfigFileName) {
runtimeTsconfigPath = joinPathFragments( runtimeTsconfigPath = joinPathFragments(

View File

@ -52,6 +52,13 @@ export async function applicationGeneratorInternal(
); );
const options = await normalizeOptions(tree, _options); const options = await normalizeOptions(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);
}
const nxJson = readNxJson(tree); const nxJson = readNxJson(tree);
options.addPlugin ??= options.addPlugin ??=
@ -60,15 +67,14 @@ export async function applicationGeneratorInternal(
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.projectName,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: options.parsedTags?.length
name: options.name, ? {
projectType: 'application', tags: options.parsedTags,
sourceRoot: `${options.appProjectRoot}/src`, }
tags: options.parsedTags?.length ? options.parsedTags : undefined, : undefined,
},
}); });
} else { } else {
addProjectConfiguration(tree, options.projectName, { addProjectConfiguration(tree, options.projectName, {
@ -108,6 +114,7 @@ export async function applicationGeneratorInternal(
setParserOptionsProject: options.setParserOptionsProject, setParserOptionsProject: options.setParserOptionsProject,
rootProject: options.rootProject, rootProject: options.rootProject,
addPlugin: options.addPlugin, addPlugin: options.addPlugin,
projectName: options.projectName,
}, },
'app' 'app'
) )
@ -146,12 +153,6 @@ export async function applicationGeneratorInternal(
); );
} }
// 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

@ -5,6 +5,7 @@ import {
} 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';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export async function normalizeOptions( export async function normalizeOptions(
host: Tree, host: Tree,
@ -27,20 +28,24 @@ export async function normalizeOptions(
? options.tags.split(',').map((s) => s.trim()) ? options.tags.split(',').map((s) => s.trim())
: []; : [];
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
const normalized = { const normalized = {
...options, ...options,
projectName: appProjectName, projectName: isUsingTsSolutionConfig
? getImportPath(host, appProjectName)
: appProjectName,
appProjectRoot, appProjectRoot,
e2eProjectName, e2eProjectName,
e2eProjectRoot, e2eProjectRoot,
parsedTags, parsedTags,
isUsingTsSolutionConfig,
} as NormalizedSchema; } as NormalizedSchema;
normalized.style = options.style ?? 'css'; normalized.style = options.style ?? 'css';
normalized.routing = normalized.routing ?? false; normalized.routing = normalized.routing ?? false;
normalized.unitTestRunner ??= 'vitest'; normalized.unitTestRunner ??= 'vitest';
normalized.e2eTestRunner = normalized.e2eTestRunner ?? 'playwright'; normalized.e2eTestRunner = normalized.e2eTestRunner ?? 'playwright';
normalized.isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
normalized.bundler = normalized.bundler ?? 'vite'; normalized.bundler = normalized.bundler ?? 'vite';
return normalized; return normalized;

View File

@ -12,6 +12,7 @@ import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generat
import { NormalizedSchema, ComponentGeneratorSchema } from '../schema'; import { NormalizedSchema, ComponentGeneratorSchema } from '../schema';
import { addImport } from '../../../utils/ast-utils'; import { addImport } from '../../../utils/ast-utils';
import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup';
let tsModule: typeof import('typescript'); let tsModule: typeof import('typescript');
@ -33,9 +34,12 @@ export async function normalizeOptions(
let { className } = names(fileName); let { className } = names(fileName);
const componentFileName = fileName; const componentFileName = fileName;
const project = getProjects(host).get(projectName); const project = getProjects(host).get(projectName);
const { sourceRoot: projectSourceRoot, projectType } = project; const { root, sourceRoot: projectSourceRoot, projectType } = project;
if (options.export && projectType === 'application') { if (
options.export &&
getProjectType(host, root, projectType) === 'application'
) {
logger.warn( logger.warn(
`The "--export" option should not be used with applications and will do nothing.` `The "--export" option should not be used with applications and will do nothing.`
); );
@ -60,12 +64,13 @@ export function addExportsToBarrel(host: Tree, options: NormalizedSchema) {
tsModule = ensureTypescript(); tsModule = ensureTypescript();
} }
const workspace = getProjects(host); const workspace = getProjects(host);
const proj = workspace.get(options.projectName);
const isApp = const isApp =
workspace.get(options.projectName).projectType === 'application'; getProjectType(host, proj.root, proj.projectType) === 'application';
if (options.export && !isApp) { if (options.export && !isApp) {
const indexFilePath = joinPathFragments( const indexFilePath = joinPathFragments(
options.projectSourceRoot, options.projectSourceRoot ?? 'src',
options.js ? 'index.js' : 'index.ts' options.js ? 'index.js' : 'index.ts'
); );
const indexSource = host.read(indexFilePath, 'utf-8'); const indexSource = host.read(indexFilePath, 'utf-8');

View File

@ -18,7 +18,7 @@ export async function addVite(
ensurePackage<typeof import('@nx/vite')>('@nx/vite', nxVersion); ensurePackage<typeof import('@nx/vite')>('@nx/vite', nxVersion);
const viteTask = await viteConfigurationGenerator(tree, { const viteTask = await viteConfigurationGenerator(tree, {
uiFramework: 'none', uiFramework: 'none',
project: options.name, project: options.projectName,
newProject: true, newProject: true,
includeLib: true, includeLib: true,
inSourceTests: options.inSourceTests, inSourceTests: options.inSourceTests,
@ -32,7 +32,7 @@ export async function addVite(
createOrEditViteConfig( createOrEditViteConfig(
tree, tree,
{ {
project: options.name, project: options.projectName,
includeLib: true, includeLib: true,
includeVitest: options.unitTestRunner === 'vitest', includeVitest: options.unitTestRunner === 'vitest',
inSourceTests: options.inSourceTests, inSourceTests: options.inSourceTests,
@ -53,7 +53,7 @@ export async function addVite(
>('@nx/vite', nxVersion); >('@nx/vite', nxVersion);
const vitestTask = await vitestGenerator(tree, { const vitestTask = await vitestGenerator(tree, {
uiFramework: 'none', uiFramework: 'none',
project: options.name, project: options.projectName,
coverageProvider: 'v8', coverageProvider: 'v8',
inSourceTests: options.inSourceTests, inSourceTests: options.inSourceTests,
skipFormat: true, skipFormat: true,
@ -66,7 +66,7 @@ export async function addVite(
createOrEditViteConfig( createOrEditViteConfig(
tree, tree,
{ {
project: options.name, project: options.projectName,
includeLib: true, includeLib: true,
includeVitest: true, includeVitest: true,
inSourceTests: options.inSourceTests, inSourceTests: options.inSourceTests,

View File

@ -11,6 +11,7 @@ import {
} 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';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export async function normalizeOptions( export async function normalizeOptions(
host: Tree, host: Tree,
@ -51,9 +52,14 @@ export async function normalizeOptions(
process.env.NX_ADD_PLUGINS !== 'false' && process.env.NX_ADD_PLUGINS !== 'false' &&
nxJson.useInferencePlugins !== false; nxJson.useInferencePlugins !== false;
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
const normalized = { const normalized = {
addPlugin, addPlugin,
...options, ...options,
projectName: isUsingTsSolutionConfig
? getImportPath(host, projectName)
: projectName,
bundler, bundler,
fileName, fileName,
routePath: `/${projectNames.projectFileName}`, routePath: `/${projectNames.projectFileName}`,
@ -61,7 +67,7 @@ export async function normalizeOptions(
projectRoot, projectRoot,
parsedTags, parsedTags,
importPath, importPath,
isUsingTsSolutionConfig: isUsingTsSolutionSetup(host), isUsingTsSolutionConfig,
} as NormalizedSchema; } as NormalizedSchema;
// Libraries with a bundler or is publishable must also be buildable. // Libraries with a bundler or is publishable must also be buildable.

View File

@ -47,6 +47,12 @@ export async function libraryGeneratorInternal(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.projectRoot);
}
if (options.isUsingTsSolutionConfig) { if (options.isUsingTsSolutionConfig) {
writeJson(tree, joinPathFragments(options.projectRoot, 'package.json'), { writeJson(tree, joinPathFragments(options.projectRoot, 'package.json'), {
name: getImportPath(tree, options.name), name: getImportPath(tree, options.name),
@ -54,12 +60,11 @@ export async function libraryGeneratorInternal(tree: Tree, schema: Schema) {
private: true, private: true,
...determineEntryFields(options), ...determineEntryFields(options),
files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined, files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined,
nx: { nx: options.parsedTags?.length
name: options.name, ? {
projectType: 'library', tags: options.parsedTags,
sourceRoot: `${options.projectRoot}/src`, }
tags: options.parsedTags?.length ? options.parsedTags : undefined, : undefined,
},
}); });
} else { } else {
addProjectConfiguration(tree, options.name, { addProjectConfiguration(tree, options.name, {
@ -149,12 +154,6 @@ export async function libraryGeneratorInternal(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.projectRoot);
}
sortPackageJsonFields(tree, options.projectRoot); sortPackageJsonFields(tree, options.projectRoot);
if (!options.skipFormat) await formatFiles(tree); if (!options.skipFormat) await formatFiles(tree);

View File

@ -27,6 +27,7 @@ export interface Schema {
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
js: boolean; js: boolean;
name: string; name: string;
projectName: string;
linter: Linter | LinterType; linter: Linter | LinterType;
fileName: string; fileName: string;
projectRoot: string; projectRoot: string;

View File

@ -30,6 +30,7 @@ export async function addLinting(
skipPackageJson?: boolean; skipPackageJson?: boolean;
rootProject?: boolean; rootProject?: boolean;
addPlugin?: boolean; addPlugin?: boolean;
projectName: string;
}, },
projectType: 'lib' | 'app' projectType: 'lib' | 'app'
) { ) {
@ -37,7 +38,7 @@ export async function addLinting(
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const lintTask = await lintProjectGenerator(host, { const lintTask = await lintProjectGenerator(host, {
linter: options.linter, linter: options.linter,
project: options.name, project: options.projectName,
tsConfigPaths: [ tsConfigPaths: [
joinPathFragments(options.projectRoot, `tsconfig.${projectType}.json`), joinPathFragments(options.projectRoot, `tsconfig.${projectType}.json`),
], ],

View File

@ -247,12 +247,11 @@ async function addProject(tree: Tree, options: NormalizedSchema) {
name: getImportPath(tree, options.name), name: getImportPath(tree, options.name),
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: { nx: options.parsedTags?.length
name: options.name, ? {
projectType: 'application', tags: options.parsedTags,
sourceRoot: `${options.appProjectRoot}/src`, }
tags: options.parsedTags?.length ? options.parsedTags : undefined, : undefined,
},
}); });
} else { } else {
addProjectConfiguration(tree, options.projectName, { addProjectConfiguration(tree, options.projectName, {
@ -288,6 +287,10 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
export async function applicationGeneratorInternal(host: Tree, schema: Schema) { export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
const options = await normalizeOptions(host, schema); const options = await normalizeOptions(host, schema);
if (options.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(host, options.appProjectRoot);
}
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, { const jsInitTask = await jsInitGenerator(host, {
@ -666,10 +669,6 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
: undefined : undefined
); );
if (options.isUsingTsSolutionConfig) {
addProjectToTsSolutionWorkspace(host, options.appProjectRoot);
}
if (!options.skipFormat) { if (!options.skipFormat) {
await formatFiles(host); await formatFiles(host);
} }
@ -719,7 +718,9 @@ async function normalizeOptions(
name: names(options.name).fileName, name: names(options.name).fileName,
compiler: options.compiler ?? 'babel', compiler: options.compiler ?? 'babel',
bundler: options.bundler ?? 'webpack', bundler: options.bundler ?? 'webpack',
projectName: appProjectName, projectName: isUsingTsSolutionConfig
? getImportPath(host, appProjectName)
: appProjectName,
strict: options.strict ?? true, strict: options.strict ?? true,
appProjectRoot, appProjectRoot,
e2eProjectRoot, e2eProjectRoot,

View File

@ -9,6 +9,7 @@ import {
writeJson, writeJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { moveGenerator } from '../move/move'; import { moveGenerator } from '../move/move';
import { getProjectType } from '../../utils/ts-solution-setup';
export async function monorepoGenerator(tree: Tree, options: {}) { export async function monorepoGenerator(tree: Tree, options: {}) {
const projects = getProjects(tree); const projects = getProjects(tree);
@ -32,7 +33,9 @@ export async function monorepoGenerator(tree: Tree, options: {}) {
// Currently, Nx only handles apps+libs or packages. You cannot mix and match them. // Currently, Nx only handles apps+libs or packages. You cannot mix and match them.
// If the standalone project is an app (React, Angular, etc), then use apps+libs. // If the standalone project is an app (React, Angular, etc), then use apps+libs.
// Otherwise, for TS standalone (lib), use packages. // Otherwise, for TS standalone (lib), use packages.
const isRootProjectApp = rootProject.projectType === 'application'; const isRootProjectApp =
getProjectType(tree, rootProject.root, rootProject.projectType) ===
'application';
const appsDir = isRootProjectApp ? 'apps' : 'packages'; const appsDir = isRootProjectApp ? 'apps' : 'packages';
const libsDir = isRootProjectApp ? 'libs' : 'packages'; const libsDir = isRootProjectApp ? 'libs' : 'packages';
@ -50,11 +53,12 @@ export async function monorepoGenerator(tree: Tree, options: {}) {
} }
for (const project of projectsToMove) { for (const project of projectsToMove) {
const projectType = getProjectType(tree, project.root, project.projectType);
await moveGenerator(tree, { await moveGenerator(tree, {
projectName: project.name, projectName: project.name,
newProjectName: project.name, newProjectName: project.name,
destination: destination:
project.projectType === 'application' projectType === 'application'
? joinPathFragments( ? joinPathFragments(
appsDir, appsDir,
project.root === '.' ? project.name : project.root project.root === '.' ? project.name : project.root
@ -63,7 +67,7 @@ export async function monorepoGenerator(tree: Tree, options: {}) {
libsDir, libsDir,
project.root === '.' ? project.name : project.root project.root === '.' ? project.name : project.root
), ),
updateImportPath: project.projectType === 'library', updateImportPath: projectType === 'library',
}); });
} }
} }

View File

@ -1,7 +1,12 @@
import { type ProjectConfiguration, type Tree } from '@nx/devkit'; import {
joinPathFragments,
type ProjectConfiguration,
type Tree,
} from '@nx/devkit';
import { getNpmScope } from '../../../utilities/get-import-path'; import { getNpmScope } from '../../../utilities/get-import-path';
import type { NormalizedSchema, Schema } from '../schema'; import type { NormalizedSchema, Schema } from '../schema';
import { normalizePathSlashes } from './utils'; import { normalizePathSlashes } from './utils';
import { getProjectType } from '../../../utils/ts-solution-setup';
export async function normalizeSchema( export async function normalizeSchema(
tree: Tree, tree: Tree,
@ -35,7 +40,7 @@ async function determineProjectNameAndRootOptions(
options: Schema, options: Schema,
projectConfiguration: ProjectConfiguration projectConfiguration: ProjectConfiguration
): Promise<ProjectNameAndRootOptions> { ): Promise<ProjectNameAndRootOptions> {
validateName(options.newProjectName, projectConfiguration); validateName(tree, options.newProjectName, projectConfiguration);
const projectNameAndRootOptions = getProjectNameAndRootOptions( const projectNameAndRootOptions = getProjectNameAndRootOptions(
tree, tree,
options, options,
@ -46,6 +51,7 @@ async function determineProjectNameAndRootOptions(
} }
function validateName( function validateName(
tree: Tree,
name: string | undefined, name: string | undefined,
projectConfiguration: ProjectConfiguration projectConfiguration: ProjectConfiguration
): void { ): void {
@ -66,15 +72,26 @@ function validateName(
const libraryPattern = const libraryPattern =
'(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$'; '(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$';
const appPattern = '^[a-zA-Z][^:]*$'; const appPattern = '^[a-zA-Z][^:]*$';
const projectType = getProjectType(
tree,
projectConfiguration.root,
projectConfiguration.projectType
);
if (projectConfiguration.projectType === 'application') { if (projectType === 'application') {
const validationRegex = new RegExp(appPattern); const validationRegex = new RegExp(appPattern);
if (!validationRegex.test(name)) { if (!validationRegex.test(name)) {
throw new Error( throw new Error(
`The new project name should match the pattern "${appPattern}". The provided value "${name}" does not match.` `The new project name should match the pattern "${appPattern}". The provided value "${name}" does not match.`
); );
} }
} else if (projectConfiguration.projectType === 'library') { } else if (
getProjectType(
tree,
projectConfiguration.root,
projectConfiguration.projectType
) === 'library'
) {
const validationRegex = new RegExp(libraryPattern); const validationRegex = new RegExp(libraryPattern);
if (!validationRegex.test(name)) { if (!validationRegex.test(name)) {
throw new Error( throw new Error(

View File

@ -1,48 +1,4 @@
import { import { normalizePath } from '@nx/devkit';
getWorkspaceLayout,
joinPathFragments,
normalizePath,
ProjectConfiguration,
Tree,
} from '@nx/devkit';
import { Schema } from '../schema';
/**
* This helper function ensures that we don't move libs or apps
* outside of the folders they should be in.
*
* This will break if someone isn't using the default libs/apps
* folders. In that case, they're on their own :/
*/
export function getDestination(
host: Tree,
schema: Schema,
project: ProjectConfiguration
): string {
const projectType = project.projectType;
const workspaceLayout = getWorkspaceLayout(host);
let rootFolder = workspaceLayout.libsDir;
if (projectType === 'application') {
rootFolder = workspaceLayout.appsDir;
}
return joinPathFragments(rootFolder, schema.destination);
}
/**
* Joins path segments replacing slashes with dashes
*
* @param path
*/
export function getNewProjectName(path: string): string {
// strip leading '/' or './' or '../' and trailing '/' and replaces '/' with '-'
return normalizePath(path)
.replace(/(^\.{0,2}\/|\.{1,2}\/|\/$)/g, '')
.split('/')
.filter((x) => !!x)
.join('-');
}
/** /**
* Normalizes slashes (removes duplicates) * Normalizes slashes (removes duplicates)

View File

@ -0,0 +1,19 @@
import { joinPathFragments, readJson, type Tree } from '@nx/devkit';
// This is copied from `@nx/js` to avoid circular dependencies.
export function getProjectType(
tree: Tree,
projectRoot: string,
projectType?: 'library' | 'application'
): 'library' | 'application' {
if (projectType) return projectType;
if (joinPathFragments(projectRoot, 'tsconfig.lib.json')) return 'library';
if (joinPathFragments(projectRoot, 'tsconfig.app.json')) return 'application';
// If there are no exports, assume it is an application since both buildable and non-buildable libraries have exports.
const packageJsonPath = joinPathFragments(projectRoot, 'package.json');
const packageJson = tree.exists(packageJsonPath)
? readJson(tree, joinPathFragments(projectRoot, 'package.json'))
: null;
if (!packageJson?.exports) return 'application';
return 'library';
}