From 7e81591f131a1176ecf75b1803e1638c8c3b9174 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Mon, 5 Aug 2024 22:22:15 +0200 Subject: [PATCH] feat: link with input HTML map when no importmap.json is present (#2577) --- docs.md | 10 +++---- src/cli.ts | 6 ++--- src/link.ts | 8 ++++-- src/utils.ts | 57 ++++++++++++++++++++++++++++------------ test/fixtures/index.html | 1 + test/link.test.ts | 12 +++++++++ 6 files changed, 67 insertions(+), 27 deletions(-) diff --git a/docs.md b/docs.md index 764b1c789..860fa46a4 100644 --- a/docs.md +++ b/docs.md @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/cli.ts b/src/cli.ts index 2312a91cd..c10d26a3f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -30,8 +30,8 @@ export const cli = cac(c.yellow("jspm")); type opt = [string, string, any]; const mapOpt: opt = [ "-m, --map ", - "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 ", @@ -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( diff --git a/src/link.ts b/src/link.ts index d3dcba712..22f8fa26d 100644 --- a/src/link.ts +++ b/src/link.ts @@ -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"; @@ -9,6 +10,7 @@ import { getInput, getInputPath, getOutputPath, + isJsExtension, startSpinner, stopSpinner, writeOutput, @@ -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); @@ -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) { diff --git a/src/utils.ts b/src/utils.ts index df8fffd62..d83fdc71b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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"; @@ -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 = ` @@ -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( @@ -213,11 +227,14 @@ export async function getGenerator( }); } -export async function getInput(flags: Flags): Promise { - 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 { + 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}.`); } @@ -249,14 +266,20 @@ async function getInputMap(flags: Flags): Promise { 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 ); } @@ -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; diff --git a/test/fixtures/index.html b/test/fixtures/index.html index 0a6fd1d59..6c5565d69 100644 --- a/test/fixtures/index.html +++ b/test/fixtures/index.html @@ -15,3 +15,4 @@

Test

} } + \ No newline at end of file diff --git a/test/link.test.ts b/test/link.test.ts index 74cdbcb0d..2eb6c3a8a 100644 --- a/test/link.test.ts +++ b/test/link.test.ts @@ -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) => { + 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);