From 22c476d3ef2d32591fb1d39d3378389627821ef2 Mon Sep 17 00:00:00 2001 From: Jackson Morgan Date: Thu, 26 Dec 2024 17:06:35 -0500 Subject: [PATCH] untested readme generator --- package-lock.json | 15 +++- packages/cli/.gitignore | 1 + packages/cli/package.json | 8 +- packages/cli/src/build.ts | 93 +++++++++------------ packages/cli/src/create.ts | 36 ++++++++ packages/cli/src/generateReadme.ts | 50 +++++++++++ packages/cli/src/index.ts | 29 ++++++- packages/cli/src/init.ts | 26 +++--- packages/cli/src/templates/readme/main.ejs | 15 ++++ packages/cli/src/templates/readme/shape.ejs | 27 ++++++ packages/cli/src/util/forAllShapes.ts | 26 ++++++ packages/cli/src/util/modifyPackageJson.ts | 25 ++++++ packages/cli/test/.shapes/foafProfile.shex | 2 +- packages/cli/test/package.json | 4 + 14 files changed, 283 insertions(+), 74 deletions(-) create mode 100644 packages/cli/.gitignore create mode 100644 packages/cli/src/create.ts create mode 100644 packages/cli/src/generateReadme.ts create mode 100644 packages/cli/src/templates/readme/main.ejs create mode 100644 packages/cli/src/templates/readme/shape.ejs create mode 100644 packages/cli/src/util/forAllShapes.ts create mode 100644 packages/cli/src/util/modifyPackageJson.ts create mode 100644 packages/cli/test/package.json diff --git a/package-lock.json b/package-lock.json index c788fe4..7775f6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28647,7 +28647,8 @@ "ejs": "^3.1.8", "fs-extra": "^10.1.0", "loading-cli": "^1.1.0", - "prettier": "^3.0.3" + "prettier": "^3.0.3", + "type-fest": "^4.31.0" }, "bin": { "ldo": "dist/index.js" @@ -28726,6 +28727,18 @@ } } }, + "packages/cli/node_modules/type-fest": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.31.0.tgz", + "integrity": "sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/cli/node_modules/typescript": { "version": "4.9.5", "dev": true, diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore new file mode 100644 index 0000000..7d05a05 --- /dev/null +++ b/packages/cli/.gitignore @@ -0,0 +1 @@ +test/.ldo \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index 80759b4..1c7d33f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -14,13 +14,14 @@ "build:ts": "tsc --project tsconfig.build.json", "clean": "rimraf dist/", "copy-files": "copyfiles -u 1 \"./src/**/*.ejs\" dist/", - "update-permission": "chmod +x ../../node_modules/.bin/ldo", + "update-permission": "chmod +x ./dist/index.js", "watch": "tsc --watch", "test": "jest --coverage", "test:watch": "jest --watch", "prepublishOnly": "npm run test && npm run build", "lint": "eslint src/** --fix --no-error-on-unmatched-pattern", - "build:ldo": "./dist/index.js build --input test/.shapes --output test/.ldo" + "build:ldo": "./dist/index.js build --input test/.shapes --output test/.ldo", + "generate-readme": "./dist/index.js generate-readme --project ./test --shapes ./test/.shapes/ --ldo ./test/.ldo/" }, "repository": { "type": "git", @@ -51,7 +52,8 @@ "ejs": "^3.1.8", "fs-extra": "^10.1.0", "loading-cli": "^1.1.0", - "prettier": "^3.0.3" + "prettier": "^3.0.3", + "type-fest": "^4.31.0" }, "files": [ "dist", diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index 0aebb7f..ab81b6e 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -6,6 +6,7 @@ import schemaConverterShex from "@ldo/schema-converter-shex"; import { renderFile } from "ejs"; import prettier from "prettier"; import loading from "loading-cli"; +import { forAllShapes } from "./util/forAllShapes"; interface BuildOptions { input: string; @@ -15,13 +16,6 @@ interface BuildOptions { export async function build(options: BuildOptions) { const load = loading("Peparing Environment"); load.start(); - const shapeDir = await fs.promises.readdir(options.input, { - withFileTypes: true, - }); - // Filter out non-shex documents - const shexFiles = shapeDir.filter( - (file) => file.isFile() && file.name.endsWith(".shex"), - ); // Prepare new folder by clearing/and/or creating it if (fs.existsSync(options.output)) { await fs.promises.rm(options.output, { recursive: true }); @@ -29,51 +23,44 @@ export async function build(options: BuildOptions) { await fs.promises.mkdir(options.output); load.text = "Generating LDO Documents"; - await Promise.all( - shexFiles.map(async (file) => { - const fileName = path.parse(file.name).name; - // Get the content of each document - const shexC = await fs.promises.readFile( - path.join(options.input, file.name), - "utf8", - ); - // Convert to ShexJ - let schema: Schema; - try { - schema = parser.construct("https://ldo.js.org/").parse(shexC); - } catch (err) { - const errMessage = - err instanceof Error - ? err.message - : typeof err === "string" - ? err - : "Unknown Error"; - console.error(`Error processing ${file.name}: ${errMessage}`); - return; - } - // Convert the content to types - const [typings, context] = await schemaConverterShex(schema); - await Promise.all( - ["context", "schema", "shapeTypes", "typings"].map( - async (templateName) => { - const finalContent = await renderFile( - path.join(__dirname, "./templates", `${templateName}.ejs`), - { - typings: typings.typings, - fileName, - schema: JSON.stringify(schema, null, 2), - context: JSON.stringify(context, null, 2), - }, - ); - // Save conversion to document - await fs.promises.writeFile( - path.join(options.output, `${fileName}.${templateName}.ts`), - await prettier.format(finalContent, { parser: "typescript" }), - ); - }, - ), - ); - }), - ); + await forAllShapes(options.input, async (fileName, shexC) => { + // Convert to ShexJ + let schema: Schema; + try { + schema = parser.construct("https://ldo.js.org/").parse(shexC); + } catch (err) { + const errMessage = + err instanceof Error + ? err.message + : typeof err === "string" + ? err + : "Unknown Error"; + console.error(`Error processing ${fileName}: ${errMessage}`); + return; + } + // Convert the content to types + const [typings, context] = await schemaConverterShex(schema); + await Promise.all( + ["context", "schema", "shapeTypes", "typings"].map( + async (templateName) => { + const finalContent = await renderFile( + path.join(__dirname, "./templates", `${templateName}.ejs`), + { + typings: typings.typings, + fileName, + schema: JSON.stringify(schema, null, 2), + context: JSON.stringify(context, null, 2), + }, + ); + // Save conversion to document + await fs.promises.writeFile( + path.join(options.output, `${fileName}.${templateName}.ts`), + await prettier.format(finalContent, { parser: "typescript" }), + ); + }, + ), + ); + }); + load.stop(); } diff --git a/packages/cli/src/create.ts b/packages/cli/src/create.ts new file mode 100644 index 0000000..b37ec60 --- /dev/null +++ b/packages/cli/src/create.ts @@ -0,0 +1,36 @@ +import { exec } from "child-process-promise"; +import { init } from "./init"; +import { modifyPackageJson } from "./util/modifyPackageJson"; +import { generateReadme } from "./generateReadme"; +import path from "path"; + +interface CreateOptions { + directory: string; + name: string; +} + +export async function create(options: CreateOptions) { + // Init the NPM Package + await exec(`npm init ${options.directory}`); + + // Init LDO + await init({ directory: options.directory }); + + // Add prepublish script + await modifyPackageJson(async (packageJson) => { + if (!packageJson.scripts) packageJson.scripts = {}; + packageJson.scripts.prepublish = + "npm run build:ldo & npm run generate-readme"; + packageJson.scripts[ + "genenerate-readme" + ] = `ldo generate-readme --readme ./README.md --shapes ./.shapes --ldo ./ldo`; + return packageJson; + }); + + // Generate ReadMe + await generateReadme({ + readmePath: path.join(options.directory, "README.md"), + shapesPath: path.join(options.directory, ".shapes"), + ldoPath: path.join(options.directory, ".ldo"), + }); +} diff --git a/packages/cli/src/generateReadme.ts b/packages/cli/src/generateReadme.ts new file mode 100644 index 0000000..0b4f885 --- /dev/null +++ b/packages/cli/src/generateReadme.ts @@ -0,0 +1,50 @@ +import { getPackageJson } from "./util/modifyPackageJson"; +import { forAllShapes } from "./util/forAllShapes"; +import { promises as fs } from "fs"; +import path from "path"; + +interface GenerateReadmeOptions { + projectFolder: string; + shapesPath: string; + ldoPath: string; +} + +interface ReadmeEjsOptions { + projectName: string; + projectDescription: string; + shapes: { + name: string; + types: { + typeName: string; + shapeTypeName: string; + }[]; + shex: string; + typescript: string; + }[]; +} + +export async function generateReadme(options: GenerateReadmeOptions) { + const packageJson = await getPackageJson(options.projectFolder); + const projectName = packageJson.name; + const projectDescription = packageJson.description; + const shapes: ReadmeEjsOptions["shapes"] = []; + + await forAllShapes(options.shapesPath, async (fileName, shexC) => { + const typesRaw = await fs.readFile( + path.join(options.shapesPath, `${fileName}.typings.ts`), + "utf8", + ); + const shapeTypesRaw = await fs.readFile( + path.join(options.shapesPath, `${fileName}.shapeTypes.ts`), + "utf8", + ); + + console.log(typesRaw); + console.log(shapeTypesRaw); + + // shapes.push({ + // name: fileName, + // shex: shexC, + // }); + }); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c7eb26b..fc12bb6 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -3,6 +3,7 @@ import { program } from "commander"; import { build } from "./build"; import { init } from "./init"; +import { create } from "./create"; program .name("LDO-CLI") @@ -18,8 +19,34 @@ program program .command("init") - .option("-d --directory>", "A parent directory for ldo files") + .argument("[directory]", "A parent directory for ldo files") .description("Initializes a project for LDO.") .action(init); +program + .command("create") + .argument("", "The package's directory", "./") + .description("Creates a standalone package for shapes to publish to NPM.") + .action(create); + +program + .command("generate-readme") + .description("Create a ReadMe from the shapes and generated code.") + .requiredOption( + "-r, --readme ", + "Provide the path to the readme", + "./.shapes", + ) + .requiredOption( + "-s, --shapes ", + "Provide the path to the shapes folder", + "./.shapes", + ) + .requiredOption( + "-s, --ldo ", + "Provide the path to the ldo folder", + "./.ldo", + ) + .action(build); + program.parse(); diff --git a/packages/cli/src/init.ts b/packages/cli/src/init.ts index d1ffb91..ddafd50 100644 --- a/packages/cli/src/init.ts +++ b/packages/cli/src/init.ts @@ -2,6 +2,7 @@ import { exec } from "child-process-promise"; import fs from "fs-extra"; import path from "path"; import { renderFile } from "ejs"; +import { modifyPackageJson } from "./util/modifyPackageJson"; const DEFAULT_SHAPES_FOLDER = "./.shapes"; const DEFAULT_LDO_FOLDER = "./.ldo"; @@ -57,21 +58,16 @@ export async function init(initOptions: InitOptions) { ); // Add build script - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const packageJson: any = JSON.parse( - (await fs.promises.readFile("./package.json")).toString(), - ); - if (!packageJson.scripts) { - packageJson.scripts = {}; - } - const ldoFolder = path.join(parentDirectory, DEFAULT_LDO_FOLDER); - packageJson.scripts[ - "build:ldo" - ] = `ldo build --input ${shapesFolderPath} --output ${ldoFolder}`; - await fs.promises.writeFile( - "./package.json", - JSON.stringify(packageJson, null, 2), - ); + await modifyPackageJson("./", async (packageJson) => { + if (!packageJson.scripts) { + packageJson.scripts = {}; + } + const ldoFolder = path.join(parentDirectory, DEFAULT_LDO_FOLDER); + packageJson.scripts[ + "build:ldo" + ] = `ldo build --input ${shapesFolderPath} --output ${ldoFolder}`; + return packageJson; + }); // Build LDO await exec("npm run build:ldo"); diff --git a/packages/cli/src/templates/readme/main.ejs b/packages/cli/src/templates/readme/main.ejs new file mode 100644 index 0000000..8c63de6 --- /dev/null +++ b/packages/cli/src/templates/readme/main.ejs @@ -0,0 +1,15 @@ +# <%= projectname %> + +<%= projectDescrition %> + +This project includes shapes and generated files for [LDO](https://ldo.js.org). + +## Installation + +```bash +npm i <%= projectname %> +``` + +<% shapes.forEach(function(shape) { %> + <%- include('shape', { shape: shape, projectname: projectname }) %> +<% }); %> \ No newline at end of file diff --git a/packages/cli/src/templates/readme/shape.ejs b/packages/cli/src/templates/readme/shape.ejs new file mode 100644 index 0000000..38b694d --- /dev/null +++ b/packages/cli/src/templates/readme/shape.ejs @@ -0,0 +1,27 @@ +## <%= shape.name %> + +### Usage with LDO + +```typescript +import { createLdoDataset } from "@ldo/ldo"; +import { <%= shape.types.map((type) => type.shapeTypeName).join(", ") %> } from "<%= projectName =>"; +import type { <%= shape.types.map((type) => type.typeName).join(", ") %> } from "<%= projectName =>"; +const ldoDataset = createLdoDataset(); +<% shape.types.forEach(function(type, index) { %> +const example<%= index %>: <%= type.typeName %> = ldoDataset + .usingType(<%= type.shapeTypeName %>) + .fromSubject("http://example.com/example<%= index %>"); +<%> }); <%> +``` + +### ShEx Typings + +```shex +<%= shape.shex %> +``` + +### TypeScript Typings + +```typescript +<%= shape.typescript %> +``` diff --git a/packages/cli/src/util/forAllShapes.ts b/packages/cli/src/util/forAllShapes.ts new file mode 100644 index 0000000..7897b35 --- /dev/null +++ b/packages/cli/src/util/forAllShapes.ts @@ -0,0 +1,26 @@ +import fs from "fs"; +import path from "path"; + +export async function forAllShapes( + shapePath: string, + callback: (filename: string, shape: string) => Promise, +): Promise { + const shapeDir = await fs.promises.readdir(shapePath, { + withFileTypes: true, + }); + // Filter out non-shex documents + const shexFiles = shapeDir.filter( + (file) => file.isFile() && file.name.endsWith(".shex"), + ); + await Promise.all( + shexFiles.map(async (file) => { + const fileName = path.parse(file.name).name; + // Get the content of each document + const shexC = await fs.promises.readFile( + path.join(shapePath, file.name), + "utf8", + ); + await callback(fileName, shexC); + }), + ); +} diff --git a/packages/cli/src/util/modifyPackageJson.ts b/packages/cli/src/util/modifyPackageJson.ts new file mode 100644 index 0000000..54adab4 --- /dev/null +++ b/packages/cli/src/util/modifyPackageJson.ts @@ -0,0 +1,25 @@ +import type { PackageJson } from "type-fest"; +import fs from "fs-extra"; +import path from "path"; + +export async function getPackageJson( + projectFolder: string, +): Promise { + return JSON.parse( + ( + await fs.promises.readFile(path.join(projectFolder, "./package.json")) + ).toString(), + ); +} + +export async function modifyPackageJson( + projectFolder: string, + modifyCallback: (packageJson: PackageJson) => Promise, +): Promise { + const packageJson: PackageJson = await getPackageJson(projectFolder); + const newPackageJson = await modifyCallback(packageJson); + await fs.promises.writeFile( + path.join(projectFolder, "./package.json"), + JSON.stringify(newPackageJson, null, 2), + ); +} diff --git a/packages/cli/test/.shapes/foafProfile.shex b/packages/cli/test/.shapes/foafProfile.shex index 95c472b..dff09d1 100644 --- a/packages/cli/test/.shapes/foafProfile.shex +++ b/packages/cli/test/.shapes/foafProfile.shex @@ -13,7 +13,7 @@ ex:FoafProfile EXTRA a { foaf:name xsd:string ? // rdfs:comment "Define a person's name." ; foaf:img xsd:string ? - // rdfs:comment "Photo link but in string form" + // rdfs:comment "Photo link but in string form" ; foaf:knows @ex:FoafProfile * // rdfs:comment "A list of WebIds for all the people this user knows." ; } diff --git a/packages/cli/test/package.json b/packages/cli/test/package.json new file mode 100644 index 0000000..554a6ba --- /dev/null +++ b/packages/cli/test/package.json @@ -0,0 +1,4 @@ +{ + "name": "foaf-profile", + "description": "A profile using FOAF." +} \ No newline at end of file