diff --git a/.size.json b/.size.json index 785590c3..06d7f7dd 100644 --- a/.size.json +++ b/.size.json @@ -1,17 +1,17 @@ [ { - "name": "dist/es2015/index.js, dist/es2015/boot.js", + "name": "dist/es2015/entrypoints/index.js, dist/es2015/entrypoints/boot.js", "passed": true, - "size": 3480 + "size": 3434 }, { - "name": "dist/es2015/index.js", + "name": "dist/es2015/entrypoints/index.js", "passed": true, - "size": 3157 + "size": 3156 }, { - "name": "dist/es2015/boot.js", + "name": "dist/es2015/entrypoints/boot.js", "passed": true, - "size": 1802 + "size": 1763 } ] diff --git a/__tests__/__fixtures__/babel/boot/actual.js b/__tests__/__fixtures__/babel/boot/actual.js new file mode 100644 index 00000000..f176d19d --- /dev/null +++ b/__tests__/__fixtures__/babel/boot/actual.js @@ -0,0 +1,13 @@ +/* eslint-disable */ +/* tslint:disable */ + +// generated by react-imported-component, DO NOT EDIT +import {assignImportedComponents} from 'react-imported-component/macro'; + +const PreloadComponent = imported(() => import('./PreloadThis')); +const PrefetchChunkComponent = imported(() => import('./ChunkThis')); + +const applicationImports = assignImportedComponents([ + [() => import('./should-not-be-transformed'), '', './some-file', false], +]); + diff --git a/__tests__/__fixtures__/babel/boot/expected.js b/__tests__/__fixtures__/babel/boot/expected.js new file mode 100644 index 00000000..712ca160 --- /dev/null +++ b/__tests__/__fixtures__/babel/boot/expected.js @@ -0,0 +1,13 @@ +var importedWrapper = require('react-imported-component/wrapper'); + +import { assignImportedComponents } from "react-imported-component/boot"; + +/* eslint-disable */ + +/* tslint:disable */ +// generated by react-imported-component, DO NOT EDIT +const PreloadComponent = imported(() => importedWrapper("imported_-hbct7n_component", import('./PreloadThis'))); +const PrefetchChunkComponent = imported(() => importedWrapper("imported_fspdct_component", import( +/* webpackChunkName: "chunked-this" */ +'./ChunkThis'))); +const applicationImports = assignImportedComponents([[() => importedWrapper("imported_12i8gg9_component", import('./should-not-be-transformed')), '', './some-file', false]]); \ No newline at end of file diff --git a/__tests__/__fixtures__/babel/webpack/actual.js b/__tests__/__fixtures__/babel/webpack/actual.js index 6aa98c55..ae7d6405 100644 --- a/__tests__/__fixtures__/babel/webpack/actual.js +++ b/__tests__/__fixtures__/babel/webpack/actual.js @@ -1,7 +1,8 @@ -import {assignImportedComponents} from 'react-imported-component/macro'; import {lazy, useImported} from 'react-imported-component/macro'; import imported from 'react-imported-component'; +const PreloadComponent = imported(() => import('./PreloadThis')); +const PrefetchChunkComponent = imported(() => import('./ChunkThis')); 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 47e20677..d35f0ded 100644 --- a/__tests__/__fixtures__/babel/webpack/expected.js +++ b/__tests__/__fixtures__/babel/webpack/expected.js @@ -1,8 +1,15 @@ var importedWrapper = require('react-imported-component/wrapper'); import { lazy, useImported } from "react-imported-component"; -import { assignImportedComponents } from "react-imported-component/boot"; import imported from 'react-imported-component'; +const PreloadComponent = imported(() => importedWrapper("imported_-hbct7n_component", import( +/* webpackPreload: true */ +'./PreloadThis'))); +const PrefetchChunkComponent = imported(() => importedWrapper("imported_fspdct_component", import( +/* webpackChunkName: "chunked-this" */ + +/* webpackPrefetch: true */ +'./ChunkThis'))); const AsyncComponent0 = imported(() => importedWrapper("imported_18g2v0c_component", import( /* webpackChunkName:namedChunk */ './MyComponent'))); diff --git a/__tests__/babel.spec.ts b/__tests__/babel.spec.ts index 6e54285c..83fb8755 100644 --- a/__tests__/babel.spec.ts +++ b/__tests__/babel.spec.ts @@ -1,22 +1,37 @@ import { transform } from '@babel/core'; import { readFileSync } from 'fs'; import { join } from 'path'; +import { ImportedConfiguration } from '../src/configuration/configuration'; const FIXTURE_PATH = join(__dirname, '__fixtures__/babel'); +const configuration: ImportedConfiguration = { + shouldPreload: filename => filename.indexOf('PreloadThis') >= 0, + shouldPrefetch: filename => filename.indexOf('ChunkThis') >= 0, + chunkName: filename => (filename.indexOf('ChunkThis') >= 0 ? 'chunked-this' : undefined), +}; + const testPlugin = { - /*node: (code: string) => { + node: (code: string) => { const result = transform(code, { presets: ['@babel/preset-react'], - plugins: [require.resolve('../dist/es5/babel'), 'dynamic-import-node'], + plugins: [require.resolve('../dist/es5/entrypoints/babel'), 'dynamic-import-node'], }); return result!.code; - },*/ + }, webpack: (code: string) => { const result = transform(code, { presets: ['@babel/preset-react'], - plugins: [require.resolve('../dist/es5/entrypoints/babel')], + plugins: [[require.resolve('../dist/es5/entrypoints/babel'), configuration]], + }); + + return result!.code; + }, + boot: (code: string) => { + const result = transform(code, { + presets: ['@babel/preset-react'], + plugins: [[require.resolve('../dist/es5/entrypoints/babel'), configuration]], }); return result!.code; diff --git a/__tests__/macro.spec.ts b/__tests__/macro.spec.ts index 3b04a0e0..e9f32055 100644 --- a/__tests__/macro.spec.ts +++ b/__tests__/macro.spec.ts @@ -1,6 +1,6 @@ -const pluginTester = require("babel-plugin-tester"); -const plugin = require("babel-plugin-macros"); -const prettier = require("prettier"); +const pluginTester = require('babel-plugin-tester'); +const plugin = require('babel-plugin-macros'); +const prettier = require('prettier'); describe('babel macro', () => { pluginTester({ @@ -11,24 +11,24 @@ describe('babel macro', () => { plugins: ['dynamic-import-node'], }, formatResult(result: string) { - return prettier.format(result, {trailingComma: "es5"}); + return prettier.format(result, { trailingComma: 'es5' }); }, tests: { - "nothing": "const a = 42;", - "no usage": `import {lazy} from "../macro";`, - "flat import": `import "../macro"; + nothing: 'const a = 42;', + 'no usage': `import {lazy} from "../macro";`, + 'flat import': `import "../macro"; import('./a.js') `, - "boot": ` + boot: ` import {assignImportedComponents, lazy} from "../macro"; assignImportedComponents([() => import('./a')]); lazy(() => import('./a')); `, - "lazy": ` + lazy: ` import {lazy} from "../macro"; const v = lazy(() => import('./a')); `, - "many": ` + many: ` import {imported, useImported} from "../macro"; const v = imported(() => import('./a')); const x = () => useImported(() => import('./b')); @@ -46,14 +46,14 @@ describe('babel macro', () => { plugins: [require.resolve('../babel'), 'dynamic-import-node'], }, formatResult(result: string) { - return prettier.format(result, {trailingComma: "es5"}); + return prettier.format(result, { trailingComma: 'es5' }); }, tests: { - "plugin combination": ` + 'plugin combination': ` import {imported, useImported} from "../macro"; const v = imported(() => import('./a')); const x = () => useImported(() => import('./b')); `, }, }); -}); \ No newline at end of file +}); diff --git a/__tests__/utils.spec.ts b/__tests__/utils.spec.ts index a9c3d5fc..eb87381a 100644 --- a/__tests__/utils.spec.ts +++ b/__tests__/utils.spec.ts @@ -91,6 +91,29 @@ describe('scanForImports', () => { ]); }); + it('should override chunk name', () => { + const imports = {}; + remapImports( + [ + { + file: 'a', + content: + 'blabla;import(/* webpackChunkName: "chunk-a" */"./a.js"); blabla; import(/* webpackChunkName: "chunk-b" */"./b.js"); import(/* webpackChunkName: "chunk-c" */"./c.js");', + }, + ], + root, + root, + (a, b) => a + b, + imports, + imported => imported.indexOf('c.js') < 0, + (imported, _, givenChunkName) => (imported.indexOf('a.js') > 0 ? `test-${givenChunkName}-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 */`, + `[() => import(/* webpackChunkName: \"chunk-b\" */'${rel}/b.js'), 'bundle-b', '${rel}/b.js', false] /* from .a */`, + ]); + }); + it('should match support multiline imports', () => { const imports = {}; remapImports( diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index a0ce1a3d..de98bff0 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -1,7 +1,7 @@ /** * react-imported-component configuration * __TO BE USED AT `imported.js`__ - * @see {@link https://github.com/theKashey/react-imported-component#.imported.js} + * @see {@link https://github.com/theKashey/react-imported-component#-imported-js} */ export interface ImportedConfiguration { /** @@ -15,10 +15,11 @@ export interface ImportedConfiguration { testFile?: (fileName: string) => boolean; /** * tests if a given import should be visible to a `imported-component` + * This method is equivalent to `client-side` magic comment * @param {String} targetFileName - import target * @param {String} sourceFileName - source filename - * @param {Object} configuration - an import configuration * @returns {Boolean} false if import should be ignored by the `imported-components` + * @see {@link https://github.com/theKashey/react-imported-component/#server-side-auto-import} * @example * testImport(filename, source, config) { * return !( @@ -29,18 +30,7 @@ export interface ImportedConfiguration { * ) * } */ - testImport?: (targetFileName: string, sourceFileName: string, configuration: object) => boolean; - /** - * tests if this import should(or could) be executed only on ClientSide - * @param targetFileName - * @param sourceFileName - * @returns {Boolean} true if only client side - * @example - * clientSideOnly(target) { - * return target.indexOf('polyfill')>0 - * } - */ - clientSideOnly: (targetFileName: string, sourceFileName: string) => boolean; + testImport?: (targetFileName: string, sourceFileName: string) => boolean; /** * marks import with prefetch comment (if possible) @@ -48,14 +38,14 @@ export interface ImportedConfiguration { * @param sourceFile * @param sourceConfiguration */ - shouldPrefetch: (targetFile: string, sourceFile: string, sourceConfiguration: object) => boolean; + shouldPrefetch?: (targetFile: string, sourceFile: string, sourceConfiguration: object) => boolean; /** * marks import with preload comment (if possible) * @param targetFile * @param sourceFile * @param sourceConfiguration */ - shouldPreload: (targetFile: string, sourceFile: string, sourceConfiguration: object) => boolean; + shouldPreload?: (targetFile: string, sourceFile: string, sourceConfiguration: object) => boolean; /** * adds custom chunkname to a import (if possible) * @param targetFile @@ -64,13 +54,13 @@ export interface ImportedConfiguration { * @returns * {string} - a new chunk name * {undefined} - keep as is - * {null} - remove + * {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, givenChunkName: string | undefined) => string | null | undefined; } /** * react-imported-component configuration * @param config */ -export const configure = (config: ImportedConfiguration):ImportedConfiguration => config; +export const configure = (config: ImportedConfiguration): ImportedConfiguration => config; diff --git a/src/configuration/constants.ts b/src/configuration/constants.ts new file mode 100644 index 00000000..5f8f32d0 --- /dev/null +++ b/src/configuration/constants.ts @@ -0,0 +1 @@ +export const CLIENT_SIDE_ONLY = 'client-side'; diff --git a/src/entrypoints/babel.ts b/src/entrypoints/babel.ts index f62f5a72..04c62585 100644 --- a/src/entrypoints/babel.ts +++ b/src/entrypoints/babel.ts @@ -2,7 +2,10 @@ import * as crc32 from 'crc-32'; import { existsSync } from 'fs'; import { dirname, join, relative, resolve } from 'path'; -import {ImportedConfiguration} from '../configuration/configuration'; +import vm from 'vm'; + +import { ImportedConfiguration } from '../configuration/configuration'; +import { CLIENT_SIDE_ONLY } from '../configuration/constants'; export const encipherImport = (str: string) => { return crc32.str(str).toString(32); @@ -42,23 +45,61 @@ function getComments(callPath: any) { return callPath.has('leadingComments') ? callPath.get('leadingComments') : []; } -export type CommentProcessor = (comments: string[], target: string, file: string) => string[]; - +const Nope = () => false as any; // load configuration const configurationFile = join(process.cwd(), '.imported.js'); -const { - shouldPrefetch, - shouldPreload, - chunkName, -} = (existsSync(configurationFile) ? require(configurationFile) : {}) as ImportedConfiguration; +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 processComment = (comments: string[], importName: string, fileName: string) => { + 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');`, @@ -70,6 +111,8 @@ export const createTransformer = ( const hasImports = new Set(); const visitedNodes = new Map(); + let isBootstrapFile = false; + return { traverse(programPath: any, fileName: string) { programPath.traverse({ @@ -84,6 +127,7 @@ export const createTransformer = ( 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))], @@ -113,7 +157,9 @@ export const createTransformer = ( const rawComments = getComments(rawImport); const comments = rawComments.map((parent: any) => parent.node.value); - const newComments = processComment(comments, importName, fileName); + const newComments = processComment(configuration, comments, importName, fileName, { + isBootstrapFile, + }); if (newComments !== comments) { rawComments.forEach((comment: any) => comment.remove()); @@ -153,8 +199,8 @@ export const createTransformer = ( }; }; -export default function(babel: any) { - const transformer = createTransformer(babel); +export default function(babel: any, options: ImportedConfiguration = {}) { + const transformer = createTransformer(babel, false, options); return { inherits: syntax, diff --git a/src/entrypoints/index.ts b/src/entrypoints/index.ts index fb0d6cd8..172b65cc 100644 --- a/src/entrypoints/index.ts +++ b/src/entrypoints/index.ts @@ -44,7 +44,6 @@ export { useLazy, addPreloader, clearImportedCache, - ImportedConfiguration, configure, }; diff --git a/src/scanners/scanForImports.ts b/src/scanners/scanForImports.ts index 16187881..2831c4b4 100644 --- a/src/scanners/scanForImports.ts +++ b/src/scanners/scanForImports.ts @@ -5,6 +5,8 @@ import { dirname, extname, join, resolve } from 'path'; import scanDirectory from 'scan-directory'; import { existsSync, Stats } from 'fs'; +import { ImportedConfiguration } from '../configuration/configuration'; +import { CLIENT_SIDE_ONLY } from '../configuration/constants'; import { getFileContent, getMatchString, getRelative, normalizePath, pWriteFile } from './shared'; const RESOLVE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.mjs']; @@ -28,7 +30,7 @@ const getComment = getMatchString(/\/\*.*\*\// as any, 0); const getChunkName = getMatchString('webpackChunkName: "([^"]*)"', 1); -const clientSideOnly = (comment: string) => comment.indexOf('client-side') >= 0; +const clientSideOnly = (comment: string) => comment.indexOf(CLIENT_SIDE_ONLY) >= 0; const clearComment = (str: string) => str.replace('webpackPrefetch: true', '').replace('webpackPreload: true', ''); @@ -79,7 +81,8 @@ export const remapImports = ( targetDir: string, getRelativeName: (a: string, b: string) => string, imports: Record, - testImport: (target: string, source: string) => boolean + testImport: NonNullable, + chunkName?: ImportedConfiguration['chunkName'] ) => data .map(({ file, content }) => mapImports(file, getDynamicImports(content))) @@ -90,9 +93,10 @@ export const remapImports = ( const sourceName = getRelativeName(root, file); if (testImport(rootName, sourceName)) { const isClientSideOnly = clientSideOnly(comment); - const def = `[() => import(${comment}'${fileName}'), '${getChunkName( - comment - )}', '${rootName}', ${isClientSideOnly}] /* from ${sourceName} */`; + const givenChunkName = getChunkName(comment)[0] || ''; + const def = `[() => import(${comment}'${fileName}'), '${(chunkName && + chunkName(rootName, sourceName, givenChunkName)) || + givenChunkName}', '${rootName}', ${isClientSideOnly}] /* from ${sourceName} */`; const slot = getRelativeName(root, name); // keep the maximal definition @@ -107,7 +111,9 @@ function scanTop(root: string, start: string, target: string) { // try load configuration const configurationFile = join(root, '.imported.js'); - const { testFile = () => true, testImport = () => true } = existsSync(configurationFile) + const { testFile = () => true, testImport = () => true, chunkName }: ImportedConfiguration = existsSync( + configurationFile + ) ? require(configurationFile) : {}; @@ -129,7 +135,7 @@ function scanTop(root: string, start: string, target: string) { const imports: Record = {}; const targetDir = resolve(root, dirname(target)); - remapImports(data, root, targetDir, getRelative, imports, testImport); + remapImports(data, root, targetDir, getRelative, imports, testImport, chunkName); console.log(`${Object.keys(imports).length} imports found, saving to ${target}`);