From cc59f8f3306e4de261301a09697f94c213a45065 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 27 Jul 2020 16:29:35 +1000 Subject: [PATCH 1/9] refine babel/webpack integration --- .size.json | 6 +- .../__fixtures__/babel/webpack/actual.js | 2 +- .../__fixtures__/babel/webpack/expected.js | 2 +- __tests__/signatures.spec.ts | 18 +- __tests__/utils.spec.ts | 2 +- src/babel/babel.ts | 177 ++++++++++++++ src/babel/magic-comments.ts | 55 +++++ src/babel/utils.ts | 23 ++ src/config.ts | 39 ++- src/configuration/configuration.ts | 16 +- src/entrypoints/babel.ts | 224 +----------------- src/entrypoints/macro.ts | 2 +- src/loadable/loadable.ts | 8 +- src/scanners/scanForImports.ts | 2 +- src/utils/signatures.ts | 3 +- 15 files changed, 338 insertions(+), 241 deletions(-) create mode 100644 src/babel/babel.ts create mode 100644 src/babel/magic-comments.ts create mode 100644 src/babel/utils.ts diff --git a/.size.json b/.size.json index 1b9fd00b..d0c8db75 100644 --- a/.size.json +++ b/.size.json @@ -2,16 +2,16 @@ { "name": "dist/es2015/entrypoints/index.js, dist/es2015/entrypoints/boot.js", "passed": true, - "size": 3601 + "size": 3646 }, { "name": "dist/es2015/entrypoints/index.js", "passed": true, - "size": 3316 + "size": 3354 }, { "name": "dist/es2015/entrypoints/boot.js", "passed": true, - "size": 1775 + "size": 1803 } ] diff --git a/__tests__/__fixtures__/babel/webpack/actual.js b/__tests__/__fixtures__/babel/webpack/actual.js index ae7d6405..074e173d 100644 --- a/__tests__/__fixtures__/babel/webpack/actual.js +++ b/__tests__/__fixtures__/babel/webpack/actual.js @@ -3,7 +3,7 @@ import imported from 'react-imported-component'; const PreloadComponent = imported(() => import('./PreloadThis')); const PrefetchChunkComponent = imported(() => import('./ChunkThis')); -const AsyncComponent0 = imported(() => import(/* webpackChunkName:namedChunk */'./MyComponent')); +const AsyncComponent0 = imported(() => import(/* webpackChunkName: "namedChunk" */'./MyComponent')); const AsyncComponent1 = imported(() => import('./MyComponent')); diff --git a/__tests__/__fixtures__/babel/webpack/expected.js b/__tests__/__fixtures__/babel/webpack/expected.js index d35f0ded..228980a2 100644 --- a/__tests__/__fixtures__/babel/webpack/expected.js +++ b/__tests__/__fixtures__/babel/webpack/expected.js @@ -11,7 +11,7 @@ const PrefetchChunkComponent = imported(() => importedWrapper("imported_fspdct_c /* webpackPrefetch: true */ './ChunkThis'))); const AsyncComponent0 = imported(() => importedWrapper("imported_18g2v0c_component", import( -/* webpackChunkName:namedChunk */ +/* webpackChunkName: "namedChunk" */ './MyComponent'))); const AsyncComponent1 = imported(() => importedWrapper("imported_18g2v0c_component", import('./MyComponent'))); const AsyncComponent2 = imported(async () => await importedWrapper("imported_18g2v0c_component", import('./MyComponent'))); diff --git a/__tests__/signatures.spec.ts b/__tests__/signatures.spec.ts index 6c8332fc..360cdb1f 100644 --- a/__tests__/signatures.spec.ts +++ b/__tests__/signatures.spec.ts @@ -97,7 +97,21 @@ describe('importMatch', () => { getFunctionSignature('()=>$(`imported_-f5674t_component`,x.e(3).then(x.bind(null,`xxx`,7)))') ); expect(getFunctionSignature('()=>$(`imported_-f5674t_component`,n.e(3).then(n.bind(null,`xxx`,7)))')).toEqual( - '()=>$(`imported_-f5674t_component`,-we(3).-wbind(null,`xxx`,7)))' + '()=>$(`imported_-f5674t_component`,-we().-wbind(null,`xxx`,7)))' + ); + }); + + it('maps internal and external signatures', () => { + // internal is with Promise.resolve + // extenal is with webpack_require.e + expect( + getFunctionSignature( + '() => importedWrapper("imported_-1135avo_component", __webpack_require__.e(/*! import() */ 12).then(__webpack_require__.bind(null, /*! universal/components/SERP */ "./universal/components/SERP/index.js")))' + ) + ).toBe( + getFunctionSignature( + '() => importedWrapper("imported_-1135avo_component", Promise.resolve(/*! import() */).then(__webpack_require__.bind(null, /*! universal/components/SERP */ "./universal/components/SERP/index.js")))' + ) ); }); @@ -106,7 +120,7 @@ describe('importMatch', () => { getFunctionSignature('()=>s("imported_-is59m_component",n.e(41).then(n.bind(null,"./Promo.jsx")))') ); expect(getFunctionSignature('()=>P("imported_-is59m_component",t.e(41).then(t.bind(null,"./Promo.jsx")))')).toEqual( - '()=>$(`imported_-is59m_component`,-we(41).-wbind(null,`./Promo.jsx`)))' + '()=>$(`imported_-is59m_component`,-we().-wbind(null,`./Promo.jsx`)))' ); }); }); diff --git a/__tests__/utils.spec.ts b/__tests__/utils.spec.ts index eb87381a..a441b034 100644 --- a/__tests__/utils.spec.ts +++ b/__tests__/utils.spec.ts @@ -106,7 +106,7 @@ describe('scanForImports', () => { (a, b) => a + b, imports, imported => imported.indexOf('c.js') < 0, - (imported, _, givenChunkName) => (imported.indexOf('a.js') > 0 ? `test-${givenChunkName}-test` : 'bundle-b') + (imported, _, options) => (imported.indexOf('a.js') > 0 ? `test-${options.chunkName}-test` : 'bundle-b') ); expect(Object.values(imports)).toEqual([ `[() => import(/* webpackChunkName: \"chunk-a\" */'${rel}/a.js'), 'test-chunk-a-test', '${rel}/a.js', false] /* from .a */`, diff --git a/src/babel/babel.ts b/src/babel/babel.ts new file mode 100644 index 00000000..51cd080a --- /dev/null +++ b/src/babel/babel.ts @@ -0,0 +1,177 @@ +// @ts-ignore +import * as crc32 from 'crc-32'; +import { existsSync } from 'fs'; +import { dirname, join, relative, resolve } from 'path'; + +import { ImportedConfiguration } from '../configuration/configuration'; +import { processComment } from './magic-comments'; + +export const encipherImport = (str: string) => { + return crc32.str(str).toString(32); +}; + +// Babel v7 compat +let syntax: any; +try { + syntax = require('babel-plugin-syntax-dynamic-import'); +} catch (err) { + 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.' + ); + } +} +syntax = syntax.default || syntax; + +const resolveImport = (importName: string, file = '') => { + if (importName.charAt(0) === '.') { + return relative(process.cwd(), resolve(dirname(file), importName)); + } + return importName; +}; + +const templateOptions = { + placeholderPattern: /^([A-Z0-9]+)([A-Z0-9_]+)$/, +}; + +function getImportArg(callPath: any) { + return callPath.get('arguments.0'); +} + +function getComments(callPath: any) { + return callPath.has('leadingComments') ? callPath.get('leadingComments') : []; +} + +// load configuration +const configurationFile = join(process.cwd(), '.imported.js'); +const defaultConfiguration: ImportedConfiguration = (existsSync(configurationFile) + ? require(configurationFile) + : {}) as ImportedConfiguration; + +export const createTransformer = ( + { types: t, template }: any, + excludeMacro = false, + configuration = defaultConfiguration +) => { + 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(); + + return { + traverse(programPath: any, fileName: string) { + let isBootstrapFile = false; + + programPath.traverse({ + ImportDeclaration(path: any) { + if (excludeMacro) { + return; + } + + const source = path.node.source.value; + if (source === 'react-imported-component/macro') { + const { specifiers } = path.node; + path.remove(); + const assignName = 'assignImportedComponents'; + if (specifiers.length === 1 && specifiers[0].imported.name === assignName) { + isBootstrapFile = true; + programPath.node.body.unshift( + t.importDeclaration( + [t.importSpecifier(t.identifier(assignName), t.identifier(assignName))], + t.stringLiteral('react-imported-component/boot') + ) + ); + } else { + programPath.node.body.unshift( + t.importDeclaration( + specifiers.map((spec: any) => + t.importSpecifier(t.identifier(spec.imported.name), t.identifier(spec.imported.name)) + ), + t.stringLiteral('react-imported-component') + ) + ); + } + } + }, + Import({ parentPath }: any) { + if (visitedNodes.has(parentPath.node)) { + return; + } + + const newImport = parentPath.node; + const rawImport = getImportArg(parentPath); + const importName = rawImport.node.value; + const rawComments = getComments(rawImport); + const comments = rawComments.map((parent: any) => parent.node.value); + + const newComments = processComment(configuration, comments, importName, fileName, { + isBootstrapFile, + }); + + if (newComments !== comments) { + rawComments.forEach((comment: any) => comment.remove()); + newComments.forEach((comment: string) => { + rawImport.addComment('leading', ` ${comment} `); + }); + } + + if (!importName) { + return; + } + const requiredFileHash = encipherImport(resolveImport(importName, fileName)); + + let replace = null; + + replace = importRegistration({ + MARK: t.stringLiteral(`imported_${requiredFileHash}_component`), + IMPORT: newImport, + }); + + hasImports.add(fileName); + visitedNodes.set(newImport, true); + + parentPath.replaceWith(replace); + }, + }); + }, + + finish(node: any, filename: string) { + if (!hasImports.has(filename)) { + return; + } + node.body.unshift(headerTemplate()); + }, + + hasImports, + }; +}; + +export const babelPlugin = (babel: any, options: ImportedConfiguration = {}) => { + const transformer = createTransformer(babel, false, { + ...defaultConfiguration, + ...options, + }); + + return { + inherits: syntax, + + visitor: { + Program: { + enter(programPath: any, { file }: any) { + transformer.traverse(programPath, file.opts.filename); + }, + + exit({ node }: any, { file }: any) { + transformer.finish(node, file.opts.filename); + }, + }, + }, + }; +}; diff --git a/src/babel/magic-comments.ts b/src/babel/magic-comments.ts new file mode 100644 index 00000000..2995f586 --- /dev/null +++ b/src/babel/magic-comments.ts @@ -0,0 +1,55 @@ +import { ImportedConfiguration, KnownImportOptions } from '../configuration/configuration'; +import { commentsToConfiguration } from './utils'; + +const preservePrefetch = (_: any, __: any, options: KnownImportOptions) => !!options.webpackPrefetch; +const preservePreload = (_: any, __: any, options: KnownImportOptions) => !!options.webpackPreload; +const preserveChunkName = (_: any, __: any, options: KnownImportOptions) => + options.webpackChunkName || options.chunkName; + +const chunkComment = (chunk: string) => `webpackChunkName: "${chunk}"`; +const preloadComment = () => `webpackPreload: true`; +const prefetchComment = () => `webpackPrefetch: true`; + +const knownMagics = ['webpackChunkName', 'webpackPrefetch', 'webpackPreload']; + +const toComments = (conf: T): string[] => + (Object.keys(conf) as Array) + .filter(key => !knownMagics.includes(key as any)) + .reduce((acc, key) => [...acc, `${key}:${JSON.stringify(conf[key])}`], [] as string[]); + +const nullish = (a: T, b: T): T => { + if (a === undefined) { + return b; + } + return a; +}; + +export const processComment = ( + configuration: ImportedConfiguration, + comments: string[], + importName: string, + fileName: string, + options: { + isBootstrapFile: boolean; + } +): string[] => { + const { + shouldPrefetch = preservePrefetch, + shouldPreload = preservePreload, + chunkName = preserveChunkName, + } = configuration; + + const importConfiguration = commentsToConfiguration(comments); + + const newChunkName = nullish( + chunkName(importName, fileName, importConfiguration), + preserveChunkName(importName, fileName, importConfiguration) + ); + const { isBootstrapFile } = options; + return [ + ...toComments(importConfiguration), + !isBootstrapFile && shouldPrefetch(importName, fileName, importConfiguration) ? prefetchComment() : '', + !isBootstrapFile && shouldPreload(importName, fileName, importConfiguration) ? preloadComment() : '', + newChunkName ? chunkComment(newChunkName) : '', + ].filter(x => !!x); +}; diff --git a/src/babel/utils.ts b/src/babel/utils.ts new file mode 100644 index 00000000..4cdad61b --- /dev/null +++ b/src/babel/utils.ts @@ -0,0 +1,23 @@ +import vm from 'vm'; +import { CLIENT_SIDE_ONLY } from '../configuration/constants'; + +const parseMagicComments = (str: string): object => { + if (str.trim() === CLIENT_SIDE_ONLY) { + return {}; + } + try { + const values = vm.runInNewContext(`(function(){return {${str}};})()`); + return values; + } catch (e) { + return {}; + } +}; + +export const commentsToConfiguration = (comments: any[]) => + comments.reduce( + (acc, comment) => ({ + ...acc, + ...parseMagicComments(comment), + }), + {} as any + ); diff --git a/src/config.ts b/src/config.ts index b1ac9fc9..8e9e0927 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,17 +2,52 @@ import { isBackend } from './utils/detectBackend'; const rejectNetwork = (url: string) => url.indexOf('http') !== 0; -export const settings = { +/** + * client-side only imported settings + */ +interface ImportedClientSettings { + /** + * enabled hot module replacement + * @autoconfig enabled if HMR is detected + */ + hot: boolean; + /** + * Sets SSR mode + * @autoconfig autodetects environment + */ + SSR: boolean; + /** + * rethrows errors from loading + * @autoconfig enabled in development + */ + rethrowErrors: boolean; + /** + * Controls which imports should be controlled via imported + * @default - everything non http + */ + fileFilter: (url: string) => boolean; + /** + * Controls import signature matching + * - true(default): checks signatures + * - false: uses "marks"(file names) only + */ + checkSignatures: boolean; +} + +export const settings: ImportedClientSettings = { hot: (!!module as any).hot, SSR: isBackend, rethrowErrors: process.env.NODE_ENV !== 'production', fileFilter: rejectNetwork, + checkSignatures: true, }; /** * allows fine tune imported logic + * client side only! * @internal + * @see configuration via imported.json {@link https://github.com/theKashey/react-imported-component#importedjs} */ -export const setConfiguration = (config: Partial) => { +export const setConfiguration = (config: Partial) => { Object.assign(settings, config); }; diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 0bdd39dd..8dc403fe 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -1,3 +1,13 @@ +export interface KnownImportOptions { + chunkName?: string; + webpackChunkName?: string; + + webpackPreload?: boolean; + webpackPrefetch?: boolean; +} + +type ImportOptions = KnownImportOptions | Record; + /** * @name ImportedConfiguration * react-imported-component configuration @@ -42,14 +52,14 @@ export interface ImportedConfiguration { * @param {String} sourceFile * @param sourceConfiguration */ - shouldPrefetch?: (targetFile: string, sourceFile: string, sourceConfiguration: object) => boolean; + shouldPrefetch?: (targetFile: string, sourceFile: string, sourceConfiguration: ImportOptions) => boolean; /** * marks import with preload comment (if possible) * @param {String} targetFile * @param {String} sourceFile * @param sourceConfiguration */ - shouldPreload?: (targetFile: string, sourceFile: string, sourceConfiguration: object) => boolean; + shouldPreload?: (targetFile: string, sourceFile: string, sourceConfiguration: ImportOptions) => boolean; /** * adds custom chunkname to a import (if possible) * @param {String} targetFile @@ -60,7 +70,7 @@ export interface ImportedConfiguration { * {undefined} - keep as is * {null} - keep as is (will remove in the future) */ - chunkName?: (targetFile: string, sourceFile: string, givenChunkName: string | undefined) => string | null | undefined; + chunkName?: (targetFile: string, sourceFile: string, importOptions: ImportOptions) => string | null | undefined; } /** diff --git a/src/entrypoints/babel.ts b/src/entrypoints/babel.ts index e1edf6eb..3bc208a9 100644 --- a/src/entrypoints/babel.ts +++ b/src/entrypoints/babel.ts @@ -1,223 +1,3 @@ -// @ts-ignore -import * as crc32 from 'crc-32'; -import { existsSync } from 'fs'; -import { dirname, join, relative, resolve } from 'path'; -import vm from 'vm'; +import { babelPlugin } from '../babel/babel'; -import { ImportedConfiguration } from '../configuration/configuration'; -import { CLIENT_SIDE_ONLY } from '../configuration/constants'; - -export const encipherImport = (str: string) => { - return crc32.str(str).toString(32); -}; - -// Babel v7 compat -let syntax: any; -try { - syntax = require('babel-plugin-syntax-dynamic-import'); -} catch (err) { - 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.' - ); - } -} -syntax = syntax.default || syntax; - -const resolveImport = (importName: string, file = '') => { - if (importName.charAt(0) === '.') { - return relative(process.cwd(), resolve(dirname(file), importName)); - } - return importName; -}; - -const templateOptions = { - placeholderPattern: /^([A-Z0-9]+)([A-Z0-9_]+)$/, -}; - -function getImportArg(callPath: any) { - return callPath.get('arguments.0'); -} - -function getComments(callPath: any) { - return callPath.has('leadingComments') ? callPath.get('leadingComments') : []; -} - -const Nope = () => false as any; -// load configuration -const configurationFile = join(process.cwd(), '.imported.js'); -const defaultConfiguration: ImportedConfiguration = (existsSync(configurationFile) - ? require(configurationFile) - : {}) as ImportedConfiguration; - -const processComment = ( - configuration: ImportedConfiguration, - comments: string[], - importName: string, - fileName: string, - options: { - isBootstrapFile: boolean; - } -): string[] => { - const { shouldPrefetch = Nope, shouldPreload = Nope, chunkName = Nope } = configuration; - const chunkComment = (chunk: string) => ` webpackChunkName: "${chunk}" `; - const preloadComment = () => ` webpackPreload: true `; - const prefetchComment = () => ` webpackPrefetch: true `; - - const parseMagicComments = (str: string): object => { - if (str.trim() === CLIENT_SIDE_ONLY) { - return {}; - } - try { - const values = vm.runInNewContext(`(function(){return {${str}};})()`); - return values; - } catch (e) { - return {}; - } - }; - - const importConfiguration = comments.reduce( - (acc, comment) => ({ - ...acc, - ...parseMagicComments(comment), - }), - {} as any - ); - - const newChunkName = chunkName(importName, fileName, importConfiguration); - const { isBootstrapFile } = options; - return [ - ...comments, - !isBootstrapFile && shouldPrefetch(importName, fileName, importConfiguration) ? prefetchComment() : '', - !isBootstrapFile && shouldPreload(importName, fileName, importConfiguration) ? preloadComment() : '', - newChunkName ? chunkComment(newChunkName) : '', - ].filter(x => !!x); -}; - -export const createTransformer = ( - { types: t, template }: any, - excludeMacro = false, - configuration = defaultConfiguration -) => { - 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(); - - let isBootstrapFile = false; - - return { - traverse(programPath: any, fileName: string) { - programPath.traverse({ - ImportDeclaration(path: any) { - if (excludeMacro) { - return; - } - - const source = path.node.source.value; - if (source === 'react-imported-component/macro') { - const { specifiers } = path.node; - path.remove(); - const assignName = 'assignImportedComponents'; - if (specifiers.length === 1 && specifiers[0].imported.name === assignName) { - isBootstrapFile = true; - programPath.node.body.unshift( - t.importDeclaration( - [t.importSpecifier(t.identifier(assignName), t.identifier(assignName))], - t.stringLiteral('react-imported-component/boot') - ) - ); - } else { - programPath.node.body.unshift( - t.importDeclaration( - specifiers.map((spec: any) => - t.importSpecifier(t.identifier(spec.imported.name), t.identifier(spec.imported.name)) - ), - t.stringLiteral('react-imported-component') - ) - ); - } - } - }, - Import({ parentPath }: any) { - if (visitedNodes.has(parentPath.node)) { - return; - } - - const newImport = parentPath.node; - const rawImport = getImportArg(parentPath); - const importName = rawImport.node.value; - const rawComments = getComments(rawImport); - const comments = rawComments.map((parent: any) => parent.node.value); - - const newComments = processComment(configuration, comments, importName, fileName, { - isBootstrapFile, - }); - - if (newComments !== comments) { - rawComments.forEach((comment: any) => comment.remove()); - newComments.forEach((comment: string) => { - rawImport.addComment('leading', comment); - }); - } - - if (!importName) { - return; - } - const requiredFileHash = encipherImport(resolveImport(importName, fileName)); - - let replace = null; - - replace = importRegistration({ - MARK: t.stringLiteral(`imported_${requiredFileHash}_component`), - IMPORT: newImport, - }); - - hasImports.add(fileName); - visitedNodes.set(newImport, true); - - parentPath.replaceWith(replace); - }, - }); - }, - - finish(node: any, filename: string) { - if (!hasImports.has(filename)) { - return; - } - node.body.unshift(headerTemplate()); - }, - - hasImports, - }; -}; - -export default function(babel: any, options: ImportedConfiguration = {}) { - const transformer = createTransformer(babel, false, { - ...defaultConfiguration, - ...options, - }); - - return { - inherits: syntax, - - visitor: { - Program: { - enter(programPath: any, { file }: any) { - transformer.traverse(programPath, file.opts.filename); - }, - - exit({ node }: any, { file }: any) { - transformer.finish(node, file.opts.filename); - }, - }, - }, - }; -} +export default babelPlugin; diff --git a/src/entrypoints/macro.ts b/src/entrypoints/macro.ts index 3272c215..9c0eb67e 100644 --- a/src/entrypoints/macro.ts +++ b/src/entrypoints/macro.ts @@ -1,7 +1,7 @@ // @ts-ignore import { createMacro } from 'babel-plugin-macros'; +import { createTransformer } from '../babel/babel'; import { assignImportedComponents } from '../loadable/loadable'; -import { createTransformer } from './babel'; function getMacroType(tagName: string) { switch (tagName) { diff --git a/src/loadable/loadable.ts b/src/loadable/loadable.ts index e7259a2d..f1453212 100644 --- a/src/loadable/loadable.ts +++ b/src/loadable/loadable.ts @@ -240,8 +240,10 @@ export function getLoadable(importFunction: DefaultImport | Loadable): return LOADABLE_WEAK_SIGNATURE.get(importFunction) as any; } + const ownMark = importMatch(String(importFunction)).join('|'); + const rawSignature = getFunctionSignature(importFunction); // read cache signature - const functionSignature = getFunctionSignature(importFunction); + const functionSignature = (!settings.checkSignatures && ownMark) || rawSignature; if (LOADABLE_SIGNATURE.has(functionSignature)) { // tslint:disable-next-line:no-shadowed-variable @@ -251,16 +253,16 @@ export function getLoadable(importFunction: DefaultImport | Loadable): return loadable as any; } - const ownMark = importMatch(functionSignature).join('|'); if (ownMark) { LOADABLE_SIGNATURE.forEach(({ mark, importer }) => { if (mark.join('|') === ownMark) { // tslint:disable-next-line:no-console console.warn( - 'Another loadable found for an existing mark. That\'s possible, signatures must match (https://github.com/theKashey/react-imported-component/issues/192):', + 'Another loadable found for an existing mark. That\'s possible, but signatures must match (https://github.com/theKashey/react-imported-component/issues/192):', { mark, knownImporter: importer, + currentImported: importFunction, currentSignature: String(importFunction), knownSignature: String(importer), } diff --git a/src/scanners/scanForImports.ts b/src/scanners/scanForImports.ts index 2831c4b4..7222ef9b 100644 --- a/src/scanners/scanForImports.ts +++ b/src/scanners/scanForImports.ts @@ -95,7 +95,7 @@ export const remapImports = ( const isClientSideOnly = clientSideOnly(comment); const givenChunkName = getChunkName(comment)[0] || ''; const def = `[() => import(${comment}'${fileName}'), '${(chunkName && - chunkName(rootName, sourceName, givenChunkName)) || + chunkName(rootName, sourceName, { chunkName: givenChunkName })) || givenChunkName}', '${rootName}', ${isClientSideOnly}] /* from ${sourceName} */`; const slot = getRelativeName(root, name); diff --git a/src/utils/signatures.ts b/src/utils/signatures.ts index 4e8251c7..795d72e2 100644 --- a/src/utils/signatures.ts +++ b/src/utils/signatures.ts @@ -21,7 +21,8 @@ export const getFunctionSignature = (fn: AnyFunction | string) => .replace(/\/\*([^\*]*)\*\//gi, '') // webpack specific - .replace(/\w+.e\(/, '-we(') + .replace(/Promise.resolve\([^)]*\)/, '-we()') + .replace(/\w+.e\([^)]*\)/, '-we()') .replace(/\w+.\w.bind\(/, '-wbind(') .replace(/\w+.bind\(/, '-wbind(') From 9389485939bc0297ac591c99239091f9b1fab532 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 27 Jul 2020 16:33:07 +1000 Subject: [PATCH 2/9] 6.3.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d5e960a..94e08da2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-imported-component", - "version": "6.3.5", + "version": "6.3.6", "description": "I will import your component, and help to handle it", "main": "dist/es5/entrypoints/index.js", "jsnext:main": "dist/es2015/entrypoints/index.js", From d7fd72acd3e306e520c7a7be42f9fe43c47a4fd4 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 27 Jul 2020 17:51:34 +1000 Subject: [PATCH 3/9] allow configuration via imported.js --- __tests__/rehydrate.spec.tsx | 2 +- __tests__/signatures.spec.ts | 8 ++++++++ src/{ => configuration}/config.ts | 4 ++-- src/configuration/configuration.ts | 7 +++++++ src/entrypoints/boot.ts | 2 +- src/entrypoints/index.ts | 2 +- src/entrypoints/server.ts | 2 +- src/loadable/loadable.ts | 4 ++-- src/scanners/scanForImports.ts | 18 ++++++++++++------ src/ui/Component.tsx | 2 +- 10 files changed, 36 insertions(+), 15 deletions(-) rename src/{ => configuration}/config.ts (93%) diff --git a/__tests__/rehydrate.spec.tsx b/__tests__/rehydrate.spec.tsx index 69cabdaf..2085a4e6 100644 --- a/__tests__/rehydrate.spec.tsx +++ b/__tests__/rehydrate.spec.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom/server'; import { act } from 'react-dom/test-utils'; -import { settings } from '../src/config'; +import { settings } from '../src/configuration/config'; import { done as whenDone, toLoadable } from '../src/loadable/loadable'; import { createLoadableStream, drainHydrateMarks, rehydrateMarks } from '../src/loadable/marks'; import { ImportedComponent } from '../src/ui/Component'; diff --git a/__tests__/signatures.spec.ts b/__tests__/signatures.spec.ts index 360cdb1f..a8fbf90c 100644 --- a/__tests__/signatures.spec.ts +++ b/__tests__/signatures.spec.ts @@ -123,4 +123,12 @@ describe('importMatch', () => { '()=>$(`imported_-is59m_component`,-we().-wbind(null,`./Promo.jsx`)))' ); }); + + it('fallback check: same signature, different function', () => { + expect( + getFunctionSignature('()=>P("imported_-one_component",t.e(41).then(t.bind(null,"./Promo.jsx")))') + ).not.toEqual( + getFunctionSignature('()=>s("imported_-another_component",n.e(41).then(n.bind(null,"./Promo.jsx")))') + ); + }); }); diff --git a/src/config.ts b/src/configuration/config.ts similarity index 93% rename from src/config.ts rename to src/configuration/config.ts index 8e9e0927..67bb5d4b 100644 --- a/src/config.ts +++ b/src/configuration/config.ts @@ -1,11 +1,11 @@ -import { isBackend } from './utils/detectBackend'; +import { isBackend } from '../utils/detectBackend'; const rejectNetwork = (url: string) => url.indexOf('http') !== 0; /** * client-side only imported settings */ -interface ImportedClientSettings { +export interface ImportedClientSettings { /** * enabled hot module replacement * @autoconfig enabled if HMR is detected diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 8dc403fe..fca9d911 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -1,3 +1,5 @@ +import { ImportedClientSettings } from './config'; + export interface KnownImportOptions { chunkName?: string; webpackChunkName?: string; @@ -71,6 +73,11 @@ export interface ImportedConfiguration { * {null} - keep as is (will remove in the future) */ chunkName?: (targetFile: string, sourceFile: string, importOptions: ImportOptions) => string | null | undefined; + + /** + * clientside configuration properties to be passed into `setConfiguration` + */ + configuration?: Partial; } /** diff --git a/src/entrypoints/boot.ts b/src/entrypoints/boot.ts index 4c389ddf..0c4abebb 100644 --- a/src/entrypoints/boot.ts +++ b/src/entrypoints/boot.ts @@ -1,4 +1,4 @@ -import { setConfiguration } from '../config'; +import { setConfiguration } from '../configuration/config'; import { assignImportedComponents, done as whenComponentsReady } from '../loadable/loadable'; import { loadByChunkname } from '../loadable/loadByChunkName'; import { rehydrateMarks } from '../loadable/marks'; diff --git a/src/entrypoints/index.ts b/src/entrypoints/index.ts index 172b65cc..c06a008d 100644 --- a/src/entrypoints/index.ts +++ b/src/entrypoints/index.ts @@ -1,4 +1,4 @@ -import { setConfiguration } from '../config'; +import { setConfiguration } from '../configuration/config'; import { configure, ImportedConfiguration } from '../configuration/configuration'; import { assignImportedComponents, diff --git a/src/entrypoints/server.ts b/src/entrypoints/server.ts index f32009ce..7660b6ae 100644 --- a/src/entrypoints/server.ts +++ b/src/entrypoints/server.ts @@ -1,4 +1,4 @@ -import { setConfiguration } from '../config'; +import { setConfiguration } from '../configuration/config'; import { getMarkedChunks, getMarkedFileNames } from '../loadable/markerMapper'; import { createLoadableStream, drainHydrateMarks, printDrainHydrateMarks } from '../loadable/marks'; import { getLoadableTrackerCallback } from '../trackers/globalTracker'; diff --git a/src/loadable/loadable.ts b/src/loadable/loadable.ts index f1453212..6b71fb76 100644 --- a/src/loadable/loadable.ts +++ b/src/loadable/loadable.ts @@ -1,4 +1,4 @@ -import { settings } from '../config'; +import { settings } from '../configuration/config'; import { AnyFunction, DefaultImport, Loadable, Mark, MarkMeta, Promised } from '../types'; import { isBackend } from '../utils/detectBackend'; import { getFunctionSignature, importMatch } from '../utils/signatures'; @@ -240,8 +240,8 @@ export function getLoadable(importFunction: DefaultImport | Loadable): return LOADABLE_WEAK_SIGNATURE.get(importFunction) as any; } - const ownMark = importMatch(String(importFunction)).join('|'); const rawSignature = getFunctionSignature(importFunction); + const ownMark = importMatch(String(rawSignature)).join('|'); // read cache signature const functionSignature = (!settings.checkSignatures && ownMark) || rawSignature; diff --git a/src/scanners/scanForImports.ts b/src/scanners/scanForImports.ts index 7222ef9b..0f994788 100644 --- a/src/scanners/scanForImports.ts +++ b/src/scanners/scanForImports.ts @@ -111,11 +111,12 @@ function scanTop(root: string, start: string, target: string) { // try load configuration const configurationFile = join(root, '.imported.js'); - const { testFile = () => true, testImport = () => true, chunkName }: ImportedConfiguration = existsSync( - configurationFile - ) - ? require(configurationFile) - : {}; + const { + testFile = () => true, + testImport = () => true, + chunkName, + configuration, + }: ImportedConfiguration = existsSync(configurationFile) ? require(configurationFile) : {}; const files = ((await scanDirectory(join(root, start), undefined, rejectSystemFiles)) as string[]) .filter(name => normalizePath(name).indexOf(target) === -1) @@ -146,7 +147,12 @@ function scanTop(root: string, start: string, target: string) { /* tslint:disable */ // generated by react-imported-component, DO NOT EDIT - import {assignImportedComponents} from 'react-imported-component/macro'; + import {assignImportedComponents} from 'react-imported-component/macro'; + ${configuration && + `import {setConfiguration} from 'react-imported-component/boot'; +// as configured in .imported.js +setConfiguration(${JSON.stringify(configuration, null, 2)}); + `} // all your imports are defined here // all, even the ones you tried to hide in comments (that's the cost of making a very fast parser) diff --git a/src/ui/Component.tsx b/src/ui/Component.tsx index 4d3faad9..c9520c4e 100644 --- a/src/ui/Component.tsx +++ b/src/ui/Component.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { ReactElement } from 'react'; -import { settings } from '../config'; +import { settings } from '../configuration/config'; import { ComponentOptions } from '../types'; import { useImported } from './useImported'; From 911e57cc86c609f3b10f3172f7e935f3295f86db Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 27 Jul 2020 17:52:15 +1000 Subject: [PATCH 4/9] 6.3.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 94e08da2..10d763c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-imported-component", - "version": "6.3.6", + "version": "6.3.7", "description": "I will import your component, and help to handle it", "main": "dist/es5/entrypoints/index.js", "jsnext:main": "dist/es2015/entrypoints/index.js", From 689cf871d921678d43bd312d2a5e71ebeebda8c4 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 27 Jul 2020 19:00:44 +1000 Subject: [PATCH 5/9] correct mark application --- .size-limit.js | 6 +++--- .size.json | 6 +++--- src/configuration/config.ts | 22 ++++++++++++++++++++-- src/loadable/loadable.ts | 16 ++++++++++------ 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 411f9098..5f6c9ba7 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -2,16 +2,16 @@ module.exports = [ { path: ['dist/es2015/entrypoints/index.js', 'dist/es2015/entrypoints/boot.js'], ignore: ['tslib'], - limit: '3.6 KB', + limit: '3.7 KB', }, { path: 'dist/es2015/entrypoints/index.js', ignore: ['tslib'], - limit: '3.3 KB', + limit: '3.5 KB', }, { path: 'dist/es2015/entrypoints/boot.js', ignore: ['tslib'], - limit: '1.8 KB', + limit: '1.9 KB', }, ]; diff --git a/.size.json b/.size.json index d0c8db75..95b0536f 100644 --- a/.size.json +++ b/.size.json @@ -2,16 +2,16 @@ { "name": "dist/es2015/entrypoints/index.js, dist/es2015/entrypoints/boot.js", "passed": true, - "size": 3646 + "size": 3717 }, { "name": "dist/es2015/entrypoints/index.js", "passed": true, - "size": 3354 + "size": 3428 }, { "name": "dist/es2015/entrypoints/boot.js", "passed": true, - "size": 1803 + "size": 1879 } ] diff --git a/src/configuration/config.ts b/src/configuration/config.ts index 67bb5d4b..77a7ec76 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -34,7 +34,7 @@ export interface ImportedClientSettings { checkSignatures: boolean; } -export const settings: ImportedClientSettings = { +const localSettings: ImportedClientSettings = { hot: (!!module as any).hot, SSR: isBackend, rethrowErrors: process.env.NODE_ENV !== 'production', @@ -42,6 +42,24 @@ export const settings: ImportedClientSettings = { checkSignatures: true, }; +export const settings = { + get hot() { + return localSettings.hot; + }, + get SSR() { + return localSettings.SSR; + }, + get rethrowErrors() { + return localSettings.rethrowErrors; + }, + get fileFilter() { + return localSettings.fileFilter; + }, + get checkSignatures() { + return localSettings.checkSignatures; + }, +}; + /** * allows fine tune imported logic * client side only! @@ -49,5 +67,5 @@ export const settings: ImportedClientSettings = { * @see configuration via imported.json {@link https://github.com/theKashey/react-imported-component#importedjs} */ export const setConfiguration = (config: Partial) => { - Object.assign(settings, config); + Object.assign(localSettings, config); }; diff --git a/src/loadable/loadable.ts b/src/loadable/loadable.ts index 6b71fb76..0a161286 100644 --- a/src/loadable/loadable.ts +++ b/src/loadable/loadable.ts @@ -20,9 +20,13 @@ const LOADABLE_SIGNATURE = new Map>(); const addPending = (promise: Promise) => pending.push(promise); const removeFromPending = (promise: Promise) => (pending = pending.filter(a => a !== promise)); +const toKnownSignature = (signature: string, marks: string[]) => + (!settings.checkSignatures && marks.join('|')) || signature; + export function toLoadable(firstImportFunction: Promised, autoImport = true): Loadable { let importFunction = firstImportFunction; - const callLoad = (): Promise => Promise.all([importFunction(), ...getPreloaders()]).then(([result]) => result); + const loadImportedComponent = (): Promise => + Promise.all([importFunction(), ...getPreloaders()]).then(([result]) => result); const functionSignature = getFunctionSignature(importFunction); const mark = importMatch(functionSignature); @@ -114,7 +118,7 @@ export function toLoadable(firstImportFunction: Promised, autoImport = tru load() { if (!this.promise) { - const promise = (this.promise = callLoad().then( + const promise = (this.promise = loadImportedComponent().then( payload => { this.done = true; this.ok = true; @@ -139,7 +143,7 @@ export function toLoadable(firstImportFunction: Promised, autoImport = tru }; if (mark && mark.length) { - LOADABLE_SIGNATURE.set(functionSignature, loadable); + LOADABLE_SIGNATURE.set(toKnownSignature(functionSignature, mark), loadable); assingLoadableMark(mark, loadable); } else { if (process.env.NODE_ENV !== 'development') { @@ -241,9 +245,9 @@ export function getLoadable(importFunction: DefaultImport | Loadable): } const rawSignature = getFunctionSignature(importFunction); - const ownMark = importMatch(String(rawSignature)).join('|'); + const ownMark = importMatch(String(rawSignature)); // read cache signature - const functionSignature = (!settings.checkSignatures && ownMark) || rawSignature; + const functionSignature = toKnownSignature(rawSignature, ownMark); if (LOADABLE_SIGNATURE.has(functionSignature)) { // tslint:disable-next-line:no-shadowed-variable @@ -255,7 +259,7 @@ export function getLoadable(importFunction: DefaultImport | Loadable): if (ownMark) { LOADABLE_SIGNATURE.forEach(({ mark, importer }) => { - if (mark.join('|') === ownMark) { + if (mark[0] === ownMark[1] && mark.join('|') === ownMark.join('|')) { // tslint:disable-next-line:no-console console.warn( 'Another loadable found for an existing mark. That\'s possible, but signatures must match (https://github.com/theKashey/react-imported-component/issues/192):', From 81729342c927f7a6fd60b47c456b1792e9170d79 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 27 Jul 2020 19:19:01 +1000 Subject: [PATCH 6/9] fix immutable for tests --- __tests__/rehydrate.spec.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/__tests__/rehydrate.spec.tsx b/__tests__/rehydrate.spec.tsx index 2085a4e6..3758f84b 100644 --- a/__tests__/rehydrate.spec.tsx +++ b/__tests__/rehydrate.spec.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom/server'; import { act } from 'react-dom/test-utils'; -import { settings } from '../src/configuration/config'; +import { setConfiguration } from '../src/configuration/config'; import { done as whenDone, toLoadable } from '../src/loadable/loadable'; import { createLoadableStream, drainHydrateMarks, rehydrateMarks } from '../src/loadable/marks'; import { ImportedComponent } from '../src/ui/Component'; @@ -22,10 +22,10 @@ describe('SSR Component', () => { describe('client-rehydrate', () => { beforeEach(() => { - settings.SSR = false; + setConfiguration({ SSR: false }); }); afterEach(() => { - settings.SSR = true; + setConfiguration({ SSR: true }); }); it('SSR green case', async () => { @@ -110,11 +110,9 @@ describe('SSR Component', () => { describe('server-rehydrate', () => { beforeEach(() => { - settings.SSR = true; - }); - afterEach(() => { - settings.SSR = true; + setConfiguration({ SSR: false }); }); + it('green case', async () => { const renderSpy2 = jest.fn().mockImplementation(A =>
{A && }
); const Component = () =>
loaded!
; From 9c358fc9ba4ceb78d31fb7bbe95af84377487760 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 27 Jul 2020 19:19:15 +1000 Subject: [PATCH 7/9] 6.3.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 10d763c8..da0dd135 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-imported-component", - "version": "6.3.7", + "version": "6.3.8", "description": "I will import your component, and help to handle it", "main": "dist/es5/entrypoints/index.js", "jsnext:main": "dist/es2015/entrypoints/index.js", From d83e141d15c0fb342fdeb824b8c71b2bec04b690 Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 27 Jul 2020 19:19:53 +1000 Subject: [PATCH 8/9] 6.3.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index da0dd135..ad8008c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-imported-component", - "version": "6.3.8", + "version": "6.3.9", "description": "I will import your component, and help to handle it", "main": "dist/es5/entrypoints/index.js", "jsnext:main": "dist/es2015/entrypoints/index.js", From 15047a27f8d3e727a49ce66d117aa6cd912054de Mon Sep 17 00:00:00 2001 From: Anton Korzunov Date: Mon, 27 Jul 2020 21:01:28 +1000 Subject: [PATCH 9/9] fix: replace detect-node by detect-node-es --- .size.json | 6 +++--- package.json | 3 +-- src/utils/detectBackend.ts | 4 ++-- yarn.lock | 10 +++++----- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.size.json b/.size.json index 95b0536f..2e23990c 100644 --- a/.size.json +++ b/.size.json @@ -2,16 +2,16 @@ { "name": "dist/es2015/entrypoints/index.js, dist/es2015/entrypoints/boot.js", "passed": true, - "size": 3717 + "size": 3704 }, { "name": "dist/es2015/entrypoints/index.js", "passed": true, - "size": 3428 + "size": 3414 }, { "name": "dist/es2015/entrypoints/boot.js", "passed": true, - "size": 1879 + "size": 1863 } ] diff --git a/package.json b/package.json index ad8008c6..c8bf6a2e 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "@babel/runtime": "^7.3.1", "@size-limit/preset-small-lib": "^4.5.1", "@theuiteam/lib-builder": "0.0.8", - "@types/detect-node": "^2.0.0", "@types/enzyme": "^3.10.3", "@types/node": "^12.12.6", "@types/react-dom": "^16.9.4", @@ -78,7 +77,7 @@ "dependencies": { "babel-plugin-macros": "^2.6.1", "crc-32": "^1.2.0", - "detect-node": "^2.0.4", + "detect-node-es": "^1.0.0", "scan-directory": "^2.0.0", "tslib": "^1.10.0" }, diff --git a/src/utils/detectBackend.ts b/src/utils/detectBackend.ts index 5ad1e875..ff2c3bb5 100644 --- a/src/utils/detectBackend.ts +++ b/src/utils/detectBackend.ts @@ -1,3 +1,3 @@ -import isNodeDetected from 'detect-node'; +import { isNode } from 'detect-node-es'; -export const isBackend = isNodeDetected || typeof window === 'undefined'; +export const isBackend = isNode || typeof window === 'undefined'; diff --git a/yarn.lock b/yarn.lock index 102ac8de..d9da3282 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1651,11 +1651,6 @@ resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.29.tgz#a1e514adfbd92f03a224ba54d693111dbf1f3754" integrity sha1-oeUUrfvZLwOiJLpU1pMRHb8fN1Q= -"@types/detect-node@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/detect-node/-/detect-node-2.0.0.tgz#696e024ddd105c72bbc6a2e3f97902a2886f2c3f" - integrity sha512-+BozjlbPTACYITf1PWf62HLtDV79HbmZosUN1mv1gGrnjDCRwBXkDKka1sf6YQJvspmfPXVcy+X6tFW62KteeQ== - "@types/enzyme@^3.10.3": version "3.10.3" resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.3.tgz#02b6c5ac7d0472005944a652e79045e2f6c66804" @@ -5346,6 +5341,11 @@ detect-newline@^2.1.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= +detect-node-es@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.0.0.tgz#c0318b9e539a5256ca780dd9575c9345af05b8ed" + integrity sha512-S4AHriUkTX9FoFvL4G8hXDcx6t3gp2HpfCza3Q0v6S78gul2hKWifLQbeW+ZF89+hSm2ZIc/uF3J97ZgytgTRg== + detect-node@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"