├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .nvmrc ├── .size.json ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ └── index.tsx ├── css └── package.json ├── example ├── app.tsx ├── assets │ └── .gitkeep ├── index.html ├── index.tsx ├── styled.ts └── utils.ts ├── jest.config.js ├── package.json ├── react └── package.json ├── src ├── css │ ├── correct-order.ts │ ├── index.ts │ └── processImportedStyles.ts ├── imported.spec.ts ├── imported.ts ├── index.ts ├── plugin.ts ├── react │ ├── Atoms.tsx │ ├── Chunk.tsx │ ├── Load.tsx │ ├── context.tsx │ └── index.ts ├── tracker.ts ├── types.ts └── utils │ ├── intent-the-order.spec.ts │ └── intent-the-order.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/typescript', 'plugin:react-hooks/recommended'], 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'prettier', 'import'], 5 | rules: { 6 | '@typescript-eslint/ban-ts-ignore': 0, 7 | '@typescript-eslint/no-var-requires': 0, 8 | '@typescript-eslint/camelcase': 0, 9 | 'import/order': [ 10 | 'error', 11 | { 12 | 'newlines-between': 'always-and-inside-groups', 13 | alphabetize: { 14 | order: 'asc', 15 | }, 16 | groups: ['builtin', 'external', 'internal', ['parent', 'index', 'sibling']], 17 | }, 18 | ], 19 | }, 20 | settings: { 21 | 'import/parsers': { 22 | '@typescript-eslint/parser': ['.ts', '.tsx'], 23 | }, 24 | 'import/resolver': { 25 | typescript: { 26 | alwaysTryTypes: true, 27 | }, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /lib/ 3 | /dist/ 4 | coverage 5 | .DS_Store 6 | .nyc_output -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.yarnpkg.com -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /.size.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "dist/es2015/react/index.js", 4 | "passed": true, 5 | "size": 2710, 6 | "sizeLimit": 3000 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | cache: yarn 5 | script: 6 | - yarn 7 | - yarn test:ci 8 | notifications: 9 | email: false -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.3.1](https://github.com/theKashey/webpack-imported/compare/v1.3.0...v1.3.1) (2024-08-08) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * added React 18 in peerDeps ([81b42a4](https://github.com/theKashey/webpack-imported/commit/81b42a4b8f4e879fa33cc9cf90140cd7e6f41224)) 7 | 8 | 9 | 10 | # [1.3.0](https://github.com/theKashey/webpack-imported/compare/v1.2.2...v1.3.0) (2024-03-14) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * fixes alias resolver to allow arrays ([07b7f35](https://github.com/theKashey/webpack-imported/commit/07b7f3590ec1e04a7a62fdef1f6730d7e8b32af0)) 16 | * places the alias inside the reduce ([83bf10c](https://github.com/theKashey/webpack-imported/commit/83bf10ca6ad6d1d11b2a1ab46f5668b3eddf4ecd)) 17 | * ugraded webpack types to v5 ([4ccd8b4](https://github.com/theKashey/webpack-imported/commit/4ccd8b456bfaee94aaaef84eada9a0b59c810434)) 18 | 19 | 20 | 21 | ## [1.2.2](https://github.com/theKashey/webpack-imported/compare/v1.2.1...v1.2.2) (2021-04-13) 22 | 23 | 24 | 25 | ## [1.2.1](https://github.com/theKashey/webpack-imported/compare/v1.2.0...v1.2.1) (2021-04-13) 26 | 27 | 28 | 29 | # [1.2.0](https://github.com/theKashey/webpack-imported/compare/v1.1.2...v1.2.0) (2021-04-13) 30 | 31 | 32 | 33 | ## [1.1.2](https://github.com/theKashey/webpack-imported/compare/v1.1.1...v1.1.2) (2020-12-23) 34 | 35 | 36 | 37 | ## [1.1.1](https://github.com/theKashey/webpack-imported/compare/v1.1.0...v1.1.1) (2020-12-10) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * use key on chunks ([6735f4f](https://github.com/theKashey/webpack-imported/commit/6735f4f0144fa09adc2a55d193b84cf441c5f3e5)) 43 | 44 | 45 | 46 | # [1.1.0](https://github.com/theKashey/webpack-imported/compare/v1.0.0...v1.1.0) (2020-11-23) 47 | 48 | 49 | ### Features 50 | 51 | * allow direct fs writes ([ceae2f1](https://github.com/theKashey/webpack-imported/commit/ceae2f1db457da05feb67004bdf591cb867c9f54)) 52 | 53 | 54 | 55 | # [1.0.0](https://github.com/theKashey/webpack-imported/compare/v0.0.7...v1.0.0) (2020-07-30) 56 | 57 | 58 | 59 | ## [0.0.7](https://github.com/theKashey/webpack-imported/compare/v0.0.6...v0.0.7) (2020-07-19) 60 | 61 | 62 | 63 | ## [0.0.6](https://github.com/theKashey/webpack-imported/compare/v0.0.5...v0.0.6) (2020-07-19) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * prefetch is not preload ([4296349](https://github.com/theKashey/webpack-imported/commit/4296349025f31e49653f3f4feadb7a78a082f6d0)) 69 | 70 | 71 | 72 | ## [0.0.5](https://github.com/theKashey/webpack-imported/compare/v0.0.4...v0.0.5) (2020-07-19) 73 | 74 | 75 | 76 | ## [0.0.4](https://github.com/theKashey/webpack-imported/compare/v0.0.3...v0.0.4) (2020-07-19) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * improve documentation ([6e1a8c9](https://github.com/theKashey/webpack-imported/commit/6e1a8c99b558e4534522bb13bd9877f1b62960e8)) 82 | 83 | 84 | 85 | ## [0.0.3](https://github.com/theKashey/webpack-imported/compare/65ab5f6ded3015175b9bfd18d6cbbfa4e690be9b...v0.0.3) (2019-10-28) 86 | 87 | 88 | ### Features 89 | 90 | * more options for Script Tag ([65ab5f6](https://github.com/theKashey/webpack-imported/commit/65ab5f6ded3015175b9bfd18d6cbbfa4e690be9b)) 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anton Korzunov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | webpack-imported 2 | ====== 3 | We'll get your asses imported in a right way. 4 | 5 | > 📝 stats-webpack-plugin and 💩webpack-flush-chunks had a baby! 6 | 7 | > code splitting, prefetching, and resource management. 8 | 9 | WebpackPlugin + ServerSide API + React Components(separate entrypoint) 10 | 11 | # Server side API 12 | ## Webpack plugin 13 | ```js 14 | const {ImportedPlugin} = require('webpack-imported'); 15 | 16 | module.exports = { 17 | plugins: [ 18 | new ImportedPlugin('imported.json') 19 | ] 20 | }; 21 | ``` 22 | This will output `imported.json` as a one of the emitted assets, with all the information carefully sorted. 23 | 24 | ## Stat.json 25 | If you have only `stat.json` generated somehow you can convert into into "imported" format 26 | ```js 27 | import {importStats} from "webpack-imported"; 28 | import stats from 'build/stats.json'; 29 | 30 | const importedStat = importStats(stats); 31 | ``` 32 | 33 | ## SSR API 34 | - `importedAssets(stats, chunks, [tracker])` - return all assets associated with provided chunks. 35 | Could be provided a `tracker` to prevent duplications between runs. 36 | - `createImportedTracker()` - creates a duplication prevention tracker 37 | 38 | ```js 39 | import {importedAssets} from "webpack-imported"; 40 | import importedStat from "build/imported.json"; // this file has to be generated 41 | 42 | const relatedAssets = importedAssets(importedStat, ['main']); // main is your "main" bundle 43 | 44 | relatedAssets.scripts.load // list scripts to load 45 | relatedAssets.scripts.preload // list scripts to preload 46 | relatedAssets.styles.load // list styles to load 47 | relatedAssets.styles.preload // list styles to preload 48 | 49 | importedStat.config.publicPath // public path used at build time 50 | ``` 51 | 52 | with tracking 53 | ```js 54 | import {importedAssets, createImportedTracker} from "webpack-imported"; 55 | import importedStat from "build/imported.json"; // this file has to be generated 56 | 57 | const tracker = createImportedTracker(); 58 | const relatedAssets1 = importedAssets(importedStat, ['main'], tracker); 59 | // use scripts and styles 60 | 61 | const relatedAssets2 = importedAssets(importedStat, ['home'], tracker); 62 | // render only new scripts and styles 63 | ``` 64 | 65 | # Client side API 66 | 67 | ## React bindings (for SSR) 68 | - `createImportedTracker()` - creates a duplication prevention tracker 69 | - `WebpackImportedProvider` - wires tracker down to React context 70 | - `WebpackImport` - chunk importer 71 | - `processImportedStyles` - helper for critical styles. 72 | ```js 73 | import {createImportedTracker, WebpackImportedProvider, WebpackImport} from "webpack-imported/react"; 74 | import importedStat from "build/imported.json"; 75 | 76 | const tracker = createImportedTracker();// this is optional, only needed if your render is multipart(head/body) 77 | 78 | 79 | 80 | 81 | ``` 82 | 83 | `WebpackImport` has many props: 84 | - [`preload`=false] - only preloads resources. If preload is set resources would be loaded via network, but not executed. 85 | Never use this option for the main chunk. 86 | - [`anonymous`=false] - should it be loaded as anonymous 87 | - [`async`=true] - loads scripts with `async` attribute, uses `deferred` in other case. 88 | - [`module`=false] - loads scripts with `module` attribute 89 | - [`critical-styles`=false] - enabled critical styles handling. No styles would be loaded or prefetched, 90 | but system will leave extra markup to prevent `MiniCssExtractPlugin` from adding them by itself. 91 | With this option enabled __you have to call__ `processImportedStyles` after the application starts to load the missing styles. 92 | 93 | 94 | # Related 95 | ### Get stats from webpack 96 | - [stats-webpack-plugin](https://github.com/unindented/stats-webpack-plugin) 97 | - [loadable components webpack plugin](https://github.com/smooth-code/loadable-components/tree/master/packages/webpack-plugin) 98 | 99 | ### Handle chunks dependencies 100 | - [webpack-flush-chunks](https://github.com/faceyspacey/webpack-flush-chunks) 101 | 102 | ### React Lazy Loading 103 | - [react-imported-component](https://github.com/theKashey/react-imported-component) 104 | 105 | ### CSS Critical extraction 106 | - [used-styles](https://github.com/theKashey/used-styles) 107 | 108 | # Licence 109 | MIT 110 | -------------------------------------------------------------------------------- /__tests__/index.tsx: -------------------------------------------------------------------------------- 1 | import '../src/index'; 2 | 3 | describe('Does nothing', () => { 4 | it('Just works', () => { 5 | expect(1).toEqual(1); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "../dist/es5/css/index.js", 4 | "jsnext:main": "../dist/es2015/css/index.js", 5 | "module": "../dist/es2015/css/index.js", 6 | "types": "../dist/es2015/css/index.d.ts" 7 | } 8 | -------------------------------------------------------------------------------- /example/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Component} from 'react'; 3 | import {GHCorner} from 'react-gh-corner'; 4 | import {AppWrapper} from './styled'; 5 | export interface AppState { 6 | 7 | } 8 | const repoUrl = 'https://github.com/zzarcon/'; 9 | export default class App extends Component <{}, AppState> { 10 | state: AppState = { 11 | 12 | } 13 | 14 | render() { 15 | return ( 16 | 17 | 18 | Example! 19 | 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /example/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theKashey/webpack-imported/2f288d0650a0d11797416241b40a40c9f3cbdbf8/example/assets/.gitkeep -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | 5 | ReactDOM.render(, document.getElementById('app')); 6 | -------------------------------------------------------------------------------- /example/styled.ts: -------------------------------------------------------------------------------- 1 | import styled, {injectGlobal} from 'styled-components'; 2 | 3 | injectGlobal` 4 | body { 5 | font-family: Helvetica; 6 | background-color: #D8D1F5; 7 | } 8 | 9 | * { 10 | box-sizing: content-box; 11 | } 12 | `; 13 | 14 | export const AppWrapper = styled.div` 15 | 16 | `; -------------------------------------------------------------------------------- /example/utils.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'react'; 2 | 3 | export class ToolboxApp extends Component { 4 | onCheckboxChange = (propName: any) => () => { 5 | const currentValue = (this.state as any)[propName]; 6 | this.setState({ [propName]: !currentValue } as any); 7 | } 8 | 9 | onFieldTextChange = (propName: any) => (e: any) => { 10 | const value = e.target.value; 11 | 12 | (this as any).setState({ 13 | [propName]: value 14 | }); 15 | } 16 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | modulePathIgnorePatterns: ['/dist'], 3 | preset: 'ts-jest', 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-imported", 3 | "version": "1.3.1", 4 | "description": "You chunks importing buddy", 5 | "main": "dist/es5/index.js", 6 | "module:es2019": "dist/es2019/index.js", 7 | "module": "dist/es2015/index.js", 8 | "types": "dist/es5/index.d.ts", 9 | "scripts": { 10 | "dev": "lib-builder dev", 11 | "test": "jest", 12 | "test:ci": "jest --runInBand --coverage", 13 | "build": "lib-builder build && yarn size:report", 14 | "release": "yarn build && yarn test", 15 | "size": "npx size-limit", 16 | "size:report": "npx size-limit --json > .size.json", 17 | "lint": "lib-builder lint", 18 | "format": "lib-builder format", 19 | "update": "lib-builder update", 20 | "prepublish": "yarn build && yarn changelog", 21 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 22 | "changelog:rewrite": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0" 23 | }, 24 | "sideEffects": false, 25 | "repository": "https://github.com/theKashey/webpack-imported/", 26 | "author": "theKashey ", 27 | "license": "MIT", 28 | "resolutions": { 29 | "typescript": "^5.0.0" 30 | }, 31 | "devDependencies": { 32 | "@size-limit/preset-small-lib": "^11.0.2", 33 | "size-limit": "^11.0.2", 34 | "@theuiteam/lib-builder": "^0.3.0", 35 | "@types/lodash": "^4.14.138", 36 | "webpack": "^5.28.5", 37 | "@types/node": "^18.0.0" 38 | }, 39 | "engines": { 40 | "node": ">=10" 41 | }, 42 | "files": [ 43 | "dist", 44 | "css", 45 | "react" 46 | ], 47 | "keywords": [ 48 | "webpack", 49 | "stat.json", 50 | "chunks", 51 | "codesplitting" 52 | ], 53 | "dependencies": { 54 | "kashe": "^1.0.3", 55 | "tslib": "^2.0.1" 56 | }, 57 | "peerDependencies": { 58 | "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", 59 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 60 | }, 61 | "size-limit": [ 62 | { 63 | "path": [ 64 | "dist/es2015/react/index.js" 65 | ], 66 | "ignore": [ 67 | "tslib", 68 | "react" 69 | ], 70 | "limit": "3 KB" 71 | } 72 | ], 73 | "peerDependenciesMeta": { 74 | "@types/react": { 75 | "optional": true 76 | } 77 | }, 78 | "husky": { 79 | "hooks": { 80 | "pre-commit": "lint-staged" 81 | } 82 | }, 83 | "lint-staged": { 84 | "*.{ts,tsx}": [ 85 | "prettier --write", 86 | "eslint --fix", 87 | "git add" 88 | ], 89 | "*.{js,css,json,md}": [ 90 | "prettier --write", 91 | "git add" 92 | ] 93 | }, 94 | "prettier": { 95 | "printWidth": 120, 96 | "trailingComma": "es5", 97 | "tabWidth": 2, 98 | "semi": true, 99 | "singleQuote": true 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "../dist/es5/react/index.js", 4 | "jsnext:main": "../dist/es2015/react/index.js", 5 | "module": "../dist/es2015/react/index.js", 6 | "types": "../dist/es2015/react/index.d.ts" 7 | } 8 | -------------------------------------------------------------------------------- /src/css/correct-order.ts: -------------------------------------------------------------------------------- 1 | import { ImportedStat } from '../types'; 2 | import { intentTheOrder } from '../utils/intent-the-order'; 3 | 4 | export const stylesheetCorrectOrder = ( 5 | /** 6 | * reference to `imported.stat` 7 | */ 8 | stats: ImportedStat 9 | ) => { 10 | return intentTheOrder(...Object.values(stats.chunks).map(({ load }) => load)); 11 | }; 12 | -------------------------------------------------------------------------------- /src/css/index.ts: -------------------------------------------------------------------------------- 1 | export { processImportedStyles } from './processImportedStyles'; 2 | export { stylesheetCorrectOrder } from './correct-order'; 3 | -------------------------------------------------------------------------------- /src/css/processImportedStyles.ts: -------------------------------------------------------------------------------- 1 | let pendingPromises: Array> = []; 2 | 3 | export const processImportedStyles = (check?: boolean): Promise | undefined => { 4 | const links = Array.from(document.getElementsByTagName('style')).filter((link) => link.dataset.deferredStyle); 5 | 6 | if (check && !links.length && !pendingPromises.length) { 7 | return undefined; 8 | } 9 | 10 | const operation = Promise.all( 11 | links.map((link) => { 12 | if (link.dataset.deferredStyle) { 13 | const style = document.createElement('link'); 14 | style.rel = 'stylesheet'; 15 | style.href = link.dataset.href!; 16 | style.type = 'text/css'; 17 | 18 | delete link.dataset.deferredStyle; 19 | 20 | return new Promise((resolve) => { 21 | style.addEventListener('load', resolve); 22 | link.parentNode!.insertBefore(style, link); 23 | link.parentNode!.removeChild(link); 24 | }); 25 | } 26 | 27 | return null; 28 | }) 29 | ); 30 | 31 | pendingPromises.push(operation); 32 | operation.then(() => { 33 | pendingPromises = pendingPromises.filter((op) => op !== operation); 34 | }); 35 | 36 | return Promise.all(pendingPromises); 37 | }; 38 | -------------------------------------------------------------------------------- /src/imported.spec.ts: -------------------------------------------------------------------------------- 1 | import { importAssets } from './imported'; 2 | import { ImportedStat } from './types'; 3 | 4 | describe('imported', () => { 5 | it('works', () => { 6 | const stats: ImportedStat = { 7 | config: { 8 | publicPath: 'publicPath', 9 | outputPath: 'outputPath', 10 | aliases: {}, 11 | }, 12 | chunks: { 13 | main: { 14 | load: [4], 15 | prefetch: [], 16 | preload: [], 17 | }, 18 | a: { 19 | load: [1, 2, 4], 20 | prefetch: [], 21 | preload: [], 22 | }, 23 | b: { 24 | load: [2, 3, 4], 25 | prefetch: [], 26 | preload: [], 27 | }, 28 | }, 29 | chunkMap: { 30 | 1: { js: ['1.js'] }, 31 | 2: { js: ['2.js'], css: ['2.css'] }, 32 | 3: { js: ['3.js'] }, 33 | 4: { js: ['4.js'], css: ['3.css', '4.css'] }, 34 | }, 35 | assets: [], 36 | moduleMap: {}, 37 | }; 38 | expect(importAssets(stats, ['main'])).toMatchInlineSnapshot(` 39 | Object { 40 | "raw": Object { 41 | "load": Object { 42 | "css": Array [ 43 | "3.css", 44 | "4.css", 45 | ], 46 | "js": Array [ 47 | "4.js", 48 | ], 49 | }, 50 | "prefetch": Object {}, 51 | "preload": Object {}, 52 | }, 53 | "scripts": Object { 54 | "load": Array [ 55 | "4.js", 56 | ], 57 | "prefetch": Array [], 58 | "preload": Array [], 59 | }, 60 | "styles": Object { 61 | "load": Array [ 62 | "3.css", 63 | "4.css", 64 | ], 65 | "prefetch": Array [], 66 | "preload": Array [], 67 | }, 68 | } 69 | `); 70 | expect(importAssets(stats, ['main', 'a', 'b'])).toMatchInlineSnapshot(` 71 | Object { 72 | "raw": Object { 73 | "load": Object { 74 | "css": Array [ 75 | "2.css", 76 | "3.css", 77 | "4.css", 78 | ], 79 | "js": Array [ 80 | "1.js", 81 | "2.js", 82 | "3.js", 83 | "4.js", 84 | ], 85 | }, 86 | "prefetch": Object {}, 87 | "preload": Object {}, 88 | }, 89 | "scripts": Object { 90 | "load": Array [ 91 | "1.js", 92 | "2.js", 93 | "3.js", 94 | "4.js", 95 | ], 96 | "prefetch": Array [], 97 | "preload": Array [], 98 | }, 99 | "styles": Object { 100 | "load": Array [ 101 | "2.css", 102 | "3.css", 103 | "4.css", 104 | ], 105 | "prefetch": Array [], 106 | "preload": Array [], 107 | }, 108 | } 109 | `); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/imported.ts: -------------------------------------------------------------------------------- 1 | import { kashe } from 'kashe'; 2 | 3 | import { createImportedTracker } from './tracker'; 4 | import { 5 | ChunkAsset, 6 | ChunkRef, 7 | ChunkRefs, 8 | Chunks, 9 | ImportedStat, 10 | ImportedTracker, 11 | RelatedAssets, 12 | RelatedImported, 13 | RelatedImportedPack, 14 | } from './types'; 15 | import { intentTheOrder } from './utils/intent-the-order'; 16 | 17 | 18 | const relatedToChunks = ( 19 | importedStat: ImportedStat, 20 | tracker: ImportedTracker, 21 | initialChunkNames: string | string[] 22 | ): ImportedTracker => { 23 | const load: ChunkRefs = []; 24 | const preload: ChunkRefs = []; 25 | const prefetch: ChunkRefs = []; 26 | 27 | const chunkNames = Array.isArray(initialChunkNames) ? initialChunkNames : [initialChunkNames]; 28 | 29 | chunkNames.forEach((chunkName) => { 30 | const chunk = importedStat.chunks[chunkName]; 31 | if (!chunk) { 32 | throw new Error(`imported-stats: chunk "${chunkName}" was not found in stats.`); 33 | } 34 | 35 | chunk.load.forEach((chunkId) => { 36 | if (tracker.load.indexOf(chunkId) < 0) { 37 | tracker.load.push(chunkId); 38 | load.push(chunkId); 39 | } 40 | }); 41 | 42 | chunk.preload.forEach((chunkId) => { 43 | if (tracker.preload.indexOf(chunkId) < 0) { 44 | tracker.preload.push(chunkId); 45 | preload.push(chunkId); 46 | } 47 | }); 48 | chunk.prefetch.forEach((chunkId) => { 49 | if (tracker.prefetch.indexOf(chunkId) < 0) { 50 | tracker.prefetch.push(chunkId); 51 | prefetch.push(chunkId); 52 | } 53 | }); 54 | }); 55 | 56 | return { 57 | load, 58 | preload, 59 | prefetch, 60 | }; 61 | }; 62 | 63 | const flattenType = (types: ChunkAsset[]): Record => { 64 | const ret: Record = {}; 65 | types.forEach((chunk) => { 66 | Object.keys(chunk).forEach((type) => { 67 | if (!(type in ret)) { 68 | ret[type] = []; 69 | } 70 | ret[type].push(...chunk[type]); 71 | }); 72 | }); 73 | 74 | return ret; 75 | }; 76 | 77 | const importedScripts = (importedStep: RelatedImported): RelatedAssets => { 78 | const { load, preload, prefetch } = importedStep; 79 | return { 80 | load: load.js || [], 81 | preload: preload.js || [], 82 | prefetch: prefetch.js || [], 83 | }; 84 | }; 85 | 86 | const importedStyles = (importedStep: RelatedImported): RelatedAssets => { 87 | const { load, preload, prefetch } = importedStep; 88 | 89 | return { 90 | load: load.css || [], 91 | preload: preload.css || [], 92 | prefetch: prefetch.css || [], 93 | }; 94 | }; 95 | 96 | type OrderMap = Map; 97 | const cachedOrder = kashe( 98 | (chunks: Chunks): OrderMap => { 99 | const order: ChunkRefs = intentTheOrder(...Object.values(chunks).map(({ load }) => load)); 100 | const map: OrderMap = new Map(); 101 | order.forEach((x, index) => map.set(x, index)); 102 | return map; 103 | } 104 | ); 105 | 106 | const correctOrder = (order: OrderMap, load: ChunkRefs) => { 107 | load.sort((a, b) => order.get(a)! - order.get(b)!); 108 | return load; 109 | }; 110 | 111 | export const importAssets = ( 112 | importedStat: ImportedStat, 113 | chunkNames: string | string[], 114 | tracker: ImportedTracker = createImportedTracker() 115 | ): RelatedImportedPack => { 116 | const { load, preload, prefetch } = relatedToChunks(importedStat, tracker, chunkNames); 117 | const order = cachedOrder(importedStat.chunks); 118 | const raw: RelatedImported = { 119 | load: flattenType(correctOrder(order, load).map((chunkId) => importedStat.chunkMap[chunkId])), 120 | preload: flattenType(correctOrder(order, preload).map((chunkId) => importedStat.chunkMap[chunkId])), 121 | prefetch: flattenType(correctOrder(order, prefetch).map((chunkId) => importedStat.chunkMap[chunkId])), 122 | }; 123 | 124 | return { 125 | raw, 126 | scripts: importedScripts(raw), 127 | styles: importedStyles(raw), 128 | }; 129 | }; 130 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { ImportedPlugin, importStats } from './plugin'; 2 | export { importAssets } from './imported'; 3 | export type { ImportedStat } from './types'; 4 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs'; 2 | import { extname, relative } from 'path'; 3 | 4 | import type { Compiler, Compilation, sources, StatsModule, StatsCompilation } from 'webpack'; 5 | 6 | import type { ChunkMap, Chunks, ImportedStat, Asset } from './types'; 7 | 8 | const merge = require('lodash/merge'); 9 | 10 | type WebStats = Required< 11 | Pick 12 | >; 13 | 14 | const moduleToChunks = ({ chunks }: WebStats) => { 15 | const manifest: Record = {}; 16 | chunks.forEach(({ id, modules }) => { 17 | if (modules) { 18 | modules.forEach((module: StatsModule) => { 19 | if (module.id && id) manifest[module.id] = id; 20 | }); 21 | } 22 | }); 23 | 24 | return manifest; 25 | }; 26 | 27 | const getAssetType = (name: string) => extname(name).substr(1); 28 | 29 | const extractPrefetch = (prefetch: any[] = []): number[] => 30 | Array.from( 31 | prefetch 32 | .map(({ chunks }) => chunks as number[]) 33 | .reduce((acc, chunks) => { 34 | chunks.forEach((chunk) => acc.add(chunk)); 35 | 36 | return acc; 37 | }, new Set()) 38 | ); 39 | 40 | const mapChunkNumbers = ({ assets }: WebStats): ChunkMap => 41 | assets.reduce((acc, { name, chunks }) => { 42 | chunks?.forEach((chunk) => { 43 | acc[chunk] = acc[chunk] || {}; 44 | const type = getAssetType(name); 45 | acc[chunk][type] = acc[chunk][type] || []; 46 | acc[chunk][type].push(name); 47 | }); 48 | 49 | return acc; 50 | }, {} as ChunkMap); 51 | 52 | const getAssets = ({ assets }: WebStats): Asset[] => 53 | assets!.map(({ name, size }) => ({ name, size, type: getAssetType(name) })); 54 | 55 | const getChunks = ({ namedChunkGroups }: WebStats): Chunks => 56 | Object.keys(namedChunkGroups).reduce((acc, key) => { 57 | const { chunks, children } = namedChunkGroups[key]; 58 | acc[key] = { 59 | load: chunks || [], 60 | preload: children?.preload ? extractPrefetch(children.preload) : [], 61 | prefetch: children?.prefetch ? extractPrefetch(children.prefetch) : [], 62 | }; 63 | 64 | return acc; 65 | }, {} as Chunks); 66 | 67 | const resolveAliases = (cwd: string, aliases: Record): Record => { 68 | return Object.keys(aliases).reduce((acc, key) => { 69 | const alias = aliases[key]; 70 | const paths = Array.isArray(alias) ? alias.map((aliasPath) => relative(cwd, aliasPath)) : relative(cwd, alias); 71 | return { ...acc, [key]: paths }; 72 | }, {}); 73 | }; 74 | 75 | export const importStats = (stats: WebStats, extraProps: Record = {}): ImportedStat => { 76 | const cwd = process.cwd(); 77 | const { publicPath, outputPath } = stats; 78 | 79 | return { 80 | config: { 81 | publicPath, 82 | outputPath: relative(cwd, outputPath), 83 | aliases: {}, 84 | ...extraProps, 85 | }, 86 | 87 | chunks: getChunks(stats), 88 | chunkMap: mapChunkNumbers(stats), 89 | assets: getAssets(stats), 90 | moduleMap: moduleToChunks(stats), 91 | }; 92 | }; 93 | 94 | interface Options { 95 | /** 96 | * bypasses webpack and saves file directly to the FS 97 | */ 98 | saveToFile?: string; 99 | } 100 | 101 | /** 102 | * Webpack plugin 103 | */ 104 | export class ImportedPlugin { 105 | constructor(private output: string, private options: Options = {}, private cache = {}) {} 106 | 107 | handleEmit = (compilation: Compilation) => { 108 | const stats = compilation.getStats().toJson({ 109 | hash: true, 110 | publicPath: true, 111 | assets: true, 112 | chunks: true, 113 | modules: false, 114 | source: false, 115 | errorDetails: false, 116 | timings: false, 117 | }) as WebStats; 118 | 119 | const cwd = process.cwd(); 120 | // not quite yet 121 | // const modules = compilation.options.resolve.modules; 122 | const aliases = resolveAliases(cwd, (compilation as any).options.resolve!.alias || {}); 123 | // const {publicPath, outputPath} = stats; 124 | 125 | const result: ImportedStat = importStats(stats, { aliases }); 126 | 127 | if (this.cache) { 128 | this.cache = merge(this.cache, result); 129 | } 130 | 131 | const stringResult = JSON.stringify(result, null, 2); 132 | 133 | if (this.options.saveToFile) { 134 | writeFileSync(this.options.saveToFile, stringResult); 135 | } 136 | 137 | if (this.output) { 138 | return { 139 | source() { 140 | return stringResult; 141 | }, 142 | size() { 143 | return stringResult.length; 144 | }, 145 | }; 146 | } 147 | 148 | return null; 149 | }; 150 | 151 | apply(compiler: Compiler) { 152 | const version = 'jsonpFunction' in compiler.options.output ? 4 : 5; 153 | 154 | if (version === 4) { 155 | compiler.hooks.emit.tap('ImportedPlugin', (compilation) => { 156 | const asset = this.handleEmit(compilation); 157 | if (asset) { 158 | compilation.assets[this.output] = asset as sources.Source; 159 | } 160 | }); 161 | } else { 162 | compiler.hooks.make.tap('ImportedPlugin', (compilation) => { 163 | compilation.hooks.processAssets.tap( 164 | { 165 | name: 'ImportedPlugin', 166 | stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT, 167 | }, 168 | () => { 169 | const asset = this.handleEmit(compilation); 170 | if (asset) { 171 | compilation.emitAsset(this.output, asset as sources.Source); 172 | } 173 | } 174 | ); 175 | }); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/react/Atoms.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | type Omit = Pick>; 4 | 5 | export interface UnknownLink { 6 | href: string; 7 | as: 'script' | 'style' | 'font'; 8 | type: string; 9 | anonymous?: boolean; 10 | } 11 | 12 | export type KnownLink = Omit; 13 | export type KnownTypedLink = Omit; 14 | 15 | const LinkPreload: React.FC = ({ href, as, type, anonymous, ...rest }) => ( 16 | 17 | ); 18 | const LinkPrefetch: React.FC = ({ href, as, type, anonymous, ...rest }) => ( 19 | 20 | ); 21 | export const PreloadFont: React.FC = ({ href, type, anonymous, ...rest }) => ( 22 | 23 | ); 24 | export const PrefetchFont: React.FC = ({ href, type, anonymous, ...rest }) => ( 25 | 26 | ); 27 | export const PrefetchStyle: React.FC = ({ href, ...rest }) => ( 28 | 29 | ); 30 | export const PreloadStyle: React.FC = ({ href, ...rest }) => ( 31 | 32 | ); 33 | export const PrefetchScript: React.FC = ({ href, ...rest }) => ( 34 | 35 | ); 36 | export const PreloadScript: React.FC = ({ href, ...rest }) => ( 37 | 38 | ); 39 | -------------------------------------------------------------------------------- /src/react/Chunk.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useContext } from 'react'; 3 | 4 | import { importAssets } from '../imported'; 5 | import { ImportedStat } from '../types'; 6 | import { PrefetchScript, PrefetchStyle, PreloadScript, PreloadStyle } from './Atoms'; 7 | import { LoadCriticalStyle, LoadScript, LoadStyle } from './Load'; 8 | import { PrefetchChunkCollectorContext } from './context'; 9 | 10 | export interface WebpackImportProps { 11 | /** 12 | * reference to `imported.stat` 13 | */ 14 | stats: ImportedStat; 15 | /** 16 | * list of chunks to load 17 | */ 18 | chunks: string | string[]; 19 | /** 20 | * should prefetch or preload be used 21 | */ 22 | scriptsHint?: 'prefetch' | 'preload'; 23 | /** 24 | * should scripts be loaded as anonymous 25 | */ 26 | anonymous?: boolean; 27 | /** 28 | * should scripts be loaded as async (default is defer) 29 | */ 30 | async?: boolean; 31 | /** 32 | * should scripts be loaded as ESM modules 33 | */ 34 | module?: boolean; 35 | /** 36 | * should found CSS files be considered as critical and NOT loaded 37 | * and if yes - should they be prefetched or preloaded (or nothing) 38 | */ 39 | criticalCSS?: boolean | 'prefetch' | 'preload'; 40 | /** 41 | * public path for all assets 42 | */ 43 | publicPath?: string; 44 | } 45 | 46 | /** 47 | * Preloads all given chunks 48 | * @example 49 | * = 0} 53 | * scriptsHint={priority >= 1 ? 'preload' : 'prefetch'} 54 | * criticalCSS="prefetch" 55 | * publicPath={`${CDN}${data.config.publicPath}${mode}/`} 56 | * /> 57 | */ 58 | export const WebpackImport: React.FC = ({ 59 | stats, 60 | chunks, 61 | scriptsHint, 62 | criticalCSS, 63 | anonymous, 64 | async = true, 65 | module, 66 | publicPath = stats.config.publicPath, 67 | }) => { 68 | const tracker = useContext(PrefetchChunkCollectorContext); 69 | const { scripts, styles } = importAssets(stats, chunks, tracker); 70 | 71 | return ( 72 | <> 73 | {scripts.load.map((asset) => ( 74 | 75 | {scriptsHint === 'prefetch' && } 76 | {scriptsHint === 'preload' && } 77 | 78 | 79 | ))} 80 | {scripts.preload.map((asset) => ( 81 | 82 | ))} 83 | {scripts.prefetch.map((asset) => ( 84 | 85 | ))} 86 | 87 | {styles.load.map((asset) => ( 88 | 89 | {criticalCSS ? ( 90 | <> 91 | {criticalCSS === 'prefetch' && } 92 | {criticalCSS === 'preload' && } 93 | 94 | 95 | ) : ( 96 | 97 | )} 98 | 99 | ))} 100 | 101 | ); 102 | }; 103 | 104 | export interface WebpackPreloadProps { 105 | /** 106 | * reference to `imported.stat` 107 | */ 108 | stats: ImportedStat; 109 | /** 110 | * list of chunks to load 111 | */ 112 | chunks: string | string[]; 113 | /** 114 | * should prefetch or preload be used 115 | */ 116 | scriptsHint: 'prefetch' | 'preload'; 117 | /** 118 | * should prefetch or preload be used 119 | */ 120 | stylesHint: 'prefetch' | 'preload'; 121 | /** 122 | * suppresses styles for the initial load as they are expected to be a part of critical CSS 123 | * styles will be placed in the correct order by calling {@link processImportedStyles} 124 | */ 125 | criticalCSS?: boolean; 126 | /** 127 | * should scripts be loaded as anonymous 128 | */ 129 | anonymous?: boolean; 130 | /** 131 | * public path for all assets 132 | */ 133 | publicPath?: string; 134 | } 135 | 136 | /** 137 | * Preloads(or prefetches) all given chunks 138 | * 139 | * @example 140 | * 147 | */ 148 | export const WebpackPreload: React.FC = ({ 149 | stats, 150 | chunks, 151 | scriptsHint, 152 | stylesHint, 153 | criticalCSS, 154 | anonymous, 155 | publicPath = stats.config.publicPath, 156 | }) => { 157 | const tracker = useContext(PrefetchChunkCollectorContext); 158 | const { scripts, styles } = importAssets(stats, chunks, tracker); 159 | 160 | return ( 161 | <> 162 | {scripts.load.map((asset) => ( 163 | 164 | {scriptsHint === 'prefetch' ? ( 165 | 166 | ) : ( 167 | 168 | )} 169 | 170 | ))} 171 | {styles.load.map((asset) => ( 172 | 173 | {stylesHint === 'prefetch' ? ( 174 | 175 | ) : ( 176 | 177 | )} 178 | {criticalCSS && } 179 | 180 | ))} 181 | 182 | ); 183 | }; 184 | -------------------------------------------------------------------------------- /src/react/Load.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface KnownScript { 4 | href: string; 5 | anonymous?: boolean; 6 | async?: boolean; 7 | module?: boolean; 8 | } 9 | 10 | export const LoadScript: React.FC = ({ href, anonymous, async = true, module }) => ( 11 |