fix(plugins): enhance plugin installation and script handling

- Updated the `pnpm install` command in `plugin-install.js` to include the `--ignore-workspace` flag, ensuring proper installation of plugins within the app tree.
- Improved the `instantiateScriptSource` function in `script-sandbox.js` to resolve `pluginDir` more effectively, allowing for better package management.
- Added a new smoke test in `plugins-smoke.js` to validate the ability to require additional packages from plugin directories, enhancing testing coverage for plugin functionality.
- Updated documentation in `AGENTS.md` to reflect changes in the installation command.
This commit is contained in:
2026-09-08 22:00:26 +07:00
parent 67ed3eecca
commit 6c82ff20eb
4 changed files with 68 additions and 3 deletions
+1 -1
View File
@@ -78,7 +78,7 @@ Copy the layout from `plugins/joplin-api`, `plugins/send-sms`, or `examples/plug
Add `dependencies` only if the script `require()`s extra npm packages. Then: Add `dependencies` only if the script `require()`s extra npm packages. Then:
```bash ```bash
pnpm install --dir plugins/<id> --ignore-scripts --prefer-offline pnpm install --dir plugins/<id> --ignore-scripts --prefer-offline --ignore-workspace
``` ```
`plugins/*/node_modules/` is gitignored. Host-allowlisted modules (`axios`, `jsonata`, …) come from the server; extra deps resolve from the plugin directory. `plugins/*/node_modules/` is gitignored. Host-allowlisted modules (`axios`, `jsonata`, …) come from the server; extra deps resolve from the plugin directory.
+10 -1
View File
@@ -51,9 +51,18 @@ export async function pnpmInstallPlugin(dir) {
} }
if (!hasDeps) return { skipped: true }; if (!hasDeps) return { skipped: true };
// --ignore-workspace: plugins live under the app tree but are not workspace
// packages; without this, pnpm install --dir can no-op against the root monorepo.
await execFileAsync( await execFileAsync(
"pnpm", "pnpm",
["install", "--dir", dir, "--ignore-scripts", "--prefer-offline"], [
"install",
"--dir",
dir,
"--ignore-scripts",
"--prefer-offline",
"--ignore-workspace",
],
{ {
cwd: dir, cwd: dir,
env: { ...process.env, CI: "1" }, env: { ...process.env, CI: "1" },
+8 -1
View File
@@ -557,16 +557,23 @@ function instantiateCompiled(
* $workflows?: { trigger: (name: string, data?: unknown) => Promise<unknown> }, * $workflows?: { trigger: (name: string, data?: unknown) => Promise<unknown> },
* pluginDir?: string | null, * pluginDir?: string | null,
* }} [opts] * }} [opts]
*
* When `pluginDir` is omitted, it is resolved from `script` so inspect/dry-run
* of `plugin/<id>` can `require()` extra packages from that plugin's node_modules.
*/ */
export function instantiateScriptSource(script, source, opts = {}) { export function instantiateScriptSource(script, source, opts = {}) {
const compiled = compileScriptSource(source, script); const compiled = compileScriptSource(source, script);
const pluginDir =
"pluginDir" in opts
? opts.pluginDir ?? null
: (resolveScriptRef(script).pluginDir ?? null);
const fn = instantiateCompiled(compiled, { const fn = instantiateCompiled(compiled, {
log: opts.log ?? inspectLog, log: opts.log ?? inspectLog,
script, script,
workflowName: opts.workflowName ?? "inspect", workflowName: opts.workflowName ?? "inspect",
owner: opts.owner ?? DEFAULT_OWNER, owner: opts.owner ?? DEFAULT_OWNER,
$workflows: opts.$workflows, $workflows: opts.$workflows,
pluginDir: opts.pluginDir ?? null, pluginDir,
}); });
return { fn, ...extractScriptMeta(fn) }; return { fn, ...extractScriptMeta(fn) };
} }
+49
View File
@@ -9,6 +9,7 @@
*/ */
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import fs from "fs"; import fs from "fs";
import path from "node:path";
import { migrate, db } from "../db.js"; import { migrate, db } from "../db.js";
import { getAppVersion, satisfiesRange } from "../app-version.js"; import { getAppVersion, satisfiesRange } from "../app-version.js";
import { import {
@@ -18,6 +19,7 @@ import {
uninstallPlugin, uninstallPlugin,
listInstalledPlugins, listInstalledPlugins,
createBlankPlugin, createBlankPlugin,
pluginDir,
} from "../plugin-store.js"; } from "../plugin-store.js";
import { installExamplePlugin } from "../plugin-install.js"; import { installExamplePlugin } from "../plugin-install.js";
import { import {
@@ -119,11 +121,58 @@ async function main() {
); );
assert.ok(meta); assert.ok(meta);
const extra = createBlankPlugin(
"extra-require-smoke",
`const extra = require("smoke-extra");
async function main() {
return { output: { n: extra.n } };
}
main.meta = {
description: "smoke extra require",
config: {},
input: {},
output: { n: { type: "number" } },
example: { data: {}, config: {} },
};
export default main;
`,
);
assert.equal(extra.scriptRef, "plugin/extra-require-smoke");
const extraPkg = path.join(
pluginDir("extra-require-smoke"),
"node_modules",
"smoke-extra",
);
fs.mkdirSync(extraPkg, { recursive: true });
fs.writeFileSync(
path.join(extraPkg, "package.json"),
`${JSON.stringify({ name: "smoke-extra", main: "index.js" })}\n`,
);
fs.writeFileSync(path.join(extraPkg, "index.js"), "module.exports = { n: 9 };\n");
clearScriptCache();
const extraSource = fs.readFileSync(
path.join(pluginDir("extra-require-smoke"), "script.js"),
"utf8",
);
const extraInspect = inspectScriptSource(
"plugin/extra-require-smoke",
extraSource,
);
assert.equal(extraInspect.metaError, null, extraInspect.metaError);
assert.equal(extraInspect.meta?.description, "smoke extra require");
const extraRun = await runScript(
"plugin/extra-require-smoke",
{ data: null, context: {}, config: null },
{ log: silent, workflowName: "smoke", owner: "default" },
);
assert.equal(extraRun.output.n, 9);
assert.ok(listInstalledPlugins().some((p) => p.id === "get-current-time")); assert.ok(listInstalledPlugins().some((p) => p.id === "get-current-time"));
uninstallPlugin("jsonata-smoke-fork"); uninstallPlugin("jsonata-smoke-fork");
uninstallPlugin("blank-smoke"); uninstallPlugin("blank-smoke");
uninstallPlugin("blank-smoke-copy"); uninstallPlugin("blank-smoke-copy");
uninstallPlugin("extra-require-smoke");
uninstallPlugin("get-current-time"); uninstallPlugin("get-current-time");
console.log("plugins-smoke: ok"); console.log("plugins-smoke: ok");