@jspm/generator
Version:
Package Import Map Generation Tool
577 lines (575 loc) • 23.8 kB
JavaScript
import { JspmError } from '../common/err.js';
import { importedFrom } from '../common/url.js';
import { pkgToStr } from '../install/package.js';
// @ts-ignore
import { SemverRange } from 'sver';
// @ts-ignore
import { fetch } from '../common/fetch.js';
let gaUrl = 'https://ga.jspm.io/';
const systemCdnUrl = 'https://ga.system.jspm.io/';
let apiUrl = 'https://api.jspm.io/';
// the URL we PUT the jspm.io publish to
let publishUrl = 'https://dev.qitkao.com/';
// the URL the published jspm.io packages can be seen
let rawUrl = 'https://jspm.io/';
let authToken;
const BUILD_POLL_TIME = 60 * 1000;
const BUILD_POLL_INTERVAL = 5 * 1000;
const AUTH_POLL_INTERVAL = 5 * 1000; // 5 seconds between auth polls
export const supportedLayers = [
'default',
'system'
];
function withTrailer(url) {
return url.endsWith('/') ? url : url + '/';
}
export function pkgToUrl(pkg, layer = 'default') {
if (pkg.registry === 'app') return `${rawUrl}${pkgToStr(pkg)}/`;
return `${layer === 'system' ? systemCdnUrl : gaUrl}${pkgToStr(pkg)}/`;
}
export async function getPackageConfig(pkgUrl) {
var _res_headers_get;
try {
var res = await fetch(`${pkgUrl}package.json`, this.fetchOpts);
} catch (e) {
return null;
}
switch(res.status){
case 200:
case 204:
case 304:
break;
case 400:
case 401:
case 403:
case 404:
let err;
try {
// if it is a build error, try surface the build error
err = await (await fetch(`${pkgUrl}_error.log`)).text();
} catch {}
if (err) throw new JspmError(err);
case 406:
case 500:
return null;
default:
throw new JspmError(`Invalid status code ${res.status} reading package config for ${pkgUrl}. ${res.statusText}`);
}
if (res.headers && !((_res_headers_get = res.headers.get('Content-Type')) === null || _res_headers_get === void 0 ? void 0 : _res_headers_get.match(/^application\/json(;|$)/))) {
return null;
} else {
try {
return await res.json();
} catch (e) {
return null;
}
}
}
export function configure(config) {
if (config.authToken) authToken = config.authToken;
if (config.cdnUrl) gaUrl = withTrailer(config.cdnUrl);
if (config.publishUrl) publishUrl = withTrailer(config.publishUrl);
if (config.rawUrl) rawUrl = withTrailer(config.rawUrl);
if (config.apiUrl) apiUrl = withTrailer(config.apiUrl);
}
const exactPkgRegEx = /^(([a-z]+):)?((?:@[^/\\%@]+\/)?[^./\\%@][^/\\%@]*)@([^\/]+)(\/.*)?$/;
export function parseUrlPkg(url) {
let subpath = null;
let layer;
if (url.startsWith(gaUrl)) layer = 'default';
else if (url.startsWith(systemCdnUrl)) layer = 'system';
else return;
const [, , registry, name, version] = url.slice((layer === 'default' ? gaUrl : systemCdnUrl).length).match(exactPkgRegEx) || [];
if (registry && name && version) {
if (registry === 'npm' && name === '@jspm/core' && url.includes('/nodelibs/')) {
subpath = `./nodelibs/${url.slice(url.indexOf('/nodelibs/') + 10).split('/')[1]}`;
if (subpath && subpath.endsWith('.js')) subpath = subpath.slice(0, -3);
else subpath = null;
}
return {
pkg: {
registry,
name,
version
},
layer,
subpath
};
}
}
function getJspmCache(context) {
const jspmCache = context.context.jspmCache;
if (!context.context.jspmCache) {
return context.context.jspmCache = {
lookupCache: new Map(),
versionsCacheMap: new Map(),
resolveCache: {},
cachedErrors: new Map(),
buildRequested: new Map()
};
}
return jspmCache;
}
async function checkBuildOrError(context, pkgUrl, fetchOpts, resolver) {
// For backward compatibility, assuming we have an outer resolver that can handle this
// In a fully refactored system, this would likely come from a different method
const pcfg = await (resolver === null || resolver === void 0 ? void 0 : resolver.getPackageConfig(pkgUrl)) || await fetch(pkgUrl + 'package.json');
if (pcfg) {
return true;
}
const { cachedErrors } = getJspmCache(context);
// no package.json! Check if there's a build error:
if (cachedErrors.has(pkgUrl)) return cachedErrors.get(pkgUrl);
const cachedErrorPromise = (async ()=>{
try {
const errLog = await getTextIfOk(`${pkgUrl}/_error.log`, fetchOpts);
throw new JspmError(`Resolved dependency ${pkgUrl} with error:\n\n${errLog}\nPlease post an issue at jspm/project on GitHub, or by following the link below:\n\nhttps://github.com/jspm/project/issues/new?title=CDN%20build%20error%20for%20${encodeURIComponent(pkgUrl)}&body=_Reporting%20CDN%20Build%20Error._%0A%0A%3C!--%20%20No%20further%20description%20necessary,%20just%20click%20%22Submit%20new%20issue%22%20--%3E`);
} catch (e) {
return false;
}
})();
cachedErrors.set(pkgUrl, cachedErrorPromise);
return cachedErrorPromise;
}
async function ensureBuild(context, pkg, fetchOpts, resolver) {
if (await checkBuildOrError(context, pkgToUrl(pkg, 'default'), fetchOpts, resolver)) return;
const fullName = `${pkg.name}@${pkg.version}`;
const { buildRequested } = getJspmCache(context);
// no package.json AND no build error -> post a build request
// once the build request has been posted, try polling for up to 2 mins
if (buildRequested.has(fullName)) return buildRequested.get(fullName);
const buildPromise = (async ()=>{
const buildRes = await fetch(`${apiUrl}build/${fullName}`, fetchOpts);
if (!buildRes.ok && buildRes.status !== 403) {
const err = (await buildRes.json()).error;
throw new JspmError(`Unable to request the JSPM API for a build of ${fullName}, with error: ${err}.`);
}
// build requested -> poll on that
let startTime = Date.now();
while(true){
await new Promise((resolve)=>setTimeout(resolve, BUILD_POLL_INTERVAL));
if (await checkBuildOrError(context, pkgToUrl(pkg, 'default'), fetchOpts, resolver)) return;
if (Date.now() - startTime >= BUILD_POLL_TIME) throw new JspmError(`Timed out waiting for the build of ${fullName} to be ready on the JSPM CDN. Try again later, or post a jspm.io project issue at https://github.com/jspm/project if the problem persists.`);
}
})();
buildRequested.set(fullName, buildPromise);
return buildPromise;
}
export async function resolveLatestTarget(target, layer, parentUrl, resolver) {
const { registry, name, range, unstable } = target;
// exact version optimization
if (range.isExact && !range.version.tag) {
const pkg = {
registry,
name,
version: range.version.toString()
};
await ensureBuild(this, pkg, this.fetchOpts, resolver);
return pkg;
}
const { resolveCache } = getJspmCache(this);
const cache = resolveCache[target.registry + ':' + target.name] = resolveCache[target.registry + ':' + target.name] || {
latest: null,
majors: Object.create(null),
minors: Object.create(null),
tags: Object.create(null)
};
if (range.isWildcard || range.isExact && range.version.tag === 'latest') {
let lookup = await (cache.latest || (cache.latest = lookupRange.call(this, registry, name, '', unstable, parentUrl)));
// Deno wat?
if (lookup instanceof Promise) lookup = await lookup;
if (!lookup) return null;
this.log('jspm/resolveLatestTarget', `${target.registry}:${target.name}@${range} -> WILDCARD ${lookup.version}${parentUrl ? ' [' + parentUrl + ']' : ''}`);
await ensureBuild(this, lookup, this.fetchOpts, resolver);
return lookup;
}
if (range.isExact && range.version.tag) {
const tag = range.version.tag;
let lookup = await (cache.tags[tag] || (cache.tags[tag] = lookupRange.call(this, registry, name, tag, unstable, parentUrl)));
// Deno wat?
if (lookup instanceof Promise) lookup = await lookup;
if (!lookup) return null;
this.log('jspm/resolveLatestTarget', `${target.registry}:${target.name}@${range} -> TAG ${tag}${parentUrl ? ' [' + parentUrl + ']' : ''}`);
await ensureBuild(this, lookup, this.fetchOpts, resolver);
return lookup;
}
let stableFallback = false;
if (range.isMajor) {
const major = range.version.major;
let lookup = await (cache.majors[major] || (cache.majors[major] = lookupRange.call(this, registry, name, major, unstable, parentUrl)));
// Deno wat?
if (lookup instanceof Promise) lookup = await lookup;
if (!lookup) return null;
// if the latest major is actually a downgrade, use the latest minor version (fallthrough)
// note this might miss later major prerelease versions, which should strictly be supported via a pkg@X@ unstable major lookup
if (range.version.gt(lookup.version)) {
stableFallback = true;
} else {
this.log('jspm/resolveLatestTarget', `${target.registry}:${target.name}@${range} -> MAJOR ${lookup.version}${parentUrl ? ' [' + parentUrl + ']' : ''}`);
await ensureBuild(this, lookup, this.fetchOpts, resolver);
return lookup;
}
}
if (stableFallback || range.isStable) {
const minor = `${range.version.major}.${range.version.minor}`;
let lookup = await (cache.minors[minor] || (cache.minors[minor] = lookupRange.call(this, registry, name, minor, unstable, parentUrl)));
// in theory a similar downgrade to the above can happen for stable prerelease ranges ~1.2.3-pre being downgraded to 1.2.2
// this will be solved by the pkg@X.Y@ unstable minor lookup
// Deno wat?
if (lookup instanceof Promise) lookup = await lookup;
if (!lookup) return null;
this.log('jspm/resolveLatestTarget', `${target.registry}:${target.name}@${range} -> MINOR ${lookup.version}${parentUrl ? ' [' + parentUrl + ']' : ''}`);
await ensureBuild(this, lookup, this.fetchOpts, resolver);
return lookup;
}
return null;
}
function pkgToLookupUrl(pkg, edge = false) {
return `${gaUrl}${pkg.registry}:${pkg.name}${pkg.version ? '@' + pkg.version : edge ? '@' : ''}`;
}
async function lookupRange(registry, name, range, unstable, parentUrl) {
const { lookupCache } = getJspmCache(this);
const url = pkgToLookupUrl({
registry,
name,
version: range
}, unstable);
if (lookupCache.has(url)) return lookupCache.get(url);
const lookupPromise = (async ()=>{
const version = await getTextIfOk(url, this.fetchOpts);
if (version) {
return {
registry,
name,
version: version.trim()
};
} else {
// not found
const versions = await fetchVersions.call(this, name);
const semverRange = new SemverRange(String(range) || '*', unstable);
const version = semverRange.bestMatch(versions, unstable);
if (version) {
return {
registry,
name,
version: version.toString()
};
}
throw new JspmError(`Unable to resolve ${registry}:${name}@${range} to a valid version${importedFrom(parentUrl)}`);
}
})();
lookupCache.set(url, lookupPromise);
return lookupPromise;
}
async function getTextIfOk(url, fetchOpts) {
const res = await fetch(url, fetchOpts);
switch(res.status){
case 200:
case 304:
return await res.text();
// not found = null
case 404:
return null;
default:
throw new Error(`Invalid status code ${res.status}`);
}
}
export async function fetchVersions(name) {
const { versionsCacheMap } = getJspmCache(this);
if (versionsCacheMap.has(name)) {
return versionsCacheMap.get(name);
}
const registryLookup = JSON.parse(await getTextIfOk(`https://npmlookup.jspm.io/${encodeURI(name)}`, {})) || {};
const versions = Object.keys(registryLookup.versions || {});
versionsCacheMap.set(name, versions);
return versions;
}
export async function download(pkg) {
const tar = await import('tar-stream');
const { default: pako } = await import('pako');
const { name, version, registry } = pkg;
if (registry !== 'app') throw new JspmError(`The JSPM provider currently only supports downloading from the jspm.io "app:" registry`);
let tarball;
try {
const tarballRes = await fetch(`${rawUrl}tarball/app:${name}@${version}`);
if (tarballRes.ok) {
tarball = await tarballRes.arrayBuffer();
} else {
try {
throw (await tarballRes.json()).error;
} catch {}
throw tarballRes.statusText || tarballRes.status;
}
} catch (e) {
throw new JspmError(`Unable to fetch tarball for ${name}@${version} from ${rawUrl}: ${e}`);
}
const output = pako.inflate(tarball, {
gzip: true
});
const extract = tar.extract();
const fileData = {};
await new Promise((resolve, reject)=>{
extract.on('entry', async function(header, stream, next) {
try {
if (header.type === 'file') {
if (header.name.indexOf('/') === -1) {
next();
return;
}
const name = header.name.slice(header.name.indexOf('/') + 1);
const target = new Uint8Array(header.size);
let offset = 0;
for await (const chunk of stream){
target.set(chunk, offset);
offset += chunk.byteLength;
}
fileData[name] = target;
}
stream.once('end', next);
stream.resume();
next();
} catch (e) {
extract.emit('error', e);
}
});
extract.once('error', reject);
extract.once('finish', resolve);
extract.end(output);
});
return fileData;
}
/**
* Publishes a package to the jspm.io app registry
*
* Input package is already validated by JSPM
*
* @returns Promise that resolves with the package URL
*/ export async function publish(pkg, files, importMap, imports, timeout = 30000) {
const { registry, name, version } = pkg;
if (registry !== 'app') {
throw new JspmError(`Invalid registry ${registry}, JSPM can only publish to the jspm.io "app:" registry currently`);
}
// Prepare the URL for the request
const packageUrl = `${publishUrl}app:${name}@${version}`;
// Prepare the package data using tar-stream
const tarball = await createTarball(files, importMap);
// Get or create token
const token = await createPublishToken(name, version);
// Upload the package
const response = await fetch(packageUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/gzip',
Authorization: `Bearer ${token}`
},
body: tarball,
timeout
});
if (!response.ok) {
let errorMessage;
switch(response.status){
case 413:
errorMessage = `Publish failed - package size of ${tarball.byteLength}B is too large`;
break;
default:
errorMessage = `Publish failed with status ${response.status}`;
}
try {
const errorJson = await response.json();
errorMessage = errorJson.message || errorJson.error || `Publish failed with status ${response.status}`;
} catch {}
throw new JspmError(errorMessage);
}
const result = await response.json();
if (!result.success) {
throw new JspmError(result.message || 'Publish failed');
}
const publicPackageUrl = pkgToUrl.call(this, pkg, 'default');
const mapUrl = publicPackageUrl + 'importmap.json';
return {
packageUrl: publicPackageUrl,
mapUrl,
codeSnippet: `<!-- jspm.io import map injection (change to ".hot.js" for hot reloading) -->
<script src="${mapUrl.slice(0, -2)}" crossorigin="anoymous"></script>
<!-- Polyfill for older browsers -->
<script async src="${await latestEsms.call(this, rawUrl)}" crossorigin="anonymous"></script>
${imports.length ? `
<!-- Import entrypoint${imports.length > 1 ? 's' : ''} -->
<script type="module" crossorigin="anonymous">${imports.length > 1 ? '\n' : ''}${imports.map((impt, idx)=>`${idx === 0 ? '' : idx === 1 ? '// Further available import map entrypoints - import as needed:\n// ' : '// '}import '${impt}';`).join('\n')}${imports.length > 1 ? '\n' : ''}</script>` : ''}
`
};
}
async function latestEsms(forUrl) {
// Obtain the latest ES Module Shims version
const esmsPkg = await resolveLatestTarget.call(this, {
name: 'es-module-shims',
registry: 'npm',
range: new SemverRange('*'),
unstable: false
}, 'default', forUrl, null);
return pkgToUrl.call(this, esmsPkg, 'default') + 'dist/es-module-shims.js';
}
/**
* Authenticate with JSPM API to obtain a token
*
* @param options Authentication options
* @returns Promise resolving to an authentication token
*/ export async function auth(options) {
// Start token request
const deviceCodeResponse = await globalThis.fetch(`${apiUrl}v1/auth/cli`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
client_id: 'jspm-cli',
scope: 'deployments'
})
});
if (!deviceCodeResponse.ok) {
throw new JspmError(`Failed to start authentication flow: ${deviceCodeResponse.status} ${deviceCodeResponse.statusText}`);
}
const deviceCodeData = await deviceCodeResponse.json();
const { device_code: deviceCode, user_code: userCode, verification_uri_complete: verificationUri, interval = 5 } = deviceCodeData;
// Prepare instructions for the user
const instructions = `Login or signup, using the code: ${userCode}`;
// If a verification callback is provided, use it
options.verify(verificationUri, instructions);
while(true){
// Wait for the polling interval
await new Promise((resolve)=>setTimeout(resolve, AUTH_POLL_INTERVAL));
try {
// Poll the token endpoint
const tokenResponse = await globalThis.fetch(`${apiUrl}v1/auth/cli/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: deviceCode,
client_id: 'jspm-cli'
})
});
const tokenData = await tokenResponse.json();
// Check for errors
if (tokenResponse.status !== 200) {
// If authorization is pending, continue polling
if (tokenData.error === 'authorization_pending') {
continue;
}
// If another error occurred, stop polling
throw new JspmError(tokenData.error_description || 'Authentication failed');
}
// Success! Store and return the token
authToken = tokenData.access_token;
return {
token: authToken
};
} catch (error) {
// Handle network errors, continue polling
if (error.name === 'FetchError') {
continue;
}
throw error;
}
}
}
/**
* Creates a JWT token for package publishing
*
* @param packageName Name of the package
* @param packageVersion Version of the package
* @returns JWT token for publish authorization
*/ async function createPublishToken(packageName, packageVersion) {
if (!authToken) {
throw new JspmError(`No auth token has been generated for jspm.io. Either set providers['jspm.io'].authToken, or first run "jspm auth jspm.io"`);
}
try {
const response = await globalThis.fetch(`${apiUrl}v1/package/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`
},
body: JSON.stringify({
package_name: packageName,
package_version: packageVersion
})
});
if (!response.ok) {
let errJson;
try {
errJson = await response.json();
} catch {
throw new Error(response.status.toString());
}
throw new Error(errJson.error ? errJson.error : JSON.stringify(errJson, null, 2));
}
const data = await response.json();
return data.token;
} catch (error) {
// Fall back to the placeholder token if there's an error
if (error.message.includes('Invalid or expired token')) throw new JspmError(`Invalid or expired token, run "jspm provider auth jspm.io" to regenerate an authentication token.`);
throw new JspmError(error.message);
}
}
/**
* Creates a gzipped tarball from the provided files
* Following npm conventions with a "package" folder at the root
*
* @param files Record of file paths to content
* @param importMap Optional import map to include
* @returns Promise that resolves with the tarball
*/ async function createTarball(files, map) {
const tar = await import('tar-stream');
const { default: pako } = await import('pako');
// Create pack stream
const pack = tar.pack();
// Collect chunks
const deflator = new pako.Deflate({
gzip: true
});
const result = new Promise((resolve, reject)=>{
pack.on('data', (chunk)=>{
deflator.push(chunk, false);
});
pack.on('finish', ()=>{
console.log('wat');
});
pack.on('end', async ()=>{
deflator.push(new Uint8Array([]), true);
if (deflator.err) reject(deflator.err);
else resolve(deflator.result);
});
pack.on('error', (err)=>{
reject(err);
});
});
if (map) {
pack.entry({
name: 'package/importmap.json'
}, new TextEncoder().encode(JSON.stringify(map.toJSON())));
}
// Add files to the tarball, placing them inside a "package" directory
// to follow npm's convention
if (files) {
for (const [path, content] of Object.entries(files)){
let contentBuffer;
if (typeof content === 'string') {
contentBuffer = new TextEncoder().encode(content);
} else {
contentBuffer = new Uint8Array(content);
}
// Prefix with "package/" to follow npm convention
const tarPath = `package/${path}`;
pack.entry({
name: tarPath
}, contentBuffer);
}
}
// Finalize the pack
pack.finalize();
return result;
}
//# sourceMappingURL=jspm.js.map