diff --git a/adminforth/commands/cli.js b/adminforth/commands/cli.js index a5c71261b..9c5356a77 100755 --- a/adminforth/commands/cli.js +++ b/adminforth/commands/cli.js @@ -6,11 +6,14 @@ const command = args[0]; import bundle from "./bundle.js"; import createApp from "./createApp/main.js"; import generateModels from "./generateModels.js"; - +import createPlugin from "./createPlugin/main.js"; switch (command) { case "create-app": createApp(args); break; + case "create-plugin": + createPlugin(args); + break; case "generate-models": generateModels(); break; @@ -18,5 +21,7 @@ switch (command) { bundle(); break; default: - console.log("Unknown command. Available commands: create-app, generate-models, bundle"); -} \ No newline at end of file + console.log( + "Unknown command. Available commands: create-app, create-plugin, generate-models, bundle" + ); +} diff --git a/adminforth/commands/createPlugin/main.js b/adminforth/commands/createPlugin/main.js new file mode 100644 index 000000000..eb5147404 --- /dev/null +++ b/adminforth/commands/createPlugin/main.js @@ -0,0 +1,26 @@ +import chalk from "chalk"; + +import { + parseArgumentsIntoOptions, + prepareWorkflow, + promptForMissingOptions, +} from "./utils.js"; + +export default async function createPlugin(args) { + // Step 1: Parse CLI arguments with `arg` + let options = parseArgumentsIntoOptions(args); + + // Step 2: Ask for missing arguments via `inquirer` + options = await promptForMissingOptions(options); + + // Step 3: Prepare a Listr-based workflow + const tasks = prepareWorkflow(options); + + // Step 4: Run tasks + try { + await tasks.run(); + } catch (err) { + console.error(chalk.red(`\nāŒ ${err.message}\n`)); + process.exit(1); + } +} diff --git a/adminforth/commands/createPlugin/templates/.gitignore.hbs b/adminforth/commands/createPlugin/templates/.gitignore.hbs new file mode 100644 index 000000000..2174ce4e7 --- /dev/null +++ b/adminforth/commands/createPlugin/templates/.gitignore.hbs @@ -0,0 +1,3 @@ +node_modules +custom/node_modules +dist \ No newline at end of file diff --git a/adminforth/commands/createPlugin/templates/custom/tsconfig.json.hbs b/adminforth/commands/createPlugin/templates/custom/tsconfig.json.hbs new file mode 100644 index 000000000..d313f46cb --- /dev/null +++ b/adminforth/commands/createPlugin/templates/custom/tsconfig.json.hbs @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "baseUrl": ".", // This should point to your project root + "paths": { + "@/*": [ + // "node_modules/adminforth/dist/spa/src/*" + "../../../spa/src/*" + ], + "*": [ + // "node_modules/adminforth/dist/spa/node_modules/*" + "../../../spa/node_modules/*" + ], + "@@/*": [ + // "node_modules/adminforth/dist/spa/src/*" + "." + ] + } + } +} \ No newline at end of file diff --git a/adminforth/commands/createPlugin/templates/index.ts.hbs b/adminforth/commands/createPlugin/templates/index.ts.hbs new file mode 100644 index 000000000..f421fff91 --- /dev/null +++ b/adminforth/commands/createPlugin/templates/index.ts.hbs @@ -0,0 +1,41 @@ +import { AdminForthPlugin } from "adminforth"; +import type { IAdminForth, IHttpServer, AdminForthResourcePages, AdminForthResourceColumn, AdminForthDataTypes, AdminForthResource } from "adminforth"; +import type { PluginOptions } from './types.js'; + + +export default class ChatGptPlugin extends AdminForthPlugin { + options: PluginOptions; + + constructor(options: PluginOptions) { + super(options, import.meta.url); + this.options = options; + } + + async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) { + super.modifyResourceConfig(adminforth, resourceConfig); + + // simply modify resourceConfig or adminforth.config. You can get access to plugin options via this.options; + } + + validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) { + // optional method where you can safely check field types after database discovery was performed + } + + instanceUniqueRepresentation(pluginOptions: any) : string { + // optional method to return unique string representation of plugin instance. + // Needed if plugin can have multiple instances on one resource + return `single`; + } + + setupEndpoints(server: IHttpServer) { + server.endpoint({ + method: 'POST', + path: `/plugin/${this.pluginInstanceId}/example`, + handler: async ({ body }) => { + const { name } = body; + return { hey: `Hello ${name}` }; + } + }); + } + +} \ No newline at end of file diff --git a/adminforth/commands/createPlugin/templates/package.json.hbs b/adminforth/commands/createPlugin/templates/package.json.hbs new file mode 100644 index 000000000..d206e45a1 --- /dev/null +++ b/adminforth/commands/createPlugin/templates/package.json.hbs @@ -0,0 +1,21 @@ +{ + "name": "{{pluginName}}", + "version": "1.0.1", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build": "tsc && rsync -av --exclude 'node_modules' custom dist/ && npm version patch" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@types/node": "latest", + "typescript": "^5.7.3" + }, + "dependencies": { + "adminforth": "latest", + } +} diff --git a/adminforth/commands/createPlugin/templates/tsconfig.json.hbs b/adminforth/commands/createPlugin/templates/tsconfig.json.hbs new file mode 100644 index 000000000..14366b567 --- /dev/null +++ b/adminforth/commands/createPlugin/templates/tsconfig.json.hbs @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include*/ + "module": "node16", /* Specify what module code is generated. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "strict": false, /* Enable all strict type-checking options. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + }, + "exclude": ["node_modules", "dist", "custom"], /* Exclude files from compilation. */ + } + \ No newline at end of file diff --git a/adminforth/commands/createPlugin/templates/types.ts.hbs b/adminforth/commands/createPlugin/templates/types.ts.hbs new file mode 100644 index 000000000..062d4a794 --- /dev/null +++ b/adminforth/commands/createPlugin/templates/types.ts.hbs @@ -0,0 +1,3 @@ +export interface PluginOptions { + +} diff --git a/adminforth/commands/createPlugin/utils.js b/adminforth/commands/createPlugin/utils.js new file mode 100644 index 000000000..6fa0cec6a --- /dev/null +++ b/adminforth/commands/createPlugin/utils.js @@ -0,0 +1,222 @@ +import arg from 'arg'; +import chalk from 'chalk'; +import fs from 'fs'; +import fse from 'fs-extra'; +import inquirer from 'inquirer'; +import path from 'path'; +import { Listr } from 'listr2'; +import { fileURLToPath } from 'url'; +import { execa } from 'execa'; +import Handlebars from 'handlebars'; + +export function parseArgumentsIntoOptions(rawArgs) { + const args = arg( + { + "--plugin-name": String, + // you can add more flags here if needed + }, + { + argv: rawArgs.slice(1), // skip "create-plugin" + } + ); + + return { + pluginName: args["--plugin-name"], + }; +} + +export async function promptForMissingOptions(options) { + const questions = []; + + if (!options.pluginName) { + questions.push({ + type: "input", + name: "pluginName", + message: "Please specify the name of the plugin >", + default: "adminforth-plugin", + }); + } + + const answers = await inquirer.prompt(questions); + return { + ...options, + pluginName: options.pluginName || answers.pluginName, + }; +} + +function checkNodeVersion(minRequiredVersion = 20) { + const current = process.versions.node.split("."); + const major = parseInt(current[0], 10); + + if (isNaN(major) || major < minRequiredVersion) { + throw new Error( + `Node.js v${minRequiredVersion}+ is required. You have ${process.versions.node}. ` + + `Please upgrade Node.js. We recommend using nvm for managing multiple Node.js versions.` + ); + } +} + +function checkForExistingPackageJson() { + if (fs.existsSync(path.join(process.cwd(), "package.json"))) { + throw new Error( + `A package.json already exists in this directory.\n` + + `Please remove it or use an empty directory.` + ); + } +} + +function initialChecks() { + return [ + { + title: "šŸ‘€ Checking Node.js version...", + task: () => checkNodeVersion(20), + }, + { + title: "šŸ‘€ Validating current working directory...", + task: () => checkForExistingPackageJson(), + }, + ]; +} + +function renderHBSTemplate(templatePath, data) { + const template = fs.readFileSync(templatePath, "utf-8"); + const compiled = Handlebars.compile(template); + return compiled(data); +} + +async function scaffoldProject(ctx, options, cwd) { + const pluginName = options.pluginName; + + const filename = fileURLToPath(import.meta.url); + const dirname = path.dirname(filename); + + // Prepare directories + ctx.customDir = path.join(cwd, "custom"); + await fse.ensureDir(ctx.customDir); + + // Write templated files + await writeTemplateFiles(dirname, cwd, { + pluginName, + }); +} + +async function writeTemplateFiles(dirname, cwd, options) { + const { pluginName } = options; + + // Build a list of files to generate + const templateTasks = [ + { + src: "tsconfig.json.hbs", + dest: "tsconfig.json", + data: {}, + }, + { + src: "package.json.hbs", + dest: "package.json", + data: { pluginName }, + }, + { + src: "index.ts.hbs", + dest: "index.ts", + data: {}, + }, + { + src: ".gitignore.hbs", + dest: ".gitignore", + data: {}, + }, + { + src: "types.ts.hbs", + dest: "types.ts", + data: {}, + }, + { + src: "custom/tsconfig.json.hbs", + dest: "custom/tsconfig.json", + data: {}, + }, + ]; + + for (const task of templateTasks) { + // If a condition is specified and false, skip this file + if (task.condition === false) continue; + + const destPath = path.join(cwd, task.dest); + fse.ensureDirSync(path.dirname(destPath)); + + if (task.empty) { + fs.writeFileSync(destPath, ""); + } else { + const templatePath = path.join(dirname, "templates", task.src); + const compiled = renderHBSTemplate(templatePath, task.data); + fs.writeFileSync(destPath, compiled); + } + } +} + +async function installDependencies(ctx, cwd) { + const customDir = ctx.customDir; + + await Promise.all([ + await execa("npm", ["install", "--no-package-lock"], { cwd }), + await execa("npm", ["install"], { cwd: customDir }), + ]); +} + +function generateFinalInstructions() { + let instruction = "ā­ļø Your plugin is ready! Next steps:\n"; + + instruction += ` + ${chalk.dim("// Build your plugin")} + ${chalk.cyan("$ npm run build")}\n`; + + instruction += ` + ${chalk.dim("// To test your plugin locally")} + ${chalk.cyan("$ npm link")}\n`; + + instruction += ` + ${chalk.dim("// In your AdminForth project")} + ${chalk.cyan("$ npm link " + chalk.italic("your-plugin-name"))}\n`; + + instruction += "\nšŸ˜‰ Happy coding!"; + + return instruction; +} + +export function prepareWorkflow(options) { + const cwd = process.cwd(); + const tasks = new Listr( + [ + { + title: "šŸ” Initial checks...", + task: (_, task) => task.newListr(initialChecks(), { concurrent: true }), + }, + { + title: "šŸš€ Scaffolding your plugin...", + task: async (ctx) => scaffoldProject(ctx, options, cwd), + }, + { + title: "šŸ“¦ Installing dependencies...", + task: async (ctx) => installDependencies(ctx, cwd), + }, + { + title: "šŸ“ Preparing final instructions...", + task: (ctx) => { + console.log( + chalk.green(`āœ… Successfully created your new AdminForth plugin!\n`) + ); + console.log(generateFinalInstructions()); + console.log("\n\n"); + }, + }, + ], + { + rendererOptions: { collapseSubtasks: false }, + concurrent: false, + exitOnError: true, + collectErrors: true, + } + ); + + return tasks; +} diff --git a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md index f5d713985..c7664f5e4 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md @@ -30,110 +30,54 @@ Now create file `ApartsPie.vue` in the `custom` folder of your project: ```html title="./custom/ApartsPie.vue" - - +
+ +
+ + + ``` diff --git a/adminforth/documentation/docs/tutorial/03-Customization/Page Injections.png b/adminforth/documentation/docs/tutorial/03-Customization/Page Injections.png index bbdd6b3d9..ea2e86673 100644 Binary files a/adminforth/documentation/docs/tutorial/03-Customization/Page Injections.png and b/adminforth/documentation/docs/tutorial/03-Customization/Page Injections.png differ