npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@epinova/opticli

v0.32.0

Published

Typed Node API and Playwright fixtures for opticli: start an Optimizely CMS site, seed test content and clean up after the tests

Readme

opticli for Node.js and Playwright

Typed access to opticli from Node.js, and Playwright fixtures that use it: start an Optimizely CMS 12 or 13 site, seed the content the tests need, give each test content of its own, and clean up after. Writes go through the CMS (versions, cache, events), so a page shows a change at once.

Install

npm install -D @epinova/opticli

The package runs the opticli CLI; it doesn't install it. Its version is the opticli version it was released with, and it needs that opticli or newer: pin it in the site's repository with a local tool manifest, so CI gets the same version:

dotnet new tool-manifest
dotnet tool install OptiCli

or install it globally (dotnet tool install -g OptiCli). opticli's own rules apply: the site runs against your local or chosen development database (opticli doctor shows which), never production.

Playwright

A seed plan: content every test can read. It is an opticli plan that setup applies with --update-existing on every run, so the first run creates it and later runs only repair what changed. Give it a fixed guidNamespace and an id on each step that creates something.

// e2e/seed.ts
import type { Plan } from '@epinova/opticli';

export const seedPlan = {
  guidNamespace: '0d6a8a5e-2f7b-4c1e-9b0a-5c3f1e7d2a61', // any GUID; keep it
  operations: [
    { op: 'create', id: 'root', parent: '/en/', type: 'StandardPage', name: 'Test fixtures', properties: { PageVisibleInMenu: false } },
    { op: 'upload', id: 'image', file: 'files/teaser.png', for: '$root' },
    { op: 'create', id: 'article', parent: '$root', type: 'ArticlePage', name: 'Seeded article', properties: { MainBody: '<p>Hello</p>' } },
  ],
} as const satisfies Plan;
// e2e/global-setup.ts
import { defineSetup } from '@epinova/opticli/playwright';
import { seedPlan } from './seed.ts';

export default defineSetup({ plan: seedPlan, planDir: import.meta.dirname });
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import type { OptiCliWorkerOptions } from '@epinova/opticli/playwright';

export default defineConfig<{}, OptiCliWorkerOptions>({
  testDir: './e2e',
  globalSetup: './e2e/global-setup.ts',
  use: {
    // Each worker's test content goes below a hidden page of its own, below the seed plan's root.
    opticliSandbox: { parent: '$root', type: 'StandardPage', properties: { PageVisibleInMenu: false } },
  },
});
// e2e/fixtures.ts: seed.refs and seed.urls typed by the plan's step ids
import { createTest } from '@epinova/opticli/playwright';
import type { seedPlan } from './seed.ts';

export const test = createTest<typeof seedPlan>();
export { expect } from '@epinova/opticli/playwright';
// e2e/article.spec.ts
import { expect, test } from './fixtures.ts';

test('the seeded article', async ({ page, seed }) => {
  await page.goto(seed.urls.article!);
  await expect(page.getByText('Hello')).toBeVisible();
});

test('a draft is not visible until it is published', async ({ page, cms }) => {
  const created = await cms.page('StandardPage', 'Draft test', { MainBody: '<p>Published text</p>' });
  const draft = await cms.set(created.ref, { MainBody: '<p>Draft text</p>' });

  await page.goto(created.url);
  await expect(page.getByText('Draft text')).toHaveCount(0);

  await cms.publish(draft.version!);
  await page.reload();
  await expect(page.getByText('Draft text')).toBeVisible();
});

What setup does

defineSetup() is the globalSetup. It starts the site with opticli serve (or reuses one that runs already: serve, or the site your IDE runs in IDE mode), checks that opticli and the site's agent are new enough, applies the seed plan, and points baseURL at the site. After the tests it stops a site it started, and leaves one that ran before. The seeded content stays for the next run.

Setup logs what the seed plan did: unchanged, or which steps changed and which of their properties. A step that changes on every run has a value the CMS stores differently from how the plan gives it; write it as the CMS stores it (the log's step, opticli get).

For a site that redirects HTTP to HTTPS, set https: true. The address is https://localhost:<port> with the ASP.NET Core development certificate, which a browser may not trust: add ignoreHTTPSErrors: true to use in playwright.config, or trust it (dotnet dev-certs https --trust).

| Option | Default | | |---|---|---| | plan | none | The seed plan. | | planDir | the working directory | Where the plan's files are (upload, "@file" values); they must be inside it. | | publish | true | Publish what the plan saves. | | https | OPTICLI_HTTPS=1, else "https" in opticli's user config | Also serve the site on https://localhost:<port> (serve --https) and browse it there, for a site that redirects HTTP to HTTPS. A site that runs already without an HTTPS address fails setup: stop it first. | | httpsPort | OPTICLI_HTTPS_PORT, else "httpsPort" in opticli's user config | That HTTPS address's port (serve --https-port, implies https): fixed, so site hosts with the port (alloy.localhost:5001) are right on every run. A taken port fails setup. | | build | OPTICLI_BUILD=1 | Build the site before starting it (serve --build). | | keepSite | OPTICLI_KEEP_SITE=1 | Leave a site that setup started running. | | opticli | see below | command, project, cwd, profile. |

Fixtures

| Fixture | Scope | | |---|---|---| | seed | worker | refs and urls of the plan's steps, by id; baseURL; sites. A URL is a path on baseURL (/en/fixtures/), except for content on another site that the served site answers on a host of its own: then that site's address (http://alloy-sv.localhost:5199/sv/news/, see Several sites). sites maps each such site's name to where to open it. | | cms | test | Content for one test: page(type, name, properties?, { parent?, publish?, keep? }) (published by default; returns ref, version and url, as seed.urls gives it), set, publish, apply(plan, { keep?, ... }). What page and apply create goes to the recycle bin after the test, unless keep. | | sandbox | worker | With the opticliSandbox option: the ref of the worker's own page, where cms.page() creates content. It goes to the recycle bin when the worker ends, with anything a failed test left. | | opticli | worker | The OptiCli client, for anything else. | | baseURL | test | The site opticli serve runs. |

opticliSandbox: parent (a ref, or $id of a seed step), type (a page type the parent allows and that allows your test pages), properties. Without it, pass { parent } to cms.page().

What stays, and what goes

| | After the test run | |---|---| | The seed plan | Stays. The next run repairs what changed. | | cms.page(), cms.apply() | Go after the test. With keep: true, stay. | | cms.set(), cms.publish() | Stay: they change content the test didn't make. On seeded content the next run's seed puts it back, but tests in other workers of the same run see the change. | | The sandbox | Goes when the worker ends, with everything below it, whatever made it. | | The opticli fixture, the Node.js client | Stay: they never remove anything (except below the sandbox, as above). |

keep: true needs a parent outside the sandbox and outside content the test made, since that goes with everything below it; cms refuses it there before saving anything. A step of cms.apply() that found its content already there (updateExisting) leaves it, with or without keep.

Deleted content goes to the recycle bin, as in the CMS: nothing is deleted for good. On a local database that is usually fine; empty the recycle bin in the CMS now and then.

A database built from scratch

A database the CMS created itself has no sites and only en and sv enabled. The seed plan can add both: a language step before the content in that language, and a site step after its start page (setup applies with updateExisting, so later runs leave the site as it is). Give the hosts the ports the site runs on, fixed with opticli serve's port and httpsPort:

export const seedPlan = {
  guidNamespace: '3f6d2a9e-6c1b-4f7e-9a51-0d2b8c4e7f10',
  operations: [
    { op: 'language', lang: 'no' },
    { op: 'create', id: 'home', parent: '1', type: 'StartPage', name: 'Hjem', lang: 'no' },
    { op: 'site', name: 'Alloy', startPage: '$home', hosts: ['localhost:5199', 'alloy-en.localhost:5199@en', 'https://localhost:5443'] },
  ],
} as const satisfies Plan;

The site gets an assets folder of its own (SysSiteAssets below the start page, at /siteassets/), as one made in admin mode does; siteAssets: 'global' keeps it on the global assets folder.

See opticli's A database built from scratch.

Several sites

opticli serve runs the site on one address, and baseURL is that address: a path on it opens the site that answers it, the one with the host localhost:<port> (or *). Another site answers on a host of its own. Browsers send *.localhost names to this machine, so a site with the host alloy-sv.localhost:5199, on a port serve listens on (its HTTP port, or httpsPort), needs no hosts file. Setup gives content on such a site its own address:

test('the Swedish site has its own start page', async ({ page, seed }) => {
  await page.goto(seed.urls.svArticle!); // http://alloy-sv.localhost:5199/sv/nyheter/
  await page.goto(seed.sites['Alloy Sverige']!); // http://alloy-sv.localhost:5199/
});

Content on the site at baseURL keeps its path, and so does content whose site has only hosts that don't answer here (a production name, another port). A plan that creates its sites after their content still gives the content its own site's address: after a site step, opticli reports every step's URL as the sites have it at the end. Each site is a separate origin for the browser, with cookies of its own.

Node.js

import { OptiCli, OptiCliError } from '@epinova/opticli';

const cms = new OptiCli({ project: '../src/Site' });
const page = await cms.create('/en/', 'StandardPage', 'Hello', { MainBody: '<p>Hi</p>' }, { publish: true });
console.log(page.url); // /en/hello/

try {
  await cms.get('999999');
} catch (e) {
  if (e instanceof OptiCliError && e.code === 'not_found') console.log(e.hint);
}

The client changes what you tell it to and removes nothing on its own, so it suits scripts that make content to keep. Typed methods: serve, serveStatus, serveStop, sites, get, url, create, set, publish, delete, apply. run(args) runs any other command and returns its JSON envelope. A failure throws OptiCliError with opticli's code, exitCode, hint, details and version; see opticli's exit codes.

Every call runs opticli once (about a second, most of it .NET starting): fine for setup, and a plan (apply) does many changes in one call.

Which opticli, which site

| | | |---|---| | command option or OPTICLI | The opticli to run: an executable, or a checkout's opticli.dll (run with dotnet). Otherwise dotnet tool run opticli when a tool manifest above the working directory has it, else opticli on PATH. | | project option or OPTICLI_PROJECT | The site's .csproj or folder, relative to cwd (default: the working directory). Otherwise opticli finds it from the working directory. | | profile option or OPTICLI_PROFILE | A launch profile (--profile), passed to every call: it picks the database, and serve runs the site with the profile's environment variables too (except those serve sets itself). The workers use the same. |

apply runs in baseDir (planDir in setup), so a tool manifest must be in or above that folder too.

Licence

MPL-2.0, as opticli.