-
-
Notifications
You must be signed in to change notification settings - Fork 23
/
Copy pathpackage.ts
359 lines (329 loc) · 10.6 KB
/
package.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
import { JspmError } from "../common/err.js";
import { baseUrl, isRelative } from "../common/url.js";
// @ts-ignore
import sver from "sver";
const { SemverRange } = sver;
// @ts-ignore
import convertRange from "sver/convert-range.js";
import { InstallTarget, PackageProvider } from "./installer.js";
import { Resolver } from "../trace/resolver.js";
import { builtinSchemes } from "../providers/index.js";
import { Install } from "../generator.js";
/**
* ExportsTarget defines specifier mappings for the public entry points of a
* package, with support for conditionals.
* see https://nodejs.org/dist/latest-v19.x/docs/api/packages.html#exports
*/
export type ExportsTarget =
| "."
| `./${string}`
| null
| { [condition: string]: ExportsTarget }
| ExportsTarget[];
/**
* ImportsTarget defines private specifier mappings that apply only to the
* internal imports of a package, with support for conditionals.
* see https://nodejs.org/dist/latest-v19.x/docs/api/packages.html#imports
*/
export type ImportsTarget =
| string
| null
| { [condition: string]: ExportsTarget }
| ExportsTarget[];
/**
* PackageConfig is a parsed version of a package's package.json file.
* see https://nodejs.org/dist/latest-v19.x/docs/api/packages.html
*/
export interface PackageConfig {
registry?: string;
name?: string;
version?: string;
main?: string;
files?: string[];
module?: string;
browser?: string | Record<string, string | false>;
imports?: Record<string, ExportsTarget>;
exports?: ExportsTarget | Record<string, ExportsTarget>;
type?: string;
dependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
}
/**
* ExactPackage pins down an exact version of a package on an external registry,
* such as npm or deno.
*/
export interface ExactPackage {
name: string;
registry: string;
version: string;
}
/**
* ExactModule pins down an exact version of a module in a package that can be
* served by a PackageProvider.
*/
export interface ExactModule {
pkg: ExactPackage;
subpath: `./${string}` | null;
source: PackageProvider;
}
/**
* PackageTarget pins down a particular version range of a package on an
* external registry, such as npm or deno.
*/
export interface PackageTarget {
registry: string;
name: string;
ranges: any[];
unstable: boolean;
}
/**
* LatestPackageTarget pins down the latest version of a package on an external
* registry, such as npm or deno.
*/
export interface LatestPackageTarget {
registry: string;
name: string;
range: any;
unstable: boolean;
}
const supportedProtocols = ["https", "http", "data", "file"];
export async function parseUrlOrBuiltinTarget(
resolver: Resolver,
targetStr: string,
parentUrl?: URL
): Promise<Install | undefined> {
const registryIndex = targetStr.indexOf(":");
if (
isRelative(targetStr) ||
(registryIndex !== -1 &&
supportedProtocols.includes(targetStr.slice(0, registryIndex))) ||
builtinSchemes.has(targetStr.slice(0, registryIndex))
) {
let target: string | InstallTarget;
let alias: string;
let subpath: "." | `./${string}` = ".";
const maybeBuiltin =
builtinSchemes.has(targetStr.slice(0, registryIndex)) &&
resolver.resolveBuiltin(targetStr);
if (maybeBuiltin) {
if (typeof maybeBuiltin === "string") {
throw new Error(
`Builtin "${targetStr}" was resolved to package specifier ${maybeBuiltin}, but JSPM does not currently support installing specifiers for builtins.`
);
} else {
({ alias, subpath = ".", target } = maybeBuiltin);
}
} else {
const subpathIndex = targetStr.indexOf("|");
if (subpathIndex !== -1) {
subpath = `./${targetStr.slice(subpathIndex + 1)}` as `./${string}`;
targetStr = targetStr.slice(0, subpathIndex);
}
target = {
pkgTarget: new URL(
targetStr + (targetStr.endsWith("/") ? "" : "/"),
parentUrl || baseUrl
),
installSubpath: null,
};
const pkgUrl = await resolver.getPackageBase(
(target.pkgTarget as URL).href
);
alias =
(pkgUrl ? await resolver.getPackageConfig(pkgUrl) : null)?.name ||
((target.pkgTarget as URL).pathname
.split("/")
.slice(0, -1)
.pop() as string);
}
if (!alias)
throw new JspmError(
`Unable to determine an alias for target package ${targetStr}`
);
return { alias, target, subpath };
}
}
// ad-hoc determination of local path v remote package for eg "jspm deno react" v "jspm deno react@2" v "jspm deno ./react.ts" v "jspm deno react.ts"
const supportedRegistries = ["npm", "github", "deno", "nest", "denoland"];
export function isPackageTarget(targetStr: string): boolean {
if (isRelative(targetStr)) return false;
const registryIndex = targetStr.indexOf(":");
if (
registryIndex !== -1 &&
supportedRegistries.includes(targetStr.slice(0, registryIndex))
)
return true;
const pkg = parsePkg(targetStr);
if (!pkg) return false;
if (pkg.pkgName.indexOf("@") !== -1) return true;
if (
targetStr.endsWith(".ts") ||
targetStr.endsWith(".js") ||
targetStr.endsWith(".mjs")
)
return false;
return true;
}
export async function parseTarget(
resolver: Resolver,
targetStr: string,
parentPkgUrl: URL,
defaultRegistry: string
): Promise<Install> {
const urlTarget = await parseUrlOrBuiltinTarget(
resolver,
targetStr,
parentPkgUrl
);
if (urlTarget) return urlTarget;
// TODO: package aliases support as per https://github.com/npm/rfcs/blob/latest/implemented/0001-package-aliases.md
const registryIndex = targetStr.indexOf(":");
const versionOrScopeIndex = targetStr.indexOf("@");
if (
targetStr.indexOf(":") !== -1 &&
versionOrScopeIndex !== -1 &&
versionOrScopeIndex < registryIndex
)
throw new Error(`Package aliases not yet supported. PRs welcome.`);
const pkg = parsePkg(
registryIndex === -1 ? targetStr : targetStr.slice(registryIndex + 1)
);
if (!pkg) throw new JspmError(`Invalid package name ${targetStr}`);
let registry = null;
if (registryIndex !== -1) registry = targetStr.slice(0, registryIndex);
let alias = pkg.pkgName;
const versionIndex = pkg.pkgName.indexOf("@", 1);
if (versionIndex !== -1) alias = pkg.pkgName.slice(0, versionIndex);
else alias = pkg.pkgName;
// If no version is specified, we fallback to the constraints in the parent
// package config if they exist:
const pcfg = await resolver.getPackageConfig(parentPkgUrl.href);
if (versionIndex === -1 && pcfg) {
const dep =
pcfg.dependencies?.[alias] ||
pcfg.peerDependencies?.[alias] ||
pcfg.optionalDependencies?.[alias] ||
pcfg.devDependencies?.[alias];
if (dep) {
return {
target: newPackageTarget(
dep,
parentPkgUrl,
registry || defaultRegistry,
pkg.pkgName
),
alias,
subpath: pkg.subpath,
};
}
}
// Otherwise we construct a package target from what we were given:
return {
target: newPackageTarget(
pkg.pkgName,
parentPkgUrl,
registry || defaultRegistry
),
alias,
subpath: pkg.subpath as "." | `./{string}`,
};
}
export function newPackageTarget(
target: string,
parentPkgUrl: URL,
defaultRegistry: string,
pkgName?: string
): InstallTarget {
if (target === ".") {
// useful shorthand
target = "./";
}
let registry: string, name: string, ranges: any[];
const registryIndex = target.indexOf(":");
if (
target.startsWith("./") ||
target.startsWith("../") ||
target.startsWith("/") ||
registryIndex === 1
)
return { pkgTarget: new URL(target, parentPkgUrl), installSubpath: null };
registry =
registryIndex < 1 ? defaultRegistry : target.slice(0, registryIndex);
if (registry === "file")
return {
pkgTarget: new URL(target.slice(registry.length + 1), parentPkgUrl),
installSubpath: null,
};
if (registry === "https" || registry === "http")
return { pkgTarget: new URL(target), installSubpath: null };
const versionIndex = target.lastIndexOf("@");
let unstable = false;
if (versionIndex > registryIndex + 1) {
name = target.slice(registryIndex + 1, versionIndex);
const version = target.slice(versionIndex + 1);
ranges =
pkgName || SemverRange.isValid(version)
? [new SemverRange(version)]
: version.split("||").map((v) => convertRange(v));
if (version === "") unstable = true;
} else if (registryIndex === -1 && pkgName) {
name = pkgName;
ranges = SemverRange.isValid(target)
? [new SemverRange(target)]
: target.split("||").map((v) => convertRange(v));
} else {
name = target.slice(registryIndex + 1);
ranges = [new SemverRange("*")];
}
if (registryIndex === -1 && name.indexOf("/") !== -1 && name[0] !== "@")
registry = "github";
const targetNameLen = name.split("/").length;
if (targetNameLen > 2 || (targetNameLen === 1 && name[0] === "@"))
throw new JspmError(`Invalid package target ${target}`);
return {
pkgTarget: { registry, name, ranges, unstable },
installSubpath: null,
};
}
export function pkgToStr(pkg: ExactPackage) {
return `${pkg.registry ? pkg.registry + ":" : ""}${pkg.name}${
pkg.version ? "@" + pkg.version : ""
}`;
}
/**
* Throws unless the given specifier is a valid npm-style package specifier.
*
* @param {string} specifier Specifier to validate.
*/
export function validatePkgName(specifier: string) {
const parsed = parsePkg(specifier);
if (!parsed || parsed.subpath !== ".")
throw new Error(
`"${specifier}" is not a valid npm-style package name. Subpaths must be provided separately to the installation package name.`
);
}
/**
* Parses an npm-style module specifier, such as '@jspm/generator/index.js',
* and splits it into the package name ('@jspm/generator') and module subpath
* ('./index.js'). Returns undefined if the given specifier is invalid.
*
* @param {string} specifier Specifier to parse.
* @returns {{ pkgName: string, subpath: '.' | `./${string}` } | undefined}
*/
export function parsePkg(
specifier: string
): { pkgName: string; subpath: "." | `./${string}` } | undefined {
let sepIndex = specifier.indexOf("/");
if (specifier[0] === "@") {
if (sepIndex === -1) return;
sepIndex = specifier.indexOf("/", sepIndex + 1);
}
// TODO: Node.js validations like percent encodng checks
if (sepIndex === -1) return { pkgName: specifier, subpath: "." };
return {
pkgName: specifier.slice(0, sepIndex),
subpath: `.${specifier.slice(sepIndex)}` as "." | `./${string}`,
};
}