Skip to content

Commit

Permalink
Add outputFormat option to compile to function body
Browse files Browse the repository at this point in the history
This exposes the currently internal `_contain` option in the public interface,
which is used by `evaluate`, so that users can depend on it too.

Related to GH-23
Closes GH-26.

Reviewed-by: Christian Murphy <[email protected]>
  • Loading branch information
wooorm authored Mar 22, 2021
1 parent 1130143 commit ab58a46
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 25 deletions.
12 changes: 6 additions & 6 deletions lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ import {nodeTypes} from './node-types.js'
* @typedef BaseProcessorOptions
* @property {boolean} [jsx=false] Whether to keep JSX
* @property {'mdx' | 'md'} [format='mdx'] Format of the files to be processed
* @property {'program' | 'function-body'} [outputFormat='program'] Whether to compile to a whole program or a function body.
* @property {string[]} [mdExtensions] Extensions (with `.`) for markdown
* @property {string[]} [mdxExtensions] Extensions (with `.`) for MDX
* @property {PluggableList} [recmaPlugins] List of recma (esast, JavaScript) plugins
* @property {PluggableList} [remarkPlugins] List of remark (mdast, markdown) plugins
* @property {PluggableList} [rehypePlugins] List of rehype (hast, HTML) plugins
* @property {boolean} [_contain=false] Semihidden option
*
* @typedef {Omit<RecmaDocumentOptions & RecmaStringifyOptions & RecmaJsxRewriteOptions, '_contain'>} PluginOptions
* @typedef {Omit<RecmaDocumentOptions & RecmaStringifyOptions & RecmaJsxRewriteOptions, 'outputFormat'>} PluginOptions
* @typedef {BaseProcessorOptions & PluginOptions} ProcessorOptions
*/

Expand All @@ -44,9 +44,9 @@ import {nodeTypes} from './node-types.js'
*/
export function createProcessor(options = {}) {
var {
_contain,
jsx,
format,
outputFormat,
providerImportSource,
recmaPlugins,
rehypePlugins,
Expand All @@ -72,11 +72,11 @@ export function createProcessor(options = {}) {
.use(rehypePlugins)
.use(format === 'md' ? rehypeRemoveRaw : undefined)
.use(rehypeRecma)
.use(recmaDocument, {...rest, _contain})
.use(recmaDocument, {...rest, outputFormat})
// @ts-ignore recma transformer uses an esast node rather than a unist node
.use(recmaJsxRewrite, {providerImportSource, _contain})
.use(recmaJsxRewrite, {providerImportSource, outputFormat})
// @ts-ignore recma transformer uses an esast node rather than a unist node
.use(jsx ? undefined : recmaJsxBuild, {_contain})
.use(jsx ? undefined : recmaJsxBuild, {outputFormat})
// @ts-ignore recma compiler is seen as a transformer
.use(recmaStringify, {SourceMapGenerator})
.use(recmaPlugins)
Expand Down
10 changes: 5 additions & 5 deletions lib/plugin/recma-document.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-objec

/**
* @typedef RecmaDocumentOptions
* @property {boolean} [_contain] Semihidden option which here results in failing on imports and adding a top-level return statement instead of an export.
* @property {'program' | 'function-body'} [outputFormat='program'] Whether to use import and export statements or get values from `arguments` and return things
* @property {string} [baseUrl] In `evaluate`, resolve relative import statements (and `export from`s) relative to this URL
* @property {string} [pragma='React.createElement'] Pragma for JSX (used in classic runtime)
* @property {string} [pragmaFrag='React.Fragment'] Pragma for JSX fragments (used in classic runtime)
Expand All @@ -22,8 +22,8 @@ import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-objec
*/
export function recmaDocument(options = {}) {
var {
_contain,
baseUrl,
outputFormat,
pragma = 'React.createElement',
pragmaFrag = 'React.Fragment',
pragmaImportSource = 'react',
Expand Down Expand Up @@ -221,7 +221,7 @@ export function recmaDocument(options = {}) {
replacement.push(createMdxContent())
}

if (_contain) {
if (outputFormat === 'function-body') {
exportedIdentifiers.push(['MDXContent', 'default'])
replacement.push(
u('ReturnStatement', {
Expand All @@ -248,7 +248,7 @@ export function recmaDocument(options = {}) {
tree.body = replacement

function handleImport(node) {
if (_contain) {
if (outputFormat === 'function-body') {
handleImportExportFrom(node)
} else {
replacement.push(node)
Expand All @@ -258,7 +258,7 @@ export function recmaDocument(options = {}) {
function handleExport(node) {
var child

if (_contain) {
if (outputFormat === 'function-body') {
// ```js
// export function a() {}
// export class A {}
Expand Down
11 changes: 6 additions & 5 deletions lib/plugin/recma-jsx-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-objec
* @typedef {import('estree').Program} Program
*
* @typedef RecmaJsxBuildOptions
* @property {boolean} [_contain] Semihidden option which here results in getting the automatic runtime from `arguments[0]` instead of importing it
* @property {'program' | 'function-body'} [outputFormat='program'] Whether to keep the import of the automatic runtime or get it from `arguments[0]` instead
*/

/**
Expand All @@ -16,7 +16,7 @@ import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-objec
* @param {RecmaJsxBuildOptions} [options]
*/
export function recmaJsxBuild(options = {}) {
var {_contain} = options
var {outputFormat} = options

return transform

Expand All @@ -26,10 +26,11 @@ export function recmaJsxBuild(options = {}) {
function transform(tree) {
build(tree)

// In contain mode, replace the import that was just generated, and get
// `jsx`, `jsxs`, and `Fragment` from `arguments[0]` instead.
// When compiling to a function body, replace the import that was just
// generated, and get `jsx`, `jsxs`, and `Fragment` from `arguments[0]`
// instead.
if (
_contain &&
outputFormat === 'function-body' &&
tree.body[0] &&
tree.body[0].type === 'ImportDeclaration' &&
typeof tree.body[0].source.value === 'string' &&
Expand Down
14 changes: 8 additions & 6 deletions lib/plugin/recma-jsx-rewrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-objec
* @typedef {import('estree').Program} Program
*
* @typedef RecmaJsxRewriteOptions
* @property {boolean} [_contain] Semihidden option which here results in getting `useMDXComponents` from `arguments[0]` instead of importing it
* @property {'program' | 'function-body'} [outputFormat='program'] Whether to use an import statement or `arguments[0]` to get the provider
* @property {string} [providerImportSource] Place to import a provider from
*/

Expand All @@ -22,7 +22,7 @@ import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-objec
* @param {RecmaJsxRewriteOptions} options
*/
export function recmaJsxRewrite(options = {}) {
var {providerImportSource, _contain} = options
var {providerImportSource, outputFormat} = options
return transform

/**
Expand All @@ -45,8 +45,10 @@ export function recmaJsxRewrite(options = {}) {

// If a provider is used (and can be used), import it.
if (importProvider) {
// @ts-ignore to do: figure out why `'init'` is not a string?
tree.body.unshift(createImportProvider(providerImportSource, _contain))
tree.body.unshift(
// @ts-ignore to do: figure out why `'init'` is not a string?
createImportProvider(providerImportSource, outputFormat)
)
}

function onenter(node) {
Expand Down Expand Up @@ -294,15 +296,15 @@ function createMissingComponentHelper() {
})
}

function createImportProvider(providerImportSource, contain) {
function createImportProvider(providerImportSource, outputFormat) {
var specifiers = [
u('ImportSpecifier', {
imported: u('Identifier', {name: 'useMDXComponents'}),
local: u('Identifier', {name: '_provideComponents'})
})
]

if (contain) {
if (outputFormat === 'function-body') {
return u('VariableDeclaration', {
kind: 'const',
declarations: [
Expand Down
4 changes: 2 additions & 2 deletions lib/util/resolve-evaluate-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @property {*} jsxs Function to generate an element with dynamic children
* @property {*} [useMDXComponents] Function to get `MDXComponents` from context
*
* @typedef {Omit<ProcessorOptions, 'jsx' | 'jsxImportSource' | 'jsxRuntime' | 'pragma' | 'pragmaFrag' | 'pragmaImportSource' | 'providerImportSource' | '_contain'> } EvaluateProcessorOptions
* @typedef {Omit<ProcessorOptions, 'jsx' | 'jsxImportSource' | 'jsxRuntime' | 'pragma' | 'pragmaFrag' | 'pragmaImportSource' | 'providerImportSource' | 'outputFormat'> } EvaluateProcessorOptions
*
* @typedef ExtraOptions
* @property {string} baseUrl URL to resolve imports from (typically: pass `import.meta.url`)
Expand All @@ -31,7 +31,7 @@ export function resolveEvaluateOptions(options) {
return {
compiletime: {
...rest,
_contain: true,
outputFormat: 'function-body',
providerImportSource: useMDXComponents ? '#' : undefined
},
runtime: {Fragment, jsx, jsxs, useMDXComponents}
Expand Down
2 changes: 1 addition & 1 deletion lib/util/resolve-file-and-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {md} from './extnames.js'

/**
* Create a file and options from a given `vfileCompatible` and options that
* might container `format: 'detect'`.
* might contain `format: 'detect'`.
*
* @param {import('vfile').VFileCompatible} vfileCompatible
* @param {import('../compile.js').CompileOptions} options
Expand Down
51 changes: 51 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,57 @@ Has no effect in `compile` or `evaluate`, but does affect [esbuild][],
[Rollup][], and the experimental ESM loader + register hook (see [👩‍🔬
Lab][lab]).

###### `options.outputFormat`

Output format to generate (`'program' | 'function-body'`, default: `'program'`).
In most cases `'program'` should be used, as it results in a whole program.
In [`evaluate`][eval] `outputFormat: 'function-body'` is used compile to code
that can be `eval`ed more easily.
In some cases, you might want to use `evaluate`, such as when compiling on the
server and running on the client.

The `'program'` format will use import statements to import the runtime (and
optionally provider) and otherwise keep the code as it was.

The `'function-body'` format normally crash on import statements, but it will
turn export statements into normal statements and return what was normally
exported.
It will also get the runtime (and optionally provider) from `arguments[0]`.

<details>
<summary>Example</summary>

A module `example.js`:

```js
import {compile} from 'xdm'

main('export var no = 3.14\n\n# hi {no}')

async function main(code) {
console.log(String(await compile(code, {outputFormat: 'program'}))) // Default
console.log(String(await compile(code, {outputFormat: 'function-body'})))
}
```

…yields:

```js
import {Fragment as _Fragment, jsx as _jsx} from 'react/jsx-runtime'
export var no = 3.14
function MDXContent(_props) { /**/ }
export default MDXContent
```

```js
const {Fragment: _Fragment, jsx: _jsx} = arguments[0]
var no = 3.14
function MDXContent(_props) { /**/ }
return {no, default: MDXContent}
```

</details>

###### `options.SourceMapGenerator`

The `SourceMapGenerator` class from [`source-map`][source-map] (optional).
Expand Down
1 change: 1 addition & 0 deletions test/esm-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ test('xdm (ESM loader)', async function (t) {
Content = await import('./context/esm-loader.mdx')
} catch (error) {
if (error.code === 'ERR_UNKNOWN_FILE_EXTENSION') {
await fs.unlink(path.join(base, 'esm-loader.mdx'))
throw new Error(
'Please run Node with `--experimental-loader=./esm-loader.js` to test the ESM loader'
)
Expand Down

0 comments on commit ab58a46

Please sign in to comment.