@bwat47/joplin-plugin-scripts 1.0.0 → 1.2.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 +98 -17
- package/bin/joplin-plugin-scripts.mjs +10 -2
- package/lib/build.mjs +32 -10
- package/lib/buildConfig.mjs +259 -32
- package/lib/errors.mjs +27 -0
- package/lib/index.mjs +2 -0
- package/lib/npm.mjs +58 -0
- package/lib/packagePlugin.mjs +98 -17
- package/lib/updateApi.mjs +33 -17
- package/lib/updateVersion.mjs +37 -7
- package/package.json +18 -5
- package/types/build.d.mts +7 -0
- package/types/buildConfig.d.mts +88 -0
- package/types/errors.d.mts +20 -0
- package/types/index.d.mts +2 -0
- package/types/npm.d.mts +21 -0
- package/types/packagePlugin.d.mts +61 -0
- package/types/updateApi.d.mts +32 -0
- package/types/updateVersion.d.mts +7 -0
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ npm install -D @bwat47/joplin-plugin-scripts
|
|
|
11
11
|
|
|
12
12
|
```json
|
|
13
13
|
"scripts": {
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
14
15
|
"dist": "npm run typecheck && joplin-plugin-scripts build",
|
|
15
16
|
"updateVersion": "joplin-plugin-scripts update-version",
|
|
16
17
|
"updateApi": "joplin-plugin-scripts update-api"
|
|
@@ -19,13 +20,13 @@ npm install -D @bwat47/joplin-plugin-scripts
|
|
|
19
20
|
|
|
20
21
|
Commands run from the plugin project root, which npm scripts do automatically.
|
|
21
22
|
|
|
22
|
-
| Command | What it does
|
|
23
|
-
| ---------------- |
|
|
24
|
-
| `build` | Validates the project, clears `dist/` and `publish/`, copies runtime assets from `src/`, bundles, and writes `publish/<id>.jpl` and `<id>.json`.
|
|
25
|
-
| `update-version` | Increments the patch version
|
|
26
|
-
| `update-api` | Replaces `api/` with the latest generator-joplin API and warns if the category or content-script external lists here differ.
|
|
23
|
+
| Command | What it does |
|
|
24
|
+
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `build` | Validates the project, clears `dist/` and `publish/`, copies runtime assets from `src/`, bundles, and writes `publish/<id>.jpl` and `<id>.json`. |
|
|
26
|
+
| `update-version` | Increments the patch version across `package.json`, `src/manifest.json`, and `package-lock.json`. |
|
|
27
|
+
| `update-api` | Replaces `api/` with the latest generator-joplin API and warns if the category or content-script external lists here differ. Requires npm, even when scripts run through Yarn or pnpm. |
|
|
27
28
|
|
|
28
|
-
###
|
|
29
|
+
### Expected project layout
|
|
29
30
|
|
|
30
31
|
- `src/index.ts`: main entry point, bundled to `dist/index.js` for the Node platform.
|
|
31
32
|
`node:` imports are rewritten to bare names for Joplin's plugin runtime.
|
|
@@ -40,16 +41,93 @@ Commands run from the plugin project root, which npm scripts do automatically.
|
|
|
40
41
|
```json
|
|
41
42
|
{
|
|
42
43
|
"extraScripts": ["contentScripts/codeMirror.ts"],
|
|
43
|
-
"
|
|
44
|
+
"webviewScripts": ["dialog/dialog.ts"],
|
|
45
|
+
"mainBuildOptions": { "external": ["jsdom"] },
|
|
46
|
+
"contentScriptBuildOptions": { "sourcemap": "inline" },
|
|
47
|
+
"webviewScriptBuildOptions": { "minify": false }
|
|
44
48
|
}
|
|
45
49
|
```
|
|
46
50
|
|
|
47
|
-
- `extraScripts`: content scripts
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`
|
|
51
|
+
- `extraScripts`: content scripts registered with `joplin.contentScripts.register`
|
|
52
|
+
(CodeMirror and Markdown-it plugins), relative to `src/`. Each is bundled for the browser
|
|
53
|
+
as CommonJS to the same path under `dist/` with a `.js` extension. Joplin's CodeMirror
|
|
54
|
+
and Lezer packages stay external so they share the host's instances.
|
|
55
|
+
- `webviewScripts`: scripts loaded into a dialog or panel with `addScript`, relative to
|
|
56
|
+
`src/`. Each builds as a self-contained IIFE and must initialize itself. Plain `.js`
|
|
57
|
+
files in `src/` are copied automatically and don't need listing.
|
|
58
|
+
|
|
59
|
+
Script output paths must be unique after normalization. They are compared without case on
|
|
60
|
+
every platform, since the archive may be installed on a case-insensitive filesystem.
|
|
61
|
+
`dist/index.js` is reserved for the main bundle. Invalid paths and collisions are rejected
|
|
62
|
+
before existing build output is cleared.
|
|
63
|
+
|
|
64
|
+
#### Advanced build options
|
|
65
|
+
|
|
66
|
+
Each option group merges esbuild options over the defaults for its bundle type:
|
|
67
|
+
|
|
68
|
+
| Option group | Applies to |
|
|
69
|
+
| --------------------------- | -------------------- |
|
|
70
|
+
| `mainBuildOptions` | Main bundle only |
|
|
71
|
+
| `contentScriptBuildOptions` | Every content script |
|
|
72
|
+
| `webviewScriptBuildOptions` | Every webview script |
|
|
73
|
+
|
|
74
|
+
- Relative `alias` targets resolve from the project root.
|
|
75
|
+
- `define` entries merge individually, preserving `process.env.NODE_ENV` as
|
|
76
|
+
`"production"` unless explicitly overridden.
|
|
77
|
+
- Reserved options: `absWorkingDir`, `entryPoints`, `outfile`, `outdir`, `write`,
|
|
78
|
+
`format`, `plugins`, `bundle`, and `packages`. Content and webview scripts also
|
|
79
|
+
cannot override `platform`.
|
|
80
|
+
- `external` keeps packages as runtime imports. Content-script entries extend the
|
|
81
|
+
CodeMirror and Lezer externals, which cannot be removed. Webview scripts cannot
|
|
82
|
+
set `external`, because they have no runtime `require`.
|
|
83
|
+
- For debugging, prefer `"sourcemap": "inline"`. Setting it to `true` writes `.js.map`
|
|
84
|
+
files into `dist/` that ship in the `.jpl`.
|
|
85
|
+
|
|
86
|
+
### Migrating from the standard Webpack setup
|
|
87
|
+
|
|
88
|
+
#### Resolve the Joplin API through TypeScript paths
|
|
89
|
+
|
|
90
|
+
Add these mappings to your existing `tsconfig.json` `compilerOptions` so `api` and
|
|
91
|
+
`api/types` imports resolve for both TypeScript and esbuild:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"compilerOptions": {
|
|
96
|
+
"paths": {
|
|
97
|
+
"api": ["./api/index.ts"],
|
|
98
|
+
"api/*": ["./api/*"]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Remove `baseUrl` ([deprecated in TypeScript 6](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-6-0.html#deprecated---baseurl))
|
|
105
|
+
and make all path targets relative to the declaring `tsconfig.json`. Add explicit
|
|
106
|
+
mappings for any other imports that relied on `baseUrl`.
|
|
107
|
+
|
|
108
|
+
#### Run type checking separately
|
|
109
|
+
|
|
110
|
+
esbuild does not check types. Keep `tsc --noEmit` before the build, as shown in Usage.
|
|
111
|
+
|
|
112
|
+
#### Translate Webpack overrides to esbuild options
|
|
113
|
+
|
|
114
|
+
`webpackOverrides` is not supported. Replace it in `plugin.config.json` with
|
|
115
|
+
equivalent esbuild options:
|
|
116
|
+
|
|
117
|
+
| Webpack setting | esbuild equivalent |
|
|
118
|
+
| ----------------------- | ------------------------------------------------------ |
|
|
119
|
+
| `resolve.alias` | `mainBuildOptions.alias`, or suitable tsconfig `paths` |
|
|
120
|
+
| `devtool` | `mainBuildOptions.sourcemap` |
|
|
121
|
+
| `optimization.minimize` | `mainBuildOptions.minify` |
|
|
122
|
+
| `externals` | `mainBuildOptions.external`, using esbuild's syntax |
|
|
123
|
+
| Loaders and plugins | Require an equivalent approach; they cannot be reused |
|
|
124
|
+
|
|
125
|
+
External dependencies remain runtime imports and must be available in Joplin's
|
|
126
|
+
plugin runtime; marking a package external does not include it in the archive.
|
|
127
|
+
|
|
128
|
+
The standard setup applies `webpackOverrides` to both main and content-script
|
|
129
|
+
builds. Here, `mainBuildOptions` affects only the main bundle. Move settings that
|
|
130
|
+
should also apply to content scripts into `contentScriptBuildOptions`.
|
|
53
131
|
|
|
54
132
|
### Testing a plugin's bundles
|
|
55
133
|
|
|
@@ -57,18 +135,21 @@ The package exports the same esbuild options the build uses, so plugin tests can
|
|
|
57
135
|
in memory and run the result:
|
|
58
136
|
|
|
59
137
|
```js
|
|
60
|
-
import { createBuildOptions,
|
|
138
|
+
import { createBuildOptions, readPluginConfig } from '@bwat47/joplin-plugin-scripts';
|
|
139
|
+
import { build } from 'esbuild';
|
|
61
140
|
|
|
62
141
|
const { mainBuildOptions } = await readPluginConfig(root);
|
|
63
142
|
const result = await build({ ...createBuildOptions(root, mainBuildOptions), write: false });
|
|
64
143
|
```
|
|
65
144
|
|
|
66
|
-
|
|
145
|
+
Also exported: `createContentScriptBuildOptions`, `createWebviewScriptBuildOptions`,
|
|
146
|
+
`contentScriptExternals`, and `PluginScriptsError` for expected configuration and validation errors.
|
|
67
147
|
|
|
68
148
|
## Development
|
|
69
149
|
|
|
70
150
|
```sh
|
|
71
151
|
npm test
|
|
152
|
+
npm run typecheck
|
|
72
153
|
npm run lint
|
|
73
154
|
npm run format
|
|
74
155
|
```
|
|
@@ -76,5 +157,5 @@ npm run format
|
|
|
76
157
|
To try a change in a plugin before publishing, run `npm pack` here and
|
|
77
158
|
`npm install -D ../joplin-plugin-scripts/bwat47-joplin-plugin-scripts-<version>.tgz` in the plugin.
|
|
78
159
|
|
|
79
|
-
Release: bump `version`, then `npm publish --access public` (`prepublishOnly` runs
|
|
80
|
-
format check, and tests).
|
|
160
|
+
Release: bump `version`, then `npm publish --access public` (`prepublishOnly` runs type checking,
|
|
161
|
+
lint, format check, and tests; `prepack` then generates the declarations).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { buildPlugin } from '../lib/build.mjs';
|
|
3
|
+
import { PluginScriptsError } from '../lib/errors.mjs';
|
|
3
4
|
import { updateApi } from '../lib/updateApi.mjs';
|
|
4
5
|
import { updateVersion } from '../lib/updateVersion.mjs';
|
|
5
6
|
|
|
@@ -12,6 +13,7 @@ Commands:
|
|
|
12
13
|
update-version Increment the patch version in package.json, src/manifest.json, and package-lock.json
|
|
13
14
|
update-api Replace api/ with the declarations from the latest generator-joplin`;
|
|
14
15
|
|
|
16
|
+
/** @type {Record<string, (root: string) => Promise<void>>} */
|
|
15
17
|
const commands = {
|
|
16
18
|
build: async (root) => {
|
|
17
19
|
await buildPlugin(root);
|
|
@@ -36,5 +38,11 @@ if (!run || rest.length > 0) {
|
|
|
36
38
|
console.error(USAGE);
|
|
37
39
|
process.exit(1);
|
|
38
40
|
}
|
|
39
|
-
|
|
40
|
-
await run(process.cwd());
|
|
41
|
+
try {
|
|
42
|
+
await run(process.cwd());
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Expected failures print only their message; anything else is a bug, so its stack is shown.
|
|
45
|
+
if (!(error instanceof PluginScriptsError)) throw error;
|
|
46
|
+
console.error(`Error: ${error.message}`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
package/lib/build.mjs
CHANGED
|
@@ -5,21 +5,30 @@ import { build } from 'esbuild';
|
|
|
5
5
|
import {
|
|
6
6
|
createBuildOptions,
|
|
7
7
|
createContentScriptBuildOptions,
|
|
8
|
+
createWebviewScriptBuildOptions,
|
|
8
9
|
isRuntimeAsset,
|
|
9
10
|
readPluginConfig,
|
|
11
|
+
scriptOutputPaths,
|
|
10
12
|
} from './buildConfig.mjs';
|
|
13
|
+
import { PluginScriptsError } from './errors.mjs';
|
|
11
14
|
import { packagePlugin, readProject } from './packagePlugin.mjs';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Builds the plugin at `root`: validates the project, clears `dist/` and `publish/`, copies runtime
|
|
15
|
-
* assets from `src/`, bundles the main entry point
|
|
18
|
+
* assets from `src/`, bundles the main entry point, extra scripts, and webview scripts, and creates
|
|
19
|
+
* the `.jpl` archive.
|
|
20
|
+
* @param {string} root Plugin project root.
|
|
16
21
|
*/
|
|
17
22
|
export async function buildPlugin(root) {
|
|
18
23
|
const dist = path.join(root, 'dist');
|
|
19
24
|
const publish = path.join(root, 'publish');
|
|
20
25
|
const { manifest } = await readProject(root);
|
|
21
|
-
const { extraScripts, mainBuildOptions } =
|
|
22
|
-
|
|
26
|
+
const { extraScripts, webviewScripts, mainBuildOptions, contentScriptBuildOptions, webviewScriptBuildOptions } =
|
|
27
|
+
await readPluginConfig(root);
|
|
28
|
+
const scriptBuilds = [
|
|
29
|
+
...extraScripts.map((script) => createContentScriptBuildOptions(root, script, contentScriptBuildOptions)),
|
|
30
|
+
...webviewScripts.map((script) => createWebviewScriptBuildOptions(root, script, webviewScriptBuildOptions)),
|
|
31
|
+
];
|
|
23
32
|
|
|
24
33
|
// Only these fixed, project-local output directories are removed.
|
|
25
34
|
await rm(dist, { recursive: true, force: true });
|
|
@@ -31,14 +40,27 @@ export async function buildPlugin(root) {
|
|
|
31
40
|
recursive: true,
|
|
32
41
|
filter: (source) => isRuntimeAsset(root, source),
|
|
33
42
|
});
|
|
34
|
-
|
|
43
|
+
// Wait for every bundle, so esbuild has printed all of its errors before the build stops.
|
|
44
|
+
const results = await Promise.allSettled([
|
|
35
45
|
build(createBuildOptions(root, mainBuildOptions)),
|
|
36
|
-
...
|
|
46
|
+
...scriptBuilds.map((options) => build(options)),
|
|
37
47
|
]);
|
|
48
|
+
const failures = results.filter((result) => result.status === 'rejected').map((result) => result.reason);
|
|
49
|
+
if (failures.length > 0) {
|
|
50
|
+
// esbuild's own failures carry `errors` and have already been logged; anything else is unexpected.
|
|
51
|
+
const unexpected = failures.find((failure) => !isEsbuildFailure(failure));
|
|
52
|
+
if (unexpected) throw unexpected;
|
|
53
|
+
throw new PluginScriptsError('Bundling failed; see the esbuild errors above.', { cause: failures[0] });
|
|
54
|
+
}
|
|
38
55
|
|
|
39
|
-
await packagePlugin(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
await packagePlugin(root, manifest, scriptOutputPaths(extraScripts, webviewScripts));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Checks whether a rejected build is an esbuild `BuildFailure`, whose messages esbuild has already logged.
|
|
61
|
+
* @param {unknown} error
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
function isEsbuildFailure(error) {
|
|
65
|
+
return error instanceof Error && 'errors' in error && Array.isArray(error.errors);
|
|
44
66
|
}
|
package/lib/buildConfig.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { errorMessage, hasErrorCode, PluginScriptsError } from './errors.mjs';
|
|
5
|
+
|
|
4
6
|
/** Name of the optional per-plugin build configuration file at the project root. */
|
|
5
7
|
export const CONFIG_FILE = 'plugin.config.json';
|
|
6
8
|
|
|
@@ -22,10 +24,56 @@ const TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
|
22
24
|
/** Matches a Node built-in import with the `node:` prefix, e.g. `node:path`. */
|
|
23
25
|
const NODE_PREFIX_PATTERN = /^node:/;
|
|
24
26
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* esbuild options that the package owns and no override may set: the input and output locations,
|
|
29
|
+
* `write` (packaging needs `dist/index.js` on disk), `format` (Joplin loads the main bundle as
|
|
30
|
+
* CommonJS), `plugins` (replacing them drops the `node:` prefix rewrite), and `bundle` and
|
|
31
|
+
* `packages`. Each output is loaded as a single file: with `bundle: false` or
|
|
32
|
+
* `packages: "external"`, its imports would stay as `require` calls for modules that are not in
|
|
33
|
+
* the archive or, in a webview, for a `require` that does not exist. Individual packages can
|
|
34
|
+
* still be made external with `external`.
|
|
35
|
+
*/
|
|
36
|
+
const RESERVED_BUILD_OPTIONS = [
|
|
37
|
+
'absWorkingDir',
|
|
38
|
+
'entryPoints',
|
|
39
|
+
'outfile',
|
|
40
|
+
'outdir',
|
|
41
|
+
'write',
|
|
42
|
+
'format',
|
|
43
|
+
'plugins',
|
|
44
|
+
'bundle',
|
|
45
|
+
'packages',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Further options that content-script and webview-script overrides may not set. Both kinds of
|
|
50
|
+
* script run in a webview, so `platform` stays `browser`: under `node`, a Node built-in import
|
|
51
|
+
* would become a `require` that fails at runtime instead of failing the build.
|
|
52
|
+
*/
|
|
53
|
+
const RESERVED_SCRIPT_BUILD_OPTIONS = [...RESERVED_BUILD_OPTIONS, 'platform'];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Webview scripts also may not set `external`: an IIFE loaded by a `<script>` tag has no
|
|
57
|
+
* `require`, so any external import would fail at runtime.
|
|
58
|
+
*/
|
|
59
|
+
const RESERVED_WEBVIEW_BUILD_OPTIONS = [...RESERVED_SCRIPT_BUILD_OPTIONS, 'external'];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Configuration keys holding esbuild overrides, with the options each may not set.
|
|
63
|
+
* @type {Record<'mainBuildOptions' | 'contentScriptBuildOptions' | 'webviewScriptBuildOptions', string[]>}
|
|
64
|
+
*/
|
|
65
|
+
const BUILD_OPTION_KEYS = {
|
|
66
|
+
mainBuildOptions: RESERVED_BUILD_OPTIONS,
|
|
67
|
+
contentScriptBuildOptions: RESERVED_SCRIPT_BUILD_OPTIONS,
|
|
68
|
+
webviewScriptBuildOptions: RESERVED_WEBVIEW_BUILD_OPTIONS,
|
|
69
|
+
};
|
|
27
70
|
|
|
28
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Excludes bundled sources, tests, and development fixtures in `<root>/src` from copied runtime assets.
|
|
73
|
+
* @param {string} root Plugin project root.
|
|
74
|
+
* @param {string} source
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
29
77
|
export function isRuntimeAsset(root, source) {
|
|
30
78
|
const parts = path.relative(path.join(root, 'src'), source).split(PATH_SEPARATOR_PATTERN);
|
|
31
79
|
return (
|
|
@@ -73,6 +121,7 @@ const stripNodePrefix = {
|
|
|
73
121
|
* Options shared by the main plugin and content-script bundles.
|
|
74
122
|
* `api` resolves through tsconfig.json `paths`: api/index.ts exports Joplin's injected global,
|
|
75
123
|
* and api/types contains runtime enums, so both are bundled.
|
|
124
|
+
* @param {string} root Plugin project root.
|
|
76
125
|
* @returns {import('esbuild').BuildOptions}
|
|
77
126
|
*/
|
|
78
127
|
function sharedOptions(root) {
|
|
@@ -92,54 +141,227 @@ function sharedOptions(root) {
|
|
|
92
141
|
/**
|
|
93
142
|
* Returns the esbuild options for the main plugin bundle, `src/index.ts` to `dist/index.js`.
|
|
94
143
|
* `mainBuildOptions` from the plugin config are merged over the defaults.
|
|
144
|
+
* `define` entries are merged individually so unrelated definitions preserve the defaults.
|
|
95
145
|
* @param {string} root Plugin project root.
|
|
96
146
|
* @param {import('esbuild').BuildOptions} [overrides] The plugin config's `mainBuildOptions`.
|
|
97
147
|
* @returns {import('esbuild').BuildOptions}
|
|
98
148
|
*/
|
|
99
149
|
export function createBuildOptions(root, overrides = {}) {
|
|
150
|
+
const defaults = sharedOptions(root);
|
|
100
151
|
return {
|
|
101
|
-
...
|
|
152
|
+
...defaults,
|
|
102
153
|
entryPoints: ['src/index.ts'],
|
|
103
154
|
outfile: 'dist/index.js',
|
|
104
155
|
platform: 'node',
|
|
105
156
|
plugins: [stripNodePrefix],
|
|
106
157
|
...overrides,
|
|
158
|
+
define: { ...defaults.define, ...overrides.define },
|
|
107
159
|
};
|
|
108
160
|
}
|
|
109
161
|
|
|
110
162
|
/**
|
|
111
|
-
*
|
|
112
|
-
* (relative to `src/`) to `dist/contentScripts/codeMirror.js`.
|
|
113
|
-
* @param {string} root Plugin project root.
|
|
163
|
+
* Rejects a script path that is not a bundleable script inside `src/`, e.g. `../a.ts` or `notes.txt`.
|
|
114
164
|
* @param {string} script Script path relative to `src/`.
|
|
115
|
-
* @returns {import('esbuild').BuildOptions}
|
|
116
165
|
*/
|
|
117
|
-
|
|
166
|
+
function assertValidScriptPath(script) {
|
|
118
167
|
if (
|
|
119
168
|
!SCRIPT_EXTENSION_PATTERN.test(script) ||
|
|
120
169
|
path.isAbsolute(script) ||
|
|
121
170
|
script.split(PATH_SEPARATOR_PATTERN).includes('..')
|
|
122
171
|
) {
|
|
123
|
-
throw new
|
|
172
|
+
throw new PluginScriptsError(`Invalid extra script path: ${script}`);
|
|
124
173
|
}
|
|
125
|
-
|
|
126
|
-
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Options shared by content-script and webview-script bundles, with `overrides` merged over them.
|
|
178
|
+
* Scripts run in a webview, so they use esbuild's default browser platform: a Node built-in
|
|
179
|
+
* import fails the build instead of becoming a runtime require.
|
|
180
|
+
* @param {string} root Plugin project root.
|
|
181
|
+
* @param {string} script Script path relative to `src/`.
|
|
182
|
+
* @param {import('esbuild').BuildOptions} overrides
|
|
183
|
+
* @returns {import('esbuild').BuildOptions}
|
|
184
|
+
*/
|
|
185
|
+
function scriptOptions(root, script, overrides) {
|
|
186
|
+
assertValidScriptPath(script);
|
|
187
|
+
const defaults = sharedOptions(root);
|
|
127
188
|
return {
|
|
128
|
-
...
|
|
189
|
+
...defaults,
|
|
129
190
|
entryPoints: [`src/${script}`],
|
|
130
191
|
outfile: `dist/${script.replace(SCRIPT_EXTENSION_PATTERN, '.js')}`,
|
|
131
|
-
|
|
192
|
+
...overrides,
|
|
193
|
+
define: { ...defaults.define, ...overrides.define },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns the esbuild options for one content script, e.g. `contentScripts/codeMirror.ts`
|
|
199
|
+
* (relative to `src/`) to `dist/contentScripts/codeMirror.js`. `overrides` are merged over the
|
|
200
|
+
* defaults; their `external` entries are added to the host libraries, which always stay external.
|
|
201
|
+
* @param {string} root Plugin project root.
|
|
202
|
+
* @param {string} script Script path relative to `src/`.
|
|
203
|
+
* @param {import('esbuild').BuildOptions} [overrides] The plugin config's `contentScriptBuildOptions`.
|
|
204
|
+
* @returns {import('esbuild').BuildOptions}
|
|
205
|
+
*/
|
|
206
|
+
export function createContentScriptBuildOptions(root, script, overrides = {}) {
|
|
207
|
+
return {
|
|
208
|
+
...scriptOptions(root, script, overrides),
|
|
209
|
+
external: [...new Set([...contentScriptExternals, ...(overrides.external ?? [])])],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Returns the esbuild options for one webview script, e.g. `dialog/resizeDialog.ts` (relative to
|
|
215
|
+
* `src/`) to `dist/dialog/resizeDialog.js`. Webview scripts are loaded by a `<script>` tag in a
|
|
216
|
+
* dialog or panel, which has no `module`, `exports`, or `require`, so they are bundled as a
|
|
217
|
+
* self-contained IIFE. Their exports are dropped; the script runs for its side effects.
|
|
218
|
+
* @param {string} root Plugin project root.
|
|
219
|
+
* @param {string} script Script path relative to `src/`.
|
|
220
|
+
* @param {import('esbuild').BuildOptions} [overrides] The plugin config's `webviewScriptBuildOptions`.
|
|
221
|
+
* @returns {import('esbuild').BuildOptions}
|
|
222
|
+
*/
|
|
223
|
+
export function createWebviewScriptBuildOptions(root, script, overrides = {}) {
|
|
224
|
+
return {
|
|
225
|
+
...scriptOptions(root, script, overrides),
|
|
226
|
+
format: 'iife',
|
|
227
|
+
// A webview has no host CodeMirror instance to share, so everything is bundled.
|
|
228
|
+
external: [],
|
|
132
229
|
};
|
|
133
230
|
}
|
|
134
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Returns `dist/`-relative output paths for every script the plugin config registers.
|
|
234
|
+
* @param {string[]} extraScripts Content-script paths relative to `src/`.
|
|
235
|
+
* @param {string[]} webviewScripts Webview-script paths relative to `src/`.
|
|
236
|
+
* @returns {string[]} Bundle paths relative to `dist/`, using native separators.
|
|
237
|
+
*/
|
|
238
|
+
export function scriptOutputPaths(extraScripts, webviewScripts) {
|
|
239
|
+
return [...extraScripts, ...webviewScripts].map((script) =>
|
|
240
|
+
path.normalize(script.replace(SCRIPT_EXTENSION_PATTERN, '.js'))
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Shows a `dist/`-relative output path with `/` separators on every platform, e.g. `scripts\a.js` as `scripts/a.js`.
|
|
246
|
+
* @param {string} output Bundle path relative to `dist/`.
|
|
247
|
+
* @returns {string}
|
|
248
|
+
*/
|
|
249
|
+
function displayOutputPath(output) {
|
|
250
|
+
return output.replaceAll('\\', '/');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Output paths are compared without case on every platform: the archive may be installed on a
|
|
255
|
+
* case-insensitive filesystem (Windows, macOS), where `Editor.js` and `editor.js` are one file.
|
|
256
|
+
* @param {string} output Bundle path relative to `dist/`.
|
|
257
|
+
* @returns {string}
|
|
258
|
+
*/
|
|
259
|
+
function outputPathKey(output) {
|
|
260
|
+
return displayOutputPath(output).toLowerCase();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Checks whether a configuration value is an array of strings.
|
|
265
|
+
* @param {unknown} value
|
|
266
|
+
* @returns {value is string[]}
|
|
267
|
+
*/
|
|
268
|
+
function isStringArray(value) {
|
|
269
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Rejects a configuration entry that is not an array of script paths.
|
|
274
|
+
* @param {unknown} value
|
|
275
|
+
* @param {string} key
|
|
276
|
+
* @param {string} configPath
|
|
277
|
+
* @returns {asserts value is string[]}
|
|
278
|
+
*/
|
|
279
|
+
function assertScriptArray(value, key, configPath) {
|
|
280
|
+
if (!isStringArray(value)) {
|
|
281
|
+
throw new PluginScriptsError(`${configPath} "${key}" must be an array of paths relative to src/.`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Checks whether a configuration value is a non-null, non-array object.
|
|
287
|
+
* @param {unknown} value
|
|
288
|
+
* @returns {value is Record<string, unknown>}
|
|
289
|
+
*/
|
|
135
290
|
function isPlainObject(value) {
|
|
136
291
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
137
292
|
}
|
|
138
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Rejects malformed script paths, scripts that build to the main bundle's `dist/index.js`,
|
|
296
|
+
* and scripts that build to the same output path.
|
|
297
|
+
* @param {string} configPath Configuration file path used in error messages.
|
|
298
|
+
* @param {string[]} extraScripts Content-script paths relative to `src/`.
|
|
299
|
+
* @param {string[]} webviewScripts Webview-script paths relative to `src/`.
|
|
300
|
+
*/
|
|
301
|
+
function assertValidScripts(configPath, extraScripts, webviewScripts) {
|
|
302
|
+
// Report a malformed path as such, before normalization can make it look like a collision.
|
|
303
|
+
for (const script of [...extraScripts, ...webviewScripts]) assertValidScriptPath(script);
|
|
304
|
+
const mainOutput = outputPathKey('index.js');
|
|
305
|
+
const seen = new Set();
|
|
306
|
+
for (const output of scriptOutputPaths(extraScripts, webviewScripts)) {
|
|
307
|
+
const key = outputPathKey(output);
|
|
308
|
+
if (key === mainOutput) {
|
|
309
|
+
throw new PluginScriptsError(
|
|
310
|
+
`${configPath} lists a script that builds to dist/index.js, which is reserved for the main bundle.`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (seen.has(key)) {
|
|
314
|
+
throw new PluginScriptsError(
|
|
315
|
+
`${configPath} lists more than one script that builds to dist/${displayOutputPath(output)}.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
seen.add(key);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Rejects build overrides that are not an object, set an option the package owns, or have
|
|
324
|
+
* `define` or `external` values the build cannot merge with its defaults.
|
|
325
|
+
* @param {unknown} options
|
|
326
|
+
* @param {keyof typeof BUILD_OPTION_KEYS} key
|
|
327
|
+
* @param {string} configPath
|
|
328
|
+
* @returns {asserts options is import('esbuild').BuildOptions}
|
|
329
|
+
*/
|
|
330
|
+
function assertBuildOptions(options, key, configPath) {
|
|
331
|
+
if (!isPlainObject(options)) {
|
|
332
|
+
throw new PluginScriptsError(`${configPath} "${key}" must be an object of esbuild options.`);
|
|
333
|
+
}
|
|
334
|
+
const reserved = BUILD_OPTION_KEYS[key].filter((option) => option in options);
|
|
335
|
+
if (reserved.length > 0) {
|
|
336
|
+
throw new PluginScriptsError(`${configPath} "${key}" cannot set ${reserved.join(', ')}.`);
|
|
337
|
+
}
|
|
338
|
+
const { define, external } = options;
|
|
339
|
+
if (
|
|
340
|
+
define !== undefined &&
|
|
341
|
+
!(isPlainObject(define) && Object.values(define).every((value) => typeof value === 'string'))
|
|
342
|
+
) {
|
|
343
|
+
throw new PluginScriptsError(`${configPath} "${key}.define" must be an object of string replacements.`);
|
|
344
|
+
}
|
|
345
|
+
if (external !== undefined && !isStringArray(external)) {
|
|
346
|
+
throw new PluginScriptsError(`${configPath} "${key}.external" must be an array of module names.`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Validated contents of `plugin.config.json`, with defaults for omitted keys.
|
|
352
|
+
* @typedef {object} PluginConfig
|
|
353
|
+
* @property {string[]} extraScripts Content-script paths relative to `src/`.
|
|
354
|
+
* @property {string[]} webviewScripts Webview-script paths relative to `src/`.
|
|
355
|
+
* @property {import('esbuild').BuildOptions} mainBuildOptions Overrides for the main bundle.
|
|
356
|
+
* @property {import('esbuild').BuildOptions} contentScriptBuildOptions Overrides for every content script.
|
|
357
|
+
* @property {import('esbuild').BuildOptions} webviewScriptBuildOptions Overrides for every webview script.
|
|
358
|
+
*/
|
|
359
|
+
|
|
139
360
|
/**
|
|
140
361
|
* Reads and validates `<root>/plugin.config.json`. The file is optional; a plugin without one
|
|
141
|
-
* has no extra scripts and uses the default
|
|
142
|
-
* @
|
|
362
|
+
* has no extra or webview scripts and uses the default build options.
|
|
363
|
+
* @param {string} root Plugin project root.
|
|
364
|
+
* @returns {Promise<PluginConfig>}
|
|
143
365
|
*/
|
|
144
366
|
export async function readPluginConfig(root) {
|
|
145
367
|
const configPath = path.join(root, CONFIG_FILE);
|
|
@@ -147,24 +369,29 @@ export async function readPluginConfig(root) {
|
|
|
147
369
|
try {
|
|
148
370
|
config = JSON.parse(await readFile(configPath, 'utf8'));
|
|
149
371
|
} catch (error) {
|
|
150
|
-
if (error
|
|
151
|
-
|
|
372
|
+
if (!hasErrorCode(error, 'ENOENT')) {
|
|
373
|
+
throw new PluginScriptsError(`Could not read ${configPath}: ${errorMessage(error)}`, { cause: error });
|
|
374
|
+
}
|
|
375
|
+
config = {};
|
|
152
376
|
}
|
|
153
|
-
if (!isPlainObject(config)) throw new
|
|
154
|
-
const {
|
|
377
|
+
if (!isPlainObject(config)) throw new PluginScriptsError(`${configPath} must contain a JSON object.`);
|
|
378
|
+
const {
|
|
379
|
+
extraScripts = [],
|
|
380
|
+
webviewScripts = [],
|
|
381
|
+
mainBuildOptions = {},
|
|
382
|
+
contentScriptBuildOptions = {},
|
|
383
|
+
webviewScriptBuildOptions = {},
|
|
384
|
+
...unknown
|
|
385
|
+
} = config;
|
|
155
386
|
const unknownKeys = Object.keys(unknown);
|
|
156
387
|
if (unknownKeys.length > 0) {
|
|
157
|
-
throw new
|
|
158
|
-
}
|
|
159
|
-
if (!Array.isArray(extraScripts) || extraScripts.some((script) => typeof script !== 'string')) {
|
|
160
|
-
throw new Error(`${configPath} "extraScripts" must be an array of paths relative to src/.`);
|
|
161
|
-
}
|
|
162
|
-
if (!isPlainObject(mainBuildOptions)) {
|
|
163
|
-
throw new Error(`${configPath} "mainBuildOptions" must be an object of esbuild options.`);
|
|
164
|
-
}
|
|
165
|
-
const reserved = RESERVED_BUILD_OPTIONS.filter((key) => key in mainBuildOptions);
|
|
166
|
-
if (reserved.length > 0) {
|
|
167
|
-
throw new Error(`${configPath} "mainBuildOptions" cannot set ${reserved.join(', ')}.`);
|
|
388
|
+
throw new PluginScriptsError(`${configPath} has unknown keys: ${unknownKeys.join(', ')}.`);
|
|
168
389
|
}
|
|
169
|
-
|
|
390
|
+
assertScriptArray(extraScripts, 'extraScripts', configPath);
|
|
391
|
+
assertScriptArray(webviewScripts, 'webviewScripts', configPath);
|
|
392
|
+
assertValidScripts(configPath, extraScripts, webviewScripts);
|
|
393
|
+
assertBuildOptions(mainBuildOptions, 'mainBuildOptions', configPath);
|
|
394
|
+
assertBuildOptions(contentScriptBuildOptions, 'contentScriptBuildOptions', configPath);
|
|
395
|
+
assertBuildOptions(webviewScriptBuildOptions, 'webviewScriptBuildOptions', configPath);
|
|
396
|
+
return { extraScripts, webviewScripts, mainBuildOptions, contentScriptBuildOptions, webviewScriptBuildOptions };
|
|
170
397
|
}
|
package/lib/errors.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks whether a caught value is an error with the given Node.js error code, e.g. `ENOENT`.
|
|
3
|
+
* @param {unknown} error
|
|
4
|
+
* @param {string} code
|
|
5
|
+
* @returns {boolean}
|
|
6
|
+
*/
|
|
7
|
+
export function hasErrorCode(error, code) {
|
|
8
|
+
return error instanceof Error && 'code' in error && error.code === code;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns a caught value's message for use in a wrapping error or warning.
|
|
13
|
+
* @param {unknown} error
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
export function errorMessage(error) {
|
|
17
|
+
return error instanceof Error ? error.message : String(error);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* An expected failure caused by the plugin project or its environment, such as invalid
|
|
22
|
+
* configuration or out-of-sync versions. The CLI prints only its message, without a stack trace;
|
|
23
|
+
* any other error is treated as a bug and shown in full.
|
|
24
|
+
*/
|
|
25
|
+
export class PluginScriptsError extends Error {
|
|
26
|
+
name = 'PluginScriptsError';
|
|
27
|
+
}
|