@farm.js/scripts 0.1.0-beta.0 → 0.1.0-beta.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,aAAa,EAId,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,eAAe,EACf,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,oBAAoB,GACrB,MAAM,YAAY,CAAC;AAyBpB,qBAAa,sBAAuB,SAAQ,KAAK;IACtC,IAAI,SAA4B;CAC1C;AAED,qBAAa,wBAAyB,SAAQ,KAAK;IACxC,IAAI,SAA8B;CAC5C;AAED,qBAAa,0BAA2B,SAAQ,KAAK;IAGjD,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM;IAHlB,IAAI,SAAgC;gBAElC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM;CAM5B;AAED,qBAAa,eAAgB,SAAQ,KAAK;IAGtC,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM;IAHb,IAAI,SAAqB;gBAEvB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM;CAIvB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAGzC,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM;IAHnB,IAAI,SAAwB;gBAE1B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM;CAI7B;AAED,qBAAa,wBAAyB,SAAQ,KAAK;IAG/C,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM;IAHpB,IAAI,SAA8B;gBAEhC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM;CAM9B;AAED,+FAA+F;AAC/F,wBAAgB,YAAY,CAAC,CAAC,GAAG,IAAI,EAAE,UAAU,EAAE,gBAAgB,GAAG,YAAY,CAAC,CAAC,CAAC,CA0CpF;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,GAAG,IAAI,CA6BlF;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAExD;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CAKrE;AAED,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,SAAS,wBAAwB,EAAE,EAChD,aAAa,GAAE,MAAM,GAAG,OAAO,UAAmB,EAClD,QAAQ,SAAM,GACb,aAAa,CA8Lf"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,aAAa,EAId,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,eAAe,EACf,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,oBAAoB,GACrB,MAAM,YAAY,CAAC;AAgCpB,qBAAa,sBAAuB,SAAQ,KAAK;IACtC,IAAI,SAA4B;CAC1C;AAED,qBAAa,wBAAyB,SAAQ,KAAK;IACxC,IAAI,SAA8B;CAC5C;AAED,qBAAa,0BAA2B,SAAQ,KAAK;IAGjD,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM;IAHlB,IAAI,SAAgC;gBAElC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM;CAM5B;AAED,qBAAa,eAAgB,SAAQ,KAAK;IAGtC,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM;IAHb,IAAI,SAAqB;gBAEvB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM;CAIvB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAGzC,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM;IAHnB,IAAI,SAAwB;gBAE1B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM;CAI7B;AAED,qBAAa,wBAAyB,SAAQ,KAAK;IAG/C,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM;IAHpB,IAAI,SAA8B;gBAEhC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM;CAM9B;AAED,+FAA+F;AAC/F,wBAAgB,YAAY,CAAC,CAAC,GAAG,IAAI,EAAE,UAAU,EAAE,gBAAgB,GAAG,YAAY,CAAC,CAAC,CAAC,CA0CpF;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,GAAG,IAAI,CA6BlF;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAExD;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CAKrE;AAED,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,SAAS,wBAAwB,EAAE,EAChD,aAAa,GAAE,MAAM,GAAG,OAAO,UAAmB,EAClD,QAAQ,SAAM,GACb,aAAa,CAkMf"}
package/dist/client.js CHANGED
@@ -80,7 +80,7 @@ export function defineScript(definition) {
80
80
  const store = currentStore();
81
81
  if (!store)
82
82
  return () => { };
83
- const entry = ensureEntry(store, resolved, false);
83
+ const entry = ensureEntry(store, resolved, null);
84
84
  entry.listeners.add(listener);
85
85
  callListener(store, listener, snapshot(entry));
86
86
  return () => entry.listeners.delete(listener);
@@ -141,7 +141,10 @@ export function getScriptConsent(category) {
141
141
  /** @internal Start the browser lifecycle for definitions serialized by the Farm plugin. */
142
142
  export function startScriptRuntime(definitions, runtimeWindow = window, basePath = "/") {
143
143
  const store = getStore(runtimeWindow, basePath);
144
- const entries = definitions.map((definition) => ensureEntry(store, cloneScriptDefinition(definition), true));
144
+ // This runtime's claim on its configured scripts. Released on close so a
145
+ // successor (an HMR re-evaluation) can re-register changed definitions.
146
+ const owner = Symbol("farm-scripts-runtime");
147
+ const entries = definitions.map((definition) => ensureEntry(store, cloneScriptDefinition(definition), owner));
145
148
  let closed = false;
146
149
  let idleHandle;
147
150
  let interactionHandler;
@@ -303,6 +306,8 @@ export function startScriptRuntime(definitions, runtimeWindow = window, basePath
303
306
  if (closed)
304
307
  return;
305
308
  closed = true;
309
+ for (const entry of entries)
310
+ entry.owners.delete(owner);
306
311
  if (idleHandle !== undefined) {
307
312
  if (typeof runtimeWindow.cancelIdleCallback === "function") {
308
313
  runtimeWindow.cancelIdleCallback(idleHandle);
@@ -361,21 +366,35 @@ function getStore(runtimeWindow, basePath) {
361
366
  function registerLocalDefinition(definition) {
362
367
  const store = currentStore();
363
368
  if (store)
364
- ensureEntry(store, definition, false);
369
+ ensureEntry(store, definition, null);
365
370
  }
366
- function ensureEntry(store, definition, configured) {
371
+ function ensureEntry(store, definition, owner) {
367
372
  const existing = store.entries.get(definition.name);
368
373
  if (existing) {
369
374
  if (!sameScriptDefinition(existing.definition, definition)) {
370
- throw new TypeError(`Script ${JSON.stringify(definition.name)} was defined with conflicting options`);
375
+ if (existing.owners.size > 0) {
376
+ // Two live registrations disagreeing about the same script is a real
377
+ // application error and keeps failing loudly.
378
+ throw new TypeError(`Script ${JSON.stringify(definition.name)} was defined with conflicting options`);
379
+ }
380
+ // No live runtime owns the old definition — its runtime has closed, as
381
+ // happens on every HMR configuration change. Adopt the new definition
382
+ // and reset load state so it loads fresh; a previously injected element
383
+ // cannot be un-executed, but the replacement no longer throws. Status
384
+ // subscribers survive the swap and are told about the reset.
385
+ existing.definition = definition;
386
+ existing.triggered = false;
387
+ existing.attempt = 0;
388
+ existing.promise = undefined;
389
+ setEntryState(store, existing, "idle");
371
390
  }
372
- if (configured)
373
- existing.configured = true;
391
+ if (owner)
392
+ existing.owners.add(owner);
374
393
  return existing;
375
394
  }
376
395
  const entry = {
377
396
  definition,
378
- configured,
397
+ owners: new Set(owner ? [owner] : []),
379
398
  status: "idle",
380
399
  attempt: 0,
381
400
  triggered: false,
@@ -387,7 +406,7 @@ function ensureEntry(store, definition, configured) {
387
406
  }
388
407
  async function loadRegisteredScript(store, name, stack = []) {
389
408
  const entry = store.entries.get(name);
390
- if (!entry?.configured) {
409
+ if (!entry || entry.owners.size === 0) {
391
410
  throw new ScriptNotRegisteredError(`Script ${JSON.stringify(name)} is not registered. Pass its defineScript() handle to scripts({ scripts: [...] })`);
392
411
  }
393
412
  entry.triggered = true;
@@ -403,10 +422,7 @@ async function loadRegisteredScript(store, name, stack = []) {
403
422
  }
404
423
  if (entry.promise)
405
424
  return entry.promise;
406
- if (entry.definition.consent && getConsent(store, entry.definition.consent) !== "granted") {
407
- setEntryState(store, entry, "blocked");
408
- throw new ScriptConsentRequiredError(name, entry.definition.consent);
409
- }
425
+ assertScriptConsent(store, entry);
410
426
  if (entry.definition.preconnect)
411
427
  addPreconnect(store, entry.definition);
412
428
  if (stack.includes(name)) {
@@ -414,6 +430,7 @@ async function loadRegisteredScript(store, name, stack = []) {
414
430
  }
415
431
  const promise = (async () => {
416
432
  for (const dependency of entry.definition.dependsOn) {
433
+ assertScriptConsent(store, entry);
417
434
  try {
418
435
  await loadRegisteredScript(store, dependency, [...stack, name]);
419
436
  }
@@ -431,6 +448,9 @@ async function loadRegisteredScript(store, name, stack = []) {
431
448
  for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
432
449
  entry.attempt = attempt;
433
450
  notify(store, entry);
451
+ // Dependencies, retry delays, and status listeners can change consent.
452
+ // A blocked load must not be treated as a retryable network failure.
453
+ assertScriptConsent(store, entry);
434
454
  try {
435
455
  const value = await loadElement(store, entry.definition);
436
456
  setEntryState(store, entry, "ready");
@@ -455,6 +475,13 @@ async function loadRegisteredScript(store, name, stack = []) {
455
475
  entry.promise = undefined;
456
476
  }
457
477
  }
478
+ function assertScriptConsent(store, entry) {
479
+ const category = entry.definition.consent;
480
+ if (category && getConsent(store, category) !== "granted") {
481
+ setEntryState(store, entry, "blocked");
482
+ throw new ScriptConsentRequiredError(entry.definition.name, category);
483
+ }
484
+ }
458
485
  async function loadElement(store, definition) {
459
486
  const existingValue = findLoadedValue(store, definition);
460
487
  if (existingValue.found)
package/dist/index.d.ts CHANGED
@@ -2,5 +2,5 @@ import { type FarmPlugin } from "@farm.js/core/plugin";
2
2
  import type { ResolvedScriptsOptions, ScriptRuntime, ScriptsOptions } from "./types.js";
3
3
  export type { AnyScriptHandle, ResolvedScriptDefinition, ResolvedScriptLoadStrategy, ScriptConsentState, ScriptDefinition, ScriptDuration, ScriptHandle, ScriptLoadStrategy, ScriptPlacement, ScriptSnapshot, ScriptStatus, ScriptStatusListener, ScriptType, ScriptsOptions, } from "./types.js";
4
4
  /** Manage explicitly registered third-party browser scripts across Farm's client lifecycle. */
5
- export declare function scripts(options: ScriptsOptions): FarmPlugin<unknown, Record<string, unknown>, ScriptRuntime, ResolvedScriptsOptions>;
5
+ export declare function scripts(options: ScriptsOptions): FarmPlugin<unknown, Record<string, unknown>, ScriptRuntime, ResolvedScriptsOptions, unknown, false, import("@farm.js/core/plugin").PluginRoutes>;
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGrE,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAExF,YAAY,EACV,eAAe,EACf,wBAAwB,EACxB,0BAA0B,EAC1B,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,UAAU,EACV,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,+FAA+F;AAC/F,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,uFA0C9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGrE,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAExF,YAAY,EACV,eAAe,EACf,wBAAwB,EACxB,0BAA0B,EAC1B,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,UAAU,EACV,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,+FAA+F;AAC/F,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,oJA0C9C"}
package/dist/shared.js CHANGED
@@ -204,6 +204,10 @@ function normalizeScriptSource(value) {
204
204
  if (src.includes("\\") || hasControlCharacter) {
205
205
  throw new TypeError("script root-relative src cannot contain backslashes or control characters");
206
206
  }
207
+ const pathname = src.split(/[?#]/, 1)[0] || "/";
208
+ if (pathname.split("/").some((segment) => !isStableRootRelativeSegment(segment))) {
209
+ throw new TypeError("script root-relative src contains a browser-unstable path segment");
210
+ }
207
211
  return src;
208
212
  }
209
213
  if (!/^https?:\/\//i.test(src)) {
@@ -224,6 +228,25 @@ function normalizeScriptSource(value) {
224
228
  }
225
229
  return src;
226
230
  }
231
+ function isStableRootRelativeSegment(segment) {
232
+ if (!segment)
233
+ return true;
234
+ let decoded = segment;
235
+ try {
236
+ decoded = decodeURIComponent(segment);
237
+ }
238
+ catch {
239
+ // Malformed escapes remain literal in browser pathnames.
240
+ }
241
+ return (decoded !== "." &&
242
+ decoded !== ".." &&
243
+ !decoded.includes("/") &&
244
+ !decoded.includes("\\") &&
245
+ !Array.from(decoded).some((character) => {
246
+ const code = character.charCodeAt(0);
247
+ return code <= 31 || (code >= 127 && code <= 159);
248
+ }));
249
+ }
227
250
  function normalizeDataAttributes(value) {
228
251
  if (value === undefined)
229
252
  return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/scripts",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.1",
4
4
  "description": "Typed, lifecycle-aware third-party browser scripts for Farm.js",
5
5
  "keywords": [
6
6
  "consent",
@@ -36,7 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@farm.js/core": "0.1.0-beta.92"
39
+ "@farm.js/core": "0.1.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^20.10.5",