diff --git a/package-lock.json b/package-lock.json index 09d2901..6698cd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@types/node": "^20.6.5", "bem-neon": "^1.0.0", "meow": "^12.1.1", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", "typescript": "^5.2.2" }, "devDependencies": { @@ -132,6 +134,23 @@ "node": ">=0.3.1" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -149,6 +168,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -192,6 +238,11 @@ } } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", diff --git a/package.json b/package.json index 541ad9c..389722f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "1.0.0", "description": "Parses a BEM file into a TypeScript type.", "main": "dist/index.js", - "type": "module", "scripts": { "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" @@ -26,6 +25,8 @@ "@types/node": "^20.6.5", "bem-neon": "^1.0.0", "meow": "^12.1.1", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", "typescript": "^5.2.2" }, "devDependencies": { diff --git a/src/BEMBlock.ts b/src/BEMBlock.ts index a290232..5ed1312 100644 --- a/src/BEMBlock.ts +++ b/src/BEMBlock.ts @@ -1,9 +1,157 @@ -export type BEMBlock< - TName extends string, - TElements extends Record, - TModifiers extends string | undefined -> = { - name: TName - elements: TElements - modifiers: TModifiers +import { parseBEM } from 'bem-neon' +import ts, { factory } from 'typescript' +import { pascalCase } from 'pascal-case' +import { paramCase } from 'param-case' +import { EOL } from 'node:os' + +export type ParseOptions = {} + +export class BEMBlock { + #block: ReturnType + + public constructor(bem: string, _options: ParseOptions = {}) { + this.#block = parseBEM(bem) + } + + public get name() { + return paramCase(this.#block.name) + } + + public set name(value) { + this.#block.name = value + } + + public get elements() { + return structuredClone(this.#block.elements) + } + + public set elements(value) { + this.#block.elements = value + } + + public get modifiers() { + return structuredClone(this.#block.modifiers) + } + + public set modifiers(value) { + this.#block.modifiers = value + } + + /** + * @returns type AST generated by the TypeScript Compiler API. + */ + public toTypeAST() { + return factory.createTypeAliasDeclaration( + [factory.createToken(ts.SyntaxKind.ExportKeyword)], + factory.createIdentifier(pascalCase(`${this.#block.name}Block`)), + undefined, + factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier('name'), + undefined, + factory.createLiteralTypeNode(factory.createStringLiteral(this.name)) + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier('elements'), + undefined, + factory.createTypeLiteralNode( + this.#block.elements.map((element) => + factory.createPropertySignature( + undefined, + factory.createIdentifier(paramCase(element.name)), + undefined, + element.modifiers.length + ? factory.createUnionTypeNode( + element.modifiers.map((modifier) => + factory.createLiteralTypeNode( + factory.createStringLiteral(paramCase(modifier)) + ) + ) + ) + : factory.createKeywordTypeNode( + ts.SyntaxKind.UndefinedKeyword + ) + ) + ) + ) + ), + factory.createPropertySignature( + undefined, + factory.createIdentifier('modifiers'), + undefined, + this.#block.modifiers.length + ? factory.createUnionTypeNode( + this.#block.modifiers.map((modifier) => + factory.createLiteralTypeNode( + factory.createStringLiteral(paramCase(modifier)) + ) + ) + ) + : factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword) + ) + ]) + ) + } + + /** + * @returns the TypeScript type that represents the BEM structure. + * @example + * fooBlock.toType() + * `export const FooBlock = { + * name: 'foo' + * elements: { + * qux: undefined + * } + * modifiers: 'bar' | 'baz' + * }` + */ + public toType(printerOptions?: ts.PrinterOptions) { + const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + omitTrailingSemicolon: true, + ...printerOptions + }) + + return printer.printNode( + ts.EmitHint.Unspecified, + this.toTypeAST(), + ts.createSourceFile( + 'block.ts', + '', + ts.ScriptTarget.Latest, + false, + ts.ScriptKind.TS + ) + ) + } + + /** + * @param eol defualts to `os.EOL` + * @returns the raw BEM file format string. + * @example fooBlock.toString() // foo[bar,baz]\nqux + */ + public toString(eol = EOL) { + let result = this.name + if (this.modifiers.length) { + result += `[${this.modifiers.join(',')}]` + } + for (const element of this.elements) { + result += eol + element.name + if (element.modifiers.length) { + result += `[${element.modifiers.join(',')}]` + } + } + result += eol + return result + } + + public valueOf() { + return structuredClone(this.#block) + } + + public toJSON() { + return this.valueOf() + } } diff --git a/src/api.ts b/src/api.ts index d460b9a..8e4ab6a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,103 +1 @@ -import { parseBEM } from 'bem-neon' -import type { BEMBlock } from './BEMBlock.js' -import ts, { factory } from 'typescript' - -export type ParseOptions = {} - -export function parse( - bem: string, - _options: ParseOptions = {} -): BEMBlock, string | undefined> { - const block = parseBEM(bem) - const blockType = factory.createTypeAliasDeclaration( - [factory.createToken(ts.SyntaxKind.ExportKeyword)], - factory.createIdentifier(`${block.name}Block`), - undefined, - factory.createTypeLiteralNode([ - factory.createPropertySignature( - undefined, - factory.createIdentifier('name'), - undefined, - factory.createLiteralTypeNode(factory.createStringLiteral(block.name)) - ), - factory.createPropertySignature( - undefined, - factory.createIdentifier('elements'), - undefined, - factory.createTypeLiteralNode( - block.elements.map((element) => - factory.createPropertySignature( - undefined, - factory.createIdentifier(element.name), - undefined, - element.modifiers.length - ? factory.createUnionTypeNode( - element.modifiers.map((modifier) => - factory.createLiteralTypeNode( - factory.createStringLiteral(modifier) - ) - ) - ) - : factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword) - ) - ) - ) - ), - factory.createPropertySignature( - undefined, - factory.createIdentifier('modifiers'), - undefined, - block.modifiers.length - ? factory.createUnionTypeNode( - block.modifiers.map((modifier) => - factory.createLiteralTypeNode( - factory.createStringLiteral(modifier) - ) - ) - ) - : factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword) - ) - ]) - ) - - // // Create a source file - // const sourceFile = factory.createSourceFile( - // 'foo.ts', - // '', - // ts.ScriptTarget.Latest, - // false, - // ts.ScriptKind.TS - // ) - - // // // Add the type alias declaration to the source file - // const updatedSourceFile = ts.updateSourceFile(sourceFile, [blockType]) - - // Create a printer to output the TypeScript code - const printer = ts.createPrinter({ - newLine: ts.NewLineKind.LineFeed - }) - - const result = printer.printNode( - ts.EmitHint.Unspecified, - blockType, - ts.createSourceFile( - 'block.ts', - '', - ts.ScriptTarget.Latest, - false, - ts.ScriptKind.TS - ) - ) - - // // Print the type alias declaration - // const result = printer.printNode( - // ts.EmitHint.Unspecified, - // blockType, - // updatedSourceFile - // ) - - // Output the result - console.log(`export ${result}`) - - return { name: block.name, elements: {}, modifiers: '' } -} +export * from './BEMBlock.js' diff --git a/src/cli.ts b/src/cli.ts index fc3b7ad..d1c90e0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,27 +1,27 @@ -import meow from 'meow' -import { parse } from './api.js' +// import meow from 'meow' +// import { parse } from './api.js' -const cli = meow( - ` - Usage - $ bem - - Options - --rainbow, -r Include a rainbow - - Examples - $ bem unicorns --rainbow - 🌈 unicorns 🌈 -`, - { - importMeta: import.meta, - flags: { - rainbow: { - type: 'boolean', - shortFlag: 'r' - } - } - } -) +// const cli = meow( +// ` +// Usage +// $ bem -parse(cli.input.at(0) ?? '', cli.flags) +// Options +// --rainbow, -r Include a rainbow + +// Examples +// $ bem unicorns --rainbow +// 🌈 unicorns 🌈 +// `, +// { +// importMeta: import.meta, +// flags: { +// rainbow: { +// type: 'boolean', +// shortFlag: 'r' +// } +// } +// } +// ) + +// parse(cli.input.at(0) ?? '', cli.flags)