@farm.js/partytown 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 ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Farm.js Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # `@farm.js/partytown`
2
+
3
+ Run explicitly opted-in third-party scripts in a web worker with
4
+ [Partytown](https://partytown.qwik.dev/), while Farm owns the development and production asset
5
+ setup.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @farm.js/partytown
11
+ ```
12
+
13
+ The Partytown runtime is included. Applications do not need to install `@qwik.dev/partytown`
14
+ separately.
15
+
16
+ ## Use
17
+
18
+ Create one typed bridge that can be imported by both the Farm config and browser components:
19
+
20
+ ```ts
21
+ // src/lib/analytics.ts
22
+ import { defineForward } from "@farm.js/partytown/client";
23
+
24
+ export const track =
25
+ defineForward<
26
+ (event: string, options?: { props?: Record<string, string | number | boolean> }) => void
27
+ >("plausible");
28
+ ```
29
+
30
+ ```ts
31
+ // farm.config.ts
32
+ import { defineConfig } from "@farm.js/core";
33
+ import { partytown } from "@farm.js/partytown";
34
+ import { track } from "./src/lib/analytics";
35
+
36
+ export default defineConfig({
37
+ plugins: [partytown({ forward: [track] })],
38
+ });
39
+ ```
40
+
41
+ Mark only the vendor scripts that should move to the worker:
42
+
43
+ ```tsx
44
+ // src/app/layout.tsx
45
+ import type { LayoutProps } from "@farm.js/core";
46
+
47
+ export default function RootLayout({ children }: LayoutProps) {
48
+ return (
49
+ <>
50
+ <script
51
+ type="text/partytown"
52
+ defer
53
+ data-domain="yourdomain.com"
54
+ src="https://plausible.io/js/script.js"
55
+ />
56
+ <main>{children}</main>
57
+ </>
58
+ );
59
+ }
60
+ ```
61
+
62
+ ```tsx
63
+ "use client";
64
+
65
+ import { track } from "../lib/analytics";
66
+
67
+ export function UpgradeButton() {
68
+ return <button onClick={() => track("Upgrade", { props: { plan: "pro" } })}>Upgrade</button>;
69
+ }
70
+ ```
71
+
72
+ `defineForward()` is fire-and-forget. It preserves the argument types supplied by the application,
73
+ but its return type is always `void` because a worker call cannot synchronously return a vendor SDK
74
+ result.
75
+
76
+ ## Options
77
+
78
+ ```ts
79
+ partytown({
80
+ forward: [track, "dataLayer.push"],
81
+ debug: false,
82
+ fallbackTimeout: 4_000,
83
+ strictProxyHas: true,
84
+ });
85
+ ```
86
+
87
+ - `forward` lists only the global calls the browser must queue and send to the worker.
88
+ - `debug` defaults to `true` in development and `false` in production.
89
+ - `fallbackTimeout` controls how long Partytown waits before running opted-in scripts on the main
90
+ thread. Use `0` to disable that fallback.
91
+ - `strictProxyHas` is an escape hatch for scripts that rely on strict JavaScript `in` behavior.
92
+
93
+ Pass `{ preserveBehavior: true }` to `defineForward()` only when an existing main-thread function
94
+ must also run:
95
+
96
+ ```ts
97
+ const push = defineForward<(event: Record<string, unknown>) => number>("dataLayer.push", {
98
+ preserveBehavior: true,
99
+ });
100
+ ```
101
+
102
+ The typed caller still returns `void`.
103
+
104
+ ## Behavior and limits
105
+
106
+ - Farm serves the Partytown bootstrap and worker files from the app's own `~partytown` path in
107
+ development and copies them into the correct Nitro public output for production and `basePath`.
108
+ - The bootstrap is an external same-origin script, so the plugin does not add plugin-owned inline
109
+ JavaScript. Vendor origins may still need to be allowed by the application's CSP.
110
+ - Scripts remain on the main thread unless the application marks them with `type="text/partytown"`.
111
+ - DOM-heavy widgets, consent UIs, and SDK methods that synchronously return values may not be good
112
+ worker candidates. Leave those as normal scripts.
113
+ - Injecting the required head bootstrap uses Farm's final HTML transform, so enabling this plugin
114
+ buffers the rendered document instead of preserving streamed HTML delivery.
@@ -0,0 +1,26 @@
1
+ import type { ResolvedPartytownOptions } from "./config.js";
2
+ export declare const PARTYTOWN_DIRECTORY = "~partytown";
3
+ export declare const PARTYTOWN_BOOTSTRAP = "farm-partytown.js";
4
+ export interface PartytownBuildInput {
5
+ outputDir: string;
6
+ publicDir?: string;
7
+ preset: string;
8
+ basePath: string;
9
+ options: ResolvedPartytownOptions;
10
+ }
11
+ export interface PartytownBuildResult {
12
+ assetsDir: string;
13
+ bootstrapPath: string;
14
+ bootstrapUrl: string;
15
+ }
16
+ export declare function writePartytownBuildArtifacts(input: PartytownBuildInput): Promise<PartytownBuildResult>;
17
+ export declare function createPartytownBootstrap(options: ResolvedPartytownOptions, basePath: string, isDev: boolean): string;
18
+ export declare function injectPartytownBootstrap(html: string, bootstrapUrl: string): string;
19
+ export declare function readPartytownDevAsset(pathname: string, basePath: string, options: ResolvedPartytownOptions): Promise<{
20
+ body: Buffer | string;
21
+ contentType: string;
22
+ } | undefined>;
23
+ export declare function normalizeBasePath(value: string | undefined): string;
24
+ export declare function partytownAssetUrl(file: string, basePath: string): string;
25
+ export declare function resolvePartytownPublicDir(outputDir: string, preset: string): string;
26
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAE5D,eAAO,MAAM,mBAAmB,eAAe,CAAC;AAChD,eAAO,MAAM,mBAAmB,sBAAsB,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,wBAAwB,CAAC;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAgB/B;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,wBAAwB,EACjC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,GACb,MAAM,CAeR;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAMnF;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CA8BrE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAUnE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIxE;AAED,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnF"}
package/dist/build.js ADDED
@@ -0,0 +1,117 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { partytownSnippet } from "@qwik.dev/partytown/integration";
4
+ import { copyLibFiles, libDirPath } from "@qwik.dev/partytown/utils";
5
+ export const PARTYTOWN_DIRECTORY = "~partytown";
6
+ export const PARTYTOWN_BOOTSTRAP = "farm-partytown.js";
7
+ export async function writePartytownBuildArtifacts(input) {
8
+ const publicDir = input.publicDir ?? resolvePartytownPublicDir(input.outputDir, input.preset);
9
+ const basePath = normalizeBasePath(input.basePath);
10
+ const relativeAssetsDir = path.posix.join(basePath.slice(1), PARTYTOWN_DIRECTORY);
11
+ const assetsDir = path.join(publicDir, ...relativeAssetsDir.split("/"));
12
+ const debug = input.options.debug ?? false;
13
+ await copyLibFiles(assetsDir, { debugDir: debug });
14
+ const bootstrapPath = path.join(assetsDir, PARTYTOWN_BOOTSTRAP);
15
+ await writeFile(bootstrapPath, createPartytownBootstrap(input.options, basePath, false), "utf8");
16
+ return {
17
+ assetsDir,
18
+ bootstrapPath,
19
+ bootstrapUrl: partytownAssetUrl(PARTYTOWN_BOOTSTRAP, basePath),
20
+ };
21
+ }
22
+ export function createPartytownBootstrap(options, basePath, isDev) {
23
+ const config = {
24
+ lib: partytownAssetUrl("", basePath),
25
+ debug: options.debug ?? isDev,
26
+ };
27
+ const forward = options.forward.map(({ path: forwardPath, preserveBehavior }) => preserveBehavior
28
+ ? [forwardPath, { preserveBehavior: true }]
29
+ : forwardPath);
30
+ if (forward.length > 0)
31
+ config.forward = forward;
32
+ if (options.fallbackTimeout !== undefined)
33
+ config.fallbackTimeout = options.fallbackTimeout;
34
+ if (options.strictProxyHas !== undefined)
35
+ config.strictProxyHas = options.strictProxyHas;
36
+ return `${partytownSnippet(config)}\n`;
37
+ }
38
+ export function injectPartytownBootstrap(html, bootstrapUrl) {
39
+ if (html.includes("data-farm-partytown"))
40
+ return html;
41
+ const closingHead = html.search(/<\/head\s*>/i);
42
+ if (closingHead < 0)
43
+ return html;
44
+ const tag = `<script src="${escapeHtmlAttribute(bootstrapUrl)}" data-farm-partytown></script>`;
45
+ return `${html.slice(0, closingHead)}${tag}${html.slice(closingHead)}`;
46
+ }
47
+ export async function readPartytownDevAsset(pathname, basePath, options) {
48
+ const prefix = partytownAssetUrl("", basePath);
49
+ if (!pathname.startsWith(prefix))
50
+ return undefined;
51
+ let relativePath;
52
+ try {
53
+ relativePath = decodeURIComponent(pathname.slice(prefix.length));
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ if (!relativePath || relativePath.includes("\\"))
59
+ return undefined;
60
+ if (relativePath === PARTYTOWN_BOOTSTRAP) {
61
+ return {
62
+ body: createPartytownBootstrap(options, basePath, true),
63
+ contentType: "text/javascript; charset=utf-8",
64
+ };
65
+ }
66
+ const libraryDir = path.resolve(libDirPath());
67
+ const assetPath = path.resolve(libraryDir, relativePath);
68
+ if (!assetPath.startsWith(`${libraryDir}${path.sep}`))
69
+ return undefined;
70
+ try {
71
+ return {
72
+ body: await readFile(assetPath),
73
+ contentType: contentTypeFor(relativePath),
74
+ };
75
+ }
76
+ catch (error) {
77
+ if (error.code === "ENOENT")
78
+ return undefined;
79
+ throw error;
80
+ }
81
+ }
82
+ export function normalizeBasePath(value) {
83
+ if (!value || value === "/")
84
+ return "/";
85
+ if (!value.startsWith("/") || /[<>"'?#\\]/.test(value)) {
86
+ throw new TypeError("partytown requires basePath to be a safe absolute URL path");
87
+ }
88
+ const segments = value.split("/").filter(Boolean);
89
+ if (segments.some((segment) => segment === "." || segment === "..")) {
90
+ throw new TypeError("partytown requires basePath without dot path segments");
91
+ }
92
+ return `/${segments.join("/")}`;
93
+ }
94
+ export function partytownAssetUrl(file, basePath) {
95
+ const base = normalizeBasePath(basePath);
96
+ const prefix = base === "/" ? "" : base;
97
+ return `${prefix}/${PARTYTOWN_DIRECTORY}/${file}`;
98
+ }
99
+ export function resolvePartytownPublicDir(outputDir, preset) {
100
+ return path.join(outputDir, preset.startsWith("vercel") ? "static" : "public");
101
+ }
102
+ function contentTypeFor(file) {
103
+ if (file.endsWith(".js"))
104
+ return "text/javascript; charset=utf-8";
105
+ if (file.endsWith(".html"))
106
+ return "text/html; charset=utf-8";
107
+ if (file.endsWith(".json"))
108
+ return "application/json; charset=utf-8";
109
+ return "application/octet-stream";
110
+ }
111
+ function escapeHtmlAttribute(value) {
112
+ return value
113
+ .replaceAll("&", "&amp;")
114
+ .replaceAll('"', "&quot;")
115
+ .replaceAll("<", "&lt;")
116
+ .replaceAll(">", "&gt;");
117
+ }
@@ -0,0 +1,9 @@
1
+ import { type PartytownForward, type PartytownForwardOptions } from "./shared.js";
2
+ export type { PartytownForward, PartytownForwardOptions };
3
+ /**
4
+ * Create a typed caller for a global function forwarded to Partytown.
5
+ * The same value can be passed to `partytown({ forward: [...] })` and called
6
+ * from browser code without declaring a vendor global on `window`.
7
+ */
8
+ export declare function defineForward<T extends (...args: any[]) => unknown>(path: string, options?: PartytownForwardOptions): PartytownForward<T>;
9
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC7B,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,CAAC;AAE1D;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,EACjE,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,uBAA4B,GACpC,gBAAgB,CAAC,CAAC,CAAC,CA4BrB"}
package/dist/client.js ADDED
@@ -0,0 +1,36 @@
1
+ import { markPartytownForward, validateForwardPath, } from "./shared.js";
2
+ /**
3
+ * Create a typed caller for a global function forwarded to Partytown.
4
+ * The same value can be passed to `partytown({ forward: [...] })` and called
5
+ * from browser code without declaring a vendor global on `window`.
6
+ */
7
+ export function defineForward(path, options = {}) {
8
+ const resolvedPath = validateForwardPath(path);
9
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
10
+ throw new TypeError("partytown forward options must be an object");
11
+ }
12
+ if (options.preserveBehavior !== undefined && typeof options.preserveBehavior !== "boolean") {
13
+ throw new TypeError("partytown forward preserveBehavior must be boolean");
14
+ }
15
+ const preserveBehavior = options.preserveBehavior ?? false;
16
+ const segments = resolvedPath.split(".");
17
+ const forward = (...args) => {
18
+ if (typeof window === "undefined")
19
+ return;
20
+ let owner = window;
21
+ for (let index = 0; index < segments.length - 1; index += 1) {
22
+ if ((typeof owner !== "object" && typeof owner !== "function") || owner === null)
23
+ return;
24
+ owner = Reflect.get(owner, segments[index]);
25
+ }
26
+ if ((typeof owner !== "object" && typeof owner !== "function") || owner === null)
27
+ return;
28
+ const callable = Reflect.get(owner, segments.at(-1));
29
+ if (typeof callable === "function")
30
+ Reflect.apply(callable, owner, args);
31
+ };
32
+ return markPartytownForward(forward, {
33
+ path: resolvedPath,
34
+ preserveBehavior,
35
+ });
36
+ }
@@ -0,0 +1,24 @@
1
+ import type { PartytownForward } from "./shared.js";
2
+ export type PartytownForwardInput = string | PartytownForward<(...args: any[]) => unknown>;
3
+ export interface PartytownOptions {
4
+ /** Global calls that should be forwarded from the main thread to the worker. */
5
+ forward?: readonly PartytownForwardInput[];
6
+ /** Use Partytown's debug runtime. Defaults to true in development and false in production. */
7
+ debug?: boolean;
8
+ /** Milliseconds before Partytown falls back to the main thread. Set to 0 to disable fallback. */
9
+ fallbackTimeout?: number;
10
+ /** Use strict proxy `has` behavior for scripts that require it. */
11
+ strictProxyHas?: boolean;
12
+ }
13
+ export interface ResolvedPartytownForward {
14
+ path: string;
15
+ preserveBehavior: boolean;
16
+ }
17
+ export interface ResolvedPartytownOptions {
18
+ forward: ResolvedPartytownForward[];
19
+ debug?: boolean;
20
+ fallbackTimeout?: number;
21
+ strictProxyHas?: boolean;
22
+ }
23
+ export declare function resolvePartytownOptions(options: PartytownOptions): ResolvedPartytownOptions;
24
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,gBAAgB,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,CAAC;AAE3F,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,OAAO,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC3C,8FAA8F;IAC9F,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iGAAiG;IACjG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,wBAAwB,EAAE,CAAC;IACpC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,gBAAgB,GAAG,wBAAwB,CA0C3F"}
package/dist/config.js ADDED
@@ -0,0 +1,41 @@
1
+ import { readPartytownForward, validateForwardPath } from "./shared.js";
2
+ export function resolvePartytownOptions(options) {
3
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
4
+ throw new TypeError("partytown options must be an object");
5
+ }
6
+ if ("enabled" in options) {
7
+ throw new TypeError("partytown does not accept enabled; remove partytown() from plugins to disable it");
8
+ }
9
+ assertBoolean(options.debug, "debug");
10
+ assertBoolean(options.strictProxyHas, "strictProxyHas");
11
+ if (options.fallbackTimeout !== undefined &&
12
+ (!Number.isInteger(options.fallbackTimeout) || options.fallbackTimeout < 0)) {
13
+ throw new TypeError("partytown fallbackTimeout must be a non-negative integer");
14
+ }
15
+ if (options.forward !== undefined && !Array.isArray(options.forward)) {
16
+ throw new TypeError("partytown forward must be an array");
17
+ }
18
+ const forwards = new Map();
19
+ for (const input of options.forward ?? []) {
20
+ const descriptor = readPartytownForward(input) ?? {
21
+ path: validateForwardPath(input),
22
+ preserveBehavior: false,
23
+ };
24
+ const previous = forwards.get(descriptor.path);
25
+ if (previous && previous.preserveBehavior !== descriptor.preserveBehavior) {
26
+ throw new TypeError(`partytown forward path ${JSON.stringify(descriptor.path)} has conflicting preserveBehavior settings`);
27
+ }
28
+ forwards.set(descriptor.path, descriptor);
29
+ }
30
+ return {
31
+ forward: [...forwards.values()],
32
+ debug: options.debug,
33
+ fallbackTimeout: options.fallbackTimeout,
34
+ strictProxyHas: options.strictProxyHas,
35
+ };
36
+ }
37
+ function assertBoolean(value, key) {
38
+ if (value !== undefined && typeof value !== "boolean") {
39
+ throw new TypeError(`partytown ${key} must be boolean`);
40
+ }
41
+ }
@@ -0,0 +1,6 @@
1
+ import { type PartytownForwardInput, type PartytownOptions } from "./config.js";
2
+ export type { PartytownForwardInput, PartytownOptions };
3
+ export type { PartytownForward, PartytownForwardOptions } from "./client.js";
4
+ /** Run explicitly marked third-party scripts in a Partytown web worker. */
5
+ export declare function partytown(options?: PartytownOptions): import("@farm.js/core/plugin").FarmPlugin;
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,CAAC;AACxD,YAAY,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE7E,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,OAAO,GAAE,gBAAqB,6CAkEvD"}
package/dist/index.js ADDED
@@ -0,0 +1,106 @@
1
+ import { definePlugin } from "@farm.js/core/plugin";
2
+ import { injectPartytownBootstrap, normalizeBasePath, partytownAssetUrl, PARTYTOWN_BOOTSTRAP, readPartytownDevAsset, writePartytownBuildArtifacts, } from "./build.js";
3
+ import { resolvePartytownOptions, } from "./config.js";
4
+ /** Run explicitly marked third-party scripts in a Partytown web worker. */
5
+ export function partytown(options = {}) {
6
+ const resolved = resolvePartytownOptions(options);
7
+ let configuredBasePath = "/";
8
+ let configuredOutputDir = ".farm/.output";
9
+ return definePlugin({
10
+ name: "farm:partytown",
11
+ enforce: "pre",
12
+ configure(config) {
13
+ configuredBasePath = normalizeBasePath(config.basePath);
14
+ configuredOutputDir = `${config.root ?? "."}/.farm/.output`;
15
+ const vitePlugin = createPartytownDevPlugin(configuredBasePath, resolved);
16
+ return {
17
+ ...config,
18
+ vite: {
19
+ ...config.vite,
20
+ plugins: [vitePlugin, ...(config.vite?.plugins ?? [])],
21
+ },
22
+ };
23
+ },
24
+ render: {
25
+ html(html, _render, context) {
26
+ const basePath = normalizeBasePath(context.config.basePath ?? configuredBasePath);
27
+ return injectPartytownBootstrap(html, partytownAssetUrl(PARTYTOWN_BOOTSTRAP, basePath));
28
+ },
29
+ },
30
+ build: {
31
+ configure(nitroConfig) {
32
+ let copied = false;
33
+ const hooks = nitroConfig.hooks ?? {};
34
+ const previousPrerenderDone = hooks["prerender:done"];
35
+ const previousRollupBefore = hooks["rollup:before"];
36
+ const copy = async () => {
37
+ if (copied)
38
+ return;
39
+ const built = await writePartytownBuildArtifacts({
40
+ outputDir: nitroConfig.output?.dir ?? configuredOutputDir,
41
+ publicDir: nitroConfig.output?.publicDir,
42
+ preset: nitroConfig.preset ?? "node-server",
43
+ basePath: configuredBasePath,
44
+ options: resolved,
45
+ });
46
+ copied = true;
47
+ console.info(`[farm:partytown] Copied worker runtime to ${built.bootstrapUrl}`);
48
+ };
49
+ return {
50
+ ...nitroConfig,
51
+ hooks: {
52
+ ...hooks,
53
+ "prerender:done": async (...args) => {
54
+ await callConfiguredHooks(previousPrerenderDone, args);
55
+ await copy();
56
+ },
57
+ "rollup:before": async (...args) => {
58
+ await callConfiguredHooks(previousRollupBefore, args);
59
+ if (isNitroPrerenderBuild(args[0]))
60
+ return;
61
+ await copy();
62
+ },
63
+ },
64
+ };
65
+ },
66
+ },
67
+ });
68
+ }
69
+ function createPartytownDevPlugin(basePath, options) {
70
+ return {
71
+ name: "farm:partytown-assets",
72
+ enforce: "pre",
73
+ configureServer(server) {
74
+ server.middlewares.use(async (request, response, next) => {
75
+ if (request.method !== "GET" && request.method !== "HEAD")
76
+ return next();
77
+ const pathname = new URL(request.url ?? "/", "http://farm.local").pathname;
78
+ try {
79
+ const asset = await readPartytownDevAsset(pathname, basePath, options);
80
+ if (!asset)
81
+ return next();
82
+ response.statusCode = 200;
83
+ response.setHeader("Content-Type", asset.contentType);
84
+ response.setHeader("Cache-Control", "no-store");
85
+ response.end(request.method === "HEAD" ? undefined : asset.body);
86
+ }
87
+ catch (error) {
88
+ next(error);
89
+ }
90
+ });
91
+ },
92
+ };
93
+ }
94
+ function isNitroPrerenderBuild(value) {
95
+ if (!value || typeof value !== "object")
96
+ return false;
97
+ const options = value.options;
98
+ return options?.preset === "nitro-prerender";
99
+ }
100
+ async function callConfiguredHooks(value, args) {
101
+ const hooks = Array.isArray(value) ? value : [value];
102
+ for (const hook of hooks) {
103
+ if (typeof hook === "function")
104
+ await hook(...args);
105
+ }
106
+ }
@@ -0,0 +1,16 @@
1
+ export interface PartytownForwardOptions {
2
+ /** Also run an existing main-thread implementation before forwarding the call. */
3
+ preserveBehavior?: boolean;
4
+ }
5
+ export type PartytownForward<T extends (...args: any[]) => unknown> = ((...args: Parameters<T>) => void) & {
6
+ readonly path: string;
7
+ readonly preserveBehavior: boolean;
8
+ };
9
+ export interface PartytownForwardDescriptor {
10
+ path: string;
11
+ preserveBehavior: boolean;
12
+ }
13
+ export declare function markPartytownForward<T extends (...args: any[]) => unknown>(value: (...args: Parameters<T>) => void, descriptor: PartytownForwardDescriptor): PartytownForward<T>;
14
+ export declare function readPartytownForward(value: unknown): PartytownForwardDescriptor | undefined;
15
+ export declare function validateForwardPath(value: unknown): string;
16
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,uBAAuB;IACtC,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,IAAI,CAAC,CACrE,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KACnB,IAAI,CAAC,GAAG;IACX,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC,CAAC;AAEF,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,EACxE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,EACvC,UAAU,EAAE,0BAA0B,GACrC,gBAAgB,CAAC,CAAC,CAAC,CAOrB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,0BAA0B,GAAG,SAAS,CAQ3F;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAiB1D"}
package/dist/shared.js ADDED
@@ -0,0 +1,35 @@
1
+ const FORWARD_MARKER = Symbol.for("@farm.js/partytown.forward");
2
+ const FORBIDDEN_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
3
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
4
+ export function markPartytownForward(value, descriptor) {
5
+ Object.defineProperties(value, {
6
+ [FORWARD_MARKER]: { value: true },
7
+ path: { value: descriptor.path, enumerable: true },
8
+ preserveBehavior: { value: descriptor.preserveBehavior, enumerable: true },
9
+ });
10
+ return value;
11
+ }
12
+ export function readPartytownForward(value) {
13
+ if (typeof value !== "function")
14
+ return undefined;
15
+ const candidate = value;
16
+ if (candidate[FORWARD_MARKER] !== true)
17
+ return undefined;
18
+ if (typeof candidate.path !== "string" || typeof candidate.preserveBehavior !== "boolean") {
19
+ return undefined;
20
+ }
21
+ return { path: candidate.path, preserveBehavior: candidate.preserveBehavior };
22
+ }
23
+ export function validateForwardPath(value) {
24
+ if (typeof value !== "string" || !value.trim()) {
25
+ throw new TypeError("partytown forward path must be a non-empty string");
26
+ }
27
+ const path = value.trim();
28
+ if (path.length > 256)
29
+ throw new TypeError("partytown forward path must be 256 characters or less");
30
+ const segments = path.split(".");
31
+ if (segments.some((segment) => !IDENTIFIER.test(segment) || FORBIDDEN_PATH_SEGMENTS.has(segment))) {
32
+ throw new TypeError(`partytown forward path ${JSON.stringify(path)} must contain only safe dotted JavaScript identifiers`);
33
+ }
34
+ return path;
35
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@farm.js/partytown",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "Run opted-in third-party scripts in a web worker in Farm.js applications",
5
+ "keywords": [
6
+ "analytics",
7
+ "farm.js",
8
+ "partytown",
9
+ "performance",
10
+ "web-worker"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/farming-labs/farm.js",
16
+ "directory": "packages/farm-partytown"
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
+ "@qwik.dev/partytown": "^0.14.4",
40
+ "@farm.js/core": "0.1.0-beta.91"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.10.5",
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
+ }