Skip to content

Commit e12f307

Browse files
committed
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.
1 parent c961af8 commit e12f307

File tree

7 files changed

+429
-67
lines changed

7 files changed

+429
-67
lines changed

packages/mdx/lib/core.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,8 @@
1616
* Add a source map (object form) as the `map` field on the resulting file
1717
* (optional).
1818
* @property {URL | string | null | undefined} [baseUrl]
19-
* Resolve `import`s (and `export … from`, and `import.meta.url`) from this
20-
* URL (optional, example: `import.meta.url`);
21-
* this option is useful when code will run in a different place, such as
22-
* when `.mdx` files are in path *a* but compiled to path *b* and imports
23-
* should run relative the path *b*, or when evaluating code, whether in Node
24-
* or a browser.
19+
* Use this URL as `import.meta.url` and resolve `import` and `export … from`
20+
* relative to it (optional, example: `import.meta.url`).
2521
* @property {boolean | null | undefined} [development=false]
2622
* Whether to add extra info to error messages in generated code and use the
2723
* development automatic JSX runtime (`Fragment` and `jsxDEV` from

packages/mdx/lib/plugin/recma-document.js

Lines changed: 263 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/**
2+
* @typedef {import('estree-jsx').CallExpression} CallExpression
23
* @typedef {import('estree-jsx').Directive} Directive
34
* @typedef {import('estree-jsx').ExportAllDeclaration} ExportAllDeclaration
45
* @typedef {import('estree-jsx').ExportDefaultDeclaration} ExportDefaultDeclaration
56
* @typedef {import('estree-jsx').ExportNamedDeclaration} ExportNamedDeclaration
67
* @typedef {import('estree-jsx').ExportSpecifier} ExportSpecifier
78
* @typedef {import('estree-jsx').Expression} Expression
89
* @typedef {import('estree-jsx').FunctionDeclaration} FunctionDeclaration
10+
* @typedef {import('estree-jsx').Identifier} Identifier
911
* @typedef {import('estree-jsx').ImportDeclaration} ImportDeclaration
1012
* @typedef {import('estree-jsx').ImportDefaultSpecifier} ImportDefaultSpecifier
1113
* @typedef {import('estree-jsx').ImportExpression} ImportExpression
@@ -36,6 +38,7 @@ import {create} from '../util/estree-util-create.js'
3638
import {declarationToExpression} from '../util/estree-util-declaration-to-expression.js'
3739
import {isDeclaration} from '../util/estree-util-is-declaration.js'
3840
import {specifiersToDeclarations} from '../util/estree-util-specifiers-to-declarations.js'
41+
import {toIdOrMemberExpression} from '../util/estree-util-to-id-or-member-expression.js'
3942

4043
/**
4144
* Wrap the estree in `MDXContent`.
@@ -46,8 +49,8 @@ import {specifiersToDeclarations} from '../util/estree-util-specifiers-to-declar
4649
* Transform.
4750
*/
4851
export function recmaDocument(options) {
49-
const baseUrl_ = options.baseUrl || undefined
50-
const baseUrl = typeof baseUrl_ === 'object' ? baseUrl_.href : baseUrl_
52+
const baseUrl = options.baseUrl || undefined
53+
const baseHref = typeof baseUrl === 'object' ? baseUrl.href : baseUrl
5154
const outputFormat = options.outputFormat || 'program'
5255
const pragma =
5356
options.pragma === undefined ? 'React.createElement' : options.pragma
@@ -321,9 +324,68 @@ export function recmaDocument(options) {
321324

322325
tree.body = replacement
323326

324-
if (baseUrl) {
327+
let usesImportMetaUrlVariable = false
328+
let usesResolveDynamicHelper = false
329+
330+
if (baseHref || outputFormat === 'function-body') {
325331
walk(tree, {
326332
enter(node) {
333+
if (
334+
(node.type === 'ExportAllDeclaration' ||
335+
node.type === 'ExportNamedDeclaration' ||
336+
node.type === 'ImportDeclaration') &&
337+
node.source
338+
) {
339+
// We never hit this branch when generating function bodies, as
340+
// statements are already compiled away into import expressions.
341+
assert(baseHref, 'unexpected missing `baseHref` in branch')
342+
343+
let value = node.source.value
344+
// The literal source for statements can only be string.
345+
assert(typeof value === 'string', 'expected string source')
346+
347+
// Resolve a specifier.
348+
// This is the same as `_resolveDynamicMdxSpecifier`, which has to
349+
// be injected to work with expressions at runtime, but as we have
350+
// `baseHref` at compile time here and statements are static
351+
// strings, we can do it now.
352+
try {
353+
// To do: use `URL.canParse` next major.
354+
// eslint-disable-next-line no-new
355+
new URL(value)
356+
// Fine: a full URL.
357+
} catch {
358+
if (
359+
value.startsWith('/') ||
360+
value.startsWith('./') ||
361+
value.startsWith('../')
362+
) {
363+
value = new URL(value, baseHref).href
364+
} else {
365+
// Fine: are bare specifier.
366+
}
367+
}
368+
369+
/** @type {SimpleLiteral} */
370+
const replacement = {type: 'Literal', value}
371+
create(node.source, replacement)
372+
node.source = replacement
373+
return
374+
}
375+
376+
if (node.type === 'ImportExpression') {
377+
usesResolveDynamicHelper = true
378+
/** @type {CallExpression} */
379+
const replacement = {
380+
type: 'CallExpression',
381+
callee: {type: 'Identifier', name: '_resolveDynamicMdxSpecifier'},
382+
arguments: [node.source],
383+
optional: false
384+
}
385+
node.source = replacement
386+
return
387+
}
388+
327389
if (
328390
node.type === 'MemberExpression' &&
329391
'object' in node &&
@@ -333,14 +395,38 @@ export function recmaDocument(options) {
333395
node.object.property.name === 'meta' &&
334396
node.property.name === 'url'
335397
) {
336-
/** @type {SimpleLiteral} */
337-
const replacement = {type: 'Literal', value: baseUrl}
398+
usesImportMetaUrlVariable = true
399+
/** @type {Identifier} */
400+
const replacement = {type: 'Identifier', name: '_importMetaUrl'}
401+
create(node, replacement)
338402
this.replace(replacement)
339403
}
340404
}
341405
})
342406
}
343407

408+
if (usesResolveDynamicHelper) {
409+
if (!baseHref) {
410+
usesImportMetaUrlVariable = true
411+
}
412+
413+
tree.body.push(
414+
resolveDynamicMdxSpecifier(
415+
baseHref
416+
? {type: 'Literal', value: baseHref}
417+
: {type: 'Identifier', name: '_importMetaUrl'}
418+
)
419+
)
420+
}
421+
422+
if (usesImportMetaUrlVariable) {
423+
assert(
424+
outputFormat === 'function-body',
425+
'expected `function-body` when using dynamic url injection'
426+
)
427+
tree.body.unshift(...createImportMetaUrlVariable())
428+
}
429+
344430
/**
345431
* @param {ExportAllDeclaration | ExportNamedDeclaration} node
346432
* Export node.
@@ -379,32 +465,6 @@ export function recmaDocument(options) {
379465
* Nothing.
380466
*/
381467
function handleEsm(node) {
382-
// Rewrite the source of the `import` / `export … from`.
383-
// See: <https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier>
384-
if (baseUrl && node.source) {
385-
let value = String(node.source.value)
386-
387-
try {
388-
// A full valid URL.
389-
value = String(new URL(value))
390-
} catch {
391-
// Relative: `/example.js`, `./example.js`, and `../example.js`.
392-
if (/^\.{0,2}\//.test(value)) {
393-
value = String(new URL(value, baseUrl))
394-
}
395-
// Otherwise, it’s a bare specifiers.
396-
// For example `some-package`, `@some-package`, and
397-
// `some-package/path`.
398-
// These are supported in Node and browsers plan to support them
399-
// with import maps (<https://github.com/WICG/import-maps>).
400-
}
401-
402-
/** @type {Literal} */
403-
const literal = {type: 'Literal', value}
404-
create(node.source, literal)
405-
node.source = literal
406-
}
407-
408468
/** @type {ModuleDeclaration | Statement | undefined} */
409469
let replace
410470
/** @type {Expression} */
@@ -639,3 +699,175 @@ export function recmaDocument(options) {
639699
]
640700
}
641701
}
702+
703+
/**
704+
* @param {Expression} importMetaUrl
705+
* @returns {FunctionDeclaration}
706+
*/
707+
function resolveDynamicMdxSpecifier(importMetaUrl) {
708+
return {
709+
type: 'FunctionDeclaration',
710+
id: {type: 'Identifier', name: '_resolveDynamicMdxSpecifier'},
711+
generator: false,
712+
async: false,
713+
params: [{type: 'Identifier', name: 'd'}],
714+
body: {
715+
type: 'BlockStatement',
716+
body: [
717+
{
718+
type: 'IfStatement',
719+
test: {
720+
type: 'BinaryExpression',
721+
left: {
722+
type: 'UnaryExpression',
723+
operator: 'typeof',
724+
prefix: true,
725+
argument: {type: 'Identifier', name: 'd'}
726+
},
727+
operator: '!==',
728+
right: {type: 'Literal', value: 'string'}
729+
},
730+
consequent: {
731+
type: 'ReturnStatement',
732+
argument: {type: 'Identifier', name: 'd'}
733+
},
734+
alternate: null
735+
},
736+
// To do: use `URL.canParse` when widely supported (see commented
737+
// out code below).
738+
{
739+
type: 'TryStatement',
740+
block: {
741+
type: 'BlockStatement',
742+
body: [
743+
{
744+
type: 'ExpressionStatement',
745+
expression: {
746+
type: 'NewExpression',
747+
callee: {type: 'Identifier', name: 'URL'},
748+
arguments: [{type: 'Identifier', name: 'd'}]
749+
}
750+
},
751+
{
752+
type: 'ReturnStatement',
753+
argument: {type: 'Identifier', name: 'd'}
754+
}
755+
]
756+
},
757+
handler: {
758+
type: 'CatchClause',
759+
param: null,
760+
body: {type: 'BlockStatement', body: []}
761+
},
762+
finalizer: null
763+
},
764+
// To do: use `URL.canParse` when widely supported.
765+
// {
766+
// type: 'IfStatement',
767+
// test: {
768+
// type: 'CallExpression',
769+
// callee: toIdOrMemberExpression(['URL', 'canParse']),
770+
// arguments: [{type: 'Identifier', name: 'd'}],
771+
// optional: false
772+
// },
773+
// consequent: {
774+
// type: 'ReturnStatement',
775+
// argument: {type: 'Identifier', name: 'd'}
776+
// },
777+
// alternate: null
778+
// },
779+
{
780+
type: 'IfStatement',
781+
test: {
782+
type: 'LogicalExpression',
783+
left: {
784+
type: 'LogicalExpression',
785+
left: {
786+
type: 'CallExpression',
787+
callee: toIdOrMemberExpression(['d', 'startsWith']),
788+
arguments: [{type: 'Literal', value: '/'}],
789+
optional: false
790+
},
791+
operator: '||',
792+
right: {
793+
type: 'CallExpression',
794+
callee: toIdOrMemberExpression(['d', 'startsWith']),
795+
arguments: [{type: 'Literal', value: './'}],
796+
optional: false
797+
}
798+
},
799+
operator: '||',
800+
right: {
801+
type: 'CallExpression',
802+
callee: toIdOrMemberExpression(['d', 'startsWith']),
803+
arguments: [{type: 'Literal', value: '../'}],
804+
optional: false
805+
}
806+
},
807+
consequent: {
808+
type: 'ReturnStatement',
809+
argument: {
810+
type: 'MemberExpression',
811+
object: {
812+
type: 'NewExpression',
813+
callee: {type: 'Identifier', name: 'URL'},
814+
arguments: [{type: 'Identifier', name: 'd'}, importMetaUrl]
815+
},
816+
property: {type: 'Identifier', name: 'href'},
817+
computed: false,
818+
optional: false
819+
}
820+
},
821+
alternate: null
822+
},
823+
{
824+
type: 'ReturnStatement',
825+
argument: {type: 'Identifier', name: 'd'}
826+
}
827+
]
828+
}
829+
}
830+
}
831+
832+
/**
833+
* @returns {Array<Statement>}
834+
*/
835+
function createImportMetaUrlVariable() {
836+
return [
837+
{
838+
type: 'VariableDeclaration',
839+
declarations: [
840+
{
841+
type: 'VariableDeclarator',
842+
id: {type: 'Identifier', name: '_importMetaUrl'},
843+
init: toIdOrMemberExpression(['arguments', 0, 'baseUrl'])
844+
}
845+
],
846+
kind: 'const'
847+
},
848+
{
849+
type: 'IfStatement',
850+
test: {
851+
type: 'UnaryExpression',
852+
operator: '!',
853+
prefix: true,
854+
argument: {type: 'Identifier', name: '_importMetaUrl'}
855+
},
856+
consequent: {
857+
type: 'ThrowStatement',
858+
argument: {
859+
type: 'NewExpression',
860+
callee: {type: 'Identifier', name: 'Error'},
861+
arguments: [
862+
{
863+
type: 'Literal',
864+
value:
865+
'Unexpected missing `options.baseUrl` needed to support `export … from`, `import`, or `import.meta.url` when generating `function-body`'
866+
}
867+
]
868+
}
869+
},
870+
alternate: null
871+
}
872+
]
873+
}

packages/mdx/lib/plugin/recma-jsx-rewrite.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ export function recmaJsxRewrite(options) {
446446

447447
createErrorHelper = true
448448

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

0 commit comments

Comments
 (0)