@ham2k/extension-tools 0.5.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.mjs +35 -2
- 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.mjs
CHANGED
|
@@ -28,14 +28,19 @@ import yazl from "yazl"
|
|
|
28
28
|
///
|
|
29
29
|
/// Raised when a bundle using something new could not work at all on an app
|
|
30
30
|
/// without it — not for every addition. 2 adds `host.webSocket` and the
|
|
31
|
-
/// manifest's `webSockets` grant
|
|
32
|
-
export const H2KEXT_API_VERSION =
|
|
31
|
+
/// manifest's `webSockets` grant; 3 adds timers and `onShow`/`onHide`.
|
|
32
|
+
export const H2KEXT_API_VERSION = 3
|
|
33
33
|
|
|
34
34
|
/// The first API with `host.webSocket`. A manifest declaring `webSockets`
|
|
35
35
|
/// under an older one is refused: an older app would install it and fail only
|
|
36
36
|
/// when it tried to connect.
|
|
37
37
|
export const WEB_SOCKETS_API_VERSION = 2
|
|
38
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
|
|
43
|
+
|
|
39
44
|
/// `<prefix>-<name>`, all lowercase. The shape is what the kernel's
|
|
40
45
|
/// `bundleMayDefine` prefix rule can reason about; the PREFIX is what keeps two
|
|
41
46
|
/// authors who never met from choosing the same key.
|
|
@@ -127,6 +132,7 @@ export const SHARED_MODULES_BY_API = {
|
|
|
127
132
|
],
|
|
128
133
|
}
|
|
129
134
|
SHARED_MODULES_BY_API[2] = SHARED_MODULES_BY_API[1]
|
|
135
|
+
SHARED_MODULES_BY_API[3] = SHARED_MODULES_BY_API[2]
|
|
130
136
|
|
|
131
137
|
/// What a scaffolded extension declares on day one: the eight modules the SDK
|
|
132
138
|
/// barrel itself reaches, so a project that has written no code yet still
|
|
@@ -254,6 +260,31 @@ function calendarDateProblem(value) {
|
|
|
254
260
|
return at.getUTCFullYear() === year && at.getUTCMonth() === month - 1 && at.getUTCDate() === day ? null : "is not a day that exists"
|
|
255
261
|
}
|
|
256
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
|
+
|
|
257
288
|
/// Everything wrong with a candidate bundle, as a list — an author fixing one
|
|
258
289
|
/// problem per run is an author running this five times.
|
|
259
290
|
export function validateManifest(manifest, { source = MANIFEST_ENTRY, forceName = false } = {}) {
|
|
@@ -344,6 +375,8 @@ export function validateManifest(manifest, { source = MANIFEST_ENTRY, forceName
|
|
|
344
375
|
// parse as "does not gate", so a malformed one would ship as worldwide.
|
|
345
376
|
for (const problem of validateRelevance(manifest.relevance)) say(problem)
|
|
346
377
|
|
|
378
|
+
for (const problem of validateAdifFieldsScope(manifest)) say(problem)
|
|
379
|
+
|
|
347
380
|
return problems
|
|
348
381
|
}
|
|
349
382
|
|