From c3d69600982b225ac3df1d124a05076096dc436f Mon Sep 17 00:00:00 2001 From: Dawid Harat Date: Thu, 23 Jul 2026 11:53:41 +0200 Subject: [PATCH 1/6] feat: add configurable component wrapper setting --- README.md | 10 ++++++++++ package.json | 10 ++++++++++ src/helpers/buildSnippets.ts | 2 +- src/helpers/extensionConfig.ts | 10 +++++++++- src/helpers/snippetPlaceholders.ts | 14 ++++++++++++-- src/sourceSnippets/sharedSnippets.ts | 4 ++-- src/types.ts | 7 +++++++ tests/generation.test.ts | 20 +++++++++++++++++++- 8 files changed, 70 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cf1dfb6..291f8bd 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,16 @@ Accepts a comma-separated list of VS Code language identifiers. Controls whether Props and State declarations use type aliases or interfaces in TypeScript snippets. +### `componentWrapper` + +**Type:** `string` + +**Default:** `fragment` + +**Allowed values:** `fragment`, `div` + +Controls whether generated web component content is wrapped in a React fragment or a `div`. + ## Editor support ### VS Code · Cursor · VSCodium diff --git a/package.json b/package.json index c89cebe..c806d95 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,16 @@ "interface" ], "scope": "application" + }, + "reactSnippets.settings.componentWrapper": { + "type": "string", + "markdownDescription": "Controls whether component snippets wrap their content in a React fragment or a `div`.", + "default": "fragment", + "enum": [ + "fragment", + "div" + ], + "scope": "application" } } }, diff --git a/src/helpers/buildSnippets.ts b/src/helpers/buildSnippets.ts index 8bad867..4f91a03 100644 --- a/src/helpers/buildSnippets.ts +++ b/src/helpers/buildSnippets.ts @@ -167,7 +167,7 @@ const compileSnippet = ( ): GeneratedSnippet => ({ prefix: snippet.prefix, body: parseSnippetToBody(snippet, settings.importReactOnTop).map((line) => - replaceSnippetPlaceholders(line, settings.typescriptPropsStatePrefix), + replaceSnippetPlaceholders(line, settings), ), ...(snippet.description === undefined ? {} diff --git a/src/helpers/extensionConfig.ts b/src/helpers/extensionConfig.ts index 7cb6c6a..1a1bcde 100644 --- a/src/helpers/extensionConfig.ts +++ b/src/helpers/extensionConfig.ts @@ -1,6 +1,10 @@ import { workspace } from 'vscode'; -import { DEFAULT_GENERATION_SETTINGS, GenerationSettings } from '../types'; +import { + ComponentWrapper, + DEFAULT_GENERATION_SETTINGS, + GenerationSettings, +} from '../types'; export type ExtensionSettings = GenerationSettings; @@ -24,6 +28,10 @@ const extensionConfig = (): ExtensionSettings => { 'typescriptPropsStatePrefix', DEFAULT_GENERATION_SETTINGS.typescriptPropsStatePrefix, ), + componentWrapper: config.get( + 'componentWrapper', + DEFAULT_GENERATION_SETTINGS.componentWrapper, + ), }; }; diff --git a/src/helpers/snippetPlaceholders.ts b/src/helpers/snippetPlaceholders.ts index 24856b8..678b970 100644 --- a/src/helpers/snippetPlaceholders.ts +++ b/src/helpers/snippetPlaceholders.ts @@ -1,9 +1,14 @@ -import { GenerationSettings, Mappings, Placeholders } from '../types'; +import { + GenerationSettings, + Mappings, + Placeholders, +} from '../types'; export const replaceSnippetPlaceholders = ( snippetString: string, - typescriptPropsStatePrefix: GenerationSettings['typescriptPropsStatePrefix'], + settings: GenerationSettings, ): string => { + const { componentWrapper, typescriptPropsStatePrefix } = settings; const propsPlaceholder = typescriptPropsStatePrefix === 'type' ? Mappings.TypeProps @@ -12,9 +17,14 @@ export const replaceSnippetPlaceholders = ( typescriptPropsStatePrefix === 'type' ? Mappings.TypeState : Mappings.InterfaceState; + const componentWrapperPlaceholder = + componentWrapper === 'fragment' + ? Mappings.FragmentComponentWrapper + : Mappings.DivComponentWrapper; return String(snippetString) .replaceAll(Placeholders.FileName, Mappings.FileName) + .replaceAll(Placeholders.ComponentWrapper, componentWrapperPlaceholder) .replaceAll(Placeholders.FirstTab, Mappings.FirstTab) .replaceAll(Placeholders.SecondTab, Mappings.SecondTab) .replaceAll(Placeholders.ThirdTab, Mappings.ThirdTab) diff --git a/src/sourceSnippets/sharedSnippets.ts b/src/sourceSnippets/sharedSnippets.ts index f4b4b50..24fba54 100644 --- a/src/sourceSnippets/sharedSnippets.ts +++ b/src/sourceSnippets/sharedSnippets.ts @@ -42,14 +42,14 @@ export const reduxTypeScriptComponentExport = [ export const innerComponent = [ ' return (', - ` <>${Placeholders.FirstTab}`, + ` ${Placeholders.ComponentWrapper}`, ' )', ]; export const innerComponentReturn = [ ' render() {', ' return (', - ` <>${Placeholders.FirstTab}`, + ` ${Placeholders.ComponentWrapper}`, ' )', ' }', ]; diff --git a/src/types.ts b/src/types.ts index 01422c8..f4ae433 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,11 +15,14 @@ export const VALID_LANGUAGE_SCOPES = [ export type LanguageScope = (typeof VALID_LANGUAGE_SCOPES)[number]; +export type ComponentWrapper = 'fragment' | 'div'; + export type GenerationSettings = { readonly languageScopes: string; readonly importReactOnTop: boolean; readonly typescript: boolean; readonly typescriptPropsStatePrefix: 'type' | 'interface'; + readonly componentWrapper: ComponentWrapper; }; export const DEFAULT_GENERATION_SETTINGS: GenerationSettings = { @@ -27,6 +30,7 @@ export const DEFAULT_GENERATION_SETTINGS: GenerationSettings = { importReactOnTop: false, typescript: true, typescriptPropsStatePrefix: 'type', + componentWrapper: 'fragment', }; export const Placeholders = { @@ -37,6 +41,7 @@ export const Placeholders = { Capitalize: 'capitalize', TypeProps: 'typeProps', TypeState: 'typeState', + ComponentWrapper: 'componentWrapper', FourthTabCapitalize: 'fourthTabCapitalize', } as const; @@ -50,5 +55,7 @@ export const Mappings = { TypeState: 'type State = {}', InterfaceProps: 'interface Props {}', InterfaceState: 'interface State {}', + FragmentComponentWrapper: '<>first', + DivComponentWrapper: '
first
', FourthTabCapitalize: '${4/(.*)/${1:/capitalize}/}', } as const; diff --git a/tests/generation.test.ts b/tests/generation.test.ts index 96780d8..4c5e92c 100644 --- a/tests/generation.test.ts +++ b/tests/generation.test.ts @@ -76,13 +76,15 @@ const parseManifestGenerationDefaults = ( const importReactOnTop = readDefault('importReactOnTop'); const typescript = readDefault('typescript'); const typescriptPropsStatePrefix = readDefault('typescriptPropsStatePrefix'); + const componentWrapper = readDefault('componentWrapper'); if ( typeof languageScopes !== 'string' || typeof importReactOnTop !== 'boolean' || typeof typescript !== 'boolean' || (typescriptPropsStatePrefix !== 'type' && - typescriptPropsStatePrefix !== 'interface') + typescriptPropsStatePrefix !== 'interface') || + (componentWrapper !== 'fragment' && componentWrapper !== 'div') ) { throw new Error('Package generation defaults have invalid types'); } @@ -92,6 +94,7 @@ const parseManifestGenerationDefaults = ( importReactOnTop, typescript, typescriptPropsStatePrefix, + componentWrapper, }; }; @@ -251,6 +254,12 @@ describe('snippet compiler', () => { typescriptPropsStatePrefix: 'interface', }), ); + const divWrappedComponents = parseGeneratedSnippets( + buildSnippets({ + ...DEFAULT_GENERATION_SETTINGS, + componentWrapper: 'div', + }), + ); expect(legacyReact.typescriptReactClassComponent).toBeUndefined(); expect( @@ -265,6 +274,15 @@ describe('snippet compiler', () => { expect( requireSnippet(interfaces, 'typescriptReactClassComponent').body, ).toContain('interface State {}'); + expect( + requireSnippet(divWrappedComponents, 'reactFunctionalComponent').body, + ).toContain('
${2:first}
'); + expect( + requireSnippet( + divWrappedComponents, + 'typescriptReactClassComponent', + ).body, + ).toContain('
${2:first}
'); }); test('uses defaults for empty scopes but rejects nonempty invalid scopes', () => { From b243830c38bd46f73420577c785a076a0c96d2aa Mon Sep 17 00:00:00 2001 From: Dawid Harat Date: Thu, 23 Jul 2026 12:14:09 +0200 Subject: [PATCH 2/6] feat: add component-derived TypeScript Props names --- README.md | 10 ++++++++ docs/Snippets.md | 2 +- package.json | 10 ++++++++ src/helpers/extensionConfig.ts | 5 ++++ src/helpers/snippetPlaceholders.ts | 36 ++++++++++++++++++++--------- src/sourceSnippets/typescript.ts | 22 +++++++++--------- src/types.ts | 8 +++++++ tests/generation.test.ts | 37 +++++++++++++++++++++++++++--- 8 files changed, 104 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 291f8bd..8e6cfcb 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,16 @@ Accepts a comma-separated list of VS Code language identifiers. Controls whether Props and State declarations use type aliases or interfaces in TypeScript snippets. +### `typescriptPropsNaming` + +**Type:** `string` + +**Default:** `generic` + +**Allowed values:** `generic`, `component` + +Controls whether TypeScript component snippets name their props type `Props` or derive it from the component name, for example `ButtonProps`. + ### `componentWrapper` **Type:** `string` diff --git a/docs/Snippets.md b/docs/Snippets.md index 2bbc8ab..b4e8bfc 100644 --- a/docs/Snippets.md +++ b/docs/Snippets.md @@ -479,7 +479,7 @@ const mapDispatchToProps = {}; ## TypeScript Components -All TypeScript component snippets use `type` for Props/State by default. Change to `interface` via the `typescriptPropsStatePrefix` setting. +All TypeScript component snippets use `type` for Props/State by default. Change to `interface` via the `typescriptPropsStatePrefix` setting. Set `typescriptPropsNaming` to `component` to derive the Props type from the component name, for example `ButtonProps`. | Prefix | Method | | -----------: | ------------------------------------------------------- | diff --git a/package.json b/package.json index c806d95..ecc55e0 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,16 @@ ], "scope": "application" }, + "reactSnippets.settings.typescriptPropsNaming": { + "type": "string", + "markdownDescription": "Controls whether TypeScript component snippets name their props type `Props` or derive it from the component name, for example `ButtonProps`.", + "default": "generic", + "enum": [ + "generic", + "component" + ], + "scope": "application" + }, "reactSnippets.settings.componentWrapper": { "type": "string", "markdownDescription": "Controls whether component snippets wrap their content in a React fragment or a `div`.", diff --git a/src/helpers/extensionConfig.ts b/src/helpers/extensionConfig.ts index 1a1bcde..993519d 100644 --- a/src/helpers/extensionConfig.ts +++ b/src/helpers/extensionConfig.ts @@ -4,6 +4,7 @@ import { ComponentWrapper, DEFAULT_GENERATION_SETTINGS, GenerationSettings, + TypescriptPropsNaming, } from '../types'; export type ExtensionSettings = GenerationSettings; @@ -28,6 +29,10 @@ const extensionConfig = (): ExtensionSettings => { 'typescriptPropsStatePrefix', DEFAULT_GENERATION_SETTINGS.typescriptPropsStatePrefix, ), + typescriptPropsNaming: config.get( + 'typescriptPropsNaming', + DEFAULT_GENERATION_SETTINGS.typescriptPropsNaming, + ), componentWrapper: config.get( 'componentWrapper', DEFAULT_GENERATION_SETTINGS.componentWrapper, diff --git a/src/helpers/snippetPlaceholders.ts b/src/helpers/snippetPlaceholders.ts index 678b970..8a55869 100644 --- a/src/helpers/snippetPlaceholders.ts +++ b/src/helpers/snippetPlaceholders.ts @@ -1,18 +1,31 @@ -import { - GenerationSettings, - Mappings, - Placeholders, -} from '../types'; +import { GenerationSettings, Mappings, Placeholders } from '../types'; + +const propsPlaceholders = { + generic: { + type: Mappings.TypeProps, + interface: Mappings.InterfaceProps, + }, + component: { + type: Mappings.ComponentTypeProps, + interface: Mappings.ComponentInterfaceProps, + }, +} as const; export const replaceSnippetPlaceholders = ( snippetString: string, settings: GenerationSettings, ): string => { - const { componentWrapper, typescriptPropsStatePrefix } = settings; + const { + componentWrapper, + typescriptPropsNaming, + typescriptPropsStatePrefix, + } = settings; const propsPlaceholder = - typescriptPropsStatePrefix === 'type' - ? Mappings.TypeProps - : Mappings.InterfaceProps; + propsPlaceholders[typescriptPropsNaming][typescriptPropsStatePrefix]; + const propsName = + typescriptPropsNaming === 'component' + ? Mappings.ComponentProps + : Mappings.Props; const statePlaceholder = typescriptPropsStatePrefix === 'type' ? Mappings.TypeState @@ -23,13 +36,14 @@ export const replaceSnippetPlaceholders = ( : Mappings.DivComponentWrapper; return String(snippetString) + .replaceAll(Placeholders.TypeProps, propsPlaceholder) + .replaceAll(Placeholders.TypeState, statePlaceholder) + .replaceAll(Placeholders.ComponentProps, propsName) .replaceAll(Placeholders.FileName, Mappings.FileName) .replaceAll(Placeholders.ComponentWrapper, componentWrapperPlaceholder) .replaceAll(Placeholders.FirstTab, Mappings.FirstTab) .replaceAll(Placeholders.SecondTab, Mappings.SecondTab) .replaceAll(Placeholders.ThirdTab, Mappings.ThirdTab) .replaceAll(Placeholders.Capitalize, Mappings.Capitalize) - .replaceAll(Placeholders.TypeProps, propsPlaceholder) - .replaceAll(Placeholders.TypeState, statePlaceholder) .replaceAll(Placeholders.FourthTabCapitalize, Mappings.FourthTabCapitalize); }; diff --git a/src/sourceSnippets/typescript.ts b/src/sourceSnippets/typescript.ts index f9a1492..ef64cfc 100644 --- a/src/sourceSnippets/typescript.ts +++ b/src/sourceSnippets/typescript.ts @@ -54,7 +54,7 @@ const typescriptReactClassComponent: TypescriptSnippet = { ...reactComponent, '', ...propsStateInterface, - `export default class ${Placeholders.FileName} extends Component {`, + `export default class ${Placeholders.FileName} extends Component<${Placeholders.ComponentProps}, State> {`, ' state = {}', '', ...innerComponentReturn, @@ -69,7 +69,7 @@ const typescriptReactClassExportComponent: TypescriptSnippet = { ...reactComponent, '', ...propsStateInterface, - `class ${Placeholders.FileName} extends Component {`, + `class ${Placeholders.FileName} extends Component<${Placeholders.ComponentProps}, State> {`, ' state = {}', '', ...innerComponentReturn, @@ -87,7 +87,7 @@ const typescriptReactFunctionalExportComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `function ${Placeholders.FileName}({}: Props) {`, + `function ${Placeholders.FileName}({}: ${Placeholders.ComponentProps}) {`, ...innerComponent, '}', ...exportDefault, @@ -103,7 +103,7 @@ const typescriptReactFunctionalComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `export default function ${Placeholders.FileName}({}: Props) {`, + `export default function ${Placeholders.FileName}({}: ${Placeholders.ComponentProps}) {`, ...innerComponent, '}', ], @@ -118,7 +118,7 @@ const typescriptReactArrowFunctionExportComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `const ${Placeholders.FileName} = (props: Props) => {`, + `const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`, ...innerComponent, '}', ...exportDefault, @@ -134,7 +134,7 @@ const typescriptReactArrowFunctionComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `export const ${Placeholders.FileName} = (props: Props) => {`, + `export const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`, ...innerComponent, '}', ], @@ -149,7 +149,7 @@ const typescriptReactClassPureComponent: TypescriptSnippet = { ...reactPureComponent, '', ...propsTypeInterface, - `export default class ${Placeholders.FileName} extends PureComponent {`, + `export default class ${Placeholders.FileName} extends PureComponent<${Placeholders.ComponentProps}> {`, ...innerComponentReturn, '}', ], @@ -164,7 +164,7 @@ const typescriptReactClassExportPureComponent: TypescriptSnippet = { ...reactPureComponent, '', ...propsTypeInterface, - `class ${Placeholders.FileName} extends PureComponent {`, + `class ${Placeholders.FileName} extends PureComponent<${Placeholders.ComponentProps}> {`, ...innerComponentReturn, '}', ...exportDefault, @@ -181,7 +181,7 @@ const typescriptReactClassComponentRedux: TypescriptSnippet = { ...reactComponent, '', ...propsStateInterface, - `export class ${Placeholders.FileName} extends Component {`, + `export class ${Placeholders.FileName} extends Component<${Placeholders.ComponentProps}, State> {`, ' state = {}', '', ...innerComponentReturn, @@ -200,7 +200,7 @@ const typescriptReactNativeArrowFunctionComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `const ${Placeholders.FileName} = (props: Props) => {`, + `const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`, ' return (', ' ', ` ${Placeholders.FirstTab}`, @@ -222,7 +222,7 @@ const typescriptReactNativeArrowFunctionComponentWithStyles: TypescriptSnippet = ...react, '', ...propsTypeInterface, - `const ${Placeholders.FileName} = (props: Props) => {`, + `const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`, ' return (', ' ', ` ${Placeholders.FirstTab}`, diff --git a/src/types.ts b/src/types.ts index f4ae433..dd4fe91 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,12 +16,14 @@ export const VALID_LANGUAGE_SCOPES = [ export type LanguageScope = (typeof VALID_LANGUAGE_SCOPES)[number]; export type ComponentWrapper = 'fragment' | 'div'; +export type TypescriptPropsNaming = 'generic' | 'component'; export type GenerationSettings = { readonly languageScopes: string; readonly importReactOnTop: boolean; readonly typescript: boolean; readonly typescriptPropsStatePrefix: 'type' | 'interface'; + readonly typescriptPropsNaming: TypescriptPropsNaming; readonly componentWrapper: ComponentWrapper; }; @@ -30,6 +32,7 @@ export const DEFAULT_GENERATION_SETTINGS: GenerationSettings = { importReactOnTop: false, typescript: true, typescriptPropsStatePrefix: 'type', + typescriptPropsNaming: 'generic', componentWrapper: 'fragment', }; @@ -41,6 +44,7 @@ export const Placeholders = { Capitalize: 'capitalize', TypeProps: 'typeProps', TypeState: 'typeState', + ComponentProps: 'componentProps', ComponentWrapper: 'componentWrapper', FourthTabCapitalize: 'fourthTabCapitalize', } as const; @@ -52,9 +56,13 @@ export const Mappings = { ThirdTab: '${4:third}', Capitalize: '${2/(.*)/${1:/capitalize}/}', TypeProps: 'type Props = {}', + ComponentTypeProps: 'type fileProps = {}', TypeState: 'type State = {}', InterfaceProps: 'interface Props {}', + ComponentInterfaceProps: 'interface fileProps {}', InterfaceState: 'interface State {}', + Props: 'Props', + ComponentProps: 'fileProps', FragmentComponentWrapper: '<>first', DivComponentWrapper: '
first
', FourthTabCapitalize: '${4/(.*)/${1:/capitalize}/}', diff --git a/tests/generation.test.ts b/tests/generation.test.ts index 4c5e92c..13272e3 100644 --- a/tests/generation.test.ts +++ b/tests/generation.test.ts @@ -76,6 +76,7 @@ const parseManifestGenerationDefaults = ( const importReactOnTop = readDefault('importReactOnTop'); const typescript = readDefault('typescript'); const typescriptPropsStatePrefix = readDefault('typescriptPropsStatePrefix'); + const typescriptPropsNaming = readDefault('typescriptPropsNaming'); const componentWrapper = readDefault('componentWrapper'); if ( @@ -84,6 +85,8 @@ const parseManifestGenerationDefaults = ( typeof typescript !== 'boolean' || (typescriptPropsStatePrefix !== 'type' && typescriptPropsStatePrefix !== 'interface') || + (typescriptPropsNaming !== 'generic' && + typescriptPropsNaming !== 'component') || (componentWrapper !== 'fragment' && componentWrapper !== 'div') ) { throw new Error('Package generation defaults have invalid types'); @@ -94,6 +97,7 @@ const parseManifestGenerationDefaults = ( importReactOnTop, typescript, typescriptPropsStatePrefix, + typescriptPropsNaming, componentWrapper, }; }; @@ -260,6 +264,19 @@ describe('snippet compiler', () => { componentWrapper: 'div', }), ); + const componentNamedProps = parseGeneratedSnippets( + buildSnippets({ + ...DEFAULT_GENERATION_SETTINGS, + typescriptPropsNaming: 'component', + }), + ); + const componentNamedInterfaces = parseGeneratedSnippets( + buildSnippets({ + ...DEFAULT_GENERATION_SETTINGS, + typescriptPropsNaming: 'component', + typescriptPropsStatePrefix: 'interface', + }), + ); expect(legacyReact.typescriptReactClassComponent).toBeUndefined(); expect( @@ -277,12 +294,26 @@ describe('snippet compiler', () => { expect( requireSnippet(divWrappedComponents, 'reactFunctionalComponent').body, ).toContain('
${2:first}
'); + expect( + requireSnippet(divWrappedComponents, 'typescriptReactClassComponent') + .body, + ).toContain('
${2:first}
'); + expect( + requireSnippet(componentNamedProps, 'typescriptReactFunctionalComponent') + .body, + ).toContain('type ${1:${TM_FILENAME_BASE}}Props = {}'); + expect( + requireSnippet(componentNamedProps, 'typescriptReactFunctionalComponent') + .body, + ).toContain( + 'export default function ${1:${TM_FILENAME_BASE}}({}: ${1:${TM_FILENAME_BASE}}Props) {', + ); expect( requireSnippet( - divWrappedComponents, - 'typescriptReactClassComponent', + componentNamedInterfaces, + 'typescriptReactFunctionalComponent', ).body, - ).toContain('
${2:first}
'); + ).toContain('interface ${1:${TM_FILENAME_BASE}}Props {}'); }); test('uses defaults for empty scopes but rejects nonempty invalid scopes', () => { From 6c191893278a3b2045dc59850bc81d4674bd3ef3 Mon Sep 17 00:00:00 2001 From: Dawid Harat Date: Thu, 23 Jul 2026 13:12:44 +0200 Subject: [PATCH 3/6] feat: support directory names for index components --- README.md | 10 +++++ docs/Snippets.md | 2 + package.json | 10 +++++ src/helpers/extensionConfig.ts | 5 +++ src/helpers/snippetPlaceholders.ts | 9 ++++ src/sourceSnippets/components.ts | 46 ++++++++++----------- src/sourceSnippets/reactNative.ts | 22 +++++----- src/sourceSnippets/sharedSnippets.ts | 9 ++-- src/sourceSnippets/typescript.ts | 26 ++++++------ src/types.ts | 12 ++++-- tests/generation.test.ts | 62 ++++++++++++++++++++++++++++ 11 files changed, 160 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 8e6cfcb..ac6260e 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,16 @@ Controls whether Props and State declarations use type aliases or interfaces in Controls whether TypeScript component snippets name their props type `Props` or derive it from the component name, for example `ButtonProps`. +### `componentNameSource` + +**Type:** `string` + +**Default:** `filename` + +**Allowed values:** `filename`, `directoryForIndex` + +Controls how component snippets derive their name. `directoryForIndex` uses the parent directory for an `index.*` file, so `Button/index.tsx` becomes `Button`; other filenames still use their basename. + ### `componentWrapper` **Type:** `string` diff --git a/docs/Snippets.md b/docs/Snippets.md index b4e8bfc..7cd2097 100644 --- a/docs/Snippets.md +++ b/docs/Snippets.md @@ -7,6 +7,8 @@ Snippet bodies preserve the spacing shown below; spaces inside braces and parent _TypeScript_ has own components and own snippets. Use search or just type `ts` before every component snippet. +Component snippets use the current filename by default. Set `componentNameSource` to `directoryForIndex` to use the parent directory for `index.*` files. + I.E. `tsrcc`
diff --git a/package.json b/package.json index ecc55e0..e5e2aa6 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,16 @@ ], "scope": "application" }, + "reactSnippets.settings.componentNameSource": { + "type": "string", + "markdownDescription": "Controls whether component snippets use the filename or use the parent directory name for `index.*` files.", + "default": "filename", + "enum": [ + "filename", + "directoryForIndex" + ], + "scope": "application" + }, "reactSnippets.settings.componentWrapper": { "type": "string", "markdownDescription": "Controls whether component snippets wrap their content in a React fragment or a `div`.", diff --git a/src/helpers/extensionConfig.ts b/src/helpers/extensionConfig.ts index 993519d..8349235 100644 --- a/src/helpers/extensionConfig.ts +++ b/src/helpers/extensionConfig.ts @@ -1,6 +1,7 @@ import { workspace } from 'vscode'; import { + ComponentNameSource, ComponentWrapper, DEFAULT_GENERATION_SETTINGS, GenerationSettings, @@ -33,6 +34,10 @@ const extensionConfig = (): ExtensionSettings => { 'typescriptPropsNaming', DEFAULT_GENERATION_SETTINGS.typescriptPropsNaming, ), + componentNameSource: config.get( + 'componentNameSource', + DEFAULT_GENERATION_SETTINGS.componentNameSource, + ), componentWrapper: config.get( 'componentWrapper', DEFAULT_GENERATION_SETTINGS.componentWrapper, diff --git a/src/helpers/snippetPlaceholders.ts b/src/helpers/snippetPlaceholders.ts index 8a55869..7cc3d9b 100644 --- a/src/helpers/snippetPlaceholders.ts +++ b/src/helpers/snippetPlaceholders.ts @@ -11,17 +11,25 @@ const propsPlaceholders = { }, } as const; +const componentNamePlaceholders = { + filename: Mappings.FileName, + directoryForIndex: Mappings.DirectoryNameForIndex, +} as const; + export const replaceSnippetPlaceholders = ( snippetString: string, settings: GenerationSettings, ): string => { const { + componentNameSource, componentWrapper, typescriptPropsNaming, typescriptPropsStatePrefix, } = settings; const propsPlaceholder = propsPlaceholders[typescriptPropsNaming][typescriptPropsStatePrefix]; + const componentNamePlaceholder = + componentNamePlaceholders[componentNameSource]; const propsName = typescriptPropsNaming === 'component' ? Mappings.ComponentProps @@ -39,6 +47,7 @@ export const replaceSnippetPlaceholders = ( .replaceAll(Placeholders.TypeProps, propsPlaceholder) .replaceAll(Placeholders.TypeState, statePlaceholder) .replaceAll(Placeholders.ComponentProps, propsName) + .replaceAll(Placeholders.ComponentName, componentNamePlaceholder) .replaceAll(Placeholders.FileName, Mappings.FileName) .replaceAll(Placeholders.ComponentWrapper, componentWrapperPlaceholder) .replaceAll(Placeholders.FirstTab, Mappings.FirstTab) diff --git a/src/sourceSnippets/components.ts b/src/sourceSnippets/components.ts index 761a5cb..9387771 100644 --- a/src/sourceSnippets/components.ts +++ b/src/sourceSnippets/components.ts @@ -44,7 +44,7 @@ const reactClassComponent: ComponentsSnippet = { body: [ ...reactComponent, '', - `export default class ${Placeholders.FileName} extends Component {`, + `export default class ${Placeholders.ComponentName} extends Component {`, ...innerComponentReturn, '}', '', @@ -58,7 +58,7 @@ const reactClassExportComponent: ComponentsSnippet = { body: [ ...reactComponent, '', - `export class ${Placeholders.FileName} extends Component {`, + `export class ${Placeholders.ComponentName} extends Component {`, ...innerComponentReturn, '}', ...exportDefault, @@ -72,7 +72,7 @@ const reactFunctionalExportComponent: ComponentsSnippet = { body: [ ...react, '', - `function ${Placeholders.FileName}() {`, + `function ${Placeholders.ComponentName}() {`, ...innerComponent, '}', ...exportDefault, @@ -86,7 +86,7 @@ const reactFunctionalComponent: ComponentsSnippet = { body: [ ...react, '', - `export default function ${Placeholders.FileName}() {`, + `export default function ${Placeholders.ComponentName}() {`, ...innerComponent, '}', '', @@ -100,11 +100,11 @@ const reactFunctionalComponentWithPropTypes: ComponentsSnippet = { body: [ ...reactPropTypes, '', - `function ${Placeholders.FileName}(props) {`, + `function ${Placeholders.ComponentName}(props) {`, ...innerComponent, '}', '', - `${Placeholders.FileName}.propTypes = {}`, + `${Placeholders.ComponentName}.propTypes = {}`, ...exportDefault, '', ], @@ -118,7 +118,7 @@ const reactArrowFunctionExportComponent: ComponentsSnippet = { body: [ ...react, '', - `const ${Placeholders.FileName} = () => {`, + `const ${Placeholders.ComponentName} = () => {`, ...innerComponent, '}', ...exportDefault, @@ -133,7 +133,7 @@ const reactArrowFunctionComponent: ComponentsSnippet = { body: [ ...react, '', - `export const ${Placeholders.FileName} = () => {`, + `export const ${Placeholders.ComponentName} = () => {`, ...innerComponent, '}', '', @@ -148,11 +148,11 @@ const reactArrowFunctionComponentWithPropTypes: ComponentsSnippet = { body: [ ...reactPropTypes, '', - `const ${Placeholders.FileName} = props => {`, + `const ${Placeholders.ComponentName} = props => {`, ...innerComponent, '}', '', - `${Placeholders.FileName}.propTypes = {}`, + `${Placeholders.ComponentName}.propTypes = {}`, ...exportDefault, ], description: @@ -166,7 +166,7 @@ const reactClassExportComponentWithPropTypes: ComponentsSnippet = { "import PropTypes from 'prop-types'", ...reactComponent, '', - `export class ${Placeholders.FileName} extends Component {`, + `export class ${Placeholders.ComponentName} extends Component {`, ' static propTypes = {}', '', ...innerComponentReturn, @@ -182,7 +182,7 @@ const reactClassPureComponent: ComponentsSnippet = { body: [ ...reactPureComponent, '', - `export default class ${Placeholders.FileName} extends PureComponent {`, + `export default class ${Placeholders.ComponentName} extends PureComponent {`, ...innerComponentReturn, '}', '', @@ -196,7 +196,7 @@ const reactClassExportPureComponent: ComponentsSnippet = { body: [ ...reactPureComponent, '', - `export class ${Placeholders.FileName} extends PureComponent {`, + `export class ${Placeholders.ComponentName} extends PureComponent {`, ...innerComponentReturn, '}', ...exportDefault, @@ -212,7 +212,7 @@ const reactClassPureComponentWithPropTypes: ComponentsSnippet = { "import PropTypes from 'prop-types'", ...reactPureComponent, '', - `export default class ${Placeholders.FileName} extends PureComponent {`, + `export default class ${Placeholders.ComponentName} extends PureComponent {`, ' static propTypes = {}', '', ...innerComponentReturn, @@ -228,7 +228,7 @@ const reactFunctionMemoComponent: ComponentsSnippet = { body: [ ...reactWithMemo, '', - `const ${Placeholders.FileName} = memo(() => {`, + `const ${Placeholders.ComponentName} = memo(() => {`, ...innerComponent, '})', ...exportDefault, @@ -243,11 +243,11 @@ const reactFunctionMemoComponentWithPropTypes: ComponentsSnippet = { "import PropTypes from 'prop-types'", ...reactWithMemo, '', - `const ${Placeholders.FileName} = memo((props) => {`, + `const ${Placeholders.ComponentName} = memo((props) => {`, ...innerComponent, '})', '', - `${Placeholders.FileName}.propTypes = {}`, + `${Placeholders.ComponentName}.propTypes = {}`, ...exportDefault, ], description: @@ -261,7 +261,7 @@ const reactClassComponentPropTypes: ComponentsSnippet = { "import PropTypes from 'prop-types'", ...reactComponent, '', - `export default class ${Placeholders.FileName} extends Component {`, + `export default class ${Placeholders.ComponentName} extends Component {`, ` static propTypes = {${Placeholders.SecondTab}: ${Placeholders.ThirdTab}}`, '', ...innerComponentReturn, @@ -278,7 +278,7 @@ const reactClassComponentRedux: ComponentsSnippet = { body: [ ...reactComponentWithReduxConnect, '', - `export class ${Placeholders.FileName} extends Component {`, + `export class ${Placeholders.ComponentName} extends Component {`, ...innerComponentReturn, '}', ...reduxComponentExport, @@ -294,7 +294,7 @@ const reactClassComponentReduxPropTypes: ComponentsSnippet = { "import PropTypes from 'prop-types'", ...reactComponentWithReduxConnect, '', - `export class ${Placeholders.FileName} extends Component {`, + `export class ${Placeholders.ComponentName} extends Component {`, ' static propTypes = {', ` ${Placeholders.SecondTab}: ${Placeholders.ThirdTab}`, ' }', @@ -313,7 +313,7 @@ const reactFunctionalComponentRedux: ComponentsSnippet = { body: [ ...reactWithReduxConnect, '', - `export const ${Placeholders.FileName} = (props) => {`, + `export const ${Placeholders.ComponentName} = (props) => {`, ...innerComponent, '}', ...reduxComponentExport, @@ -329,11 +329,11 @@ const reactFunctionalComponentReduxPropTypes: ComponentsSnippet = { "import PropTypes from 'prop-types'", ...reactWithReduxConnect, '', - `export const ${Placeholders.FileName} = (props) => {`, + `export const ${Placeholders.ComponentName} = (props) => {`, ...innerComponent, '}', '', - `${Placeholders.FileName}.propTypes = {`, + `${Placeholders.ComponentName}.propTypes = {`, ` ${Placeholders.SecondTab}: PropTypes.${Placeholders.ThirdTab}`, '}', ...reduxComponentExport, diff --git a/src/sourceSnippets/reactNative.ts b/src/sourceSnippets/reactNative.ts index ac06f4a..f529770 100644 --- a/src/sourceSnippets/reactNative.ts +++ b/src/sourceSnippets/reactNative.ts @@ -62,7 +62,7 @@ const reactNativeComponent: ReactNativeSnippet = { "import { Text, View } from 'react-native'", ...reactComponent, '', - `export default class ${Placeholders.FileName} extends Component {`, + `export default class ${Placeholders.ComponentName} extends Component {`, ...reactNativeComponentReturn, '}', ], @@ -75,7 +75,7 @@ const reactNativeComponentWithStyles: ReactNativeSnippet = { "import { Text, StyleSheet, View } from 'react-native'", ...reactComponent, '', - `export default class ${Placeholders.FileName} extends Component {`, + `export default class ${Placeholders.ComponentName} extends Component {`, ...reactNativeComponentReturn, '}', '', @@ -90,7 +90,7 @@ const reactNativeComponentExport: ReactNativeSnippet = { "import { Text, View } from 'react-native'", ...reactComponent, '', - `export class ${Placeholders.FileName} extends Component {`, + `export class ${Placeholders.ComponentName} extends Component {`, ...reactNativeComponentReturn, '}', ...exportDefault, @@ -104,7 +104,7 @@ const reactNativePureComponent: ReactNativeSnippet = { "import { Text, View } from 'react-native'", ...reactPureComponent, '', - `export default class ${Placeholders.FileName} extends PureComponent {`, + `export default class ${Placeholders.ComponentName} extends PureComponent {`, ...reactNativeComponentReturn, '}', ], @@ -117,7 +117,7 @@ const reactNativePureComponentExport: ReactNativeSnippet = { "import { Text, View } from 'react-native'", ...reactPureComponent, '', - `export class ${Placeholders.FileName} extends PureComponent {`, + `export class ${Placeholders.ComponentName} extends PureComponent {`, ...reactNativeComponentReturn, '}', ...exportDefault, @@ -131,7 +131,7 @@ const reactNativeFunctionalExportComponent: ReactNativeSnippet = { "import { View, Text } from 'react-native'", ...react, '', - `const ${Placeholders.FileName} = () => {`, + `const ${Placeholders.ComponentName} = () => {`, ...reactNativeReturn, '}', ...exportDefault, @@ -145,7 +145,7 @@ const reactNativeFunctionalExportComponentWithStyles: ReactNativeSnippet = { "import { StyleSheet, Text, View } from 'react-native'", ...react, '', - `const ${Placeholders.FileName} = () => {`, + `const ${Placeholders.ComponentName} = () => {`, ...reactNativeReturn, '}', ...exportDefault, @@ -161,7 +161,7 @@ const reactNativeFunctionalComponent: ReactNativeSnippet = { "import { View, Text } from 'react-native'", ...react, '', - `export default function ${Placeholders.FileName}() {`, + `export default function ${Placeholders.ComponentName}() {`, ...reactNativeReturn, '}', ], @@ -174,7 +174,7 @@ const reactNativeFunctionalComponentWithStyles: ReactNativeSnippet = { "import { StyleSheet, Text, View } from 'react-native'", ...react, '', - `export default function ${Placeholders.FileName}() {`, + `export default function ${Placeholders.ComponentName}() {`, ...reactNativeReturn, '}', '', @@ -189,7 +189,7 @@ const reactNativeExtendedFunctional: ReactNativeSnippet = { "import React, { useState, useEffect } from 'react'", "import { View, SafeAreaView, Text, StyleSheet } from 'react-native'", '', - `const ${Placeholders.FileName} = ({ \${2:props} }) => {`, + `const ${Placeholders.ComponentName} = ({ \${2:props} }) => {`, ' const [${3:state}, ${4:setState}] = useState(${5:initialState})', '', ' useEffect(() => {', @@ -213,7 +213,7 @@ const reactNativeExtendedFunctional: ReactNativeSnippet = { ' },', '})', '', - `export default ${Placeholders.FileName}`, + `export default ${Placeholders.ComponentName}`, ], description: 'React Native functional component with useState, useEffect, SafeAreaView and basic styling', diff --git a/src/sourceSnippets/sharedSnippets.ts b/src/sourceSnippets/sharedSnippets.ts index 24fba54..8c93d36 100644 --- a/src/sourceSnippets/sharedSnippets.ts +++ b/src/sourceSnippets/sharedSnippets.ts @@ -28,7 +28,7 @@ export const reduxComponentExport = [ '', 'const mapDispatchToProps = {}', '', - `export default connect(mapStateToProps, mapDispatchToProps)(${Placeholders.FileName})`, + `export default connect(mapStateToProps, mapDispatchToProps)(${Placeholders.ComponentName})`, ]; export const reduxTypeScriptComponentExport = [ @@ -37,7 +37,7 @@ export const reduxTypeScriptComponentExport = [ '', 'const mapDispatchToProps = {}', '', - `export default connect(mapStateToProps, mapDispatchToProps)(${Placeholders.FileName})`, + `export default connect(mapStateToProps, mapDispatchToProps)(${Placeholders.ComponentName})`, ]; export const innerComponent = [ @@ -54,7 +54,10 @@ export const innerComponentReturn = [ ' }', ]; -export const exportDefault = ['', `export default ${Placeholders.FileName}`]; +export const exportDefault = [ + '', + `export default ${Placeholders.ComponentName}`, +]; export const propsTypeInterface = [Placeholders.TypeProps, '']; export const stateTypeInterface = [Placeholders.TypeState, '']; diff --git a/src/sourceSnippets/typescript.ts b/src/sourceSnippets/typescript.ts index ef64cfc..eca9201 100644 --- a/src/sourceSnippets/typescript.ts +++ b/src/sourceSnippets/typescript.ts @@ -54,7 +54,7 @@ const typescriptReactClassComponent: TypescriptSnippet = { ...reactComponent, '', ...propsStateInterface, - `export default class ${Placeholders.FileName} extends Component<${Placeholders.ComponentProps}, State> {`, + `export default class ${Placeholders.ComponentName} extends Component<${Placeholders.ComponentProps}, State> {`, ' state = {}', '', ...innerComponentReturn, @@ -69,7 +69,7 @@ const typescriptReactClassExportComponent: TypescriptSnippet = { ...reactComponent, '', ...propsStateInterface, - `class ${Placeholders.FileName} extends Component<${Placeholders.ComponentProps}, State> {`, + `class ${Placeholders.ComponentName} extends Component<${Placeholders.ComponentProps}, State> {`, ' state = {}', '', ...innerComponentReturn, @@ -87,7 +87,7 @@ const typescriptReactFunctionalExportComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `function ${Placeholders.FileName}({}: ${Placeholders.ComponentProps}) {`, + `function ${Placeholders.ComponentName}({}: ${Placeholders.ComponentProps}) {`, ...innerComponent, '}', ...exportDefault, @@ -103,7 +103,7 @@ const typescriptReactFunctionalComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `export default function ${Placeholders.FileName}({}: ${Placeholders.ComponentProps}) {`, + `export default function ${Placeholders.ComponentName}({}: ${Placeholders.ComponentProps}) {`, ...innerComponent, '}', ], @@ -118,7 +118,7 @@ const typescriptReactArrowFunctionExportComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`, + `const ${Placeholders.ComponentName} = (props: ${Placeholders.ComponentProps}) => {`, ...innerComponent, '}', ...exportDefault, @@ -134,7 +134,7 @@ const typescriptReactArrowFunctionComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `export const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`, + `export const ${Placeholders.ComponentName} = (props: ${Placeholders.ComponentProps}) => {`, ...innerComponent, '}', ], @@ -149,7 +149,7 @@ const typescriptReactClassPureComponent: TypescriptSnippet = { ...reactPureComponent, '', ...propsTypeInterface, - `export default class ${Placeholders.FileName} extends PureComponent<${Placeholders.ComponentProps}> {`, + `export default class ${Placeholders.ComponentName} extends PureComponent<${Placeholders.ComponentProps}> {`, ...innerComponentReturn, '}', ], @@ -164,7 +164,7 @@ const typescriptReactClassExportPureComponent: TypescriptSnippet = { ...reactPureComponent, '', ...propsTypeInterface, - `class ${Placeholders.FileName} extends PureComponent<${Placeholders.ComponentProps}> {`, + `class ${Placeholders.ComponentName} extends PureComponent<${Placeholders.ComponentProps}> {`, ...innerComponentReturn, '}', ...exportDefault, @@ -181,7 +181,7 @@ const typescriptReactClassComponentRedux: TypescriptSnippet = { ...reactComponent, '', ...propsStateInterface, - `export class ${Placeholders.FileName} extends Component<${Placeholders.ComponentProps}, State> {`, + `export class ${Placeholders.ComponentName} extends Component<${Placeholders.ComponentProps}, State> {`, ' state = {}', '', ...innerComponentReturn, @@ -200,7 +200,7 @@ const typescriptReactNativeArrowFunctionComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`, + `const ${Placeholders.ComponentName} = (props: ${Placeholders.ComponentProps}) => {`, ' return (', ' ', ` ${Placeholders.FirstTab}`, @@ -222,7 +222,7 @@ const typescriptReactNativeArrowFunctionComponentWithStyles: TypescriptSnippet = ...react, '', ...propsTypeInterface, - `const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`, + `const ${Placeholders.ComponentName} = (props: ${Placeholders.ComponentProps}) => {`, ' return (', ' ', ` ${Placeholders.FirstTab}`, @@ -254,7 +254,7 @@ const routeWithLoaderActionTypescript: TypescriptSnippet = { ` ${Placeholders.SecondTab}`, '}', '', - `function ${Placeholders.FileName}() {`, + `function ${Placeholders.ComponentName}() {`, ' const data = useLoaderData()', '', ' return (', @@ -263,7 +263,7 @@ const routeWithLoaderActionTypescript: TypescriptSnippet = { '}', '', 'export { loader, action }', - `export default ${Placeholders.FileName}`, + `export default ${Placeholders.ComponentName}`, ], description: 'React Router v6.4+ TypeScript route with loader and action', }; diff --git a/src/types.ts b/src/types.ts index dd4fe91..90c1e7d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,7 @@ export const VALID_LANGUAGE_SCOPES = [ export type LanguageScope = (typeof VALID_LANGUAGE_SCOPES)[number]; export type ComponentWrapper = 'fragment' | 'div'; +export type ComponentNameSource = 'filename' | 'directoryForIndex'; export type TypescriptPropsNaming = 'generic' | 'component'; export type GenerationSettings = { @@ -24,6 +25,7 @@ export type GenerationSettings = { readonly typescript: boolean; readonly typescriptPropsStatePrefix: 'type' | 'interface'; readonly typescriptPropsNaming: TypescriptPropsNaming; + readonly componentNameSource: ComponentNameSource; readonly componentWrapper: ComponentWrapper; }; @@ -33,11 +35,13 @@ export const DEFAULT_GENERATION_SETTINGS: GenerationSettings = { typescript: true, typescriptPropsStatePrefix: 'type', typescriptPropsNaming: 'generic', + componentNameSource: 'filename', componentWrapper: 'fragment', }; export const Placeholders = { FileName: 'file', + ComponentName: 'componentName', FirstTab: 'first', SecondTab: 'second', ThirdTab: 'third', @@ -51,18 +55,20 @@ export const Placeholders = { export const Mappings = { FileName: '${1:${TM_FILENAME_BASE}}', + DirectoryNameForIndex: + '${1:${TM_FILEPATH/.*?[\\\\/](?:([^\\\\/]+)[\\\\/]index|([^\\\\/]+?))(?:\\.[^\\\\/.]+)?$/${1}${2}/}}', FirstTab: '${2:first}', SecondTab: '${3:second}', ThirdTab: '${4:third}', Capitalize: '${2/(.*)/${1:/capitalize}/}', TypeProps: 'type Props = {}', - ComponentTypeProps: 'type fileProps = {}', + ComponentTypeProps: 'type componentNameProps = {}', TypeState: 'type State = {}', InterfaceProps: 'interface Props {}', - ComponentInterfaceProps: 'interface fileProps {}', + ComponentInterfaceProps: 'interface componentNameProps {}', InterfaceState: 'interface State {}', Props: 'Props', - ComponentProps: 'fileProps', + ComponentProps: 'componentNameProps', FragmentComponentWrapper: '<>first', DivComponentWrapper: '
first
', FourthTabCapitalize: '${4/(.*)/${1:/capitalize}/}', diff --git a/tests/generation.test.ts b/tests/generation.test.ts index 13272e3..48afcc5 100644 --- a/tests/generation.test.ts +++ b/tests/generation.test.ts @@ -13,6 +13,9 @@ import { import { parseSnippet } from '../src/helpers/formatters'; import { persistGeneratedSnippets } from '../src/helpers/persistGeneratedSnippets'; import { replaceOrRemoveReactImport } from '../src/helpers/replaceOrRemoveReactImport'; +import componentsSnippets from '../src/sourceSnippets/components'; +import reactNativeSnippets from '../src/sourceSnippets/reactNative'; +import typescriptSnippets from '../src/sourceSnippets/typescript'; import { isGeneratedSnippetAvailableForLanguage, parseGeneratedSnippetCatalog, @@ -21,6 +24,8 @@ import { } from '../src/snippetTypes'; import { DEFAULT_GENERATION_SETTINGS, + Mappings, + Placeholders, type GenerationSettings, } from '../src/types'; @@ -36,6 +41,13 @@ const packageManifestPath = fileURLToPath( type JsonObject = Readonly>; +const componentSnippetSources = [ + ...componentsSnippets, + ...reactNativeSnippets, + ...typescriptSnippets, +]; +const directoryNameForIndex = Mappings.DirectoryNameForIndex; + const requireJsonObject = (value: unknown, context: string): JsonObject => { if (typeof value !== 'object' || value === null || Array.isArray(value)) { throw new Error(`Expected ${context} to be an object`); @@ -77,6 +89,7 @@ const parseManifestGenerationDefaults = ( const typescript = readDefault('typescript'); const typescriptPropsStatePrefix = readDefault('typescriptPropsStatePrefix'); const typescriptPropsNaming = readDefault('typescriptPropsNaming'); + const componentNameSource = readDefault('componentNameSource'); const componentWrapper = readDefault('componentWrapper'); if ( @@ -87,6 +100,8 @@ const parseManifestGenerationDefaults = ( typescriptPropsStatePrefix !== 'interface') || (typescriptPropsNaming !== 'generic' && typescriptPropsNaming !== 'component') || + (componentNameSource !== 'filename' && + componentNameSource !== 'directoryForIndex') || (componentWrapper !== 'fragment' && componentWrapper !== 'div') ) { throw new Error('Package generation defaults have invalid types'); @@ -98,6 +113,7 @@ const parseManifestGenerationDefaults = ( typescript, typescriptPropsStatePrefix, typescriptPropsNaming, + componentNameSource, componentWrapper, }; }; @@ -277,6 +293,14 @@ describe('snippet compiler', () => { typescriptPropsStatePrefix: 'interface', }), ); + const directoryNamedComponents = parseGeneratedSnippets( + buildSnippets({ + ...DEFAULT_GENERATION_SETTINGS, + componentNameSource: 'directoryForIndex', + typescriptPropsNaming: 'component', + }), + ); + const fileName = '${1:${TM_FILENAME_BASE}}'; expect(legacyReact.typescriptReactClassComponent).toBeUndefined(); expect( @@ -314,6 +338,44 @@ describe('snippet compiler', () => { 'typescriptReactFunctionalComponent', ).body, ).toContain('interface ${1:${TM_FILENAME_BASE}}Props {}'); + expect( + requireSnippet(directoryNamedComponents, 'reactFunctionalComponent').body, + ).toContain(`export default function ${directoryNameForIndex}() {`); + expect( + requireSnippet( + directoryNamedComponents, + 'typescriptReactFunctionalComponent', + ).body, + ).toContain(`type ${directoryNameForIndex}Props = {}`); + expect( + requireSnippet(directoryNamedComponents, 'reactNativeFunctionalComponent') + .body, + ).toContain(`export default function ${directoryNameForIndex}() {`); + expect( + requireSnippet(directoryNamedComponents, 'setupReactNativeTest').body, + ).toContain(`import ${fileName} from '../${fileName}'`); + }); + + test('applies directory-derived names to every component scaffold', () => { + const snippets = parseGeneratedSnippets( + buildSnippets({ + ...DEFAULT_GENERATION_SETTINGS, + componentNameSource: 'directoryForIndex', + }), + ); + const namedComponentSources = componentSnippetSources.filter((snippet) => + snippet.body.some((line) => line.includes(Placeholders.ComponentName)), + ); + + expect(namedComponentSources).toHaveLength(41); + for (const snippet of componentSnippetSources) { + expect(snippet.body.join('\n')).not.toContain(Placeholders.FileName); + } + for (const snippet of namedComponentSources) { + expect(requireSnippet(snippets, snippet.key).body.join('\n')).toContain( + directoryNameForIndex, + ); + } }); test('uses defaults for empty scopes but rejects nonempty invalid scopes', () => { From 8526737a6a6696593a332d9ab797c0d1e2c05e31 Mon Sep 17 00:00:00 2001 From: Dawid Harat Date: Wed, 29 Jul 2026 14:18:03 +0200 Subject: [PATCH 4/6] fix: resolve anonymous memo components in rmc and rmcp --- docs/Snippets.md | 4 ++-- src/snippets/generated.code-snippets | 4 ++-- src/sourceSnippets/components.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/Snippets.md b/docs/Snippets.md index 7cd2097..969fbcb 100644 --- a/docs/Snippets.md +++ b/docs/Snippets.md @@ -386,7 +386,7 @@ export default FileName; ```javascript import { memo } from 'react'; -const FileName = memo(() => { +const FileName = memo(function FileName() { return <>$2; }); @@ -399,7 +399,7 @@ export default FileName; import { memo } from 'react'; import PropTypes from 'prop-types'; -const FileName = memo((props) => { +const FileName = memo(function FileName(props) { return <>$2; }); diff --git a/src/snippets/generated.code-snippets b/src/snippets/generated.code-snippets index ba42746..e9176e7 100644 --- a/src/snippets/generated.code-snippets +++ b/src/snippets/generated.code-snippets @@ -487,7 +487,7 @@ "body": [ "import { memo } from 'react'", "", - "const ${1:${TM_FILENAME_BASE}} = memo(() => {", + "const ${1:${TM_FILENAME_BASE}} = memo(function ${1:${TM_FILENAME_BASE}}() {", " return (", " <>${2:first}", " )", @@ -504,7 +504,7 @@ "import PropTypes from 'prop-types'", "import { memo } from 'react'", "", - "const ${1:${TM_FILENAME_BASE}} = memo((props) => {", + "const ${1:${TM_FILENAME_BASE}} = memo(function ${1:${TM_FILENAME_BASE}}(props) {", " return (", " <>${2:first}", " )", diff --git a/src/sourceSnippets/components.ts b/src/sourceSnippets/components.ts index 9387771..02d8125 100644 --- a/src/sourceSnippets/components.ts +++ b/src/sourceSnippets/components.ts @@ -228,7 +228,7 @@ const reactFunctionMemoComponent: ComponentsSnippet = { body: [ ...reactWithMemo, '', - `const ${Placeholders.ComponentName} = memo(() => {`, + `const ${Placeholders.ComponentName} = memo(function ${Placeholders.ComponentName}() {`, ...innerComponent, '})', ...exportDefault, @@ -243,7 +243,7 @@ const reactFunctionMemoComponentWithPropTypes: ComponentsSnippet = { "import PropTypes from 'prop-types'", ...reactWithMemo, '', - `const ${Placeholders.ComponentName} = memo((props) => {`, + `const ${Placeholders.ComponentName} = memo(function ${Placeholders.ComponentName}(props) {`, ...innerComponent, '})', '', From 66f27cd4009126fd0a33e458cb0bd3dcb9cd2eef Mon Sep 17 00:00:00 2001 From: Dawid Harat Date: Wed, 29 Jul 2026 16:10:58 +0200 Subject: [PATCH 5/6] fix: derive valid identifiers from filenames of any casing --- README.md | 4 + src/helpers/snippetPlaceholders.ts | 5 +- src/snippets/generated.code-snippets | 158 +++++++++++++-------------- src/sourceSnippets/others.ts | 4 +- src/sourceSnippets/redux.ts | 12 +- src/types.ts | 8 +- tests/generation.test.ts | 50 ++++++++- 7 files changed, 144 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index ac6260e..3fcb931 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,10 @@ Controls whether TypeScript component snippets name their props type `Props` or Controls how component snippets derive their name. `directoryForIndex` uses the parent directory for an `index.*` file, so `Button/index.tsx` becomes `Button`; other filenames still use their basename. +Either way the name is converted to PascalCase, so `delete-modal.tsx` becomes `DeleteModal`. Separators such as `-`, `_`, and `.` are dropped, and names that are already PascalCase are left unchanged. + +Redux slice snippets are not components, so they are only renamed when the filename would not be a valid identifier: `user-slice.ts` becomes `userSlice`, while `UserSlice.ts` is left alone. + ### `componentWrapper` **Type:** `string` diff --git a/src/helpers/snippetPlaceholders.ts b/src/helpers/snippetPlaceholders.ts index 7cc3d9b..472ada7 100644 --- a/src/helpers/snippetPlaceholders.ts +++ b/src/helpers/snippetPlaceholders.ts @@ -12,8 +12,8 @@ const propsPlaceholders = { } as const; const componentNamePlaceholders = { - filename: Mappings.FileName, - directoryForIndex: Mappings.DirectoryNameForIndex, + filename: Mappings.ComponentFileName, + directoryForIndex: Mappings.ComponentDirectoryNameForIndex, } as const; export const replaceSnippetPlaceholders = ( @@ -48,6 +48,7 @@ export const replaceSnippetPlaceholders = ( .replaceAll(Placeholders.TypeState, statePlaceholder) .replaceAll(Placeholders.ComponentProps, propsName) .replaceAll(Placeholders.ComponentName, componentNamePlaceholder) + .replaceAll(Placeholders.SliceName, Mappings.SliceFileName) .replaceAll(Placeholders.FileName, Mappings.FileName) .replaceAll(Placeholders.ComponentWrapper, componentWrapperPlaceholder) .replaceAll(Placeholders.FirstTab, Mappings.FirstTab) diff --git a/src/snippets/generated.code-snippets b/src/snippets/generated.code-snippets index e9176e7..369d765 100644 --- a/src/snippets/generated.code-snippets +++ b/src/snippets/generated.code-snippets @@ -22,7 +22,7 @@ "", "type State = {}", "", - "export default class ${1:${TM_FILENAME_BASE}} extends Component {", + "export default class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " state = {}", "", " render() {", @@ -44,7 +44,7 @@ "", "type State = {}", "", - "class ${1:${TM_FILENAME_BASE}} extends Component {", + "class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " state = {}", "", " render() {", @@ -54,7 +54,7 @@ " }", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React component class with ES7 module system and TypeScript interfaces", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -64,13 +64,13 @@ "body": [ "type Props = {}", "", - "function ${1:${TM_FILENAME_BASE}}({}: Props) {", + "function ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}({}: Props) {", " return (", " <>${2:first}", " )", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React Functional Component with ES7 module system and TypeScript interface", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -80,7 +80,7 @@ "body": [ "type Props = {}", "", - "export default function ${1:${TM_FILENAME_BASE}}({}: Props) {", + "export default function ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}({}: Props) {", " return (", " <>${2:first}", " )", @@ -94,13 +94,13 @@ "body": [ "type Props = {}", "", - "const ${1:${TM_FILENAME_BASE}} = (props: Props) => {", + "const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = (props: Props) => {", " return (", " <>${2:first}", " )", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React Arrow Function Component with ES7 module system and TypeScript interface", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -110,7 +110,7 @@ "body": [ "type Props = {}", "", - "export const ${1:${TM_FILENAME_BASE}} = (props: Props) => {", + "export const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = (props: Props) => {", " return (", " <>${2:first}", " )", @@ -126,7 +126,7 @@ "", "type Props = {}", "", - "export default class ${1:${TM_FILENAME_BASE}} extends PureComponent {", + "export default class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends PureComponent {", " render() {", " return (", " <>${2:first}", @@ -144,7 +144,7 @@ "", "type Props = {}", "", - "class ${1:${TM_FILENAME_BASE}} extends PureComponent {", + "class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends PureComponent {", " render() {", " return (", " <>${2:first}", @@ -152,7 +152,7 @@ " }", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React pure component class with ES7 module system and TypeScript interface", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -167,7 +167,7 @@ "", "type State = {}", "", - "export class ${1:${TM_FILENAME_BASE}} extends Component {", + "export class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " state = {}", "", " render() {", @@ -181,7 +181,7 @@ "", "const mapDispatchToProps = {}", "", - "export default connect(mapStateToProps, mapDispatchToProps)(${1:${TM_FILENAME_BASE}})" + "export default connect(mapStateToProps, mapDispatchToProps)(${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}})" ], "description": "Creates a React component class with connected redux and ES7 module system and TypeScript interfaces", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -192,7 +192,7 @@ "import { View, Text } from 'react-native'", "type Props = {}", "", - "const ${1:${TM_FILENAME_BASE}} = (props: Props) => {", + "const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = (props: Props) => {", " return (", " ", " ${2:first}", @@ -200,7 +200,7 @@ " )", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React Native Arrow Function Component with ES7 module system in TypeScript", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -211,7 +211,7 @@ "import { StyleSheet, Text, View } from 'react-native'", "type Props = {}", "", - "const ${1:${TM_FILENAME_BASE}} = (props: Props) => {", + "const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = (props: Props) => {", " return (", " ", " ${2:first}", @@ -219,7 +219,7 @@ " )", "}", "", - "export default ${1:${TM_FILENAME_BASE}}", + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}", "", "const styles = StyleSheet.create({})" ], @@ -241,7 +241,7 @@ " ${3:second}", "}", "", - "function ${1:${TM_FILENAME_BASE}}() {", + "function ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}() {", " const data = useLoaderData()", "", " return (", @@ -250,7 +250,7 @@ "}", "", "export { loader, action }", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "React Router v6.4+ TypeScript route with loader and action", "scope": "typescriptreact" @@ -258,7 +258,7 @@ "reactArrowFunctionComponent": { "prefix": "rafc", "body": [ - "export const ${1:${TM_FILENAME_BASE}} = () => {", + "export const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = () => {", " return (", " <>${2:first}", " )", @@ -273,15 +273,15 @@ "body": [ "import PropTypes from 'prop-types'", "", - "const ${1:${TM_FILENAME_BASE}} = props => {", + "const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = props => {", " return (", " <>${2:first}", " )", "}", "", - "${1:${TM_FILENAME_BASE}}.propTypes = {}", + "${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}.propTypes = {}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React Arrow Function Component with PropTypes (React 17-18; ignored by React 19)", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -289,13 +289,13 @@ "reactArrowFunctionExportComponent": { "prefix": "rafce", "body": [ - "const ${1:${TM_FILENAME_BASE}} = () => {", + "const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = () => {", " return (", " <>${2:first}", " )", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React Arrow Function Component with ES7 module system", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -305,7 +305,7 @@ "body": [ "import { Component } from 'react'", "", - "export default class ${1:${TM_FILENAME_BASE}} extends Component {", + "export default class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " render() {", " return (", " <>${2:first}", @@ -323,7 +323,7 @@ "import PropTypes from 'prop-types'", "import { Component } from 'react'", "", - "export default class ${1:${TM_FILENAME_BASE}} extends Component {", + "export default class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " static propTypes = {${3:second}: ${4:third}}", "", " render() {", @@ -343,7 +343,7 @@ "import { Component } from 'react'", "import { connect } from 'react-redux'", "", - "export class ${1:${TM_FILENAME_BASE}} extends Component {", + "export class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " render() {", " return (", " <>${2:first}", @@ -355,7 +355,7 @@ "", "const mapDispatchToProps = {}", "", - "export default connect(mapStateToProps, mapDispatchToProps)(${1:${TM_FILENAME_BASE}})" + "export default connect(mapStateToProps, mapDispatchToProps)(${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}})" ], "description": "Creates a React component class with connected redux and ES7 module system", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -367,7 +367,7 @@ "import { Component } from 'react'", "import { connect } from 'react-redux'", "", - "export class ${1:${TM_FILENAME_BASE}} extends Component {", + "export class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " static propTypes = {", " ${3:second}: ${4:third}", " }", @@ -383,7 +383,7 @@ "", "const mapDispatchToProps = {}", "", - "export default connect(mapStateToProps, mapDispatchToProps)(${1:${TM_FILENAME_BASE}})" + "export default connect(mapStateToProps, mapDispatchToProps)(${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}})" ], "description": "Creates a React component class with PropTypes with connected redux and ES7 module system", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -393,7 +393,7 @@ "body": [ "import { Component } from 'react'", "", - "export class ${1:${TM_FILENAME_BASE}} extends Component {", + "export class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " render() {", " return (", " <>${2:first}", @@ -401,7 +401,7 @@ " }", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React component class with ES7 module system", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -412,7 +412,7 @@ "import PropTypes from 'prop-types'", "import { Component } from 'react'", "", - "export class ${1:${TM_FILENAME_BASE}} extends Component {", + "export class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " static propTypes = {}", "", " render() {", @@ -422,7 +422,7 @@ " }", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React component class with ES7 module system", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -432,7 +432,7 @@ "body": [ "import { PureComponent } from 'react'", "", - "export class ${1:${TM_FILENAME_BASE}} extends PureComponent {", + "export class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends PureComponent {", " render() {", " return (", " <>${2:first}", @@ -440,7 +440,7 @@ " }", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React pure component class with ES7 module system export", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -450,7 +450,7 @@ "body": [ "import { PureComponent } from 'react'", "", - "export default class ${1:${TM_FILENAME_BASE}} extends PureComponent {", + "export default class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends PureComponent {", " render() {", " return (", " <>${2:first}", @@ -468,7 +468,7 @@ "import PropTypes from 'prop-types'", "import { PureComponent } from 'react'", "", - "export default class ${1:${TM_FILENAME_BASE}} extends PureComponent {", + "export default class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends PureComponent {", " static propTypes = {}", "", " render() {", @@ -487,13 +487,13 @@ "body": [ "import { memo } from 'react'", "", - "const ${1:${TM_FILENAME_BASE}} = memo(function ${1:${TM_FILENAME_BASE}}() {", + "const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = memo(function ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}() {", " return (", " <>${2:first}", " )", "})", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React Memo Function Component with ES7 module system", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -504,15 +504,15 @@ "import PropTypes from 'prop-types'", "import { memo } from 'react'", "", - "const ${1:${TM_FILENAME_BASE}} = memo(function ${1:${TM_FILENAME_BASE}}(props) {", + "const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = memo(function ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}(props) {", " return (", " <>${2:first}", " )", "})", "", - "${1:${TM_FILENAME_BASE}}.propTypes = {}", + "${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}.propTypes = {}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React Memo Function Component with PropTypes (React 17-18; ignored by React 19)", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -520,7 +520,7 @@ "reactFunctionalComponent": { "prefix": "rfc", "body": [ - "export default function ${1:${TM_FILENAME_BASE}}() {", + "export default function ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}() {", " return (", " <>${2:first}", " )", @@ -535,7 +535,7 @@ "body": [ "import { connect } from 'react-redux'", "", - "export const ${1:${TM_FILENAME_BASE}} = (props) => {", + "export const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = (props) => {", " return (", " <>${2:first}", " )", @@ -545,7 +545,7 @@ "", "const mapDispatchToProps = {}", "", - "export default connect(mapStateToProps, mapDispatchToProps)(${1:${TM_FILENAME_BASE}})" + "export default connect(mapStateToProps, mapDispatchToProps)(${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}})" ], "description": "Creates a React functional component with connected redux and ES7 module system", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -556,13 +556,13 @@ "import PropTypes from 'prop-types'", "import { connect } from 'react-redux'", "", - "export const ${1:${TM_FILENAME_BASE}} = (props) => {", + "export const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = (props) => {", " return (", " <>${2:first}", " )", "}", "", - "${1:${TM_FILENAME_BASE}}.propTypes = {", + "${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}.propTypes = {", " ${3:second}: PropTypes.${4:third}", "}", "", @@ -570,7 +570,7 @@ "", "const mapDispatchToProps = {}", "", - "export default connect(mapStateToProps, mapDispatchToProps)(${1:${TM_FILENAME_BASE}})" + "export default connect(mapStateToProps, mapDispatchToProps)(${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}})" ], "description": "DEPRECATED: React Redux function component with PropTypes (React 17-18; ignored by React 19)", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -580,15 +580,15 @@ "body": [ "import PropTypes from 'prop-types'", "", - "function ${1:${TM_FILENAME_BASE}}(props) {", + "function ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}(props) {", " return (", " <>${2:first}", " )", "}", "", - "${1:${TM_FILENAME_BASE}}.propTypes = {}", + "${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}.propTypes = {}", "", - "export default ${1:${TM_FILENAME_BASE}}", + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}", "" ], "description": "Creates a React Functional Component with PropTypes (React 17-18; ignored by React 19)", @@ -597,13 +597,13 @@ "reactFunctionalExportComponent": { "prefix": "rfce", "body": [ - "function ${1:${TM_FILENAME_BASE}}() {", + "function ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}() {", " return (", " <>${2:first}", " )", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "Creates a React Functional Component with ES7 module system", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -1360,7 +1360,7 @@ "import { Text, View } from 'react-native'", "import { Component } from 'react'", "", - "export default class ${1:${TM_FILENAME_BASE}} extends Component {", + "export default class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " render() {", " return (", " ", @@ -1378,7 +1378,7 @@ "import { Text, View } from 'react-native'", "import { Component } from 'react'", "", - "export class ${1:${TM_FILENAME_BASE}} extends Component {", + "export class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " render() {", " return (", " ", @@ -1388,7 +1388,7 @@ " }", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "scope": "typescript,typescriptreact,javascript,javascriptreact" }, @@ -1398,7 +1398,7 @@ "import { Text, StyleSheet, View } from 'react-native'", "import { Component } from 'react'", "", - "export default class ${1:${TM_FILENAME_BASE}} extends Component {", + "export default class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends Component {", " render() {", " return (", " ", @@ -1418,7 +1418,7 @@ "import { useState, useEffect } from 'react'", "import { View, SafeAreaView, Text, StyleSheet } from 'react-native'", "", - "const ${1:${TM_FILENAME_BASE}} = ({ ${2:props} }) => {", + "const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = ({ ${2:props} }) => {", " const [${3:state}, ${4:setState}] = useState(${5:initialState})", "", " useEffect(() => {", @@ -1442,7 +1442,7 @@ " },", "})", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "React Native functional component with useState, useEffect, SafeAreaView and basic styling", "scope": "typescript,typescriptreact,javascript,javascriptreact" @@ -1451,7 +1451,7 @@ "prefix": "rnf", "body": [ "import { View, Text } from 'react-native'", - "export default function ${1:${TM_FILENAME_BASE}}() {", + "export default function ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}() {", " return (", " ", " ${2:first}", @@ -1465,7 +1465,7 @@ "prefix": "rnfs", "body": [ "import { StyleSheet, Text, View } from 'react-native'", - "export default function ${1:${TM_FILENAME_BASE}}() {", + "export default function ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}() {", " return (", " ", " ${2:first}", @@ -1481,7 +1481,7 @@ "prefix": "rnfe", "body": [ "import { View, Text } from 'react-native'", - "const ${1:${TM_FILENAME_BASE}} = () => {", + "const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = () => {", " return (", " ", " ${2:first}", @@ -1489,7 +1489,7 @@ " )", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "scope": "typescript,typescriptreact,javascript,javascriptreact" }, @@ -1497,7 +1497,7 @@ "prefix": "rnfes", "body": [ "import { StyleSheet, Text, View } from 'react-native'", - "const ${1:${TM_FILENAME_BASE}} = () => {", + "const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = () => {", " return (", " ", " ${2:first}", @@ -1505,7 +1505,7 @@ " )", "}", "", - "export default ${1:${TM_FILENAME_BASE}}", + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}", "", "const styles = StyleSheet.create({})" ], @@ -1524,7 +1524,7 @@ "import { Text, View } from 'react-native'", "import { PureComponent } from 'react'", "", - "export default class ${1:${TM_FILENAME_BASE}} extends PureComponent {", + "export default class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends PureComponent {", " render() {", " return (", " ", @@ -1542,7 +1542,7 @@ "import { Text, View } from 'react-native'", "import { PureComponent } from 'react'", "", - "export class ${1:${TM_FILENAME_BASE}} extends PureComponent {", + "export class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} extends PureComponent {", " render() {", " return (", " ", @@ -1552,7 +1552,7 @@ " }", "}", "", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "scope": "typescript,typescriptreact,javascript,javascriptreact" }, @@ -1655,15 +1655,15 @@ "", "}", "", - "const ${1:${TM_FILENAME_BASE}} = createSlice({", + "const ${1:${TM_FILENAME_BASE/^(?:([A-Za-z_$][A-Za-z0-9_$]*)|(.*))$/${1}${2:/camelcase}/}} = createSlice({", " name: ${3:second},", " initialState,", " reducers: {}", "});", "", - "export const {} = ${1:${TM_FILENAME_BASE}}.actions", + "export const {} = ${1:${TM_FILENAME_BASE/^(?:([A-Za-z_$][A-Za-z0-9_$]*)|(.*))$/${1}${2:/camelcase}/}}.actions", "", - "export default ${1:${TM_FILENAME_BASE}}.reducer" + "export default ${1:${TM_FILENAME_BASE/^(?:([A-Za-z_$][A-Za-z0-9_$]*)|(.*))$/${1}${2:/camelcase}/}}.reducer" ], "scope": "typescript,typescriptreact,javascript,javascriptreact" }, @@ -1677,7 +1677,7 @@ " status: 'idle',", "}", "", - "const ${1:${TM_FILENAME_BASE}} = createSlice({", + "const ${1:${TM_FILENAME_BASE/^(?:([A-Za-z_$][A-Za-z0-9_$]*)|(.*))$/${1}${2:/camelcase}/}} = createSlice({", " name: '${3:second}',", " initialState,", " reducers: {},", @@ -1695,9 +1695,9 @@ " },", "})", "", - "export const {} = ${1:${TM_FILENAME_BASE}}.actions", + "export const {} = ${1:${TM_FILENAME_BASE/^(?:([A-Za-z_$][A-Za-z0-9_$]*)|(.*))$/${1}${2:/camelcase}/}}.actions", "", - "export default ${1:${TM_FILENAME_BASE}}.reducer" + "export default ${1:${TM_FILENAME_BASE/^(?:([A-Za-z_$][A-Za-z0-9_$]*)|(.*))$/${1}${2:/camelcase}/}}.reducer" ], "scope": "typescript,typescriptreact,javascript,javascriptreact" }, @@ -2225,7 +2225,7 @@ " ${3:second}", "}", "", - "function ${1:${TM_FILENAME_BASE}}() {", + "function ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}() {", " const data = useLoaderData()", "", " return (", @@ -2234,7 +2234,7 @@ "}", "", "export { loader, action }", - "export default ${1:${TM_FILENAME_BASE}}" + "export default ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}" ], "description": "React Router v6.4+ JavaScript route with loader and action", "scope": "javascriptreact" diff --git a/src/sourceSnippets/others.ts b/src/sourceSnippets/others.ts index d14a720..8023ef1 100644 --- a/src/sourceSnippets/others.ts +++ b/src/sourceSnippets/others.ts @@ -454,7 +454,7 @@ const routeWithLoaderAction: OthersSnippet = { ` ${Placeholders.SecondTab}`, '}', '', - `function ${Placeholders.FileName}() {`, + `function ${Placeholders.ComponentName}() {`, ' const data = useLoaderData()', '', ' return (', @@ -463,7 +463,7 @@ const routeWithLoaderAction: OthersSnippet = { '}', '', `export { loader, action }`, - `export default ${Placeholders.FileName}`, + `export default ${Placeholders.ComponentName}`, ], description: 'React Router v6.4+ JavaScript route with loader and action', scope: 'javascriptreact', diff --git a/src/sourceSnippets/redux.ts b/src/sourceSnippets/redux.ts index a4ee4a5..b3a79e7 100644 --- a/src/sourceSnippets/redux.ts +++ b/src/sourceSnippets/redux.ts @@ -79,15 +79,15 @@ const reduxSlice: ReduxSnippet = { '', '}', '', - `const ${Placeholders.FileName} = createSlice({`, + `const ${Placeholders.SliceName} = createSlice({`, ` name: ${Placeholders.SecondTab},`, ' initialState,', ' reducers: {}', '});', '', - `export const {} = ${Placeholders.FileName}.actions`, + `export const {} = ${Placeholders.SliceName}.actions`, '', - `export default ${Placeholders.FileName}.reducer`, + `export default ${Placeholders.SliceName}.reducer`, ], }; @@ -102,7 +102,7 @@ const reduxSliceWithExtraReducers: ReduxSnippet = { " status: 'idle',", '}', '', - `const ${Placeholders.FileName} = createSlice({`, + `const ${Placeholders.SliceName} = createSlice({`, ` name: '${Placeholders.SecondTab}',`, ' initialState,', ' reducers: {},', @@ -120,9 +120,9 @@ const reduxSliceWithExtraReducers: ReduxSnippet = { ' },', '})', '', - `export const {} = ${Placeholders.FileName}.actions`, + `export const {} = ${Placeholders.SliceName}.actions`, '', - `export default ${Placeholders.FileName}.reducer`, + `export default ${Placeholders.SliceName}.reducer`, ], }; diff --git a/src/types.ts b/src/types.ts index 90c1e7d..dca0273 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,6 +42,7 @@ export const DEFAULT_GENERATION_SETTINGS: GenerationSettings = { export const Placeholders = { FileName: 'file', ComponentName: 'componentName', + SliceName: 'sliceName', FirstTab: 'first', SecondTab: 'second', ThirdTab: 'third', @@ -55,8 +56,11 @@ export const Placeholders = { export const Mappings = { FileName: '${1:${TM_FILENAME_BASE}}', - DirectoryNameForIndex: - '${1:${TM_FILEPATH/.*?[\\\\/](?:([^\\\\/]+)[\\\\/]index|([^\\\\/]+?))(?:\\.[^\\\\/.]+)?$/${1}${2}/}}', + ComponentFileName: '${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}', + ComponentDirectoryNameForIndex: + '${1:${TM_FILEPATH/.*?[\\\\/](?:([^\\\\/]+)[\\\\/]index|([^\\\\/]+?))(?:\\.[^\\\\/.]+)?$/${1:/pascalcase}${2:/pascalcase}/}}', + SliceFileName: + '${1:${TM_FILENAME_BASE/^(?:([A-Za-z_$][A-Za-z0-9_$]*)|(.*))$/${1}${2:/camelcase}/}}', FirstTab: '${2:first}', SecondTab: '${3:second}', ThirdTab: '${4:third}', diff --git a/tests/generation.test.ts b/tests/generation.test.ts index 48afcc5..8168d46 100644 --- a/tests/generation.test.ts +++ b/tests/generation.test.ts @@ -46,7 +46,8 @@ const componentSnippetSources = [ ...reactNativeSnippets, ...typescriptSnippets, ]; -const directoryNameForIndex = Mappings.DirectoryNameForIndex; +const directoryNameForIndex = Mappings.ComponentDirectoryNameForIndex; +const componentName = Mappings.ComponentFileName; const requireJsonObject = (value: unknown, context: string): JsonObject => { if (typeof value !== 'object' || value === null || Array.isArray(value)) { @@ -201,14 +202,14 @@ describe('snippet compiler', () => { const conflictingBodies = Object.values(snippets).filter((snippet) => { const body = snippet.body.join('\n'); return ( - body.includes('${1:${TM_FILENAME_BASE}}') && body.includes('${1:first}') + body.includes('${1:${TM_FILENAME_BASE') && body.includes('${1:first}') ); }); const rnxf = requireSnippet(snippets, 'reactNativeExtendedFunctional'); expect(conflictingBodies).toEqual([]); expect(rnxf.body.join('\n')).toContain( - 'const ${1:${TM_FILENAME_BASE}} = ({ ${2:props} }) => {', + `const ${componentName} = ({ ${'${2:props}'} }) => {`, ); expect(rnxf.body.join('\n')).toContain( 'const [${3:state}, ${4:setState}] = useState(${5:initialState})', @@ -325,19 +326,19 @@ describe('snippet compiler', () => { expect( requireSnippet(componentNamedProps, 'typescriptReactFunctionalComponent') .body, - ).toContain('type ${1:${TM_FILENAME_BASE}}Props = {}'); + ).toContain(`type ${componentName}Props = {}`); expect( requireSnippet(componentNamedProps, 'typescriptReactFunctionalComponent') .body, ).toContain( - 'export default function ${1:${TM_FILENAME_BASE}}({}: ${1:${TM_FILENAME_BASE}}Props) {', + `export default function ${componentName}({}: ${componentName}Props) {`, ); expect( requireSnippet( componentNamedInterfaces, 'typescriptReactFunctionalComponent', ).body, - ).toContain('interface ${1:${TM_FILENAME_BASE}}Props {}'); + ).toContain(`interface ${componentName}Props {}`); expect( requireSnippet(directoryNamedComponents, 'reactFunctionalComponent').body, ).toContain(`export default function ${directoryNameForIndex}() {`); @@ -378,6 +379,43 @@ describe('snippet compiler', () => { } }); + test('derives valid identifiers from filenames of any casing', () => { + const snippets = defaultSnippets(); + + for (const snippet of componentSnippetSources) { + expect( + requireSnippet(snippets, snippet.key).body.join('\n'), + ).not.toContain(Mappings.FileName); + } + expect(requireSnippet(snippets, 'routeWithLoaderAction').body).toContain( + `function ${componentName}() {`, + ); + expect(requireSnippet(snippets, 'reduxSlice').body).toContain( + `const ${Mappings.SliceFileName} = createSlice({`, + ); + expect( + requireSnippet(snippets, 'reduxSliceWithExtraReducers').body, + ).toContain(`const ${Mappings.SliceFileName} = createSlice({`); + expect(requireSnippet(snippets, 'setupReactTest').body).toContain( + `import { ${Mappings.FileName} } from '../${Mappings.FileName}'`, + ); + }); + + test('renames slices only when the filename is not a valid identifier', () => { + const pattern = Mappings.SliceFileName.match( + /TM_FILENAME_BASE\/(.+?)\/\$\{1\}/, + )?.[1]; + if (!pattern) throw new Error('Expected SliceFileName to define a pattern'); + const isPassedThrough = (name: string) => + new RegExp(pattern).exec(name)?.[1] !== undefined; + + expect(isPassedThrough('UserSlice')).toBe(true); + expect(isPassedThrough('userSlice')).toBe(true); + expect(isPassedThrough('delete_modal')).toBe(true); + expect(isPassedThrough('delete-modal')).toBe(false); + expect(isPassedThrough('user profile')).toBe(false); + }); + test('uses defaults for empty scopes but rejects nonempty invalid scopes', () => { const snippets = parseGeneratedSnippets( buildSnippets({ From 577c7c59e1332894496a4e697723c808dcffad1e Mon Sep 17 00:00:00 2001 From: Dawid Harat Date: Thu, 30 Jul 2026 11:23:01 +0200 Subject: [PATCH 6/6] chore(deps): bump brace-expansion override to 5.0.8 --- bun.lock | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 3295147..58623a0 100644 --- a/bun.lock +++ b/bun.lock @@ -16,7 +16,7 @@ }, }, "overrides": { - "brace-expansion": "5.0.7", + "brace-expansion": "5.0.8", "fast-uri": "3.1.4", "form-data": "4.0.6", "js-yaml": "4.3.0", @@ -275,7 +275,7 @@ "boundary": ["boundary@2.0.0", "", {}, "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="], - "brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], + "brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], diff --git a/package.json b/package.json index e5e2aa6..b83fb00 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "typescript": "6.0.3" }, "overrides": { - "brace-expansion": "5.0.7", + "brace-expansion": "5.0.8", "fast-uri": "3.1.4", "form-data": "4.0.6", "js-yaml": "4.3.0",