diff --git a/tools/create-lib/src/generate.ts b/tools/create-lib/src/generate.ts index 04e5ef8..9dbbc9f 100644 --- a/tools/create-lib/src/generate.ts +++ b/tools/create-lib/src/generate.ts @@ -7,22 +7,21 @@ import { fileURLToPath } from "node:url" import type { GenerateFunction } from "./types/GenerateFunction" import type { CreateLibOptions } from "./types/CreateLibOptions" import { argv } from "node:process" +import { processArguments, ArgumentError } from "./utils/args" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) -class ArgumentError extends Error {} await main() interface FileContent { fileName: string content: string - subFolder?: string } async function main() { try { - const options = getOptionsFromArgs() + const options = processArguments(argv) const templates = await getTemplatesFromFolder(options) createLibFromTemplateContent(templates, options) @@ -35,42 +34,8 @@ async function main() { } } -function getOptionsWithDefaults(libName: string) { - return { - libName, - packageName: `@libs/${toKebabCase(libName)}`, - mainFileName: "src/exports.ts", - templateFolder: "template", - isPrivate: true, - version: "0.0.0", - libFolderName: toKebabCase(libName), - libFunctionName: toLowerCamelCase(libName), - } satisfies CreateLibOptions -} - -function getOptionsFromArgs(): Required { - const args = argv.slice(2) - if (args.length === 0) { - throw new ArgumentError("No arguments provided. Expected: [description] [version] [public|private]") - } - const libName = args[0] - const description = args[1] - const version = args[2] - const isPrivate = args[3] !== "public" - - if (version && !/[^\d+.\d+.\d+$]/.test(version)) { - throw new ArgumentError("Version (argument 3) must be in the format of x.x.x") - } - - return { - ...getOptionsWithDefaults(libName), - description, - version, - isPrivate, - } -} - type Formatter = (options: CreateLibOptions, filePath: URL) => Promise + async function getTemplatesFromFolder( options: CreateLibOptions, folderName?: string, @@ -95,9 +60,10 @@ async function getTemplatesFromFolder( if (await isFileDirectory(filePath)) { return await getTemplatesFromFolder(options, fileLocation, depth + 1) } + const templatedFile = await generateTemplateFromFile(options, formatters, modulePath, fileLocation) + return { - ...(await generateTemplateFromFile(options, formatters, modulePath, fileLocation)), - subFolder: folderName, + ...templatedFile, depth, } }), @@ -117,25 +83,26 @@ async function isFileDirectory(filePath: string): Promise { return stat.isDirectory() } +function matchesExtension(fileName: string, extension: string | RegExp): string | null { + if (extension instanceof RegExp) { + const match = fileName.match(extension) + if (match) { + return match[0] + } + } else if (fileName.endsWith(extension)) { + return extension + } + return null +} + async function generateTemplateFromFile( options: CreateLibOptions, formatters: [string | RegExp, Formatter][], modulePath: URL, fileName: string, ): Promise { - const matchesExtension = (extension: string | RegExp) => { - if (extension instanceof RegExp) { - const match = fileName.match(extension) - if (match) { - return match[0] - } - } else if (fileName.endsWith(extension)) { - return extension - } - return null - } for (const [extension, formatter] of formatters) { - const extensionMatch = matchesExtension(extension) + const extensionMatch = matchesExtension(fileName, extension) if (extensionMatch) { return { fileName: formatFileName(fileName, options, extensionMatch), @@ -177,6 +144,11 @@ function createLibFromTemplateContent(content: FileContent[], options: Required< for (const { fileName, content: fileContent } of content) { const filePath = path.join(getWorkspaceRoot(), "libs", options.libFolderName, fileName) const folder = path.dirname(filePath) + if (options.isDryRun) { + console.log(`Dry run: Create folder ${folder}`) + console.log(`Dry run: Create file ${filePath}`) + continue + } fs.mkdirSync(folder, { recursive: true }) fs.writeFileSync(filePath, fileContent) } @@ -185,16 +157,3 @@ function createLibFromTemplateContent(content: FileContent[], options: Required< function getWorkspaceRoot() { return path.join(__dirname, "..", "..", "..") } - -function toKebabCase(string: string): string { - return string - .replace(/[\s_]+/g, "-") - .toLowerCase() - .replace(/[^a-z0-9-]/g, "") -} - -function toLowerCamelCase(string: string): string { - return string - .replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => (index === 0 ? letter.toLowerCase() : letter.toUpperCase())) - .replace(/[^a-zA-Z0-9_]/g, "") -} diff --git a/tools/create-lib/src/template/package.json.ts b/tools/create-lib/src/template/package.json.ts index 6f94744..d8bb3f0 100644 --- a/tools/create-lib/src/template/package.json.ts +++ b/tools/create-lib/src/template/package.json.ts @@ -1,19 +1,13 @@ import { JSONSchemaForNPMPackageJsonFiles as PackageJson } from "@schemastore/package" import { CreateLibOptions } from "../types/CreateLibOptions" -export default function generate({ - packageName, - description, - version = "0.0.0", - isPrivate = true, - mainFileName = "src/exports.ts", -}: CreateLibOptions) { +export default function generate(options: Required) { const packageJson: PackageJson = { - name: packageName, - version: version || "0.0.0", - description: description, - private: isPrivate, - main: mainFileName ?? "src/exports.ts", + name: options.packageName, + version: options.version, + description: options.description, + private: options.isPrivate, + main: `./src/${options.mainFileName}`, } return JSON.stringify(packageJson, null, 2) diff --git a/tools/create-lib/src/template/src/__mainFileName__.ts b/tools/create-lib/src/template/src/__mainFileName__.ts index 4aa8c62..5ebf2ad 100644 --- a/tools/create-lib/src/template/src/__mainFileName__.ts +++ b/tools/create-lib/src/template/src/__mainFileName__.ts @@ -1,5 +1,5 @@ import { CreateLibOptions } from "../../types/CreateLibOptions" -export default function generate(_options: CreateLibOptions) { - return 'export * from "./lib/hello"' +export default function generate(options: CreateLibOptions) { + return `export * from "./lib/${options.libFolderName}"\n` } diff --git a/tools/create-lib/src/types/CreateLibOptions.d.ts b/tools/create-lib/src/types/CreateLibOptions.d.ts index e350340..d3c53ec 100644 --- a/tools/create-lib/src/types/CreateLibOptions.d.ts +++ b/tools/create-lib/src/types/CreateLibOptions.d.ts @@ -14,7 +14,7 @@ export interface CreateLibOptions { */ isPrivate?: boolean /** - * @default "src/exports.ts" + * @default "exports.ts" */ mainFileName?: string /** @@ -30,4 +30,5 @@ export interface CreateLibOptions { * By default, this will format the libName to lowerCamelCase. */ libFunctionName?: string + isDryRun?: boolean } diff --git a/tools/create-lib/src/utils/args.ts b/tools/create-lib/src/utils/args.ts new file mode 100644 index 0000000..25175d1 --- /dev/null +++ b/tools/create-lib/src/utils/args.ts @@ -0,0 +1,96 @@ +import { CreateLibOptions } from "../types/CreateLibOptions" +import { toKebabCase, toLowerCamelCase } from "./strings" + +export class ArgumentError extends Error {} + +const USAGE = `Usage: create-lib [description] [--version=] [--public] [--dry-run]` +// thanks copilot, this would have been a pain to write +const HELP_DOCS = ` +${USAGE} + + libName: + The name of the library to create. This will be used to create the + package name, folder name, and function name. Required. + + description: + The description of the library. This will be used in the + package.json file. Optional. + + --version= + The version of the library. This will be used in the package.json + file. Must be in the format of x.x.x + + --public + If provided, the library will be public. Otherwise, it will be + private. + + --dry-run + If provided, the library will not be created. Instead, the output + will be logged to the console. + + --help + If provided as the only argument, this message will be displayed. +` + +export function processArguments(argv: ReadonlyArray): Required { + const args = argv.slice(2) + if (args[0] === "--help") { + console.log(HELP_DOCS) + process.exit(0) + } + if (args.length === 0) { + throw new ArgumentError(`No arguments provided. Run with --help for more information.\n\n${USAGE}`) + } + const version = getArgsValue(args, "--version") + const isPrivate = getArgsFlag(args, "--public") + const isDryRun = getArgsFlag(args, "--dry-run") + const libName = args[0] + const description = args[1] ?? "" + if (args.length > 2 || libName.startsWith("--") || description.startsWith("--")) { + throw new ArgumentError(`Unexpected arguments provided. Run with --help for more information.\n\n${USAGE}`) + } + + if (version && !/[^\d+.\d+.\d+$]/.test(version)) { + throw new ArgumentError("Version (argument 3) must be in the format of x.x.x") + } + + return { + libName, + description, + packageName: `@libs/${toKebabCase(libName)}`, + mainFileName: "exports.ts", + templateFolder: "template", + isPrivate: isPrivate ?? true, + version: version || "0.0.0", + libFolderName: toKebabCase(libName), + libFunctionName: toLowerCamelCase(libName), + isDryRun: isDryRun, + } +} + +export function getArgsFlag(args: string[], flag: string): boolean { + const index = args.indexOf(flag) + if (index === -1) { + return false + } + args.splice(index, 1) + return true +} + +export function getArgsValue(args: string[], flag: string): string | undefined { + const index = args.findIndex((arg) => arg.startsWith(flag)) + if (index === -1) { + return undefined + } + const arg = args[index].split("=") + if (arg.length === 2) { + args.splice(index, 1) + return arg[1] + } + const value = args[index + 1] + if (!value) { + throw new ArgumentError(`No value provided for ${flag}`) + } + args.splice(index, 2) + return value +} diff --git a/tools/create-lib/src/utils/strings.ts b/tools/create-lib/src/utils/strings.ts new file mode 100644 index 0000000..b097b09 --- /dev/null +++ b/tools/create-lib/src/utils/strings.ts @@ -0,0 +1,12 @@ +export function toKebabCase(string: string): string { + return string + .replace(/[\s_]+/g, "-") + .toLowerCase() + .replace(/[^a-z0-9-]/g, "") +} + +export function toLowerCamelCase(string: string): string { + return string + .replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => (index === 0 ? letter.toLowerCase() : letter.toUpperCase())) + .replace(/[^a-zA-Z0-9_]/g, "") +}