Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reinstate trace install #171

Merged
merged 5 commits into from
Aug 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ The second HTML Generation options include:

* `htmlUrl`: The URL of the HTML file for relative path handling in the map
* `rootUrl`: The root URL of the HTML file for root-relative path handling in the map
* `trace`: Whether to trace the HTML imports before injection (via `generator.pin`)
* `trace`: Whether to trace the HTML imports before injection (via `generator.traceInstall`)
* `pins`: List of top-level pinned `"imports"` to inject, or `true` to inject all (the default if not tracing).
* `comment`: Defaults to `Built with @jspm/generator` comment, set to false or an empty string to remove.
* `preload`: Boolean, injects `<link rel="modulepreload">` preload tags. By default only injects static dependencies. Set to `'all'` to inject dyamic import preloads as well (this is the default when applying `integrity`).
Expand Down Expand Up @@ -242,8 +242,7 @@ To execute the application, the import map needs to be included in the HTML dire
</script>
```

With the import map embedded in the page, all `import` statements will have access to the defined mappings
allowing direct `import 'lit/html.js'` style JS code in the browser.
With the import map embedded in the page, all `import` statements will have access to the defined mappings allowing direct `import 'lit/html.js'` style JS code in the browser.

For browsers without import maps, it is recommended to use the [ES Module Shims](https://github.com/guybedford/es-module-shims) import maps polyfill.
This is a highly optimized polyfill supporting almost native speeds, see [the performance benchmarks](https://github.com/guybedford/es-module-shims/blob/main/bench/README.md) for more information.
Expand All @@ -253,19 +252,18 @@ modules have yet executed on the page. For dynamic import map injection workflow
for each import map and injecting it into this frame can be used to get around this constraint for
in-page refreshing application workflows.

### Traced Pins
### Trace Install

Instead of installing specific packages into the map, you can also just trace any module
module directly and JSPM will generate the scoped mappings to support that modules execution.

We do this via `generator.pin` because we want to be explicit that this graph is being included
in the import map (unused mappings are always pruned if not pinned as "imports" or custom pins).
We do this via `generator.traceInstall` because we want to be explicit that this graph is being included in the import map (unused mappings are always pruned if not pinned as "imports" or custom pins).

generate.mjs
```js
// all static and dynamic dependencies necessary to execute app will be traced and
// put into the map as necessary
await generator.pin('./app.js');
await generator.traceInstall('./app.js');
```

The benefit of tracing is that it directly implements a Node.js-compatible resolver so that if you can trace something
Expand Down
6 changes: 3 additions & 3 deletions chompfile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,15 @@ run = '''
import { readFile, writeFile } from 'fs/promises';

const generator = new Generator({
mapUrl: import.meta.url,
mapUrl: new URL('./test/test.html', import.meta.url.replace('//[', '/[')),
env: ['browser', 'module', 'production']
});

await generator.pin('@jspm/generator');
await generator.traceInstall('@jspm/generator');
await generator.install('node:assert');

const html = await generator.htmlInject(await readFile(process.env.TARGET, 'utf8'), {
htmlUrl: new URL(process.env.TARGET, import.meta.url)
htmlUrl: new URL(process.env.TARGET, import.meta.url.replace('//[', '/['))
});
await writeFile(process.env.TARGET, html);
'''
32 changes: 15 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions src/common/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ declare global {
var process: any;
}

export function isMappableScheme (specifier) {
return specifier.startsWith('node:') || specifier.startsWith('deno:');
}

export function isKnownProtocol (protocol) {
return protocol === 'file:' ||
protocol === 'https:' ||
protocol === 'http:' ||
protocol === 'node:' ||
protocol === 'data:' ||
protocol === 'deno:';
}

export let baseUrl: URL;
// @ts-ignore
if (typeof Deno !== 'undefined') {
Expand Down
35 changes: 19 additions & 16 deletions src/generator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { baseUrl as _baseUrl, relativeUrl, resolveUrl } from "./common/url.js";
import { baseUrl as _baseUrl, isMappableScheme, isPlain, relativeUrl, resolveUrl } from "./common/url.js";
import { ExactPackage, PackageConfig, toPackageTarget } from "./install/package.js";
import TraceMap from './trace/tracemap.js';
// @ts-ignore
Expand Down Expand Up @@ -467,20 +467,30 @@ export class Generator {
* Trace and pin a module, installing all dependencies necessary into the map
* to support its execution including static and dynamic module imports.
*
* @param specifier Import specifier to pin
* @param parentUrl Optional parent URL
* @deprecated Use "traceInstall" instead.
*/
async pin (specifier: string, parentUrl?: string): Promise<{
staticDeps: string[];
dynamicDeps: string[];
}> {
return this.traceInstall(specifier, parentUrl);
}

/**
* Trace a module, installing all dependencies necessary into the map
* to support its execution including static and dynamic module imports.
*
* @param specifier Module to trace
* @param parentUrl Optional parent URL
*/
async traceInstall (specifier: string, parentUrl?: string): Promise<{ staticDeps: string[], dynamicDeps: string[] }> {
let error = false;
if (this.installCnt++ === 0)
this.traceMap.startInstall();
await this.traceMap.processInputMap;
try {
await this.traceMap.visit(specifier, { mode: 'new-primary' }, parentUrl);
this.traceMap.pin(specifier);
await this.traceMap.visit(specifier, { mode: 'new', toplevel: true }, parentUrl || this.mapUrl.href);
this.traceMap.pins.push(specifier);
}
catch (e) {
error = true;
Expand All @@ -496,14 +506,6 @@ export class Generator {
}
}

/**
* @deprecated Renamed to "pin" instead.
* @param specifier Import specifier to trace
*/
async traceInstall (specifier: string): Promise<{ staticDeps: string[], dynamicDeps: string[] }> {
return this.pin(specifier);
}

/**
* Generate and inject an import map for an HTML file
*
Expand Down Expand Up @@ -611,7 +613,7 @@ export class Generator {
let modules = pins === true ? this.traceMap.pins : Array.isArray(pins) ? pins : [];
if (trace) {
const impts = [...new Set([...analysis.staticImports, ...analysis.dynamicImports])];
await Promise.all(impts.map(impt => this.pin(impt)));
await Promise.all(impts.map(impt => this.traceInstall(impt)));
modules = [...new Set([...modules, ...impts])];
}

Expand Down Expand Up @@ -742,8 +744,9 @@ export class Generator {
({ alias, target, subpath } = install);
}
await this.traceMap.add(alias, target);
await this.traceMap.visit(alias + subpath.slice(1), { mode: 'new-primary' });
this.traceMap.pin(alias + subpath.slice(1));
await this.traceMap.visit(alias + subpath.slice(1), { mode: 'new', toplevel: true }, this.mapUrl.href);
if (!this.traceMap.pins.includes(alias + subpath.slice(1)))
this.traceMap.pins.push(alias + subpath.slice(1));
}
catch (e) {
error = true;
Expand Down
39 changes: 11 additions & 28 deletions src/install/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class Installer {
if (visited.has(name + '##' + pkgUrl))
return;
visited.add(name + '##' + pkgUrl);
const { installUrl } = await this.install(name, pkgUrl === this.installBaseUrl ? 'existing-primary' : 'existing-secondary', pkgUrl);
const { installUrl } = await this.install(name, 'existing', pkgUrl);
const deps = await this.resolver.getDepList(installUrl);
const existingDeps = Object.keys(this.installs.secondary[installUrl] || {});
await Promise.all([...new Set([...deps, ...existingDeps])].map(dep => visitInstall(dep, installUrl)));
Expand Down Expand Up @@ -195,14 +195,8 @@ export class Installer {
return provider;
}

async installTarget (pkgName: string, target: InstallTarget, mode: 'new-primary' | 'existing-primary' | 'new-secondary' | 'existing-secondary', pkgScope: `${string}/` | null, parentUrl: string): Promise<InstalledResolution> {
if (mode.endsWith('-primary') && pkgScope !== null) {
throw new Error('Should have null scope for primary');
}
if (mode.endsWith('-secondary') && pkgScope === null) {
throw new Error('Should not have null scope for secondary');
}
if (this.opts.freeze && mode.startsWith('existing'))
async installTarget (pkgName: string, target: InstallTarget, mode: 'new' | 'existing', pkgScope: `${string}/` | null, parentUrl: string): Promise<InstalledResolution> {
if (this.opts.freeze && mode === 'existing')
throw new JspmError(`"${pkgName}" is not installed in the current map to freeze install, imported from ${parentUrl}.`, 'ERR_NOT_INSTALLED');

// resolutions are authoritative at the top-level
Expand All @@ -221,7 +215,7 @@ export class Installer {

const provider = this.getProvider(target);

if ((this.opts.freeze || mode.startsWith('existing') || mode.endsWith('secondary')) && !this.opts.latest) {
if ((this.opts.freeze || mode === 'existing' || pkgScope !== null) && !this.opts.latest) {
const pkg = this.getBestExistingMatch(target);
if (pkg) {
this.log('install', `${pkgName} ${pkgScope} -> ${pkg} (existing match)`);
Expand All @@ -236,7 +230,7 @@ export class Installer {
const latestUrl = this.resolver.pkgToUrl(latest.pkg, provider);
const installed = getInstallsFor(this.constraints, latest.pkg.registry, latest.pkg.name);
if (!this.opts.freeze && !this.tryUpgradeAllTo(latest.pkg, latestUrl, installed)) {
if (!mode.endsWith('-primary') && !this.opts.latest) {
if (pkgScope && !this.opts.latest) {
const pkg = this.getBestExistingMatch(target);
// cannot upgrade to latest -> stick with existing resolution (if compatible)
if (pkg) {
Expand All @@ -256,13 +250,7 @@ export class Installer {
return { installUrl: latestUrl, installSubpath: latest.subpath };
}

async install (pkgName: string, mode: 'new-primary' | 'new-secondary' | 'existing-primary' | 'existing-secondary', pkgScope: `${string}/` | null = null, flattenedSubpath: `.${string}` | null = null, nodeBuiltins = true, parentUrl: string = this.installBaseUrl): Promise<InstalledResolution> {
if (mode.endsWith('-primary') && pkgScope !== null) {
throw new Error('Should have null scope for primary');
}
if (mode.endsWith('-secondary') && pkgScope === null) {
throw new Error('Should not have null scope for secondary');
}
async install (pkgName: string, mode: 'new' | 'existing', pkgScope: `${string}/` | null = null, flattenedSubpath: `.${string}` | null = null, nodeBuiltins = true, parentUrl: string = this.installBaseUrl): Promise<InstalledResolution> {
if (!this.installing)
throwInternalError('Not installing');

Expand All @@ -274,7 +262,7 @@ export class Installer {
if (existingResolution)
return existingResolution;
// flattened resolution cascading for secondary
if (mode === 'existing-secondary' && !this.opts.latest || mode === 'new-secondary' && this.opts.freeze) {
if (pkgScope && mode === 'existing' && !this.opts.latest || pkgScope && mode === 'new' && this.opts.freeze) {
const flattenedResolution = getFlattenedResolution(this.installs, pkgName, pkgScope, flattenedSubpath);
// resolved flattened resolutions become real resolutions as they get picked up
if (flattenedResolution) {
Expand All @@ -284,13 +272,8 @@ export class Installer {
}
}

// setup for dependencies
if (!pkgScope)
pkgScope = await this.resolver.getPackageBase(parentUrl);
if (mode.endsWith('-primary'))
mode = mode.replace('-primary', '-secondary') as 'new-secondary' | 'existing-secondary';

const pcfg = await this.resolver.getPackageConfig(pkgScope) || {};
const definitelyPkgScope = pkgScope || await this.resolver.getPackageBase(parentUrl);
const pcfg = await this.resolver.getPackageConfig(definitelyPkgScope) || {};

// node.js core
if (nodeBuiltins && nodeBuiltinSet.has(pkgName)) {
Expand All @@ -300,7 +283,7 @@ export class Installer {
// package dependencies
const installTarget = pcfg.dependencies?.[pkgName] || pcfg.peerDependencies?.[pkgName] || pcfg.optionalDependencies?.[pkgName] || pkgScope === this.installBaseUrl && pcfg.devDependencies?.[pkgName];
if (installTarget) {
const target = newPackageTarget(installTarget, new URL(pkgScope), this.defaultRegistry, pkgName);
const target = newPackageTarget(installTarget, new URL(definitelyPkgScope), this.defaultRegistry, pkgName);
return this.installTarget(pkgName, target, mode, pkgScope, parentUrl);
}

Expand All @@ -309,7 +292,7 @@ export class Installer {
return getResolution(this.installs, pkgName, null);

// global install fallback
const target = newPackageTarget('*', new URL(pkgScope), this.defaultRegistry, pkgName);
const target = newPackageTarget('*', new URL(definitelyPkgScope), this.defaultRegistry, pkgName);
const exactInstall = await this.installTarget(pkgName, target, mode, pkgScope, parentUrl);
return exactInstall;
}
Expand Down
2 changes: 2 additions & 0 deletions src/trace/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,8 @@ export class Resolver {

async analyze (resolvedUrl: string, parentUrl: string, system: boolean, isRequire: boolean, retry = true): Promise<Analysis> {
const res = await fetch(resolvedUrl, this.fetchOpts);
if (!res)
throw new JspmError(`Unable to fetch URL "${resolvedUrl}" for ${parentUrl}`);
switch (res.status) {
case 200:
case 304:
Expand Down
Loading