mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 08:24:46 +00:00
feat(plugins): prepare scaffolded packages for publishing
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+104
-100
@@ -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 <package>`. 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.
|
||||
|
||||
Reference in New Issue
Block a user