feat(js): provide a new way of generating project name and root directory for libraries (#18420)
This commit is contained in:
parent
9cd0cf899a
commit
730260320d
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "library",
|
"name": "library",
|
||||||
"factory": "./src/generators/library/library#libraryGenerator",
|
"factory": "./src/generators/library/library#libraryGeneratorInternal",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$schema": "http://json-schema.org/schema",
|
"$schema": "http://json-schema.org/schema",
|
||||||
"$id": "NxTypescriptLibrary",
|
"$id": "NxTypescriptLibrary",
|
||||||
@ -14,13 +14,18 @@
|
|||||||
"description": "Library name.",
|
"description": "Library name.",
|
||||||
"$default": { "$source": "argv", "index": 0 },
|
"$default": { "$source": "argv", "index": 0 },
|
||||||
"x-prompt": "What name would you like to use for the library?",
|
"x-prompt": "What name would you like to use for the library?",
|
||||||
"pattern": "^[a-zA-Z].*$"
|
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$"
|
||||||
},
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A directory where the lib is placed.",
|
"description": "A directory where the lib is placed.",
|
||||||
"x-priority": "important"
|
"x-priority": "important"
|
||||||
},
|
},
|
||||||
|
"projectNameAndRootFormat": {
|
||||||
|
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["as-provided", "derived"]
|
||||||
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"description": "The tool to use for running lint checks.",
|
"description": "The tool to use for running lint checks.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -142,7 +147,7 @@
|
|||||||
"aliases": ["lib"],
|
"aliases": ["lib"],
|
||||||
"x-type": "library",
|
"x-type": "library",
|
||||||
"description": "Create a library",
|
"description": "Create a library",
|
||||||
"implementation": "/packages/js/src/generators/library/library#libraryGenerator.ts",
|
"implementation": "/packages/js/src/generators/library/library#libraryGeneratorInternal.ts",
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"path": "/packages/js/src/generators/library/schema.json",
|
"path": "/packages/js/src/generators/library/schema.json",
|
||||||
"type": "generator"
|
"type": "generator"
|
||||||
|
|||||||
@ -230,4 +230,55 @@ export function ${lib}Wildcard() {
|
|||||||
// restore nx.json
|
// restore nx.json
|
||||||
updateFile('nx.json', () => originalNxJson);
|
updateFile('nx.json', () => originalNxJson);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should generate project with name and directory as provided when --project-name-and-root-format=as-provided', async () => {
|
||||||
|
const lib1 = uniq('lib1');
|
||||||
|
runCLI(
|
||||||
|
`generate @nx/js:lib ${lib1} --directory=shared --bundler=tsc --project-name-and-root-format=as-provided`
|
||||||
|
);
|
||||||
|
|
||||||
|
// check files are generated without the layout directory ("libs/") and
|
||||||
|
// in the directory provided (no project name appended)
|
||||||
|
checkFilesExist('shared/src/index.ts');
|
||||||
|
// check project name is as provided (no prefixed directory name)
|
||||||
|
expect(runCLI(`build ${lib1}`)).toContain(
|
||||||
|
'Done compiling TypeScript files'
|
||||||
|
);
|
||||||
|
// check tests pass
|
||||||
|
const testResult = await runCLIAsync(`test ${lib1}`);
|
||||||
|
expect(testResult.combinedOutput).toContain(
|
||||||
|
'Test Suites: 1 passed, 1 total'
|
||||||
|
);
|
||||||
|
}, 500_000);
|
||||||
|
|
||||||
|
it('should support generating with a scoped project name when --project-name-and-root-format=as-provided', async () => {
|
||||||
|
const scopedLib = uniq('@my-org/lib1');
|
||||||
|
|
||||||
|
// assert scoped project names are not supported when --project-name-and-root-format=derived
|
||||||
|
expect(() =>
|
||||||
|
runCLI(
|
||||||
|
`generate @nx/js:lib ${scopedLib} --bundler=tsc --project-name-and-root-format=derived`
|
||||||
|
)
|
||||||
|
).toThrow();
|
||||||
|
|
||||||
|
runCLI(
|
||||||
|
`generate @nx/js:lib ${scopedLib} --bundler=tsc --project-name-and-root-format=as-provided`
|
||||||
|
);
|
||||||
|
|
||||||
|
// check files are generated without the layout directory ("libs/") and
|
||||||
|
// using the project name as the directory when no directory is provided
|
||||||
|
checkFilesExist(
|
||||||
|
`${scopedLib}/src/index.ts`,
|
||||||
|
`${scopedLib}/src/lib/${scopedLib.split('/')[1]}.ts`
|
||||||
|
);
|
||||||
|
// check build works
|
||||||
|
expect(runCLI(`build ${scopedLib}`)).toContain(
|
||||||
|
'Done compiling TypeScript files'
|
||||||
|
);
|
||||||
|
// check tests pass
|
||||||
|
const testResult = await runCLIAsync(`test ${scopedLib}`);
|
||||||
|
expect(testResult.combinedOutput).toContain(
|
||||||
|
'Test Suites: 1 passed, 1 total'
|
||||||
|
);
|
||||||
|
}, 500_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
"homepage": "https://nx.dev",
|
"homepage": "https://nx.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ejs": "^3.1.7",
|
"ejs": "^3.1.7",
|
||||||
|
"enquirer": "~2.3.6",
|
||||||
"ignore": "^5.0.4",
|
"ignore": "^5.0.4",
|
||||||
"tmp": "~0.2.1",
|
"tmp": "~0.2.1",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
|
|||||||
@ -0,0 +1,580 @@
|
|||||||
|
import * as enquirer from 'enquirer';
|
||||||
|
import { createTreeWithEmptyWorkspace } from 'nx/src/generators/testing-utils/create-tree-with-empty-workspace';
|
||||||
|
import type { Tree } from 'nx/src/generators/tree';
|
||||||
|
import { updateJson } from 'nx/src/generators/utils/json';
|
||||||
|
import { determineProjectNameAndRootOptions } from './project-name-and-root-utils';
|
||||||
|
|
||||||
|
describe('determineProjectNameAndRootOptions', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
let originalInteractiveValue;
|
||||||
|
let originalCIValue;
|
||||||
|
let originalIsTTYValue;
|
||||||
|
|
||||||
|
function ensureInteractiveMode() {
|
||||||
|
process.env.NX_INTERACTIVE = 'true';
|
||||||
|
process.env.CI = 'false';
|
||||||
|
process.stdout.isTTY = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreOriginalInteractiveMode() {
|
||||||
|
process.env.NX_INTERACTIVE = originalInteractiveValue;
|
||||||
|
process.env.CI = originalCIValue;
|
||||||
|
process.stdout.isTTY = originalIsTTYValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalInteractiveValue = process.env.NX_INTERACTIVE;
|
||||||
|
originalCIValue = process.env.CI;
|
||||||
|
originalIsTTYValue = process.stdout.isTTY;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('no layout', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the project name and directory as provided', async () => {
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toStrictEqual({
|
||||||
|
projectName: 'lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@proj/lib-name',
|
||||||
|
projectDirectory: 'shared',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use a scoped package name as the project name and import path when format is "as-provided"', async () => {
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '@scope/libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: '@scope/lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@scope/lib-name',
|
||||||
|
projectDirectory: 'shared',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use provided import path over scoped name when format is "as-provided"', async () => {
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '@scope/libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
importPath: '@custom-scope/lib-name',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: '@scope/lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@custom-scope/lib-name',
|
||||||
|
projectDirectory: 'shared',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the directory as the project name when directory is not provided and format is "as-provided"', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.name = 'lib-name';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '@scope/libName',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: '@scope/lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@scope/lib-name',
|
||||||
|
projectDirectory: '@scope/lib-name',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the project name and directory as provided for root projects', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.name = 'lib-name';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
rootProject: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: 'lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: 'lib-name',
|
||||||
|
projectDirectory: '.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should derive import path for root projects when package.json does not have a name and format is as-provided', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.name = undefined;
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
rootProject: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.importPath).toBe('@proj/lib-name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when an invalid name is provided', async () => {
|
||||||
|
await expect(
|
||||||
|
determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '!scope/libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
})
|
||||||
|
).rejects.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the derived project name and directory', async () => {
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'derived',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: 'shared-lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'shared-lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@proj/shared/lib-name',
|
||||||
|
projectDirectory: 'shared/lib-name',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when using a scoped package name as the project name and format is "derived"', async () => {
|
||||||
|
await expect(
|
||||||
|
determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '@scope/libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'derived',
|
||||||
|
})
|
||||||
|
).rejects.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the derived project name and directory for root projects', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.name = 'lib-name';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'derived',
|
||||||
|
rootProject: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: 'lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: 'lib-name',
|
||||||
|
projectDirectory: '.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should derive import path for root projects when package.json does not have a name and format is "derived"', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.name = undefined;
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
rootProject: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.importPath).toBe('@proj/lib-name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prompt for the project name and root format', async () => {
|
||||||
|
// simulate interactive mode
|
||||||
|
ensureInteractiveMode();
|
||||||
|
const promptSpy = jest
|
||||||
|
.spyOn(enquirer, 'prompt')
|
||||||
|
.mockImplementation(() => Promise.resolve({ format: 'as-provided' }));
|
||||||
|
|
||||||
|
await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
projectType: 'library',
|
||||||
|
directory: 'shared',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(promptSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'select',
|
||||||
|
name: 'format',
|
||||||
|
message:
|
||||||
|
'What should be the project name and where should it be generated?',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: `Recommended:
|
||||||
|
Name: lib-name
|
||||||
|
Root: shared`,
|
||||||
|
name: 'as-provided',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: `Legacy:
|
||||||
|
Name: shared-lib-name
|
||||||
|
Root: shared/lib-name`,
|
||||||
|
name: 'derived',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initial: 'as-provided',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// restore original interactive mode
|
||||||
|
restoreOriginalInteractiveMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should directly use format as-provided and not prompt when the name is a scoped package name', async () => {
|
||||||
|
// simulate interactive mode
|
||||||
|
ensureInteractiveMode();
|
||||||
|
const promptSpy = jest.spyOn(enquirer, 'prompt');
|
||||||
|
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '@scope/libName',
|
||||||
|
projectType: 'library',
|
||||||
|
directory: 'shared',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(promptSpy).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: '@scope/lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@scope/lib-name',
|
||||||
|
projectDirectory: 'shared',
|
||||||
|
});
|
||||||
|
|
||||||
|
// restore original interactive mode
|
||||||
|
restoreOriginalInteractiveMode();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with layout', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the project name and directory as provided', async () => {
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: 'lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@proj/lib-name',
|
||||||
|
projectDirectory: 'shared',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use a scoped package name as the project name and import path when format is "as-provided"', async () => {
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '@scope/libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: '@scope/lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@scope/lib-name',
|
||||||
|
projectDirectory: 'shared',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use provided import path over scoped name when format is "as-provided"', async () => {
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '@scope/libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
importPath: '@custom-scope/lib-name',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: '@scope/lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@custom-scope/lib-name',
|
||||||
|
projectDirectory: 'shared',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the directory as the project name when directory is not provided and format is "as-provided"', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.name = 'lib-name';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '@scope/libName',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: '@scope/lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@scope/lib-name',
|
||||||
|
projectDirectory: '@scope/lib-name',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the project name and directory as provided for root projects', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.name = 'lib-name';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
rootProject: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: 'lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: 'lib-name',
|
||||||
|
projectDirectory: '.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should derive import path for root projects when package.json does not have a name and format is "as-provided"', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.name = undefined;
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
rootProject: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.importPath).toBe('@proj/lib-name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when an invalid name is provided', async () => {
|
||||||
|
await expect(
|
||||||
|
determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '!scope/libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
})
|
||||||
|
).rejects.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the derived project name and directory', async () => {
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'derived',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: 'shared-lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'shared-lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@proj/shared/lib-name',
|
||||||
|
projectDirectory: 'libs/shared/lib-name',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when using a scoped package name as the project name and format is derived', async () => {
|
||||||
|
await expect(
|
||||||
|
determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '@scope/libName',
|
||||||
|
directory: 'shared',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'derived',
|
||||||
|
})
|
||||||
|
).rejects.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the derived project name and directory for root projects', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.name = 'lib-name';
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'derived',
|
||||||
|
rootProject: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: 'lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: 'lib-name',
|
||||||
|
projectDirectory: '.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should derive import path for root projects when package.json does not have a name and format is "derived"', async () => {
|
||||||
|
updateJson(tree, 'package.json', (json) => {
|
||||||
|
json.name = undefined;
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
projectType: 'library',
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
rootProject: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.importPath).toBe('@proj/lib-name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prompt for the project name and root format', async () => {
|
||||||
|
// simulate interactive mode
|
||||||
|
ensureInteractiveMode();
|
||||||
|
const promptSpy = jest
|
||||||
|
.spyOn(enquirer, 'prompt')
|
||||||
|
.mockImplementation(() => Promise.resolve({ format: 'as-provided' }));
|
||||||
|
|
||||||
|
await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: 'libName',
|
||||||
|
projectType: 'library',
|
||||||
|
directory: 'shared',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(promptSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'select',
|
||||||
|
name: 'format',
|
||||||
|
message:
|
||||||
|
'What should be the project name and where should it be generated?',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: `Recommended:
|
||||||
|
Name: lib-name
|
||||||
|
Root: shared`,
|
||||||
|
name: 'as-provided',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: `Legacy:
|
||||||
|
Name: shared-lib-name
|
||||||
|
Root: libs/shared/lib-name`,
|
||||||
|
name: 'derived',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initial: 'as-provided',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// restore original interactive mode
|
||||||
|
restoreOriginalInteractiveMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should directly use format as-provided and not prompt when the name is a scoped package name', async () => {
|
||||||
|
// simulate interactive mode
|
||||||
|
ensureInteractiveMode();
|
||||||
|
const promptSpy = jest.spyOn(enquirer, 'prompt');
|
||||||
|
|
||||||
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: '@scope/libName',
|
||||||
|
projectType: 'library',
|
||||||
|
directory: 'shared',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(promptSpy).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
projectName: '@scope/lib-name',
|
||||||
|
names: {
|
||||||
|
projectSimpleName: 'lib-name',
|
||||||
|
projectFileName: 'lib-name',
|
||||||
|
},
|
||||||
|
importPath: '@scope/lib-name',
|
||||||
|
projectDirectory: 'shared',
|
||||||
|
});
|
||||||
|
|
||||||
|
// restore original interactive mode
|
||||||
|
restoreOriginalInteractiveMode();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
290
packages/devkit/src/generators/project-name-and-root-utils.ts
Normal file
290
packages/devkit/src/generators/project-name-and-root-utils.ts
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
import { prompt } from 'enquirer';
|
||||||
|
import type { ProjectType } from 'nx/src/config/workspace-json-project-json';
|
||||||
|
import type { Tree } from 'nx/src/generators/tree';
|
||||||
|
import { requireNx } from '../../nx';
|
||||||
|
import {
|
||||||
|
extractLayoutDirectory,
|
||||||
|
getWorkspaceLayout,
|
||||||
|
} from '../utils/get-workspace-layout';
|
||||||
|
import { names } from '../utils/names';
|
||||||
|
|
||||||
|
const { joinPathFragments, readJson, readNxJson } = requireNx();
|
||||||
|
|
||||||
|
export type ProjectNameAndRootFormat = 'as-provided' | 'derived';
|
||||||
|
export type ProjectGenerationOptions = {
|
||||||
|
name: string;
|
||||||
|
projectType: ProjectType;
|
||||||
|
directory?: string;
|
||||||
|
importPath?: string;
|
||||||
|
projectNameAndRootFormat?: ProjectNameAndRootFormat;
|
||||||
|
rootProject?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProjectNameAndRootOptions = {
|
||||||
|
/**
|
||||||
|
* Normalized full project name, including scope if name was provided with
|
||||||
|
* scope (e.g., `@scope/name`, only available when `projectNameAndRootFormat`
|
||||||
|
* is `as-provided`).
|
||||||
|
*/
|
||||||
|
projectName: string;
|
||||||
|
/**
|
||||||
|
* Normalized project directory, including the layout directory if configured.
|
||||||
|
*/
|
||||||
|
projectDirectory: string;
|
||||||
|
names: {
|
||||||
|
/**
|
||||||
|
* Normalized project name without scope. It's meant to be used when
|
||||||
|
* generating file names that contain the project name.
|
||||||
|
*/
|
||||||
|
projectFileName: string;
|
||||||
|
/**
|
||||||
|
* Normalized project name without scope or directory. It's meant to be used
|
||||||
|
* when generating shorter file names that contain the project name.
|
||||||
|
*/
|
||||||
|
projectSimpleName: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Normalized import path for the project.
|
||||||
|
*/
|
||||||
|
importPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectNameAndRootFormats = {
|
||||||
|
'as-provided': {
|
||||||
|
description: string;
|
||||||
|
options: ProjectNameAndRootOptions;
|
||||||
|
};
|
||||||
|
derived?: {
|
||||||
|
description: string;
|
||||||
|
options: ProjectNameAndRootOptions;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function determineProjectNameAndRootOptions(
|
||||||
|
tree: Tree,
|
||||||
|
options: ProjectGenerationOptions
|
||||||
|
): Promise<ProjectNameAndRootOptions> {
|
||||||
|
validateName(options.name, options.projectNameAndRootFormat);
|
||||||
|
const formats = getProjectNameAndRootFormats(tree, options);
|
||||||
|
const format =
|
||||||
|
options.projectNameAndRootFormat ?? (await determineFormat(formats));
|
||||||
|
|
||||||
|
return formats[format].options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateName(
|
||||||
|
name: string,
|
||||||
|
projectNameAndRootFormat?: ProjectNameAndRootFormat
|
||||||
|
): void {
|
||||||
|
if (projectNameAndRootFormat === 'derived' && name.startsWith('@')) {
|
||||||
|
throw new Error(
|
||||||
|
`The project name "${name}" cannot start with "@" when the "projectNameAndRootFormat" is "derived".`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches two types of project names:
|
||||||
|
*
|
||||||
|
* 1. Valid npm package names (e.g., '@scope/name' or 'name').
|
||||||
|
* 2. Names starting with a letter and can contain any character except whitespace and ':'.
|
||||||
|
*
|
||||||
|
* The second case is to support the legacy behavior (^[a-zA-Z].*$) with the difference
|
||||||
|
* that it doesn't allow the ":" character. It was wrong to allow it because it would
|
||||||
|
* conflict with the notation for tasks.
|
||||||
|
*/
|
||||||
|
const pattern =
|
||||||
|
'(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$';
|
||||||
|
const validationRegex = new RegExp(pattern);
|
||||||
|
if (!validationRegex.test(name)) {
|
||||||
|
throw new Error(
|
||||||
|
`The project name should match the pattern "${pattern}". The provided value "${name}" does not match.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function determineFormat(
|
||||||
|
formats: ProjectNameAndRootFormats
|
||||||
|
): Promise<ProjectNameAndRootFormat> {
|
||||||
|
if (!formats.derived) {
|
||||||
|
return 'as-provided';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NX_INTERACTIVE !== 'true' || !isTTY()) {
|
||||||
|
return 'derived';
|
||||||
|
}
|
||||||
|
|
||||||
|
return await prompt<{ format: ProjectNameAndRootFormat }>({
|
||||||
|
type: 'select',
|
||||||
|
name: 'format',
|
||||||
|
message:
|
||||||
|
'What should be the project name and where should it be generated?',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: formats['as-provided'].description,
|
||||||
|
name: 'as-provided',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: formats['derived'].description,
|
||||||
|
name: 'derived',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initial: 'as-provided' as any,
|
||||||
|
}).then(({ format }) => format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProjectNameAndRootFormats(
|
||||||
|
tree: Tree,
|
||||||
|
options: ProjectGenerationOptions
|
||||||
|
): ProjectNameAndRootFormats {
|
||||||
|
const name = names(options.name).fileName;
|
||||||
|
const directory = options.directory?.replace(/^\.?\//, '');
|
||||||
|
|
||||||
|
const asProvidedProjectName = name;
|
||||||
|
const asProvidedProjectDirectory = directory
|
||||||
|
? names(directory).fileName
|
||||||
|
: options.rootProject
|
||||||
|
? '.'
|
||||||
|
: asProvidedProjectName;
|
||||||
|
|
||||||
|
if (name.startsWith('@')) {
|
||||||
|
const nameWithoutScope = asProvidedProjectName.split('/')[1];
|
||||||
|
return {
|
||||||
|
'as-provided': {
|
||||||
|
description: `Recommended:
|
||||||
|
Name: ${asProvidedProjectName}
|
||||||
|
Root: ${asProvidedProjectDirectory}`,
|
||||||
|
options: {
|
||||||
|
projectName: asProvidedProjectName,
|
||||||
|
names: {
|
||||||
|
projectSimpleName: nameWithoutScope,
|
||||||
|
projectFileName: nameWithoutScope,
|
||||||
|
},
|
||||||
|
importPath: options.importPath ?? asProvidedProjectName,
|
||||||
|
projectDirectory: asProvidedProjectDirectory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let asProvidedImportPath: string;
|
||||||
|
let npmScope: string;
|
||||||
|
if (options.projectType === 'library') {
|
||||||
|
asProvidedImportPath = options.importPath;
|
||||||
|
if (!asProvidedImportPath) {
|
||||||
|
npmScope = getNpmScope(tree);
|
||||||
|
asProvidedImportPath =
|
||||||
|
asProvidedProjectDirectory === '.'
|
||||||
|
? readJson<{ name?: string }>(tree, 'package.json').name ??
|
||||||
|
getImportPath(npmScope, asProvidedProjectName)
|
||||||
|
: getImportPath(npmScope, asProvidedProjectName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let { projectDirectory, layoutDirectory } = getDirectories(
|
||||||
|
tree,
|
||||||
|
directory,
|
||||||
|
options.projectType
|
||||||
|
);
|
||||||
|
const derivedProjectDirectoryWithoutLayout = projectDirectory
|
||||||
|
? `${names(projectDirectory).fileName}/${name}`
|
||||||
|
: options.rootProject
|
||||||
|
? '.'
|
||||||
|
: name;
|
||||||
|
// the project name uses the directory without the layout directory
|
||||||
|
const derivedProjectName =
|
||||||
|
derivedProjectDirectoryWithoutLayout === '.'
|
||||||
|
? name
|
||||||
|
: derivedProjectDirectoryWithoutLayout.replace(/\//g, '-');
|
||||||
|
const derivedSimpleProjectName = name;
|
||||||
|
let derivedProjectDirectory = derivedProjectDirectoryWithoutLayout;
|
||||||
|
if (derivedProjectDirectoryWithoutLayout !== '.') {
|
||||||
|
// prepend the layout directory
|
||||||
|
derivedProjectDirectory = joinPathFragments(
|
||||||
|
layoutDirectory,
|
||||||
|
derivedProjectDirectory
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let derivedImportPath: string;
|
||||||
|
if (options.projectType === 'library') {
|
||||||
|
derivedImportPath = options.importPath;
|
||||||
|
if (!derivedImportPath) {
|
||||||
|
derivedImportPath =
|
||||||
|
derivedProjectDirectory === '.'
|
||||||
|
? readJson<{ name?: string }>(tree, 'package.json').name ??
|
||||||
|
getImportPath(npmScope, derivedProjectName)
|
||||||
|
: getImportPath(npmScope, derivedProjectDirectoryWithoutLayout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'as-provided': {
|
||||||
|
description: `Recommended:
|
||||||
|
Name: ${asProvidedProjectName}
|
||||||
|
Root: ${asProvidedProjectDirectory}`,
|
||||||
|
options: {
|
||||||
|
projectName: asProvidedProjectName,
|
||||||
|
names: {
|
||||||
|
projectSimpleName: asProvidedProjectName,
|
||||||
|
projectFileName: asProvidedProjectName,
|
||||||
|
},
|
||||||
|
importPath: asProvidedImportPath,
|
||||||
|
projectDirectory: asProvidedProjectDirectory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
derived: {
|
||||||
|
description: `Legacy:
|
||||||
|
Name: ${derivedProjectName}
|
||||||
|
Root: ${derivedProjectDirectory}`,
|
||||||
|
options: {
|
||||||
|
projectName: derivedProjectName,
|
||||||
|
names: {
|
||||||
|
projectSimpleName: derivedSimpleProjectName,
|
||||||
|
projectFileName: derivedProjectName,
|
||||||
|
},
|
||||||
|
importPath: derivedImportPath,
|
||||||
|
projectDirectory: derivedProjectDirectory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDirectories(
|
||||||
|
tree: Tree,
|
||||||
|
directory: string | undefined,
|
||||||
|
projectType: ProjectType
|
||||||
|
): {
|
||||||
|
projectDirectory: string;
|
||||||
|
layoutDirectory: string;
|
||||||
|
} {
|
||||||
|
let { projectDirectory, layoutDirectory } = extractLayoutDirectory(directory);
|
||||||
|
if (!layoutDirectory) {
|
||||||
|
const { appsDir, libsDir } = getWorkspaceLayout(tree);
|
||||||
|
layoutDirectory = projectType === 'application' ? appsDir : libsDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { projectDirectory, layoutDirectory };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImportPath(npmScope: string | undefined, name: string) {
|
||||||
|
return npmScope ? `${npmScope === '@' ? '' : '@'}${npmScope}/${name}` : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNpmScope(tree: Tree): string | undefined {
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
|
||||||
|
// TODO(v17): Remove reading this from nx.json
|
||||||
|
if (nxJson?.npmScope) {
|
||||||
|
return nxJson.npmScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name } = tree.exists('package.json')
|
||||||
|
? readJson<{ name?: string }>(tree, 'package.json')
|
||||||
|
: { name: null };
|
||||||
|
|
||||||
|
return name?.startsWith('@') ? name.split('/')[0].substring(1) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTTY(): boolean {
|
||||||
|
return !!process.stdout.isTTY && process.env['CI'] !== 'true';
|
||||||
|
}
|
||||||
@ -38,7 +38,7 @@
|
|||||||
},
|
},
|
||||||
"generators": {
|
"generators": {
|
||||||
"library": {
|
"library": {
|
||||||
"factory": "./src/generators/library/library#libraryGenerator",
|
"factory": "./src/generators/library/library#libraryGeneratorInternal",
|
||||||
"schema": "./src/generators/library/schema.json",
|
"schema": "./src/generators/library/schema.json",
|
||||||
"aliases": ["lib"],
|
"aliases": ["lib"],
|
||||||
"x-type": "library",
|
"x-type": "library",
|
||||||
|
|||||||
@ -3,22 +3,23 @@ import {
|
|||||||
addProjectConfiguration,
|
addProjectConfiguration,
|
||||||
convertNxGenerator,
|
convertNxGenerator,
|
||||||
ensurePackage,
|
ensurePackage,
|
||||||
extractLayoutDirectory,
|
|
||||||
formatFiles,
|
formatFiles,
|
||||||
generateFiles,
|
generateFiles,
|
||||||
GeneratorCallback,
|
GeneratorCallback,
|
||||||
getWorkspaceLayout,
|
|
||||||
joinPathFragments,
|
joinPathFragments,
|
||||||
names,
|
names,
|
||||||
offsetFromRoot,
|
offsetFromRoot,
|
||||||
ProjectConfiguration,
|
ProjectConfiguration,
|
||||||
readJson,
|
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
toJS,
|
toJS,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
updateJson,
|
||||||
writeJson,
|
writeJson,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
|
import {
|
||||||
|
determineProjectNameAndRootOptions,
|
||||||
|
type ProjectNameAndRootOptions,
|
||||||
|
} from '@nx/devkit/src/generators/project-name-and-root-utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addTsConfigPath,
|
addTsConfigPath,
|
||||||
@ -29,7 +30,6 @@ import { addMinimalPublishScript } from '../../utils/minimal-publish-script';
|
|||||||
import { Bundler, LibraryGeneratorSchema } from '../../utils/schema';
|
import { Bundler, LibraryGeneratorSchema } from '../../utils/schema';
|
||||||
import { addSwcConfig } from '../../utils/swc/add-swc-config';
|
import { addSwcConfig } from '../../utils/swc/add-swc-config';
|
||||||
import { addSwcDependencies } from '../../utils/swc/add-swc-dependencies';
|
import { addSwcDependencies } from '../../utils/swc/add-swc-dependencies';
|
||||||
import { getImportPath } from '../../utils/get-import-path';
|
|
||||||
import {
|
import {
|
||||||
esbuildVersion,
|
esbuildVersion,
|
||||||
nxVersion,
|
nxVersion,
|
||||||
@ -46,22 +46,20 @@ export async function libraryGenerator(
|
|||||||
tree: Tree,
|
tree: Tree,
|
||||||
schema: LibraryGeneratorSchema
|
schema: LibraryGeneratorSchema
|
||||||
) {
|
) {
|
||||||
const { layoutDirectory, projectDirectory } = extractLayoutDirectory(
|
return await libraryGeneratorInternal(tree, {
|
||||||
schema.directory
|
// provide a default projectNameAndRootFormat to avoid breaking changes
|
||||||
);
|
// to external generators invoking this one
|
||||||
schema.directory = projectDirectory;
|
projectNameAndRootFormat: 'derived',
|
||||||
const libsDir = schema.rootProject
|
...schema,
|
||||||
? '.'
|
});
|
||||||
: layoutDirectory ?? getWorkspaceLayout(tree).libsDir;
|
|
||||||
return projectGenerator(tree, schema, libsDir, join(__dirname, './files'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function projectGenerator(
|
export async function libraryGeneratorInternal(
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
schema: LibraryGeneratorSchema,
|
schema: LibraryGeneratorSchema
|
||||||
destinationDir: string,
|
|
||||||
filesDir: string
|
|
||||||
) {
|
) {
|
||||||
|
const filesDir = join(__dirname, './files');
|
||||||
|
|
||||||
const tasks: GeneratorCallback[] = [];
|
const tasks: GeneratorCallback[] = [];
|
||||||
tasks.push(
|
tasks.push(
|
||||||
await jsInitGenerator(tree, {
|
await jsInitGenerator(tree, {
|
||||||
@ -70,11 +68,11 @@ export async function projectGenerator(
|
|||||||
tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json',
|
tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const options = normalizeOptions(tree, schema, destinationDir);
|
const options = await normalizeOptions(tree, schema);
|
||||||
|
|
||||||
createFiles(tree, options, `${filesDir}/lib`);
|
createFiles(tree, options, `${filesDir}/lib`);
|
||||||
|
|
||||||
addProject(tree, options, destinationDir);
|
addProject(tree, options);
|
||||||
|
|
||||||
tasks.push(addProjectDependencies(tree, options));
|
tasks.push(addProjectDependencies(tree, options));
|
||||||
|
|
||||||
@ -144,18 +142,14 @@ export async function projectGenerator(
|
|||||||
|
|
||||||
export interface NormalizedSchema extends LibraryGeneratorSchema {
|
export interface NormalizedSchema extends LibraryGeneratorSchema {
|
||||||
name: string;
|
name: string;
|
||||||
|
projectNames: ProjectNameAndRootOptions['names'];
|
||||||
fileName: string;
|
fileName: string;
|
||||||
projectRoot: string;
|
projectRoot: string;
|
||||||
projectDirectory: string;
|
|
||||||
parsedTags: string[];
|
parsedTags: string[];
|
||||||
importPath?: string;
|
importPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addProject(
|
function addProject(tree: Tree, options: NormalizedSchema) {
|
||||||
tree: Tree,
|
|
||||||
options: NormalizedSchema,
|
|
||||||
destinationDir: string
|
|
||||||
) {
|
|
||||||
const projectConfiguration: ProjectConfiguration = {
|
const projectConfiguration: ProjectConfiguration = {
|
||||||
root: options.projectRoot,
|
root: options.projectRoot,
|
||||||
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
|
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
|
||||||
@ -169,7 +163,7 @@ function addProject(
|
|||||||
options.bundler !== 'none' &&
|
options.bundler !== 'none' &&
|
||||||
options.config !== 'npm-scripts'
|
options.config !== 'npm-scripts'
|
||||||
) {
|
) {
|
||||||
const outputPath = getOutputPath(options, destinationDir);
|
const outputPath = getOutputPath(options);
|
||||||
projectConfiguration.targets.build = {
|
projectConfiguration.targets.build = {
|
||||||
executor: getBuildExecutor(options.bundler),
|
executor: getBuildExecutor(options.bundler),
|
||||||
outputs: ['{options.outputPath}'],
|
outputs: ['{options.outputPath}'],
|
||||||
@ -233,9 +227,19 @@ function addProject(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AddLintOptions = Pick<
|
||||||
|
NormalizedSchema,
|
||||||
|
| 'name'
|
||||||
|
| 'linter'
|
||||||
|
| 'projectRoot'
|
||||||
|
| 'unitTestRunner'
|
||||||
|
| 'js'
|
||||||
|
| 'setParserOptionsProject'
|
||||||
|
| 'rootProject'
|
||||||
|
>;
|
||||||
export async function addLint(
|
export async function addLint(
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
options: NormalizedSchema
|
options: AddLintOptions
|
||||||
): Promise<GeneratorCallback> {
|
): Promise<GeneratorCallback> {
|
||||||
const { lintProjectGenerator } = ensurePackage('@nx/linter', nxVersion);
|
const { lintProjectGenerator } = ensurePackage('@nx/linter', nxVersion);
|
||||||
const task = lintProjectGenerator(tree, {
|
const task = lintProjectGenerator(tree, {
|
||||||
@ -316,7 +320,9 @@ function addBabelRc(tree: Tree, options: NormalizedSchema) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createFiles(tree: Tree, options: NormalizedSchema, filesDir: string) {
|
function createFiles(tree: Tree, options: NormalizedSchema, filesDir: string) {
|
||||||
const { className, name, propertyName } = names(options.name);
|
const { className, name, propertyName } = names(
|
||||||
|
options.projectNames.projectFileName
|
||||||
|
);
|
||||||
|
|
||||||
createProjectTsConfigJson(tree, options);
|
createProjectTsConfigJson(tree, options);
|
||||||
|
|
||||||
@ -458,11 +464,10 @@ function replaceJestConfig(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeOptions(
|
async function normalizeOptions(
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
options: LibraryGeneratorSchema,
|
options: LibraryGeneratorSchema
|
||||||
destinationDir: string
|
): Promise<NormalizedSchema> {
|
||||||
): NormalizedSchema {
|
|
||||||
/**
|
/**
|
||||||
* We are deprecating the compiler and the buildable options.
|
* We are deprecating the compiler and the buildable options.
|
||||||
* However, we want to keep the existing behavior for now.
|
* However, we want to keep the existing behavior for now.
|
||||||
@ -523,13 +528,6 @@ function normalizeOptions(
|
|||||||
options.skipTypeCheck = false;
|
options.skipTypeCheck = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = names(options.name).fileName;
|
|
||||||
const projectDirectory = options.directory
|
|
||||||
? `${names(options.directory).fileName}/${name}`
|
|
||||||
: options.rootProject
|
|
||||||
? '.'
|
|
||||||
: name;
|
|
||||||
|
|
||||||
if (!options.unitTestRunner && options.bundler === 'vite') {
|
if (!options.unitTestRunner && options.bundler === 'vite') {
|
||||||
options.unitTestRunner = 'vitest';
|
options.unitTestRunner = 'vitest';
|
||||||
} else if (!options.unitTestRunner && options.config !== 'npm-scripts') {
|
} else if (!options.unitTestRunner && options.config !== 'npm-scripts') {
|
||||||
@ -540,32 +538,39 @@ function normalizeOptions(
|
|||||||
options.linter = Linter.EsLint;
|
options.linter = Linter.EsLint;
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectName = options.rootProject
|
const {
|
||||||
? name
|
projectName,
|
||||||
: projectDirectory.replace(new RegExp('/', 'g'), '-');
|
names: projectNames,
|
||||||
|
projectDirectory,
|
||||||
|
importPath,
|
||||||
|
} = await determineProjectNameAndRootOptions(tree, {
|
||||||
|
name: options.name,
|
||||||
|
projectType: 'library',
|
||||||
|
directory: options.directory,
|
||||||
|
importPath: options.importPath,
|
||||||
|
projectNameAndRootFormat: options.projectNameAndRootFormat,
|
||||||
|
rootProject: options.rootProject,
|
||||||
|
});
|
||||||
|
options.rootProject = projectDirectory === '.';
|
||||||
const fileName = getCaseAwareFileName({
|
const fileName = getCaseAwareFileName({
|
||||||
fileName: options.simpleName ? name : projectName,
|
fileName: options.simpleName
|
||||||
|
? projectNames.projectSimpleName
|
||||||
|
: projectNames.projectFileName,
|
||||||
pascalCaseFiles: options.pascalCaseFiles,
|
pascalCaseFiles: options.pascalCaseFiles,
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectRoot = joinPathFragments(destinationDir, projectDirectory);
|
|
||||||
|
|
||||||
const parsedTags = options.tags
|
const parsedTags = options.tags
|
||||||
? options.tags.split(',').map((s) => s.trim())
|
? options.tags.split(',').map((s) => s.trim())
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const importPath = options.rootProject
|
|
||||||
? readJson(tree, 'package.json').name ?? getImportPath(tree, 'core')
|
|
||||||
: options.importPath || getImportPath(tree, projectDirectory);
|
|
||||||
|
|
||||||
options.minimal ??= false;
|
options.minimal ??= false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...options,
|
...options,
|
||||||
fileName,
|
fileName,
|
||||||
name: projectName,
|
name: projectName,
|
||||||
projectRoot,
|
projectNames,
|
||||||
projectDirectory,
|
projectRoot: projectDirectory,
|
||||||
parsedTags,
|
parsedTags,
|
||||||
importPath,
|
importPath,
|
||||||
};
|
};
|
||||||
@ -631,15 +636,12 @@ function getBuildExecutor(bundler: Bundler) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOutputPath(options: NormalizedSchema, destinationDir?: string) {
|
function getOutputPath(options: NormalizedSchema) {
|
||||||
const parts = ['dist'];
|
const parts = ['dist'];
|
||||||
if (destinationDir) {
|
if (options.projectRoot === '.') {
|
||||||
parts.push(destinationDir);
|
|
||||||
}
|
|
||||||
if (options.projectDirectory === '.') {
|
|
||||||
parts.push(options.name);
|
parts.push(options.name);
|
||||||
} else {
|
} else {
|
||||||
parts.push(options.projectDirectory);
|
parts.push(options.projectRoot);
|
||||||
}
|
}
|
||||||
return joinPathFragments(...parts);
|
return joinPathFragments(...parts);
|
||||||
}
|
}
|
||||||
@ -741,4 +743,4 @@ function determineEntryFields(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default libraryGenerator;
|
export default libraryGenerator;
|
||||||
export const librarySchematic = convertNxGenerator(libraryGenerator);
|
export const librarySchematic = convertNxGenerator(libraryGeneratorInternal);
|
||||||
|
|||||||
@ -14,13 +14,18 @@
|
|||||||
"index": 0
|
"index": 0
|
||||||
},
|
},
|
||||||
"x-prompt": "What name would you like to use for the library?",
|
"x-prompt": "What name would you like to use for the library?",
|
||||||
"pattern": "^[a-zA-Z].*$"
|
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$"
|
||||||
},
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "A directory where the lib is placed.",
|
"description": "A directory where the lib is placed.",
|
||||||
"x-priority": "important"
|
"x-priority": "important"
|
||||||
},
|
},
|
||||||
|
"projectNameAndRootFormat": {
|
||||||
|
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["as-provided", "derived"]
|
||||||
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"description": "The tool to use for running lint checks.",
|
"description": "The tool to use for running lint checks.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
2
packages/js/src/utils/schema.d.ts
vendored
2
packages/js/src/utils/schema.d.ts
vendored
@ -1,3 +1,4 @@
|
|||||||
|
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-directory-utils';
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
const { Linter } = require('@nx/linter'); // use require to import to avoid circular dependency
|
const { Linter } = require('@nx/linter'); // use require to import to avoid circular dependency
|
||||||
import type { AssetGlob, FileInputOutput } from './assets/assets';
|
import type { AssetGlob, FileInputOutput } from './assets/assets';
|
||||||
@ -9,6 +10,7 @@ export type Bundler = 'swc' | 'tsc' | 'rollup' | 'vite' | 'esbuild' | 'none';
|
|||||||
export interface LibraryGeneratorSchema {
|
export interface LibraryGeneratorSchema {
|
||||||
name: string;
|
name: string;
|
||||||
directory?: string;
|
directory?: string;
|
||||||
|
projectNameAndRootFormat?: ProjectNameAndRootFormat;
|
||||||
skipFormat?: boolean;
|
skipFormat?: boolean;
|
||||||
tags?: string;
|
tags?: string;
|
||||||
skipTsConfig?: boolean;
|
skipTsConfig?: boolean;
|
||||||
|
|||||||
@ -9,10 +9,7 @@ import {
|
|||||||
addLintingToApplication,
|
addLintingToApplication,
|
||||||
NormalizedSchema as AddLintForApplicationSchema,
|
NormalizedSchema as AddLintForApplicationSchema,
|
||||||
} from '@nx/node/src/generators/application/application';
|
} from '@nx/node/src/generators/application/application';
|
||||||
import {
|
import { addLint as addLintingToLibraryGenerator } from '@nx/js/src/generators/library/library';
|
||||||
addLint as addLintingToLibraryGenerator,
|
|
||||||
NormalizedSchema as AddLintForLibrarySchema,
|
|
||||||
} from '@nx/js/src/generators/library/library';
|
|
||||||
import type { Linter } from 'eslint';
|
import type { Linter } from 'eslint';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,11 +65,7 @@ export async function conversionGenerator(
|
|||||||
projectRoot: projectConfig.root,
|
projectRoot: projectConfig.root,
|
||||||
js,
|
js,
|
||||||
setParserOptionsProject,
|
setParserOptionsProject,
|
||||||
projectDirectory: '',
|
});
|
||||||
fileName: '',
|
|
||||||
parsedTags: [],
|
|
||||||
skipFormat: true,
|
|
||||||
} as AddLintForLibrarySchema);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user