@farm.js/scripts 0.1.0-beta.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/LICENSE +22 -0
- package/README.md +55 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +669 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +68 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/shared.d.ts +18 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +272 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +56 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { cloneScriptDefinition, readScriptHandle } from "./shared.js";
|
|
2
|
+
export function resolveScriptsOptions(options) {
|
|
3
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
4
|
+
throw new TypeError("scripts options must be an object");
|
|
5
|
+
}
|
|
6
|
+
if ("enabled" in options) {
|
|
7
|
+
throw new TypeError("scripts does not accept enabled; remove scripts() from plugins to disable it");
|
|
8
|
+
}
|
|
9
|
+
if (!Array.isArray(options.scripts) || options.scripts.length === 0) {
|
|
10
|
+
throw new TypeError("scripts requires at least one defineScript() handle");
|
|
11
|
+
}
|
|
12
|
+
const definitions = [];
|
|
13
|
+
const names = new Map();
|
|
14
|
+
const sources = new Map();
|
|
15
|
+
for (const handle of options.scripts) {
|
|
16
|
+
const marked = readScriptHandle(handle);
|
|
17
|
+
if (!marked) {
|
|
18
|
+
throw new TypeError("scripts entries must come from defineScript() in @farm.js/scripts/client");
|
|
19
|
+
}
|
|
20
|
+
const definition = cloneScriptDefinition(marked);
|
|
21
|
+
if (names.has(definition.name)) {
|
|
22
|
+
throw new TypeError(`scripts contains duplicate name ${JSON.stringify(definition.name)}`);
|
|
23
|
+
}
|
|
24
|
+
const sourceOwner = sources.get(definition.src);
|
|
25
|
+
if (sourceOwner) {
|
|
26
|
+
throw new TypeError(`scripts ${JSON.stringify(sourceOwner)} and ${JSON.stringify(definition.name)} use the same src; use one typed handle for that vendor script`);
|
|
27
|
+
}
|
|
28
|
+
names.set(definition.name, definition);
|
|
29
|
+
sources.set(definition.src, definition.name);
|
|
30
|
+
definitions.push(definition);
|
|
31
|
+
}
|
|
32
|
+
for (const definition of definitions) {
|
|
33
|
+
for (const dependency of definition.dependsOn) {
|
|
34
|
+
if (dependency === definition.name) {
|
|
35
|
+
throw new TypeError(`script ${JSON.stringify(definition.name)} cannot depend on itself`);
|
|
36
|
+
}
|
|
37
|
+
if (!names.has(dependency)) {
|
|
38
|
+
throw new TypeError(`script ${JSON.stringify(definition.name)} depends on unregistered script ${JSON.stringify(dependency)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
assertAcyclicDependencies(names);
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
scripts: Object.freeze(definitions),
|
|
45
|
+
basePath: "/",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function assertAcyclicDependencies(definitions) {
|
|
49
|
+
const visiting = new Set();
|
|
50
|
+
const visited = new Set();
|
|
51
|
+
function visit(name, path) {
|
|
52
|
+
if (visited.has(name))
|
|
53
|
+
return;
|
|
54
|
+
if (visiting.has(name)) {
|
|
55
|
+
const start = path.indexOf(name);
|
|
56
|
+
const cycle = [...path.slice(start), name];
|
|
57
|
+
throw new TypeError(`scripts dependency cycle: ${cycle.join(" -> ")}`);
|
|
58
|
+
}
|
|
59
|
+
visiting.add(name);
|
|
60
|
+
const definition = definitions.get(name);
|
|
61
|
+
for (const dependency of definition.dependsOn)
|
|
62
|
+
visit(dependency, [...path, name]);
|
|
63
|
+
visiting.delete(name);
|
|
64
|
+
visited.add(name);
|
|
65
|
+
}
|
|
66
|
+
for (const name of definitions.keys())
|
|
67
|
+
visit(name, []);
|
|
68
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type FarmPlugin } from "@farm.js/core/plugin";
|
|
2
|
+
import type { ResolvedScriptsOptions, ScriptRuntime, ScriptsOptions } from "./types.js";
|
|
3
|
+
export type { AnyScriptHandle, ResolvedScriptDefinition, ResolvedScriptLoadStrategy, ScriptConsentState, ScriptDefinition, ScriptDuration, ScriptHandle, ScriptLoadStrategy, ScriptPlacement, ScriptSnapshot, ScriptStatus, ScriptStatusListener, ScriptType, ScriptsOptions, } from "./types.js";
|
|
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>;
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { definePlugin } from "@farm.js/core/plugin";
|
|
2
|
+
import { resolveScriptsOptions } from "./config.js";
|
|
3
|
+
import { normalizeScriptBasePath } from "./shared.js";
|
|
4
|
+
/** Manage explicitly registered third-party browser scripts across Farm's client lifecycle. */
|
|
5
|
+
export function scripts(options) {
|
|
6
|
+
const resolved = resolveScriptsOptions(options);
|
|
7
|
+
const publicConfig = {
|
|
8
|
+
scripts: resolved.scripts,
|
|
9
|
+
basePath: resolved.basePath,
|
|
10
|
+
};
|
|
11
|
+
let plugin;
|
|
12
|
+
plugin = definePlugin({
|
|
13
|
+
name: "farm:scripts",
|
|
14
|
+
enforce: "post",
|
|
15
|
+
configure(config) {
|
|
16
|
+
publicConfig.basePath = normalizeScriptBasePath(config.basePath);
|
|
17
|
+
if ((config.plugins ?? []).filter((candidate) => candidate.name === "farm:scripts").length > 1) {
|
|
18
|
+
throw new Error("[farm:scripts] Configure all browser scripts in one scripts() instance");
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
client: {
|
|
22
|
+
public: publicConfig,
|
|
23
|
+
async setup({ public: config }) {
|
|
24
|
+
const runtime = await import("@farm.js/scripts/client");
|
|
25
|
+
return runtime.startScriptRuntime(config.scripts, window, config.basePath);
|
|
26
|
+
},
|
|
27
|
+
hydration: {
|
|
28
|
+
after({ state }) {
|
|
29
|
+
state.afterHydration();
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
navigation: {
|
|
33
|
+
rendered({ state }) {
|
|
34
|
+
state.refresh();
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
close({ state }) {
|
|
38
|
+
state.close();
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
return plugin;
|
|
43
|
+
}
|
package/dist/shared.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AnyScriptHandle, ResolvedScriptDefinition, ScriptDefinition, ScriptDependency, ScriptDuration, ScriptHandle } from "./types.js";
|
|
2
|
+
export declare const FARM_SCRIPT_HANDLE: unique symbol;
|
|
3
|
+
interface MarkedScriptHandle {
|
|
4
|
+
readonly [FARM_SCRIPT_HANDLE]: Readonly<ResolvedScriptDefinition>;
|
|
5
|
+
}
|
|
6
|
+
export declare function markScriptHandle<T>(handle: ScriptHandle<T>, definition: ResolvedScriptDefinition): ScriptHandle<T>;
|
|
7
|
+
export declare function readScriptHandle(value: unknown): Readonly<ResolvedScriptDefinition> | undefined;
|
|
8
|
+
export declare function normalizeScriptDefinition(input: ScriptDefinition): ResolvedScriptDefinition;
|
|
9
|
+
export declare function cloneScriptDefinition(definition: Readonly<ResolvedScriptDefinition>): ResolvedScriptDefinition;
|
|
10
|
+
export declare function sameScriptDefinition(left: Readonly<ResolvedScriptDefinition>, right: Readonly<ResolvedScriptDefinition>): boolean;
|
|
11
|
+
export declare function scriptHandleName(value: ScriptDependency): string;
|
|
12
|
+
export declare function parseScriptDuration(value: ScriptDuration, label: string, allowZero: boolean): number;
|
|
13
|
+
export declare function validateGlobalPath(value: unknown): string;
|
|
14
|
+
export declare function isMarkedScriptHandle(value: unknown): value is AnyScriptHandle & MarkedScriptHandle;
|
|
15
|
+
export declare function normalizeScriptBasePath(value: unknown): string;
|
|
16
|
+
export declare function withScriptBasePath(source: string, basePath: string): string;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,wBAAwB,EAExB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,kBAAkB,eAAwC,CAAC;AAgBxE,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,CAAC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,wBAAwB,CAAC,CAAC;CACnE;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,EACvB,UAAU,EAAE,wBAAwB,GACnC,YAAY,CAAC,CAAC,CAAC,CAQjB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC,wBAAwB,CAAC,GAAG,SAAS,CAK/F;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,gBAAgB,GAAG,wBAAwB,CA2E3F;AAED,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,QAAQ,CAAC,wBAAwB,CAAC,GAC7C,wBAAwB,CAO1B;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EACxC,KAAK,EAAE,QAAQ,CAAC,wBAAwB,CAAC,GACxC,OAAO,CAET;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAOhE;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,cAAc,EACrB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,OAAO,GACjB,MAAM,CAsBR;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAmBzD;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,eAAe,GAAG,kBAAkB,CAE/C;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAG9D;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM3E"}
|
package/dist/shared.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
export const FARM_SCRIPT_HANDLE = Symbol.for("@farm.js/scripts.handle");
|
|
2
|
+
const SAFE_GLOBAL_SEGMENT = /^[A-Za-z_$][\w$]*$/;
|
|
3
|
+
const UNSAFE_GLOBAL_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
4
|
+
const REFERRER_POLICIES = new Set([
|
|
5
|
+
"",
|
|
6
|
+
"no-referrer",
|
|
7
|
+
"no-referrer-when-downgrade",
|
|
8
|
+
"origin",
|
|
9
|
+
"origin-when-cross-origin",
|
|
10
|
+
"same-origin",
|
|
11
|
+
"strict-origin",
|
|
12
|
+
"strict-origin-when-cross-origin",
|
|
13
|
+
"unsafe-url",
|
|
14
|
+
]);
|
|
15
|
+
export function markScriptHandle(handle, definition) {
|
|
16
|
+
Object.defineProperty(handle, FARM_SCRIPT_HANDLE, {
|
|
17
|
+
configurable: false,
|
|
18
|
+
enumerable: false,
|
|
19
|
+
writable: false,
|
|
20
|
+
value: definition,
|
|
21
|
+
});
|
|
22
|
+
return handle;
|
|
23
|
+
}
|
|
24
|
+
export function readScriptHandle(value) {
|
|
25
|
+
if (!value || (typeof value !== "object" && typeof value !== "function"))
|
|
26
|
+
return undefined;
|
|
27
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, FARM_SCRIPT_HANDLE);
|
|
28
|
+
if (!descriptor || !("value" in descriptor))
|
|
29
|
+
return undefined;
|
|
30
|
+
return descriptor.value;
|
|
31
|
+
}
|
|
32
|
+
export function normalizeScriptDefinition(input) {
|
|
33
|
+
if (!isPlainObject(input))
|
|
34
|
+
throw new TypeError("script definition must be an object");
|
|
35
|
+
const name = normalizeText(input.name, "name");
|
|
36
|
+
if ([...name].some((character) => character.charCodeAt(0) <= 31 || character.charCodeAt(0) === 127)) {
|
|
37
|
+
throw new TypeError("script name cannot contain control characters");
|
|
38
|
+
}
|
|
39
|
+
const src = normalizeScriptSource(input.src);
|
|
40
|
+
const global = input.global === undefined ? undefined : validateGlobalPath(input.global);
|
|
41
|
+
const load = normalizeLoadStrategy(input.load);
|
|
42
|
+
const consent = input.consent === undefined ? undefined : normalizeText(input.consent, "consent category");
|
|
43
|
+
const dependsOn = normalizeDependencies(input.dependsOn);
|
|
44
|
+
const type = input.type ?? "classic";
|
|
45
|
+
if (type !== "classic" && type !== "module") {
|
|
46
|
+
throw new TypeError('script type must be "classic" or "module"');
|
|
47
|
+
}
|
|
48
|
+
const placement = input.placement ?? "head";
|
|
49
|
+
if (placement !== "head" && placement !== "body") {
|
|
50
|
+
throw new TypeError('script placement must be "head" or "body"');
|
|
51
|
+
}
|
|
52
|
+
assertBoolean(input.async, "async");
|
|
53
|
+
assertBoolean(input.preconnect, "preconnect");
|
|
54
|
+
const retries = input.retries ?? 0;
|
|
55
|
+
if (!Number.isInteger(retries) || retries < 0 || retries > 10) {
|
|
56
|
+
throw new TypeError("script retries must be an integer from 0 to 10");
|
|
57
|
+
}
|
|
58
|
+
const crossOrigin = input.crossOrigin;
|
|
59
|
+
if (crossOrigin !== undefined &&
|
|
60
|
+
crossOrigin !== "anonymous" &&
|
|
61
|
+
crossOrigin !== "use-credentials") {
|
|
62
|
+
throw new TypeError('script crossOrigin must be "anonymous" or "use-credentials"');
|
|
63
|
+
}
|
|
64
|
+
if (input.referrerPolicy !== undefined && !REFERRER_POLICIES.has(input.referrerPolicy)) {
|
|
65
|
+
throw new TypeError("script referrerPolicy is not a supported browser referrer policy");
|
|
66
|
+
}
|
|
67
|
+
if (input.fetchPriority !== undefined &&
|
|
68
|
+
input.fetchPriority !== "high" &&
|
|
69
|
+
input.fetchPriority !== "low" &&
|
|
70
|
+
input.fetchPriority !== "auto") {
|
|
71
|
+
throw new TypeError('script fetchPriority must be "high", "low", or "auto"');
|
|
72
|
+
}
|
|
73
|
+
const definition = {
|
|
74
|
+
name,
|
|
75
|
+
src,
|
|
76
|
+
load,
|
|
77
|
+
dependsOn,
|
|
78
|
+
type,
|
|
79
|
+
placement,
|
|
80
|
+
async: input.async ?? true,
|
|
81
|
+
timeoutMs: parseScriptDuration(input.timeout ?? "15s", "timeout", false),
|
|
82
|
+
readyTimeoutMs: parseScriptDuration(input.readyTimeout ?? "1s", "readyTimeout", true),
|
|
83
|
+
retries,
|
|
84
|
+
retryDelayMs: parseScriptDuration(input.retryDelay ?? 250, "retryDelay", true),
|
|
85
|
+
preconnect: input.preconnect ?? false,
|
|
86
|
+
attributes: normalizeDataAttributes(input.attributes),
|
|
87
|
+
...(global ? { global } : {}),
|
|
88
|
+
...(consent ? { consent } : {}),
|
|
89
|
+
...(input.id === undefined ? {} : { id: normalizeText(input.id, "id") }),
|
|
90
|
+
...(input.integrity === undefined
|
|
91
|
+
? {}
|
|
92
|
+
: { integrity: normalizeText(input.integrity, "integrity") }),
|
|
93
|
+
...(crossOrigin ? { crossOrigin } : {}),
|
|
94
|
+
...(input.referrerPolicy === undefined ? {} : { referrerPolicy: input.referrerPolicy }),
|
|
95
|
+
...(input.fetchPriority === undefined ? {} : { fetchPriority: input.fetchPriority }),
|
|
96
|
+
};
|
|
97
|
+
return deepFreezeDefinition(definition);
|
|
98
|
+
}
|
|
99
|
+
export function cloneScriptDefinition(definition) {
|
|
100
|
+
return deepFreezeDefinition({
|
|
101
|
+
...definition,
|
|
102
|
+
load: typeof definition.load === "object" ? { ...definition.load } : definition.load,
|
|
103
|
+
dependsOn: [...definition.dependsOn],
|
|
104
|
+
attributes: { ...definition.attributes },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export function sameScriptDefinition(left, right) {
|
|
108
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
109
|
+
}
|
|
110
|
+
export function scriptHandleName(value) {
|
|
111
|
+
if (typeof value === "string")
|
|
112
|
+
return normalizeText(value, "dependency");
|
|
113
|
+
const definition = readScriptHandle(value);
|
|
114
|
+
if (!definition) {
|
|
115
|
+
throw new TypeError("script dependency must be a script name or a defineScript() handle");
|
|
116
|
+
}
|
|
117
|
+
return definition.name;
|
|
118
|
+
}
|
|
119
|
+
export function parseScriptDuration(value, label, allowZero) {
|
|
120
|
+
if (typeof value === "number") {
|
|
121
|
+
if (!Number.isFinite(value) || value < 0 || (!allowZero && value === 0)) {
|
|
122
|
+
throw new TypeError(`script ${label} must be ${allowZero ? "a non-negative" : "a positive"} finite duration`);
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
if (typeof value !== "string") {
|
|
127
|
+
throw new TypeError(`script ${label} must be a number or duration string`);
|
|
128
|
+
}
|
|
129
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(value);
|
|
130
|
+
if (!match) {
|
|
131
|
+
throw new TypeError(`script ${label} must use a duration such as "250ms", "5s", or "1m"`);
|
|
132
|
+
}
|
|
133
|
+
const amount = Number(match[1]);
|
|
134
|
+
if (!allowZero && amount === 0) {
|
|
135
|
+
throw new TypeError(`script ${label} must be a positive duration`);
|
|
136
|
+
}
|
|
137
|
+
const multiplier = match[2] === "ms" ? 1 : match[2] === "s" ? 1_000 : 60_000;
|
|
138
|
+
return amount * multiplier;
|
|
139
|
+
}
|
|
140
|
+
export function validateGlobalPath(value) {
|
|
141
|
+
const path = normalizeText(value, "global");
|
|
142
|
+
const segments = path.split(".");
|
|
143
|
+
if (segments[0] === "window" || segments[0] === "globalThis" || segments[0] === "self") {
|
|
144
|
+
throw new TypeError(`script global is relative to window; use ${JSON.stringify(segments.slice(1).join("."))}`);
|
|
145
|
+
}
|
|
146
|
+
if (segments.some((segment) => !SAFE_GLOBAL_SEGMENT.test(segment) || UNSAFE_GLOBAL_SEGMENTS.has(segment.toLowerCase()))) {
|
|
147
|
+
throw new TypeError(`script global must be a safe dotted path, received ${JSON.stringify(path)}`);
|
|
148
|
+
}
|
|
149
|
+
return path;
|
|
150
|
+
}
|
|
151
|
+
export function isMarkedScriptHandle(value) {
|
|
152
|
+
return readScriptHandle(value) !== undefined;
|
|
153
|
+
}
|
|
154
|
+
export function normalizeScriptBasePath(value) {
|
|
155
|
+
if (typeof value !== "string" || !value.trim() || value === "/")
|
|
156
|
+
return "/";
|
|
157
|
+
return `/${value.replace(/^\/+|\/+$/g, "")}`;
|
|
158
|
+
}
|
|
159
|
+
export function withScriptBasePath(source, basePath) {
|
|
160
|
+
if (!source.startsWith("/") || source.startsWith("//"))
|
|
161
|
+
return source;
|
|
162
|
+
const normalizedBase = normalizeScriptBasePath(basePath);
|
|
163
|
+
if (normalizedBase === "/")
|
|
164
|
+
return source;
|
|
165
|
+
if (source === normalizedBase || source.startsWith(`${normalizedBase}/`))
|
|
166
|
+
return source;
|
|
167
|
+
return `${normalizedBase}${source}`;
|
|
168
|
+
}
|
|
169
|
+
function normalizeLoadStrategy(value) {
|
|
170
|
+
const strategy = value ?? "after-hydration";
|
|
171
|
+
if (strategy === "immediate" ||
|
|
172
|
+
strategy === "after-hydration" ||
|
|
173
|
+
strategy === "idle" ||
|
|
174
|
+
strategy === "interaction" ||
|
|
175
|
+
strategy === "manual") {
|
|
176
|
+
return strategy;
|
|
177
|
+
}
|
|
178
|
+
if (!isPlainObject(strategy) || strategy.when !== "visible") {
|
|
179
|
+
throw new TypeError('script load must be "immediate", "after-hydration", "idle", "interaction", "manual", or a visible trigger');
|
|
180
|
+
}
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
when: "visible",
|
|
183
|
+
selector: normalizeText(strategy.selector, "visible selector"),
|
|
184
|
+
rootMargin: strategy.rootMargin === undefined
|
|
185
|
+
? "200px"
|
|
186
|
+
: normalizeText(strategy.rootMargin, "visible rootMargin"),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function normalizeDependencies(value) {
|
|
190
|
+
if (value === undefined)
|
|
191
|
+
return [];
|
|
192
|
+
if (!Array.isArray(value))
|
|
193
|
+
throw new TypeError("script dependsOn must be an array");
|
|
194
|
+
return [...new Set(value.map(scriptHandleName))];
|
|
195
|
+
}
|
|
196
|
+
function normalizeScriptSource(value) {
|
|
197
|
+
const src = normalizeText(value, "src");
|
|
198
|
+
const rootRelative = src.startsWith("/") && !src.startsWith("//");
|
|
199
|
+
if (rootRelative) {
|
|
200
|
+
const hasControlCharacter = Array.from(src).some((character) => {
|
|
201
|
+
const code = character.charCodeAt(0);
|
|
202
|
+
return code <= 31 || code === 127;
|
|
203
|
+
});
|
|
204
|
+
if (src.includes("\\") || hasControlCharacter) {
|
|
205
|
+
throw new TypeError("script root-relative src cannot contain backslashes or control characters");
|
|
206
|
+
}
|
|
207
|
+
return src;
|
|
208
|
+
}
|
|
209
|
+
if (!/^https?:\/\//i.test(src)) {
|
|
210
|
+
throw new TypeError("script src must be root-relative or use an absolute HTTP(S) URL");
|
|
211
|
+
}
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
parsed = new URL(src);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
throw new TypeError(`script src must be a valid URL, received ${JSON.stringify(src)}`);
|
|
218
|
+
}
|
|
219
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
220
|
+
throw new TypeError("script src must use HTTP(S)");
|
|
221
|
+
}
|
|
222
|
+
if (parsed.username || parsed.password) {
|
|
223
|
+
throw new TypeError("script src cannot contain URL credentials");
|
|
224
|
+
}
|
|
225
|
+
return src;
|
|
226
|
+
}
|
|
227
|
+
function normalizeDataAttributes(value) {
|
|
228
|
+
if (value === undefined)
|
|
229
|
+
return {};
|
|
230
|
+
if (!isPlainObject(value))
|
|
231
|
+
throw new TypeError("script attributes must be a plain object");
|
|
232
|
+
const result = {};
|
|
233
|
+
for (const [key, attributeValue] of Object.entries(value)) {
|
|
234
|
+
if (!/^data-[a-z0-9_.:-]+$/i.test(key)) {
|
|
235
|
+
throw new TypeError(`script attribute ${JSON.stringify(key)} must be a data-* attribute`);
|
|
236
|
+
}
|
|
237
|
+
if (key.toLowerCase().startsWith("data-farm-script")) {
|
|
238
|
+
throw new TypeError(`script attribute ${JSON.stringify(key)} is reserved by @farm.js/scripts`);
|
|
239
|
+
}
|
|
240
|
+
if (typeof attributeValue !== "string" &&
|
|
241
|
+
typeof attributeValue !== "number" &&
|
|
242
|
+
typeof attributeValue !== "boolean") {
|
|
243
|
+
throw new TypeError(`script attribute ${JSON.stringify(key)} must be a string, number, or boolean`);
|
|
244
|
+
}
|
|
245
|
+
result[key] = String(attributeValue);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
function normalizeText(value, label) {
|
|
250
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
251
|
+
throw new TypeError(`script ${label} must be a non-empty string`);
|
|
252
|
+
}
|
|
253
|
+
return value.trim();
|
|
254
|
+
}
|
|
255
|
+
function assertBoolean(value, label) {
|
|
256
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
257
|
+
throw new TypeError(`script ${label} must be boolean`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function isPlainObject(value) {
|
|
261
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
262
|
+
return false;
|
|
263
|
+
const prototype = Object.getPrototypeOf(value);
|
|
264
|
+
return prototype === Object.prototype || prototype === null;
|
|
265
|
+
}
|
|
266
|
+
function deepFreezeDefinition(definition) {
|
|
267
|
+
if (typeof definition.load === "object")
|
|
268
|
+
Object.freeze(definition.load);
|
|
269
|
+
Object.freeze(definition.dependsOn);
|
|
270
|
+
Object.freeze(definition.attributes);
|
|
271
|
+
return Object.freeze(definition);
|
|
272
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export type ScriptDuration = number | `${number}${"ms" | "s" | "m"}`;
|
|
2
|
+
export type ScriptLoadStrategy = "immediate" | "after-hydration" | "idle" | "interaction" | "manual" | {
|
|
3
|
+
when: "visible";
|
|
4
|
+
selector: string;
|
|
5
|
+
rootMargin?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ScriptStatus = "idle" | "blocked" | "loading" | "ready" | "error";
|
|
8
|
+
export type ScriptConsentState = "unknown" | "granted" | "denied";
|
|
9
|
+
export type ScriptPlacement = "head" | "body";
|
|
10
|
+
export type ScriptType = "classic" | "module";
|
|
11
|
+
export interface ScriptSnapshot {
|
|
12
|
+
name: string;
|
|
13
|
+
status: ScriptStatus;
|
|
14
|
+
attempt: number;
|
|
15
|
+
error?: unknown;
|
|
16
|
+
}
|
|
17
|
+
export type ScriptStatusListener = (snapshot: Readonly<ScriptSnapshot>) => void;
|
|
18
|
+
export interface ScriptHandle<T = unknown> {
|
|
19
|
+
/** Stable registry name chosen by the application. */
|
|
20
|
+
readonly name: string;
|
|
21
|
+
/** Normalized, browser-safe definition used by the Farm plugin. */
|
|
22
|
+
readonly definition: Readonly<ResolvedScriptDefinition>;
|
|
23
|
+
/** Current browser loading state. SSR reads return `idle`. */
|
|
24
|
+
readonly status: ScriptStatus;
|
|
25
|
+
/** Last terminal loading error, when status is `error`. */
|
|
26
|
+
readonly error: unknown;
|
|
27
|
+
/** Load the external script and resolve its configured global. */
|
|
28
|
+
load(): Promise<T>;
|
|
29
|
+
/** Run code with the resolved SDK after it becomes ready. */
|
|
30
|
+
use<TResult>(callback: (sdk: T) => TResult | PromiseLike<TResult>): Promise<Awaited<TResult>>;
|
|
31
|
+
/** Observe state changes. The listener is called immediately in the browser. */
|
|
32
|
+
subscribe(listener: ScriptStatusListener): () => void;
|
|
33
|
+
}
|
|
34
|
+
export type AnyScriptHandle = ScriptHandle<any>;
|
|
35
|
+
export type ScriptDependency = string | AnyScriptHandle;
|
|
36
|
+
export interface ScriptDefinition {
|
|
37
|
+
/** Stable registry name. Local variable names can be different. */
|
|
38
|
+
name: string;
|
|
39
|
+
/** Root-relative or HTTP(S) script URL. */
|
|
40
|
+
src: string;
|
|
41
|
+
/** Safe dotted global path created by the loaded script, such as `mixpanel` or `google.maps`. */
|
|
42
|
+
global?: string;
|
|
43
|
+
/** When Farm should request the script. Defaults to `after-hydration`. */
|
|
44
|
+
load?: ScriptLoadStrategy;
|
|
45
|
+
/** Consent category that must be explicitly granted before loading. */
|
|
46
|
+
consent?: string;
|
|
47
|
+
/** Scripts that must become ready before this script starts loading. */
|
|
48
|
+
dependsOn?: readonly ScriptDependency[];
|
|
49
|
+
/** Script execution type. Defaults to `classic`. */
|
|
50
|
+
type?: ScriptType;
|
|
51
|
+
/** DOM destination for the generated element. Defaults to `head`. */
|
|
52
|
+
placement?: ScriptPlacement;
|
|
53
|
+
/** Dynamic script async behavior. Defaults to true. */
|
|
54
|
+
async?: boolean;
|
|
55
|
+
/** Network loading timeout. Numbers are milliseconds. Defaults to 15s. */
|
|
56
|
+
timeout?: ScriptDuration;
|
|
57
|
+
/** How long to wait for `global` after the script load event. Defaults to 1s. */
|
|
58
|
+
readyTimeout?: ScriptDuration;
|
|
59
|
+
/** Additional loading attempts after the first failure. Defaults to 0. */
|
|
60
|
+
retries?: number;
|
|
61
|
+
/** Delay between loading attempts. Numbers are milliseconds. Defaults to 250ms. */
|
|
62
|
+
retryDelay?: ScriptDuration;
|
|
63
|
+
/** Add a preconnect hint for a cross-origin script when the client runtime starts. */
|
|
64
|
+
preconnect?: boolean;
|
|
65
|
+
/** Optional element ID. */
|
|
66
|
+
id?: string;
|
|
67
|
+
/** Subresource Integrity value supplied by the script provider. */
|
|
68
|
+
integrity?: string;
|
|
69
|
+
/** CORS mode used by the script request. */
|
|
70
|
+
crossOrigin?: "anonymous" | "use-credentials";
|
|
71
|
+
/** Referrer policy used by the script request. */
|
|
72
|
+
referrerPolicy?: ReferrerPolicy;
|
|
73
|
+
/** Fetch priority hint. */
|
|
74
|
+
fetchPriority?: "high" | "low" | "auto";
|
|
75
|
+
/** Vendor-specific `data-*` attributes. */
|
|
76
|
+
attributes?: Readonly<Record<`data-${string}`, string | number | boolean>>;
|
|
77
|
+
}
|
|
78
|
+
export interface ResolvedVisibleScriptLoadStrategy {
|
|
79
|
+
when: "visible";
|
|
80
|
+
selector: string;
|
|
81
|
+
rootMargin: string;
|
|
82
|
+
}
|
|
83
|
+
export type ResolvedScriptLoadStrategy = Exclude<ScriptLoadStrategy, {
|
|
84
|
+
when: "visible";
|
|
85
|
+
}> | ResolvedVisibleScriptLoadStrategy;
|
|
86
|
+
export interface ResolvedScriptDefinition {
|
|
87
|
+
name: string;
|
|
88
|
+
src: string;
|
|
89
|
+
global?: string;
|
|
90
|
+
load: ResolvedScriptLoadStrategy;
|
|
91
|
+
consent?: string;
|
|
92
|
+
dependsOn: string[];
|
|
93
|
+
type: ScriptType;
|
|
94
|
+
placement: ScriptPlacement;
|
|
95
|
+
async: boolean;
|
|
96
|
+
timeoutMs: number;
|
|
97
|
+
readyTimeoutMs: number;
|
|
98
|
+
retries: number;
|
|
99
|
+
retryDelayMs: number;
|
|
100
|
+
preconnect: boolean;
|
|
101
|
+
id?: string;
|
|
102
|
+
integrity?: string;
|
|
103
|
+
crossOrigin?: "anonymous" | "use-credentials";
|
|
104
|
+
referrerPolicy?: ReferrerPolicy;
|
|
105
|
+
fetchPriority?: "high" | "low" | "auto";
|
|
106
|
+
attributes: Record<string, string>;
|
|
107
|
+
}
|
|
108
|
+
export interface ScriptsOptions {
|
|
109
|
+
scripts: readonly AnyScriptHandle[];
|
|
110
|
+
}
|
|
111
|
+
export interface ResolvedScriptsOptions {
|
|
112
|
+
scripts: ResolvedScriptDefinition[];
|
|
113
|
+
basePath: string;
|
|
114
|
+
}
|
|
115
|
+
export interface ScriptRuntime {
|
|
116
|
+
afterHydration(): void;
|
|
117
|
+
refresh(): void;
|
|
118
|
+
close(): void;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@farm.js/scripts",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "Typed, lifecycle-aware third-party browser scripts for Farm.js",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"consent",
|
|
7
|
+
"farm.js",
|
|
8
|
+
"script-loader",
|
|
9
|
+
"third-party",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/farming-labs/farm.js",
|
|
16
|
+
"directory": "packages/farm-scripts"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./client": {
|
|
31
|
+
"types": "./dist/client.d.ts",
|
|
32
|
+
"import": "./dist/client.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@farm.js/core": "0.1.0-beta.92"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20.10.5",
|
|
43
|
+
"jsdom": "^25.0.0",
|
|
44
|
+
"typescript": "^5.3.3",
|
|
45
|
+
"vitest": "^3.2.7"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22.13.0"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|
|
52
|
+
"dev": "tsc --watch",
|
|
53
|
+
"type-check": "tsc --noEmit",
|
|
54
|
+
"test": "vitest run"
|
|
55
|
+
}
|
|
56
|
+
}
|