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: link with input HTML map when no importmap.json is present #2577

Merged
merged 3 commits into from
Aug 5, 2024
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
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:[email protected]`, `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:[email protected]`, `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)",
{},
guybedford marked this conversation as resolved.
Show resolved Hide resolved
];
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:[email protected]/dev.index.js": "sha384-eSJrEMXot96AKVLYz8C1nY3CpLMuBMHIAiYhs7vfM09SQo+5X+1w6t3Ldpnw+VWU"'))
},
},
];

await runScenarios(scenarios);
Loading