@gjsify/module 0.3.12 → 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/esm/index.js +229 -198
- package/lib/esm/pnp.js +179 -0
- package/lib/types/pnp.d.ts +48 -0
- package/package.json +7 -7
- package/src/index.ts +23 -2
- package/src/pnp.ts +286 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
interface PnpPackageInformation {
|
|
2
|
+
packageLocation: string;
|
|
3
|
+
packageDependencies?: ReadonlyArray<readonly [string, string | null]>;
|
|
4
|
+
linkType: 'SOFT' | 'HARD';
|
|
5
|
+
discardFromLookup?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parsed manifest, indexed for O(1) lookup. Cached per file path so repeat
|
|
9
|
+
* `createRequire(...)` calls on the same workspace don't re-parse.
|
|
10
|
+
*/
|
|
11
|
+
export interface PnpManifest {
|
|
12
|
+
/** Absolute path of the `.pnp.cjs` file's parent directory. */
|
|
13
|
+
readonly rootDir: string;
|
|
14
|
+
/** Map<packageName, Map<packageReference, info>> — null name = workspace root. */
|
|
15
|
+
readonly packages: Map<string | null, Map<string | null, PnpPackageInformation>>;
|
|
16
|
+
/** Locator-by-location reverse index for "which package owns this path". */
|
|
17
|
+
readonly locatorsByLocation: Map<string, {
|
|
18
|
+
name: string | null;
|
|
19
|
+
reference: string | null;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
/** Walk up from `startDir` looking for the nearest `.pnp.cjs`. */
|
|
23
|
+
export declare function findPnpManifest(startDir: string): string | null;
|
|
24
|
+
/** Read + parse `.pnp.cjs`'s `RAW_RUNTIME_STATE`. Cached. */
|
|
25
|
+
export declare function loadPnpManifest(pnpCjsPath: string): PnpManifest | null;
|
|
26
|
+
/**
|
|
27
|
+
* Find which package owns `absolutePath`. Returns the locator + its info, or
|
|
28
|
+
* null when the path isn't covered by any package in the manifest.
|
|
29
|
+
*
|
|
30
|
+
* Uses longest-prefix-match against `packageLocation` entries (Yarn does the
|
|
31
|
+
* same in `findPackageLocator`).
|
|
32
|
+
*/
|
|
33
|
+
export declare function findPackageOwning(manifest: PnpManifest, absolutePath: string): {
|
|
34
|
+
locator: {
|
|
35
|
+
name: string | null;
|
|
36
|
+
reference: string | null;
|
|
37
|
+
};
|
|
38
|
+
info: PnpPackageInformation;
|
|
39
|
+
} | null;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a bare specifier through PnP. Returns the absolute on-disk path,
|
|
42
|
+
* or null when the request can't be resolved this way.
|
|
43
|
+
*
|
|
44
|
+
* `id` is a bare specifier like `@scope/foo` or `@scope/foo/bar/baz.js`.
|
|
45
|
+
* `callerPath` is the absolute path of the file doing the require.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveBareViaPnp(manifest: PnpManifest, id: string, callerPath: string): string | null;
|
|
48
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/module",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"description": "Node.js module module for Gjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "lib/esm/index.js",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"module"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@girs/gio-2.0": "
|
|
34
|
-
"@girs/gjs": "
|
|
35
|
-
"@girs/glib-2.0": "
|
|
36
|
-
"@gjsify/utils": "^0.3.
|
|
33
|
+
"@girs/gio-2.0": "2.88.0-4.0.0-rc.9",
|
|
34
|
+
"@girs/gjs": "4.0.0-rc.9",
|
|
35
|
+
"@girs/glib-2.0": "2.88.0-4.0.0-rc.9",
|
|
36
|
+
"@gjsify/utils": "^0.3.14"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@gjsify/cli": "^0.3.
|
|
40
|
-
"@gjsify/unit": "^0.3.
|
|
39
|
+
"@gjsify/cli": "^0.3.14",
|
|
40
|
+
"@gjsify/unit": "^0.3.14",
|
|
41
41
|
"@types/node": "^25.6.0",
|
|
42
42
|
"typescript": "^6.0.3"
|
|
43
43
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import '@girs/gjs';
|
|
|
6
6
|
import Gio from '@girs/gio-2.0';
|
|
7
7
|
import GLib from '@girs/glib-2.0';
|
|
8
8
|
import { resolve as resolvePath, readJSON } from '@gjsify/utils';
|
|
9
|
+
import { findPnpManifest, loadPnpManifest, resolveBareViaPnp } from './pnp.js';
|
|
9
10
|
|
|
10
11
|
export const builtinModules = [
|
|
11
12
|
'assert',
|
|
@@ -135,6 +136,23 @@ function fileUrlToPath(filenameOrURL: string | URL): string {
|
|
|
135
136
|
return String(filenameOrURL);
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Try resolving a bare specifier through a Yarn PnP manifest (`.pnp.cjs`)
|
|
141
|
+
* sitting above `callerDir`. Returns null when no manifest is found, or when
|
|
142
|
+
* PnP can't resolve the request (e.g. the dep isn't listed in the caller
|
|
143
|
+
* package's `packageDependencies`). Callers fall back to the node_modules walk.
|
|
144
|
+
*/
|
|
145
|
+
function resolveBareViaPnpFromCaller(id: string, callerDir: string): Gio.File | null {
|
|
146
|
+
const pnpPath = findPnpManifest(callerDir);
|
|
147
|
+
if (!pnpPath) return null;
|
|
148
|
+
const manifest = loadPnpManifest(pnpPath);
|
|
149
|
+
if (!manifest) return null;
|
|
150
|
+
const resolved = resolveBareViaPnp(manifest, id, callerDir);
|
|
151
|
+
if (!resolved) return null;
|
|
152
|
+
const file = Gio.File.new_for_path(resolved);
|
|
153
|
+
return file.query_exists(null) ? file : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
138
156
|
/**
|
|
139
157
|
* Resolve a bare package specifier by walking ALL ancestor node_modules dirs.
|
|
140
158
|
* Mirrors Node.js module resolution: try nearest node_modules first, then each
|
|
@@ -171,8 +189,11 @@ function resolveModulePath(id: string, callerDir: string): string {
|
|
|
171
189
|
} else if (id.startsWith('.')) {
|
|
172
190
|
file = resolvePath(callerDir, id);
|
|
173
191
|
} else {
|
|
174
|
-
// Bare specifier:
|
|
175
|
-
|
|
192
|
+
// Bare specifier: try Yarn PnP first (for PnP-built workspaces where no
|
|
193
|
+
// node_modules/ tree exists on disk), then fall back to the standard
|
|
194
|
+
// node_modules walk.
|
|
195
|
+
const pnpFile = resolveBareViaPnpFromCaller(id, callerDir);
|
|
196
|
+
file = pnpFile ?? resolveInNodeModules(id, callerDir);
|
|
176
197
|
}
|
|
177
198
|
|
|
178
199
|
// Extension fallback for absolute/relative paths (bare specifiers handled in resolveInNodeModules)
|
package/src/pnp.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Yarn PnP-aware resolution for `@gjsify/module`'s `createRequire`.
|
|
2
|
+
//
|
|
3
|
+
// Yarn-PnP keeps every package in `.yarn/cache/<pkg>.zip` and `.yarn/unplugged/`,
|
|
4
|
+
// addressed via a `.pnp.cjs` manifest at the workspace root. No real
|
|
5
|
+
// `node_modules/` tree exists on disk, so the standard "walk up
|
|
6
|
+
// `node_modules/`" resolver fails for any consumer running inside a
|
|
7
|
+
// PnP-built workspace — including a freshly-bundled GJS executable that
|
|
8
|
+
// uses `createRequire(import.meta.url)` to find a sibling package's
|
|
9
|
+
// `package.json`.
|
|
10
|
+
//
|
|
11
|
+
// We don't execute Yarn's manifest (it pulls in Node-specific APIs and is
|
|
12
|
+
// 5-10 MB of code). Instead we parse the `RAW_RUNTIME_STATE` JSON literal
|
|
13
|
+
// at the top of `.pnp.cjs` — that's the entire resolution table. Every
|
|
14
|
+
// package's `packageDependencies` lists the *resolved reference* for each
|
|
15
|
+
// requested dependency, so we don't need to run version selection: we just
|
|
16
|
+
// look up the locator and return its `packageLocation`.
|
|
17
|
+
//
|
|
18
|
+
// What this implementation supports
|
|
19
|
+
// ---------------------------------
|
|
20
|
+
//
|
|
21
|
+
// * `linkType: "SOFT"` packages — workspaces and unplugged deps; their
|
|
22
|
+
// `packageLocation` is a real on-disk directory, so resolved paths are
|
|
23
|
+
// usable directly with `readFileSync` / `Gio.File`.
|
|
24
|
+
// * `linkType: "HARD"` packages — zip-cached deps. The resolved path
|
|
25
|
+
// points into a `.yarn/cache/<pkg>.zip/...` virtual directory; reads
|
|
26
|
+
// against it require the `@yarnpkg/fslib` ZipFS layer that the gjsify
|
|
27
|
+
// plugin uses at build time. We surface the path so callers can
|
|
28
|
+
// decide what to do with it; runtime read support for zips is tracked
|
|
29
|
+
// separately in gjsify STATUS.md.
|
|
30
|
+
//
|
|
31
|
+
// What is NOT supported
|
|
32
|
+
// ---------------------
|
|
33
|
+
//
|
|
34
|
+
// * Resolution of bare specifiers FROM a PnP-virtual location (e.g. a
|
|
35
|
+
// module loaded from inside a zip resolving its own dependencies).
|
|
36
|
+
// We could reach this via `dependencyTreeRoots` indirection but no
|
|
37
|
+
// current consumer needs it.
|
|
38
|
+
// * Conditional exports / package.json `exports` map. We respect
|
|
39
|
+
// `main`/`module`/`/<sub-path>` splits like the rest of
|
|
40
|
+
// `@gjsify/module`'s resolver, but we don't honour `exports`-only
|
|
41
|
+
// entries.
|
|
42
|
+
|
|
43
|
+
import Gio from '@girs/gio-2.0';
|
|
44
|
+
import GLib from '@girs/glib-2.0';
|
|
45
|
+
|
|
46
|
+
interface PnpPackageInformation {
|
|
47
|
+
packageLocation: string;
|
|
48
|
+
packageDependencies?: ReadonlyArray<readonly [string, string | null]>;
|
|
49
|
+
linkType: 'SOFT' | 'HARD';
|
|
50
|
+
discardFromLookup?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface PnpRuntimeState {
|
|
54
|
+
packageRegistryData: ReadonlyArray<
|
|
55
|
+
readonly [
|
|
56
|
+
string | null,
|
|
57
|
+
ReadonlyArray<readonly [string | null, PnpPackageInformation]>,
|
|
58
|
+
]
|
|
59
|
+
>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parsed manifest, indexed for O(1) lookup. Cached per file path so repeat
|
|
64
|
+
* `createRequire(...)` calls on the same workspace don't re-parse.
|
|
65
|
+
*/
|
|
66
|
+
export interface PnpManifest {
|
|
67
|
+
/** Absolute path of the `.pnp.cjs` file's parent directory. */
|
|
68
|
+
readonly rootDir: string;
|
|
69
|
+
/** Map<packageName, Map<packageReference, info>> — null name = workspace root. */
|
|
70
|
+
readonly packages: Map<
|
|
71
|
+
string | null,
|
|
72
|
+
Map<string | null, PnpPackageInformation>
|
|
73
|
+
>;
|
|
74
|
+
/** Locator-by-location reverse index for "which package owns this path". */
|
|
75
|
+
readonly locatorsByLocation: Map<
|
|
76
|
+
string,
|
|
77
|
+
{ name: string | null; reference: string | null }
|
|
78
|
+
>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const manifestCache = new Map<string, PnpManifest | null>();
|
|
82
|
+
|
|
83
|
+
/** Walk up from `startDir` looking for the nearest `.pnp.cjs`. */
|
|
84
|
+
export function findPnpManifest(startDir: string): string | null {
|
|
85
|
+
let dir = Gio.File.new_for_path(startDir);
|
|
86
|
+
while (dir.has_parent(null)) {
|
|
87
|
+
const candidate = dir.resolve_relative_path('.pnp.cjs');
|
|
88
|
+
if (candidate.query_exists(null)) return candidate.get_path();
|
|
89
|
+
dir = dir.get_parent()!;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Read + parse `.pnp.cjs`'s `RAW_RUNTIME_STATE`. Cached. */
|
|
95
|
+
export function loadPnpManifest(pnpCjsPath: string): PnpManifest | null {
|
|
96
|
+
const cached = manifestCache.get(pnpCjsPath);
|
|
97
|
+
if (cached !== undefined) return cached;
|
|
98
|
+
|
|
99
|
+
const file = Gio.File.new_for_path(pnpCjsPath);
|
|
100
|
+
let text: string;
|
|
101
|
+
try {
|
|
102
|
+
const [ok, bytes] = file.load_contents(null);
|
|
103
|
+
if (!ok) {
|
|
104
|
+
manifestCache.set(pnpCjsPath, null);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
text = new TextDecoder().decode(bytes);
|
|
108
|
+
} catch {
|
|
109
|
+
manifestCache.set(pnpCjsPath, null);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const state = extractRawRuntimeState(text);
|
|
114
|
+
if (!state) {
|
|
115
|
+
manifestCache.set(pnpCjsPath, null);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const rootDir = GLib.path_get_dirname(pnpCjsPath);
|
|
120
|
+
const packages = new Map<
|
|
121
|
+
string | null,
|
|
122
|
+
Map<string | null, PnpPackageInformation>
|
|
123
|
+
>();
|
|
124
|
+
const locatorsByLocation = new Map<
|
|
125
|
+
string,
|
|
126
|
+
{ name: string | null; reference: string | null }
|
|
127
|
+
>();
|
|
128
|
+
|
|
129
|
+
for (const [name, store] of state.packageRegistryData) {
|
|
130
|
+
const inner = new Map<string | null, PnpPackageInformation>();
|
|
131
|
+
for (const [reference, info] of store) {
|
|
132
|
+
inner.set(reference, info);
|
|
133
|
+
if (!info.discardFromLookup) {
|
|
134
|
+
// Yarn keys locations on the trailing-slash form; preserve it.
|
|
135
|
+
locatorsByLocation.set(info.packageLocation, { name, reference });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
packages.set(name, inner);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const manifest: PnpManifest = { rootDir, packages, locatorsByLocation };
|
|
142
|
+
manifestCache.set(pnpCjsPath, manifest);
|
|
143
|
+
return manifest;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Strip the line-continuations (`\\\n`) Yarn writes around the JSON literal
|
|
148
|
+
* and JSON.parse the result. Returns null if we can't find the literal.
|
|
149
|
+
*/
|
|
150
|
+
function extractRawRuntimeState(text: string): PnpRuntimeState | null {
|
|
151
|
+
// The literal is `const RAW_RUNTIME_STATE =\n'<json-with-line-conts>';`
|
|
152
|
+
// followed by another JS statement on a new line. Match it directly.
|
|
153
|
+
const start = text.indexOf("const RAW_RUNTIME_STATE =");
|
|
154
|
+
if (start < 0) return null;
|
|
155
|
+
const openQuote = text.indexOf("'", start);
|
|
156
|
+
if (openQuote < 0) return null;
|
|
157
|
+
|
|
158
|
+
// Find the closing `';` — line-continuations escape newlines, NOT
|
|
159
|
+
// single-quotes, so the next un-escaped `';` ends the literal.
|
|
160
|
+
let i = openQuote + 1;
|
|
161
|
+
while (i < text.length) {
|
|
162
|
+
const ch = text.charCodeAt(i);
|
|
163
|
+
if (ch === 0x5c /* \ */) {
|
|
164
|
+
// Skip the escaped char (line-continuation \\\n, escaped quote \', etc.)
|
|
165
|
+
i += 2;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (ch === 0x27 /* ' */) {
|
|
169
|
+
// End of literal.
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
i++;
|
|
173
|
+
}
|
|
174
|
+
if (i >= text.length) return null;
|
|
175
|
+
|
|
176
|
+
let raw = text.slice(openQuote + 1, i);
|
|
177
|
+
// Yarn's only escape in this literal is `\<newline>` (line continuation).
|
|
178
|
+
// Strip them; the result is valid JSON.
|
|
179
|
+
raw = raw.replace(/\\\n/g, '');
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(raw) as PnpRuntimeState;
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Find which package owns `absolutePath`. Returns the locator + its info, or
|
|
189
|
+
* null when the path isn't covered by any package in the manifest.
|
|
190
|
+
*
|
|
191
|
+
* Uses longest-prefix-match against `packageLocation` entries (Yarn does the
|
|
192
|
+
* same in `findPackageLocator`).
|
|
193
|
+
*/
|
|
194
|
+
export function findPackageOwning(
|
|
195
|
+
manifest: PnpManifest,
|
|
196
|
+
absolutePath: string,
|
|
197
|
+
): {
|
|
198
|
+
locator: { name: string | null; reference: string | null };
|
|
199
|
+
info: PnpPackageInformation;
|
|
200
|
+
} | null {
|
|
201
|
+
const relPath = relativeFromRoot(manifest.rootDir, absolutePath);
|
|
202
|
+
if (relPath === null) return null;
|
|
203
|
+
|
|
204
|
+
let bestMatch: string | null = null;
|
|
205
|
+
for (const candidateLocation of manifest.locatorsByLocation.keys()) {
|
|
206
|
+
if (
|
|
207
|
+
relPath.startsWith(candidateLocation) &&
|
|
208
|
+
(bestMatch === null || candidateLocation.length > bestMatch.length)
|
|
209
|
+
) {
|
|
210
|
+
bestMatch = candidateLocation;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (bestMatch === null) return null;
|
|
214
|
+
|
|
215
|
+
const locator = manifest.locatorsByLocation.get(bestMatch)!;
|
|
216
|
+
const info = manifest.packages.get(locator.name)?.get(locator.reference);
|
|
217
|
+
if (!info) return null;
|
|
218
|
+
return { locator, info };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Resolve a bare specifier through PnP. Returns the absolute on-disk path,
|
|
223
|
+
* or null when the request can't be resolved this way.
|
|
224
|
+
*
|
|
225
|
+
* `id` is a bare specifier like `@scope/foo` or `@scope/foo/bar/baz.js`.
|
|
226
|
+
* `callerPath` is the absolute path of the file doing the require.
|
|
227
|
+
*/
|
|
228
|
+
export function resolveBareViaPnp(
|
|
229
|
+
manifest: PnpManifest,
|
|
230
|
+
id: string,
|
|
231
|
+
callerPath: string,
|
|
232
|
+
): string | null {
|
|
233
|
+
const owner = findPackageOwning(manifest, callerPath);
|
|
234
|
+
if (!owner) return null;
|
|
235
|
+
|
|
236
|
+
// Split `@scope/name/sub/path` → name=`@scope/name`, subPath=`sub/path`.
|
|
237
|
+
const { pkgName, subPath } = splitSpecifier(id);
|
|
238
|
+
|
|
239
|
+
const dep = owner.info.packageDependencies?.find(([name]) => name === pkgName);
|
|
240
|
+
if (!dep) return null;
|
|
241
|
+
const [, reference] = dep;
|
|
242
|
+
if (reference === null) return null;
|
|
243
|
+
|
|
244
|
+
const target = manifest.packages.get(pkgName)?.get(reference);
|
|
245
|
+
if (!target) return null;
|
|
246
|
+
|
|
247
|
+
// `packageLocation` is `./<rel>/` from manifest.rootDir, OR an absolute
|
|
248
|
+
// path under `.yarn/unplugged/...` for unplugged packages — Gio.File
|
|
249
|
+
// handles the join either way.
|
|
250
|
+
const baseFile = target.packageLocation.startsWith('/')
|
|
251
|
+
? Gio.File.new_for_path(target.packageLocation)
|
|
252
|
+
: Gio.File.new_for_path(manifest.rootDir).resolve_relative_path(
|
|
253
|
+
stripLeadingDotSlash(target.packageLocation),
|
|
254
|
+
);
|
|
255
|
+
const finalFile = subPath
|
|
256
|
+
? baseFile.resolve_relative_path(subPath)
|
|
257
|
+
: baseFile;
|
|
258
|
+
return finalFile.get_path();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Split `@scope/foo/sub/path` into `pkgName: '@scope/foo'`, `subPath: 'sub/path'`. */
|
|
262
|
+
function splitSpecifier(id: string): { pkgName: string; subPath: string } {
|
|
263
|
+
if (id.startsWith('@')) {
|
|
264
|
+
const slash1 = id.indexOf('/');
|
|
265
|
+
if (slash1 < 0) return { pkgName: id, subPath: '' };
|
|
266
|
+
const slash2 = id.indexOf('/', slash1 + 1);
|
|
267
|
+
if (slash2 < 0) return { pkgName: id, subPath: '' };
|
|
268
|
+
return { pkgName: id.slice(0, slash2), subPath: id.slice(slash2 + 1) };
|
|
269
|
+
}
|
|
270
|
+
const slash = id.indexOf('/');
|
|
271
|
+
if (slash < 0) return { pkgName: id, subPath: '' };
|
|
272
|
+
return { pkgName: id.slice(0, slash), subPath: id.slice(slash + 1) };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function stripLeadingDotSlash(p: string): string {
|
|
276
|
+
return p.startsWith('./') ? p.slice(2) : p;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Returns `<absolutePath>` minus `<rootDir>/`, with a trailing slash on
|
|
280
|
+
* directory components, matching Yarn's `packageLocation` keys. */
|
|
281
|
+
function relativeFromRoot(rootDir: string, absolutePath: string): string | null {
|
|
282
|
+
if (absolutePath === rootDir) return './';
|
|
283
|
+
const prefix = rootDir.endsWith('/') ? rootDir : rootDir + '/';
|
|
284
|
+
if (!absolutePath.startsWith(prefix)) return null;
|
|
285
|
+
return './' + absolutePath.slice(prefix.length);
|
|
286
|
+
}
|