Skip to content

Commit

Permalink
lib generator fixes & documentation (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
hiddenist authored Nov 25, 2023
1 parent 216f588 commit fb74b4f
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 80 deletions.
89 changes: 24 additions & 65 deletions tools/create-lib/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<CreateLibOptions> {
const args = argv.slice(2)
if (args.length === 0) {
throw new ArgumentError("No arguments provided. Expected: <libName> [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<string>

async function getTemplatesFromFolder(
options: CreateLibOptions,
folderName?: string,
Expand All @@ -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,
}
}),
Expand All @@ -117,25 +83,26 @@ async function isFileDirectory(filePath: string): Promise<boolean> {
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<FileContent> {
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),
Expand Down Expand Up @@ -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)
}
Expand All @@ -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, "")
}
18 changes: 6 additions & 12 deletions tools/create-lib/src/template/package.json.ts
Original file line number Diff line number Diff line change
@@ -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<CreateLibOptions>) {
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)
Expand Down
4 changes: 2 additions & 2 deletions tools/create-lib/src/template/src/__mainFileName__.ts
Original file line number Diff line number Diff line change
@@ -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`
}
3 changes: 2 additions & 1 deletion tools/create-lib/src/types/CreateLibOptions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface CreateLibOptions {
*/
isPrivate?: boolean
/**
* @default "src/exports.ts"
* @default "exports.ts"
*/
mainFileName?: string
/**
Expand All @@ -30,4 +30,5 @@ export interface CreateLibOptions {
* By default, this will format the libName to lowerCamelCase.
*/
libFunctionName?: string
isDryRun?: boolean
}
96 changes: 96 additions & 0 deletions tools/create-lib/src/utils/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { CreateLibOptions } from "../types/CreateLibOptions"
import { toKebabCase, toLowerCamelCase } from "./strings"

export class ArgumentError extends Error {}

const USAGE = `Usage: create-lib <libName> [description] [--version=<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=<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<string>): Required<CreateLibOptions> {
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
}
12 changes: 12 additions & 0 deletions tools/create-lib/src/utils/strings.ts
Original file line number Diff line number Diff line change
@@ -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, "")
}

0 comments on commit fb74b4f

Please sign in to comment.