diff --git a/README.md b/README.md index cf1dfb6..3fcb931 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,40 @@ 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`. + +### `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. + +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` + +**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/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/docs/Snippets.md b/docs/Snippets.md index 2bbc8ab..969fbcb 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`
@@ -384,7 +386,7 @@ export default FileName; ```javascript import { memo } from 'react'; -const FileName = memo(() => { +const FileName = memo(function FileName() { return <>$2; }); @@ -397,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; }); @@ -479,7 +481,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 c89cebe..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", @@ -121,6 +121,36 @@ "interface" ], "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.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`.", + "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..8349235 100644 --- a/src/helpers/extensionConfig.ts +++ b/src/helpers/extensionConfig.ts @@ -1,6 +1,12 @@ import { workspace } from 'vscode'; -import { DEFAULT_GENERATION_SETTINGS, GenerationSettings } from '../types'; +import { + ComponentNameSource, + ComponentWrapper, + DEFAULT_GENERATION_SETTINGS, + GenerationSettings, + TypescriptPropsNaming, +} from '../types'; export type ExtensionSettings = GenerationSettings; @@ -24,6 +30,18 @@ const extensionConfig = (): ExtensionSettings => { 'typescriptPropsStatePrefix', DEFAULT_GENERATION_SETTINGS.typescriptPropsStatePrefix, ), + typescriptPropsNaming: config.get( + '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 24856b8..472ada7 100644 --- a/src/helpers/snippetPlaceholders.ts +++ b/src/helpers/snippetPlaceholders.ts @@ -1,25 +1,59 @@ import { GenerationSettings, Mappings, Placeholders } from '../types'; +const propsPlaceholders = { + generic: { + type: Mappings.TypeProps, + interface: Mappings.InterfaceProps, + }, + component: { + type: Mappings.ComponentTypeProps, + interface: Mappings.ComponentInterfaceProps, + }, +} as const; + +const componentNamePlaceholders = { + filename: Mappings.ComponentFileName, + directoryForIndex: Mappings.ComponentDirectoryNameForIndex, +} as const; + export const replaceSnippetPlaceholders = ( snippetString: string, - typescriptPropsStatePrefix: GenerationSettings['typescriptPropsStatePrefix'], + settings: GenerationSettings, ): string => { + const { + componentNameSource, + componentWrapper, + typescriptPropsNaming, + typescriptPropsStatePrefix, + } = settings; const propsPlaceholder = - typescriptPropsStatePrefix === 'type' - ? Mappings.TypeProps - : Mappings.InterfaceProps; + propsPlaceholders[typescriptPropsNaming][typescriptPropsStatePrefix]; + const componentNamePlaceholder = + componentNamePlaceholders[componentNameSource]; + const propsName = + typescriptPropsNaming === 'component' + ? Mappings.ComponentProps + : Mappings.Props; const statePlaceholder = typescriptPropsStatePrefix === 'type' ? Mappings.TypeState : Mappings.InterfaceState; + const componentWrapperPlaceholder = + componentWrapper === 'fragment' + ? Mappings.FragmentComponentWrapper + : Mappings.DivComponentWrapper; return String(snippetString) + .replaceAll(Placeholders.TypeProps, propsPlaceholder) + .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) .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/snippets/generated.code-snippets b/src/snippets/generated.code-snippets index ba42746..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(() => {", + "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((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/components.ts b/src/sourceSnippets/components.ts index 761a5cb..02d8125 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(function ${Placeholders.ComponentName}() {`, ...innerComponent, '})', ...exportDefault, @@ -243,11 +243,11 @@ const reactFunctionMemoComponentWithPropTypes: ComponentsSnippet = { "import PropTypes from 'prop-types'", ...reactWithMemo, '', - `const ${Placeholders.FileName} = memo((props) => {`, + `const ${Placeholders.ComponentName} = memo(function ${Placeholders.ComponentName}(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/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/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/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/sourceSnippets/sharedSnippets.ts b/src/sourceSnippets/sharedSnippets.ts index f4b4b50..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,24 +37,27 @@ export const reduxTypeScriptComponentExport = [ '', 'const mapDispatchToProps = {}', '', - `export default connect(mapStateToProps, mapDispatchToProps)(${Placeholders.FileName})`, + `export default connect(mapStateToProps, mapDispatchToProps)(${Placeholders.ComponentName})`, ]; export const innerComponent = [ ' return (', - ` <>${Placeholders.FirstTab}`, + ` ${Placeholders.ComponentWrapper}`, ' )', ]; export const innerComponentReturn = [ ' render() {', ' return (', - ` <>${Placeholders.FirstTab}`, + ` ${Placeholders.ComponentWrapper}`, ' )', ' }', ]; -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 f9a1492..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 {`, + `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 {`, + `class ${Placeholders.ComponentName} extends Component<${Placeholders.ComponentProps}, State> {`, ' state = {}', '', ...innerComponentReturn, @@ -87,7 +87,7 @@ const typescriptReactFunctionalExportComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `function ${Placeholders.FileName}({}: Props) {`, + `function ${Placeholders.ComponentName}({}: ${Placeholders.ComponentProps}) {`, ...innerComponent, '}', ...exportDefault, @@ -103,7 +103,7 @@ const typescriptReactFunctionalComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `export default function ${Placeholders.FileName}({}: Props) {`, + `export default function ${Placeholders.ComponentName}({}: ${Placeholders.ComponentProps}) {`, ...innerComponent, '}', ], @@ -118,7 +118,7 @@ const typescriptReactArrowFunctionExportComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `const ${Placeholders.FileName} = (props: Props) => {`, + `const ${Placeholders.ComponentName} = (props: ${Placeholders.ComponentProps}) => {`, ...innerComponent, '}', ...exportDefault, @@ -134,7 +134,7 @@ const typescriptReactArrowFunctionComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `export const ${Placeholders.FileName} = (props: Props) => {`, + `export const ${Placeholders.ComponentName} = (props: ${Placeholders.ComponentProps}) => {`, ...innerComponent, '}', ], @@ -149,7 +149,7 @@ const typescriptReactClassPureComponent: TypescriptSnippet = { ...reactPureComponent, '', ...propsTypeInterface, - `export default class ${Placeholders.FileName} extends PureComponent {`, + `export default class ${Placeholders.ComponentName} extends PureComponent<${Placeholders.ComponentProps}> {`, ...innerComponentReturn, '}', ], @@ -164,7 +164,7 @@ const typescriptReactClassExportPureComponent: TypescriptSnippet = { ...reactPureComponent, '', ...propsTypeInterface, - `class ${Placeholders.FileName} extends PureComponent {`, + `class ${Placeholders.ComponentName} extends PureComponent<${Placeholders.ComponentProps}> {`, ...innerComponentReturn, '}', ...exportDefault, @@ -181,7 +181,7 @@ const typescriptReactClassComponentRedux: TypescriptSnippet = { ...reactComponent, '', ...propsStateInterface, - `export class ${Placeholders.FileName} extends Component {`, + `export class ${Placeholders.ComponentName} extends Component<${Placeholders.ComponentProps}, State> {`, ' state = {}', '', ...innerComponentReturn, @@ -200,7 +200,7 @@ const typescriptReactNativeArrowFunctionComponent: TypescriptSnippet = { ...react, '', ...propsTypeInterface, - `const ${Placeholders.FileName} = (props: Props) => {`, + `const ${Placeholders.ComponentName} = (props: ${Placeholders.ComponentProps}) => {`, ' return (', ' ', ` ${Placeholders.FirstTab}`, @@ -222,7 +222,7 @@ const typescriptReactNativeArrowFunctionComponentWithStyles: TypescriptSnippet = ...react, '', ...propsTypeInterface, - `const ${Placeholders.FileName} = (props: Props) => {`, + `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 01422c8..dca0273 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,11 +15,18 @@ 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 = { readonly languageScopes: string; readonly importReactOnTop: boolean; readonly typescript: boolean; readonly typescriptPropsStatePrefix: 'type' | 'interface'; + readonly typescriptPropsNaming: TypescriptPropsNaming; + readonly componentNameSource: ComponentNameSource; + readonly componentWrapper: ComponentWrapper; }; export const DEFAULT_GENERATION_SETTINGS: GenerationSettings = { @@ -27,28 +34,46 @@ export const DEFAULT_GENERATION_SETTINGS: GenerationSettings = { importReactOnTop: false, typescript: true, typescriptPropsStatePrefix: 'type', + typescriptPropsNaming: 'generic', + componentNameSource: 'filename', + componentWrapper: 'fragment', }; export const Placeholders = { FileName: 'file', + ComponentName: 'componentName', + SliceName: 'sliceName', FirstTab: 'first', SecondTab: 'second', ThirdTab: 'third', Capitalize: 'capitalize', TypeProps: 'typeProps', TypeState: 'typeState', + ComponentProps: 'componentProps', + ComponentWrapper: 'componentWrapper', FourthTabCapitalize: 'fourthTabCapitalize', } as const; export const Mappings = { FileName: '${1:${TM_FILENAME_BASE}}', + 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}', Capitalize: '${2/(.*)/${1:/capitalize}/}', TypeProps: 'type Props = {}', + ComponentTypeProps: 'type componentNameProps = {}', TypeState: 'type State = {}', InterfaceProps: 'interface Props {}', + ComponentInterfaceProps: 'interface componentNameProps {}', InterfaceState: 'interface State {}', + Props: 'Props', + ComponentProps: 'componentNameProps', + FragmentComponentWrapper: '<>first', + DivComponentWrapper: '
first
', FourthTabCapitalize: '${4/(.*)/${1:/capitalize}/}', } as const; diff --git a/tests/generation.test.ts b/tests/generation.test.ts index 96780d8..8168d46 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,14 @@ const packageManifestPath = fileURLToPath( type JsonObject = Readonly>; +const componentSnippetSources = [ + ...componentsSnippets, + ...reactNativeSnippets, + ...typescriptSnippets, +]; +const directoryNameForIndex = Mappings.ComponentDirectoryNameForIndex; +const componentName = Mappings.ComponentFileName; + 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`); @@ -76,13 +89,21 @@ const parseManifestGenerationDefaults = ( const importReactOnTop = readDefault('importReactOnTop'); const typescript = readDefault('typescript'); const typescriptPropsStatePrefix = readDefault('typescriptPropsStatePrefix'); + const typescriptPropsNaming = readDefault('typescriptPropsNaming'); + const componentNameSource = readDefault('componentNameSource'); + const componentWrapper = readDefault('componentWrapper'); if ( typeof languageScopes !== 'string' || typeof importReactOnTop !== 'boolean' || typeof typescript !== 'boolean' || (typescriptPropsStatePrefix !== 'type' && - typescriptPropsStatePrefix !== 'interface') + typescriptPropsStatePrefix !== 'interface') || + (typescriptPropsNaming !== 'generic' && + typescriptPropsNaming !== 'component') || + (componentNameSource !== 'filename' && + componentNameSource !== 'directoryForIndex') || + (componentWrapper !== 'fragment' && componentWrapper !== 'div') ) { throw new Error('Package generation defaults have invalid types'); } @@ -92,6 +113,9 @@ const parseManifestGenerationDefaults = ( importReactOnTop, typescript, typescriptPropsStatePrefix, + typescriptPropsNaming, + componentNameSource, + componentWrapper, }; }; @@ -178,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})', @@ -251,6 +275,33 @@ describe('snippet compiler', () => { typescriptPropsStatePrefix: 'interface', }), ); + const divWrappedComponents = parseGeneratedSnippets( + buildSnippets({ + ...DEFAULT_GENERATION_SETTINGS, + componentWrapper: 'div', + }), + ); + const componentNamedProps = parseGeneratedSnippets( + buildSnippets({ + ...DEFAULT_GENERATION_SETTINGS, + typescriptPropsNaming: 'component', + }), + ); + const componentNamedInterfaces = parseGeneratedSnippets( + buildSnippets({ + ...DEFAULT_GENERATION_SETTINGS, + typescriptPropsNaming: 'component', + typescriptPropsStatePrefix: 'interface', + }), + ); + const directoryNamedComponents = parseGeneratedSnippets( + buildSnippets({ + ...DEFAULT_GENERATION_SETTINGS, + componentNameSource: 'directoryForIndex', + typescriptPropsNaming: 'component', + }), + ); + const fileName = '${1:${TM_FILENAME_BASE}}'; expect(legacyReact.typescriptReactClassComponent).toBeUndefined(); expect( @@ -265,6 +316,104 @@ 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}
'); + expect( + requireSnippet(componentNamedProps, 'typescriptReactFunctionalComponent') + .body, + ).toContain(`type ${componentName}Props = {}`); + expect( + requireSnippet(componentNamedProps, 'typescriptReactFunctionalComponent') + .body, + ).toContain( + `export default function ${componentName}({}: ${componentName}Props) {`, + ); + expect( + requireSnippet( + componentNamedInterfaces, + 'typescriptReactFunctionalComponent', + ).body, + ).toContain(`interface ${componentName}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('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', () => {