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

[pull] main from mdx-js:main #20

Merged
merged 4 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add support for passing baseUrl when running
Previously, `baseUrl` was supported at compile time.
That meant it wasn’t possible to use `compile` with `outputFormat:
'function-body'` on a server and `run` on a client, and choosing the
URL there, which is likely what you want in that setup, to pass
`import.meta.url`.

Additionally, `import()` expressions using an exression (e.g.,
`'@mdx-js/' + 'mdx'`) are now supported.

If you use `run` or `evaluate`, you *should* pass `baseUrl`, likely as
`import.meta.url`.

If you don’t, and it is needed (because `export … from`, `import`, or
`import.meta.url`), you will get a runtime error.
  • Loading branch information
wooorm committed Oct 23, 2023
commit e12f3079ac3556ed3ca29829be87223648f264df
8 changes: 2 additions & 6 deletions packages/mdx/lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@
* Add a source map (object form) as the `map` field on the resulting file
* (optional).
* @property {URL | string | null | undefined} [baseUrl]
* Resolve `import`s (and `export … from`, and `import.meta.url`) from this
* URL (optional, example: `import.meta.url`);
* this option is useful when code will run in a different place, such as
* when `.mdx` files are in path *a* but compiled to path *b* and imports
* should run relative the path *b*, or when evaluating code, whether in Node
* or a browser.
* Use this URL as `import.meta.url` and resolve `import` and `export … from`
* relative to it (optional, example: `import.meta.url`).
* @property {boolean | null | undefined} [development=false]
* Whether to add extra info to error messages in generated code and use the
* development automatic JSX runtime (`Fragment` and `jsxDEV` from
Expand Down
294 changes: 263 additions & 31 deletions packages/mdx/lib/plugin/recma-document.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/**
* @typedef {import('estree-jsx').CallExpression} CallExpression
* @typedef {import('estree-jsx').Directive} Directive
* @typedef {import('estree-jsx').ExportAllDeclaration} ExportAllDeclaration
* @typedef {import('estree-jsx').ExportDefaultDeclaration} ExportDefaultDeclaration
* @typedef {import('estree-jsx').ExportNamedDeclaration} ExportNamedDeclaration
* @typedef {import('estree-jsx').ExportSpecifier} ExportSpecifier
* @typedef {import('estree-jsx').Expression} Expression
* @typedef {import('estree-jsx').FunctionDeclaration} FunctionDeclaration
* @typedef {import('estree-jsx').Identifier} Identifier
* @typedef {import('estree-jsx').ImportDeclaration} ImportDeclaration
* @typedef {import('estree-jsx').ImportDefaultSpecifier} ImportDefaultSpecifier
* @typedef {import('estree-jsx').ImportExpression} ImportExpression
Expand Down Expand Up @@ -36,6 +38,7 @@ import {create} from '../util/estree-util-create.js'
import {declarationToExpression} from '../util/estree-util-declaration-to-expression.js'
import {isDeclaration} from '../util/estree-util-is-declaration.js'
import {specifiersToDeclarations} from '../util/estree-util-specifiers-to-declarations.js'
import {toIdOrMemberExpression} from '../util/estree-util-to-id-or-member-expression.js'

/**
* Wrap the estree in `MDXContent`.
Expand All @@ -46,8 +49,8 @@ import {specifiersToDeclarations} from '../util/estree-util-specifiers-to-declar
* Transform.
*/
export function recmaDocument(options) {
const baseUrl_ = options.baseUrl || undefined
const baseUrl = typeof baseUrl_ === 'object' ? baseUrl_.href : baseUrl_
const baseUrl = options.baseUrl || undefined
const baseHref = typeof baseUrl === 'object' ? baseUrl.href : baseUrl
const outputFormat = options.outputFormat || 'program'
const pragma =
options.pragma === undefined ? 'React.createElement' : options.pragma
Expand Down Expand Up @@ -321,9 +324,68 @@ export function recmaDocument(options) {

tree.body = replacement

if (baseUrl) {
let usesImportMetaUrlVariable = false
let usesResolveDynamicHelper = false

if (baseHref || outputFormat === 'function-body') {
walk(tree, {
enter(node) {
if (
(node.type === 'ExportAllDeclaration' ||
node.type === 'ExportNamedDeclaration' ||
node.type === 'ImportDeclaration') &&
node.source
) {
// We never hit this branch when generating function bodies, as
// statements are already compiled away into import expressions.
assert(baseHref, 'unexpected missing `baseHref` in branch')

let value = node.source.value
// The literal source for statements can only be string.
assert(typeof value === 'string', 'expected string source')

// Resolve a specifier.
// This is the same as `_resolveDynamicMdxSpecifier`, which has to
// be injected to work with expressions at runtime, but as we have
// `baseHref` at compile time here and statements are static
// strings, we can do it now.
try {
// To do: use `URL.canParse` next major.
// eslint-disable-next-line no-new
new URL(value)
// Fine: a full URL.
} catch {
if (
value.startsWith('/') ||
value.startsWith('./') ||
value.startsWith('../')
) {
value = new URL(value, baseHref).href
} else {
// Fine: are bare specifier.
}
}

/** @type {SimpleLiteral} */
const replacement = {type: 'Literal', value}
create(node.source, replacement)
node.source = replacement
return
}

if (node.type === 'ImportExpression') {
usesResolveDynamicHelper = true
/** @type {CallExpression} */
const replacement = {
type: 'CallExpression',
callee: {type: 'Identifier', name: '_resolveDynamicMdxSpecifier'},
arguments: [node.source],
optional: false
}
node.source = replacement
return
}

if (
node.type === 'MemberExpression' &&
'object' in node &&
Expand All @@ -333,14 +395,38 @@ export function recmaDocument(options) {
node.object.property.name === 'meta' &&
node.property.name === 'url'
) {
/** @type {SimpleLiteral} */
const replacement = {type: 'Literal', value: baseUrl}
usesImportMetaUrlVariable = true
/** @type {Identifier} */
const replacement = {type: 'Identifier', name: '_importMetaUrl'}
create(node, replacement)
this.replace(replacement)
}
}
})
}

if (usesResolveDynamicHelper) {
if (!baseHref) {
usesImportMetaUrlVariable = true
}

tree.body.push(
resolveDynamicMdxSpecifier(
baseHref
? {type: 'Literal', value: baseHref}
: {type: 'Identifier', name: '_importMetaUrl'}
)
)
}

if (usesImportMetaUrlVariable) {
assert(
outputFormat === 'function-body',
'expected `function-body` when using dynamic url injection'
)
tree.body.unshift(...createImportMetaUrlVariable())
}

/**
* @param {ExportAllDeclaration | ExportNamedDeclaration} node
* Export node.
Expand Down Expand Up @@ -379,32 +465,6 @@ export function recmaDocument(options) {
* Nothing.
*/
function handleEsm(node) {
// Rewrite the source of the `import` / `export … from`.
// See: <https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier>
if (baseUrl && node.source) {
let value = String(node.source.value)

try {
// A full valid URL.
value = String(new URL(value))
} catch {
// Relative: `/example.js`, `./example.js`, and `../example.js`.
if (/^\.{0,2}\//.test(value)) {
value = String(new URL(value, baseUrl))
}
// Otherwise, it’s a bare specifiers.
// For example `some-package`, `@some-package`, and
// `some-package/path`.
// These are supported in Node and browsers plan to support them
// with import maps (<https://github.com/WICG/import-maps>).
}

/** @type {Literal} */
const literal = {type: 'Literal', value}
create(node.source, literal)
node.source = literal
}

/** @type {ModuleDeclaration | Statement | undefined} */
let replace
/** @type {Expression} */
Expand Down Expand Up @@ -639,3 +699,175 @@ export function recmaDocument(options) {
]
}
}

/**
* @param {Expression} importMetaUrl
* @returns {FunctionDeclaration}
*/
function resolveDynamicMdxSpecifier(importMetaUrl) {
return {
type: 'FunctionDeclaration',
id: {type: 'Identifier', name: '_resolveDynamicMdxSpecifier'},
generator: false,
async: false,
params: [{type: 'Identifier', name: 'd'}],
body: {
type: 'BlockStatement',
body: [
{
type: 'IfStatement',
test: {
type: 'BinaryExpression',
left: {
type: 'UnaryExpression',
operator: 'typeof',
prefix: true,
argument: {type: 'Identifier', name: 'd'}
},
operator: '!==',
right: {type: 'Literal', value: 'string'}
},
consequent: {
type: 'ReturnStatement',
argument: {type: 'Identifier', name: 'd'}
},
alternate: null
},
// To do: use `URL.canParse` when widely supported (see commented
// out code below).
{
type: 'TryStatement',
block: {
type: 'BlockStatement',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'NewExpression',
callee: {type: 'Identifier', name: 'URL'},
arguments: [{type: 'Identifier', name: 'd'}]
}
},
{
type: 'ReturnStatement',
argument: {type: 'Identifier', name: 'd'}
}
]
},
handler: {
type: 'CatchClause',
param: null,
body: {type: 'BlockStatement', body: []}
},
finalizer: null
},
// To do: use `URL.canParse` when widely supported.
// {
// type: 'IfStatement',
// test: {
// type: 'CallExpression',
// callee: toIdOrMemberExpression(['URL', 'canParse']),
// arguments: [{type: 'Identifier', name: 'd'}],
// optional: false
// },
// consequent: {
// type: 'ReturnStatement',
// argument: {type: 'Identifier', name: 'd'}
// },
// alternate: null
// },
{
type: 'IfStatement',
test: {
type: 'LogicalExpression',
left: {
type: 'LogicalExpression',
left: {
type: 'CallExpression',
callee: toIdOrMemberExpression(['d', 'startsWith']),
arguments: [{type: 'Literal', value: '/'}],
optional: false
},
operator: '||',
right: {
type: 'CallExpression',
callee: toIdOrMemberExpression(['d', 'startsWith']),
arguments: [{type: 'Literal', value: './'}],
optional: false
}
},
operator: '||',
right: {
type: 'CallExpression',
callee: toIdOrMemberExpression(['d', 'startsWith']),
arguments: [{type: 'Literal', value: '../'}],
optional: false
}
},
consequent: {
type: 'ReturnStatement',
argument: {
type: 'MemberExpression',
object: {
type: 'NewExpression',
callee: {type: 'Identifier', name: 'URL'},
arguments: [{type: 'Identifier', name: 'd'}, importMetaUrl]
},
property: {type: 'Identifier', name: 'href'},
computed: false,
optional: false
}
},
alternate: null
},
{
type: 'ReturnStatement',
argument: {type: 'Identifier', name: 'd'}
}
]
}
}
}

/**
* @returns {Array<Statement>}
*/
function createImportMetaUrlVariable() {
return [
{
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: {type: 'Identifier', name: '_importMetaUrl'},
init: toIdOrMemberExpression(['arguments', 0, 'baseUrl'])
}
],
kind: 'const'
},
{
type: 'IfStatement',
test: {
type: 'UnaryExpression',
operator: '!',
prefix: true,
argument: {type: 'Identifier', name: '_importMetaUrl'}
},
consequent: {
type: 'ThrowStatement',
argument: {
type: 'NewExpression',
callee: {type: 'Identifier', name: 'Error'},
arguments: [
{
type: 'Literal',
value:
'Unexpected missing `options.baseUrl` needed to support `export … from`, `import`, or `import.meta.url` when generating `function-body`'
}
]
}
},
alternate: null
}
]
}
2 changes: 1 addition & 1 deletion packages/mdx/lib/plugin/recma-jsx-rewrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ export function recmaJsxRewrite(options) {

createErrorHelper = true

if (development && place !== '1:1-1:1') {
if (development && place) {
parameters.push({type: 'Literal', value: place})
}

Expand Down
Loading