-
Notifications
You must be signed in to change notification settings - Fork 208
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
simplify package path resolution #8186
base: master
Are you sure you want to change the base?
Conversation
f45f6ee
to
c2f2dc3
Compare
c2f2dc3
to
9f9f7c1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For resolving paths that are known at design-time, I'm fine with sync access.
But for paths that vary at runtime, let's not dip into sync I/O any more than we must.
* @returns {Promise<string>} the absolute path corresponding to `specPath` if it can be | ||
* @returns {string} the absolute path corresponding to `specPath` if it can be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a case where the path is dynamic - not determined until runtime. Let's keep it async, please.
Using sync I/O for a sort of linker/loader makes sense, but otherwise, let's not force clients into the world of sync I/O.
@@ -195,31 +194,27 @@ async function resolveSpecFromConfig(referrer, specPath) { | |||
* @param {boolean} expectParameters `true` if the entries should have parameters (for | |||
* example, `true` for `vats` but `false` for bundles). | |||
*/ | |||
async function normalizeConfigDescriptor(desc, referrer, expectParameters) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
likewise
packages/SwingSet/tools/paths.js
Outdated
* @param {string} specifier | ||
* @returns {string} file system absolute path of the package resource specifier | ||
*/ | ||
export const pkgAbsPath = specifier => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear that this is only used with paths that are known at design-time. Let's keep runtime I/O async.
const fullPath = await importMetaResolve(specifier, import.meta.url).then( | ||
u => new URL(u).pathname, | ||
); | ||
const fullPath = pkgAbsPath(specifier); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
presuming the specifier
arg is always a design-time constant, sync access is OK here. I'd be OK with copying pkgAbsPath
into this module.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My preference is to inline url.fileURLToPath(importMetaResolve(specifier, import.meta.url))
here with the expectation that this will eventually become url.fileURLToPath(import.meta.resolve(specifier))
when we drop support for Node.js versions that lack import.meta.resolve
.
I also encourage a general move toward framing Agoric APIs in terms of URLs instead of paths (as in Compartment Mapper) since they are more portable and obviate any impulse toward making this utility function.
import { resolve } from 'import-meta-resolve'; | ||
|
||
/** | ||
* @param {string} specifier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function should not be. Hiding the mechanism of resolution obscures the limitations of its use. I strongly prefer inlining url.filePathToURL(resolve(specifier, import.meta.url))
since this works anywhere without qualification.
If we kept this function, we would need to express the qualifications: it can only be reliably used for absolute module specifiers like @foo/bar
and not for relative module specifiers like ./foo.js
within this package. The resolution of an absolute specifier is not guaranteed to be consistent between packages, differing version constraints between packages, and varying node_modules layout algorithms.
The reason for these limitations is that it uses its own import.meta.url
instead of receiving the referrer specifier as a parameter.
That is to say, if present, this function should not be exported to other packages, which is antithetical to exporting it from tools.js
and its usage throughout this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolution of an absolute specifier is not guaranteed to be consistent between packages, differing version constraints between packages, and varying node_modules layout algorithms.
This I hadn't considered.
What if this function took parent
the way import-meta-resolve
does?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this function accepted the import.meta.url
from the caller, I would be converted from a “hard no” to a “soft no”, on the grounds that I’d prefer transparency at the call site. url.fileURLToPath(resolve(specifier, import.meta.url))
is very clear and not very long. Just removing the await is an improvement.
@@ -35,7 +35,7 @@ | |||
"@endo/far": "^0.2.19", | |||
"@endo/promise-kit": "^0.2.57", | |||
"@endo/stream": "^0.3.26", | |||
"import-meta-resolve": "^2.2.1" | |||
"import-meta-resolve": "^3.0.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m ambivalent about upgrading import-meta-resolve
to the new synchronous version, even though I’m sure it does degenerate to synchronous I/O (to inspect package.json
for imports
and exports
clauses). The shim is not privy to the information that Node.js collected before evaluating the module in which import.meta.resolve
gets expressed.
The native implementations in Node.js and the web of import.meta.resolve
do not degenerate to synchronous I/O and I am sure that we can safely virtualize import.meta.resolve
for Endo. Although the shim is not able to do this, it does make our code more closely resemble the eventual convergence on the (de-facto but not de-jure standard!) import.meta.resolve
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't parse out the argument against upgrading it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The downside is that the shim will perform synchronous I/O (to read package.json
files). The native implementation doesn’t need to read or interpret package.json
files because we can be sure Node.js already has them memoized. It had to read them in order to link the surrounding module.
The downside of inducing I/O in these cases is tenuous. It will reduce event loop availability, which is a good reason to avoid them while serving traffic or other operations. If tail latency for a server is not an issue, synchronous I/O tends overall to be faster than concurrent I/O.
I don’t fully grasp an absolute censure for I/O for OCaps. If we’re generally using this mechanism on boot-time or test-time constants, there’s not much risk of an attacker gaining the ability to indirectly tease these programs into reading more package.json
files than they strictly need. But this mechanism could induce an exception.
I’d say: be careful with dynamic import, even dynamic resolution. Because it requires care, as a reviewer, I would rather see import.meta.resolve
where it’s getting used rather than pkgAbsPath
. It raises a flag I need to see.
const fullPath = await importMetaResolve(specifier, import.meta.url).then( | ||
u => new URL(u).pathname, | ||
); | ||
const fullPath = pkgAbsPath(specifier); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My preference is to inline url.fileURLToPath(importMetaResolve(specifier, import.meta.url))
here with the expectation that this will eventually become url.fileURLToPath(import.meta.resolve(specifier))
when we drop support for Node.js versions that lack import.meta.resolve
.
I also encourage a general move toward framing Agoric APIs in terms of URLs instead of paths (as in Compartment Mapper) since they are more portable and obviate any impulse toward making this utility function.
@@ -188,9 +188,7 @@ export const getNodeTestVaultsConfig = async ( | |||
bundleDir = 'bundles', | |||
specifier = '@agoric/vm-config/decentral-itest-vaults-config.json', | |||
) => { | |||
const fullPath = await importMetaResolve(specifier, import.meta.url).then( | |||
u => new URL(u).pathname, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m enthusiastic about replacing new URL(u).pathname
with url.filePathToURL
everywhere it’s found, in order to eventually be portable to Windows! Thank you.
importMetaResolve(`@agoric/vm-config/${configName}`, import.meta.url).then( | ||
u => new URL(u).pathname, | ||
); | ||
pkgAbsPath(`@agoric/vm-config/${configName}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The correctness of this expression relies on the incidental detail that @agoric/vm-config
got installed somewhere accessible to @agoric/swingset-vat
. Your pnpm
experiment presumably revealed that this is not a reliable incidental detail unless @agoric/vm-config
also explicitly depends upon @agoric/swingset-vat
, and even then, is not guaranteed unless both of these packages depend on compatible versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect that peerDependencies will save the day for vm-config.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, import.meta.url
needs to get threaded to the call site. If there are import specifiers in a config file, import.meta.url
needs to be the file URL of the config file. If the config is embedded in a JavaScript file, it should pass its own import.meta.url
.
And I would rather see import.meta.url
or import.meta.resolve
than have them called by a utility function since there are nuances to whether they’re safe to use.
import { resolve } from 'import-meta-resolve'; | ||
|
||
/** | ||
* @param {string} specifier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this function accepted the import.meta.url
from the caller, I would be converted from a “hard no” to a “soft no”, on the grounds that I’d prefer transparency at the call site. url.fileURLToPath(resolve(specifier, import.meta.url))
is very clear and not very long. Just removing the await is an improvement.
@@ -35,7 +35,7 @@ | |||
"@endo/far": "^0.2.19", | |||
"@endo/promise-kit": "^0.2.57", | |||
"@endo/stream": "^0.3.26", | |||
"import-meta-resolve": "^2.2.1" | |||
"import-meta-resolve": "^3.0.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The downside is that the shim will perform synchronous I/O (to read package.json
files). The native implementation doesn’t need to read or interpret package.json
files because we can be sure Node.js already has them memoized. It had to read them in order to link the surrounding module.
The downside of inducing I/O in these cases is tenuous. It will reduce event loop availability, which is a good reason to avoid them while serving traffic or other operations. If tail latency for a server is not an issue, synchronous I/O tends overall to be faster than concurrent I/O.
I don’t fully grasp an absolute censure for I/O for OCaps. If we’re generally using this mechanism on boot-time or test-time constants, there’s not much risk of an attacker gaining the ability to indirectly tease these programs into reading more package.json
files than they strictly need. But this mechanism could induce an exception.
I’d say: be careful with dynamic import, even dynamic resolution. Because it requires care, as a reviewer, I would rather see import.meta.resolve
where it’s getting used rather than pkgAbsPath
. It raises a flag I need to see.
packages/SwingSet/tools/paths.js
Outdated
export const resolvePath = (specifier, base) => { | ||
const href = resolve(specifier, base); | ||
return url.fileURLToPath(href); | ||
}; | ||
|
||
export const makeResolvePath = base => { | ||
return specifier => resolvePath(specifier, base); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, this is a good compromise.
@warner I would encourage us to in general migrate as much of our API surface to treating file URLs as first-class, starting with @endo/bundle-source
and working our way out. @endo/compartment-mapper
APIs are already framed in terms of file URLs.
* @returns {string} file system absolute path of the package resource specifier | ||
*/ | ||
export const pkgAbsPath = specifier => { | ||
const href = resolve(specifier, import.meta.url); | ||
export const resolvePath = (specifier, base) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe resolveToPath
would be more clear. The arguments are both module specifiers and a path returns.
Description
import.meta.resolve returns a string now instead of a promise. https://github.com/wooorm/import-meta-resolve/releases/tag/3.0.0 updates the ponyfill.
This updates to it and goes on to simplify getting paths from packages.
Before
After
Where
pkgAbsPath
is imported from a utility module. It has its ownimport.meta.url
, which is sufficient for resolving packages.Security Considerations
Scaling Considerations
Documentation Considerations
Testing Considerations
Upgrade Considerations