Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retool build to produce plugin markdown files, minified/non-minified builds, and compatible JS code #2

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions PLUGIN_README.md
Original file line number Diff line number Diff line change
@@ -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!
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -53,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)
233 changes: 221 additions & 12 deletions esbuild.js
Original file line number Diff line number Diff line change
@@ -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, "<br />"); // 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();
14 changes: 14 additions & 0 deletions plugin.config.js
Original file line number Diff line number Diff line change
@@ -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"],
};