From 78f26b77bc10fea3785559e1cf7fdd039c119b1f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 17 Sep 2026 09:57:13 +0200 Subject: [PATCH] feat(plugins): prepare scaffolded packages for publishing --- .../app/e2e/browser/plugin-management.spec.ts | 245 +++++++++++------- .../cli/src/commands/plugin/scaffold.test.ts | 10 + packages/cli/src/commands/plugin/scaffold.ts | 10 + packages/protocol/src/messages.ts | 2 + .../protocol/src/plugin-requirements.test.ts | 2 +- public-docs/plugins/publishing.md | 204 ++++++++------- 6 files changed, 282 insertions(+), 191 deletions(-) diff --git a/packages/app/e2e/browser/plugin-management.spec.ts b/packages/app/e2e/browser/plugin-management.spec.ts index 738ff6b7a0..7e32f61350 100644 --- a/packages/app/e2e/browser/plugin-management.spec.ts +++ b/packages/app/e2e/browser/plugin-management.spec.ts @@ -330,6 +330,67 @@ test("installs, reloads, recovers, disables, and removes a trusted local plugin" page, pluginEnvironment, }, testInfo) => { + const directory = await installLocalPluginWithStatusExamples(page, pluginEnvironment, testInfo); + await reloadAndRecoverLocalPlugin(page, pluginEnvironment.client, directory); + await toggleRemoveAndReinstallLocalPlugin(page, directory); +}); + +test("installs a Git source after a failed source remains editable", async ({ + page, + pluginEnvironment, +}, testInfo) => { + const source = await prepareCompactGitInstall(page, pluginEnvironment); + await retryGitInstallAndInspectRows(page, source, testInfo); +}); + +for (const sourceSupport of [undefined, false]) { + test(`keeps installed plugins manageable without source capability (${sourceSupport})`, async ({ + page, + pluginEnvironment, + }) => { + const directory = await prepareLegacyPluginHost(page, pluginEnvironment, sourceSupport); + await verifyLegacyPluginManagement(page, directory); + }); +} + +for (const viewport of [ + { width: 1280, height: 900 }, + { width: 390, height: 844 }, +]) { + test(`installs an npm source and manages its row at ${viewport.width}px`, async ({ + page, + pluginEnvironment, + }, testInfo) => { + await prepareNpmPluginHost(page, pluginEnvironment, viewport); + await retryNpmInstallAndInspectRow(page, viewport.width, testInfo); + await reloadAndRemoveNpmPlugin(page); + }); +} + +async function openNpmPluginSettings(page: Page, width: number) { + if (width < 600) await openCompactPluginSettings(page); + else await openPluginSettings(page); +} + +async function expectSourceHierarchy(page: Page, description: string, source: string) { + const descriptionText = page.getByText(description, { exact: false }); + const sourceText = page.getByText(source, { exact: false }); + const descriptionSize = await descriptionText.evaluate((element) => + Number.parseFloat(getComputedStyle(element).fontSize), + ); + const sourceSize = await sourceText.evaluate((element) => + Number.parseFloat(getComputedStyle(element).fontSize), + ); + expect(sourceSize).toBeLessThan(descriptionSize); + await expect(sourceText).toHaveCSS("color", "rgb(161, 161, 170)"); + await expect(descriptionText).toHaveCSS("color", "rgb(113, 113, 122)"); +} + +async function installLocalPluginWithStatusExamples( + page: Page, + pluginEnvironment: PluginEnvironment, + testInfo: TestInfo, +) { const directory = await mkdtemp(path.join(pluginEnvironment.directory, "local-")); const disabledDirectory = await createDirectoryPlugin( pluginEnvironment.directory, @@ -385,6 +446,14 @@ test("installs, reloads, recovers, disables, and removes a trusted local plugin" await page.getByRole("button", { name: "Close", exact: true }).click(); await openContributionFromSettings(page, "Plugin v1", "Plugin v1 cleanup 0"); + return directory; +} + +async function reloadAndRecoverLocalPlugin( + page: Page, + client: PluginEnvironment["client"], + directory: string, +) { await openPluginSettings(page); await reloadPlugin(page); await openContributionFromSettings(page, "Plugin v1", "Plugin v1 cleanup 1"); @@ -422,7 +491,9 @@ test("installs, reloads, recovers, disables, and removes a trusted local plugin" await selectPluginAction(page, "e2e-plugin", "Reload"); await expect(page.getByLabel("e2e-plugin running")).toBeVisible(); await openContributionFromSettings(page, "Plugin v3", "Plugin v3 cleanup 5"); +} +async function toggleRemoveAndReinstallLocalPlugin(page: Page, directory: string) { await openPluginSettings(page); await page.getByRole("switch", { name: "e2e-plugin: Disable", exact: true }).click(); await expect(page.getByLabel("e2e-plugin disabled")).toBeVisible(); @@ -443,12 +514,9 @@ test("installs, reloads, recovers, disables, and removes a trusted local plugin" await expect(page.getByLabel("e2e-plugin running")).toBeVisible(); await gotoAppShell(page); await openContribution(page, "Plugin v3", "Plugin v3 cleanup 7"); -}); +} -test("installs a Git source after a failed source remains editable", async ({ - page, - pluginEnvironment, -}, testInfo) => { +async function prepareCompactGitInstall(page: Page, pluginEnvironment: PluginEnvironment) { const root = pluginEnvironment.directory; const repository = await createGitPluginRepository(root); const longDirectory = await createDirectoryPlugin( @@ -477,6 +545,16 @@ test("installs a Git source after a failed source remains editable", async ({ await page.getByRole("switch", { name: "Enable plugins" }).click(); await expect(page.getByText("Plugins enabled", { exact: true })).toBeVisible(); + return { repository, missingSource }; +} + +async function retryGitInstallAndInspectRows( + page: Page, + source: { repository: string; missingSource: string }, + testInfo: TestInfo, +) { + const { repository, missingSource } = source; + await installPlugin(page, missingSource); await expect(page.getByTestId("plugin-management-feedback")).toContainText( "Plugin source is neither an existing directory nor a Git URL", @@ -499,94 +577,81 @@ test("installs a Git source after a failed source remains editable", async ({ await waitForSettledPosition(removeAction); await capturePluginInstallForm(page, testInfo, "compact-menu"); await page.keyboard.press("Escape"); -}); +} -for (const sourceSupport of [undefined, false]) { - test(`keeps installed plugins manageable without source capability (${sourceSupport})`, async ({ +async function prepareLegacyPluginHost( + page: Page, + pluginEnvironment: PluginEnvironment, + sourceSupport: boolean | undefined, +) { + const directory = await createDirectoryPlugin( + pluginEnvironment.directory, + "legacy-source-plugin", + undefined, + "Legacy plugin", + ); + const { client } = pluginEnvironment; + await client.patchDaemonConfig({ pluginsEnabled: true }); + await client.installPluginSource({ source: directory }); + await advertisePluginCapabilities(page, { + pluginSourceInstallation: sourceSupport, + pluginGitManagement: true, + }); + await gotoAppShell(page); + await openPluginSettings(page); + + return directory; +} + +async function verifyLegacyPluginManagement(page: Page, directory: string) { + await expect( + page.getByText("Update this host to install plugins", { exact: true }), + ).toBeVisible(); + await expect(page.getByRole("textbox", { name: "Plugin source", exact: true })).toHaveCount(0); + await expect(page.getByText(directory, { exact: true })).toBeVisible(); + await selectPluginAction(page, "legacy-source-plugin", "Reload"); + await expect(page.getByText("Reloaded legacy-source-plugin", { exact: true })).toBeVisible(); +} + +async function prepareNpmPluginHost( + page: Page, + pluginEnvironment: PluginEnvironment, + viewport: { width: number; height: number }, +) { + const { client } = pluginEnvironment; + await page.setViewportSize(viewport); + await advertisePluginCapabilities(page, { pluginGitManagement: false }); + await client.patchDaemonConfig({ pluginsEnabled: true }); + await gotoAppShell(page); + await openNpmPluginSettings(page, viewport.width); +} + +async function retryNpmInstallAndInspectRow(page: Page, width: number, testInfo: TestInfo) { + await installPlugin(page, "npm:missing-plugin"); + await expect(page.getByTestId("plugin-management-feedback")).toContainText("404"); + await expect(page.getByLabel("Plugin source")).toHaveValue("npm:missing-plugin"); + await installPlugin(page, "npm:@paseo-fixture/review@^2.0.0"); + await expect(page.getByText("Installed npm-review", { exact: true })).toBeVisible(); + await expect(page.getByLabel("npm-review running")).toBeVisible(); + await expectSourceHierarchy( page, - pluginEnvironment, - }) => { - const directory = await createDirectoryPlugin( - pluginEnvironment.directory, - "legacy-source-plugin", - undefined, - "Legacy plugin", - ); - const { client } = pluginEnvironment; - await client.patchDaemonConfig({ pluginsEnabled: true }); - await client.installPluginSource({ source: directory }); - await advertisePluginCapabilities(page, { - pluginSourceInstallation: sourceSupport, - pluginGitManagement: true, - }); - await gotoAppShell(page); - await openPluginSettings(page); - await expect( - page.getByText("Update this host to install plugins", { exact: true }), - ).toBeVisible(); - await expect(page.getByRole("textbox", { name: "Plugin source", exact: true })).toHaveCount(0); - await expect(page.getByText(directory, { exact: true })).toBeVisible(); - await selectPluginAction(page, "legacy-source-plugin", "Reload"); - await expect(page.getByText("Reloaded legacy-source-plugin", { exact: true })).toBeVisible(); + "Installed from the npm fixture registry", + "npm:@paseo-fixture/review · 2.0.0", + ); + await expect( + page.getByText("Installed from the npm fixture registry", { exact: true }), + ).toBeVisible(); + await page.screenshot({ + path: testInfo.outputPath(`npm-plugin-${width}.png`), + animations: "disabled", }); } -for (const viewport of [ - { width: 1280, height: 900 }, - { width: 390, height: 844 }, -]) { - test(`installs an npm source and manages its row at ${viewport.width}px`, async ({ - page, - pluginEnvironment, - }, testInfo) => { - const { client } = pluginEnvironment; - await page.setViewportSize(viewport); - await advertisePluginCapabilities(page, { pluginGitManagement: false }); - await client.patchDaemonConfig({ pluginsEnabled: true }); - await gotoAppShell(page); - await openNpmPluginSettings(page, viewport.width); - await installPlugin(page, "npm:missing-plugin"); - await expect(page.getByTestId("plugin-management-feedback")).toContainText("404"); - await expect(page.getByLabel("Plugin source")).toHaveValue("npm:missing-plugin"); - await installPlugin(page, "npm:@paseo-fixture/review@^2.0.0"); - await expect(page.getByText("Installed npm-review", { exact: true })).toBeVisible(); - await expect(page.getByLabel("npm-review running")).toBeVisible(); - await expectSourceHierarchy( - page, - "Installed from the npm fixture registry", - "npm:@paseo-fixture/review · 2.0.0", - ); - await expect( - page.getByText("Installed from the npm fixture registry", { exact: true }), - ).toBeVisible(); - await page.screenshot({ - path: testInfo.outputPath(`npm-plugin-${viewport.width}.png`), - animations: "disabled", - }); - await selectPluginAction(page, "npm-review", "Reload"); - await expect(page.getByText("Reloaded npm-review", { exact: true })).toBeVisible(); - page.once("dialog", (dialog) => dialog.accept()); - await selectPluginAction(page, "npm-review", "Remove"); - await expect(page.getByText("Removed npm-review", { exact: true })).toBeVisible(); - await expect(page.getByLabel("npm-review running")).toHaveCount(0); - }); -} - -async function openNpmPluginSettings(page: Page, width: number) { - if (width < 600) await openCompactPluginSettings(page); - else await openPluginSettings(page); -} - -async function expectSourceHierarchy(page: Page, description: string, source: string) { - const descriptionText = page.getByText(description, { exact: false }); - const sourceText = page.getByText(source, { exact: false }); - const descriptionSize = await descriptionText.evaluate((element) => - Number.parseFloat(getComputedStyle(element).fontSize), - ); - const sourceSize = await sourceText.evaluate((element) => - Number.parseFloat(getComputedStyle(element).fontSize), - ); - expect(sourceSize).toBeLessThan(descriptionSize); - await expect(sourceText).toHaveCSS("color", "rgb(161, 161, 170)"); - await expect(descriptionText).toHaveCSS("color", "rgb(113, 113, 122)"); +async function reloadAndRemoveNpmPlugin(page: Page) { + await selectPluginAction(page, "npm-review", "Reload"); + await expect(page.getByText("Reloaded npm-review", { exact: true })).toBeVisible(); + page.once("dialog", (dialog) => dialog.accept()); + await selectPluginAction(page, "npm-review", "Remove"); + await expect(page.getByText("Removed npm-review", { exact: true })).toBeVisible(); + await expect(page.getByLabel("npm-review running")).toHaveCount(0); } diff --git a/packages/cli/src/commands/plugin/scaffold.test.ts b/packages/cli/src/commands/plugin/scaffold.test.ts index f4186449d2..d4d8a682cf 100644 --- a/packages/cli/src/commands/plugin/scaffold.test.ts +++ b/packages/cli/src/commands/plugin/scaffold.test.ts @@ -58,6 +58,16 @@ describe("plugin scaffold", () => { name: "hello-plugin", private: true, version: "0.0.0", + files: [ + "paseo-plugin.json", + "index.client.ts", + "index.client.tsx", + "index.server.ts", + "index.server.tsx", + "client/", + "server/", + "shared/", + ], scripts: { typecheck: "tsc --noEmit" }, devDependencies: { "@getpaseo/plugin": cliPackageJson.version, diff --git a/packages/cli/src/commands/plugin/scaffold.ts b/packages/cli/src/commands/plugin/scaffold.ts index d73f713d89..2046b0cd8e 100644 --- a/packages/cli/src/commands/plugin/scaffold.ts +++ b/packages/cli/src/commands/plugin/scaffold.ts @@ -147,6 +147,16 @@ export async function scaffoldPluginDirectory( name: id, private: true, version: "0.0.0", + files: [ + "paseo-plugin.json", + "index.client.ts", + "index.client.tsx", + "index.server.ts", + "index.server.tsx", + "client/", + "server/", + "shared/", + ], scripts: { typecheck: "tsc --noEmit" }, devDependencies: { "@getpaseo/plugin": version, diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 30a4bb0d36..5eb84d5850 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -3625,7 +3625,9 @@ export const ServerInfoStatusPayloadSchema = z pluginLogs: z.boolean().optional(), // COMPAT(pluginGitManagement): added in v0.7.0, remove gate after 2027-08-26. pluginGitManagement: z.boolean().optional(), + // COMPAT(pluginSourceInstallation): added in v0.8.0; remove gate after 2027-03-16 once daemon floor supports source identifiers. pluginSourceInstallation: z.boolean().optional(), + // COMPAT(pluginSourceUpdates): added in v0.8.0; remove gate after 2027-03-16 once daemon floor supports reviewed updates. pluginSourceUpdates: z.boolean().optional(), // COMPAT(pluginThemes): added in v0.5.0, remove gate after 2027-08-20. // A daemon that predates this flag keeps `addTheme` in the server bundle it compiles, diff --git a/packages/protocol/src/plugin-requirements.test.ts b/packages/protocol/src/plugin-requirements.test.ts index 9d47e68e1f..e679c8f5d5 100644 --- a/packages/protocol/src/plugin-requirements.test.ts +++ b/packages/protocol/src/plugin-requirements.test.ts @@ -4,7 +4,7 @@ import { assertPluginCompatibility, validatePluginRequirements } from "./plugin- describe.each(["daemon", "app"] as const)("plugin requirements on %s", (runtime) => { it("rejects legacy manifests on the first breaking release with migration instructions", () => { expect(() => assertPluginCompatibility({ id: "legacy", version: "0.8.0", runtime })).toThrow( - /legacy.*<0\.8\.0.*0\.8\.0.*https:\/\/paseo.sh\/docs\/plugins\/v0.8\/migration/, + /legacy.*<0\.8\.0.*0\.8\.0.*https:\/\/paseo.sh\/docs\/plugins\/migration/, ); }); diff --git a/public-docs/plugins/publishing.md b/public-docs/plugins/publishing.md index 2c19eedbd2..0e7f2f89cf 100644 --- a/public-docs/plugins/publishing.md +++ b/public-docs/plugins/publishing.md @@ -8,41 +8,27 @@ category: Plugins # Publish a plugin -Publish your plugin so other people can install and use it in Paseo. Once you have a working -[plugin project](/docs/plugins), choose where to share it: +Publish your plugin so other people can install and use it in Paseo. Start with a working +[plugin project](/docs/plugins), then choose where to share it: - [npm](#publish-on-npm): publish a package on the public npm registry. -- [Private registry](#use-a-private-github-package): share a package with your team using GitHub Packages. -- [GitHub or Git](#share-through-github-or-git): let users install directly from a repository. +- [GitHub or Git](#share-through-github-or-git): let users install from a repository. ## Publish on npm -Give your `package.json` a unique package name and version, and include the plugin files in the -published package. For a plugin with both client and server entries, add these fields to the -scaffold's existing `package.json`: +The scaffold prepares the package files and development dependencies. You choose the package name +and release version. -```json -{ - "name": "@acme/paseo-review", - "version": "1.0.0", - "private": false, - "files": [ - "paseo-plugin.json", - "index.client.tsx", - "index.server.ts", - "client/", - "server/", - "shared/" - ] -} +### 1. Set your package name and version + +From the plugin directory, set your package details and allow publication. Replace `@acme` with your npm scope: + +```bash +npm pkg set name=@acme/paseo-review version=1.0.0 +npm pkg delete private ``` -The scaffold sets `private: true`; change it to `false` to allow publication. -Replace `@acme` with your npm scope. Match the entry filenames to your project, and include any -additional assets your plugin reads. Keep the scaffold's development dependencies and scripts. -The package name identifies the npm source; `paseo-plugin.json` supplies the installed plugin ID. - -From the plugin directory, check and publish: +### 2. Check and publish ```bash npm run typecheck @@ -50,113 +36,131 @@ npm pack --dry-run npm publish --access public ``` -Inspect the pack output for the manifest, entries, imported files, and assets before publishing. -Then test the published package through Paseo. npm must be on the daemon host's `PATH` for installation: +Check the pack output includes any assets you added to the project. + +### 3. Test the published plugin + +On a daemon host with npm available: ```bash paseo plugin install npm:@acme/paseo-review@1.0.0 ``` -Users can paste `npm:@acme/paseo-review` into **Settings → Plugins → Plugin source** to install the -latest release. An explicit version selects that installation; it does not pin future updates. -See [source identifiers](/docs/plugins/reference#plugin-sources) for tags, ranges, and subdirectories. +Users can also paste `npm:@acme/paseo-review` into **Settings → Plugins → Plugin source**. -### Dependencies and generated files +:::example[Package configuration] -Put libraries your plugin needs at runtime in `dependencies`. Paseo uses npm to install those -libraries and their transitive dependencies. Development dependencies are omitted and automatic -peer dependency installation is disabled. Keep Paseo's host modules—the plugin SDK, React, React -Native, TanStack Query, and Zod—in `devDependencies` for authoring; Paseo supplies their runtime -instances. Declare other required peer modules as dependencies. - -Paseo compiles TypeScript and bundles imports separately for the client and server. You do not need -to precompile or bundle an ordinary TypeScript plugin. The entry names remain `index.client.ts` -or `.tsx` and `index.server.ts` or `.tsx`; at least one is required. npm's `main` and `exports` -fields do not select Paseo entries. - -If your plugin generates code or assets, generate them before publishing and include the output -in `files`. Prebuilt JavaScript can live under `client/`, `server/`, or `shared/`, imported by the -normal TypeScript entry. Preserve the [runtime boundaries](/docs/plugins/reference#project-files) -and host module imports. Paseo still compiles the entries. - -npm lifecycle scripts, including dependency `install`, `postinstall`, and `prepare` scripts, do not -run when Paseo acquires a package. Publish ready-to-use files where possible. If installation needs -host-specific preparation, such as rebuilding a native dependency, declare it in the manifest's -[`build` commands](/docs/plugins/reference#cli-reference). These commands run on installation and -update after the Paseo requirements check. An npm plugin does not need a build command to install -its ordinary dependencies. - -## Use a private GitHub package - -Paseo uses the daemon user's npm configuration and environment for registries and authentication. -For a company plugin, publish a scoped package such as `@acme/paseo-review` to GitHub Packages. -Add the repository and publication registry to its `package.json`: +The scaffold includes this `files` list in `package.json`: ```json { - "name": "@acme/paseo-review", - "repository": { - "type": "git", - "url": "https://github.com/acme/paseo-review.git" - }, - "publishConfig": { - "registry": "https://npm.pkg.github.com" - } + "files": [ + "paseo-plugin.json", + "index.client.ts", + "index.client.tsx", + "index.server.ts", + "index.server.tsx", + "client/", + "server/", + "shared/" + ] } ``` -Authenticate with GitHub Packages and run `npm publish` from the plugin directory. Follow -[GitHub's npm registry guide](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry) -for publishing credentials and package access permissions. +- Add any assets stored outside these directories to `files`. +- Keep the scaffold's SDK and host libraries in `devDependencies`. +- Add other runtime libraries with `npm install `. Paseo installs their dependencies too. +- The npm package name identifies the source. The manifest's `id` identifies the installed plugin. -To install the private package, configure npm **on the daemon host, as the user running Paseo**. -Add the scope mapping to that user's `~/.npmrc`: +See the [project reference](/docs/plugins/reference#project-files) for entry points and runtime boundaries. -```ini -@acme:registry=https://npm.pkg.github.com -``` +::: -Log in with a GitHub personal access token (classic) with `read:packages` and access to the package: +### Plugins with a build step + +Paseo compiles TypeScript. An ordinary plugin needs no separate build before publication. +If your plugin generates files, include the generated output in the package. + +**Installation scripts do not run automatically.** If a dependency needs host-specific setup, +declare a [preparation command](/docs/plugins/reference#cli-reference). + +:::example[Generated files and dependencies] + +- Generate code and assets before running `npm publish`. +- Keep generated JavaScript in its runtime directory and import it from the TypeScript entry. +- Include generated assets in `files`. +- Keep host-provided modules external when producing your own bundle. +- Remove Git-only dependency-install commands from the published manifest; npm installation already + installs production dependencies. + +Paseo skips npm lifecycle scripts during installation, including dependency scripts. For example, +a native dependency that needs rebuilding requires an explicit preparation command. + +::: + +:::example[Publish a private package with GitHub Packages] + +You can publish a company plugin to GitHub Packages. Follow the npm steps above, using your +organization's scope, and replace the publish command with: ```bash -npm login --scope=@acme --auth-type=legacy --registry=https://npm.pkg.github.com -paseo plugin install npm:@acme/paseo-review +npm publish --registry=https://npm.pkg.github.com ``` -Use your GitHub username and the token as the password. Keep credentials on the daemon host; -enter only the source identifier in the app. Updates use the host's current npm configuration too. -Other npm-compatible registries use the same npm configuration mechanism. +[Configure GitHub authentication and package access](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry) +before publishing. + +To install the plugin, configure npm **on the daemon host, as the user running Paseo**: + +1. Add your organization's registry to `~/.npmrc`: + + ```ini + @acme:registry=https://npm.pkg.github.com + ``` + +2. Log in with your GitHub username and a personal access token (classic) as the password. + The token needs `read:packages` and access to the package. + + ```bash + npm login --scope=@acme --auth-type=legacy --registry=https://npm.pkg.github.com + ``` + +3. Install the plugin: + + ```bash + paseo plugin install npm:@acme/paseo-review + ``` + +Paseo uses the host's npm registry settings and credentials for installation and updates. +In the app, enter only the source identifier. + +::: ## Share through GitHub or Git -You can also share a repository containing the plugin project: +Push the plugin project to a repository. Users can install it with: ```bash paseo plugin install github:acme/paseo-review +``` + +For another Git host: + +```bash paseo plugin install git:https://git.example.com/acme/paseo-review.git ``` -For a plugin that only imports host modules, no preparation is needed. If it has runtime npm -dependencies, commit `package.json` and `package-lock.json`, then add this to `paseo-plugin.json`: +If your plugin has runtime npm dependencies, commit `package-lock.json` and add a preparation +command to `paseo-plugin.json`: ```json { - "id": "paseo-review", - "requirements": { "paseo": ">=0.8.0" }, "build": [["npm", "ci", "--omit=dev"]] } ``` -`npm ci` installs from the committed lockfile and fails if it disagrees with `package.json`. -`--omit=dev` leaves authoring tools out of the installation. Paseo runs the command in the plugin -directory on the daemon host, where npm must be on `PATH`. Git installation does not infer this -command from the presence of a package file. +- `npm ci` installs the versions in the committed lockfile. +- `--omit=dev` excludes development tools. +- npm must be available on the daemon host. -If additional preparation needs development tools, `--omit=dev` will not install those tools. -Prefer generating and publishing the required files on npm. If you distribute the same project on -npm and Git, omit the dependency-install build command from the published manifest: npm acquisition -already installs production dependencies. - -Set `requirements.paseo` to the earliest release whose APIs your plugin uses. `>=0.8.0` permits -compatible later releases, including 0.9; add an upper bound only for a known incompatibility. -See [requirements](/docs/plugins/reference#requirements). +Plugins that only use Paseo's host libraries need no preparation command.