@gjsify/rolldown-plugin-pnp 0.3.14

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/lib/index.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { Plugin } from 'rolldown';
2
+ export interface PnpPluginOptions {
3
+ /**
4
+ * `import.meta.url` of the caller. Used to anchor relay-issuer resolution
5
+ * on the caller's installation rather than this package's own location.
6
+ * Defaults to this module's own URL.
7
+ */
8
+ issuerUrl?: string;
9
+ /**
10
+ * Packages whose `package.json` paths serve as fallback relay issuers
11
+ * when a direct `pnpApi.resolveRequest` fails with `UNDECLARED_DEPENDENCY`.
12
+ * Defaults to gjsify's polyfill meta-packages.
13
+ */
14
+ relayPackages?: string[];
15
+ }
16
+ /**
17
+ * Build the gjsify-flavoured PnP relay plugin for Rolldown.
18
+ *
19
+ * Returns null when not running under Yarn PnP. Callers can spread this into
20
+ * a plugin array and Rolldown will skip the null entry.
21
+ */
22
+ export declare function pnpPlugin(opts?: PnpPluginOptions): Promise<Plugin | null>;
package/lib/index.js ADDED
@@ -0,0 +1,165 @@
1
+ // Yarn PnP resolver plugin for Rolldown / Rollup / Vite.
2
+ //
3
+ // Replaces the esbuild-only `@yarnpkg/esbuild-plugin-pnp` wrapper that
4
+ // previously lived in `@gjsify/resolve-npm/lib/pnp-relay.mjs`. Same relay
5
+ // semantics, Rollup-shaped hooks instead of esbuild's `setup(build)`.
6
+ //
7
+ // The plugin behaves like a no-op when not running under Yarn PnP (no
8
+ // `.pnp.cjs` ancestor of cwd, or `module.findPnpApi` unavailable).
9
+ //
10
+ // Rolldown does NOT have esbuild's "first onLoad wins" rule — the path
11
+ // rewriter that used to run inside this plugin's onLoad now lives as an
12
+ // independent `transform(code, id)` plugin. See
13
+ // `@gjsify/rolldown-plugin-gjsify/src/import-meta-url.ts`.
14
+ import { createRequire } from 'node:module';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { dirname, join } from 'node:path';
17
+ import { existsSync } from 'node:fs';
18
+ import { readFile } from 'node:fs/promises';
19
+ /** Walk up from dir until .pnp.cjs is found; return its directory or null. */
20
+ function findPnpRoot(dir) {
21
+ let current = dir;
22
+ while (true) {
23
+ if (existsSync(join(current, '.pnp.cjs')))
24
+ return current;
25
+ const parent = dirname(current);
26
+ if (parent === current)
27
+ return null;
28
+ current = parent;
29
+ }
30
+ }
31
+ /** Try to load the runtime PnP API for the cwd's PnP installation. */
32
+ async function loadPnpApi() {
33
+ try {
34
+ // `pnpapi` is a virtual CJS module Yarn injects when running under PnP.
35
+ // `await import()` of a CJS module yields `{ default, "module.exports" }`,
36
+ // not the exports object directly — unwrap `.default`.
37
+ // @ts-expect-error pnpapi has no npm package
38
+ const mod = await import('pnpapi');
39
+ return (mod.default ?? mod);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * Build the gjsify-flavoured PnP relay plugin for Rolldown.
47
+ *
48
+ * Returns null when not running under Yarn PnP. Callers can spread this into
49
+ * a plugin array and Rolldown will skip the null entry.
50
+ */
51
+ export async function pnpPlugin(opts = {}) {
52
+ const issuerUrl = opts.issuerUrl ?? import.meta.url;
53
+ if (!findPnpRoot(process.cwd()))
54
+ return null;
55
+ const pnpApi = await loadPnpApi();
56
+ if (pnpApi === null)
57
+ return null;
58
+ // Relay issuers — anchor resolution on whoever called us first, then fall
59
+ // back to the polyfill meta-packages (which depend on every individual
60
+ // polyfill, so transitive `@gjsify/*` lookups always reach them).
61
+ const issuerPath = fileURLToPath(issuerUrl);
62
+ const requireFromIssuer = createRequire(issuerPath);
63
+ const relayPackages = opts.relayPackages ?? ['@gjsify/node-polyfills', '@gjsify/web-polyfills'];
64
+ const relayIssuers = [];
65
+ for (const pkg of relayPackages) {
66
+ try {
67
+ relayIssuers.push(requireFromIssuer.resolve(`${pkg}/package.json`));
68
+ }
69
+ catch {
70
+ // not in dep tree — relay won't cover it
71
+ }
72
+ }
73
+ return {
74
+ name: 'gjsify-pnp',
75
+ // Per-hook `order: 'pre'` runs us before Rolldown's default resolver
76
+ // so we own resolution for every bare specifier under PnP. Rolldown
77
+ // uses Rollup's per-hook ordering (not Vite's top-level `enforce`).
78
+ resolveId: {
79
+ order: 'pre',
80
+ async handler(source, importer) {
81
+ // Skip relative / absolute paths — let Rolldown handle them.
82
+ if (source.startsWith('.') || source.startsWith('/'))
83
+ return null;
84
+ // GJS gi:// imports are externalised by the orchestrator's
85
+ // `external` predicate; we must not run them through PnP
86
+ // (which would error with `UNDECLARED_DEPENDENCY` because
87
+ // `@girs/*` packages don't list `gi:` as a dep).
88
+ if (source.startsWith('gi://'))
89
+ return { id: source, external: true };
90
+ if (!importer)
91
+ return null;
92
+ // Importer may be a file URL string or an absolute path.
93
+ const importerPath = importer.startsWith('file://')
94
+ ? fileURLToPath(importer)
95
+ : importer;
96
+ // Fast path: resolve from the importer's own context.
97
+ try {
98
+ const resolved = pnpApi.resolveRequest(source, importerPath);
99
+ if (resolved !== null)
100
+ return { id: resolved };
101
+ // Yarn returns null for built-ins (`fs`, `path`, …) —
102
+ // leave them external; Rolldown picks them up downstream.
103
+ return null;
104
+ }
105
+ catch (err) {
106
+ if (!isUndeclaredDependency(err))
107
+ throw err;
108
+ // Relay through caller's anchor first, then polyfill meta-packages.
109
+ try {
110
+ const resolved = pnpApi.resolveRequest(source, issuerPath);
111
+ if (resolved !== null)
112
+ return { id: resolved };
113
+ }
114
+ catch (relayErr) {
115
+ if (!isUndeclaredDependency(relayErr))
116
+ throw relayErr;
117
+ }
118
+ for (const relayIssuer of relayIssuers) {
119
+ try {
120
+ const resolved = pnpApi.resolveRequest(source, relayIssuer);
121
+ if (resolved !== null)
122
+ return { id: resolved };
123
+ }
124
+ catch (relayErr) {
125
+ if (!isUndeclaredDependency(relayErr))
126
+ throw relayErr;
127
+ }
128
+ }
129
+ // Fall through — bare aliases (e.g. `abort-controller`,
130
+ // `fetch/register/*`) are handled by the gjsify alias
131
+ // layer after this returns null; the re-resolved
132
+ // `@gjsify/*` path then comes back through this hook on
133
+ // the second try.
134
+ return null;
135
+ }
136
+ },
137
+ },
138
+ async load(id) {
139
+ // Only handle paths that look like PnP-resolved zip-resident or
140
+ // cache-resident files. For everything else, defer to the
141
+ // default loader (returning null = pass-through).
142
+ if (!id.includes('/.yarn/') && !id.includes('.zip/'))
143
+ return null;
144
+ try {
145
+ // Yarn's PnP runtime patches Node's `fs` module so reads
146
+ // against zip-resident virtual paths transparently work.
147
+ // Rolldown's Rust core does NOT participate in that patch;
148
+ // delegating to Node fs from here is what makes the read
149
+ // succeed.
150
+ const code = await readFile(id, 'utf8');
151
+ return { code };
152
+ }
153
+ catch (err) {
154
+ const message = err instanceof Error ? err.message : String(err);
155
+ this.error(`gjsify-pnp: failed to read ${id}: ${message}`);
156
+ }
157
+ },
158
+ };
159
+ }
160
+ function isUndeclaredDependency(err) {
161
+ return typeof err === 'object'
162
+ && err !== null
163
+ && 'pnpCode' in err
164
+ && err.pnpCode === 'UNDECLARED_DEPENDENCY';
165
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@gjsify/rolldown-plugin-pnp",
3
+ "version": "0.3.14",
4
+ "description": "Yarn PnP resolver plugin for Rolldown / Rollup / Vite",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "module": "lib/index.js",
8
+ "types": "lib/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/index.d.ts",
12
+ "default": "./lib/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "clear": "rm -rf lib tsconfig.tsbuildinfo || exit 0",
17
+ "check": "tsc --noEmit",
18
+ "build": "tsc"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/gjsify/gjsify.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/gjsify/gjsify/issues"
26
+ },
27
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/rolldown-plugin-pnp#readme",
28
+ "keywords": [
29
+ "rolldown",
30
+ "rollup",
31
+ "vite",
32
+ "yarn",
33
+ "pnp",
34
+ "resolver"
35
+ ],
36
+ "license": "MIT",
37
+ "peerDependencies": {
38
+ "rolldown": "^1.0.0-rc.18"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "rolldown": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^25.6.0",
47
+ "rolldown": "^1.0.0-rc.18",
48
+ "typescript": "^6.0.3"
49
+ }
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,180 @@
1
+ // Yarn PnP resolver plugin for Rolldown / Rollup / Vite.
2
+ //
3
+ // Replaces the esbuild-only `@yarnpkg/esbuild-plugin-pnp` wrapper that
4
+ // previously lived in `@gjsify/resolve-npm/lib/pnp-relay.mjs`. Same relay
5
+ // semantics, Rollup-shaped hooks instead of esbuild's `setup(build)`.
6
+ //
7
+ // The plugin behaves like a no-op when not running under Yarn PnP (no
8
+ // `.pnp.cjs` ancestor of cwd, or `module.findPnpApi` unavailable).
9
+ //
10
+ // Rolldown does NOT have esbuild's "first onLoad wins" rule — the path
11
+ // rewriter that used to run inside this plugin's onLoad now lives as an
12
+ // independent `transform(code, id)` plugin. See
13
+ // `@gjsify/rolldown-plugin-gjsify/src/import-meta-url.ts`.
14
+
15
+ import { createRequire } from 'node:module';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { dirname, join } from 'node:path';
18
+ import { existsSync } from 'node:fs';
19
+ import { readFile } from 'node:fs/promises';
20
+ import type { Plugin } from 'rolldown';
21
+
22
+ export interface PnpPluginOptions {
23
+ /**
24
+ * `import.meta.url` of the caller. Used to anchor relay-issuer resolution
25
+ * on the caller's installation rather than this package's own location.
26
+ * Defaults to this module's own URL.
27
+ */
28
+ issuerUrl?: string;
29
+ /**
30
+ * Packages whose `package.json` paths serve as fallback relay issuers
31
+ * when a direct `pnpApi.resolveRequest` fails with `UNDECLARED_DEPENDENCY`.
32
+ * Defaults to gjsify's polyfill meta-packages.
33
+ */
34
+ relayPackages?: string[];
35
+ }
36
+
37
+ /** PnP API exposed by Yarn at runtime. Subset we actually use. */
38
+ interface PnpApi {
39
+ resolveRequest(specifier: string, issuer: string): string | null;
40
+ }
41
+
42
+ /** Walk up from dir until .pnp.cjs is found; return its directory or null. */
43
+ function findPnpRoot(dir: string): string | null {
44
+ let current = dir;
45
+ while (true) {
46
+ if (existsSync(join(current, '.pnp.cjs'))) return current;
47
+ const parent = dirname(current);
48
+ if (parent === current) return null;
49
+ current = parent;
50
+ }
51
+ }
52
+
53
+ /** Try to load the runtime PnP API for the cwd's PnP installation. */
54
+ async function loadPnpApi(): Promise<PnpApi | null> {
55
+ try {
56
+ // `pnpapi` is a virtual CJS module Yarn injects when running under PnP.
57
+ // `await import()` of a CJS module yields `{ default, "module.exports" }`,
58
+ // not the exports object directly — unwrap `.default`.
59
+ // @ts-expect-error pnpapi has no npm package
60
+ const mod = await import('pnpapi');
61
+ return (mod.default ?? mod) as PnpApi;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Build the gjsify-flavoured PnP relay plugin for Rolldown.
69
+ *
70
+ * Returns null when not running under Yarn PnP. Callers can spread this into
71
+ * a plugin array and Rolldown will skip the null entry.
72
+ */
73
+ export async function pnpPlugin(opts: PnpPluginOptions = {}): Promise<Plugin | null> {
74
+ const issuerUrl = opts.issuerUrl ?? import.meta.url;
75
+ if (!findPnpRoot(process.cwd())) return null;
76
+
77
+ const pnpApi = await loadPnpApi();
78
+ if (pnpApi === null) return null;
79
+
80
+ // Relay issuers — anchor resolution on whoever called us first, then fall
81
+ // back to the polyfill meta-packages (which depend on every individual
82
+ // polyfill, so transitive `@gjsify/*` lookups always reach them).
83
+ const issuerPath = fileURLToPath(issuerUrl);
84
+ const requireFromIssuer = createRequire(issuerPath);
85
+ const relayPackages = opts.relayPackages ?? ['@gjsify/node-polyfills', '@gjsify/web-polyfills'];
86
+ const relayIssuers: string[] = [];
87
+ for (const pkg of relayPackages) {
88
+ try {
89
+ relayIssuers.push(requireFromIssuer.resolve(`${pkg}/package.json`));
90
+ } catch {
91
+ // not in dep tree — relay won't cover it
92
+ }
93
+ }
94
+
95
+ return {
96
+ name: 'gjsify-pnp',
97
+
98
+ // Per-hook `order: 'pre'` runs us before Rolldown's default resolver
99
+ // so we own resolution for every bare specifier under PnP. Rolldown
100
+ // uses Rollup's per-hook ordering (not Vite's top-level `enforce`).
101
+ resolveId: {
102
+ order: 'pre' as const,
103
+ async handler(source, importer) {
104
+ // Skip relative / absolute paths — let Rolldown handle them.
105
+ if (source.startsWith('.') || source.startsWith('/')) return null;
106
+ // GJS gi:// imports are externalised by the orchestrator's
107
+ // `external` predicate; we must not run them through PnP
108
+ // (which would error with `UNDECLARED_DEPENDENCY` because
109
+ // `@girs/*` packages don't list `gi:` as a dep).
110
+ if (source.startsWith('gi://')) return { id: source, external: true };
111
+ if (!importer) return null;
112
+
113
+ // Importer may be a file URL string or an absolute path.
114
+ const importerPath = importer.startsWith('file://')
115
+ ? fileURLToPath(importer)
116
+ : importer;
117
+
118
+ // Fast path: resolve from the importer's own context.
119
+ try {
120
+ const resolved = pnpApi.resolveRequest(source, importerPath);
121
+ if (resolved !== null) return { id: resolved };
122
+ // Yarn returns null for built-ins (`fs`, `path`, …) —
123
+ // leave them external; Rolldown picks them up downstream.
124
+ return null;
125
+ } catch (err) {
126
+ if (!isUndeclaredDependency(err)) throw err;
127
+
128
+ // Relay through caller's anchor first, then polyfill meta-packages.
129
+ try {
130
+ const resolved = pnpApi.resolveRequest(source, issuerPath);
131
+ if (resolved !== null) return { id: resolved };
132
+ } catch (relayErr) {
133
+ if (!isUndeclaredDependency(relayErr)) throw relayErr;
134
+ }
135
+ for (const relayIssuer of relayIssuers) {
136
+ try {
137
+ const resolved = pnpApi.resolveRequest(source, relayIssuer);
138
+ if (resolved !== null) return { id: resolved };
139
+ } catch (relayErr) {
140
+ if (!isUndeclaredDependency(relayErr)) throw relayErr;
141
+ }
142
+ }
143
+ // Fall through — bare aliases (e.g. `abort-controller`,
144
+ // `fetch/register/*`) are handled by the gjsify alias
145
+ // layer after this returns null; the re-resolved
146
+ // `@gjsify/*` path then comes back through this hook on
147
+ // the second try.
148
+ return null;
149
+ }
150
+ },
151
+ },
152
+
153
+ async load(id) {
154
+ // Only handle paths that look like PnP-resolved zip-resident or
155
+ // cache-resident files. For everything else, defer to the
156
+ // default loader (returning null = pass-through).
157
+ if (!id.includes('/.yarn/') && !id.includes('.zip/')) return null;
158
+
159
+ try {
160
+ // Yarn's PnP runtime patches Node's `fs` module so reads
161
+ // against zip-resident virtual paths transparently work.
162
+ // Rolldown's Rust core does NOT participate in that patch;
163
+ // delegating to Node fs from here is what makes the read
164
+ // succeed.
165
+ const code = await readFile(id, 'utf8');
166
+ return { code };
167
+ } catch (err) {
168
+ const message = err instanceof Error ? err.message : String(err);
169
+ this.error(`gjsify-pnp: failed to read ${id}: ${message}`);
170
+ }
171
+ },
172
+ };
173
+ }
174
+
175
+ function isUndeclaredDependency(err: unknown): boolean {
176
+ return typeof err === 'object'
177
+ && err !== null
178
+ && 'pnpCode' in err
179
+ && (err as { pnpCode?: unknown }).pnpCode === 'UNDECLARED_DEPENDENCY';
180
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "src",
4
+ "outDir": "lib",
5
+ "declaration": true,
6
+ "target": "ESNext",
7
+ "module": "ESNext",
8
+ "moduleResolution": "bundler",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["src/**/*"]
15
+ }