From 79a061857c4c638ef83092312518375036a38427 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Sun, 29 Dec 2019 17:20:28 +1100 Subject: [PATCH 01/10] fix: terser might compress different files differently, fixes #166 --- .size-limit.js | 2 +- .size.json | 6 +- __tests__/__fixtures__/babel/node/expected.js | 82 ++++++++++++++--- .../__fixtures__/babel/webpack/expected.js | 47 ++++++---- __tests__/__snapshots__/macro.spec.ts.snap | 48 ++-------- __tests__/loadable.spec.ts | 54 +---------- __tests__/signatures.spec.ts | 92 +++++++++++++++++++ package.json | 3 +- src/babel.ts | 60 ++++++------ src/loadable.ts | 30 ++---- src/signatures.ts | 14 +++ src/types.ts | 51 +++++----- wrapper.js | 8 ++ 13 files changed, 287 insertions(+), 210 deletions(-) create mode 100644 __tests__/signatures.spec.ts create mode 100644 src/signatures.ts create mode 100644 wrapper.js diff --git a/.size-limit.js b/.size-limit.js index a8dfd6be..b359fe5d 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -12,6 +12,6 @@ module.exports = [ { path: 'dist/es2015/boot.js', ignore: ['tslib'], - limit: '1.7 KB', + limit: '1.8 KB', }, ]; diff --git a/.size.json b/.size.json index c12b7a40..b80cfc9a 100644 --- a/.size.json +++ b/.size.json @@ -2,16 +2,16 @@ { "name": "dist/es2015/index.js, dist/es2015/boot.js", "passed": true, - "size": 3407 + "size": 3426 }, { "name": "dist/es2015/index.js", "passed": true, - "size": 3123 + "size": 3130 }, { "name": "dist/es2015/boot.js", "passed": true, - "size": 1695 + "size": 1756 } ] diff --git a/__tests__/__fixtures__/babel/node/expected.js b/__tests__/__fixtures__/babel/node/expected.js index 1406b327..e1c97baa 100644 --- a/__tests__/__fixtures__/babel/node/expected.js +++ b/__tests__/__fixtures__/babel/node/expected.js @@ -1,17 +1,71 @@ -var importedWrapper = function (marker, realImport) { - if (typeof __deoptimization_sideEffect__ !== 'undefined') { - __deoptimization_sideEffect__(marker, realImport); - } - - return realImport; -}; +var importedWrapper = require('react-imported-component/wrapper'); -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } +function _interopRequireWildcard(obj) { + if (obj && obj.__esModule) { + return obj; + } else { + var newObj = {}; + if (obj != null) { + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = + Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; + if (desc.get || desc.set) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + } + newObj.default = obj; + return newObj; + } +} import imported from 'react-imported-component'; -const AsyncComponent0 = imported(() => importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))); -const AsyncComponent1 = imported(() => importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))); -const AsyncComponent2 = imported(async () => await importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))); -const AsyncComponent3 = imported(() => Promise.all([importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent')))), importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))])); -const AsyncComponent4 = imported(async () => (await Promise.all([importedWrapper("imported_-1qs8n90_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent1')))), importedWrapper("imported_9j5sqq_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent2'))))]))[0]); -export default AsyncComponent1; \ No newline at end of file +const AsyncComponent0 = imported(() => + importedWrapper( + 'imported_18g2v0c_component', + Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))) + ) +); +const AsyncComponent1 = imported(() => + importedWrapper( + 'imported_18g2v0c_component', + Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))) + ) +); +const AsyncComponent2 = imported( + async () => + await importedWrapper( + 'imported_18g2v0c_component', + Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))) + ) +); +const AsyncComponent3 = imported(() => + Promise.all([ + importedWrapper( + 'imported_18g2v0c_component', + Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))) + ), + importedWrapper( + 'imported_18g2v0c_component', + Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))) + ), + ]) +); +const AsyncComponent4 = imported( + async () => + (await Promise.all([ + importedWrapper( + 'imported_-1qs8n90_component', + Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent1'))) + ), + importedWrapper( + 'imported_9j5sqq_component', + Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent2'))) + ), + ]))[0] +); +export default AsyncComponent1; diff --git a/__tests__/__fixtures__/babel/webpack/expected.js b/__tests__/__fixtures__/babel/webpack/expected.js index ab4f5808..22d57581 100644 --- a/__tests__/__fixtures__/babel/webpack/expected.js +++ b/__tests__/__fixtures__/babel/webpack/expected.js @@ -1,19 +1,32 @@ -var importedWrapper = function (marker, realImport) { - if (typeof __deoptimization_sideEffect__ !== 'undefined') { - __deoptimization_sideEffect__(marker, realImport); - } +var importedWrapper = require('react-imported-component/wrapper'); - return realImport; -}; - -import { lazy, useImported } from "react-imported-component"; -import { assignImportedComponents } from "react-imported-component/boot"; +import { lazy, useImported } from 'react-imported-component'; +import { assignImportedComponents } from 'react-imported-component/boot'; import imported from 'react-imported-component'; -const AsyncComponent0 = imported(() => importedWrapper("imported_18g2v0c_component", import( -/* webpackChunkName:namedChunk */ -'./MyComponent'))); -const AsyncComponent1 = imported(() => importedWrapper("imported_18g2v0c_component", import('./MyComponent'))); -const AsyncComponent2 = imported(async () => await importedWrapper("imported_18g2v0c_component", import('./MyComponent'))); -const AsyncComponent3 = imported(() => Promise.all([importedWrapper("imported_18g2v0c_component", import('./MyComponent')), importedWrapper("imported_18g2v0c_component", import('./MyComponent'))])); -const AsyncComponent4 = imported(async () => (await Promise.all([importedWrapper("imported_-1qs8n90_component", import('./MyComponent1')), importedWrapper("imported_9j5sqq_component", import('./MyComponent2'))]))[0]); -export default AsyncComponent1; \ No newline at end of file +const AsyncComponent0 = imported(() => + importedWrapper( + 'imported_18g2v0c_component', + import( + /* webpackChunkName:namedChunk */ + './MyComponent' + ) + ) +); +const AsyncComponent1 = imported(() => importedWrapper('imported_18g2v0c_component', import('./MyComponent'))); +const AsyncComponent2 = imported( + async () => await importedWrapper('imported_18g2v0c_component', import('./MyComponent')) +); +const AsyncComponent3 = imported(() => + Promise.all([ + importedWrapper('imported_18g2v0c_component', import('./MyComponent')), + importedWrapper('imported_18g2v0c_component', import('./MyComponent')), + ]) +); +const AsyncComponent4 = imported( + async () => + (await Promise.all([ + importedWrapper('imported_-1qs8n90_component', import('./MyComponent1')), + importedWrapper('imported_9j5sqq_component', import('./MyComponent2')), + ]))[0] +); +export default AsyncComponent1; diff --git a/__tests__/__snapshots__/macro.spec.ts.snap b/__tests__/__snapshots__/macro.spec.ts.snap index 941992f9..bfebcac4 100644 --- a/__tests__/__snapshots__/macro.spec.ts.snap +++ b/__tests__/__snapshots__/macro.spec.ts.snap @@ -33,13 +33,7 @@ function _interopRequireWildcard(obj) { } } -var importedWrapper = function(marker, realImport) { - if (typeof __deoptimization_sideEffect__ !== \\"undefined\\") { - __deoptimization_sideEffect__(marker, realImport); - } - - return realImport; -}; +var importedWrapper = require(\\"react-imported-component/wrapper\\"); import { lazy } from \\"react-imported-component\\"; import { assignImportedComponents } from \\"react-imported-component/boot\\"; @@ -92,13 +86,7 @@ function _interopRequireWildcard(obj) { } } -var importedWrapper = function(marker, realImport) { - if (typeof __deoptimization_sideEffect__ !== \\"undefined\\") { - __deoptimization_sideEffect__(marker, realImport); - } - - return realImport; -}; +var importedWrapper = require(\\"react-imported-component/wrapper\\"); importedWrapper( \\"imported_-1ko6oiq_component\\", @@ -140,13 +128,7 @@ function _interopRequireWildcard(obj) { } } -var importedWrapper = function(marker, realImport) { - if (typeof __deoptimization_sideEffect__ !== \\"undefined\\") { - __deoptimization_sideEffect__(marker, realImport); - } - - return realImport; -}; +var importedWrapper = require(\\"react-imported-component/wrapper\\"); import { lazy } from \\"react-imported-component\\"; const v = lazy(() => @@ -192,13 +174,7 @@ function _interopRequireWildcard(obj) { } } -var importedWrapper = function(marker, realImport) { - if (typeof __deoptimization_sideEffect__ !== \\"undefined\\") { - __deoptimization_sideEffect__(marker, realImport); - } - - return realImport; -}; +var importedWrapper = require(\\"react-imported-component/wrapper\\"); import { imported, useImported } from \\"react-imported-component\\"; const v = imported(() => @@ -249,13 +225,7 @@ const x = () => useImported(() => import('./b')); ↓ ↓ ↓ ↓ ↓ ↓ -var importedWrapper = function(marker, realImport) { - if (typeof __deoptimization_sideEffect__ !== \\"undefined\\") { - __deoptimization_sideEffect__(marker, realImport); - } - - return realImport; -}; +var importedWrapper = require(\\"react-imported-component/wrapper\\"); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { @@ -282,13 +252,7 @@ function _interopRequireWildcard(obj) { } } -var importedWrapper = function(marker, realImport) { - if (typeof __deoptimization_sideEffect__ !== \\"undefined\\") { - __deoptimization_sideEffect__(marker, realImport); - } - - return realImport; -}; +var importedWrapper = require(\\"react-imported-component/wrapper\\"); import { imported, useImported } from \\"react-imported-component\\"; const v = imported(() => diff --git a/__tests__/loadable.spec.ts b/__tests__/loadable.spec.ts index c29ca564..10c4c551 100644 --- a/__tests__/loadable.spec.ts +++ b/__tests__/loadable.spec.ts @@ -1,4 +1,4 @@ -import {getFunctionSignature, getLoadable, importMatch} from "../src/loadable"; +import { getLoadable } from '../src/loadable'; describe('getLoadable', () => { const importedWrapper = (_: any, b: any) => b; @@ -7,61 +7,13 @@ describe('getLoadable', () => { const l1 = getLoadable(() => importedWrapper('imported_mark1_component', Promise.resolve(42))); const l2 = getLoadable(() => importedWrapper('imported_mark1_component', Promise.resolve(42))); - expect(l1).toEqual(l2) + expect(l1).toEqual(l2); }); it('cache test - no mark present', () => { const l1 = getLoadable(() => Promise.resolve(42)); const l2 = getLoadable(() => Promise.resolve(42)); - expect(l1).not.toEqual(l2) + expect(l1).not.toEqual(l2); }); }); - -describe('importMatch', () => { - it('standard', () => { - expect( - importMatch(getFunctionSignature(`() => importedWrapper('imported_mark1_component', Promise.resolve(TargetComponent)), true)`)) - ).toEqual(['mark1']); - }); - - it('webpack', () => { - expect( - importMatch(getFunctionSignature(`() => importedWrapper("imported_mark1_component", __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./components/Another */ "./app/components/Another.tsx")))`)) - ).toEqual(['mark1']); - }); - - it('webpack-prod', () => { - expect( - importMatch(getFunctionSignature(`() => importedWrapper('imported_mark1_component',__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./components/Another */ "./app/components/Another.tsx")))`)) - ).toEqual(['mark1']); - }); - - it('functional', () => { - expect(importMatch(getFunctionSignature(`"function loadable() { - return importedWrapper('imported_1ubbetg_component', __webpack_require__.e(/*! import() | namedChunk-1 */ "namedChunk-1").then(__webpack_require__.t.bind(null, /*! ./DeferredRender */ "./src/DeferredRender.js", 7))); - }"`))).toEqual(['1ubbetg']); - }); - - it('parcel', () => { - expect(importMatch(getFunctionSignature(`function _() { - return importedWrapper('imported_mark1_component', require("_bundle_loader")(require.resolve('./HelloWorld3'))); - }`))).toEqual(['mark1']); - }); - - it('ie11 uglify', () => { - expect(importMatch(getFunctionSignature(`function _() { - var t = 'imported_mark1_component'; - }`))).toEqual(['mark1']); - }); - - it('multiple imports in one line', () => { - expect(importMatch(getFunctionSignature(`function _() { - "imported_1pn9k36_component", blablabla- importedWrapper("imported_-1556gns_component") - }`))).toEqual(['1pn9k36', '-1556gns']); - }); - - it('maps function signatures', () => { - expect(getFunctionSignature(`import('file')`)).toEqual(getFunctionSignature(`import(/* */'file')`)) - }) -}); \ No newline at end of file diff --git a/__tests__/signatures.spec.ts b/__tests__/signatures.spec.ts new file mode 100644 index 00000000..c2d25de2 --- /dev/null +++ b/__tests__/signatures.spec.ts @@ -0,0 +1,92 @@ +import { getFunctionSignature, importMatch } from '../src/signatures'; + +describe('signatures', () => { + const a = (i: any) => i; + const b = (i: any) => i; + + it('extract markers from function', () => { + expect(importMatch(getFunctionSignature(() => a('imported_XXYY_component')))).toEqual(['XXYY']); + }); + + it('work similar for similar functions', () => { + expect(getFunctionSignature(() => a('imported_XXYY_component'))).toBe( + getFunctionSignature(() => b('imported_XXYY_component')) + ); + }); +}); + +describe('importMatch', () => { + it('standard', () => { + expect( + importMatch( + getFunctionSignature( + `() => importedWrapper('imported_mark1_component', Promise.resolve(TargetComponent)), true)` + ) + ) + ).toEqual(['mark1']); + }); + + it('webpack', () => { + expect( + importMatch( + getFunctionSignature( + `() => importedWrapper("imported_mark1_component", __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./components/Another */ "./app/components/Another.tsx")))` + ) + ) + ).toEqual(['mark1']); + }); + + it('webpack-prod', () => { + expect( + importMatch( + getFunctionSignature( + `() => importedWrapper('imported_mark1_component',__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./components/Another */ "./app/components/Another.tsx")))` + ) + ) + ).toEqual(['mark1']); + }); + + it('functional', () => { + expect( + importMatch( + getFunctionSignature(`"function loadable() { + return importedWrapper('imported_1ubbetg_component', __webpack_require__.e(/*! import() | namedChunk-1 */ "namedChunk-1").then(__webpack_require__.t.bind(null, /*! ./DeferredRender */ "./src/DeferredRender.js", 7))); + }"`) + ) + ).toEqual(['1ubbetg']); + }); + + it('parcel', () => { + expect( + importMatch( + getFunctionSignature(`function _() { + return importedWrapper('imported_mark1_component', require("_bundle_loader")(require.resolve('./HelloWorld3'))); + }`) + ) + ).toEqual(['mark1']); + }); + + it('ie11 uglify', () => { + expect( + importMatch( + getFunctionSignature(`function _() { + var t = 'imported_mark1_component'; + }`) + ) + ).toEqual(['mark1']); + }); + + it('multiple imports in one line', () => { + expect( + importMatch( + getFunctionSignature(`function _() { + "imported_1pn9k36_component", blablabla- importedWrapper("imported_-1556gns_component") + }`) + ) + ).toEqual(['1pn9k36', '-1556gns']); + }); + + it('maps function signatures', () => { + expect(getFunctionSignature(`import('file')`)).toEqual(getFunctionSignature(`import(/* */'file')`)); + }); +}); diff --git a/package.json b/package.json index d5c0edbb..460f05da 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,8 @@ "boot", "server", "macro", - "babel.js" + "babel.js", + "wrapper.js" ], "husky": { "hooks": { diff --git a/src/babel.ts b/src/babel.ts index d9434969..9988c2aa 100644 --- a/src/babel.ts +++ b/src/babel.ts @@ -1,6 +1,6 @@ -import {resolve, relative, dirname} from 'path'; // @ts-ignore -import * as crc32 from "crc-32"; +import * as crc32 from 'crc-32'; +import { dirname, relative, resolve } from 'path'; export const encipherImport = (str: string) => { return crc32.str(str).toString(32); @@ -14,7 +14,9 @@ try { try { syntax = require('@babel/plugin-syntax-dynamic-import'); } catch (e) { - throw new Error('react-imported-component babel plugin is requiring `babel-plugin-syntax-dynamic-import` or `@babel/plugin-syntax-dynamic-import` to work. Please add this dependency.') + throw new Error( + 'react-imported-component babel plugin is requiring `babel-plugin-syntax-dynamic-import` or `@babel/plugin-syntax-dynamic-import` to work. Please add this dependency.' + ); } } syntax = syntax.default || syntax; @@ -30,19 +32,14 @@ const templateOptions = { placeholderPattern: /^([A-Z0-9]+)([A-Z0-9_]+)$/, }; -export const createTransformer = ({types: t, template}: any, excludeMacro = false) => { - const headerTemplate = template(`var importedWrapper = function(marker, realImport) { - if (typeof __deoptimization_sideEffect__ !== 'undefined') { - __deoptimization_sideEffect__(marker, realImport); - } - return realImport; - }`, templateOptions); - - const importRegistration = template( - 'importedWrapper(MARK, IMPORT)', - templateOptions, +export const createTransformer = ({ types: t, template }: any, excludeMacro = false) => { + const headerTemplate = template( + `var importedWrapper = require('react-imported-component/wrapper');`, + templateOptions ); + const importRegistration = template('importedWrapper(MARK, IMPORT)', templateOptions); + const hasImports = new Set(); const visitedNodes = new Map(); @@ -50,23 +47,22 @@ export const createTransformer = ({types: t, template}: any, excludeMacro = fals traverse(programPath: any, fileName: string) { programPath.traverse({ ImportDeclaration(path: any) { - if(excludeMacro){ + if (excludeMacro) { return; } const source = path.node.source.value; if (source === 'react-imported-component/macro') { - const {specifiers} = path.node; + const { specifiers } = path.node; path.remove(); const assignName = 'assignImportedComponents'; if (specifiers.length === 1 && specifiers[0].imported.name === assignName) { programPath.node.body.unshift( - t.importDeclaration([ - t.importSpecifier(t.identifier(assignName), t.identifier(assignName)) - ], + t.importDeclaration( + [t.importSpecifier(t.identifier(assignName), t.identifier(assignName))], t.stringLiteral('react-imported-component/boot') ) - ) + ); } else { programPath.node.body.unshift( t.importDeclaration( @@ -75,11 +71,11 @@ export const createTransformer = ({types: t, template}: any, excludeMacro = fals ), t.stringLiteral('react-imported-component') ) - ) + ); } } }, - Import({parentPath}: any) { + Import({ parentPath }: any) { if (visitedNodes.has(parentPath.node)) { return; } @@ -96,27 +92,27 @@ export const createTransformer = ({types: t, template}: any, excludeMacro = fals replace = importRegistration({ MARK: t.stringLiteral(`imported_${requiredFileHash}_component`), - IMPORT: newImport + IMPORT: newImport, }); hasImports.add(fileName); visitedNodes.set(newImport, true); parentPath.replaceWith(replace); - } + }, }); }, finish(node: any, filename: string) { - if (!hasImports.has(filename)) return; + if (!hasImports.has(filename)) { return; } node.body.unshift(headerTemplate()); }, hasImports, - } + }; }; -export default function (babel: any) { +export default function(babel: any) { const transformer = createTransformer(babel); return { @@ -124,14 +120,14 @@ export default function (babel: any) { visitor: { Program: { - enter(programPath: any, {file}: any) { + enter(programPath: any, { file }: any) { transformer.traverse(programPath, file.opts.filename); }, - exit({node}: any, {file}: any) { + exit({ node }: any, { file }: any) { transformer.finish(node, file.opts.filename); - } + }, }, - } - } + }, + }; } diff --git a/src/loadable.ts b/src/loadable.ts index 02a21a52..29852bfb 100644 --- a/src/loadable.ts +++ b/src/loadable.ts @@ -2,9 +2,8 @@ import { settings } from './config'; import { isBackend } from './detectBackend'; import { assingLoadableMark } from './marks'; import { getPreloaders } from './preloaders'; -import { DefaultImport, Loadable, Mark, MarkMeta, Promised } from './types'; - -type AnyFunction = (x: any) => any; +import { getFunctionSignature, importMatch } from './signatures'; +import { AnyFunction, DefaultImport, Loadable, Mark, MarkMeta, Promised } from './types'; export interface InnerLoadable extends Loadable { ok: boolean; @@ -19,17 +18,6 @@ const LOADABLE_SIGNATURE = new Map>(); const addPending = (promise: Promise) => pending.push(promise); const removeFromPending = (promise: Promise) => (pending = pending.filter(a => a !== promise)); -const trimImport = (str: string) => str.replace(/['"]/g, ''); - -export const importMatch = (functionString: string): Mark => { - const markMatches = functionString.match(/`imported_(.*?)_component`/g) || []; - return markMatches.map(match => match && trimImport((match.match(/`imported_(.*?)_component`/i) || [])[1])); -}; - -export const getFunctionSignature = (fn: AnyFunction | string) => - String(fn) - .replace(/(["'])/g, '`') - .replace(/\/\*([^\*]*)\*\//gi, ''); export function toLoadable(firstImportFunction: Promised, autoImport = true): Loadable { let importFunction = firstImportFunction; @@ -149,12 +137,14 @@ export function toLoadable(firstImportFunction: Promised, autoImport = tru LOADABLE_SIGNATURE.set(functionSignature, loadable); assingLoadableMark(mark, loadable); } else { - // tslint:disable-next-line:no-console - console.warn( - 'react-imported-component: no mark found at', - importFunction, - 'Please check babel plugin or macro setup, as well as imported-component\'s limitations. See https://github.com/theKashey/react-imported-component/issues/147' - ); + if (process.env.NODE_ENV !== 'development') { + // tslint:disable-next-line:no-console + console.warn( + 'react-imported-component: no mark found at', + importFunction, + 'Please check babel plugin or macro setup, as well as imported-component\'s limitations. See https://github.com/theKashey/react-imported-component/issues/147' + ); + } } // trigger preload on the server side diff --git a/src/signatures.ts b/src/signatures.ts new file mode 100644 index 00000000..a1cf441f --- /dev/null +++ b/src/signatures.ts @@ -0,0 +1,14 @@ +import { AnyFunction, Mark } from './types'; + +const trimImport = (str: string) => str.replace(/['"]/g, ''); + +export const importMatch = (functionString: string): Mark => { + const markMatches = functionString.match(/`imported_(.*?)_component`/g) || []; + return markMatches.map(match => match && trimImport((match.match(/`imported_(.*?)_component`/i) || [])[1])); +}; + +export const getFunctionSignature = (fn: AnyFunction | string) => + String(fn) + .replace(/(["'])/g, '`') + .replace(/\/\*([^\*]*)\*\//gi, '') + .replace(/([A-z0-9_]+)\(`imported_/g, '$(`imported_'); diff --git a/src/types.ts b/src/types.ts index 86f74b19..fdfb181d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,27 +1,28 @@ -import {ComponentType, ForwardRefExoticComponent, Ref, ReactElement, ReactNode} from "react"; +import { ComponentType, ForwardRefExoticComponent, ReactElement, ReactNode, Ref } from 'react'; export interface DefaultImportedComponent

{ default: ComponentType

; } +export type AnyFunction = (x: any) => any; + export interface Default { default: T; } -export type Stream = { - marks: Record -}; +export interface Stream { + marks: Record; +} export type Mark = string[]; export type Promised = () => Promise; export type DefaultComponent

= ComponentType

| DefaultImportedComponent

; -export type DefaultComponentImport = () => Promise> - +export type DefaultComponentImport = () => Promise>; export type Defaultable

= P | Default

; -export type DefaultImport = () => Promise> +export type DefaultImport = () => Promise>; export interface MarkMeta { loadable: Loadable; @@ -30,9 +31,9 @@ export interface MarkMeta { fileName: string; } -export type LazyImport = () => Promise> +export type LazyImport = () => Promise>; -export type LoadableComponentState = { +export interface LoadableComponentState { loading?: boolean; error?: any; } @@ -61,14 +62,13 @@ export interface Loadable { then(callback: (x: T) => void, err: () => void): Promise; } +export interface ComponentOptions { + loadable: DefaultComponentImport

| Loadable>; -export type ComponentOptions = { - loadable: DefaultComponentImport

| Loadable>, + LoadingComponent?: ComponentType; + ErrorComponent?: ComponentType; - LoadingComponent?: ComponentType, - ErrorComponent?: ComponentType, - - onError?: (a: any) => void, + onError?: (a: any) => void; async?: boolean; @@ -78,42 +78,35 @@ export type ComponentOptions = { forwardProps?: K; } -export type HOCOptions = { +export interface HOCOptions { noAutoImport?: boolean; } -export type AdditionalHOC = { - preload(): Promise; +export interface AdditionalHOC { done: Promise; + preload(): Promise; } -export type HOCType = - ForwardRefExoticComponent> }> & +export type HOCType = ForwardRefExoticComponent> }> & AdditionalHOC; export interface ImportModuleHOCProps { - fallback: NonNullable|null; + fallback: NonNullable | null; } export interface ImportModuleProps extends ImportModuleHOCProps { children: (arg: T) => ReactElement | null; } - export interface FullImportModuleProps extends ImportModuleProps { import: DefaultImport | Loadable; } export type ModuleFC = (props: ImportModuleProps) => ReactElement | null; -export type HOCModuleType = - ModuleFC & - AdditionalHOC; +export type HOCModuleType = ModuleFC & AdditionalHOC; - -export interface HOC { - (loader: DefaultComponentImport

, options?: Partial> & HOCOptions): HOCType; -} +export type HOC = (loader: DefaultComponentImport

, options?: Partial> & HOCOptions) => HOCType; export interface ImportedComponents { [index: number]: () => Promise>; diff --git a/wrapper.js b/wrapper.js new file mode 100644 index 00000000..03a0ed6a --- /dev/null +++ b/wrapper.js @@ -0,0 +1,8 @@ +var importedWrapper = function(marker, realImport) { + if (typeof __deoptimization_sideEffect__ !== 'undefined') { + __deoptimization_sideEffect__(marker, realImport); + } + return realImport; +}; + +module.exports = importedWrapper; From 08bb67f0780a7e3891afcb34309019593ee42aff Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Sun, 29 Dec 2019 17:20:51 +1100 Subject: [PATCH 02/10] 6.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 460f05da..79bc4815 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-imported-component", - "version": "6.2.1", + "version": "6.2.2", "description": "I will import your component, and help to handle it", "main": "dist/es5/index.js", "jsnext:main": "dist/es2015/index.js", From be277ffd959437168999bf59a0d95a05a9ac7e6e Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 30 Dec 2019 19:29:06 +1100 Subject: [PATCH 03/10] update snapshots From 5a4bb6d0c77188e08bf3db3f808161c1a44a1182 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 30 Dec 2019 19:29:34 +1100 Subject: [PATCH 04/10] suppress secondary update if loadable is already done --- src/useImported.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/useImported.ts b/src/useImported.ts index 3b553bdb..57f1f883 100644 --- a/src/useImported.ts +++ b/src/useImported.ts @@ -47,7 +47,10 @@ export function useLoadable(loadable: Loadable, options: HookOptions = {}) if (options.track !== false) { useMark(UID, loadable.mark); } - loadLoadable(loadable, forceUpdate); + // on the clientside it might be already loaded + if (!loadable.done) { + loadLoadable(loadable, forceUpdate); + } } }, [loadable, options.import]); From 89865644e1a7ca9853270cd5719f0986bf43dcba Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 30 Dec 2019 19:29:41 +1100 Subject: [PATCH 05/10] 6.2.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79bc4815..884085e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-imported-component", - "version": "6.2.2", + "version": "6.2.3", "description": "I will import your component, and help to handle it", "main": "dist/es5/index.js", "jsnext:main": "dist/es2015/index.js", From 8002355ab6658fd68c3b988f5f23cbe6aea9a158 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Thu, 2 Jan 2020 15:48:51 +1100 Subject: [PATCH 06/10] more correct usage of loading reaction --- src/useImported.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/useImported.ts b/src/useImported.ts index 57f1f883..f2ea6413 100644 --- a/src/useImported.ts +++ b/src/useImported.ts @@ -42,13 +42,15 @@ export function useLoadable(loadable: Loadable, options: HookOptions = {}) return {}; }); + const wasDone = loadable.done; + useEffect(() => { if (options.import !== false) { if (options.track !== false) { useMark(UID, loadable.mark); } // on the clientside it might be already loaded - if (!loadable.done) { + if (!wasDone) { loadLoadable(loadable, forceUpdate); } } From 97130f1d3db2066e927172d2ece2d115ebeb40ce Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Thu, 2 Jan 2020 15:56:01 +1100 Subject: [PATCH 07/10] disable prettier for tests --- .prettierignore | 1 + .size.json | 4 +- __tests__/__fixtures__/babel/node/expected.js | 74 ++----------------- .../__fixtures__/babel/webpack/expected.js | 39 +++------- 4 files changed, 20 insertions(+), 98 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..480698db --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +__tests__/__fixtures__ \ No newline at end of file diff --git a/.size.json b/.size.json index b80cfc9a..7252f13d 100644 --- a/.size.json +++ b/.size.json @@ -2,12 +2,12 @@ { "name": "dist/es2015/index.js, dist/es2015/boot.js", "passed": true, - "size": 3426 + "size": 3428 }, { "name": "dist/es2015/index.js", "passed": true, - "size": 3130 + "size": 3133 }, { "name": "dist/es2015/boot.js", diff --git a/__tests__/__fixtures__/babel/node/expected.js b/__tests__/__fixtures__/babel/node/expected.js index e1c97baa..8aed23d8 100644 --- a/__tests__/__fixtures__/babel/node/expected.js +++ b/__tests__/__fixtures__/babel/node/expected.js @@ -1,71 +1,11 @@ var importedWrapper = require('react-imported-component/wrapper'); -function _interopRequireWildcard(obj) { - if (obj && obj.__esModule) { - return obj; - } else { - var newObj = {}; - if (obj != null) { - for (var key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - var desc = - Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; - if (desc.get || desc.set) { - Object.defineProperty(newObj, key, desc); - } else { - newObj[key] = obj[key]; - } - } - } - } - newObj.default = obj; - return newObj; - } -} +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } import imported from 'react-imported-component'; -const AsyncComponent0 = imported(() => - importedWrapper( - 'imported_18g2v0c_component', - Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))) - ) -); -const AsyncComponent1 = imported(() => - importedWrapper( - 'imported_18g2v0c_component', - Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))) - ) -); -const AsyncComponent2 = imported( - async () => - await importedWrapper( - 'imported_18g2v0c_component', - Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))) - ) -); -const AsyncComponent3 = imported(() => - Promise.all([ - importedWrapper( - 'imported_18g2v0c_component', - Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))) - ), - importedWrapper( - 'imported_18g2v0c_component', - Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))) - ), - ]) -); -const AsyncComponent4 = imported( - async () => - (await Promise.all([ - importedWrapper( - 'imported_-1qs8n90_component', - Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent1'))) - ), - importedWrapper( - 'imported_9j5sqq_component', - Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent2'))) - ), - ]))[0] -); -export default AsyncComponent1; +const AsyncComponent0 = imported(() => importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))); +const AsyncComponent1 = imported(() => importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))); +const AsyncComponent2 = imported(async () => await importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))); +const AsyncComponent3 = imported(() => Promise.all([importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent')))), importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))])); +const AsyncComponent4 = imported(async () => (await Promise.all([importedWrapper("imported_-1qs8n90_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent1')))), importedWrapper("imported_9j5sqq_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent2'))))]))[0]); +export default AsyncComponent1; \ No newline at end of file diff --git a/__tests__/__fixtures__/babel/webpack/expected.js b/__tests__/__fixtures__/babel/webpack/expected.js index 22d57581..47e20677 100644 --- a/__tests__/__fixtures__/babel/webpack/expected.js +++ b/__tests__/__fixtures__/babel/webpack/expected.js @@ -1,32 +1,13 @@ var importedWrapper = require('react-imported-component/wrapper'); -import { lazy, useImported } from 'react-imported-component'; -import { assignImportedComponents } from 'react-imported-component/boot'; +import { lazy, useImported } from "react-imported-component"; +import { assignImportedComponents } from "react-imported-component/boot"; import imported from 'react-imported-component'; -const AsyncComponent0 = imported(() => - importedWrapper( - 'imported_18g2v0c_component', - import( - /* webpackChunkName:namedChunk */ - './MyComponent' - ) - ) -); -const AsyncComponent1 = imported(() => importedWrapper('imported_18g2v0c_component', import('./MyComponent'))); -const AsyncComponent2 = imported( - async () => await importedWrapper('imported_18g2v0c_component', import('./MyComponent')) -); -const AsyncComponent3 = imported(() => - Promise.all([ - importedWrapper('imported_18g2v0c_component', import('./MyComponent')), - importedWrapper('imported_18g2v0c_component', import('./MyComponent')), - ]) -); -const AsyncComponent4 = imported( - async () => - (await Promise.all([ - importedWrapper('imported_-1qs8n90_component', import('./MyComponent1')), - importedWrapper('imported_9j5sqq_component', import('./MyComponent2')), - ]))[0] -); -export default AsyncComponent1; +const AsyncComponent0 = imported(() => importedWrapper("imported_18g2v0c_component", import( +/* webpackChunkName:namedChunk */ +'./MyComponent'))); +const AsyncComponent1 = imported(() => importedWrapper("imported_18g2v0c_component", import('./MyComponent'))); +const AsyncComponent2 = imported(async () => await importedWrapper("imported_18g2v0c_component", import('./MyComponent'))); +const AsyncComponent3 = imported(() => Promise.all([importedWrapper("imported_18g2v0c_component", import('./MyComponent')), importedWrapper("imported_18g2v0c_component", import('./MyComponent'))])); +const AsyncComponent4 = imported(async () => (await Promise.all([importedWrapper("imported_-1qs8n90_component", import('./MyComponent1')), importedWrapper("imported_9j5sqq_component", import('./MyComponent2'))]))[0]); +export default AsyncComponent1; \ No newline at end of file From 34f3958adfedaa1d5067e8726537f97abc9e832c Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Thu, 2 Jan 2020 16:11:51 +1100 Subject: [PATCH 08/10] add cached test --- __tests__/useImported.spec.tsx | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/__tests__/useImported.spec.tsx b/__tests__/useImported.spec.tsx index 92441902..e3d8c688 100644 --- a/__tests__/useImported.spec.tsx +++ b/__tests__/useImported.spec.tsx @@ -155,4 +155,32 @@ describe('useImported', () => { expect(wrapper.update().html()).toContain('loaded!'); expect(drainHydrateMarks()).toEqual(['conditional-mark']); }); + + it('cached import', async () => { + // this test is not working as it should (it should be broken) + const importer = () => () => loaded!; + + const Comp = () => { + const { loading, imported: Component } = useImported(importer as any); + + if (Component) { + return ; + } + + if (loading) { + return loading; + } + return nothing; + }; + + const wrapper = mount(); + expect(wrapper.html()).toContain('loading'); + expect(wrapper.update().html()).toContain('loading'); + + await act(async () => { + await done(); + }); + + expect(wrapper.update().html()).toContain('loaded!'); + }); }); From 7c4e0a06c3007349a193a0858794a7d93f6c9ba1 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Thu, 2 Jan 2020 16:13:31 +1100 Subject: [PATCH 09/10] 6.2.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 884085e5..f01b1574 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-imported-component", - "version": "6.2.3", + "version": "6.2.4", "description": "I will import your component, and help to handle it", "main": "dist/es5/index.js", "jsnext:main": "dist/es2015/index.js", From 985327afb910d9924a791823b1b55f075e22cb06 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Fri, 10 Jan 2020 08:21:24 +1100 Subject: [PATCH 10/10] refactor imported useEffect to useMemo --- __tests__/useImported.spec.tsx | 2 +- src/config.ts | 6 +++--- src/useImported.ts | 23 +++++++---------------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/__tests__/useImported.spec.tsx b/__tests__/useImported.spec.tsx index e3d8c688..10b2eefa 100644 --- a/__tests__/useImported.spec.tsx +++ b/__tests__/useImported.spec.tsx @@ -146,7 +146,7 @@ describe('useImported', () => { expect(wrapper.update().html()).toContain('nothing'); wrapper.setProps({ loadit: true }); - expect(wrapper.update().html()).toContain('nothing'); + expect(wrapper.update().html()).toContain('loading'); await act(async () => { await done(); diff --git a/src/config.ts b/src/config.ts index 8e88d45c..c9b2e9e6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,4 @@ -import {isBackend} from "./detectBackend"; +import { isBackend } from './detectBackend'; const rejectNetwork = (url: string) => url.indexOf('http') !== 0; @@ -7,9 +7,9 @@ export const settings = { SSR: isBackend, rethrowErrors: process.env.NODE_ENV !== 'production', fileFilter: rejectNetwork, - updateOnReload: false, + updateOnReload: true, }; export const setConfiguration = (config: Partial) => { Object.assign(settings, config); -}; \ No newline at end of file +}; diff --git a/src/useImported.ts b/src/useImported.ts index f2ea6413..87ca0271 100644 --- a/src/useImported.ts +++ b/src/useImported.ts @@ -1,4 +1,4 @@ -import { ComponentType, lazy, LazyExoticComponent, useCallback, useContext, useEffect, useState } from 'react'; +import { ComponentType, lazy, LazyExoticComponent, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { settings } from './config'; import { streamContext } from './context'; import { isBackend } from './detectBackend'; @@ -30,31 +30,22 @@ interface HookOptions { export function useLoadable(loadable: Loadable, options: HookOptions = {}) { const UID = useContext(streamContext); - const [, forceUpdate] = useState(() => { - // use mark - if (options.import !== false) { - if (options.track !== false) { - useMark(UID, loadable.mark); - } - loadable.loadIfNeeded(); - } - - return {}; - }); const wasDone = loadable.done; - useEffect(() => { + const [, forceUpdate] = useState({}); + + useMemo(() => { if (options.import !== false) { if (options.track !== false) { useMark(UID, loadable.mark); } - // on the clientside it might be already loaded if (!wasDone) { loadLoadable(loadable, forceUpdate); } } - }, [loadable, options.import]); + return true; + }, [loadable, options.import, options.track]); if (isBackend && !isItReady() && loadable.isLoading()) { /* tslint:disable:next-line no-console */ @@ -107,7 +98,7 @@ export function useImported( if (settings.updateOnReload) { (topLoadable as InnerLoadable)._probeChanges().then(changed => changed && update({})); } - }, []); + }, ['hot']); } if (loadable.error) {