Skip to content

Commit

Permalink
feat: link with input HTML map when no importmap.json is present (#2577)
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford authored Aug 5, 2024
1 parent 2755a7b commit 7e81591
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 27 deletions.
10 changes: 5 additions & 5 deletions docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ In some cases there may be ambiguity. For instance, you may want to link the NPM
If no modules are given, all "imports" in the initial map are relinked.

### Options
* `-m, --map` _<file>_ File containing initial import map (default: importmap.json)
* `-m, --map` _<file>_ File containing initial import map (defaults to importmap.json, or the input HTML if linking)
* `-o, --output` _<file>_ File to inject the final import map into (default: --map / importmap.json)
* `-e, --env` <[environments](#environments)> Comma-separated environment condition overrides
* `-r, --resolution` <[resolutions](#resolutions)> Comma-separated dependency resolution overrides
Expand Down Expand Up @@ -65,7 +65,7 @@ jspm link ./src/cli.js
Link an HTML file and update its import map including preload and integrity tags

```
jspm link --map index.html --integrity --preload dynamic
jspm link --map index.html --integrity --preload
```
## install

Expand All @@ -79,7 +79,7 @@ Installs packages into an import map, along with all of the dependencies that ar
If no packages are provided, all "imports" in the initial map are reinstalled.

### Options
* `-m, --map` _<file>_ File containing initial import map (default: importmap.json)
* `-m, --map` _<file>_ File containing initial import map (defaults to importmap.json, or the input HTML if linking)
* `-o, --output` _<file>_ File to inject the final import map into (default: --map / importmap.json)
* `-e, --env` <[environments](#environments)> Comma-separated environment condition overrides
* `-r, --resolution` <[resolutions](#resolutions)> Comma-separated dependency resolution overrides
Expand Down Expand Up @@ -129,7 +129,7 @@ jspm uninstall [flags] [...packages]
Uninstalls packages from an import map. The given packages must be valid package specifiers, such as `npm:react@18.0.0`, `denoland:oak` or `lit`, and must be present in the initial import map.

### Options
* `-m, --map` _<file>_ File containing initial import map (default: importmap.json)
* `-m, --map` _<file>_ File containing initial import map (defaults to importmap.json, or the input HTML if linking)
* `-o, --output` _<file>_ File to inject the final import map into (default: --map / importmap.json)
* `-e, --env` <[environments](#environments)> Comma-separated environment condition overrides
* `-r, --resolution` <[resolutions](#resolutions)> Comma-separated dependency resolution overrides
Expand Down Expand Up @@ -161,7 +161,7 @@ jspm update [flags] [...packages]
Updates packages in an import map to the latest versions that are compatible with the local `package.json`. The given packages must be valid package specifiers, such as `npm:react@18.0.0`, `denoland:oak` or `lit`, and must be present in the initial import map.

### Options
* `-m, --map` _<file>_ File containing initial import map (default: importmap.json)
* `-m, --map` _<file>_ File containing initial import map (defaults to importmap.json, or the input HTML if linking)
* `-o, --output` _<file>_ File to inject the final import map into (default: --map / importmap.json)
* `-e, --env` <[environments](#environments)> Comma-separated environment condition overrides
* `-r, --resolution` <[resolutions](#resolutions)> Comma-separated dependency resolution overrides
Expand Down
6 changes: 3 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export const cli = cac(c.yellow("jspm"));
type opt = [string, string, any];
const mapOpt: opt = [
"-m, --map <file>",
"File containing initial import map",
{ default: "importmap.json" },
"File containing initial import map (defaults to importmap.json, or the input HTML if linking)",
{},
];
const envOpt: opt = [
"-e, --env <environments>",
Expand Down Expand Up @@ -144,7 +144,7 @@ cli
(
name
) => `Link an HTML file and update its import map including preload and integrity tags
$ ${name} link --map index.html --integrity --preload dynamic
$ ${name} link --map index.html --integrity --preload
`
)
.usage(
Expand Down
8 changes: 6 additions & 2 deletions src/link.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as fs from "node:fs/promises";
import { extname } from "node:path";
import { pathToFileURL } from "url";
import c from "picocolors";
import { type Generator } from "@jspm/generator";
Expand All @@ -9,6 +10,7 @@ import {
getInput,
getInputPath,
getOutputPath,
isJsExtension,
startSpinner,
stopSpinner,
writeOutput,
Expand All @@ -21,8 +23,10 @@ export default async function link(modules: string[], flags: Flags) {
log(`Linking modules: ${modules.join(", ")}`);
log(`Flags: ${JSON.stringify(flags)}`);

const fallbackMap = !modules[0] || isJsExtension(extname(modules[0])) ? undefined : modules[0];

const env = await getEnv(flags);
const inputMapPath = getInputPath(flags);
const inputMapPath = getInputPath(flags, fallbackMap);
const outputMapPath = getOutputPath(flags);
const generator = await getGenerator(flags);

Expand All @@ -36,7 +40,7 @@ export default async function link(modules: string[], flags: Flags) {
// The input map is either from a JSON file or extracted from an HTML file.
// In the latter case we want to trace any inline modules from the HTML file
// as well, since they may have imports that are not in the import map yet:
const input = await getInput(flags);
const input = await getInput(flags, fallbackMap);
const pins = inlinePins.concat(resolvedModules.map((p) => p.target));
let allPins = pins;
if (input) {
Expand Down
57 changes: 40 additions & 17 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from "node:fs/promises";
import { accessSync } from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { Generator, analyzeHtml } from "@jspm/generator";
Expand All @@ -8,7 +9,20 @@ import { withType } from "./logger";
import type { Flags, IImportMapJspm } from "./types";

// Default import map to use if none is provided:
const defaultInputPath = "./importmap.json";
const defaultMapPath = "importmap.json";

export function isJsExtension(ext) {
return (
ext === ".js" ||
ext === ".mjs" ||
ext === ".cjs" ||
ext === ".ts" ||
ext === ".mts" ||
ext === ".cts" ||
ext === ".jsx" ||
ext === ".tsx"
);
}

// Default HTML for import map injection:
const defaultHtmlTemplate = `<!DOCTYPE html>
Expand Down Expand Up @@ -105,7 +119,7 @@ async function writeHtmlOutput(
);

const mapFileRel = path.relative(process.cwd(), mapFile);
if (!(await exists(mapFile))) {
if (!exists(mapFile)) {
!silent &&
console.warn(
`${c.cyan(
Expand Down Expand Up @@ -213,11 +227,14 @@ export async function getGenerator(
});
}

export async function getInput(flags: Flags): Promise<string | undefined> {
const mapFile = getInputPath(flags);
if (!(await exists(mapFile))) return undefined;
if (!(await canRead(mapFile))) {
if (mapFile === defaultInputPath) return undefined;
export async function getInput(
flags: Flags,
fallbackDefaultMap = defaultMapPath
): Promise<string | undefined> {
const mapFile = getInputPath(flags, fallbackDefaultMap);
if (!exists(mapFile)) return undefined;
if (!canRead(mapFile)) {
if (mapFile === defaultMapPath) return undefined;
else
throw new JspmError(`JSPM does not have permission to read ${mapFile}.`);
}
Expand Down Expand Up @@ -249,14 +266,20 @@ async function getInputMap(flags: Flags): Promise<IImportMapJspm> {
return (inputMap || {}) as IImportMapJspm;
}

export function getInputPath(flags: Flags): string {
return path.resolve(process.cwd(), flags?.map || defaultInputPath);
export function getInputPath(
flags: Flags,
fallbackDefaultMap = defaultMapPath
): string {
return path.resolve(
process.cwd(),
flags?.map || (exists(defaultMapPath) ? defaultMapPath : fallbackDefaultMap)
);
}

export function getOutputPath(flags: Flags): string | undefined {
return path.resolve(
process.cwd(),
flags.output || flags.map || defaultInputPath
flags.output || flags.map || defaultMapPath
);
}

Expand Down Expand Up @@ -409,28 +432,28 @@ export function stopSpinner() {
spinner.stop();
}

export async function exists(file: string) {
export function exists(file: string) {
try {
await fs.access(file);
accessSync(file);
return true;
} catch (e) {
return false;
}
}

async function canRead(file: string) {
function canRead(file: string) {
try {
await fs.access(file, (fs.constants || fs).R_OK);
accessSync(file, (fs.constants || fs).R_OK);
return true;
} catch (e) {
return false;
}
}

async function canWrite(file: string) {
function canWrite(file: string) {
try {
if (!(await exists(file))) return true;
await fs.access(file, (fs.constants || fs).W_OK);
if (!exists(file)) return true;
accessSync(file, (fs.constants || fs).W_OK);
return true;
} catch (e) {
return false;
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ <h1>Test</h1>
}
}
</script>
<script type="module" src="app.js"></script>
12 changes: 12 additions & 0 deletions test/link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,18 @@ const scenarios: Scenario[] = [
assert(!map.imports?.["react-dom"]);
},
},

// Support the HTML as being the import map when there is no importmap.json:
{
files: new Map([...htmlFile, ['app.js', 'import "react"']]),
commands: ["jspm link index.html -o index.html --integrity"],
validationFn: async (files: Map<string, string>) => {
const source = files.get('index.html');
assert(source.includes('"integrity"'));
assert(source.includes('"./app.js": "sha384-f+bWmpnsmFol2CAkqy/ALGgZsi/mIaBIIhbvFLVuQzt0LNz96zLSDcz1fnF2K22q"'));
assert(source.includes('"https://ga.jspm.io/npm:react@18.2.0/dev.index.js": "sha384-eSJrEMXot96AKVLYz8C1nY3CpLMuBMHIAiYhs7vfM09SQo+5X+1w6t3Ldpnw+VWU"'))
},
},
];

await runScenarios(scenarios);

0 comments on commit 7e81591

Please sign in to comment.