├── .editorconfig ├── .eslintignore ├── .gitattributes ├── .gitignore ├── .npmrc ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── globals.d.ts ├── package.json ├── src ├── Loader.js └── Plugin.js ├── test ├── fixtures.spec.js └── fixtures │ ├── copyshader.js │ ├── effectcomposer.js │ ├── renderpass.js │ ├── ts-renderpass.ts │ ├── ts-with-examples.ts │ ├── with-examples.js │ ├── without-examples.js │ └── wrong-examples.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | tab_width = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | 10 | [package.json] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures/*.js 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | 3 | .editorconfig text 4 | .eslintignore text 5 | .gitattributes text 6 | .gitignore text 7 | .npmrc text 8 | LICENSE text 9 | *.md text 10 | *.js text 11 | *.ts text 12 | *.json text 13 | *.yml text 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #-----------------------------------------------------------------------------# 3 | # Ignore everything by default: 4 | #-----------------------------------------------------------------------------# 5 | 6 | /* 7 | /*/ 8 | 9 | 10 | #-----------------------------------------------------------------------------# 11 | # But allow those files and folders: 12 | #-----------------------------------------------------------------------------# 13 | 14 | !/LICENSE 15 | !/README.md 16 | !/.editorconfig 17 | !/.eslintignore 18 | !/.gitattributes 19 | !/.gitignore 20 | !/.npmrc 21 | !/.travis.yml 22 | !/.vscode 23 | !/src 24 | !/test 25 | !/globals.d.ts 26 | !/package.json 27 | !/tsconfig.json 28 | 29 | #-----------------------------------------------------------------------------# 30 | # But make sure to ignore those regardless: 31 | #-----------------------------------------------------------------------------# 32 | 33 | .DS_Store 34 | Thumbs.db 35 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | loglevel=silent 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | - 'lts/*' 5 | sudo: required 6 | before_script: 7 | - "export DISPLAY=:99.0" 8 | - "sh -e /etc/init.d/xvfb start" 9 | - sleep 3 10 | addons: 11 | chrome: stable 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "EditorConfig.editorconfig", 4 | "dbaeumer.vscode-eslint", 5 | "wallabyjs.wallaby-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.options": { 3 | "ignorePath": ".eslintignore" 4 | }, 5 | "files.exclude": { 6 | "*.lnk": true, 7 | "*.url": true, 8 | "LICENSE": true, 9 | ".chrome": true, 10 | ".editorconfig": true, 11 | ".gitignore": true, 12 | ".gitattributes": true, 13 | ".npmrc": true, 14 | "node_modules": true, 15 | "package-lock.json": true, 16 | "dist": true, 17 | "temp": true 18 | }, 19 | "search.exclude": { 20 | "*.lnk": true, 21 | "*.url": true, 22 | "LICENSE": true, 23 | ".chrome": true, 24 | ".editorconfig": true, 25 | ".gitignore": true, 26 | ".gitattributes": true, 27 | ".npmrc": true, 28 | "node_modules": true, 29 | "package-lock.json": true, 30 | "dist": true, 31 | "temp": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 wildpeaks 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webpack Plugin: Three 2 | 3 | Webpack plugin to use the additional **[Three.js](https://threejs.org/) "examples" classes that aren't ES Modules**, 4 | such as [THREE.OrbitControls](https://threejs.org/docs/index.html#examples/controls/OrbitControls). 5 | 6 | 7 | ## Usage 8 | 9 | Install packages `three` and `@wildpeaks/three-webpack-plugin`: 10 | 11 | npm install --save-dev three @wildpeaks/three-webpack-plugin 12 | 13 | Add the plugin in your `webpack.config.js`: 14 | ````js 15 | const ThreeWebpackPlugin = require('@wildpeaks/three-webpack-plugin'); 16 | 17 | module.exports = { 18 | //... 19 | plugins: [ 20 | //... 21 | new ThreeWebpackPlugin() 22 | ] 23 | }; 24 | ```` 25 | 26 | You can now import the classes in your application: 27 | ````js 28 | 29 | // Import from "three" for core classes 30 | import {Scene, WebGLRenderer} from 'three'; 31 | 32 | // Import from "three/examples/js" for addditional classes 33 | import {OrbitControls} from 'three/examples/js/controls/OrbitControls'; 34 | 35 | // Use the imported classes 36 | const scene = new Scene(); 37 | const renderer = new WebGLRenderer(); 38 | const controls = new OrbitControls(); 39 | ```` 40 | 41 | 42 | ## Typescript 43 | 44 | Until definitions are integrated directly in `@types/three`, add a file `globals.d.ts` 45 | at the root of your project to specify the types of the imports, e.g.: 46 | 47 | ````ts 48 | declare module 'three/examples/js/controls/OrbitControls' { 49 | export const OrbitControls: typeof THREE.OrbitControls; 50 | } 51 | ```` 52 | 53 | Note that this is *not* required for compiling to JS, it improves Intellisense in your code editor. 54 | 55 | -------------------------------------------------------------------------------- /globals.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module 'three/examples/js/controls/OrbitControls' { 3 | export const OrbitControls: typeof THREE.OrbitControls; 4 | } 5 | 6 | declare module 'three/examples/js/loaders/OBJLoader' { 7 | export const OBJLoader: typeof THREE.OBJLoader; 8 | } 9 | 10 | declare module 'three/examples/js/postprocessing/EffectComposer' { 11 | export const EffectComposer: typeof THREE.EffectComposer; 12 | export const Pass: typeof THREE.Pass; 13 | } 14 | 15 | declare module 'three/examples/js/postprocessing/RenderPass' { 16 | export const RenderPass: typeof THREE.RenderPass; 17 | } 18 | 19 | declare module 'three/examples/js/shaders/CopyShader' { 20 | export const CopyShader: typeof THREE.CopyShader; 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wildpeaks/three-webpack-plugin", 3 | "version": "2.0.0", 4 | "description": "Webpack plugin to use Three.js \"examples\" classes that aren't ES Modules", 5 | "author": "Cecile Muller", 6 | "license": "MIT", 7 | "eslintConfig": { 8 | "extends": "@wildpeaks/commonjs" 9 | }, 10 | "main": "src/Plugin.js", 11 | "files": [ 12 | "src" 13 | ], 14 | "scripts": { 15 | "test": "jasmine test/*.spec.js" 16 | }, 17 | "repository": "https://github.com/wildpeaks/package-three-webpack-plugin", 18 | "keywords": [ 19 | "wildpeaks", 20 | "webpack", 21 | "three.js" 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/wildpeaks/package-three-webpack-plugin/issues" 25 | }, 26 | "homepage": "https://github.com/wildpeaks/package-three-webpack-plugin#readme", 27 | "dependencies": { 28 | "loader-utils": "1.1.0" 29 | }, 30 | "devDependencies": { 31 | "@types/three": "0.92.14", 32 | "@wildpeaks/eslint-config-commonjs": "5.1.0", 33 | "eslint": "5.14.0", 34 | "express": "4.16.3", 35 | "html-webpack-plugin": "3.2.0", 36 | "jasmine": "3.1.0", 37 | "puppeteer": "1.6.2", 38 | "rimraf": "2.6.2", 39 | "three": "0.94.0", 40 | "ts-loader": "4.4.1", 41 | "typescript": "2.9.2", 42 | "webpack": "4.21.0" 43 | }, 44 | "peerDependencies": { 45 | "three": "*", 46 | "webpack": "*" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {getOptions} = require('loader-utils'); 3 | 4 | 5 | function Loader(source){ 6 | const options = getOptions(this); 7 | let code = `'use strict';\nvar THREE = require('three');\n`; 8 | 9 | const optionsRequires = options.requires; 10 | if (Array.isArray(optionsRequires)){ 11 | code += optionsRequires.map(moduleId => `require(${JSON.stringify(moduleId)});`).join('\n'); 12 | } 13 | 14 | code += `${source}\n`; 15 | 16 | const optionsExports = options.exports; 17 | if ((typeof optionsExports === 'object') && (optionsExports !== null)){ 18 | const lines = []; 19 | for (const id in optionsExports){ 20 | const exportId = optionsExports[id]; 21 | lines.push(`${JSON.stringify(id)}: ${exportId}`); 22 | } 23 | code += 'module.exports = {' + lines.join(',') + '}'; // eslint-disable-line prefer-template 24 | } 25 | 26 | return code; 27 | } 28 | 29 | 30 | module.exports = Loader; 31 | -------------------------------------------------------------------------------- /src/Plugin.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | const PLUGIN_ID = 'wildpeaks-three'; 4 | const Loader = require.resolve('./Loader'); 5 | 6 | class Plugin { 7 | apply(compiler){ // eslint-disable-line class-methods-use-this 8 | compiler.hooks.normalModuleFactory.tap(PLUGIN_ID, normalModuleFactory => { 9 | normalModuleFactory.hooks.afterResolve.tap(PLUGIN_ID, data => { 10 | const {loaders, rawRequest} = data; 11 | if (rawRequest.startsWith('three/examples/js/')){ 12 | const exportId = rawRequest.split('/').pop(); 13 | if (rawRequest === 'three/examples/js/postprocessing/EffectComposer'){ 14 | loaders.push({ 15 | loader: Loader, 16 | options: { 17 | exports: { 18 | EffectComposer: 'THREE.EffectComposer', 19 | Pass: 'THREE.Pass' 20 | } 21 | } 22 | }); 23 | } else if (rawRequest.startsWith('three/examples/js/postprocessing/')){ 24 | loaders.push({ 25 | loader: Loader, 26 | options: { 27 | requires: [ 28 | 'three/examples/js/postprocessing/EffectComposer' 29 | ], 30 | exports: { 31 | [exportId]: `THREE.${exportId}` 32 | } 33 | } 34 | }); 35 | } else { 36 | loaders.push({ 37 | loader: Loader, 38 | options: { 39 | exports: { 40 | [exportId]: `THREE.${exportId}` 41 | } 42 | } 43 | }); 44 | } 45 | } 46 | return data; 47 | }); 48 | }); 49 | } 50 | } 51 | 52 | module.exports = Plugin; 53 | -------------------------------------------------------------------------------- /test/fixtures.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jasmine */ 2 | 'use strict'; 3 | const {mkdirSync, readdirSync} = require('fs'); 4 | const {join} = require('path'); 5 | const express = require('express'); 6 | const puppeteer = require('puppeteer'); 7 | const rimraf = require('rimraf'); 8 | const webpack = require('webpack'); 9 | const ModuleNotFoundError = require('webpack/lib/ModuleNotFoundError'); 10 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 11 | const Plugin = require('../src/Plugin'); 12 | 13 | const rootFolder = join(__dirname, 'fixtures'); 14 | const outputFolder = join(__dirname, '../temp'); 15 | let app; 16 | let server; 17 | 18 | 19 | /** 20 | * @param {webpack.Configuration} config 21 | */ 22 | function compile(config){ 23 | return new Promise((resolve, reject) => { 24 | webpack(config, (err, stats) => { 25 | if (err){ 26 | reject(err); 27 | } else { 28 | resolve(stats); 29 | } 30 | }); 31 | }); 32 | } 33 | 34 | 35 | /** 36 | * @param {String} entry 37 | * @param {Boolean} expectError 38 | * @param {String} expectText 39 | */ 40 | async function testFixture(entry, expectError, expectText){ 41 | const config = { 42 | target: 'web', 43 | devtool: false, 44 | mode: 'development', 45 | context: rootFolder, 46 | entry: { 47 | application: entry 48 | }, 49 | output: { 50 | publicPath: '', 51 | path: outputFolder, 52 | filename: '[name].js' 53 | }, 54 | module: { 55 | rules: entry.endsWith('.ts') ? [ 56 | { 57 | test: /\.(ts|js)$/, 58 | use: [ 59 | { 60 | loader: 'ts-loader', 61 | options: { 62 | transpileOnly: true 63 | } 64 | } 65 | ] 66 | } 67 | ] : [] 68 | }, 69 | plugins: [ 70 | new HtmlWebpackPlugin(), 71 | new Plugin() 72 | ] 73 | }; 74 | 75 | const stats = await compile(config); 76 | const {errors} = stats.compilation; 77 | if (expectError){ 78 | expect(errors).not.toEqual([], 'Has errors'); 79 | } else { 80 | expect(errors).toEqual([], 'No errors'); 81 | 82 | const actualFiles = readdirSync(outputFolder); 83 | expect(actualFiles.sort()).toEqual(['application.js', 'index.html']); 84 | 85 | const browser = await puppeteer.launch(); 86 | try { 87 | const page = await browser.newPage(); 88 | await page.goto('http://localhost:8888/'); 89 | const found = await page.evaluate(() => { 90 | /* global document */ 91 | const el = document.getElementById('fixture'); 92 | if (el === null){ 93 | return '#fixture not found'; 94 | } 95 | return String(el.innerText); 96 | }); 97 | expect(found).toBe(expectText, 'DOM tests'); 98 | } finally { 99 | await browser.close(); 100 | } 101 | } 102 | 103 | return errors; 104 | } 105 | 106 | 107 | beforeAll(() => { 108 | app = express(); 109 | app.use(express.static(outputFolder)); 110 | server = app.listen(8888); 111 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; 112 | }); 113 | 114 | afterAll(done => { 115 | server.close(() => { 116 | done(); 117 | }); 118 | }); 119 | 120 | beforeEach(done => { 121 | rimraf(outputFolder, () => { 122 | mkdirSync(outputFolder); 123 | done(); 124 | }); 125 | }); 126 | 127 | 128 | it('Without examples', async() => { 129 | await testFixture('./without-examples.js', false, 'function'); 130 | }); 131 | 132 | it('With examples', async() => { 133 | await testFixture('./with-examples.js', false, 'function function function'); 134 | }); 135 | 136 | it('EffectComposer', async() => { 137 | await testFixture('./effectcomposer.js', false, 'function function function'); 138 | }); 139 | 140 | it('RenderPass', async() => { 141 | await testFixture('./renderpass.js', false, 'function function'); 142 | }); 143 | 144 | it('CopyShader', async() => { 145 | await testFixture('./copyshader.js', false, 'object object string string'); 146 | }); 147 | 148 | it('Invalid path', async() => { 149 | const errors = await testFixture('./wrong-examples.js', true, ''); 150 | expect(errors.length).toBe(1, 'Has one error'); 151 | expect(errors[0] instanceof ModuleNotFoundError).toBe(true, 'The error is a ModuleNotFoundError'); 152 | }); 153 | 154 | it('Typescript: With examples', async() => { 155 | await testFixture('./ts-with-examples.ts', false, 'function function function'); 156 | }); 157 | 158 | it('Typescript: RenderPass', async() => { 159 | await testFixture('./ts-renderpass.ts', false, 'function function'); 160 | }); 161 | -------------------------------------------------------------------------------- /test/fixtures/copyshader.js: -------------------------------------------------------------------------------- 1 | import {CopyShader} from 'three/examples/js/shaders/CopyShader'; 2 | 3 | const $div = document.createElement('div'); 4 | $div.setAttribute('id', 'fixture'); 5 | $div.innerText = `${typeof CopyShader} ${typeof CopyShader.uniforms} ${typeof CopyShader.vertexShader} ${typeof CopyShader.fragmentShader}`; 6 | document.body.appendChild($div); 7 | -------------------------------------------------------------------------------- /test/fixtures/effectcomposer.js: -------------------------------------------------------------------------------- 1 | import {Vector3} from 'three'; 2 | import {EffectComposer, Pass} from 'three/examples/js/postprocessing/EffectComposer'; 3 | 4 | const $div = document.createElement('div'); 5 | $div.setAttribute('id', 'fixture'); 6 | $div.innerText = ` ${typeof Vector3} ${typeof EffectComposer} ${typeof Pass}`; 7 | document.body.appendChild($div); 8 | -------------------------------------------------------------------------------- /test/fixtures/renderpass.js: -------------------------------------------------------------------------------- 1 | import {Vector3} from 'three'; 2 | import {RenderPass} from 'three/examples/js/postprocessing/RenderPass'; 3 | 4 | const $div = document.createElement('div'); 5 | $div.setAttribute('id', 'fixture'); 6 | $div.innerText = ` ${typeof Vector3} ${typeof RenderPass}`; 7 | document.body.appendChild($div); 8 | -------------------------------------------------------------------------------- /test/fixtures/ts-renderpass.ts: -------------------------------------------------------------------------------- 1 | import {Vector3} from 'three'; 2 | import {RenderPass} from 'three/examples/js/postprocessing/RenderPass'; 3 | 4 | const $div: HTMLDivElement = document.createElement('div'); 5 | $div.setAttribute('id', 'fixture'); 6 | $div.innerText = ` ${typeof Vector3} ${typeof RenderPass}`; 7 | document.body.appendChild($div); 8 | -------------------------------------------------------------------------------- /test/fixtures/ts-with-examples.ts: -------------------------------------------------------------------------------- 1 | import {Vector3} from 'three'; 2 | import {OrbitControls} from 'three/examples/js/controls/OrbitControls'; 3 | import {OBJLoader} from 'three/examples/js/loaders/OBJLoader'; 4 | 5 | const $div: HTMLDivElement = document.createElement('div'); 6 | $div.setAttribute('id', 'fixture'); 7 | $div.innerText = `${typeof Vector3} ${typeof OBJLoader} ${typeof OrbitControls}`; 8 | document.body.appendChild($div); 9 | -------------------------------------------------------------------------------- /test/fixtures/with-examples.js: -------------------------------------------------------------------------------- 1 | import {Vector3} from 'three'; 2 | import {OrbitControls} from 'three/examples/js/controls/OrbitControls'; 3 | import {OBJLoader} from 'three/examples/js/loaders/OBJLoader'; 4 | 5 | const $div = document.createElement('div'); 6 | $div.setAttribute('id', 'fixture'); 7 | $div.innerText = `${typeof Vector3} ${typeof OBJLoader} ${typeof OrbitControls}`; 8 | document.body.appendChild($div); 9 | -------------------------------------------------------------------------------- /test/fixtures/without-examples.js: -------------------------------------------------------------------------------- 1 | import {Vector3} from 'three'; 2 | 3 | const $div = document.createElement('div'); 4 | $div.setAttribute('id', 'fixture'); 5 | $div.innerText = typeof Vector3; 6 | document.body.appendChild($div); 7 | -------------------------------------------------------------------------------- /test/fixtures/wrong-examples.js: -------------------------------------------------------------------------------- 1 | import {Vector3} from 'three'; 2 | import OBJLoader from 'three/examples/js/fake/OBJLoader'; 3 | 4 | const $div = document.createElement('div'); 5 | $div.setAttribute('id', 'fixture'); 6 | $div.innerText = `${typeof Vector3} ${typeof OBJLoader}`; 7 | document.body.appendChild($div); 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "newLine": "LF", 5 | "alwaysStrict": true, 6 | "noEmitOnError": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "removeComments": true, 15 | "sourceMap": true, 16 | "module": "esnext", 17 | "target": "es5", 18 | "lib": ["es2017", "dom"], 19 | "allowJs": true 20 | } 21 | } 22 | --------------------------------------------------------------------------------