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

Fix importComponent #2070

Merged
merged 10 commits into from
Feb 16, 2022
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@
"@commitlint/cli": "^13.0.0",
"@commitlint/config-conventional": "^13.0.0",
"@commitlint/config-lerna-scopes": "^13.0.0",
"canarist": "^2.3.1",
"canarist": "^2.3.2",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.0.0",
"eslint-config-prettier": "^8.3.0",
Expand Down
10 changes: 9 additions & 1 deletion packages/jest-environment/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ const build = ({ cwd, env = {}, argv = [] }) =>
const startServer = ({ cwd, command, env = {}, argv = [] }) => {
const teardownPromise = new EProm();
const urlPromise = new EProm();

let stdout = '';
let stderr = '';
const success1 = "bundling 'develop' finished";
const success2 = "bundling 'node' finished";

const hopsBin = resolveFrom(cwd, 'hops/bin');
const args = [hopsBin, command].concat(argv);
Expand All @@ -61,15 +64,20 @@ const startServer = ({ cwd, command, env = {}, argv = [] }) => {
return teardownPromise;
};

let serverUrl = '';
started.stdout.on('data', (data) => {
const line = data.toString('utf-8');
debug('stdout >', line);
stdout += line;

const [, url] = line.match(/listening at (.*)/i) || [];
if (url) {
serverUrl = url;
debug('found match:', url);
urlPromise.resolve(url);
}

if (stdout.includes(success1) && stdout.includes(success2) && serverUrl) {
urlPromise.resolve(serverUrl);
}
});

Expand Down
1 change: 1 addition & 0 deletions packages/jest-preset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"dependencies": {
"@babel/core": "^7.9.0",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-syntax-jsx": "^7.8.3",
"@babel/plugin-transform-flow-strip-types": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"@babel/preset-react": "^7.9.4",
Expand Down
20 changes: 15 additions & 5 deletions packages/jest-preset/transforms/esbuild.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
// eslint-disable-next-line node/no-extraneous-require
const { createTransformer } = require('esbuild-jest');
const { transformSync } = require('@babel/core');

const transformer = createTransformer({
sourcemap: true,
});

const regex = /importComponent/g;

module.exports = {
process(content, filename, config, opts) {
content = content.replace(
/importComponent\s*\(\s*\(\)\s+=>\s+import\(\s*'([^']+)'\s*\)\s*\)/g,
"importComponent({ component: require('$1') })"
);
if (!regex.test(content)) {
return transformer.process(content, filename, config, opts);
}

const result = transformSync(content, {
plugins: [
require.resolve('../helpers/babel-plugin-import-component'),
require.resolve('@babel/plugin-syntax-jsx'),
],
filename,
});

return transformer.process(content, filename, config, opts);
return transformer.process(result.code, filename, config, opts);
},
};
73 changes: 68 additions & 5 deletions packages/react/import-component/babel.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
'use strict';

const nodePath = require('path');
const crypto = require('crypto');

/**
* This plugin transforms the following function call:
* ```javascript
* importComponent(() => import('./component'));
* ```
*
* Into the following:
*
* ```javascript
* importComponent({
* load: () => import('./component'),
* moduleId: require.resolveWeak('./component'),
* chunkName: () => 'hash'
* })
* ```
*/

const regex = /webpackChunkName:\s*(?:(['"])([^'"]*)\1|([^\s]*))/;

function extractChunkName(t, leadingComments) {
if (!leadingComments || !Array.isArray(leadingComments)) {
return;
}

const comment = leadingComments.find((comment) =>
comment.value.includes('webpackChunkName')
);

if (!comment) {
return;
}

const results = comment.value.match(regex);
const cleanChunkName = results[2] || results[3];

return cleanChunkName;
}

module.exports = ({ types: t }) => ({
visitor: {
ImportDeclaration(path) {
ImportDeclaration(path, state) {
const modules = ['hops-react', this.opts.module];
const source = path.node.source.value;
if (!modules.includes(source)) return;
Expand Down Expand Up @@ -31,19 +72,37 @@ module.exports = ({ types: t }) => ({
t.assertCallExpression(argument.get('body'));
t.assertImport(argument.get('body.callee'));

const { node } = argument.get('body.arguments.0');
const importedComponent = node.value;
const leadingComments = node.leadingComments;
const argPath = argument.get('body.arguments.0');
const importedComponent = argPath.node.value;
const resourcePathNode = t.stringLiteral(importedComponent);

let chunkName = extractChunkName(t, argPath.node.leadingComments);
if (!chunkName) {
const hasher = crypto.createHash('md4');
hasher.update(nodePath.relative(this.opts.rootDir, state.filename));
hasher.update(importedComponent);
const hash = hasher.digest('base64').slice(0, 4);

t.addComment(
argPath.node,
'leading',
` webpackChunkName: '${hash}' `
);
chunkName = hash;
}

argument.replaceWith(
t.objectExpression([
t.objectProperty(
t.identifier('load'),
t.arrowFunctionExpression(
[],
t.callExpression(t.identifier('import'), [
t.addComments(resourcePathNode, 'leading', leadingComments),
t.addComments(
resourcePathNode,
'leading',
argPath.node.leadingComments
),
])
)
),
Expand All @@ -57,6 +116,10 @@ module.exports = ({ types: t }) => ({
[resourcePathNode]
)
),
t.objectProperty(
t.identifier('chunkName'),
t.arrowFunctionExpression([], t.stringLiteral(chunkName))
),
])
);
});
Expand Down
31 changes: 26 additions & 5 deletions packages/react/import-component/import-component-loader.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
const regex =
/importComponent\s*\(\s*\(\)\s+=>\s+import\(\s*'([^']+)'\s*\)\s*\)/g;
const { transform } = require('@babel/core');
const regex = /importComponent/g;

function importComponentLoader(source) {
return source.replace(
regex,
"importComponent({ load: () => import('$1'), moduleId: require.resolveWeak('$1') })"
const callback = this.async();

if (!regex.test(source)) {
return callback(null, source);
}

const options = this.getOptions();

transform(
source,
{
plugins: [
[require.resolve('./babel'), options],
require.resolve('@babel/plugin-syntax-jsx'),
],
filename: this.resourcePath,
},
(err, result) => {
if (err) {
return callback(err);
}

callback(null, result.code, result.map);
}
);
}

Expand Down
14 changes: 6 additions & 8 deletions packages/react/import-component/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import PropTypes from 'prop-types';
import ImportComponentContext from './context';

export const importComponent = (
{ load, moduleId },
{ load, moduleId, chunkName },
resolve = (module) => module.default
) => {
class Importer extends Component {
constructor({ hasModules }) {
constructor() {
super();
if (hasModules || __webpack_modules__[moduleId]) {
if (__webpack_modules__[moduleId]) {
this.state = { Component: resolve(__webpack_require__(moduleId)) };
} else {
this.state = { loading: true };
Expand Down Expand Up @@ -57,21 +57,19 @@ export const importComponent = (
}

Importer.propTypes = {
hasModules: PropTypes.bool,
ownProps: PropTypes.object.isRequired,
loader: PropTypes.func,
render: PropTypes.func,
};

function Import({ loader, render, ...ownProps }) {
const modules = useContext(ImportComponentContext);
const chunks = useContext(ImportComponentContext);

if (modules) {
modules.push(moduleId);
if (chunks) {
chunks.push(chunkName());
}

return createElement(Importer, {
hasModules: Boolean(modules && modules.length > 0),
loader,
render,
ownProps,
Expand Down
7 changes: 5 additions & 2 deletions packages/react/import-component/mixin.core.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ class ImportComponentCoreMixin extends Mixin {
const { experimentalEsbuild } = this.options;

if (experimentalEsbuild) {
jsLoaderConfig.use.push(require.resolve('./import-component-loader.js'));
jsLoaderConfig.use.push({
loader: require.resolve('./import-component-loader.js'),
options: { module: 'hops', rootDir: this.config.rootDir },
});
} else {
jsLoaderConfig.options.plugins.push([
require.resolve('../lib/babel'),
{ module: 'hops' },
{ module: 'hops', rootDir: this.config.rootDir },
]);
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/react/import-component/mixin.runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { Provider } from './context';
class ImportComponentMixin extends Mixin {
bootstrap(_req, res) {
if (res) {
res.locals.modules = this.modules = [];
res.locals.chunks = this.chunks = [];
}
}

enhanceElement(element) {
return createElement(Provider, { value: this.modules }, element);
return createElement(Provider, { value: this.chunks }, element);
}
}

Expand Down
18 changes: 11 additions & 7 deletions packages/react/lib/assets.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
'use strict';

import { extname } from 'path';
import { ensureLeadingSlash } from 'pathifist';

export default (stats, modules) => {
const { entryFiles, vendorFiles, moduleFileMap } = stats;
const moduleFiles = modules.reduce(
(result, module) => [...result, ...moduleFileMap[module]],
[]
);
export default (stats, chunks) => {
const { entryFiles, vendorFiles } = stats;
const chunkFiles = chunks.reduce((result, chunk) => {
const chunkGroup = stats.namedChunkGroups[chunk];
const assets = chunkGroup.assets.map((asset) =>
ensureLeadingSlash(asset.name)
);
return [...result, ...assets];
}, []);

return [...vendorFiles, ...moduleFiles, ...entryFiles]
return [...vendorFiles, ...chunkFiles, ...entryFiles]
.filter(
(asset, index, self) =>
self.indexOf(asset) === index &&
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dependencies": {
"@babel/core": "^7.9.0",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-syntax-jsx": "^7.8.3",
"@babel/plugin-transform-flow-strip-types": "^7.9.0",
"@babel/preset-react": "^7.9.4",
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
Expand Down
8 changes: 4 additions & 4 deletions packages/react/render/mixin.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ class ReactMixin extends Mixin {
return renderToFragments(element);
}

renderTemplate(fragments, { modules }) {
const assets = getAssets(this.stats, modules);
renderTemplate(fragments, { chunks }) {
const assets = getAssets(this.stats, chunks);
const resourceHints = getResourceHints(this.stats);
const globals = { _env: this.config._env };

Expand Down Expand Up @@ -70,9 +70,9 @@ class ReactMixin extends Mixin {
} else {
res.status(router.status || 200);

// note: res.locals.modules is set by the ImportComponentMixin
// note: res.locals.chunks is set by the ImportComponentMixin
return this.renderTemplate(fragments, {
modules: res.locals.modules,
chunks: res.locals.chunks,
}).then((page) => res.send(page));
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/integration/pwa/__tests__/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('pwa production build', () => {
// all responses should now come from the service worker
expect(
Array.from(requests.values()).map((r) => r.response().fromServiceWorker())
).toEqual([true, true, true]);
).toEqual([true, true]);

await page.close();
});
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/integration/react/__tests__/develop.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ describe('react development server', () => {
const preloadAs = await getProperty('as', 'link[rel="preload"]');
const prefetchHref = await getProperty('href', 'link[rel="prefetch"]');

expect(preloadHref).toMatch(/\/fixture-react-[0-9].js$/);
expect(preloadHref).toMatch(/\/fixture-react-[^.]+.js$/);
expect(preloadAs).toBe('script');
expect(prefetchHref).toMatch(/\/fixture-react-[0-9].js$/);
expect(prefetchHref).toMatch(/\/fixture-react-[^.]+.js$/);

await page.close();
});
Expand Down
Loading