component-gen-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +47 -0
- package/package.json +43 -0
- package/src/commands/generate.js +42 -0
- package/src/generators/angular.js +44 -0
- package/src/generators/index.js +42 -0
- package/src/generators/next.js +41 -0
- package/src/generators/react-like.js +76 -0
- package/src/generators/svelte.js +32 -0
- package/src/generators/vue.js +33 -0
- package/src/index.js +3 -0
- package/src/lib/fs.js +24 -0
- package/src/lib/naming.js +64 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Command, Option } from "commander";
|
|
6
|
+
import { generateComponent } from "../src/commands/generate.js";
|
|
7
|
+
import { FRAMEWORKS } from "../src/generators/index.js";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
11
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name("cg")
|
|
17
|
+
.description("Generate UI components for popular JavaScript frameworks")
|
|
18
|
+
.version(pkg.version ?? "0.0.0");
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command("generate")
|
|
22
|
+
.alias("g")
|
|
23
|
+
.argument("<name>", "Component name (PascalCase recommended, e.g. UserCard)")
|
|
24
|
+
.addOption(
|
|
25
|
+
new Option("-f, --framework <fw>", `Framework (${FRAMEWORKS.join("|")})`)
|
|
26
|
+
.choices([...FRAMEWORKS])
|
|
27
|
+
.makeOptionMandatory(true)
|
|
28
|
+
)
|
|
29
|
+
.option("-o, --out <dir>", "Output directory (default: current directory)", ".")
|
|
30
|
+
.option("--ts, --typescript", "Use TypeScript where applicable", false)
|
|
31
|
+
.option("--scss", "Use SCSS for style files (Angular / optional CSS splits)", false)
|
|
32
|
+
.option("--client", "Next.js: add \"use client\" directive", false)
|
|
33
|
+
.description("Create a component file (or Angular multi-file scaffold)")
|
|
34
|
+
.action(async (name, opts) => {
|
|
35
|
+
try {
|
|
36
|
+
const result = await generateComponent(name, opts);
|
|
37
|
+
console.log(result.message);
|
|
38
|
+
if (result.files?.length) {
|
|
39
|
+
for (const f of result.files) console.log(` ${f}`);
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "component-gen-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Generate framework components for React, Vue, Angular, Svelte, Solid, and Next.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cg": "bin/cli.js",
|
|
8
|
+
"component-gen": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "src/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin/",
|
|
16
|
+
"src/",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "node bin/cli.js",
|
|
21
|
+
"test": "node --test"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"cli",
|
|
25
|
+
"generator",
|
|
26
|
+
"react",
|
|
27
|
+
"vue",
|
|
28
|
+
"angular",
|
|
29
|
+
"svelte",
|
|
30
|
+
"solid",
|
|
31
|
+
"nextjs",
|
|
32
|
+
"scaffold",
|
|
33
|
+
"component"
|
|
34
|
+
],
|
|
35
|
+
"author": "",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"commander": "^13.1.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FRAMEWORKS, buildFiles } from "../generators/index.js";
|
|
2
|
+
import { parseComponentName } from "../lib/naming.js";
|
|
3
|
+
import { writeTree } from "../lib/fs.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{ framework: string; out: string; typescript?: boolean; scss?: boolean; client?: boolean; ts?: boolean }} GenerateOpts
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} name
|
|
11
|
+
* @param {GenerateOpts} opts
|
|
12
|
+
*/
|
|
13
|
+
export async function generateComponent(name, opts) {
|
|
14
|
+
const fw = String(opts.framework ?? "").toLowerCase();
|
|
15
|
+
|
|
16
|
+
const allowed = FRAMEWORKS;
|
|
17
|
+
if (!allowed.includes(fw)) {
|
|
18
|
+
throw new Error(`Unknown framework "${opts.framework}". Use one of: ${allowed.join(", ")}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const identifiers = parseComponentName(name);
|
|
22
|
+
|
|
23
|
+
/** @type {GenerateOpts & { typescript: boolean }} */
|
|
24
|
+
const full = {
|
|
25
|
+
...opts,
|
|
26
|
+
framework: fw,
|
|
27
|
+
typescript: Boolean(opts.typescript ?? opts.ts),
|
|
28
|
+
scss: Boolean(opts.scss),
|
|
29
|
+
client: Boolean(opts.client),
|
|
30
|
+
out: opts.out ?? ".",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const files = buildFiles(fw, identifiers, full);
|
|
34
|
+
const written = await writeTree(full.out, files);
|
|
35
|
+
|
|
36
|
+
const count = written.length;
|
|
37
|
+
const messageCount = `${count === 1 ? "1 file" : `${count} files`}`;
|
|
38
|
+
return {
|
|
39
|
+
message: `${messageCount} wrote for ${fw} component ${identifiers.pascal}.`,
|
|
40
|
+
files: written,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** @typedef {import("../lib/naming.js").Identifiers} Identifiers */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Identifiers} ids
|
|
5
|
+
* @param {{ scss?: boolean }} opts
|
|
6
|
+
*/
|
|
7
|
+
export function angularFiles(ids, opts) {
|
|
8
|
+
const styleExt = opts.scss ? "scss" : "css";
|
|
9
|
+
const folder = ids.kebab;
|
|
10
|
+
const selector = `app-${ids.kebab}`;
|
|
11
|
+
const className = `${ids.pascal}Component`;
|
|
12
|
+
|
|
13
|
+
const tsPath = `${folder}/${ids.kebab}.component.ts`;
|
|
14
|
+
const htmlPath = `${folder}/${ids.kebab}.component.html`;
|
|
15
|
+
const stylePath = `${folder}/${ids.kebab}.component.${styleExt}`;
|
|
16
|
+
|
|
17
|
+
const tsContent = `import { Component } from "@angular/core";
|
|
18
|
+
|
|
19
|
+
@Component({
|
|
20
|
+
selector: "${selector}",
|
|
21
|
+
standalone: true,
|
|
22
|
+
imports: [],
|
|
23
|
+
templateUrl: "./${ids.kebab}.component.html",
|
|
24
|
+
styleUrl: "./${ids.kebab}.component.${styleExt}",
|
|
25
|
+
})
|
|
26
|
+
export class ${className} {}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const htmlContent = `<section class="${ids.kebab}">
|
|
30
|
+
<p>${ids.pascal}</p>
|
|
31
|
+
</section>
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const styleContent = `.${ids.kebab} {
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
[tsPath]: tsContent,
|
|
41
|
+
[htmlPath]: htmlContent,
|
|
42
|
+
[stylePath]: styleContent.trimEnd() + `\n`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** @typedef {{ pascal: string; kebab: string; camel: string; tag: string }} Identifiers */
|
|
2
|
+
|
|
3
|
+
import { angularFiles } from "./angular.js";
|
|
4
|
+
import { nextFiles } from "./next.js";
|
|
5
|
+
import { reactLikeFiles } from "./react-like.js";
|
|
6
|
+
import { svelteFiles } from "./svelte.js";
|
|
7
|
+
import { vueFiles } from "./vue.js";
|
|
8
|
+
|
|
9
|
+
export const FRAMEWORKS = /** @type {const} */ ([
|
|
10
|
+
"react",
|
|
11
|
+
"vue",
|
|
12
|
+
"angular",
|
|
13
|
+
"svelte",
|
|
14
|
+
"solid",
|
|
15
|
+
"next",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {typeof FRAMEWORKS[number]} fw
|
|
20
|
+
* @param {Identifiers} ids
|
|
21
|
+
* @param {{ typescript: boolean; scss?: boolean; client?: boolean }} opts
|
|
22
|
+
*/
|
|
23
|
+
export function buildFiles(fw, ids, opts) {
|
|
24
|
+
switch (fw) {
|
|
25
|
+
case "react":
|
|
26
|
+
return reactLikeFiles(ids, { ...opts, solid: false });
|
|
27
|
+
case "solid":
|
|
28
|
+
return reactLikeFiles(ids, { ...opts, solid: true });
|
|
29
|
+
case "next":
|
|
30
|
+
return nextFiles(ids, opts);
|
|
31
|
+
case "vue":
|
|
32
|
+
return vueFiles(ids, opts);
|
|
33
|
+
case "angular":
|
|
34
|
+
return angularFiles(ids, opts);
|
|
35
|
+
case "svelte":
|
|
36
|
+
return svelteFiles(ids, opts);
|
|
37
|
+
default: {
|
|
38
|
+
const _exhaustive = fw;
|
|
39
|
+
throw new Error(`Unsupported framework: ${_exhaustive}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** @typedef {import("../lib/naming.js").Identifiers} Identifiers */
|
|
2
|
+
|
|
3
|
+
/** @param {boolean} typescript */
|
|
4
|
+
function extTs(typescript) {
|
|
5
|
+
return typescript ? "tsx" : "jsx";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {Identifiers} ids
|
|
10
|
+
* @param {{ typescript: boolean; client?: boolean }} opts
|
|
11
|
+
*/
|
|
12
|
+
export function nextFiles(ids, opts) {
|
|
13
|
+
const ext = extTs(opts.typescript);
|
|
14
|
+
const fname = `${ids.pascal}.${ext}`;
|
|
15
|
+
let head = "";
|
|
16
|
+
|
|
17
|
+
if (opts.client) {
|
|
18
|
+
head = "'use client';\n\n";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (opts.typescript) {
|
|
22
|
+
head += `import type { ReactNode } from "react";
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const nl = `
|
|
27
|
+
`;
|
|
28
|
+
const propsType = opts.typescript
|
|
29
|
+
? `type ${ids.pascal}Props = {
|
|
30
|
+
children?: ReactNode;
|
|
31
|
+
};${nl}${nl}`
|
|
32
|
+
: "";
|
|
33
|
+
|
|
34
|
+
const param = opts.typescript ? `(props: ${ids.pascal}Props)` : "(props)";
|
|
35
|
+
const body = `{${nl} return <div>${ids.pascal}</div>;${nl}}`;
|
|
36
|
+
|
|
37
|
+
const content = `${head}${propsType}export function ${ids.pascal}${param}${body}${nl}${nl}export default ${ids.pascal};
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
return { [fname]: content };
|
|
41
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** @typedef {import("../lib/naming.js").Identifiers} Identifiers */
|
|
2
|
+
|
|
3
|
+
/** @param {boolean} typescript */
|
|
4
|
+
function extTs(typescript) {
|
|
5
|
+
return typescript ? "tsx" : "jsx";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {Identifiers} ids
|
|
10
|
+
* @param {{ typescript: boolean; solid: boolean }} opts
|
|
11
|
+
*/
|
|
12
|
+
export function reactLikeFiles(ids, opts) {
|
|
13
|
+
const ext = extTs(opts.typescript);
|
|
14
|
+
const fname = `${ids.pascal}.${ext}`;
|
|
15
|
+
|
|
16
|
+
if (opts.solid) {
|
|
17
|
+
return { [fname]: solidTemplate(ids, opts) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { [fname]: reactTemplate(ids, opts) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Identifiers} ids
|
|
25
|
+
* @param {{ typescript: boolean }} opts
|
|
26
|
+
*/
|
|
27
|
+
function reactTemplate(ids, opts) {
|
|
28
|
+
const nl = `
|
|
29
|
+
`;
|
|
30
|
+
let head = "";
|
|
31
|
+
|
|
32
|
+
if (opts.typescript) {
|
|
33
|
+
head = `import type { ReactNode } from "react";
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const propsType = opts.typescript
|
|
38
|
+
? `type ${ids.pascal}Props = {
|
|
39
|
+
children?: ReactNode;
|
|
40
|
+
};${nl}${nl}`
|
|
41
|
+
: "";
|
|
42
|
+
|
|
43
|
+
const param = opts.typescript ? `(props: ${ids.pascal}Props)` : "(props)";
|
|
44
|
+
const body = `{${nl} return <div>${ids.pascal}</div>;${nl}}`;
|
|
45
|
+
|
|
46
|
+
return `${head}${propsType}export function ${ids.pascal}${param}${body}${nl}${nl}export default ${ids.pascal};
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {Identifiers} ids
|
|
52
|
+
* @param {{ typescript: boolean }} opts
|
|
53
|
+
*/
|
|
54
|
+
function solidTemplate(ids, opts) {
|
|
55
|
+
const nl = `
|
|
56
|
+
`;
|
|
57
|
+
let head = "";
|
|
58
|
+
|
|
59
|
+
if (opts.typescript) {
|
|
60
|
+
head = `import type { Component } from "solid-js";
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const propsType = opts.typescript ? `export type ${ids.pascal}Props = {};${nl}${nl}` : "";
|
|
65
|
+
|
|
66
|
+
const annotation = opts.typescript
|
|
67
|
+
? `: Component<${ids.pascal}Props>`
|
|
68
|
+
: "";
|
|
69
|
+
const body = `{${nl} return <div>${ids.pascal}</div>;${nl}}`;
|
|
70
|
+
|
|
71
|
+
const exportLine = opts.typescript
|
|
72
|
+
? `${head}${propsType}export const ${ids.pascal}${annotation} = (_props`
|
|
73
|
+
: `${head}export const ${ids.pascal} = (_props`;
|
|
74
|
+
|
|
75
|
+
return `${exportLine})${body}${nl}`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** @typedef {import("../lib/naming.js").Identifiers} Identifiers */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Identifiers} ids
|
|
5
|
+
* @param {{ typescript: boolean }} opts
|
|
6
|
+
*/
|
|
7
|
+
export function svelteFiles(ids, opts) {
|
|
8
|
+
const fname = `${ids.pascal}.svelte`;
|
|
9
|
+
|
|
10
|
+
const scriptTag = opts.typescript ? `<script lang="ts">` : `<script>`;
|
|
11
|
+
const nl = `\n`;
|
|
12
|
+
|
|
13
|
+
const propsLine = opts.typescript
|
|
14
|
+
? ` export let label = "${ids.pascal}"`
|
|
15
|
+
: ` export let label = "${ids.pascal}"`;
|
|
16
|
+
|
|
17
|
+
const content = `${scriptTag}
|
|
18
|
+
${propsLine};
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<div class="${ids.kebab}">
|
|
22
|
+
{label}
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<style>
|
|
26
|
+
.${ids.kebab} {
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
return { [fname]: content };
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** @typedef {import("../lib/naming.js").Identifiers} Identifiers */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Identifiers} ids
|
|
5
|
+
* @param {{ typescript: boolean }} opts
|
|
6
|
+
*/
|
|
7
|
+
export function vueFiles(ids, opts) {
|
|
8
|
+
const fname = `${ids.pascal}.vue`;
|
|
9
|
+
|
|
10
|
+
const scriptOpen = opts.typescript ? '<script setup lang="ts">' : "<script setup>";
|
|
11
|
+
const nl = `\n`;
|
|
12
|
+
|
|
13
|
+
const content = `<template>
|
|
14
|
+
<div class="${ids.kebab}">
|
|
15
|
+
{{ message }}
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
${scriptOpen}
|
|
20
|
+
import { ref } from "vue";
|
|
21
|
+
|
|
22
|
+
const message = ref("${ids.pascal}");
|
|
23
|
+
${opts.typescript ? `// define typed props here${nl}` : ""}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<style scoped>
|
|
27
|
+
.${ids.kebab} {
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
return { [fname]: content };
|
|
33
|
+
}
|
package/src/index.js
ADDED
package/src/lib/fs.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} cwd
|
|
7
|
+
* @param {Record<string, string>} files Relative path → content
|
|
8
|
+
*/
|
|
9
|
+
export async function writeTree(cwd, files) {
|
|
10
|
+
const base = resolve(cwd);
|
|
11
|
+
const written = [];
|
|
12
|
+
|
|
13
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
14
|
+
const absolute = resolve(base, relPath);
|
|
15
|
+
if (!absolute.startsWith(base)) {
|
|
16
|
+
throw new Error(`Refused to write outside output directory: ${relPath}`);
|
|
17
|
+
}
|
|
18
|
+
await mkdir(dirname(absolute), { recursive: true });
|
|
19
|
+
await writeFile(absolute, content.endsWith("\n") ? content : `${content}\n`, "utf8");
|
|
20
|
+
written.push(absolute);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return written;
|
|
24
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** @param {string} raw User input component name */
|
|
2
|
+
|
|
3
|
+
export function parseComponentName(raw) {
|
|
4
|
+
const trimmed = String(raw ?? "").trim();
|
|
5
|
+
if (!trimmed) throw new Error("Component name is required.");
|
|
6
|
+
|
|
7
|
+
const pascal = toPascalCase(trimmed);
|
|
8
|
+
if (/^\d/.test(pascal)) {
|
|
9
|
+
throw new Error("Component name cannot start with a digit.");
|
|
10
|
+
}
|
|
11
|
+
const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
12
|
+
const kebab = toKebabCase(pascal);
|
|
13
|
+
|
|
14
|
+
return { pascal, kebab, camel, tag: kebab };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** PascalCase-ish → kebab-case */
|
|
18
|
+
/** @param {string} input */
|
|
19
|
+
export function toKebabCase(input) {
|
|
20
|
+
return input
|
|
21
|
+
.replace(/([a-z\d])([A-Z])/g, "$1-$2")
|
|
22
|
+
.replace(/_/g, "-")
|
|
23
|
+
.toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @param {string} one Alphanumeric word possibly camelCase/PascalCase */
|
|
27
|
+
function splitWords(one) {
|
|
28
|
+
return one
|
|
29
|
+
|
|
30
|
+
.replace(/([a-z\d])([A-Z])/g, "$1 $2")
|
|
31
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
32
|
+
.split(/[^\w$]+/)
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {string} input */
|
|
37
|
+
export function toPascalCase(input) {
|
|
38
|
+
const raw = String(input).trim().replace(/^[^\w$]*|[^\w$]*$/g, "");
|
|
39
|
+
if (!raw) throw new Error(`Invalid component name: "${input}"`);
|
|
40
|
+
|
|
41
|
+
const segments = raw.split(/[^a-zA-Z0-9$]+/).filter(Boolean);
|
|
42
|
+
|
|
43
|
+
const words =
|
|
44
|
+
segments.length === 1
|
|
45
|
+
? splitWords(segments[0])
|
|
46
|
+
|
|
47
|
+
: segments.flatMap(splitWords).filter(Boolean);
|
|
48
|
+
|
|
49
|
+
if (!words.length) throw new Error(`Invalid component name: "${input}"`);
|
|
50
|
+
|
|
51
|
+
const pascal = words.map(capitalizeSegment).join("");
|
|
52
|
+
|
|
53
|
+
return pascal;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @param {string} w */
|
|
57
|
+
function capitalizeSegment(w) {
|
|
58
|
+
const head = w.charAt(0).toUpperCase();
|
|
59
|
+
const tail = w.slice(1).toLowerCase();
|
|
60
|
+
const result = `${head}${tail}`;
|
|
61
|
+
const looksLikeAcronym = /^[A-Z]{2,}\d*$/.test(w);
|
|
62
|
+
|
|
63
|
+
return looksLikeAcronym ? w : result;
|
|
64
|
+
}
|