Add Node ESM Loader and Register Entrypoints (#20274)

* Add Node ESM loader build

This adds a loader build as a first-class export. This will grow in
complexity so it deserves its own module.

* Add Node CommonJS regiter build

This adds a build as a first-class export for legacy CommonJS registration
in Node.js. This will grow in complexity so it deserves its own module.

* Simplify fixture a bit to easier show usage with or without esm

* Bump es version

We leave async function in here which are newer than ES2015.
This commit is contained in:
Sebastian Markbåge
2020-11-16 23:46:27 -05:00
committed by GitHub
parent bf7b7aeb10
commit 82e99e1b02
18 changed files with 181 additions and 84 deletions

View File

@@ -0,0 +1,24 @@
import {resolve, getSource} from 'react-transport-dom-webpack/node-loader';
export {resolve, getSource};
import babel from '@babel/core';
const babelOptions = {
babelrc: false,
ignore: [/\/(build|node_modules)\//],
plugins: [
'@babel/plugin-syntax-import-meta',
'@babel/plugin-transform-react-jsx',
],
};
export async function transformSource(source, context, defaultTransformSource) {
const {format} = context;
if (format === 'module') {
const opt = Object.assign({filename: context.url}, babelOptions);
const {code} = await babel.transformAsync(source, opt);
return {source: code};
}
return defaultTransformSource(source, context, defaultTransformSource);
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -67,7 +67,7 @@
"prebuild": "cp -r ../../build/node_modules/* ./node_modules/",
"start": "concurrently \"npm run start:server\" \"npm run start:client\"",
"start:client": "node scripts/start.js",
"start:server": "NODE_ENV=development node --experimental-loader ./server/loader.mjs server",
"start:server": "NODE_ENV=development node --experimental-loader ./loader/index.js server",
"start:prod": "node scripts/build.js && NODE_ENV=production node server",
"build": "node scripts/build.js",
"test": "node scripts/test.js --env=jsdom"

View File

@@ -2,18 +2,26 @@
import {pipeToNodeWritable} from 'react-transport-dom-webpack/server';
import * as React from 'react';
import App from '../src/App.server';
module.exports = function(req, res) {
import url from 'url';
function resolve(path) {
return url.pathToFileURL(require.resolve(path)).href;
}
module.exports = async function(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
const m = await import('../src/App.server.js');
// const m = require('../src/App.server.js');
const App = m.default.default || m.default;
pipeToNodeWritable(<App />, res, {
// TODO: Read from a map on the disk.
[require.resolve('../src/Counter.client.js')]: {
[resolve('../src/Counter.client.js')]: {
id: './src/Counter.client.js',
chunks: ['1'],
name: 'default',
},
[require.resolve('../src/ShowMore.client.js')]: {
[resolve('../src/ShowMore.client.js')]: {
id: './src/ShowMore.client.js',
chunks: ['2'],
name: 'default',

View File

@@ -1,27 +0,0 @@
import {pipeToNodeWritable} from 'react-transport-dom-webpack/server.js';
import * as React from 'react';
import App from '../src/App.server.js';
import {URL} from 'url';
const rootPath = import.meta.url;
function resolve(relative) {
return new URL(relative, rootPath).href;
}
export default function(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
pipeToNodeWritable(<App />, res, {
// TODO: Read from a map on the disk.
[resolve('../src/Counter.client.js')]: {
id: './src/Counter.client.js',
chunks: ['1'],
name: 'default',
},
[resolve('../src/ShowMore.client.js')]: {
id: './src/ShowMore.client.js',
chunks: ['2'],
name: 'default',
},
});
};

View File

@@ -1,11 +1,7 @@
'use strict';
require.extensions['.client.js'] = function(module, path) {
module.exports = {
$$typeof: Symbol.for('react.module.reference'),
name: path,
};
};
const register = require('react-transport-dom-webpack/node-register');
register();
const babelRegister = require('@babel/register');
@@ -25,8 +21,7 @@ app.get('/', function(req, res) {
delete require.cache[key];
}
}
import('./handler.server.mjs').then(m => m.default(req, res));
// require('./handler.server.js')(req, res);
require('./handler.server.js')(req, res);
});
app.listen(3001, () => {

View File

@@ -1,42 +0,0 @@
import babel from '@babel/core';
const options = {
babelrc: false,
ignore: [/\/(build|node_modules)\//],
plugins: [
'@babel/plugin-syntax-import-meta',
'@babel/plugin-transform-react-jsx',
],
};
const optionsCommonJS = {
ignore: [/\/(build|node_modules)\//],
presets: ['react-app'],
plugins: ['@babel/transform-modules-commonjs'],
};
export async function transformSource(source, context, defaultTransformSource) {
const {format} = context;
if (format === 'module' || format === 'commonjs') {
const opt = Object.assign(
{filename: context.url},
format === 'commonjs' ? optionsCommonJS : options
);
const {code} = await babel.transformAsync(source, opt);
return {source: code};
}
return defaultTransformSource(source, context);
}
export async function getSource(url, context, defaultGetSource) {
if (url.endsWith('.client.js')) {
const name = url;
return {
source:
"export default { $$typeof: Symbol.for('react.module.reference'), name: " +
JSON.stringify(name) +
'}',
};
}
return defaultGetSource(url, context, defaultGetSource);
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from '../src/ReactFlightWebpackNodeLoader.js';

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from './src/ReactFlightWebpackNodeRegister';

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,3 @@
'use strict';
module.exports = require('./cjs/react-transport-dom-webpack-node-register.js');

View File

@@ -17,9 +17,21 @@
"server.js",
"server.browser.js",
"server.node.js",
"node-register.js",
"cjs/",
"umd/"
"umd/",
"esm/"
],
"exports": {
".": "./index.js",
"./plugin": "./plugin.js",
"./server": "./server.js",
"./server.browser": "./server.browser.js",
"./server.node": "./server.node.js",
"./node-loader": "./esm/react-transport-dom-webpack-node-loader.js",
"./node-register": "./node-register.js",
"./package.json": "./package.json"
},
"browser": {
"./server.js": "./server.browser.js"
},

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
type ResolveContext = {
conditions: Array<string>,
parentURL: string | void,
};
type ResolveFunction = (
string,
ResolveContext,
ResolveFunction,
) => Promise<string>;
type GetSourceContext = {
format: string,
url: string,
};
type GetSourceFunction = (
string,
GetSourceContext,
GetSourceFunction,
) => Promise<{source: Source}>;
type Source = string | ArrayBuffer | Uint8Array;
export async function resolve(
specifier: string,
context: ResolveContext,
defaultResolve: ResolveFunction,
): Promise<string> {
// TODO: Resolve server-only files.
return defaultResolve(specifier, context, defaultResolve);
}
export async function getSource(
url: string,
context: GetSourceContext,
defaultGetSource: GetSourceFunction,
): Promise<{source: Source}> {
if (url.endsWith('.client.js')) {
// TODO: Named exports.
const src =
"export default { $$typeof: Symbol.for('react.module.reference'), name: " +
JSON.stringify(url) +
'}';
return {source: src};
}
return defaultGetSource(url, context, defaultGetSource);
}

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
const url = require('url');
module.exports = function register() {
(require: any).extensions['.client.js'] = function(module, path) {
module.exports = {
$$typeof: Symbol.for('react.module.reference'),
name: url.pathToFileURL(path).href,
};
};
};

View File

@@ -286,6 +286,24 @@ const bundles = [
externals: [],
},
/******* React Transport DOM Webpack Node.js Loader *******/
{
bundleTypes: [NODE_ESM],
moduleType: RENDERER_UTILS,
entry: 'react-transport-dom-webpack/node-loader',
global: 'ReactFlightWebpackNodeLoader',
externals: [],
},
/******* React Transport DOM Webpack Node.js CommonJS Loader *******/
{
bundleTypes: [NODE_ES2015],
moduleType: RENDERER_UTILS,
entry: 'react-transport-dom-webpack/node-register',
global: 'ReactFlightWebpackNodeRegister',
externals: ['url'],
},
/******* React Transport DOM Server Relay *******/
{
bundleTypes: [FB_WWW_DEV, FB_WWW_PROD],

View File

@@ -45,7 +45,7 @@ module.exports = {
jest: true,
},
parserOptions: {
ecmaVersion: 2015,
ecmaVersion: 2017,
sourceType: 'module',
},
rules: {

View File

@@ -10,6 +10,7 @@
const esNextPaths = [
// Internal forwarding modules
'packages/*/*.js',
'packages/*/esm/*.js',
// Source files
'packages/*/src/**/*.js',
'packages/dom-event-testing-library/**/*.js',