@arraypress/extension-kit 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -14
- package/bin/extension-kit.js +46 -10
- package/package.json +1 -1
- package/src/build/index.d.ts +4 -2
- package/src/build/index.js +6 -2
- package/src/build/store-frame.d.ts +176 -0
- package/src/build/store-frame.js +241 -0
- package/src/build/test-build.d.ts +85 -0
- package/src/build/test-build.js +82 -0
- package/src/build/toolbar-icons.d.ts +36 -18
- package/src/build/toolbar-icons.js +91 -24
- package/src/build/vendor.d.ts +78 -8
- package/src/build/vendor.js +133 -10
- package/src/build/zip.d.ts +3 -2
- package/src/build/zip.js +6 -4
- package/src/dom/focus.d.ts +28 -0
- package/src/dom/focus.js +36 -0
- package/src/dom/setup.d.ts +54 -0
- package/src/dom/setup.js +46 -0
- package/src/index.d.ts +29 -2
- package/src/index.js +23 -2
- package/src/sites/sites.d.ts +82 -0
- package/src/sites/sites.js +128 -0
- package/src/styles/chrome-ui.css +18 -7
- package/src/testing/browser.d.ts +52 -0
- package/src/testing/browser.js +79 -0
- package/src/testing/e2e.d.ts +18 -5
- package/src/testing/e2e.js +17 -5
- package/src/testing/index.d.ts +15 -4
- package/src/testing/index.js +15 -4
- package/src/testing/package.d.ts +62 -4
- package/src/testing/package.js +39 -8
- package/src/testing/pool.d.ts +23 -0
- package/src/testing/pool.js +33 -0
- package/src/testing/saved-groups.d.ts +33 -0
- package/src/testing/saved-groups.js +222 -0
- package/src/testing/time.d.ts +35 -0
- package/src/testing/time.js +46 -0
- package/src/testing/warnings.d.ts +57 -0
- package/src/testing/warnings.js +69 -0
- package/src/ui/menu.d.ts +257 -0
- package/src/ui/menu.js +283 -0
- package/src/ui/site-list.d.ts +199 -0
- package/src/ui/site-list.js +194 -0
- package/src/ui/text-dialog.d.ts +132 -0
- package/src/ui/text-dialog.js +187 -0
- package/src/ui/toast.d.ts +89 -0
- package/src/ui/toast.js +103 -0
package/README.md
CHANGED
|
@@ -10,8 +10,13 @@ The pieces ArrayPress's Chrome extensions share, so each is written once:
|
|
|
10
10
|
service worker shows the matching icon.
|
|
11
11
|
- **About links**: README, Issues and PRIVACY.md in the extension's GitHub repository, plus
|
|
12
12
|
package-time warnings for placeholder links.
|
|
13
|
-
- **
|
|
14
|
-
|
|
13
|
+
- **Chrome's page components**: the ⋮ action menu and its row button, the one-field dialog
|
|
14
|
+
("Rename folder"), the toast with Undo and a settings card's site list, built on the
|
|
15
|
+
stylesheet's classes; they work in a content script's shadow root too.
|
|
16
|
+
- **Sites**: what people type into a per-site list, and whether a page is on one.
|
|
17
|
+
- **Build and test helpers** (Node): icon rendering, vendoring npm packages (with a staleness
|
|
18
|
+
check), test builds, store screenshots, zipping for the Chrome Web Store, a fake
|
|
19
|
+
`chrome.storage`, Playwright helpers with time limits, packaging checks, and a copied-code guard.
|
|
15
20
|
|
|
16
21
|
Zero dependencies. Every runtime function takes the `chrome` APIs it uses as arguments, so tests run
|
|
17
22
|
the real logic against fakes.
|
|
@@ -50,6 +55,22 @@ import { startToolbarTheme } from '../vendor/extension-kit/index.js';
|
|
|
50
55
|
The copy is made from the installed package every `npm install`, so it can't drift from the
|
|
51
56
|
version in `package.json`. The zip helper leaves its `.d.ts` files out.
|
|
52
57
|
|
|
58
|
+
Other libraries are copied the same way, by name (a path ending in `/` copies a folder's contents):
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"scripts": {
|
|
63
|
+
"postinstall": "extension-kit vendor extension/vendor/extension-kit && extension-kit vendor extension/vendor/clean-link @arraypress/clean-link src/",
|
|
64
|
+
"test": "extension-kit vendor-check && eslint . && …"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`npm install some-package` doesn't run the extension's own `postinstall`, so a copy can go stale
|
|
70
|
+
without a word. Each copy is stamped with its package's version (`vendored.json`, left out of the
|
|
71
|
+
zip), and `extension-kit vendor-check` fails, naming the copy, until `npm run postinstall` remakes
|
|
72
|
+
it. From a script: `vendorPackage(name, files, dest)`, `checkVendored(dir)`.
|
|
73
|
+
|
|
53
74
|
### An extension with a bundler
|
|
54
75
|
|
|
55
76
|
Import `@arraypress/extension-kit` directly and copy the stylesheet into the build from
|
|
@@ -111,14 +132,17 @@ call `createToolbarTheme({storage, action, extensionId})` in the worker and pass
|
|
|
111
132
|
the reports, which the content script sends with `reportColorScheme(media, stored,
|
|
112
133
|
schemeSender(chrome.runtime))`.
|
|
113
134
|
|
|
114
|
-
Render the glyphs from `icons/toolbar/glyph.svg` (and optional pixel-fitted `glyph-16.svg`)
|
|
135
|
+
Render the glyphs from `icons/toolbar/glyph.svg` (and optional pixel-fitted `glyph-16.svg`), and
|
|
136
|
+
the extension's own icon from `icons/icon.svg` to `icon-16.png` … `icon-128.png`, with the
|
|
137
|
+
extension's Playwright:
|
|
115
138
|
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
import { renderToolbarIcons } from '@arraypress/extension-kit/build';
|
|
119
|
-
await renderToolbarIcons({ chromium, dir: 'extension/icons/toolbar' });
|
|
139
|
+
```json
|
|
140
|
+
{ "scripts": { "icons": "extension-kit icons", "toolbar-icons": "extension-kit toolbar-icons" } }
|
|
120
141
|
```
|
|
121
142
|
|
|
143
|
+
(`extension-kit toolbar-icons src/icons/toolbar` for another folder; from a script,
|
|
144
|
+
`renderIcons` and `renderToolbarIcons({chromium, dir})`.)
|
|
145
|
+
|
|
122
146
|
Colours are Chrome 157's toolbar icon colours, #474747 / #C7C7C7 (sources in the file).
|
|
123
147
|
|
|
124
148
|
## About links
|
|
@@ -132,6 +156,117 @@ At the end of `npm run package`, `printReleaseChecks(ABOUT_LINKS, REPOSITORY)` w
|
|
|
132
156
|
link is a placeholder (`isPlaceholder`: example.com/.org/.net) and reminds you the repository must
|
|
133
157
|
be public before the store listing goes live.
|
|
134
158
|
|
|
159
|
+
## Page start-up
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
const t = setUpPage(chrome, { title: 'optionsTitle', substitutions: [name] });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Reports the page's colour scheme for the toolbar glyph (`watchColorScheme`; a failure is only
|
|
166
|
+
logged), fills its `data-i18n` text (`localize`), sets its title from a message, and returns the
|
|
167
|
+
page's `t`.
|
|
168
|
+
|
|
169
|
+
## Sites
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
siteOrigin('example.com'); // 'https://example.com', what Chrome grants access to
|
|
173
|
+
siteHost(' https://www.GitHub.com/a '); // 'github.com', what a per-site list keeps
|
|
174
|
+
onSite(siteHost(location.href), neverSites); // on a listed site or a subdomain of one
|
|
175
|
+
hostPatterns('github.com'); // ['*://github.com/*', '*://*.github.com/*'] (excludeMatches)
|
|
176
|
+
originPattern('https://bit.ly/x'); // 'https://bit.ly/*' (permissions.request)
|
|
177
|
+
siteLabel('https://example.com'); // 'example.com', as a list shows it
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
A site is http or https with a dot in its host (or `localhost`); text without a scheme is https;
|
|
181
|
+
`localhost:8080` is a host and port. Pure, so a content script can load `sites/sites.js` alone.
|
|
182
|
+
|
|
183
|
+
## Page components
|
|
184
|
+
|
|
185
|
+
Chrome's own components, made in the page by script (no markup to copy), styled by `chrome-ui.css`.
|
|
186
|
+
Each takes `doc` (default `document`); the menu and the toast also take `root`, an element or a
|
|
187
|
+
shadow root to go in (a content script's, so the page's styles never reach them), and follow the
|
|
188
|
+
focus through shadow roots. The wording defaults to Chrome's English and takes a translation.
|
|
189
|
+
|
|
190
|
+
### ⋮ menu (cr-action-menu)
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
import { createMenu, moreActionsButton } from '../vendor/extension-kit/index.js';
|
|
194
|
+
|
|
195
|
+
const menu = createMenu(); // one <dialog class="menu" role="menu"> for the page
|
|
196
|
+
row.append(moreActionsButton(workspace.name, {
|
|
197
|
+
onClick: (anchor) => menu.open([
|
|
198
|
+
{ label: 'Rename', run: () => rename(workspace) },
|
|
199
|
+
'separator',
|
|
200
|
+
{ label: 'Delete', run: () => remove(workspace), disabled: locked },
|
|
201
|
+
], { anchor }),
|
|
202
|
+
}));
|
|
203
|
+
// A right-click: menu.open(items, { x: event.clientX, y: event.clientY, focusTo: row });
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
It opens modally over its button, as Chrome's does (its top at the button's top, its end at the
|
|
207
|
+
button's end), moved to stay 8px inside the window. A click outside, Esc, Tab or the window losing
|
|
208
|
+
focus closes it; ↑/↓ (wrapping, past disabled items), Home and End move; focus goes back to the
|
|
209
|
+
button (or `focusTo`); an item runs once the menu has closed. `createMenu({id, placement: 'below'})`
|
|
210
|
+
opens it below the button instead. `menuPosition` and `isInside` are the pure geometry.
|
|
211
|
+
|
|
212
|
+
`moreActionsButton(name, options)` is the 32px ⋮ icon button, labelled "More actions for “name”"
|
|
213
|
+
(`label` for a translation), with `aria-haspopup="menu"`; its icon is the stylesheet's
|
|
214
|
+
`.icon-more-vert` (`--icon-more-vert`, cr:more-vert), so the page needs no sprite. `className`,
|
|
215
|
+
`tooltip` (a `title`) and `focusable: false` (out of the tab order, for a row that takes the focus)
|
|
216
|
+
cover the rest.
|
|
217
|
+
|
|
218
|
+
### One-field dialog (cr-dialog)
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
const name = await askText({ title: 'Rename workspace', label: 'Name', value: workspace.name, maxLength: 200 });
|
|
222
|
+
const site = await askText({
|
|
223
|
+
title: 'Add site', label: 'Site', action: 'Add',
|
|
224
|
+
validate: (text) => (siteFromInput(text) ? null : 'Enter a site, like example.com'),
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Resolves to the text, trimmed, or `null` when cancelled. The action button (default "Save") stays
|
|
229
|
+
disabled while the text is blank or `validate` gives a reason, which shows under the field
|
|
230
|
+
(`checkText` is that rule, pure). Enter presses the action button; Esc and Cancel cancel; the
|
|
231
|
+
focus goes back to what had it, or to `returnTo` (an element, or a function asked at the end).
|
|
232
|
+
One `#text-dialog` per page, made on first use.
|
|
233
|
+
|
|
234
|
+
### Toast with Undo (cr-toast)
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
const toast = createToast(); // at load, so screen readers know the live region
|
|
238
|
+
toast.show(`Deleted “${workspace.name}”`, () => restore(workspace));
|
|
239
|
+
toast.show('Copied'); // no Undo
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`#toast` (`role="status"`), bottom left, a manual `popover` so it's in the top layer, over
|
|
243
|
+
anything the page stacks (`:popover-open` while it shows). A new message replaces the last and
|
|
244
|
+
restarts the wait: 10 seconds, as chrome://bookmarks's (`TOAST_MS`), or `duration` /
|
|
245
|
+
`undoDuration`. Undo hides it and runs once. `undoLabel` translates the button.
|
|
246
|
+
|
|
247
|
+
### Site list
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// A short list with an add row (chrome://settings), the markup in the page:
|
|
251
|
+
// <ul class="list" id="sites"></ul> <p class="row text" id="no-sites">…</p>
|
|
252
|
+
// <form class="row add" id="add-site"><input type="text" …><button type="submit">Add</button></form>
|
|
253
|
+
const sites = createSiteList({
|
|
254
|
+
list: byId('sites'), empty: byId('no-sites'), form: byId('add-site', HTMLFormElement),
|
|
255
|
+
parse: siteOrigin,
|
|
256
|
+
add: async (origin) => (await chrome.permissions.request({ origins: [originPattern(origin)] })) && save(origin),
|
|
257
|
+
remove: (origin) => unsave(origin),
|
|
258
|
+
words: { remove: t('optionsRemove'), removeLabel: (site) => t('optionsRemoveLabel', [site]) },
|
|
259
|
+
});
|
|
260
|
+
sites.render(settings.sites);
|
|
261
|
+
|
|
262
|
+
// Memory Saver's way (chrome://settings/performance): an Add button and the dialog, ⋮ Edit/Remove.
|
|
263
|
+
createSiteList({ list, empty, addButton: byId('add-site'), parse: siteHost, add, remove, replace: (old, edited) => … });
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`add` resolving to false keeps the typed text (access refused); `invalid(text)` hears what isn't
|
|
267
|
+
a site in the add row; the dialog keeps its Add off with “Enter a site, like example.com” under
|
|
268
|
+
the field. `words` translates `SITE_LIST_WORDS`.
|
|
269
|
+
|
|
135
270
|
## Build helpers (`@arraypress/extension-kit/build`)
|
|
136
271
|
|
|
137
272
|
| Export | What it does |
|
|
@@ -139,7 +274,12 @@ be public before the store listing goes live.
|
|
|
139
274
|
| `zipExtension(from, to, exclude?)` | Zip a folder for the Chrome Web Store, files only, less `ZIP_EXCLUDE` (icon SVG sources, `.d.ts`, `.DS_Store`). |
|
|
140
275
|
| `shipsFile(file)` | Whether a file ships, by the same rules. |
|
|
141
276
|
| `vendorKit(dest)` | Copy the browser half into an extension (the `extension-kit vendor` command). |
|
|
142
|
-
| `
|
|
277
|
+
| `vendorPackage(name, files, dest)`, `packageDir(name)` | Copy files of an installed package, stamped (`extension-kit vendor <dir> <package> <path>...`). |
|
|
278
|
+
| `checkVendored(dir)` | The stale copies under `dir` (`extension-kit vendor-check`). |
|
|
279
|
+
| `renderIcons`, `renderToolbarIcons({chromium, dir})` | See above. |
|
|
280
|
+
| `buildTestCopy({from, to, hook, manifest})` | A test build: the extension, `hook` as its service worker (`e2e-hook.js`), the manifest changed. |
|
|
281
|
+
| `testBuildDir(root, name)` | `.test-builds/<name>`, or under `$EXTENSION_TEST_BUILDS` (to test another build, or run two side by side). |
|
|
282
|
+
| `storeFrame` | Store screenshots: `canvas`, `windowChrome`, `geometry`, `letterIcon`, `cursor`, `THEMES`, `W`×`H`. |
|
|
143
283
|
| `placeholderWarning`, `repositoryReminder`, `printReleaseChecks` | See About links. |
|
|
144
284
|
| `findDisallowedClones({runJscpd})` | Run jscpd and return clones `.jscpd-allowlist.json` doesn't cover (the `extension-kit duplication` command). |
|
|
145
285
|
|
|
@@ -159,12 +299,20 @@ except those listed, each with a reason:
|
|
|
159
299
|
| `fakeChromeStorage({local, sync, session, now})` | In-memory `chrome.storage`: JSON copies, `onChanged` for real changes only, sync's 8,192-byte item quota and 120 writes a minute. `settled()` waits for listeners. |
|
|
160
300
|
| `launchExtension(chromium, dir, options)` | Load an unpacked extension into a fresh profile; `{context, worker, extensionId, url(page), profile, close()}`. |
|
|
161
301
|
| `extensionProfile(chromium, dir, options)` | A profile to start the browser on more than once (a restart, a first start with a changed manifest): `{dir, launch(), remove()}`. |
|
|
162
|
-
| `checkRunner()` | Named checks that report and go on: `check(name, fn)`, `fail(name)` for an error found outside a check, `finish()`. |
|
|
302
|
+
| `checkRunner({timeout})` | Named checks that report and go on: `check(name, fn, {timeout})`, `fail(name)` for an error found outside a check, `finish()`. A check that runs past its limit (`CHECK_MS`, 60 s) fails, and the run goes on. |
|
|
303
|
+
| `within(promise, ms, fallback)` | The value in time, or the fallback (late or failed); never rejects; its timer cleared. |
|
|
304
|
+
| `eachLimited(items, limit, work)` | Work through a list at most `limit` at once, `work(item, slot, index)`. |
|
|
305
|
+
| `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
|
|
306
|
+
| `closeOnSignal(close)` | On Ctrl-C or a kill, close the run's browsers, then exit. |
|
|
307
|
+
| `testBuildDir(root, name)` | As in `/build`, for the tests that load the build. |
|
|
163
308
|
| `serve(routes, {port})` | Serve test pages on 127.0.0.1 from a table, a `Map` or a function of the path; `{origin, port, server, close()}`. |
|
|
164
309
|
| `until(fn, what, timeout?)` | Poll until truthy. |
|
|
165
310
|
| `offscreenEmulator(cdp)` | Emulate light/dark inside the offscreen document. |
|
|
166
311
|
| `recordIconCalls(worker)` | Record `chrome.action.setIcon` calls Chrome accepted. |
|
|
167
|
-
| `
|
|
312
|
+
| `installWarnings(worker, manifest)` | Chrome's own install warnings for a manifest (`chrome.management.getPermissionWarningsByManifest`, no permission needed), from a loaded extension's worker. |
|
|
313
|
+
| `warningsAddedBy(worker, manifest, names)` | `{warnings, without, added}`: the warnings with and without some permissions, and those only they bring (`withoutPermissions` is the pure part). |
|
|
314
|
+
| `savedTabGroups(profile)` | Chrome's saved tab groups (`{title, pinned, urls}`) read from a closed profile's `Sync Data/LevelDB`: no extension API lists them. |
|
|
315
|
+
| `filesUnder`, `zipEntries`, `missingReferences`, `manifestReferences` | For a packaging test: the zip is the folder, and every path the extension names resolves (a `#fragment` or `?query` ignored: `referencedPath`). |
|
|
168
316
|
|
|
169
317
|
## Stylesheet classes
|
|
170
318
|
|
|
@@ -186,12 +334,14 @@ except those listed, each with a reason:
|
|
|
186
334
|
| Toggle (cr-toggle) | `<input type="checkbox" class="switch" role="switch">` |
|
|
187
335
|
| Select (md-select) | plain `<select>` |
|
|
188
336
|
| Text field (cr-input) | plain `<input>`, `<textarea>`; `.field` > `label` + input for a labelled one |
|
|
337
|
+
| Add row under a list | `<form class="row add">` with an `<input>` and a submit `<button>` (or `createSiteList`) |
|
|
189
338
|
| Buttons (cr-button) | plain `<button>`; `.action` filled; `.link` as a link; `.icon-button` 32px round |
|
|
190
339
|
| Settings menu | `.nav` > `ul.nav-list` > `a.nav-item[aria-current="page"]`, `.nav-separator` |
|
|
191
340
|
| Manager layout | `.manager`/`.layout`, `.sidebar` with `.tree` > `.tree-item`, `.main` with `.list-card` |
|
|
192
|
-
| Menu (cr-action-menu) | `<dialog class="menu">` of `button[role="menuitem"]`, `hr` |
|
|
193
|
-
|
|
|
194
|
-
|
|
|
341
|
+
| Menu (cr-action-menu) | `<dialog class="menu">` of `button[role="menuitem"]`, `hr` (or `createMenu`) |
|
|
342
|
+
| ⋮ icon | `<span class="icon icon-more-vert" aria-hidden="true">` (or `moreActionsButton`) |
|
|
343
|
+
| Dialog (cr-dialog) | `<dialog class="dialog"><form method="dialog">` `.dialog-title`, `.dialog-body`, `.dialog-buttons` (one field: `askText`) |
|
|
344
|
+
| Toast (cr-toast) | `<div class="toast" popover="manual">text <button>Undo</button></div>` (or `createToast`); with `hidden` instead of `popover`, it shows over the page but not the top layer |
|
|
195
345
|
| Drawer (cr-drawer) | `<dialog class="drawer">` with `.drawer-head` and a `.nav` |
|
|
196
346
|
| Side panel page | `<body class="side-panel">`: the panel's colours, a column 100vh high |
|
|
197
347
|
| Panel tabs (cr-tabs) | `<div class="tabs" role="tablist">` of `button.tab[role="tab"][aria-selected]` |
|
|
@@ -209,7 +359,9 @@ chrome://bookmarks in Chromium 156 / Chrome 157; the sources are listed at the t
|
|
|
209
359
|
|
|
210
360
|
## Development
|
|
211
361
|
|
|
212
|
-
`npm test` runs ESLint, a strict TypeScript check of the JSDoc (`checkJs`), the tests
|
|
362
|
+
`npm test` runs ESLint, a strict TypeScript check of the JSDoc (`checkJs`), the tests (the menu,
|
|
363
|
+
the dialog, the toast, the dialog site list, icon rendering and the Chrome readers in Playwright's
|
|
364
|
+
Chromium, the rest in Node and jsdom), and the
|
|
213
365
|
copied-code guard. The `.d.ts` files are generated from the JSDoc: `npm run types` writes them, and
|
|
214
366
|
the type check fails while one is stale (it regenerates them into `.types/` and compares).
|
|
215
367
|
|
package/bin/extension-kit.js
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The kit's command line, run from an extension's npm scripts:
|
|
4
4
|
*
|
|
5
|
-
* extension-kit vendor <dir>
|
|
6
|
-
* extension-kit
|
|
5
|
+
* extension-kit vendor <dir> copy the kit's browser half into <dir> (postinstall)
|
|
6
|
+
* extension-kit vendor <dir> <package> <path>... copy files of an installed package into <dir>
|
|
7
|
+
* (a path ending in / copies its contents)
|
|
8
|
+
* extension-kit vendor-check [dir] fail when a copy in [dir] (default extension/vendor)
|
|
9
|
+
* no longer matches the installed package
|
|
10
|
+
* extension-kit icons [dir] render [dir]/icon.svg (default extension/icons)
|
|
11
|
+
* extension-kit toolbar-icons [dir] render the toolbar glyph (default extension/icons/toolbar)
|
|
12
|
+
* extension-kit duplication fail on copied code jscpd finds, less .jscpd-allowlist.json
|
|
13
|
+
*
|
|
14
|
+
* Playwright and jscpd are the extension's devDependencies, so they're resolved from there.
|
|
7
15
|
*
|
|
8
16
|
* @module bin/extension-kit
|
|
9
17
|
*/
|
|
@@ -11,16 +19,44 @@
|
|
|
11
19
|
import { createRequire } from 'node:module';
|
|
12
20
|
import path from 'node:path';
|
|
13
21
|
import { findDisallowedClones } from '../src/build/duplication.js';
|
|
14
|
-
import {
|
|
22
|
+
import { renderIcons, renderToolbarIcons } from '../src/build/toolbar-icons.js';
|
|
23
|
+
import { checkVendored, vendorKit, vendorPackage } from '../src/build/vendor.js';
|
|
24
|
+
|
|
25
|
+
const USAGE = `Usage:
|
|
26
|
+
extension-kit vendor <dir> [<package> <path>...]
|
|
27
|
+
extension-kit vendor-check [dir]
|
|
28
|
+
extension-kit icons [dir]
|
|
29
|
+
extension-kit toolbar-icons [dir]
|
|
30
|
+
extension-kit duplication`;
|
|
15
31
|
|
|
16
|
-
const [command,
|
|
32
|
+
const [command, ...args] = process.argv.slice(2);
|
|
33
|
+
/** The extension's own `require`, for its devDependencies. */
|
|
34
|
+
const extensionRequire = createRequire(path.join(process.cwd(), 'package.json'));
|
|
17
35
|
|
|
18
|
-
if (command === 'vendor' &&
|
|
19
|
-
await vendorKit(path.resolve(
|
|
20
|
-
console.log(`extension-kit: copied into ${
|
|
36
|
+
if (command === 'vendor' && args.length === 1) {
|
|
37
|
+
await vendorKit(path.resolve(args[0]));
|
|
38
|
+
console.log(`extension-kit: copied into ${args[0]}`);
|
|
39
|
+
} else if (command === 'vendor' && args.length >= 3) {
|
|
40
|
+
const [dir, name, ...files] = args;
|
|
41
|
+
await vendorPackage(name, files, path.resolve(dir));
|
|
42
|
+
console.log(`extension-kit: copied ${name} into ${dir}`);
|
|
43
|
+
} else if (command === 'vendor-check' && args.length <= 1) {
|
|
44
|
+
const dir = args[0] ?? 'extension/vendor';
|
|
45
|
+
const stale = await checkVendored(dir);
|
|
46
|
+
for (const line of stale) console.error(line);
|
|
47
|
+
if (stale.length) {
|
|
48
|
+
console.error('\nThe vendored copies are out of date: run `npm run postinstall` (installing a package doesn\'t rerun it).');
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
} else {
|
|
51
|
+
console.log(`${dir} matches the installed packages.`);
|
|
52
|
+
}
|
|
53
|
+
} else if ((command === 'icons' || command === 'toolbar-icons') && args.length <= 1) {
|
|
54
|
+
const { chromium } = /** @type {typeof import('playwright')} */ (extensionRequire('playwright'));
|
|
55
|
+
const render = command === 'icons' ? renderIcons : renderToolbarIcons;
|
|
56
|
+
const dir = args[0] ?? (command === 'icons' ? 'extension/icons' : 'extension/icons/toolbar');
|
|
57
|
+
for (const file of await render({ chromium, dir })) console.log(`wrote ${file}`);
|
|
21
58
|
} else if (command === 'duplication') {
|
|
22
|
-
|
|
23
|
-
const { runJscpd } = createRequire(path.join(process.cwd(), 'package.json'))('jscpd');
|
|
59
|
+
const { runJscpd } = extensionRequire('jscpd');
|
|
24
60
|
const clones = await findDisallowedClones({ runJscpd });
|
|
25
61
|
for (const clone of clones) console.error(`Copied code (${clone.lines} lines): ${clone.where}`);
|
|
26
62
|
if (clones.length) {
|
|
@@ -30,6 +66,6 @@ if (command === 'vendor' && arg) {
|
|
|
30
66
|
console.log('No copied code.');
|
|
31
67
|
}
|
|
32
68
|
} else {
|
|
33
|
-
console.error(
|
|
69
|
+
console.error(USAGE);
|
|
34
70
|
process.exitCode = 2;
|
|
35
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arraypress/extension-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Shared pieces for Chrome extensions: Chrome's own page styles, a Chrome Sync settings store, the light/dark toolbar glyph, About links, and build and test helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/build/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { clonesFromReport, disallowedClones, findDisallowedClones } from './duplication.js';
|
|
8
8
|
export { placeholderWarning, printReleaseChecks, repositoryReminder } from './release-checks.js';
|
|
9
|
-
export
|
|
10
|
-
export {
|
|
9
|
+
export * as storeFrame from './store-frame.js';
|
|
10
|
+
export { HOOK_FILE, TEST_BUILDS_ENV, buildTestCopy, testBuildDir } from './test-build.js';
|
|
11
|
+
export { ICON_SIZES, TOOLBAR_COLORS, TOOLBAR_ICON_COLORS, TOOLBAR_ICON_SIZES, renderIcons, renderToolbarIcons, toolbarIconFile, } from './toolbar-icons.js';
|
|
12
|
+
export { RUNTIME_ENTRIES, STYLESHEET, VENDOR_STAMP, checkVendored, packageDir, vendorKit, vendorPackage } from './vendor.js';
|
|
11
13
|
export { ZIP_EXCLUDE, shipsFile, zipExtension } from './zip.js';
|
package/src/build/index.js
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
export { clonesFromReport, disallowedClones, findDisallowedClones } from './duplication.js';
|
|
9
9
|
export { placeholderWarning, printReleaseChecks, repositoryReminder } from './release-checks.js';
|
|
10
|
-
export
|
|
11
|
-
export {
|
|
10
|
+
export * as storeFrame from './store-frame.js';
|
|
11
|
+
export { HOOK_FILE, TEST_BUILDS_ENV, buildTestCopy, testBuildDir } from './test-build.js';
|
|
12
|
+
export {
|
|
13
|
+
ICON_SIZES, TOOLBAR_COLORS, TOOLBAR_ICON_COLORS, TOOLBAR_ICON_SIZES, renderIcons, renderToolbarIcons, toolbarIconFile,
|
|
14
|
+
} from './toolbar-icons.js';
|
|
15
|
+
export { RUNTIME_ENTRIES, STYLESHEET, VENDOR_STAMP, checkVendored, packageDir, vendorKit, vendorPackage } from './vendor.js';
|
|
12
16
|
export { ZIP_EXCLUDE, shipsFile, zipExtension } from './zip.js';
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drawing kit for store screenshots: a 1280×800 canvas with a caption band and a Chrome window
|
|
3
|
+
* (macOS, light or dark, default theme). Everything here is presentation around real captures;
|
|
4
|
+
* nothing is a feature of the extension.
|
|
5
|
+
*
|
|
6
|
+
* Window metrics follow Chrome's 2023 refresh on macOS (tabs 34px high in a 42px strip, a 46px
|
|
7
|
+
* toolbar with a pill omnibox).
|
|
8
|
+
*
|
|
9
|
+
* Use it from an extension's `store/tools/make-screenshots.mjs`: capture the page content with
|
|
10
|
+
* Playwright at `geometry(zoom).content`, then screenshot `canvas({caption, chrome:
|
|
11
|
+
* windowChrome({tabs, url}), content})` at {@link W}×{@link H}.
|
|
12
|
+
*
|
|
13
|
+
* @module build/store-frame
|
|
14
|
+
*/
|
|
15
|
+
/** The store screenshot's width. */
|
|
16
|
+
export declare const W = 1280;
|
|
17
|
+
/** The store screenshot's height. */
|
|
18
|
+
export declare const H = 800;
|
|
19
|
+
/** Where the window sits: its left edge, top edge, full width, and tab strip and toolbar heights. */
|
|
20
|
+
export declare const WIN: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
w: number;
|
|
24
|
+
strip: number;
|
|
25
|
+
toolbar: number;
|
|
26
|
+
};
|
|
27
|
+
export type Geometry = {
|
|
28
|
+
/**
|
|
29
|
+
* The window's scale.
|
|
30
|
+
*/
|
|
31
|
+
zoom: number;
|
|
32
|
+
/**
|
|
33
|
+
* The window's unscaled width.
|
|
34
|
+
*/
|
|
35
|
+
w: number;
|
|
36
|
+
/**
|
|
37
|
+
* The size to capture page content at.
|
|
38
|
+
*/
|
|
39
|
+
content: {
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* How a shot's window is laid out.
|
|
46
|
+
*
|
|
47
|
+
* @typedef {object} Geometry
|
|
48
|
+
* @property {number} zoom The window's scale.
|
|
49
|
+
* @property {number} w The window's unscaled width.
|
|
50
|
+
* @property {{ width: number, height: number }} content The size to capture page content at.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* A shot's window is drawn at `zoom` (scaled about its top-left corner) so text stays legible
|
|
54
|
+
* when the store shows screenshots small. Content is captured at 2× so the scaling stays sharp.
|
|
55
|
+
*
|
|
56
|
+
* @param {number} [zoom] - The window's scale. Default 1.
|
|
57
|
+
* @returns {Geometry} The window's unscaled width and the size to capture page content at.
|
|
58
|
+
*/
|
|
59
|
+
export declare function geometry(zoom?: number): Geometry;
|
|
60
|
+
/**
|
|
61
|
+
* A letter favicon for made-up sites (no real brands).
|
|
62
|
+
*
|
|
63
|
+
* @param {string} letter - The letter.
|
|
64
|
+
* @param {string} bg - Background colour.
|
|
65
|
+
* @param {string} [fg] - Letter colour. Default white.
|
|
66
|
+
* @returns {string} A data: URL of the SVG.
|
|
67
|
+
*/
|
|
68
|
+
export declare const letterIcon: (letter: string, bg: string, fg?: string) => string;
|
|
69
|
+
/** Chrome's light and dark window colours, and the canvas behind. */
|
|
70
|
+
export declare const THEMES: {
|
|
71
|
+
light: {
|
|
72
|
+
bg: string;
|
|
73
|
+
caption: string;
|
|
74
|
+
sub: string;
|
|
75
|
+
frame: string;
|
|
76
|
+
tabText: string;
|
|
77
|
+
tabTextInactive: string;
|
|
78
|
+
tabSep: string;
|
|
79
|
+
activeTab: string;
|
|
80
|
+
toolbar: string;
|
|
81
|
+
toolbarIcon: string;
|
|
82
|
+
omnibox: string;
|
|
83
|
+
url: string;
|
|
84
|
+
urlDim: string;
|
|
85
|
+
contentLine: string;
|
|
86
|
+
shadow: string;
|
|
87
|
+
};
|
|
88
|
+
dark: {
|
|
89
|
+
bg: string;
|
|
90
|
+
caption: string;
|
|
91
|
+
sub: string;
|
|
92
|
+
frame: string;
|
|
93
|
+
tabText: string;
|
|
94
|
+
tabTextInactive: string;
|
|
95
|
+
tabSep: string;
|
|
96
|
+
activeTab: string;
|
|
97
|
+
toolbar: string;
|
|
98
|
+
toolbarIcon: string;
|
|
99
|
+
omnibox: string;
|
|
100
|
+
url: string;
|
|
101
|
+
urlDim: string;
|
|
102
|
+
contentLine: string;
|
|
103
|
+
shadow: string;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
export type Theme = keyof typeof THEMES;
|
|
107
|
+
export type Tab = {
|
|
108
|
+
/**
|
|
109
|
+
* Its title.
|
|
110
|
+
*/
|
|
111
|
+
title: string;
|
|
112
|
+
/**
|
|
113
|
+
* Its favicon's URL.
|
|
114
|
+
*/
|
|
115
|
+
icon: string;
|
|
116
|
+
/**
|
|
117
|
+
* The selected tab.
|
|
118
|
+
*/
|
|
119
|
+
active?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Playing sound.
|
|
122
|
+
*/
|
|
123
|
+
audible?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Muted.
|
|
126
|
+
*/
|
|
127
|
+
muted?: boolean;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* The window's tab strip and toolbar, as HTML. Positions are window-relative.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} options - What to draw.
|
|
133
|
+
* @param {readonly Tab[]} options.tabs - The tabs, left to right.
|
|
134
|
+
* @param {string} options.url - The address bar's text.
|
|
135
|
+
* @param {string} [options.urlHost] - The start of `url` to show in full; the rest is dimmed.
|
|
136
|
+
* @param {Theme} [options.theme] - Default light.
|
|
137
|
+
* @param {number} [options.w] - The window's width. Default the full window.
|
|
138
|
+
* @param {readonly string[]} [options.ext] - Toolbar extension icons' URLs.
|
|
139
|
+
* @returns {string} The HTML, with its own styles.
|
|
140
|
+
*/
|
|
141
|
+
export declare function windowChrome({ tabs, url, urlHost, theme, w, ext }: {
|
|
142
|
+
tabs: readonly Tab[];
|
|
143
|
+
url: string;
|
|
144
|
+
urlHost?: string;
|
|
145
|
+
theme?: Theme;
|
|
146
|
+
w?: number;
|
|
147
|
+
ext?: readonly string[];
|
|
148
|
+
}): string;
|
|
149
|
+
/**
|
|
150
|
+
* The macOS arrow pointer.
|
|
151
|
+
*
|
|
152
|
+
* @param {number} x - Its tip, across the canvas.
|
|
153
|
+
* @param {number} y - Its tip, down the canvas.
|
|
154
|
+
* @returns {string} Absolutely positioned SVG.
|
|
155
|
+
*/
|
|
156
|
+
export declare const cursor: (x: number, y: number) => string;
|
|
157
|
+
/**
|
|
158
|
+
* The full page: caption band, window, content and overlays.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} options - What to draw.
|
|
161
|
+
* @param {string} options.caption - The headline above the window.
|
|
162
|
+
* @param {Theme} [options.theme] - Default light.
|
|
163
|
+
* @param {string} options.chrome - From windowChrome.
|
|
164
|
+
* @param {string} options.content - An <img> src (a capture at `geo.content` size) or raw HTML.
|
|
165
|
+
* @param {string} [options.overlays] - HTML drawn over the window.
|
|
166
|
+
* @param {Geometry} [options.geo] - Default geometry(1).
|
|
167
|
+
* @returns {string} A whole HTML document, W×H.
|
|
168
|
+
*/
|
|
169
|
+
export declare function canvas({ caption, theme, chrome, content, overlays, geo }: {
|
|
170
|
+
caption: string;
|
|
171
|
+
theme?: Theme;
|
|
172
|
+
chrome: string;
|
|
173
|
+
content: string;
|
|
174
|
+
overlays?: string;
|
|
175
|
+
geo?: Geometry;
|
|
176
|
+
}): string;
|