@openbrf/plugin-sdk 0.0.0-stage → 1.0.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.
Files changed (79) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSE-EXCEPTION.md +28 -0
  3. package/dist/action-schema.d.ts +42 -0
  4. package/dist/action-schema.d.ts.map +1 -0
  5. package/dist/action-schema.js +64 -0
  6. package/dist/action-schema.js.map +1 -0
  7. package/dist/actions.d.ts +199 -0
  8. package/dist/actions.d.ts.map +1 -0
  9. package/dist/actions.js +68 -0
  10. package/dist/actions.js.map +1 -0
  11. package/dist/api-version.d.ts +23 -0
  12. package/dist/api-version.d.ts.map +1 -0
  13. package/dist/api-version.js +27 -0
  14. package/dist/api-version.js.map +1 -0
  15. package/dist/catalog.d.ts +315 -0
  16. package/dist/catalog.d.ts.map +1 -0
  17. package/dist/catalog.js +203 -0
  18. package/dist/catalog.js.map +1 -0
  19. package/dist/define-plugin.d.ts +14 -0
  20. package/dist/define-plugin.d.ts.map +1 -0
  21. package/dist/define-plugin.js +15 -0
  22. package/dist/define-plugin.js.map +1 -0
  23. package/dist/findings.d.ts +67 -0
  24. package/dist/findings.d.ts.map +1 -0
  25. package/dist/findings.js +57 -0
  26. package/dist/findings.js.map +1 -0
  27. package/dist/host.d.ts +208 -0
  28. package/dist/host.d.ts.map +1 -0
  29. package/dist/host.js +41 -0
  30. package/dist/host.js.map +1 -0
  31. package/dist/index.d.ts +24 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +22 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/integrity.d.ts +38 -0
  36. package/dist/integrity.d.ts.map +1 -0
  37. package/dist/integrity.js +105 -0
  38. package/dist/integrity.js.map +1 -0
  39. package/dist/manifest.d.ts +392 -0
  40. package/dist/manifest.d.ts.map +1 -0
  41. package/dist/manifest.js +324 -0
  42. package/dist/manifest.js.map +1 -0
  43. package/dist/module.d.ts +35 -0
  44. package/dist/module.d.ts.map +1 -0
  45. package/dist/module.js +2 -0
  46. package/dist/module.js.map +1 -0
  47. package/dist/package-check.d.ts +66 -0
  48. package/dist/package-check.d.ts.map +1 -0
  49. package/dist/package-check.js +755 -0
  50. package/dist/package-check.js.map +1 -0
  51. package/dist/permissions.d.ts +65 -0
  52. package/dist/permissions.d.ts.map +1 -0
  53. package/dist/permissions.js +69 -0
  54. package/dist/permissions.js.map +1 -0
  55. package/dist/settings-schema.d.ts +95 -0
  56. package/dist/settings-schema.d.ts.map +1 -0
  57. package/dist/settings-schema.js +219 -0
  58. package/dist/settings-schema.js.map +1 -0
  59. package/dist/standard-schema.d.ts +43 -0
  60. package/dist/standard-schema.d.ts.map +1 -0
  61. package/dist/standard-schema.js +2 -0
  62. package/dist/standard-schema.js.map +1 -0
  63. package/package.json +55 -3
  64. package/src/action-schema.ts +86 -0
  65. package/src/actions.ts +247 -0
  66. package/src/api-version.ts +28 -0
  67. package/src/catalog.ts +236 -0
  68. package/src/define-plugin.ts +18 -0
  69. package/src/findings.ts +71 -0
  70. package/src/host.ts +240 -0
  71. package/src/index.ts +123 -0
  72. package/src/integrity.ts +125 -0
  73. package/src/manifest.ts +385 -0
  74. package/src/module.ts +38 -0
  75. package/src/package-check.ts +894 -0
  76. package/src/permissions.ts +75 -0
  77. package/src/settings-schema.ts +272 -0
  78. package/src/standard-schema.ts +39 -0
  79. package/README.md +0 -3
@@ -0,0 +1,755 @@
1
+ import { dependencyNames, isEmptyDependencyField, parsePluginPackage, RUNTIME_DEPENDENCY_FIELDS, } from "./manifest.js";
2
+ /**
3
+ * The checks a plugin package passes before it is listed.
4
+ *
5
+ * Everything here is something an instance would otherwise find out after the
6
+ * board had consented and the application had restarted: a dependency the
7
+ * installer cannot fetch without a registry, a bundle reaching for a module it
8
+ * cannot resolve from the data volume, an entry the loader cannot find. Run
9
+ * against the packed tarball rather than the working tree, it answers whether
10
+ * what is published is installable, which is what a plugin's own CI and the
11
+ * catalog's check both need to know.
12
+ *
13
+ * It answers nothing about whether the package is safe. A plugin runs in the
14
+ * host's process with its privileges (ADR 0003), every Node built-in is open
15
+ * to it, and code has more ways to load a module than any reading of the
16
+ * source can follow. A package that passes installs and loads; what it does
17
+ * once loaded is for a person reading its code before it is listed.
18
+ *
19
+ * Pure: the caller reads the package and hands its contents in, so the same
20
+ * function serves a script, a test and a check that has the archive in
21
+ * memory.
22
+ */
23
+ /**
24
+ * Packages a plugin must share with the host rather than carry its own copy of.
25
+ *
26
+ * Two reasons a package belongs here, and they are different.
27
+ *
28
+ * The Nest packages hold process-wide state - a DI container, a metadata
29
+ * registry - so a second copy is not a duplicate but a second and disconnected
30
+ * system: decorators from one are invisible to the other.
31
+ *
32
+ * zod holds no state at all, and is here for a reason about identity rather
33
+ * than about state. An action's input and output schemas cross from the plugin
34
+ * into the host, which converts them to the JSON Schema a caller is published
35
+ * and validates against them on every call. A schema built by a second copy
36
+ * carries that copy's internals, so the host's realm check refuses it and its
37
+ * converter could not read it. What matters here is therefore which module the
38
+ * object came from, not what that module remembers.
39
+ *
40
+ * With Node's built-in modules, they are also the only modules a server bundle
41
+ * may require: an installed plugin sits on the data volume, where nothing but
42
+ * these, bridged in from the host, and the built-ins can be resolved.
43
+ */
44
+ export const HOST_SHARED_PACKAGES = [
45
+ "@nestjs/common",
46
+ "@nestjs/core",
47
+ "zod",
48
+ ];
49
+ /**
50
+ * Words after which a `/` opens a regular expression rather than dividing.
51
+ *
52
+ * After any other word, a number, a string or a closing bracket it divides.
53
+ * Getting this wrong only matters inside the one line the misread literal
54
+ * sits on, because a string never runs past the end of its line.
55
+ */
56
+ const KEYWORDS_BEFORE_EXPRESSION = new Set([
57
+ "await",
58
+ "case",
59
+ "delete",
60
+ "do",
61
+ "else",
62
+ "in",
63
+ "instanceof",
64
+ "new",
65
+ "of",
66
+ "return",
67
+ "throw",
68
+ "typeof",
69
+ "void",
70
+ "yield",
71
+ ]);
72
+ /**
73
+ * The words in KEYWORDS_BEFORE_EXPRESSION that a script may also use as a
74
+ * plain name, so that a `/` after one may divide: `await / 2`. After the
75
+ * others, such as `return`, a `/` always opens a regular expression.
76
+ */
77
+ const KEYWORDS_THAT_MAY_BE_NAMES = new Set([
78
+ "await",
79
+ "of",
80
+ "yield",
81
+ ]);
82
+ /**
83
+ * Tokens after which a `/` divides. "postfix" stands for a `++` or `--` that
84
+ * follows its operand on the same line; before its operand, as in
85
+ * `++/a/.lastIndex`, a `/` opens a regular expression.
86
+ */
87
+ const TOKENS_BEFORE_DIVISION = new Set([
88
+ "literal",
89
+ "postfix",
90
+ ")",
91
+ "]",
92
+ "}",
93
+ ]);
94
+ /**
95
+ * Tokens after which a method name can stand, in a class body or an object
96
+ * literal: `{ require(name) { ... } }`, `static require(name) { ... }`.
97
+ */
98
+ const TOKENS_BEFORE_METHOD_NAME = new Set([
99
+ "{",
100
+ ",",
101
+ ";",
102
+ "}",
103
+ "*",
104
+ "async",
105
+ "get",
106
+ "set",
107
+ "static",
108
+ ]);
109
+ const IDENTIFIER_START = /[\p{ID_Start}$_]/u;
110
+ const IDENTIFIER_PART = /[\p{ID_Continue}$\u200c\u200d]/u;
111
+ /** A `\uXXXX` escape, or a `\u{...}` one, which may pad its digits with zeros. */
112
+ const UNICODE_ESCAPE = /\\u(?:([0-9a-fA-F]{4})|\{([0-9a-fA-F]+)\})/y;
113
+ /** The characters that end a line in JavaScript, and so a line comment. */
114
+ const LINE_TERMINATOR = /[\n\r\u2028\u2029]/;
115
+ const NEXT_LINE_TERMINATOR = new RegExp(LINE_TERMINATOR.source, "g");
116
+ /**
117
+ * Punctuators of more than one character that the scanner tells apart: `++`
118
+ * and `--` because a `/` after a postfix one divides, `?.` because it reaches
119
+ * a property as `.` does, and `...` because it spreads rather than reaching
120
+ * one.
121
+ */
122
+ const LONG_PUNCTUATORS = ["...", "?.", "++", "--"];
123
+ /**
124
+ * Every problem that would stop a package installing or loading, one English
125
+ * sentence each, for an author's terminal. Empty when there is none.
126
+ */
127
+ export function pluginPackageProblems(contents) {
128
+ const problems = [];
129
+ const packageJson = asRecord(contents.packageJson);
130
+ // The dependency fields get sentences of their own below, which name what to
131
+ // do instead, so the manifest is read without them rather than reported twice.
132
+ const parsed = parsePluginPackage(withoutRuntimeDependencies(packageJson));
133
+ if (!parsed.ok) {
134
+ for (const issue of parsed.issues) {
135
+ problems.push(`The manifest in package.json is invalid at ${issue}.`);
136
+ }
137
+ }
138
+ problems.push(...dependencyProblems(packageJson));
139
+ if (parsed.ok) {
140
+ const entries = parsed.value.openbrf.entry;
141
+ const files = new Set(contents.files.map(normalizedPath));
142
+ for (const [kind, declared] of [
143
+ ["server", entries.server],
144
+ ["client", entries.client],
145
+ ]) {
146
+ if (declared !== undefined && !files.has(normalizedPath(declared))) {
147
+ problems.push(`The manifest declares ${declared} as the ${kind} entry, and the package does not contain it.`);
148
+ }
149
+ }
150
+ if (entries.server !== undefined && contents.serverBundle !== null) {
151
+ problems.push(...serverBundleProblems(contents.serverBundle));
152
+ }
153
+ }
154
+ problems.push(...localeProblems(contents));
155
+ return problems;
156
+ }
157
+ function dependencyProblems(packageJson) {
158
+ if (packageJson === null) {
159
+ return [];
160
+ }
161
+ const problems = [];
162
+ for (const field of RUNTIME_DEPENDENCY_FIELDS) {
163
+ const value = packageJson[field];
164
+ if (value !== undefined &&
165
+ dependencyNames(value).length === 0 &&
166
+ !isEmptyDependencyField(value)) {
167
+ problems.push(`package.json gives ${field} as something other than a map or a list of packages. A plugin declares no runtime dependencies: leave the field out, or leave it empty.`);
168
+ continue;
169
+ }
170
+ // A host package gets its own sentence below, which says what to do
171
+ // instead; naming it here as well would report one entry twice.
172
+ const names = dependencyNames(packageJson[field]).filter((name) => !HOST_SHARED_PACKAGES.includes(name));
173
+ if (names.length > 0) {
174
+ problems.push(`package.json lists ${names.join(", ")} under ${field}. A plugin declares no runtime dependencies: the installer's npm would fetch them from a registry, which an instance never contacts.`);
175
+ }
176
+ }
177
+ for (const host of HOST_SHARED_PACKAGES) {
178
+ for (const field of RUNTIME_DEPENDENCY_FIELDS) {
179
+ if (dependencyNames(packageJson[field]).includes(host)) {
180
+ problems.push(`package.json lists ${host} under ${field}. The host shares its own copy, so declare it under peerDependencies and never bundle it.`);
181
+ }
182
+ }
183
+ }
184
+ return problems;
185
+ }
186
+ function serverBundleProblems(source) {
187
+ const problems = [];
188
+ const { specifiers, computed } = requireCalls(source);
189
+ const foreign = [
190
+ ...new Set(specifiers.filter((specifier) => !isHostPackage(specifier) && !isNodeBuiltinModule(specifier))),
191
+ ];
192
+ if (foreign.length > 0) {
193
+ problems.push(`The server bundle requires ${foreign.join(", ")}. Its only externals may be the host packages (${HOST_SHARED_PACKAGES.join(", ")}) and Node's built-in modules; an installed plugin cannot resolve anything else.`);
194
+ }
195
+ if (computed) {
196
+ problems.push("The server bundle calls require with something other than a string literal, so what it loads cannot be checked.");
197
+ }
198
+ if (!source.includes("exports.createPlugin")) {
199
+ problems.push("The server bundle does not assign exports.createPlugin. The host loads it with require and reads that export.");
200
+ }
201
+ return problems;
202
+ }
203
+ /**
204
+ * Whether a specifier names a host package or a path inside one.
205
+ *
206
+ * `zod/v4` resolves through the same bridge as `zod`: the bridge makes the
207
+ * host's node_modules a fallback for every lookup, not for a list of names.
208
+ */
209
+ function isHostPackage(specifier) {
210
+ return HOST_SHARED_PACKAGES.some((host) => specifier === host || specifier.startsWith(`${host}/`));
211
+ }
212
+ /**
213
+ * Every call to require in a bundle: the specifiers written as plain string
214
+ * literals, and whether any call's argument is something else.
215
+ *
216
+ * One pass finds the calls and reads each argument, so a spelling of the call
217
+ * is either seen by both questions or by neither, and a call the literal
218
+ * reading does not recognise is a computed one rather than no call at all.
219
+ *
220
+ * A scanner rather than a regular expression, for two reasons. The bundle is
221
+ * a file someone outside the project submits, and a pattern that lets
222
+ * comments stand between `require` and its parenthesis can be made to
223
+ * backtrack for hours; this reads each character a bounded number of times.
224
+ * And it knows where code is: `require(` inside a comment, a string, template
225
+ * text or a regular expression is not a call, and neither is a method of that
226
+ * name on some other object. `module.require` is the one exception, since it
227
+ * is the same loader under another name.
228
+ *
229
+ * A method or accessor named require, `{ require(name) { ... } }`, reads like
230
+ * a call up to its closing parenthesis, and only the `{` after that tells the
231
+ * two apart. So a call is recorded when the scanner reaches its closing
232
+ * parenthesis rather than when it reads the word, which keeps the reading a
233
+ * single pass. Every doubt goes to the call, since a check that fails a
234
+ * method is better than one that passes a call:
235
+ *
236
+ * - The word must stand where a method name can, after `{`, `,`, `;`, `}`,
237
+ * `static`, `get`, `set`, `async` or `*`. In
238
+ * `class A extends require("x") {}` the `{` opens the class body.
239
+ * - The `{` must be on the same line: there a line break may be the end of a
240
+ * statement.
241
+ * - No `/` that the scanner may have misread may stand between the
242
+ * parentheses, since a `)` inside a misread regular expression or division
243
+ * would close the call early. That is every `/` read as a division, and a
244
+ * regular expression read after a word that may be a name, as in
245
+ * `await / 2`. A regular expression after a punctuator or a reserved word,
246
+ * as in `require(pattern = /x/) {}`, cannot be a division.
247
+ */
248
+ function requireCalls(source) {
249
+ const specifiers = [];
250
+ let computed = false;
251
+ const record = (call) => {
252
+ if (call === undefined) {
253
+ return;
254
+ }
255
+ else if (call.specifier === null) {
256
+ computed = true;
257
+ }
258
+ else {
259
+ specifiers.push(call.specifier);
260
+ }
261
+ };
262
+ /**
263
+ * A require whose parenthesis is the next one the scanner reaches, and the
264
+ * open parentheses, each with the require it opened, if it opened one.
265
+ */
266
+ let awaiting;
267
+ const parentheses = [];
268
+ /**
269
+ * How many `/` the scanner has read that could be misread: divisions, and
270
+ * regular expressions after a word.
271
+ */
272
+ let slashes = 0;
273
+ /**
274
+ * The last three words or punctuators seen, newest last, and whether a line
275
+ * has ended since the newest, in a comment or not.
276
+ */
277
+ const recent = [];
278
+ let lineBreak = false;
279
+ const remember = (token) => {
280
+ lineBreak = false;
281
+ recent.push(token);
282
+ if (recent.length > 3) {
283
+ recent.shift();
284
+ }
285
+ };
286
+ const previous = (back) => recent.at(-back);
287
+ /** Brace depth, and the depths at which a template's `${` was opened. */
288
+ let depth = 0;
289
+ const substitutions = [];
290
+ /** Skips template text; code resumes after it, inside a `${` or not. */
291
+ const templateText = (from) => {
292
+ const end = afterTemplateText(source, from);
293
+ if (source[end - 1] === "{") {
294
+ depth += 1;
295
+ substitutions.push(depth);
296
+ remember("{");
297
+ }
298
+ else {
299
+ remember("literal");
300
+ }
301
+ return end;
302
+ };
303
+ let index = 0;
304
+ while (index < source.length) {
305
+ const char = source[index];
306
+ const next = source[index + 1];
307
+ if (/\s/.test(char)) {
308
+ lineBreak ||= LINE_TERMINATOR.test(char);
309
+ index += 1;
310
+ }
311
+ else if (char === "/" && (next === "/" || next === "*")) {
312
+ const end = afterComment(source, index);
313
+ lineBreak ||= LINE_TERMINATOR.test(source.slice(index, end));
314
+ index = end;
315
+ }
316
+ else if (char === '"' || char === "'") {
317
+ index = afterString(source, index).end;
318
+ remember("literal");
319
+ }
320
+ else if (char === "`") {
321
+ index = templateText(index + 1);
322
+ }
323
+ else if (char === "/" && startsExpression(previous)) {
324
+ // After a name the guess can be wrong: `await / 2` divides.
325
+ if (KEYWORDS_THAT_MAY_BE_NAMES.has(previous(1) ?? "")) {
326
+ slashes += 1;
327
+ }
328
+ index = afterRegularExpression(source, index);
329
+ remember("literal");
330
+ }
331
+ else if (char === "#" && startsIdentifier(source, index + 1)) {
332
+ // A private name, `#require`, which is never the loader.
333
+ const identifier = afterIdentifier(source, index + 1);
334
+ remember(`#${identifier.name}`);
335
+ index = identifier.end;
336
+ }
337
+ else if (startsIdentifier(source, index)) {
338
+ const { name: word, end } = afterIdentifier(source, index);
339
+ // `typeof require` and `require.resolve` name the loader without
340
+ // calling it, so they leave nothing awaiting.
341
+ const argument = word === "require" && isLoaderReference(previous)
342
+ ? requireArgument(source, end)
343
+ : "not-a-call";
344
+ if (argument !== "not-a-call") {
345
+ awaiting = {
346
+ specifier: argument,
347
+ mayBeMethod: TOKENS_BEFORE_METHOD_NAME.has(previous(1) ?? ""),
348
+ slashes,
349
+ };
350
+ }
351
+ remember(word);
352
+ index = end;
353
+ }
354
+ else if (char === "(") {
355
+ parentheses.push(awaiting);
356
+ awaiting = undefined;
357
+ remember(char);
358
+ index += 1;
359
+ }
360
+ else if (char === ")") {
361
+ const call = parentheses.pop();
362
+ const parameterList = call !== undefined &&
363
+ call.mayBeMethod &&
364
+ call.slashes === slashes &&
365
+ opensBody(source, index + 1);
366
+ if (!parameterList) {
367
+ record(call);
368
+ }
369
+ remember(char);
370
+ index += 1;
371
+ }
372
+ else if (/[0-9]/.test(char)) {
373
+ index += 1;
374
+ while (index < source.length && /[\w.]/.test(source[index])) {
375
+ index += 1;
376
+ }
377
+ remember("literal");
378
+ }
379
+ else if (char === "}" && substitutions.at(-1) === depth) {
380
+ // The end of a template's `${ }`: its text resumes here.
381
+ substitutions.pop();
382
+ depth -= 1;
383
+ index = templateText(index + 1);
384
+ }
385
+ else {
386
+ if (char === "{") {
387
+ depth += 1;
388
+ }
389
+ else if (char === "}") {
390
+ depth -= 1;
391
+ }
392
+ if (char === "/") {
393
+ slashes += 1;
394
+ }
395
+ const punctuator = longPunctuator(source, index) ?? char;
396
+ // `a++ / b` divides, `++/a/.lastIndex` does not: a postfix operator
397
+ // follows what could end an expression, on the same line. After a line
398
+ // break, `a\n++/a/.lastIndex` is two statements.
399
+ const postfix = (punctuator === "++" || punctuator === "--") &&
400
+ !lineBreak &&
401
+ !startsExpression(previous);
402
+ remember(postfix ? "postfix" : punctuator);
403
+ index += punctuator.length;
404
+ }
405
+ }
406
+ // A call whose parenthesis is left open is still counted.
407
+ for (const call of parentheses) {
408
+ record(call);
409
+ }
410
+ return { specifiers, computed };
411
+ }
412
+ /** The multi-character punctuator starting at `from`, if there is one. */
413
+ function longPunctuator(source, from) {
414
+ return LONG_PUNCTUATORS.find((punctuator) => source.startsWith(punctuator, from) &&
415
+ // `a ?.5 : b` is a conditional, not an optional chain.
416
+ !(punctuator === "?." && /[0-9]/.test(source[from + 2] ?? "")));
417
+ }
418
+ /**
419
+ * Whether a `require` the scanner reached names the loader: a bare name, or
420
+ * `module.require`, but not `function require` or another object's method.
421
+ */
422
+ function isLoaderReference(previous) {
423
+ const reachesProperty = (token) => token === "." || token === "?.";
424
+ if (previous(1) === "function") {
425
+ return false;
426
+ }
427
+ if (!reachesProperty(previous(1))) {
428
+ return true;
429
+ }
430
+ return previous(2) === "module" && !reachesProperty(previous(3));
431
+ }
432
+ /**
433
+ * Whether a `{` opening a function body follows the closing parenthesis just
434
+ * before `from` on the same line, which makes what that parenthesis closed a
435
+ * parameter list rather than a call's arguments.
436
+ */
437
+ function opensBody(source, from) {
438
+ const end = afterGap(source, from);
439
+ // Only the gap itself: searching on to the next line break would rescan the
440
+ // rest of a one-line bundle at every parenthesis.
441
+ return source[end] === "{" && !LINE_TERMINATOR.test(source.slice(from, end));
442
+ }
443
+ /** Whether a `/` after the newest token opens a regular expression. */
444
+ function startsExpression(previous) {
445
+ const token = previous(1);
446
+ if (token === undefined) {
447
+ return true;
448
+ }
449
+ if (TOKENS_BEFORE_DIVISION.has(token)) {
450
+ return false;
451
+ }
452
+ if (isWord(token)) {
453
+ // After a `.`, a word is a property name, whatever it is spelled:
454
+ // `o.of / 2` divides.
455
+ return (!(previous(2) === "." || previous(2) === "?.") &&
456
+ KEYWORDS_BEFORE_EXPRESSION.has(token));
457
+ }
458
+ return true;
459
+ }
460
+ /**
461
+ * Whether a remembered token is a name or a keyword, rather than a punctuator
462
+ * or one of the stand-ins "literal" and "postfix".
463
+ */
464
+ function isWord(token) {
465
+ return (token !== undefined &&
466
+ !TOKENS_BEFORE_DIVISION.has(token) &&
467
+ (token.startsWith("#") || IDENTIFIER_START.test(characterAt(token, 0))));
468
+ }
469
+ /**
470
+ * The character at `from`, which is two UTF-16 code units when it lies outside
471
+ * the Basic Multilingual Plane, as `𐐀` does; empty past the end.
472
+ */
473
+ function characterAt(source, from) {
474
+ const code = source.codePointAt(from);
475
+ return code === undefined ? "" : String.fromCodePoint(code);
476
+ }
477
+ /**
478
+ * Whether an identifier starts at `from`, spelled out or opening with a
479
+ * Unicode escape: `requ\u0069re` is the same name as `require`.
480
+ */
481
+ function startsIdentifier(source, from) {
482
+ return (IDENTIFIER_START.test(characterAt(source, from)) ||
483
+ unicodeEscape(source, from) !== null);
484
+ }
485
+ /** Past the identifier starting at `from`, and its name with escapes read. */
486
+ function afterIdentifier(source, from) {
487
+ // The name is sliced from the source up to the first escape, and built
488
+ // from there on.
489
+ let name;
490
+ let index = from;
491
+ while (index < source.length) {
492
+ const escape = unicodeEscape(source, index);
493
+ const char = characterAt(source, index);
494
+ if (escape !== null) {
495
+ name = (name ?? source.slice(from, index)) + escape.char;
496
+ index = escape.end;
497
+ }
498
+ else if (index === from ? IDENTIFIER_START.test(char) : IDENTIFIER_PART.test(char)) {
499
+ if (name !== undefined) {
500
+ name += char;
501
+ }
502
+ index += char.length;
503
+ }
504
+ else {
505
+ break;
506
+ }
507
+ }
508
+ return { end: index, name: name ?? source.slice(from, index) };
509
+ }
510
+ /** The `\uXXXX` or `\u{X}` escape at `from`: the character, and past it. */
511
+ function unicodeEscape(source, from) {
512
+ if (source[from] !== "\\") {
513
+ return null;
514
+ }
515
+ UNICODE_ESCAPE.lastIndex = from;
516
+ const match = UNICODE_ESCAPE.exec(source);
517
+ const code = match === null
518
+ ? Number.NaN
519
+ : Number.parseInt(match[1] ?? match[2] ?? "", 16);
520
+ if (match === null || code > 0x10ffff) {
521
+ return null;
522
+ }
523
+ return { char: String.fromCodePoint(code), end: from + match[0].length };
524
+ }
525
+ /**
526
+ * What a `require` is called with, read from just after the word: the
527
+ * specifier when it is one plain string literal and nothing else, null when
528
+ * it is anything else, and "not-a-call" when no parenthesis follows. An
529
+ * optional call, `require?.(name)`, is a call.
530
+ */
531
+ function requireArgument(source, from) {
532
+ let index = afterGap(source, from);
533
+ if (longPunctuator(source, index) === "?.") {
534
+ index = afterGap(source, index + 2);
535
+ }
536
+ if (source[index] !== "(") {
537
+ return "not-a-call";
538
+ }
539
+ index = afterGap(source, index + 1);
540
+ const quote = source[index];
541
+ if (quote !== '"' && quote !== "'") {
542
+ return null;
543
+ }
544
+ const literal = afterString(source, index);
545
+ if (!literal.plain || literal.end - index <= 2) {
546
+ return null;
547
+ }
548
+ if (source[afterGap(source, literal.end)] !== ")") {
549
+ return null;
550
+ }
551
+ return source.slice(index + 1, literal.end - 1);
552
+ }
553
+ /** Past whitespace and comments, which may stand between parts of a call. */
554
+ function afterGap(source, from) {
555
+ let index = from;
556
+ while (index < source.length) {
557
+ const char = source[index];
558
+ if (/\s/.test(char)) {
559
+ index += 1;
560
+ }
561
+ else if (char === "/" &&
562
+ (source[index + 1] === "/" || source[index + 1] === "*")) {
563
+ index = afterComment(source, index);
564
+ }
565
+ else {
566
+ break;
567
+ }
568
+ }
569
+ return index;
570
+ }
571
+ /** Past the comment starting at `from`, which is a `/` followed by `/` or `*`. */
572
+ function afterComment(source, from) {
573
+ if (source[from + 1] === "/") {
574
+ NEXT_LINE_TERMINATOR.lastIndex = from + 2;
575
+ const end = NEXT_LINE_TERMINATOR.exec(source);
576
+ return end === null ? source.length : end.index + 1;
577
+ }
578
+ const end = source.indexOf("*/", from + 2);
579
+ return end === -1 ? source.length : end + 2;
580
+ }
581
+ /**
582
+ * Past the string literal opening at `from`, and whether it was plain: no
583
+ * escape inside it, and closed on the line it opened on. An unclosed string
584
+ * ends at the line break, so a misread quote costs one line at most.
585
+ *
586
+ * Only CR and LF end a string: U+2028 and U+2029 may stand inside one.
587
+ */
588
+ function afterString(source, from) {
589
+ const quote = source[from];
590
+ let plain = true;
591
+ let index = from + 1;
592
+ while (index < source.length) {
593
+ const char = source[index];
594
+ if (char === "\\") {
595
+ plain = false;
596
+ // A backslash before CRLF continues the string onto the next line.
597
+ index += source.startsWith("\r\n", index + 1) ? 3 : 2;
598
+ }
599
+ else if (char === quote) {
600
+ return { end: index + 1, plain };
601
+ }
602
+ else if (char === "\n" || char === "\r") {
603
+ return { end: index, plain: false };
604
+ }
605
+ else {
606
+ index += 1;
607
+ }
608
+ }
609
+ return { end: source.length, plain: false };
610
+ }
611
+ /**
612
+ * Past template text starting at `from`: to just after the closing backtick,
613
+ * or just after a `${`, where code resumes.
614
+ */
615
+ function afterTemplateText(source, from) {
616
+ let index = from;
617
+ while (index < source.length) {
618
+ const char = source[index];
619
+ if (char === "\\") {
620
+ index += 2;
621
+ }
622
+ else if (char === "`") {
623
+ return index + 1;
624
+ }
625
+ else if (char === "$" && source[index + 1] === "{") {
626
+ return index + 2;
627
+ }
628
+ else {
629
+ index += 1;
630
+ }
631
+ }
632
+ return source.length;
633
+ }
634
+ /** Past the regular expression literal opening at `from`, flags included. */
635
+ function afterRegularExpression(source, from) {
636
+ let index = from + 1;
637
+ let inClass = false;
638
+ while (index < source.length) {
639
+ const char = source[index];
640
+ if (LINE_TERMINATOR.test(char)) {
641
+ return index;
642
+ }
643
+ if (char === "\\") {
644
+ // Not even an escaped line break continues a regular expression.
645
+ if (LINE_TERMINATOR.test(source[index + 1] ?? "")) {
646
+ return index + 1;
647
+ }
648
+ index += 2;
649
+ continue;
650
+ }
651
+ if (char === "[") {
652
+ inClass = true;
653
+ }
654
+ else if (char === "]") {
655
+ inClass = false;
656
+ }
657
+ else if (char === "/" && !inClass) {
658
+ index += 1;
659
+ while (index < source.length && /\w/.test(source[index])) {
660
+ index += 1;
661
+ }
662
+ return index;
663
+ }
664
+ index += 1;
665
+ }
666
+ return source.length;
667
+ }
668
+ /**
669
+ * Whether a specifier names one of Node's built-in modules.
670
+ *
671
+ * A built-in resolves from any directory, the data volume included, and a
672
+ * plugin already runs in the host's process, so requiring one is outside what
673
+ * the host-package rule exists for.
674
+ */
675
+ function isNodeBuiltinModule(specifier) {
676
+ return specifier.startsWith("node:") || nodeBuiltinModules().has(specifier);
677
+ }
678
+ let builtinModules;
679
+ /**
680
+ * `module.builtinModules`, read through `process.getBuiltinModule`.
681
+ *
682
+ * Not imported from `node:module`: this package is also bundled into the
683
+ * browser for its constants, where that module does not exist. Outside Node
684
+ * the set is empty, and only a `node:` specifier is recognised.
685
+ */
686
+ function nodeBuiltinModules() {
687
+ if (builtinModules === undefined) {
688
+ const host = globalThis.process;
689
+ const moduleApi = host?.getBuiltinModule?.("node:module");
690
+ builtinModules = new Set(moduleApi?.builtinModules ?? []);
691
+ }
692
+ return builtinModules;
693
+ }
694
+ function localeProblems(contents) {
695
+ const problems = [];
696
+ const files = new Set(contents.files.map(normalizedPath));
697
+ const keys = {};
698
+ for (const language of ["sv", "en"]) {
699
+ const path = `locales/${language}.json`;
700
+ const parsed = contents.locales[language];
701
+ if (!files.has(path) || parsed === undefined) {
702
+ problems.push(`The package has no ${path}.`);
703
+ continue;
704
+ }
705
+ const record = asRecord(parsed);
706
+ if (record === null) {
707
+ problems.push(`${path} is not a JSON object of translation keys.`);
708
+ continue;
709
+ }
710
+ keys[language] = new Set(leafKeys(record));
711
+ }
712
+ const { sv, en } = keys;
713
+ if (sv !== undefined && en !== undefined) {
714
+ const missingFromSv = [...en].filter((key) => !sv.has(key));
715
+ const missingFromEn = [...sv].filter((key) => !en.has(key));
716
+ if (missingFromSv.length > 0) {
717
+ problems.push(`locales/sv.json lacks ${missingFromSv.join(", ")}, which locales/en.json has. The two carry identical keys.`);
718
+ }
719
+ if (missingFromEn.length > 0) {
720
+ problems.push(`locales/en.json lacks ${missingFromEn.join(", ")}, which locales/sv.json has. The two carry identical keys.`);
721
+ }
722
+ }
723
+ return problems;
724
+ }
725
+ /** Dotted paths to every value that is not itself a nested object. */
726
+ function leafKeys(record, prefix = "") {
727
+ return Object.entries(record).flatMap(([key, value]) => {
728
+ const path = prefix === "" ? key : `${prefix}.${key}`;
729
+ const nested = asRecord(value);
730
+ return nested === null ? [path] : leafKeys(nested, path);
731
+ });
732
+ }
733
+ /** A package path without `./` or empty segments, as a file list spells it. */
734
+ function normalizedPath(path) {
735
+ return path
736
+ .split("/")
737
+ .filter((segment) => segment !== "" && segment !== ".")
738
+ .join("/");
739
+ }
740
+ function withoutRuntimeDependencies(packageJson) {
741
+ if (packageJson === null) {
742
+ return packageJson;
743
+ }
744
+ const rest = { ...packageJson };
745
+ for (const field of RUNTIME_DEPENDENCY_FIELDS) {
746
+ delete rest[field];
747
+ }
748
+ return rest;
749
+ }
750
+ function asRecord(value) {
751
+ return typeof value === "object" && value !== null && !Array.isArray(value)
752
+ ? value
753
+ : null;
754
+ }
755
+ //# sourceMappingURL=package-check.js.map