From bb5225f32c6fd60d2d31d84b7f005fcb259ce9bd Mon Sep 17 00:00:00 2001 From: Paul Strong Date: Thu, 10 Oct 2024 14:27:42 -0600 Subject: [PATCH 1/2] Retool build to produce plugin markdown files in non-minified and minified versions. Use plugin.config file to produce metadata table. --- PLUGIN_README.md | 26 ++++++ README.md | 4 + esbuild.js | 233 ++++++++++++++++++++++++++++++++++++++++++++--- plugin.config.js | 14 +++ 4 files changed, 265 insertions(+), 12 deletions(-) create mode 100644 PLUGIN_README.md create mode 100644 plugin.config.js diff --git a/PLUGIN_README.md b/PLUGIN_README.md new file mode 100644 index 0000000..0d42c32 --- /dev/null +++ b/PLUGIN_README.md @@ -0,0 +1,26 @@ +# Your Cool Amplenote Plugin Name + +In this section you can provide some details about the [Amplenote plugin](https://www.amplenote.com/help/developing_amplenote_plugins) +that this repo will implement. + +## **Features** + +In this section you can elaborate on the features + +## **Usage** + +In this section you can give any usage instructions + +## **Author** + +Any information about you + +**Published by**: your user + +**Date**: publish date + +**Last Updated**: last update date + +## **Feedback** + +If you have any questions, issues, or feedback, please feel free to reach out! diff --git a/README.md b/README.md index 5cdd3fe..a71e013 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ In this section you can provide some details about the [Amplenote plugin](https://www.amplenote.com/help/developing_amplenote_plugins) that this repo will implement. +## Build Configuration + +Put your plugin readme in `PLUGIN_README.md` and configure your plugin details in `plugin.json` + ## Installation 1. Clone this repo. `git clone git@github.com:alloy-org/plugin-template.git` diff --git a/esbuild.js b/esbuild.js index 1ac2bd9..7d2a66a 100644 --- a/esbuild.js +++ b/esbuild.js @@ -1,15 +1,224 @@ -import dotenv from "dotenv" -import esbuild from "esbuild" +import dotenv from "dotenv"; +import esbuild from "esbuild"; +import path from "path"; +import fs from "fs"; +import pluginConfig from "./plugin.config.js"; dotenv.config(); -const result = await esbuild.build({ - entryPoints: [`lib/plugin.js`], - bundle: true, - format: "iife", - outfile: "build/compiled.js", - packages: "external", - platform: "node", - write: true, -}); -console.log("Result was", result) +const README_FILE = path.resolve("PLUGIN_README.md"); + +/** + * Escapes Markdown special characters in a string to prevent formatting issues. + * + * @param {any} text - The text to escape. + * @returns {string} - The escaped text. + */ +function escapeMarkdown(text) { + if (typeof text !== "string") { + text = String(text); + } + return text + .replace(/\\/g, "\\\\") // Escape backslashes + .replace(/\|/g, "\\|") // Escape pipe characters + .replace(/\n/g, "
"); // Replace newlines with backslash + newline for Markdown +} + +/** + * Converts a JSON object into a uniformly formatted Markdown string with a header and a table, + * respecting a maximum column width to ensure consistent formatting. + * + * @param {Object} jsonObj - The JSON object to convert. + * @param {number} [maxCol1Width=30] - Maximum width for the first column (keys). + * @param {number} [maxCol2Width=65] - Maximum width for the second column (values). + * @returns {string} - The resulting Markdown string with aligned columns. + */ +function jsonToMetadataTable(jsonObj, maxCol1Width = 30, maxCol2Width = 65) { + // Define the header line + const header = "## Table - Plugin Parameters:\n\n"; + + // Initialize an array to hold all table rows + const allRows = []; + + // Initialize arrays to hold rows within the max width for column width calculation + const validRowsForWidth = []; + + // Iterate over each key-value pair in the JSON object + for (const [key, value] of Object.entries(jsonObj)) { + if (Array.isArray(value)) { + // If the value is an array, create a separate row for each item + value.forEach((item) => { + const escapedKey = escapeMarkdown(key); + const escapedValue = escapeMarkdown(item); + allRows.push([escapedKey, escapedValue]); + + // Check if both key and value are within the max widths + if (escapedKey.length <= maxCol1Width && escapedValue.length <= maxCol2Width) { + validRowsForWidth.push([escapedKey, escapedValue]); + } + }); + } else { + // For single values, create one row + const escapedKey = escapeMarkdown(key); + const escapedValue = escapeMarkdown(value); + allRows.push([escapedKey, escapedValue]); + + // Check if both key and value are within the max widths + if (escapedKey.length <= maxCol1Width && escapedValue.length <= maxCol2Width) { + validRowsForWidth.push([escapedKey, escapedValue]); + } + } + } + + // Determine the maximum width for each column based on valid rows + const col1Width = Math.min( + Math.max( + ...validRowsForWidth.map((row) => row[0].length), + "Key".length // Minimum width based on header + ), + maxCol1Width + ); + + const col2Width = Math.min( + Math.max( + ...validRowsForWidth.map((row) => row[1].length), + "Value".length // Minimum width based on header + ), + maxCol2Width + ); + + // Helper function to pad a string with spaces to a desired length + const pad = (str, length) => { + if (str.length >= length) return str; + return str + " ".repeat(length - str.length); + }; + + // Create the table header with padded columns + const tableHeader = + `| ${pad("", col1Width)} | ${pad("", col2Width)} |\n` + + `| ${"-".repeat(col1Width)} | ${"-".repeat(col2Width)} |\n`; + + // Create table rows with padded columns + const tableRows = + allRows + .map((row) => { + const [col1, col2] = row; + return `| ${pad(col1, col1Width)} | ${pad(col2, col2Width)} |`; + }) + .join("\n") + "\n"; + + // Combine the header, table header, and all table rows + return header + tableHeader + tableRows; +} + +// Custom plugin to modify the output +// Amplenote is sensitive about the code format and expects to evaluate to a JS Object, so we use this plugin to produce the code in a way it accepts +const amplenoteifyPlugin = { + name: "amplenoteify", + setup(build) { + build.onEnd(async () => { + const outfilePath = path.resolve("build/compiled.js"); + + // Read the content of the outfile + let modifiedOutput = fs.readFileSync(outfilePath, "utf-8"); + + // Modify the output content as needed + // Amplenote doesn't like the code block to end with a semicolon + // And it expects a JS object to be returned which iife modules don't do by default + // So we modify the compiled js to return the plugin object, and remove the final semicolon + modifiedOutput = modifiedOutput.replace( + /var plugin_default = plugin;\s*\}\)\(\);/, + "var plugin_default = plugin;\nreturn plugin;\n})()" + ); + + // Write the modified output back to the file system + fs.writeFileSync(outfilePath, modifiedOutput); + }); + }, +}; + +/** + * Build and generate a markdown file + * @param {String} compiledCode + * @param {String} markdownFile Target file name + */ +async function generateMarkdownFile(compiledCode, markdownFile) { + try { + // Read the contents of README.md and plugin.config.js + const readmeContent = fs.readFileSync(README_FILE, "utf-8"); + + const { name, description, version, sourceRepo, icon, instructions, setting } = pluginConfig; + + // Prepare metadata table from plugin config using only relevant parameters + const metadataTable = jsonToMetadataTable({ name, description, icon, instructions, setting }); + + // Prepare the compiled code block + const compiledCodeBlock = "```js\n" + compiledCode + "\n```"; + + // Concatenate all parts + const outputContent = `${readmeContent} + +# ${name}${version ? " (" + version + ")" : ""} + +${metadataTable} + +## Code Base:${sourceRepo ? "\n\nSource Repo: [" + sourceRepo + "](" + sourceRepo + ")" : ""} + +${compiledCodeBlock}`; + + // Write to the markdown file + fs.writeFileSync(markdownFile, outputContent); + + console.log(`Successfully generated ${markdownFile}`); + } catch (err) { + console.error(`Error generating ${markdownFile}:`, err); + process.exit(1); + } +} +/** + * Build and generate the markdown in a two-step process to generate normal and minified versions + */ +async function buildAndGenerateMarkdown() { + const outfile = "build/compiled.js"; + const markdownFile = "build/plugin.md"; + const minifiedMarkdownFile = "build/plugin.min.md"; + + // Build the code without minification + await esbuild.build({ + entryPoints: ["lib/plugin.js"], + bundle: true, + format: "iife", + outfile: outfile, + platform: "node", + plugins: [amplenoteifyPlugin], + minify: false, + write: true, + }); + + console.log(`Build completed for ${outfile}`); + + // Read the unminified code + const compiledCode = fs.readFileSync(outfile, "utf-8"); + + // Generate the unminified markdown file + await generateMarkdownFile(compiledCode, markdownFile); + + // Minify the code using esbuild.transform + const minifiedResult = await esbuild.transform(compiledCode, { + minify: true, + loader: "js", + }); + + // Remove the final semicolon from the minified code to make it acceptable by amplenote + const finalCode = minifiedResult.code.replace(/;[\s]*$/, ""); + + // Write the minified code to a file + fs.writeFileSync("build/compiled.min.js", finalCode); + + console.log("Minified code generated at build/compiled.min.js"); + + // Generate the minified markdown file + await generateMarkdownFile(finalCode, minifiedMarkdownFile); +} + +await buildAndGenerateMarkdown(); diff --git a/plugin.config.js b/plugin.config.js new file mode 100644 index 0000000..5661c96 --- /dev/null +++ b/plugin.config.js @@ -0,0 +1,14 @@ +/** The config file for your plugin + * The version and sourceRepo parameters are optional, they will be output in your plugin + * setting is an array of all your Settings, but you can remove the key if your plugin doesn't have any settings. + */ +export default { + name: "Your Plugin Name", + description: "A plugin to ... (add description here)", + icon: "extension", + version: "1.0.0", + sourceRepo: "https://github.com/acct/repo", // This is optional and can be removed + instructions: `![](https://linktoimage) +Put any instructions **here**`, + setting: ["Setting #1 (default: false)", "Setting #2", "Setting #3"], +}; From d03de5ff2b0616670f006c35a6a6a7335e3e277a Mon Sep 17 00:00:00 2001 From: Paul Strong Date: Thu, 10 Oct 2024 15:11:56 -0600 Subject: [PATCH 2/2] Add short message for how you can use the markdown build in publishing or development --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a71e013..6e2c9cd 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,5 @@ Once your plugin is ready to test within Amplenote, you can build and test it wi 3. Commit the resulting file (default location: `build/compiled.js`) to your git repo (e.g., `git add build/compiled.js && git commit -m "Compiled plugin"`) 4. Push your changes to GitHub (`git push`) 5. Choose "Github Plugin Builder: Refresh" from the note options menu in your plugin note + +Alternatively, you can copy/paste the output markdown build (`plugin.md` or `plugin.min.md`) into your plugin note file using the [Markdown plugin](https://www.amplenote.com/plugins/KKfwtmMVtoxSCdsK5bNdnad8)