diff --git a/packages/devkit/src/generators/artifact-name-and-directory-utils.ts b/packages/devkit/src/generators/artifact-name-and-directory-utils.ts index fcf2872ee42481..ac71f7d5c5f357 100644 --- a/packages/devkit/src/generators/artifact-name-and-directory-utils.ts +++ b/packages/devkit/src/generators/artifact-name-and-directory-utils.ts @@ -23,7 +23,7 @@ export type ArtifactGenerationOptions = { name: string; directory?: string; disallowPathInNameForDerived?: boolean; - fileExtension?: 'js' | 'jsx' | 'ts' | 'tsx'; + fileExtension?: 'js' | 'jsx' | 'ts' | 'tsx' | 'vue'; fileName?: string; flat?: boolean; nameAndDirectoryFormat?: NameAndDirectoryFormat; diff --git a/packages/vue/generators.json b/packages/vue/generators.json index b601513ac50360..77c2beff5d8dd8 100644 --- a/packages/vue/generators.json +++ b/packages/vue/generators.json @@ -23,7 +23,7 @@ "description": "Create a Vue library." }, "component": { - "factory": "./src/generators/component/component", + "factory": "./src/generators/component/component#componentGeneratorInternal", "schema": "./src/generators/component/schema.json", "aliases": ["c"], "x-type": "component", diff --git a/packages/vue/src/generators/component/component.spec.ts b/packages/vue/src/generators/component/component.spec.ts index 0fc89e4d16e4cb..b2be38e02c316d 100644 --- a/packages/vue/src/generators/component/component.spec.ts +++ b/packages/vue/src/generators/component/component.spec.ts @@ -29,12 +29,34 @@ describe('component', () => { unitTestRunner: 'vitest', }); - expect( - appTree.read(`${libName}/src/components/hello/hello.vue`, 'utf-8') - ).toMatchSnapshot(); - expect( - appTree.read(`${libName}/src/components/hello/hello.spec.ts`, 'utf-8') - ).toMatchSnapshot(); + expect(appTree.read(`${libName}/src/lib/hello/hello.vue`, 'utf-8')) + .toMatchInlineSnapshot(` + " + + + + + " + `); + expect(appTree.read(`${libName}/src/lib/hello/hello.spec.ts`, 'utf-8')) + .toMatchInlineSnapshot(` + "import { describe, it, expect } from 'vitest'; + + import { mount } from '@vue/test-utils'; + import MyLibHello from './hello.vue'; + + describe('MyLibHello', () => { + it('renders properly', () => { + const wrapper = mount(MyLibHello, {}); + expect(wrapper.text()).toContain('Welcome to MyLibHello'); + }); + }); + " + `); }); it('should generate files with jest', async () => { @@ -44,12 +66,32 @@ describe('component', () => { unitTestRunner: 'jest', }); - expect( - appTree.read(`${libName}/src/components/hello/hello.vue`, 'utf-8') - ).toMatchSnapshot(); - expect( - appTree.read(`${libName}/src/components/hello/hello.spec.ts`, 'utf-8') - ).toMatchSnapshot(); + expect(appTree.read(`${libName}/src/lib/hello/hello.vue`, 'utf-8')) + .toMatchInlineSnapshot(` + " + + + + + " + `); + expect(appTree.read(`${libName}/src/lib/hello/hello.spec.ts`, 'utf-8')) + .toMatchInlineSnapshot(` + "import { mount } from '@vue/test-utils'; + import MyLibHello from './hello.vue'; + + describe('MyLibHello', () => { + it('renders properly', () => { + const wrapper = mount(MyLibHello, {}); + expect(wrapper.text()).toContain('Welcome to MyLibHello'); + }); + }); + " + `); }); it('should have correct component name based on directory', async () => { @@ -61,7 +103,10 @@ describe('component', () => { }); expect( - appTree.read(`${libName}/src/foo/bar/hello-world.vue`, 'utf-8') + appTree.read( + `${libName}/src/foo/bar/hello-world/hello-world.vue`, + 'utf-8' + ) ).toContain('FooBarHelloWorld'); }); @@ -74,7 +119,10 @@ describe('component', () => { }); expect( - appTree.read(`${libName}/src/foo/bar-baz/hello-world.vue`, 'utf-8') + appTree.read( + `${libName}/src/foo/bar-baz/hello-world/hello-world.vue`, + 'utf-8' + ) ).toContain('FooBarBazHelloWorld'); }); @@ -86,10 +134,10 @@ describe('component', () => { }); expect( - appTree.read(`${appName}/src/components/hello/hello.vue`, 'utf-8') + appTree.read(`${appName}/src/app/hello/hello.vue`, 'utf-8') ).toContain('AppHello'); expect( - appTree.exists(`${appName}/src/components/hello/hello.spec.ts`) + appTree.exists(`${appName}/src/app/hello/hello.spec.ts`) ).toBeTruthy(); }); @@ -100,9 +148,11 @@ describe('component', () => { project: libName, export: true, }); - expect( - appTree.read(`${libName}/src/index.ts`, 'utf-8') - ).toMatchSnapshot(); + expect(appTree.read(`${libName}/src/index.ts`, 'utf-8')) + .toMatchInlineSnapshot(` + "export { default as MyLibHello } from './lib/hello/hello.vue'; + " + `); }); it('should not export from an app', async () => { @@ -114,7 +164,7 @@ describe('component', () => { expect( appTree.read(`${appName}/src/index.ts`, 'utf-8') - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(`null`); }); }); @@ -127,10 +177,10 @@ describe('component', () => { directory: 'foo/bar', }); expect( - appTree.read(`${libName}/src/foo/bar/Hello.vue`, 'utf-8') + appTree.read(`${libName}/src/foo/bar/hello/Hello.vue`, 'utf-8') ).toContain('FooBarHello'); expect( - appTree.exists(`${libName}/src/foo/bar/Hello.spec.ts`) + appTree.exists(`${libName}/src/foo/bar/hello/Hello.spec.ts`) ).toBeTruthy(); }); }); @@ -144,12 +194,10 @@ describe('component', () => { pascalCaseDirectory: true, }); expect( - appTree.exists(`${libName}/src/components/HelloWorld/HelloWorld.vue`) + appTree.exists(`${libName}/src/lib/HelloWorld/HelloWorld.vue`) ).toBeTruthy(); expect( - appTree.exists( - `${libName}/src/components/HelloWorld/HelloWorld.spec.ts` - ) + appTree.exists(`${libName}/src/lib/HelloWorld/HelloWorld.spec.ts`) ).toBeTruthy(); }); }); @@ -162,7 +210,7 @@ describe('component', () => { flat: true, }); - expect(appTree.exists(`${libName}/src/components/hello.vue`)); + expect(appTree.exists(`${libName}/src/lib/hello.vue`)); }); it('should work with custom directory path', async () => { await componentGenerator(appTree, { diff --git a/packages/vue/src/generators/component/component.ts b/packages/vue/src/generators/component/component.ts index b6a4c7f5b9b23d..67dfd0fd0fb520 100644 --- a/packages/vue/src/generators/component/component.ts +++ b/packages/vue/src/generators/component/component.ts @@ -3,7 +3,6 @@ import { generateFiles, GeneratorCallback, getProjects, - joinPathFragments, runTasksInSerial, toJS, Tree, @@ -13,6 +12,13 @@ import { join } from 'path'; import { addExportsToBarrel, normalizeOptions } from './lib/utils'; export async function componentGenerator(host: Tree, schema: Schema) { + return componentGeneratorInternal(host, { + nameAndDirectoryFormat: 'derived', + ...schema, + }); +} + +export async function componentGeneratorInternal(host: Tree, schema: Schema) { const workspace = getProjects(host); const isApp = workspace.get(schema.project).projectType === 'application'; @@ -32,12 +38,7 @@ export async function componentGenerator(host: Tree, schema: Schema) { } function createComponentFiles(host: Tree, options: NormalizedSchema) { - const componentDir = joinPathFragments( - options.projectSourceRoot, - options.directory - ); - - generateFiles(host, join(__dirname, './files'), componentDir, { + generateFiles(host, join(__dirname, './files'), options.directory, { ...options, tmpl: '', unitTestRunner: options.unitTestRunner, diff --git a/packages/vue/src/generators/component/lib/utils.ts b/packages/vue/src/generators/component/lib/utils.ts index c9e4188eeb015d..1d141429e4e804 100644 --- a/packages/vue/src/generators/component/lib/utils.ts +++ b/packages/vue/src/generators/component/lib/utils.ts @@ -6,8 +6,11 @@ import { names, Tree, } from '@nx/devkit'; -import { NormalizedSchema, Schema } from '../schema'; +import { parse, relative, dirname } from 'path'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; + +import { NormalizedSchema, Schema } from '../schema'; import { addImport } from '../../../utils/ast-utils'; let tsModule: typeof import('typescript'); @@ -19,17 +22,29 @@ export async function normalizeOptions( ): Promise { assertValidOptions(options); - let { className, fileName } = names(options.name); - const componentFileName = - options.fileName ?? (options.pascalCaseFiles ? className : fileName); - const project = getProjects(host).get(options.project); - - if (!project) { - throw new Error( - `Cannot find the ${options.project} project. Please double check the project name.` - ); - } + const { + artifactName: name, + directory, + fileName, + filePath, + project: projectName, + } = await determineArtifactNameAndDirectoryOptions(host, { + artifactType: 'component', + callingGenerator: '@nx/vue:component', + name: options.name, + directory: options.directory, + derivedDirectory: options.directory, + flat: options.flat, + nameAndDirectoryFormat: options.nameAndDirectoryFormat, + project: options.project, + fileExtension: 'vue', + pascalCaseFile: options.pascalCaseFiles, + pascalCaseDirectory: options.pascalCaseDirectory, + }); + let { className } = names(fileName); + const componentFileName = fileName; + const project = getProjects(host).get(projectName); const { sourceRoot: projectSourceRoot, projectType } = project; className = getComponentClassName( @@ -39,8 +54,6 @@ export async function normalizeOptions( options.directory ); - const directory = await getDirectory(options); - if (options.export && projectType === 'application') { logger.warn( `The "--export" option should not be used with applications and will do nothing.` @@ -52,6 +65,7 @@ export async function normalizeOptions( return { ...options, + filePath, directory, className, fileName: componentFileName, @@ -106,11 +120,16 @@ export function addExportsToBarrel( tsModule.ScriptTarget.Latest, true ); + + const relativePathFromIndex = getRelativeImportToFile( + indexFilePath, + options.filePath + ); const changes = applyChangesToString( indexSource, addImport( indexSourceFile, - `export { default as ${options.className} } from './${options.directory}/${options.fileName}.vue';` + `export { default as ${options.className} } from '${relativePathFromIndex}';` ) ); host.write(indexFilePath, changes); @@ -118,12 +137,10 @@ export function addExportsToBarrel( } } -export async function getDirectory(options: Schema) { - if (options.directory) return options.directory; - if (options.flat) return 'components'; - const { className, fileName } = names(options.name); - const nestedDir = options.pascalCaseDirectory === true ? className : fileName; - return joinPathFragments('components', nestedDir); +function getRelativeImportToFile(indexPath: string, filePath: string) { + const { base, dir } = parse(filePath); + const relativeDirToTarget = relative(dirname(indexPath), dir); + return `./${joinPathFragments(relativeDirToTarget, base)}`; } export function assertValidOptions(options: Schema) { diff --git a/packages/vue/src/generators/component/schema.d.ts b/packages/vue/src/generators/component/schema.d.ts index 42cae02cd4f3cf..1080d6b4f66b78 100644 --- a/packages/vue/src/generators/component/schema.d.ts +++ b/packages/vue/src/generators/component/schema.d.ts @@ -13,10 +13,12 @@ export interface Schema { inSourceTests?: boolean; skipFormat?: boolean; unitTestRunner?: 'jest' | 'vitest' | 'none'; + nameAndDirectoryFormat?: 'as-provided' | 'derived'; } export interface NormalizedSchema extends Schema { projectSourceRoot: string; fileName: string; className: string; + filePath: string; } diff --git a/packages/vue/src/generators/component/schema.json b/packages/vue/src/generators/component/schema.json index 27a4305b959954..d9d4bbddcf9de7 100644 --- a/packages/vue/src/generators/component/schema.json +++ b/packages/vue/src/generators/component/schema.json @@ -22,9 +22,7 @@ "alias": "p", "$default": { "$source": "projectName" - }, - "x-prompt": "What is the name of the project for this component?", - "x-priority": "important" + } }, "name": { "type": "string", @@ -36,6 +34,11 @@ "x-prompt": "What name would you like to use for the component?", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "js": { "type": "boolean", "description": "Generate JavaScript files rather than TypeScript files.", @@ -56,7 +59,8 @@ "flat": { "type": "boolean", "description": "Create component at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. It will be removed in Nx v18." }, "export": { "type": "boolean", @@ -103,5 +107,5 @@ "x-prompt": "What unit test runner should be used?" } }, - "required": ["name", "project"] + "required": ["name"] }