From 9a4f50ea701360e307e6ca98e836fa2a97b13a9b Mon Sep 17 00:00:00 2001 From: Curve Date: Thu, 9 Nov 2023 13:04:50 +0100 Subject: [PATCH] refactor: project structure --- .github/workflows/publish.yml | 2 +- .gitignore | 2 +- package.json | 11 ++- pnpm-lock.yaml | 14 +++ src/commands/embed.tsx | 145 +++++++++++++++++++++++++++++++ src/components/error.tsx | 17 ++++ src/components/line.tsx | 31 +++++++ src/components/table.tsx | 57 +++++++----- src/embed.tsx | 157 ---------------------------------- src/file.ts | 2 +- src/index.ts | 2 +- src/templates/all.eta | 4 +- src/theme/index.ts | 6 ++ src/utils/colors.ts | 3 - tsconfig.json | 21 +++-- 15 files changed, 279 insertions(+), 195 deletions(-) create mode 100644 src/commands/embed.tsx create mode 100644 src/components/error.tsx create mode 100644 src/components/line.tsx delete mode 100644 src/embed.tsx create mode 100644 src/theme/index.ts delete mode 100644 src/utils/colors.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5c0ebb2..f6a0a01 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,7 +27,7 @@ jobs: run_install: false - name: 🔨 Build - run: "pnpm install && pnpm run build:executable" + run: "pnpm install && pnpm run bundle:executable" - name: 🛒 Publish run: pnpm publish --no-git-checks diff --git a/.gitignore b/.gitignore index f06235c..dd87e2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ node_modules -dist +build diff --git a/package.json b/package.json index 038e610..5721f1a 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "keywords": [ "saucer" ], - "version": "4.1.1", + "version": "4.2.0", "license": "MIT", "author": "Curve (https://github.com/Curve)", "type": "module", @@ -17,13 +17,15 @@ }, "scripts": { "build": "tsc", - "build:executable": "pnpm run build && chmod +x dist/index.js && cp -r src/templates dist" + "copy": "cp -r src/templates build", + "bundle": "pnpm run build && pnpm run copy", + "bundle:executable": "pnpm run bundle && chmod +x build/index.js" }, "bin": { - "saucer": "dist/index.js" + "saucer": "build/index.js" }, "files": [ - "dist" + "build" ], "dependencies": { "commander": "^11.0.0", @@ -41,6 +43,7 @@ "devDependencies": { "@sindresorhus/tsconfig": "^3.0.1", "@types/fs-extra": "^11.0.2", + "@types/ink-spinner": "^3.0.4", "@types/mime-types": "^2.1.1", "@types/node": "^20.6.1", "@types/react": "^18.0.32", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d756db..d881af3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,9 @@ devDependencies: '@types/fs-extra': specifier: ^11.0.2 version: 11.0.3 + '@types/ink-spinner': + specifier: ^3.0.4 + version: 3.0.4 '@types/mime-types': specifier: ^2.1.1 version: 2.1.3 @@ -158,6 +161,10 @@ packages: engines: {node: '>=14'} dev: true + /@types/cli-spinners@1.3.3: + resolution: {integrity: sha512-B5/ya7/tb6zH2sFza0WYMuuIP3klS94+bkGAJ4ISXstmIaesoG1PZ3glNllmPUx94Oh4kguzgoZucEBqBUds6w==} + dev: true + /@types/eslint-utils@3.0.5: resolution: {integrity: sha512-dGOLJqHXpjomkPgZiC7vnVSJtFIOM1Y6L01EyUhzPuD0y0wfIGiqxiGs3buUBfzxLIQHrCvZsIMDaCZ8R5IIoA==} dependencies: @@ -183,6 +190,13 @@ packages: '@types/node': 20.8.7 dev: true + /@types/ink-spinner@3.0.4: + resolution: {integrity: sha512-R9RFxdeYGCSN87zi79E8ZorHJdsSzrAnJ6Rn3wYATvTLYIEwejTQ7OLvgSPsstI2/+DoXXbfTUWlp3HRo2tZTw==} + dependencies: + '@types/cli-spinners': 1.3.3 + '@types/react': 18.2.31 + dev: true + /@types/json-schema@7.0.14: resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} diff --git a/src/commands/embed.tsx b/src/commands/embed.tsx new file mode 100644 index 0000000..3d54356 --- /dev/null +++ b/src/commands/embed.tsx @@ -0,0 +1,145 @@ +import { Eta } from "eta"; +import figureSet from "figures"; +import { existsSync, lstatSync, statSync } from "fs"; +import { outputFileSync } from "fs-extra/esm"; +import { mkdir } from "fs/promises"; +import { Newline, Text, render } from "ink"; +import Spinner from "ink-spinner"; +import { fromPromise, fromThrowable } from "neverthrow"; +import path, { dirname, resolve } from "path"; +import { Fragment, ReactNode } from "react"; +import { Error } from "../components/error.js"; +import { Table } from "../components/table.js"; +import { File, parse } from "../file.js"; +import theme from "../theme/index.js"; +import { recursiveDirectoryIterator } from "../utils/fs.js"; +import { fileURLToPath } from "url"; +import { Line } from "../components/line.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export async function embed(source: string, destination: string) +{ + const errors: ReactNode[] = []; + const { unmount, rerender } = render(); + + const eta = new Eta({ views: path.join(__dirname, "..", "templates") }); + const writeFile = fromThrowable(outputFileSync, error => (error as string)); + + if (!existsSync(destination)) + { + const result = await fromPromise(mkdir(destination, { recursive: true }), error => (error as string)); + + if (result.isErr()) + { + rerender( + + ); + + unmount(1); + return; + } + } + + if (!existsSync(source) || !lstatSync(source).isDirectory()) + { + rerender( + + ); + + unmount(1); + return; + } + + const files: [string, File][] = []; + + for await (const { absolute, relative } of recursiveDirectoryIterator(source)) + { + const file = parse(absolute, relative); + + if (file.isErr()) + { + errors.push( + + ); + + continue; + } + + rerender( + <> + {...errors} + + + + } + text={["Embedding ", [relative, "greenBright"]]} + /> + + ); + + const target = resolve(destination, `${relative}.hpp`); + const result = writeFile(target, eta.render("embed", { name: file.value.name, data: file.value.data, size: file.value.size })); + + if (result.isErr()) + { + errors.push( + + ); + + continue; + } + + files.push([target, file.value]); + } + + const result = writeFile(resolve(destination, "all.hpp"), eta.render("all", { files: files.map(x => x[1]) })); + + if (result.isErr()) + { + errors.push( + + ); + + unmount(1); + } + + const table_data = files.map(([path, file]) => ({ + File : file.path, + Mime : file.mime, + Header : path, + "Size (KB)": (statSync(path).size / 1024).toFixed(1), + })); + + rerender( + <> + + + {...errors} + {figureSet.tick}} + text={["Embedded ", [files.length.toString(), theme.colors.purple], " file(s)\n"]} + /> + + ); + + unmount(0); +} diff --git a/src/components/error.tsx b/src/components/error.tsx new file mode 100644 index 0000000..9d99073 --- /dev/null +++ b/src/components/error.tsx @@ -0,0 +1,17 @@ +import figureSet from "figures"; +import { Text } from "ink"; +import { ColoredText, Line } from "./line.js"; + +interface ErrorProps +{ + error?: string | ColoredText[]; + description: string | ColoredText[]; +} + +export function Error({ description, error }: ErrorProps) +{ + return {figureSet.cross}} + text={[...description, ...(error ? [": ", ...error] : [])]} + />; +} diff --git a/src/components/line.tsx b/src/components/line.tsx new file mode 100644 index 0000000..ae98f07 --- /dev/null +++ b/src/components/line.tsx @@ -0,0 +1,31 @@ +import { Text, TextProps } from "ink"; + +export type ColoredText = string | [text: string, color: TextProps["color"]]; + +interface LineProps +{ + icon: React.JSX.Element; + text: string | ColoredText[]; +} + +function convert(item: ColoredText) +{ + if (typeof item === "string") + { + return {item}; + } + + return {item[0]}; +} + +export function Line({ icon, text }: LineProps) +{ + return + {icon} + {" "} + { + Array.isArray(text) ? + text.map(convert) : convert(text) + } + ; +} diff --git a/src/components/table.tsx b/src/components/table.tsx index e87cfa8..eeeb55c 100644 --- a/src/components/table.tsx +++ b/src/components/table.tsx @@ -1,14 +1,21 @@ import figureSet from "figures"; import { Box, BoxProps, Text, TextProps } from "ink"; import { ReactNode } from "react"; -import colors from "../utils/colors.js"; +import theme from "../theme/index.js"; function unique(array: T[]) { return [...new Set(array)]; } -export function Table({ data, distribution, color, ...props }: { data: any[], distribution: number[], color?: TextProps["color"][] } & BoxProps) +interface TableProps extends BoxProps +{ + data: any[]; + distribution: number[]; + colors?: TextProps["color"][]; +} + +export function Table({ data, distribution, colors, ...props }: TableProps) { const headers = unique(data.map(item => Object.keys(item)).flat()); const width = (index: number) => `${distribution[index]}%`; @@ -48,34 +55,38 @@ export function Table({ data, distribution, color, ...props }: { data: any[], di { const rtn = { ...style, borderBottom: false }; - if (headers.length <= 1) - { - return rtn; - } - - switch (index) + if (headers.length > 1 && index < headers.length - 1) { - default: - case 0: rtn.borderRight = true; - break; - case (headers.length - 1): - break; } return rtn; }; const Cell = ({ index, children }: { index: number, children: ReactNode}) => - + {children} ; - return + return {headers.map((header, index) => - - + + {header} @@ -83,10 +94,16 @@ export function Table({ data, distribution, color, ...props }: { data: any[], di {data.map((row, key) => - + {headers.map((header, index) => - - + + {row[header]} diff --git a/src/embed.tsx b/src/embed.tsx deleted file mode 100644 index f792762..0000000 --- a/src/embed.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { Eta } from "eta"; -import figureSet from "figures"; -import { existsSync, lstatSync, statSync } from "fs"; -import { outputFileSync } from "fs-extra/esm"; -import { mkdir } from "fs/promises"; -import { Newline, Text, render } from "ink"; -import Spinner from "ink-spinner"; -import { Result } from "neverthrow"; -import path, { resolve } from "path"; -import { Fragment, ReactNode } from "react"; -import { Table } from "./components/table.js"; -import { File, parse } from "./file.js"; -import colors from "./utils/colors.js"; -import { recursiveDirectoryIterator } from "./utils/fs.js"; - -export async function embed(source: string, destination: string) -{ - const writeFile = Result.fromThrowable(outputFileSync); - const eta = new Eta({ views: path.join(new URL(path.dirname(import.meta.url)).pathname, "templates") }); - - const { unmount, rerender } = render(); - const errors: ReactNode[] = []; - - if (!existsSync(destination)) - { - try - { - await mkdir(destination, { recursive: true }); - } - catch (error) - { - rerender( - - {figureSet.cross} - {" Failed to create destination ("} - {destination} - {`): ${error}`} - - ); - - unmount(1); - return; - } - } - - if (!existsSync(source) || !lstatSync(source).isDirectory()) - { - rerender( - - {figureSet.cross} - {" Expected "} - {source} - {" to be directory"} - - ); - - unmount(1); - return; - } - - const files: [string, File][] = []; - - for await (const { absolute, relative } of recursiveDirectoryIterator(source)) - { - const file = parse(absolute, relative); - - if (file.isErr()) - { - errors.push( - - {figureSet.cross} - {" Failed to embed "} - {relative} - {`: ${file.error}`} - - ); - - continue; - } - - rerender( - <> - {...errors} - - - - - {" Embedding "} - - {relative} - - - - ); - - const target = resolve(destination, `${relative}.hpp`); - const result = writeFile(target, eta.render("embed", { name: file.value.name, data: file.value.data, size: file.value.size })); - - if (result.isErr()) - { - errors.push( - - {figureSet.cross} - {" Failed to write "} - {target} - {`: ${result.error}`} - - ); - - continue; - } - - files.push([target, file.value]); - } - - const result = writeFile(resolve(destination, "all.hpp"), eta.render("all", { files: files.map(x => x[1]) })); - - if (result.isErr()) - { - errors.push( - - {figureSet.cross} - {" Failed to write "} - all.hpp - {`: ${result.error}`} - - ); - - unmount(1); - } - - const table_data = files.map(([path, file]) => - { - return { - File : file.path, - Mime : file.mime, - Header : path, - "Size (KB)": (statSync(path).size / 1024).toFixed(1), - }; - }); - - rerender( - <> -
- - {...errors} - - {figureSet.tick} - {" Embedded "} - {files.length} - {" file(s)"} - - - ); - - unmount(0); -} diff --git a/src/file.ts b/src/file.ts index 6e2fcb5..5cee074 100644 --- a/src/file.ts +++ b/src/file.ts @@ -1,6 +1,6 @@ import { existsSync, lstatSync, readFileSync } from "fs"; -import mimes from "mime-types"; import { Result, err, ok } from "neverthrow"; +import mimes from "mime-types"; export interface File { diff --git a/src/index.ts b/src/index.ts index 03f26f4..efc8cae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { program } from "commander"; -import { embed } from "./embed.js"; +import { embed } from "./commands/embed.js"; program.command("embed") .description("Generate embedding headers for given files") diff --git a/src/templates/all.eta b/src/templates/all.eta index bb9ee34..1185a09 100644 --- a/src/templates/all.eta +++ b/src/templates/all.eta @@ -13,9 +13,9 @@ namespace saucer::embedded { std::map rtn; - <% it.files.forEach(function(file){ %> +<% it.files.forEach(function(file){ %> rtn.emplace("<%= file.path %>", embedded_file{"<%= file.mime %>", <%= file.name %>); - <% }) %> +<% }) %> return rtn; } diff --git a/src/theme/index.ts b/src/theme/index.ts new file mode 100644 index 0000000..6125d8c --- /dev/null +++ b/src/theme/index.ts @@ -0,0 +1,6 @@ +export default +{ + colors: { + purple: "#7474FF" + } +} as const; diff --git a/src/utils/colors.ts b/src/utils/colors.ts deleted file mode 100644 index 3e9e15c..0000000 --- a/src/utils/colors.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default { - purple: "#7474FF" -} as const; diff --git a/tsconfig.json b/tsconfig.json index f22f612..425d5f1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,21 @@ { - "extends": "@sindresorhus/tsconfig", "compilerOptions": { + "outDir": "build", + "target": "ESNext", + "module": "NodeNext", + "lib": [ + "dom", + "ES2023", + ], + "strict": true, + "strictNullChecks": true, "jsx": "react-jsx", - "outDir": "dist", + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, }, - "include": [ - "src", - ] }