Skip to content

Commit

Permalink
first pass resolveSpecifier
Browse files Browse the repository at this point in the history
  • Loading branch information
dburles committed Dec 27, 2024
1 parent 1df906c commit 8b1f47a
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 37 deletions.
117 changes: 85 additions & 32 deletions createResolveLinkRelations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import resolveImportMap from "./resolve-import-map/resolveImportMap.mjs";

/** @typedef {Map<any, any> | AsyncMap} AsyncMapLike */

/** @typedef {(specifier: string) => string} ResolveSpecifier */

// The import map parser requries a base url. We don't require one for our purposes,
// but it allows us to use the parser without modifying the source. One quirk is that it will try map
// this url to files locally if it's specified, but no one should do that.
Expand Down Expand Up @@ -52,8 +54,12 @@ async function exists(filePath) {
* @param {object} options Options.
* @param {string} options.url The module URL to resolve.
* @param {object} [options.parsedImportMap] A parsed import map.
* @param {ResolveSpecifier} [options.resolveSpecifierOverride] Override specifier resolution.
*/
function resolveSpecifier(specifier, { url, parsedImportMap }) {
function resolveSpecifier(
specifier,
{ url, parsedImportMap, resolveSpecifierOverride = (x) => x },
) {
// If an import map is supplied, everything resolves through it.
if (parsedImportMap) {
const importMapResolved = resolveImportMap(
Expand All @@ -64,11 +70,18 @@ function resolveSpecifier(specifier, { url, parsedImportMap }) {

if (importMapResolved.hostname === DUMMY_HOSTNAME) {
// It will match if it's a local module.
return "." + importMapResolved.pathname;
return {
importMap: true,
importMapResolved,
specifier: resolveSpecifierOverride(importMapResolved.pathname),
};
}
}

return specifier;
return {
importMap: false,
specifier: resolveSpecifierOverride(specifier),
};
}

/**
Expand All @@ -77,10 +90,16 @@ function resolveSpecifier(specifier, { url, parsedImportMap }) {
* @param {object} options Options.
* @param {string} options.url The module URL to resolve.
* @param {object} [options.parsedImportMap] A parsed import map.
* @param {ResolveSpecifier} [options.resolveSpecifierOverride] Override specifier resolution.
* @param {string} options.rootPath The absolute path to the specified application root.
* @param {boolean} [root] Whether the module is the root module.
* @returns An array containing paths to modules that can be preloaded.
*/
async function resolveImports(module, { url, parsedImportMap }, root = true) {
async function resolveImports(
module,
{ url, parsedImportMap, resolveSpecifierOverride, rootPath },
root = true,
) {
/** @type {Array<string>} */
let modules = [];

Expand All @@ -99,21 +118,31 @@ async function resolveImports(module, { url, parsedImportMap }, root = true) {
const resolvedSpecifier = resolveSpecifier(specifier, {
url,
parsedImportMap,
resolveSpecifierOverride,
});
const resolvedModule = path.resolve(
path.dirname(module),
resolvedSpecifier,
const resolvedModule = path.join(
resolvedSpecifier.importMap ? rootPath : path.dirname(module),
resolvedSpecifier.specifier,
);

// If the module has resolved to a local file (and it exists), then it's preloadable.
if (resolvedModule && (await exists(resolvedModule))) {
if (
resolvedModule &&
resolvedModule.startsWith(rootPath) &&
(await exists(resolvedModule))
) {
if (!root) {
modules.push(resolvedModule);
}

const graph = await resolveImports(
resolvedModule,
{ parsedImportMap, url },
{
parsedImportMap,
url: resolvedSpecifier.importMapResolved?.pathname || url,
resolveSpecifierOverride,
rootPath,
},
false,
);

Expand All @@ -135,15 +164,25 @@ async function resolveImports(module, { url, parsedImportMap }, root = true) {
* @param {AsyncMapLike} options.cache Resolved imports cache.
* @param {string} options.url The module URL to resolve.
* @param {object} [options.parsedImportMap] A parsed import map.
* @param {ResolveSpecifier} [options.resolveSpecifierOverride] Override specifier resolution.
* @param {string} options.rootPath The absolute path to the specified application root.
* @returns An array containing paths to modules that can be preloaded, or otherwise `undefined`.
*/
async function resolveImportsCached(module, { cache, url, parsedImportMap }) {
async function resolveImportsCached(
module,
{ cache, url, parsedImportMap, resolveSpecifierOverride, rootPath },
) {
const paths = await cache.get(module);

if (paths) {
return paths;
} else {
const graph = await resolveImports(module, { parsedImportMap, url });
const graph = await resolveImports(module, {
parsedImportMap,
url,
resolveSpecifierOverride,
rootPath,
});

if (graph.length > 0) {
await cache.set(module, graph);
Expand All @@ -164,39 +203,53 @@ export default function createResolveLinkRelations(
appPath,
{ importMap: importMapString, cache = new Map() } = {},
) {
/** @type {object} */
let parsedImportMap;

if (importMapString !== undefined) {
parsedImportMap = parseFromString(
importMapString,
`https://${DUMMY_HOSTNAME}`,
);
}

/**
* Resolves link relations for a given URL.
* @param {string} url The module URL to resolve.
* @param {object} [options] Options.
* @param {ResolveSpecifier} [options.resolveSpecifier] Override specifier resolution.
* @returns An array containing relative paths to modules that can be preloaded, or otherwise `undefined`.
*/
return async function resolveLinkRelations(url) {
let parsedImportMap;

if (importMapString !== undefined) {
parsedImportMap = parseFromString(
importMapString,
`https://${DUMMY_HOSTNAME}`,
);
}

return async function resolveLinkRelations(
url,
{ resolveSpecifier: resolveSpecifierOverride } = {},
) {
const rootPath = path.resolve(appPath);

const resolvedSpecifier = resolveSpecifier(url, { url, parsedImportMap });
const resolvedModule = path.join(rootPath, resolvedSpecifier);

const modules = await resolveImportsCached(resolvedModule, {
cache,
url: resolvedSpecifier,
const resolvedSpecifier = resolveSpecifier(url, {
url,
parsedImportMap,
resolveSpecifierOverride,
});
const resolvedModule = path.join(rootPath, resolvedSpecifier.specifier);

if (Array.isArray(modules) && modules.length > 0) {
const resolvedModules = modules.map((module) => {
return "/" + path.relative(rootPath, module);
if (resolvedModule.startsWith(rootPath)) {
const modules = await resolveImportsCached(resolvedModule, {
cache,
url,
parsedImportMap,
resolveSpecifierOverride,
rootPath,
});

if (resolvedModules.length > 0) {
return resolvedModules;
if (Array.isArray(modules) && modules.length > 0) {
const resolvedModules = modules.map((module) => {
return "/" + path.relative(rootPath, module);
});

if (resolvedModules.length > 0) {
return resolvedModules;
}
}
}
};
Expand Down
32 changes: 28 additions & 4 deletions createResolveLinkRelations.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,20 @@ test("createResolveLinkRelations", async (t) => {
assert.equal(resolvedModulesCached.length, 4);
});

await t.test("can't reach outside of appPath", async () => {
const resolveLinkRelations = createResolveLinkRelations("test-fixtures");
const resolvedModules = await resolveLinkRelations("../../a.mjs");
await t.test("can't reach outside of appPath", async (tt) => {
await tt.test("root module", async () => {
const resolveLinkRelations = createResolveLinkRelations("test-fixtures");
const resolvedModules = await resolveLinkRelations("../../a.mjs");

assert.equal(resolvedModules, undefined);
assert.equal(resolvedModules, undefined);
});

await tt.test("imports", async () => {
const resolveLinkRelations = createResolveLinkRelations("test-fixtures");
const resolvedModules = await resolveLinkRelations("./outside.mjs");

assert.equal(resolvedModules, undefined);
});
});

await t.test("module without imports", async () => {
Expand Down Expand Up @@ -139,4 +148,19 @@ test("createResolveLinkRelations", async (t) => {

assert.equal(resolvedModulesCached.length, 4);
});

await t.test("resolveSpecifier", async (tt) => {
await tt.test("basic", async () => {
const resolveLinkRelations = createResolveLinkRelations("test-fixtures");
const resolvedModules = await resolveLinkRelations("/a.mjs", {
resolveSpecifier(specifier) {
return specifier.replace("/a", "/b");
},
});

assert.ok(Array.isArray(resolvedModules));

assert.ok(resolvedModules.includes("/d.mjs"));
});
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "modulepreload-link-relations",
"version": "3.0.0",
"version": "3.1.0-beta.0",
"description": "Utility for generating modulepreload link relations based on a JavaScript module import graph.",
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions test-fixtures/outside.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "../createResolveLinkRelations.mjs";

0 comments on commit 8b1f47a

Please sign in to comment.