@bwat47/joplin-plugin-scripts 1.0.0 → 1.1.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 CHANGED
@@ -40,13 +40,20 @@ Commands run from the plugin project root, which npm scripts do automatically.
40
40
  ```json
41
41
  {
42
42
  "extraScripts": ["contentScripts/codeMirror.ts"],
43
+ "webviewScripts": ["dialog/dialog.ts"],
43
44
  "mainBuildOptions": { "external": ["jsdom"] }
44
45
  }
45
46
  ```
46
47
 
47
- - `extraScripts`: content scripts relative to `src/`, bundled for the browser to the same
48
- path under `dist/` with a `.js` extension. Joplin's CodeMirror and Lezer packages stay
49
- external so they share the host's instances.
48
+ - `extraScripts`: content scripts registered with `joplin.contentScripts.register`
49
+ (CodeMirror and Markdown-it plugins), relative to `src/`. Each is bundled for the browser
50
+ as CommonJS to the same path under `dist/` with a `.js` extension. Joplin's CodeMirror
51
+ and Lezer packages stay external so they share the host's instances.
52
+ - `webviewScripts`: scripts loaded into a dialog or panel with `addScript`, relative to
53
+ `src/`. A webview loads them as plain `<script>` tags with no `module`, `exports`, or
54
+ `require`, so each is bundled as a self-contained IIFE with nothing external. Exports
55
+ are dropped, so the script must start itself, e.g. by calling its init function at the
56
+ top level. Plain `.js` files in `src/` are copied as-is and don't need listing.
50
57
  - `mainBuildOptions`: esbuild options merged over the main bundle's defaults. Relative
51
58
  `alias` targets resolve from the project root. `absWorkingDir`, `entryPoints`,
52
59
  `outfile`, and `outdir` cannot be overridden.
@@ -63,7 +70,7 @@ const { mainBuildOptions } = await readPluginConfig(root);
63
70
  const result = await build({ ...createBuildOptions(root, mainBuildOptions), write: false });
64
71
  ```
65
72
 
66
- `contentScriptExternals` is also exported.
73
+ `createWebviewScriptBuildOptions` and `contentScriptExternals` are also exported.
67
74
 
68
75
  ## Development
69
76
 
package/lib/build.mjs CHANGED
@@ -5,21 +5,27 @@ 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';
11
13
  import { packagePlugin, readProject } from './packagePlugin.mjs';
12
14
 
13
15
  /**
14
16
  * Builds the plugin at `root`: validates the project, clears `dist/` and `publish/`, copies runtime
15
- * assets from `src/`, bundles the main entry point and extra scripts, and creates the `.jpl` archive.
17
+ * assets from `src/`, bundles the main entry point, extra scripts, and webview scripts, and creates
18
+ * the `.jpl` archive.
16
19
  */
17
20
  export async function buildPlugin(root) {
18
21
  const dist = path.join(root, 'dist');
19
22
  const publish = path.join(root, 'publish');
20
23
  const { manifest } = await readProject(root);
21
- const { extraScripts, mainBuildOptions } = await readPluginConfig(root);
22
- const extraBuilds = extraScripts.map((script) => createContentScriptBuildOptions(root, script));
24
+ const { extraScripts, webviewScripts, mainBuildOptions } = await readPluginConfig(root);
25
+ const scriptBuilds = [
26
+ ...extraScripts.map((script) => createContentScriptBuildOptions(root, script)),
27
+ ...webviewScripts.map((script) => createWebviewScriptBuildOptions(root, script)),
28
+ ];
23
29
 
24
30
  // Only these fixed, project-local output directories are removed.
25
31
  await rm(dist, { recursive: true, force: true });
@@ -33,12 +39,8 @@ export async function buildPlugin(root) {
33
39
  });
34
40
  await Promise.all([
35
41
  build(createBuildOptions(root, mainBuildOptions)),
36
- ...extraBuilds.map((options) => build(options)),
42
+ ...scriptBuilds.map((options) => build(options)),
37
43
  ]);
38
44
 
39
- await packagePlugin(
40
- root,
41
- manifest,
42
- extraBuilds.map((options) => path.relative(dist, path.resolve(root, options.outfile)))
43
- );
45
+ await packagePlugin(root, manifest, scriptOutputPaths(extraScripts, webviewScripts));
44
46
  }
@@ -132,14 +132,41 @@ export function createContentScriptBuildOptions(root, script) {
132
132
  };
133
133
  }
134
134
 
135
+ /**
136
+ * Returns the esbuild options for one webview script, e.g. `dialog/resizeDialog.ts` (relative to
137
+ * `src/`) to `dist/dialog/resizeDialog.js`. Webview scripts are loaded by a `<script>` tag in a
138
+ * dialog or panel, which has no `module`, `exports`, or `require`, so they are bundled as a
139
+ * self-contained IIFE. Their exports are dropped; the script runs for its side effects.
140
+ * @param {string} root Plugin project root.
141
+ * @param {string} script Script path relative to `src/`.
142
+ * @returns {import('esbuild').BuildOptions}
143
+ */
144
+ export function createWebviewScriptBuildOptions(root, script) {
145
+ return {
146
+ ...createContentScriptBuildOptions(root, script),
147
+ format: 'iife',
148
+ // A webview has no host CodeMirror instance to share, so everything is bundled.
149
+ external: [],
150
+ };
151
+ }
152
+
153
+ /** Returns `dist/`-relative output paths for every script the plugin config registers. */
154
+ export function scriptOutputPaths(extraScripts, webviewScripts) {
155
+ return [...extraScripts, ...webviewScripts].map((script) => script.replace(SCRIPT_EXTENSION_PATTERN, '.js'));
156
+ }
157
+
158
+ function isStringArray(value) {
159
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
160
+ }
161
+
135
162
  function isPlainObject(value) {
136
163
  return typeof value === 'object' && value !== null && !Array.isArray(value);
137
164
  }
138
165
 
139
166
  /**
140
167
  * Reads and validates `<root>/plugin.config.json`. The file is optional; a plugin without one
141
- * has no extra scripts and uses the default main build options.
142
- * @returns {Promise<{ extraScripts: string[], mainBuildOptions: import('esbuild').BuildOptions }>}
168
+ * has no extra or webview scripts and uses the default main build options.
169
+ * @returns {Promise<{ extraScripts: string[], webviewScripts: string[], mainBuildOptions: import('esbuild').BuildOptions }>}
143
170
  */
144
171
  export async function readPluginConfig(root) {
145
172
  const configPath = path.join(root, CONFIG_FILE);
@@ -147,17 +174,27 @@ export async function readPluginConfig(root) {
147
174
  try {
148
175
  config = JSON.parse(await readFile(configPath, 'utf8'));
149
176
  } catch (error) {
150
- if (error.code === 'ENOENT') return { extraScripts: [], mainBuildOptions: {} };
177
+ if (error.code === 'ENOENT') return { extraScripts: [], webviewScripts: [], mainBuildOptions: {} };
151
178
  throw new Error(`Could not read ${configPath}: ${error.message}`, { cause: error });
152
179
  }
153
180
  if (!isPlainObject(config)) throw new Error(`${configPath} must contain a JSON object.`);
154
- const { extraScripts = [], mainBuildOptions = {}, ...unknown } = config;
181
+ const { extraScripts = [], webviewScripts = [], mainBuildOptions = {}, ...unknown } = config;
155
182
  const unknownKeys = Object.keys(unknown);
156
183
  if (unknownKeys.length > 0) {
157
184
  throw new Error(`${configPath} has unknown keys: ${unknownKeys.join(', ')}.`);
158
185
  }
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/.`);
186
+ for (const [key, value] of [
187
+ ['extraScripts', extraScripts],
188
+ ['webviewScripts', webviewScripts],
189
+ ]) {
190
+ if (!isStringArray(value)) {
191
+ throw new Error(`${configPath} "${key}" must be an array of paths relative to src/.`);
192
+ }
193
+ }
194
+ const outputs = scriptOutputPaths(extraScripts, webviewScripts);
195
+ const duplicate = outputs.find((output, index) => outputs.indexOf(output) !== index);
196
+ if (duplicate !== undefined) {
197
+ throw new Error(`${configPath} lists more than one script that builds to dist/${duplicate}.`);
161
198
  }
162
199
  if (!isPlainObject(mainBuildOptions)) {
163
200
  throw new Error(`${configPath} "mainBuildOptions" must be an object of esbuild options.`);
@@ -166,5 +203,5 @@ export async function readPluginConfig(root) {
166
203
  if (reserved.length > 0) {
167
204
  throw new Error(`${configPath} "mainBuildOptions" cannot set ${reserved.join(', ')}.`);
168
205
  }
169
- return { extraScripts, mainBuildOptions };
206
+ return { extraScripts, webviewScripts, mainBuildOptions };
170
207
  }
package/lib/index.mjs CHANGED
@@ -3,5 +3,6 @@ export {
3
3
  contentScriptExternals,
4
4
  createBuildOptions,
5
5
  createContentScriptBuildOptions,
6
+ createWebviewScriptBuildOptions,
6
7
  readPluginConfig,
7
8
  } from './buildConfig.mjs';
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@bwat47/joplin-plugin-scripts",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Shared esbuild build, packaging, version, and API-update scripts for Joplin plugins",
5
5
  "license": "MIT",
6
6
  "author": "bwat47",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/bwat47/joplin-plugin-scripts.git"
10
+ },
7
11
  "type": "module",
8
12
  "engines": {
9
13
  "node": ">=22"