diff --git a/e2e/workspace-create-npm/src/create-nx-workspace-npm.test.ts b/e2e/workspace-create-npm/src/create-nx-workspace-npm.test.ts index 850e2d7c6023a..c6f5f7a5a0580 100644 --- a/e2e/workspace-create-npm/src/create-nx-workspace-npm.test.ts +++ b/e2e/workspace-create-npm/src/create-nx-workspace-npm.test.ts @@ -136,6 +136,7 @@ describe('create-nx-workspace --preset=npm', () => { const tsconfig = readJson(`tsconfig.base.json`); expect(tsconfig.compilerOptions.paths).toEqual({ [libName]: [`packages/${libName}/src/index.ts`], + [`${libName}/server`]: [`packages/${libName}/src/server.ts`], }); }); diff --git a/packages/angular/src/generators/library/library.ts b/packages/angular/src/generators/library/library.ts index 805ff94640752..8494d82412c79 100644 --- a/packages/angular/src/generators/library/library.ts +++ b/packages/angular/src/generators/library/library.ts @@ -3,12 +3,13 @@ import { formatFiles, GeneratorCallback, installPackagesTask, + joinPathFragments, removeDependenciesFromPackageJson, Tree, } from '@nrwl/devkit'; import { jestProjectGenerator } from '@nrwl/jest'; import { Linter } from '@nrwl/linter'; -import { updateRootTsConfig } from '@nrwl/js'; +import { addTsConfigPath } from '@nrwl/js'; import { lt } from 'semver'; import init from '../../generators/init/init'; import { E2eTestRunner } from '../../utils/test-runners'; @@ -107,7 +108,9 @@ export async function libraryGenerator( addBuildableLibrariesPostCssDependencies(tree); } - updateRootTsConfig(tree, { ...libraryOptions, js: false }); + addTsConfigPath(tree, libraryOptions.importPath, [ + joinPathFragments(libraryOptions.projectRoot, './src', 'index.ts'), + ]); if (!libraryOptions.skipFormat) { await formatFiles(tree); diff --git a/packages/expo/src/generators/library/library.ts b/packages/expo/src/generators/library/library.ts index b4d2f1a1402f8..b1d79b82ea8b0 100644 --- a/packages/expo/src/generators/library/library.ts +++ b/packages/expo/src/generators/library/library.ts @@ -15,7 +15,7 @@ import { updateJson, } from '@nrwl/devkit'; -import { updateRootTsConfig } from '@nrwl/js'; +import { addTsConfigPath } from '@nrwl/js'; import init from '../init/init'; import { addLinting } from '../../utils/add-linting'; @@ -54,7 +54,13 @@ export async function expoLibraryGenerator( ); if (!options.skipTsConfig) { - updateRootTsConfig(host, options); + addTsConfigPath(host, options.importPath, [ + joinPathFragments( + options.projectRoot, + './src', + 'index.' + (options.js ? 'js' : 'ts') + ), + ]); } const jestTask = await addJest( diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 23be7456e67c4..d94e955032091 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -21,8 +21,8 @@ import { import { getImportPath } from 'nx/src/utils/path'; import { + addTsConfigPath, getRelativePathToRootTsConfig, - updateRootTsConfig, } from '../../utils/typescript/ts-config'; import { join } from 'path'; import { addMinimalPublishScript } from '../../utils/minimal-publish-script'; @@ -117,7 +117,13 @@ export async function projectGenerator( } if (!schema.skipTsConfig) { - updateRootTsConfig(tree, options); + addTsConfigPath(tree, options.importPath, [ + joinPathFragments( + options.projectRoot, + './src', + 'index.' + (options.js ? 'js' : 'ts') + ), + ]); } if (!options.skipFormat) { diff --git a/packages/js/src/utils/typescript/ts-config.ts b/packages/js/src/utils/typescript/ts-config.ts index e52f146a3a210..6743ecaa8a4c7 100644 --- a/packages/js/src/utils/typescript/ts-config.ts +++ b/packages/js/src/utils/typescript/ts-config.ts @@ -52,38 +52,22 @@ export function getRootTsConfigFileName(tree: Tree): string | null { return null; } -export function updateRootTsConfig( - host: Tree, - options: { - name: string; - importPath?: string; - projectRoot: string; - js?: boolean; - } +export function addTsConfigPath( + tree: Tree, + importPath: string, + lookupPaths: string[] ) { - if (!options.importPath) { - throw new Error( - `Unable to update ${options.name} using the import path "${options.importPath}". Make sure to specify a valid import path one.` - ); - } - updateJson(host, getRootTsConfigPathInTree(host), (json) => { + updateJson(tree, getRootTsConfigPathInTree(tree), (json) => { const c = json.compilerOptions; - c.paths = c.paths || {}; - delete c.paths[options.name]; + c.paths ??= {}; - if (c.paths[options.importPath]) { + if (c.paths[importPath]) { throw new Error( - `You already have a library using the import path "${options.importPath}". Make sure to specify a unique one.` + `You already have a library using the import path "${importPath}". Make sure to specify a unique one.` ); } - c.paths[options.importPath] = [ - joinPathFragments( - options.projectRoot, - './src', - 'index.' + (options.js ? 'js' : 'ts') - ), - ]; + c.paths[importPath] = lookupPaths; return json; }); diff --git a/packages/next/src/generators/library/lib/normalize-options.spec.ts b/packages/next/src/generators/library/lib/normalize-options.spec.ts new file mode 100644 index 0000000000000..4af2562f4fec9 --- /dev/null +++ b/packages/next/src/generators/library/lib/normalize-options.spec.ts @@ -0,0 +1,26 @@ +import type { Tree } from '@nrwl/devkit'; +import { Linter } from '@nrwl/linter'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { normalizeOptions } from './normalize-options'; + +describe('normalizeOptions', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should set importPath and projectRoot', () => { + const options = normalizeOptions(tree, { + name: 'my-lib', + style: 'css', + linter: Linter.None, + unitTestRunner: 'jest', + }); + + expect(options).toMatchObject({ + importPath: '@proj/my-lib', + projectRoot: 'my-lib', + }); + }); +}); diff --git a/packages/next/src/generators/library/lib/normalize-options.ts b/packages/next/src/generators/library/lib/normalize-options.ts new file mode 100644 index 0000000000000..13f23c0447964 --- /dev/null +++ b/packages/next/src/generators/library/lib/normalize-options.ts @@ -0,0 +1,32 @@ +import { + getImportPath, + getWorkspaceLayout, + joinPathFragments, + names, + Tree, +} from '@nrwl/devkit'; +import { Schema } from '../schema'; + +export interface NormalizedSchema extends Schema { + importPath: string; + projectRoot: string; +} + +export function normalizeOptions( + host: Tree, + options: Schema +): NormalizedSchema { + const name = names(options.name).fileName; + const projectDirectory = options.directory + ? `${names(options.directory).fileName}/${name}` + : name; + + const { libsDir } = getWorkspaceLayout(host); + const projectRoot = joinPathFragments(libsDir, projectDirectory); + const { npmScope } = getWorkspaceLayout(host); + return { + ...options, + importPath: options.importPath ?? getImportPath(npmScope, projectDirectory), + projectRoot, + }; +} diff --git a/packages/next/src/generators/library/library.spec.ts b/packages/next/src/generators/library/library.spec.ts index 7905b85fa53ea..604ebac9a2270 100644 --- a/packages/next/src/generators/library/library.spec.ts +++ b/packages/next/src/generators/library/library.spec.ts @@ -4,9 +4,11 @@ import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Linter } from '@nrwl/linter'; import libraryGenerator from './library'; import { Schema } from './schema'; + // need to mock cypress otherwise it'll use the nx installed version from package.json // which is v9 while we are testing for the new v10 version jest.mock('@nrwl/cypress/src/utils/cypress-version'); + describe('next library', () => { let mockedInstalledCypressVersion: jest.Mock< ReturnType @@ -133,4 +135,31 @@ describe('next library', () => { .jsxImportSource ).toEqual('@emotion/react'); }); + + it('should generate a server-only entry point', async () => { + const appTree = createTreeWithEmptyWorkspace(); + + await libraryGenerator(appTree, { + name: 'myLib', + linter: Linter.EsLint, + skipFormat: false, + skipTsConfig: false, + unitTestRunner: 'jest', + style: 'css', + component: true, + }); + + expect(appTree.read('my-lib/src/index.ts', 'utf-8')).toContain( + 'React client components' + ); + expect(appTree.read('my-lib/src/server.ts', 'utf-8')).toContain( + 'React server components' + ); + expect( + readJson(appTree, 'tsconfig.base.json').compilerOptions.paths + ).toMatchObject({ + '@proj/my-lib': ['my-lib/src/index.ts'], + '@proj/my-lib/server': ['my-lib/src/server.ts'], + }); + }); }); diff --git a/packages/next/src/generators/library/library.ts b/packages/next/src/generators/library/library.ts index 427c9bac4f5ca..b082d0c7ec56b 100644 --- a/packages/next/src/generators/library/library.ts +++ b/packages/next/src/generators/library/library.ts @@ -2,6 +2,7 @@ import { convertNxGenerator, formatFiles, GeneratorCallback, + getImportPath, getWorkspaceLayout, joinPathFragments, names, @@ -10,19 +11,15 @@ import { updateJson, } from '@nrwl/devkit'; import { libraryGenerator as reactLibraryGenerator } from '@nrwl/react/src/generators/library/library'; +import { addTsConfigPath } from '@nrwl/js'; import { nextInitGenerator } from '../init/init'; import { Schema } from './schema'; +import { normalizeOptions } from './lib/normalize-options'; -export async function libraryGenerator(host: Tree, options: Schema) { - const name = names(options.name).fileName; +export async function libraryGenerator(host: Tree, rawOptions: Schema) { + const options = normalizeOptions(host, rawOptions); const tasks: GeneratorCallback[] = []; - const projectDirectory = options.directory - ? `${names(options.directory).fileName}/${name}` - : name; - - const { libsDir } = getWorkspaceLayout(host); - const projectRoot = joinPathFragments(libsDir, projectDirectory); const initTask = await nextInitGenerator(host, { ...options, skipFormat: true, @@ -36,42 +33,78 @@ export async function libraryGenerator(host: Tree, options: Schema) { }); tasks.push(libTask); - updateJson(host, joinPathFragments(projectRoot, '.babelrc'), (json) => { - if (options.style === '@emotion/styled') { - json.presets = [ - [ - '@nrwl/next/babel', - { - 'preset-react': { - runtime: 'automatic', - importSource: '@emotion/react', + const indexPath = joinPathFragments( + options.projectRoot, + 'src', + `index.${options.js ? 'js' : 'ts'}` + ); + const indexContent = host.read(indexPath, 'utf-8'); + host.write( + indexPath, + `// Use this file to export React client components (e.g. those with 'use client' directive) or other non-server utilities\n${indexContent}` + ); + // Additional entry for Next.js libraries so React Server Components are exported from a separate entry point. + // This is needed because RSC exported from `src/index.ts` will mark the entire file as server-only and throw an error when used from a client component. + // See: https://github.com/nrwl/nx/issues/15830 + const serverEntryPath = joinPathFragments( + options.projectRoot, + './src', + 'server.' + (options.js ? 'js' : 'ts') + ); + host.write( + joinPathFragments( + options.projectRoot, + 'src', + `server.${options.js ? 'js' : 'ts'}` + ), + `// Use this file to export React server components\n` + ); + addTsConfigPath(host, `${options.importPath}/server`, [serverEntryPath]); + + updateJson( + host, + joinPathFragments(options.projectRoot, '.babelrc'), + (json) => { + if (options.style === '@emotion/styled') { + json.presets = [ + [ + '@nrwl/next/babel', + { + 'preset-react': { + runtime: 'automatic', + importSource: '@emotion/react', + }, }, - }, - ], - ]; - } else if (options.style === 'styled-jsx') { - // next.js doesn't require the `styled-jsx/babel' plugin as it is already - // built-into the `next/babel` preset - json.presets = ['@nrwl/next/babel']; - json.plugins = (json.plugins || []).filter( - (x) => x !== 'styled-jsx/babel' - ); - } else { - json.presets = ['@nrwl/next/babel']; + ], + ]; + } else if (options.style === 'styled-jsx') { + // next.js doesn't require the `styled-jsx/babel' plugin as it is already + // built-into the `next/babel` preset + json.presets = ['@nrwl/next/babel']; + json.plugins = (json.plugins || []).filter( + (x) => x !== 'styled-jsx/babel' + ); + } else { + json.presets = ['@nrwl/next/babel']; + } + return json; } - return json; - }); + ); - updateJson(host, joinPathFragments(projectRoot, 'tsconfig.json'), (json) => { - if (options.style === '@emotion/styled') { - json.compilerOptions.jsxImportSource = '@emotion/react'; + updateJson( + host, + joinPathFragments(options.projectRoot, 'tsconfig.json'), + (json) => { + if (options.style === '@emotion/styled') { + json.compilerOptions.jsxImportSource = '@emotion/react'; + } + return json; } - return json; - }); + ); updateJson( host, - joinPathFragments(projectRoot, 'tsconfig.lib.json'), + joinPathFragments(options.projectRoot, 'tsconfig.lib.json'), (json) => { if (!json.files) { json.files = []; diff --git a/packages/next/src/generators/library/schema.d.ts b/packages/next/src/generators/library/schema.d.ts index 82862f1591225..7b707e50daab9 100644 --- a/packages/next/src/generators/library/schema.d.ts +++ b/packages/next/src/generators/library/schema.d.ts @@ -5,8 +5,8 @@ export interface Schema { name: string; directory?: string; style: SupportedStyles; - skipTsConfig: boolean; - skipFormat: boolean; + skipTsConfig?: boolean; + skipFormat?: boolean; tags?: string; pascalCaseFiles?: boolean; routing?: boolean; diff --git a/packages/next/src/utils/versions.ts b/packages/next/src/utils/versions.ts index 53d0ae41f0bb6..b715ba421a059 100644 --- a/packages/next/src/utils/versions.ts +++ b/packages/next/src/utils/versions.ts @@ -1,8 +1,8 @@ export const nxVersion = require('../../package.json').version; -export const nextVersion = '13.1.1'; -export const eslintConfigNextVersion = '13.1.1'; -export const sassVersion = '1.55.0'; +export const nextVersion = '13.3.0'; +export const eslintConfigNextVersion = '13.3.0'; +export const sassVersion = '1.61.0'; export const lessLoader = '11.1.0'; export const stylusLoader = '7.1.0'; export const emotionServerVersion = '11.10.0'; diff --git a/packages/react-native/src/generators/library/library.ts b/packages/react-native/src/generators/library/library.ts index df89fad5edd42..13e75be167aeb 100644 --- a/packages/react-native/src/generators/library/library.ts +++ b/packages/react-native/src/generators/library/library.ts @@ -15,7 +15,7 @@ import { updateJson, } from '@nrwl/devkit'; -import { getRelativePathToRootTsConfig, updateRootTsConfig } from '@nrwl/js'; +import { addTsConfigPath, getRelativePathToRootTsConfig } from '@nrwl/js'; import init from '../init/init'; import { addLinting } from '../../utils/add-linting'; import { addJest } from '../../utils/add-jest'; @@ -64,7 +64,9 @@ export async function reactNativeLibraryGenerator( } if (!options.skipTsConfig) { - updateRootTsConfig(host, { ...options, js: false }); + addTsConfigPath(host, options.importPath, [ + joinPathFragments(options.projectRoot, './src', 'index.ts'), + ]); } if (!options.skipFormat) { diff --git a/packages/react/src/generators/library/library.ts b/packages/react/src/generators/library/library.ts index 19fc801aed1b0..f9145ad2cf81d 100644 --- a/packages/react/src/generators/library/library.ts +++ b/packages/react/src/generators/library/library.ts @@ -10,7 +10,7 @@ import { updateJson, } from '@nrwl/devkit'; -import { updateRootTsConfig } from '@nrwl/js'; +import { addTsConfigPath } from '@nrwl/js'; import { nxVersion } from '../../utils/versions'; import componentGenerator from '../component/component'; @@ -162,8 +162,15 @@ export async function libraryGenerator(host: Tree, schema: Schema) { setDefaults(host, options); extractTsConfigBase(host); + if (!options.skipTsConfig) { - updateRootTsConfig(host, options); + addTsConfigPath(host, options.importPath, [ + joinPathFragments( + options.projectRoot, + './src', + 'index.' + (options.js ? 'js' : 'ts') + ), + ]); } if (!options.skipFormat) {