@ham2k/extension-tools 0.4.0 → 0.6.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/h2kext-build.mjs +40 -2
- package/h2kext-pack.mjs +3 -0
- package/h2kext.mjs +52 -1
- package/package.json +1 -1
package/h2kext-build.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { copyFile, mkdir, readFile } from "node:fs/promises"
|
|
|
24
24
|
import { createRequire } from "node:module"
|
|
25
25
|
import { dirname, join, resolve } from "node:path"
|
|
26
26
|
|
|
27
|
-
import { H2KEXT_API_VERSION, MANIFEST_ENTRY, sharedModulesFor } from "./h2kext.mjs"
|
|
27
|
+
import { H2KEXT_API_VERSION, MANIFEST_ENTRY, TIMERS_API_VERSION, sharedModulesFor } from "./h2kext.mjs"
|
|
28
28
|
|
|
29
29
|
/// What an extension's own `build.mjs` says, for everything that has to write
|
|
30
30
|
/// one: `h2kext-init`'s scaffold, and the samples the SDK ships.
|
|
@@ -202,14 +202,51 @@ function liquidBrowserBuild(fromDir) {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/// Where the author's SDK keeps the file that stands in for the timer globals
|
|
206
|
+
/// (sdk/src/timers.ts), or null if it has none. Resolved from the CALLER's
|
|
207
|
+
/// directory for the same reason as [liquidBrowserBuild]: it must be the very
|
|
208
|
+
/// SDK copy the extension imports, or the bundle carries a second one whose
|
|
209
|
+
/// timers were never handed an identity.
|
|
210
|
+
///
|
|
211
|
+
/// Missing is not an error: a bundle that never schedules anything builds as
|
|
212
|
+
/// it always did, and one that does fails where it calls `setTimeout`. But an
|
|
213
|
+
/// SDK that is installed and simply too old gets a warning, since the manifest
|
|
214
|
+
/// has just claimed an API it cannot fully build.
|
|
215
|
+
function sdkTimersFile(fromDir) {
|
|
216
|
+
// Resolved under this process's conditions, while esbuild resolves the
|
|
217
|
+
// extension's own SDK import under its defaults. The two agree for an
|
|
218
|
+
// author's build, which passes no custom condition; this repo's own
|
|
219
|
+
// extensions pass [timers] explicitly rather than rely on it.
|
|
220
|
+
try {
|
|
221
|
+
return createRequire(resolve(fromDir, "noop.js")).resolve("@ham2k/extension-sdk/timers")
|
|
222
|
+
} catch (e) {
|
|
223
|
+
if (e.code === "ERR_PACKAGE_PATH_NOT_EXPORTED") {
|
|
224
|
+
console.warn(
|
|
225
|
+
"warning: extension API 3 wants an @ham2k/extension-sdk with timers — this one has none, so a bare setTimeout in the bundle is left undefined",
|
|
226
|
+
)
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
if (e.code === "MODULE_NOT_FOUND") return null
|
|
230
|
+
throw e
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
205
234
|
/// The esbuild options a Ham2K extension bundle is built with.
|
|
206
235
|
///
|
|
207
236
|
/// Everything here is a requirement of the runtime rather than a preference:
|
|
208
237
|
/// one IIFE because the sandbox evaluates a script and has no module loader;
|
|
209
238
|
/// `platform: 'neutral'` because there is no node and no DOM; es2020 because
|
|
210
239
|
/// that is what QuickJS-NG speaks.
|
|
211
|
-
|
|
240
|
+
///
|
|
241
|
+
/// The sandbox has no timer globals of its own, so from extension API 3 the
|
|
242
|
+
/// bundle's `setTimeout` and friends are INJECTED: every bare use, the npm
|
|
243
|
+
/// dependencies' included, becomes the SDK's, which runs on the extension's
|
|
244
|
+
/// own timers. [timers] names the file to inject when the SDK is not the
|
|
245
|
+
/// package the caller's directory resolves, and `false` injects nothing — the
|
|
246
|
+
/// kernel, which IS the implementation.
|
|
247
|
+
export function buildOptionsFor({ entry, outfile, manifest = {}, dir = process.cwd(), alias = {}, inline = [], timers, ...rest } = {}) {
|
|
212
248
|
const liquid = liquidBrowserBuild(dir)
|
|
249
|
+
const timersFile = timers !== false && (manifest.api ?? H2KEXT_API_VERSION) >= TIMERS_API_VERSION ? (timers ?? sdkTimersFile(dir)) : null
|
|
213
250
|
return {
|
|
214
251
|
entryPoints: [entry],
|
|
215
252
|
...(outfile ? { outfile } : { write: false }),
|
|
@@ -225,6 +262,7 @@ export function buildOptionsFor({ entry, outfile, manifest = {}, dir = process.c
|
|
|
225
262
|
...alias,
|
|
226
263
|
},
|
|
227
264
|
plugins: [sharedModulesPlugin({ sharedDependencies: manifest.sharedDependencies, api: manifest.api, inline }), ...(rest.plugins ?? [])],
|
|
265
|
+
inject: [...(timersFile ? [timersFile] : []), ...(rest.inject ?? [])],
|
|
228
266
|
}
|
|
229
267
|
}
|
|
230
268
|
|
package/h2kext-pack.mjs
CHANGED
|
@@ -64,6 +64,9 @@ try {
|
|
|
64
64
|
// person installing it will be asked to approve.
|
|
65
65
|
console.log(` may reach: ${result.manifest.domains.join(", ")}`)
|
|
66
66
|
}
|
|
67
|
+
if (result.manifest.webSockets?.length) {
|
|
68
|
+
console.log(` may hold connections to: ${result.manifest.webSockets.join(", ")}`)
|
|
69
|
+
}
|
|
67
70
|
} catch (e) {
|
|
68
71
|
if (e instanceof PackError) {
|
|
69
72
|
console.error(`h2kext-pack: ${dir} is not ready to package:\n${e.message}`)
|
package/h2kext.mjs
CHANGED
|
@@ -25,7 +25,21 @@ import yazl from "yazl"
|
|
|
25
25
|
/// The extension API this format describes. A bundle declares the version it
|
|
26
26
|
/// was built against and the host refuses one it does not speak, so an old app
|
|
27
27
|
/// meeting a newer bundle says so instead of failing at a call site.
|
|
28
|
-
|
|
28
|
+
///
|
|
29
|
+
/// Raised when a bundle using something new could not work at all on an app
|
|
30
|
+
/// without it — not for every addition. 2 adds `host.webSocket` and the
|
|
31
|
+
/// manifest's `webSockets` grant; 3 adds timers and `onShow`/`onHide`.
|
|
32
|
+
export const H2KEXT_API_VERSION = 3
|
|
33
|
+
|
|
34
|
+
/// The first API with `host.webSocket`. A manifest declaring `webSockets`
|
|
35
|
+
/// under an older one is refused: an older app would install it and fail only
|
|
36
|
+
/// when it tried to connect.
|
|
37
|
+
export const WEB_SOCKETS_API_VERSION = 2
|
|
38
|
+
|
|
39
|
+
/// The first API with timers. The build points a bundle's bare `setTimeout`
|
|
40
|
+
/// and friends at the SDK's only from here on: an older app's kernel hands an
|
|
41
|
+
/// extension no timers, and a bundle declaring less says it runs there.
|
|
42
|
+
export const TIMERS_API_VERSION = 3
|
|
29
43
|
|
|
30
44
|
/// `<prefix>-<name>`, all lowercase. The shape is what the kernel's
|
|
31
45
|
/// `bundleMayDefine` prefix rule can reason about; the PREFIX is what keeps two
|
|
@@ -117,6 +131,8 @@ export const SHARED_MODULES_BY_API = {
|
|
|
117
131
|
"liquidjs",
|
|
118
132
|
],
|
|
119
133
|
}
|
|
134
|
+
SHARED_MODULES_BY_API[2] = SHARED_MODULES_BY_API[1]
|
|
135
|
+
SHARED_MODULES_BY_API[3] = SHARED_MODULES_BY_API[2]
|
|
120
136
|
|
|
121
137
|
/// What a scaffolded extension declares on day one: the eight modules the SDK
|
|
122
138
|
/// barrel itself reaches, so a project that has written no code yet still
|
|
@@ -244,6 +260,31 @@ function calendarDateProblem(value) {
|
|
|
244
260
|
return at.getUTCFullYear() === year && at.getUTCMonth() === month - 1 && at.getUTCDate() === day ? null : "is not a day that exists"
|
|
245
261
|
}
|
|
246
262
|
|
|
263
|
+
/// Everything wrong with how a manifest scopes its `adifFields` hook, as a
|
|
264
|
+
/// list — empty for one with no such hook.
|
|
265
|
+
///
|
|
266
|
+
/// The full ADIF export asks an extension for fields only about a contact
|
|
267
|
+
/// carrying a reference that extension answers for, unless the manifest says
|
|
268
|
+
/// `adifFieldsScope: "always"`. A bundle that adds fields and declares neither
|
|
269
|
+
/// a `ref:` type nor the opt-in writes nothing to that file, and nothing at
|
|
270
|
+
/// export time says so. The opt-in is a manifest field rather than something
|
|
271
|
+
/// on the hook so that this check can see it without running the bundle.
|
|
272
|
+
///
|
|
273
|
+
/// Shared with build-config.mjs's manifest checks, like [validateRelevance].
|
|
274
|
+
export function validateAdifFieldsScope(manifest) {
|
|
275
|
+
const scope = manifest?.adifFieldsScope
|
|
276
|
+
if (scope !== undefined && scope !== "always") {
|
|
277
|
+
return [`'adifFieldsScope' must be "always", or left out to scope 'adifFields' to the references this extension handles`]
|
|
278
|
+
}
|
|
279
|
+
const hooks = Array.isArray(manifest?.hooks) ? manifest.hooks : []
|
|
280
|
+
if (!hooks.includes("adifFields") || scope === "always") return []
|
|
281
|
+
if (hooks.some((hook) => typeof hook === "string" && hook.startsWith("ref:"))) return []
|
|
282
|
+
return [
|
|
283
|
+
`'hooks' lists 'adifFields' but no 'ref:' type — the full ADIF export asks an extension only about contacts carrying a reference it handles, ` +
|
|
284
|
+
`so these fields would never be written; declare the 'ref:' types, or set "adifFieldsScope": "always" (docs/extensions/hooks.md)`,
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
|
|
247
288
|
/// Everything wrong with a candidate bundle, as a list — an author fixing one
|
|
248
289
|
/// problem per run is an author running this five times.
|
|
249
290
|
export function validateManifest(manifest, { source = MANIFEST_ENTRY, forceName = false } = {}) {
|
|
@@ -273,6 +314,14 @@ export function validateManifest(manifest, { source = MANIFEST_ENTRY, forceName
|
|
|
273
314
|
say(`'api' is ${manifest.api}, but this packer only knows ${H2KEXT_API_VERSION}`)
|
|
274
315
|
}
|
|
275
316
|
|
|
317
|
+
if (manifest.webSockets !== undefined) {
|
|
318
|
+
if (!Array.isArray(manifest.webSockets) || manifest.webSockets.some((host) => typeof host !== "string")) {
|
|
319
|
+
say("'webSockets' must be a list of hosts")
|
|
320
|
+
} else if (manifest.webSockets.length && Number.isInteger(manifest.api) && manifest.api < WEB_SOCKETS_API_VERSION) {
|
|
321
|
+
say(`'webSockets' needs 'api' ${WEB_SOCKETS_API_VERSION} or later — an app older than that would install this and fail only when it connects`)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
276
325
|
// An extension with no category is CORE to the host: always enabled, and
|
|
277
326
|
// never listed in the Extensions panel. That is a status the app grants its
|
|
278
327
|
// own extensions, not one a distributed bundle may claim for itself — a user
|
|
@@ -326,6 +375,8 @@ export function validateManifest(manifest, { source = MANIFEST_ENTRY, forceName
|
|
|
326
375
|
// parse as "does not gate", so a malformed one would ship as worldwide.
|
|
327
376
|
for (const problem of validateRelevance(manifest.relevance)) say(problem)
|
|
328
377
|
|
|
378
|
+
for (const problem of validateAdifFieldsScope(manifest)) say(problem)
|
|
379
|
+
|
|
329
380
|
return problems
|
|
330
381
|
}
|
|
331
382
|
|