Skip to content

Commit

Permalink
patching support import.meta.resolve during build
Browse files Browse the repository at this point in the history
  • Loading branch information
thescientist13 committed Dec 8, 2024
1 parent 8de4ab9 commit 0e65869
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 8 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,10 @@
"@greenwood/plugin-adapter-vercel": "~0.31.0-alpha.0",
"@greenwood/plugin-renderer-lit": "~0.31.0-alpha.0",
"rimraf": "^6.0.1"
},
"pnpm": {
"patchedDependencies": {
"@greenwood/cli": "patches/@greenwood__cli.patch"
}
}
}
318 changes: 318 additions & 0 deletions patches/@greenwood__cli.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
diff --git a/src/lib/node-modules-utils.js b/src/lib/node-modules-utils.js
index c1d6f1c1010020ce63c1736c528d2ecc461ca08d..2364cf74512d01f9b4b94679b678fd41309ce8c2 100644
--- a/src/lib/node-modules-utils.js
+++ b/src/lib/node-modules-utils.js
@@ -1,47 +1,23 @@
-import { createRequire } from 'module';
import { checkResourceExists } from './resource-utils.js';
+import { resolveBareSpecifier, derivePackageRoot } from './walker-package-ranger.js';
import fs from 'fs/promises';

-// TODO delete me and everything else in this file
-// https://github.com/ProjectEvergreen/greenwood/issues/684
-async function getNodeModulesLocationForPackage(packageName) {
- let nodeModulesUrl;
-
- // require.resolve may fail in the event a package has no main in its package.json
- // so as a fallback, ask for node_modules paths and find its location manually
- // https://github.com/ProjectEvergreen/greenwood/issues/557#issuecomment-923332104
- // // https://stackoverflow.com/a/62499498/417806
- const require = createRequire(import.meta.url);
- const locations = require.resolve.paths(packageName);
-
- for (const location in locations) {
- const nodeModulesPackageRoot = `${locations[location]}/${packageName}`;
- const packageJsonLocation = `${nodeModulesPackageRoot}/package.json`;
-
- if (await checkResourceExists(new URL(`file://${packageJsonLocation}`))) {
- nodeModulesUrl = nodeModulesPackageRoot;
- }
+// take a "shortcut" pathname, e.g. /node_modules/lit/lit-html.js
+// and resolve it using import.meta.resolve
+function getResolvedHrefFromPathnameShortcut(pathname) {
+ const segments = pathname.replace('/node_modules/', '').split('/');
+ const hasScope = segments[0].startsWith('@');
+ const specifier = hasScope ? `${segments[0]}/${segments[1]}` : segments[0];
+ const resolved = resolveBareSpecifier(specifier);
+
+ if (resolved) {
+ const root = derivePackageRoot(resolved);
+
+ return `${root}${segments.slice(hasScope ? 2 : 1).join('/')}`;
+ } else {
+ // for example, local theme pack development
+ return `file://${pathname}`;
}
-
- if (!nodeModulesUrl) {
- console.debug(`Unable to look up ${packageName} using NodeJS require.resolve. Falling back to process.cwd()`);
- nodeModulesUrl = new URL(`./node_modules/${packageName}`, `file://${process.cwd()}`).pathname;
- }
-
- return nodeModulesUrl;
-}
-
-// extract the package name from a URL like /node_modules/<some>/<package>/index.js
-function getPackageNameFromUrl(url) {
- const packagePathPieces = url.split('node_modules/')[1].split('/'); // double split to handle node_modules within nested paths
- let packageName = packagePathPieces.shift();
-
- // handle scoped packages
- if (packageName.indexOf('@') === 0) {
- packageName = `${packageName}/${packagePathPieces.shift()}`;
- }
-
- return packageName;
}

async function getPackageJsonForProject({ userWorkspace, projectDirectory }) {
@@ -59,6 +35,5 @@ async function getPackageJsonForProject({ userWorkspace, projectDirectory }) {

export {
getPackageJsonForProject,
- getNodeModulesLocationForPackage,
- getPackageNameFromUrl
+ getResolvedHrefFromPathnameShortcut
};
\ No newline at end of file
diff --git a/src/lib/resource-utils.js b/src/lib/resource-utils.js
index 6a78e464785b6dc11e909acf64f66db975e77909..0ab3458dc1542929a8b003509e6d63783b446967 100644
--- a/src/lib/resource-utils.js
+++ b/src/lib/resource-utils.js
@@ -1,5 +1,6 @@
import fs from 'fs/promises';
import { hashString } from './hashing-utils.js';
+import { getResolvedHrefFromPathnameShortcut } from './node-modules-utils.js';
import htmlparser from 'node-html-parser';

async function modelResource(context, type, src = undefined, contents = undefined, optimizationAttr = undefined, rawAttributes = undefined) {
@@ -7,9 +8,10 @@ async function modelResource(context, type, src = undefined, contents = undefine
const extension = type === 'script' ? 'js' : 'css';
let sourcePathURL;

+ getResolvedHrefFromPathnameShortcut
if (src) {
sourcePathURL = src.startsWith('/node_modules')
- ? new URL(`.${src}`, projectDirectory)
+ ? new URL(getResolvedHrefFromPathnameShortcut(src))
: src.startsWith('/')
? new URL(`.${src}`, userWorkspace)
: new URL(`./${src.replace(/\.\.\//g, '').replace('./', '')}`, userWorkspace);
diff --git a/src/lib/walker-package-ranger.js b/src/lib/walker-package-ranger.js
index 3f5b0e870892bc59786ab466bcb7689fb5122539..0adb52cbb2f0f7aa8548855dbd0fdff7ac4e2255 100644
--- a/src/lib/walker-package-ranger.js
+++ b/src/lib/walker-package-ranger.js
@@ -3,11 +3,14 @@ import fs from 'fs';
/* eslint-disable max-depth,complexity */
// priority if from L -> R
const SUPPORTED_EXPORT_CONDITIONS = ['import', 'module-sync', 'default'];
+const IMPORT_MAP_RESOLVED_PREFIX = '/~';
const importMap = {};
const diagnostics = {};

-function updateImportMap(key, value) {
- importMap[key.replace('./', '')] = value.replace('./', '');
+function updateImportMap(key, value, resolvedRoot) {
+ if (!importMap[key.replace('./', '')]) {
+ importMap[key.replace('./', '')] = `${IMPORT_MAP_RESOLVED_PREFIX}${resolvedRoot.replace('file://', '')}${value.replace('./', '')}`;
+ }
}

// wrapper around import.meta.resolve to provide graceful error handling / logging
@@ -35,11 +38,27 @@ function resolveBareSpecifier(specifier) {
* root: 'file:///path/to/project/greenwood-lit-ssr/node_modules/.pnpm/lit-html@3.2.1/node_modules/lit-html/package.json'
* }
*/
-function derivePackageRoot(dependencyName, resolved) {
- const root = resolved.slice(0, resolved.lastIndexOf(`/node_modules/${dependencyName}/`));
- const derived = `${root}/node_modules/${dependencyName}/`;
+function derivePackageRoot(resolved) {
+ // can't rely on the specifier, for example in monorepos
+ // where @foo/bar may point to a non node_modules location
+ // e.g. packages/some-namespace/package.json
+ // so we walk backwards looking for nearest package.json
+ const segments = resolved
+ .replace('file://', '')
+ .split('/')
+ .filter(segment => segment !== '')
+ .reverse();
+ let root = resolved.replace(segments[0], '');
+
+ for (const segment of segments.slice(1)) {
+ if (fs.existsSync(new URL('./package.json', root).pathname)) {
+ break;
+ }

- return derived;
+ root = root.replace(`${segment}/`, '');
+ }
+
+ return root;
}

// Helper function to convert export patterns to a regex (thanks ChatGPT :D)
@@ -109,7 +128,7 @@ async function walkExportPatterns(dependency, sub, subValue, resolvedRoot) {
// https://unpkg.com/browse/@uswds/uswds@3.10.0/package.json
const rootSubRelativePath = relativePath.replace(rootSubValueOffset, '');

- updateImportMap(`${dependency}${rootSubOffset}${rootSubRelativePath}`, `/node_modules/${dependency}${relativePath}`);
+ updateImportMap(`${dependency}${rootSubOffset}${rootSubRelativePath}`, relativePath, resolvedRoot);
}
});
}
@@ -117,18 +136,18 @@ async function walkExportPatterns(dependency, sub, subValue, resolvedRoot) {
walkDirectoryForExportPatterns(new URL(`.${rootSubValueOffset}/`, resolvedRoot));
}

-function trackExportConditions(dependency, exports, sub, condition) {
+function trackExportConditions(dependency, exports, sub, condition, resolvedRoot) {
if (typeof exports[sub] === 'object') {
// also check for nested conditions of conditions, default to default for now
// https://unpkg.com/browse/@floating-ui/dom@1.6.12/package.json
if (sub === '.') {
- updateImportMap(dependency, `/node_modules/${dependency}/${exports[sub][condition].default ?? exports[sub][condition]}`);
+ updateImportMap(dependency, `${exports[sub][condition].default ?? exports[sub][condition]}`, resolvedRoot);
} else {
- updateImportMap(`${dependency}/${sub}`, `/node_modules/${dependency}/${exports[sub][condition].default ?? exports[sub][condition]}`);
+ updateImportMap(`${dependency}/${sub}`, `${exports[sub][condition].default ?? exports[sub][condition]}`, resolvedRoot);
}
} else {
// https://unpkg.com/browse/redux@5.0.1/package.json
- updateImportMap(dependency, `/node_modules/${dependency}/${exports[sub][condition]}`);
+ updateImportMap(dependency, `${exports[sub][condition]}`);
}
}

@@ -151,7 +170,7 @@ async function walkPackageForExports(dependency, packageJson, resolvedRoot) {
for (const condition of SUPPORTED_EXPORT_CONDITIONS) {
if (exports[sub][condition]) {
matched = true;
- trackExportConditions(dependency, exports, sub, condition);
+ trackExportConditions(dependency, exports, sub, condition, resolvedRoot);
break;
}
}
@@ -163,16 +182,16 @@ async function walkPackageForExports(dependency, packageJson, resolvedRoot) {
} else {
// handle (unconditional) subpath exports
if (sub === '.') {
- updateImportMap(dependency, `/node_modules/${dependency}/${exports[sub]}`);
+ updateImportMap(dependency, `${exports[sub]}`, resolvedRoot);
} else if (sub.indexOf('*') >= 0) {
await walkExportPatterns(dependency, sub, exports[sub], resolvedRoot);
} else {
- updateImportMap(`${dependency}/${sub}`, `/node_modules/${dependency}/${exports[sub]}`);
+ updateImportMap(`${dependency}/${sub}`, `${exports[sub]}`, resolvedRoot);
}
}
}
} else if (module || main) {
- updateImportMap(dependency, `/node_modules/${dependency}/${module ?? main}`);
+ updateImportMap(dependency, `${module ?? main}`, resolvedRoot);
} else {
// ex: https://unpkg.com/browse/uuid@3.4.0/package.json
diagnostics[dependency] = `WARNING: No supported entry point detected for => \`${dependency}\``;
@@ -186,7 +205,7 @@ async function walkPackageJson(packageJson = {}) {
const resolved = resolveBareSpecifier(dependency);

if (resolved) {
- const resolvedRoot = derivePackageRoot(dependency, resolved);
+ const resolvedRoot = derivePackageRoot(resolved);
const resolvedPackageJson = (await import(new URL('./package.json', resolvedRoot), { with: { type: 'json' } })).default;

walkPackageForExports(dependency, resolvedPackageJson, resolvedRoot);
@@ -196,7 +215,7 @@ async function walkPackageJson(packageJson = {}) {
const resolved = resolveBareSpecifier(dependency);

if (resolved) {
- const resolvedRoot = derivePackageRoot(dependency, resolved);
+ const resolvedRoot = derivePackageRoot(resolved);
const resolvedPackageJson = (await import(new URL('./package.json', resolvedRoot), { with: { type: 'json' } })).default;

walkPackageForExports(dependency, resolvedPackageJson, resolvedRoot);
@@ -246,5 +265,8 @@ function mergeImportMap(html = '', map = {}, shouldShim = false) {

export {
walkPackageJson,
- mergeImportMap
+ mergeImportMap,
+ resolveBareSpecifier,
+ derivePackageRoot,
+ IMPORT_MAP_RESOLVED_PREFIX
};
\ No newline at end of file
diff --git a/src/plugins/resource/plugin-node-modules.js b/src/plugins/resource/plugin-node-modules.js
index a80b54d6e094762056595635a7c9d95eab139260..92125cdcdaac5584c7a31b92ad3814b9529412b6 100644
--- a/src/plugins/resource/plugin-node-modules.js
+++ b/src/plugins/resource/plugin-node-modules.js
@@ -6,11 +6,10 @@
import { checkResourceExists } from '../../lib/resource-utils.js';
import fs from 'fs/promises';
import { nodeResolve } from '@rollup/plugin-node-resolve';
-import { getNodeModulesLocationForPackage, getPackageJsonForProject, getPackageNameFromUrl } from '../../lib/node-modules-utils.js';
-import { resolveForRelativeUrl } from '../../lib/resource-utils.js';
+import { getPackageJsonForProject, getResolvedHrefFromPathnameShortcut } from '../../lib/node-modules-utils.js';
import { ResourceInterface } from '../../lib/resource-interface.js';
import { mergeImportMap } from '../../lib/walker-package-ranger.js';
-import { walkPackageJson } from '../../lib/walker-package-ranger.js';
+import { walkPackageJson, IMPORT_MAP_RESOLVED_PREFIX } from '../../lib/walker-package-ranger.js';

let generatedImportMap;

@@ -22,42 +21,36 @@ class NodeModulesResource extends ResourceInterface {
}

async shouldResolve(url) {
- return url.pathname.indexOf('/node_modules/') === 0;
+ const { pathname } = url;
+
+ return pathname.startsWith(IMPORT_MAP_RESOLVED_PREFIX) || pathname.startsWith('/node_modules/');
}

async resolve(url) {
const { projectDirectory } = this.compilation.context;
const { pathname, searchParams } = url;
- const packageName = getPackageNameFromUrl(pathname);
- const absoluteNodeModulesLocation = await getNodeModulesLocationForPackage(packageName);
- const packagePathPieces = pathname.split('node_modules/')[1].split('/'); // double split to handle node_modules within nested paths
- // use node modules resolution logic first, else hope for the best from the root of the project
- const absoluteNodeModulesPathname = absoluteNodeModulesLocation
- ? `${absoluteNodeModulesLocation}${packagePathPieces.join('/').replace(packageName, '')}`
- : (await resolveForRelativeUrl(url, projectDirectory)).pathname;
+ const fromImportMap = pathname.startsWith(IMPORT_MAP_RESOLVED_PREFIX);
+ const isNodeModulesPathnameShortcut = pathname.startsWith('/node_modules/');
+ const resolvedHref = fromImportMap
+ ? pathname.replace(IMPORT_MAP_RESOLVED_PREFIX, 'file://')
+ : isNodeModulesPathnameShortcut
+ ? getResolvedHrefFromPathnameShortcut(pathname)
+ : new URL(`.${pathname}`, projectDirectory).href; // worst case fall back, assume project root
const params = searchParams.size > 0
? `?${searchParams.toString()}`
: '';

- return new Request(`file://${absoluteNodeModulesPathname}${params}`);
+ return new Request(`${resolvedHref}${params}`);
}

async shouldServe(url) {
- const { href, pathname, protocol } = url;
- const extension = pathname.split('.').pop();
- const existsAsJs = protocol === 'file:' && await checkResourceExists(new URL(`${href}.js`));
+ const { href, protocol } = url;

- return extension === 'mjs'
- || extension === '' && existsAsJs
- || extension === 'js' && url.pathname.startsWith('/node_modules/');
+ return protocol === 'file:' && await checkResourceExists(new URL(href));
}

async serve(url) {
- const pathname = url.pathname;
- const urlExtended = pathname.split('.').pop() === ''
- ? new URL(`file://${pathname}.js`)
- : url;
- const body = await fs.readFile(urlExtended, 'utf-8');
+ const body = await fs.readFile(url, 'utf-8');

return new Response(body, {
headers: new Headers({
21 changes: 13 additions & 8 deletions pnpm-lock.yaml

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

0 comments on commit 0e65869

Please sign in to comment.