mrdoob / three.js

JavaScript 3D Library.
https://threejs.org/
MIT License
101.83k stars 35.31k forks source link

Importing examples jsm modules causes bundlers to bundle three.js source code twice #17482

Closed adrian-delgado closed 3 years ago

adrian-delgado commented 5 years ago

Importing from three/examples/jsm/.../<module> causes bundlers (tested with rollup) to include the library twice (or multiple times).

For example, when doing import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls', the bundler will follow the import and in the OrbitControls.js the imports come from ../../../build/three.module.js. However, there is no way for the (external) bundler to know that ../../../build/three.module.js is the same module as three.

A solution for this would be to treat the examples modules as external packages and import from three instead of ../../../build/three.module.js. This might break the rollup config of three.js, but it should be possible to tell rollup that three is an alias for the main entry point of three (src/Three.js).

Mugen87 commented 5 years ago

(tested with rollup)

I can not confirm this with rollup. If you are doing it like in the following project setup, everything works as expected.

https://github.com/Mugen87/three-jsm

adrian-delgado commented 5 years ago

If you treat three as external dependency:

export default {
    input: 'src/main.js',
    external: ['three'],
    output: [
        {
            format: 'umd',
            name: 'LIB',
            file: 'build/main.js'
        }
    ],
    plugins: [ resolve() ]
};

then the output should not contain the source code of three.js, yet it includes everything.

If, however, you don't import the OrbitControls, then the output will only include the source code of the main.js file.

you can try it out by commenting out the OrbitControls import, and then building again (but with 'three' as external dependency).

gkjohnson commented 5 years ago

This is related to #17220 -- one of the solutions proposed there was to replace the main field in package.json with the module build path but that would not fix this use case.

Just to be clear the issue here is that while three is being marked as external to build a separate package that is dependent on three in the rollup config that does not catch the hard reference to ../../../build/three.module.js and includes it in the build. For example building the following file will inadvertently include the OrbitControls code and the threejs code in the bundle, as well as import another copy of three when built with @adrian-delgado's posted config.

// src/main.js
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

console.log(THREE, OrbitControls);

@adrian-delgado it might be worth noting that even if the path in OrbitControls.js is changed to three OrbitControls will still be included in your bundle which may or not be desired and could result in at least the OrbitControls code being included twice in dependent applications.

I don't mean to propose this as a long term or best solution but changing the config to mark OrbitControls (and all files in the three folder) as external would solve this in both cases:

export default {
    // ...

    external: p => /^three/.test(p),

    // ...
};
adrian-delgado commented 5 years ago

I don't mean to propose this as a long term or best solution but changing the config to mark OrbitControls (and all files in the three folder) as external would solve this in both cases:

For some reason I expected rollup to treat 'three/examples/jsm/controls/OrbitControls.js' as external too by default. So your proposed solution is good for my use case.

The related #17220 is very relevant. The conversation should probably continue there.

mrdoob commented 5 years ago

So what happens if you do this?

// src/main.js
import * as THREE from 'three/build/three.module.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

console.log(THREE, OrbitControls);
drcmda commented 5 years ago

It would work, but it's not feasible, because any other lib or piece of code that's dependent on three will import from "three" and then it breaks again. Package.json normally tells the environment how to resolve, "build/three.module" is a distribution detail that shouldn't leak out. When resolution is skipped that just invites namespace problems.

EliasHasle commented 5 years ago
  external: p => /^three/.test(p),

@gkjohnson What if the user wants to include the "three" instance and OrbitControls in the bundle?

greggman commented 5 years ago

Not sure it's related a similar happens if you try to use modules live like this

import * as three from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.module.js';
import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';

loads three.js twice, once from the CDN and again from threejs.org

Maybe that's not the way modules are supposed to be used with three but just going from pre 106 there are 1000s of sites and examples that do

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

All the examples show using modules live instead of building(bundling) so in a sense they aren't showing the actual way to use three.js like they used to. In other words, the old examples worked out of the box. The new examples don't AFAIK. In order for an example to work you'd need to extract the JavaScript out of the example and put in a separate .js file, then put three.js locally (probably via npm). Fix all the paths in the examples so they are package based paths (no ../.././build), and finally use rollup

That's pretty big change from the non module versions for which just changing the paths was enough

gkjohnson commented 5 years ago

@mrdoob

With @adrian-delgado's original config three.js will be included once and orbit controls will be included once and no packages will be marked as external. With the config I proposed there will be an external dependency on three/build/three.module.js and three/examples/jsm/controls/OrbitControls.js in the produced bundle.

@EliasHasle

What if the user wants to include the "three" instance and OrbitControls in the bundle?

Then the external field should be excluded in which case a single copy of three and orbit controls will be included in the bundle. rollup-plugin-node-resolve (which is needed for rollup to support module resolution and is being used in the above configs) defaults to use the module field of package.json (see the mainFields option) so the orbit controls three reference and "three" will resolve to the same script. If mainFields is changed to ["main", "module"] so "main" is used instead of "module" in package.json then two copies of three will be included here and things will break in the ways that have been mentioned previously. However it does require changing that field. If "main" is used, though, then rollup-plugin-commonjs will likely need to be needed, as well, because rollup does not know how to process commonjs files that use require by default.

@greggman

Unfortunately I don't think a naive replacement of modules will work so easily in this case. None of the proposed solutions will address this case and I don't think there's anything official at the moment that can be used to help the case of importing the core script and example from separate hosts. Import maps are the only thing that's in the works to help with this as far as I know. If both the example and three are imported from the same host then only a single copy of three will be loaded:

import * as three from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.module.js';
import { OrbitControls } from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/examples/jsm/controls/OrbitControls.js';

// or

import * as three from 'https://threejs.org/build/three.module.js';
import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';

Depending on the use case maybe it's preferable to continue using the classic script tags?

mrdoob commented 5 years ago

@greggman

Not sure it's related a similar happens if you try to use modules live like this

import * as three from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.module.js';
import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';

Yeah... Don't use modules like that 😁

greggman commented 5 years ago

Yeah... Don't use modules like that 😁

Agreed. It's just arguably the docs and examples are targeting mostly inexperienced developers and the fact that jsm examples are the default and none of them will work without a builder nor will they work via any CDN is a kind of big change.

It used to be you could basically view source on an example, copy and paste into jsfiddle/codepen etc, fix the paths in the script tags and it would run. Now though all the examples won't run unless you link directly into the three.js site and watch them break each time the version gets bumped. (yes I know the non module examples exist but those are not the ones linked to from https://threejs.org/examples)

@gkjohnson

import * as three from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.module.js';
import { OrbitControls } from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/examples/jsm/controls/OrbitControls.js';

Doesn't work, OrbitControls are not on the CDN and the paths inside the OrbitContrls ../../../bulild/three.js is not the correct path to make it work

// or

import * as three from 'https://threejs.org/build/three.module.js';
import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js'

Also doesn't work as it will break every time three.js pushes a new version

Maybe push the examples/js folder to a CDN and three such that just fixing the urls in the example code will still work? That means three.module.js needs to be at

https://cdnjs.cloudflare.com/ajax/libs/three.js/108/build/three.module.js

build added to the path

donmccurdy commented 4 years ago

To mention the options, other CDNs like jsdelivr or unpkg do support ES modules:

donmccurdy commented 4 years ago

It used to be you could basically view source on an example, copy and paste into jsfiddle/codepen etc, fix the paths in the script tags and it would run...

I think we'll need import maps to do anything useful about that, for better or worse.

Now though all the examples won't run unless you link directly into the three.js site

I would really not encourage anyone to link directly to live scripts on the threejs site... that will never be a good idea. There are versioned alternatives, per comment above.

The documentation that would, ideally, answer these questions is the Import via modules page. Are there cases we should cover there? I suppose mentioning the CDNs would be a good idea.

greggman commented 4 years ago

Mentioning the CDNs would be a good idea. Also mentioning that the cloudflare CDN, the first hit, on Google is no good for modules (unless that changes)

mrdoob commented 4 years ago

@greggman

It used to be you could basically view source on an example, copy and paste into jsfiddle/codepen etc, fix the paths in the script tags and it would run.

I'm on your side. The worst part of modules is that you can't access camera or renderer from the console in the examples anymore 😟

mrdoob commented 4 years ago

How about we start using unpkg?

donmccurdy commented 4 years ago

How about we start using unpkg?

Do you mean start using it in documentation like the Import via modules page, or using it in the project somehow?

donmccurdy commented 4 years ago

The worst part of modules is that you can't access camera or renderer from the console in the examples anymore

Yeah, that's frustrating. I've been throwing this (or similar) into the examples when developing locally:

Object.assign( window, { camera, renderer, scene } );

I assume that's something we hope to solve with a dev tools extension?

donmccurdy commented 4 years ago

One idea that would take some investigation, but could be interesting... if we'd be willing to add an import map polyfill to all the examples, I think we could make the imports used there 100% copy/paste compatible with npm- and bundler-based workflows. For example:

<script defer src="es-module-shims.js"></script>
<script type="importmap-shim" src="importmap.dev.js"></script>

<!-- ... -->

<script type="module-shim">
  import { Scene, WebGLRenderer } from 'three';
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

  // ...
</script>
mrdoob commented 4 years ago

How about we start using unpkg?

Do you mean start using it in documentation like the Import via modules page, or using it in the project somehow?

Instead of pointing to https://threejs.org/build/. Currently we're using that link in ISSUE_TEMPLATE.

And @greggman could probably switch from https://cdnjs.cloudflare.com/ajax/libs/three.js/108/ to https://unpkg.com/three@0.108.0/?

Just seems like unpkg solves the problems we're discussing here.

mrdoob commented 4 years ago

Yeah, that's frustrating. I've been throwing this (or similar) into the examples when developing locally:

Object.assign( window, { camera, renderer, scene } );

Ugh! Haha

I assume that's something we hope to solve with a dev tools extension?

Yes! 🤞

mrdoob commented 4 years ago

@greggman

Not sure it's related a similar happens if you try to use modules live like this

import * as three from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.module.js';
import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';

Yeah... Don't use modules like that 😁

So today I found myself doing just that... 😅 It's a bad habit indeed, but the problem is that most things kind of work but if something breaks is pretty hard to nail down.

In my case I was importing three.module.js from dev and OBJLoader from master. OBJLoader imported three.module.js from master so the BufferGeometry didn't have the new usage property, and WebGLRenderer did not render the mesh because it didn't find usage, everything else worked though 😶

This is pretty hairy...

greggman commented 4 years ago

I think it's just something to get used to. Now that I think I get it I'm fine with the way it is.

BTW I updated threejsfundamentals to all be esm based so 🤞

greggman commented 4 years ago

It does kind of seem like it might be good to have a three.module.min.js though (or is that three.min.module.js 😜)

Rumyra commented 4 years ago

+1

I am just importing three & orbit controls as ES6 modules & because (it appears) orbit controls refers to three within the build folder it took me a while to figure out my paths

Super fan we can use three as modules, but would be nice to have more flexibility around this, I'm not going to go into the orbit controls file and start messing about, assuming this is the case with other modules too.

Also +1 for a three.min.module.js 😎

0b5vr commented 4 years ago

moving from #18239, I got caught in it a similar problem by doing npm link on another package that uses three.js.

yushijinhun commented 4 years ago

I've developed a plugin three-minifier which may help solve this problem.

chabb commented 4 years ago

I'm facing the same issue. I'm writing a React component using three.js, and i am importing some modules from the examples. Once it's bundled with rollup, if i look at the bundle, i can see that there is one import statement for three, and then the Three.js code.

If i use this import statement in my component: import * as THREE from "three/build/three.module" things work correctly but Three is then embedded in the bundle, which is something i do not want. I'd like to have an import statement for three. If i use import * as THREE from "three, the bundle will have three imported as a module, but as soon as I use one of the examples, then three.js is added in the bundle ( = i have one import statement for three, and then the code of three ), which ultimately cause my code to break

gkjohnson commented 4 years ago

@chabb

I'm writing a React component using three.js, and i am importing some modules from the examples. Once it's bundled with rollup, if i look at the bundle, i can see that there is one import statement for three, and then the Three.js code.

The posted solution here should solve your issue: https://github.com/mrdoob/three.js/issues/17482#issuecomment-530957570.

I feel a lot of these issues are derived from people not fully understanding what's happening with their bundler (which is understandable) but these problems are not unique to three. It's possible, though, that accidentally double importing three core is just more noticeable than with other libraries. Bundling a dependency that's intended to be external like lodash, a react component, or OrbitControls can just be more easily missed.

Regarding depending on an external package Rollup documents this behavior and provides an option here and Webpack has a similar option here. In this case if the example files instead referred to "three" then while the core library would not be bundled you'd still get duplicate bundles of example code which is it's own problem. And I don't think there's anything this project can do to help a bundler interpret npm link pitfalls. I think the only problematic case I've seen that I feel isn't the result of a misconfigured bundler is the codesandbox case.

For the bundler cases maybe the answer is to document, add a troubleshooting guide, or link to how to configure the common bundlers on the importing via modules page.

donmccurdy commented 4 years ago

I have a hunch that if examples/jsm packages could change this pattern...

// @file GLTFLoader.js

// Before
import { Mesh } from '../../build/three.module.js';

// After
import { Mesh } from 'three';

... these issues would be much easier to resolve. Unfortunately, I don't know how we'd manage the HTML examples within the threejs website without a complex build setup then. An import map polyfill on the threejs website might solve it, but I'm not sure. :/

if the example files instead referred to "three" then while the core library would not be bundled you'd still get duplicate bundles of example code...

I don't quite follow this. Because they're relative path imports? We could make them package-relative.

gkjohnson commented 4 years ago

@donmccurdy

I have a hunch that if examples/jsm packages could change this pattern... these issues would be much easier to resolve.

I think this would make it looks resolved but people would still have duplicated code that's just harder to notice becaues it doesn't cause the application to break.

I don't quite follow this. Because they're relative path imports? We could make them package-relative.

Sorry if I'm unclear I think this is a bit difficult to explain -- hopefully this is a bit more clear. I'll use the Rollup case:

In the cases above where people want to rollup a package with three marked as external I assume they're building a library where three.js would be a peer dependency that another application could rely on:

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { stuff } from './local/src/index.js';

// library code with exports...

Here the goal would be for the above three.js imports to remain in the library and the bundle to load three and OrbitControls as peer dependencies so if the application also uses three.js and OrbitControls you don't import either twice.

People expect the option external: [ 'three' ] to achieve this behavior for them (I certainly did) but it doesn't because the string doesn't match the OrbitControls import path. This results in OrbitControls being unintentionally bundled and therefore ../../../build/three.module.js being bundled, as well (because it also doesn't match the string). I think people point to the three.js core file being bundled because it's much more noticeable -- the applications break, the library bundle is much larger, etc -- where the reality is that the OrbitControls file shouldn't have been bundled in the first place. The correct way to configure Rollup here is to set the option to external: path => /^three/.test( path ).

This isn't unique to three. Rollup uses lodash as an example in its docs but it's going to be hard / impossible to notice if 'lodash/merge' gets bundled in your library code because it's so small and won't cause duplicate import bugs. Material UI encourages nested file references in imports and likewise the setting external: ['@material-ui/core'] would fail to exclude '@material-ui/core/Button' from the bundle.

I don't think it's worthwhile to change the example code for these use cases because it will still result in duplicate code that wouldn't be there if the bundler was configured properly.

donmccurdy commented 4 years ago

Two cases here:

(1) user wants threejs and examples included once, gets something twice

E.g. while building an application.

(2) user wants threejs and examples included zero times, gets something 1+ times

E.g. while building a library with three as an external or peer dependency.


As far as I know both (1) and (2) are still easy problems to stumble into? If the approach above solves (1), that alone is helpful. I'm not sure about (2). Maybe the /^three/.test( path ) trick should be mentioned on import via modules?

chabb commented 4 years ago

@gkjohnson Thanks for this explanation, it really helped me clarify my thoughts

In my rollup config, i was defining the external this way

[
        ...Object.keys(pkg.dependencies || {}),
        ...Object.keys(pkg.peerDependencies || {}),
        ...other_stuff
      ]

I thought it would work, as three would be treated as external dependencies; but as you mentioned, you have to use a regex ( as far as I understand, I guess it's because the examples are doing import from "../../../build/three.module.js"; ). So I ended up doing

external: p => {
      if ([
        ...Object.keys(pkg.dependencies || {}),
        ...Object.keys(pkg.peerDependencies || {}),
        'prop-types'
      ].indexOf(p) > -1) {
        return true;
      }
      return /^three/.test(p) ;
    }

It's a bit unrelated question, but I would expect that all the dependencies that I've declared in package.json are not part of the bundle ? Is it a correct assumption ?

gkjohnson commented 4 years ago

@donmccurdy

As far as I know both (1) and (2) are still easy problems to stumble into?

In my opinion (2) is a result of configuring the bundler incorrectly and maybe we can address that by updating the docs with some suggestions for bundlers. (1) can occur as a result from using a package that suffers from problem (2) but other than that I'm not convinced (1) is easy to stumble upon. I'd like to see a real world use case that demonstrates the issue to see how someone configured their bundler but here's a list of the ways I know that you can hit this (so far):

  1. Explicitly import from 'three/src/Three.js', or 'three/build/three.min.js' (which is not recommended in the docs).
  2. Reconfigure your bundler to use the package.main field rather than the package.module field when resolving. The three big bundlers Rollup, Webpack, and Parcel all prefer module over main by default, however. This use case feels like it would be uncommon but that's just an assumption.
  3. Use npm link to include a symlinked package that depends on three (this is fixed by using rollup's preserveSymlinks option)
  4. Use three and examples in codesandbox.io because the platform prioritizes the main field over module.

Number 4 seems like the only one that could be stumbled upon easily, though I know people are doing 1 for tree shaking. The others feel like they're outside of our control or would be very uncommon.

@chabb

as far as I understand, I guess it's because the examples are doing import from "../../../build/three.module.js"; ...

This isn't the case please read what I've explained here: https://github.com/mrdoob/three.js/issues/17482#issuecomment-583694493. /^three works because it matches the string 'three/examples/jsm/controls/OrbitControls.js' which should also be external because it's part of the three.js library while the string 'three' does not. The same can happen with other dependencies, too. I'd recommend using regex for all dependencies to avoid other unknown pitfalls or match against any package with a bare module specifier.

donmccurdy commented 4 years ago

@gkjohnson Thanks for the detailed explanation, that makes sense to me.

It sounds like this doesn't solve the issue in this thread after all, but since I've already mentioned it a couple times in the thread, I finally tested out an import map polyfill: https://github.com/KhronosGroup/KTX-Software/pull/172/files. With that polyfill, import * as THREE from 'three'; works in the web browser.

mrdoob commented 4 years ago

If only browser showed some confidence... https://github.com/WICG/import-maps/issues/212#issuecomment-663564421

Mcgode commented 3 years ago

I've encountered the same issue when adding a pass subclass to one of my projects

import { /* stuff */ } from 'three'
import { Pass } from 'three/examples/jsm/postprocessing/Pass.js'

And since I preferred to copy the Pass code in my module, in order not to have to import it later from three.js on browser, I went ahead found a workaround :

const threeModulePath = path.resolve( __dirname, 'node_modules/three/build/three.module.js' );

export default {
    /* ..... */
    external: [ 'three' ],
    output: [
        {
            /* .... */
            globals : {
                'three': 'THREE',
                [ threeModulePath ]: 'THREE',
            }
        }
    ]
};

This ways, it work with browsers, and module imports should work as well.

Edit:

Loading from a local three project (see example below) will break this approach and require some additional workaround.

"dependencies" : {
    "three": "file:../three.js"
}
Mcgode commented 3 years ago

Well, I went ahead and made a new version that supports local link :

const threeName = "three"; // Change with your three package name
const dependencies = require('./package.json').dependencies;
const splits = dependencies[threeName].split('file:');

const modulePath = (splits.length > 1) ?
    path.resolve(__dirname, splits[1], 'build/three.module.js'):                  // Resolve local path
    path.resolve(__dirname, 'node_modules', threeName, 'build/three.module.js');  // Resolve node_module path

const external = [
    threeName,
    modulePath,
]

const globals = {
    [threeName]: 'THREE',
    [modulePath]: 'THREE',
}
gkjohnson commented 3 years ago

@Mcgode This has been addressed above in https://github.com/mrdoob/three.js/issues/17482#issuecomment-530957570. If you are using Rollup and would like to mark three.js as external when using example modules you have to do the following as suggested:

externals: p => /^three/.test(p),

There's no reason to make the config so complicated. This will ensure that both the the Pass.js file and three.js module are marked as externel.

Mcgode commented 3 years ago

@gkjohnson My use case is not exactly the same, since I only want the three lib to be marked as external, not the example (I want the example to be bundled with my build).

recardinal commented 3 years ago

I'm building a library with three as an external, I want the example to be bundled width the build but without three, and as discussed above, when import module from examples, output will contain the code of three. Is possible to achieve with webpack?

import {  } from "three";
import { Line2 } from "three/examples/jsm/lines/Line2";
import { LineGeometry } from "three/examples/jsm/lines/LineGeometry";
adrian-delgado commented 3 years ago

@Mcgode @recardinal I don't think it's possible. I wanted to do the same so I just copy/pasted the code from the examples; in my case I had to 'adjust' the imports and exports and that was it. Obviously this is not ideal but it was good enough for my use case.

meinders commented 3 years ago

I have a similar use case here with Webpack and THREE as an external. The following imports cause three.module.js to be included in the bundled output.

import * as THREE from 'three';
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

I read somewhere that examples/js/* will be removed at some point. It would be nice if the jsm examples would "just work" before then.

backspaces commented 3 years ago

Using the skypack cdn, this apparently includes three twice, once as three.module.js, once as three@0.120.0 according to chrome's Network devtools tab. test.html contains these:

import * as THREE from 'https://cdn.skypack.dev/three@0.120.0'
import { OrbitControls } from 'https://cdn.skypack.dev/three@0.120.0/examples/jsm/controls/OrbitControls.js'

Snap 01 17 21-14 26 33

The only solution I've found is using npm in a rollup by first creating a local file, workflow/three.index.js:

import * as THREE from '../node_modules/three/build/three.module.js'
import { OrbitControls } from '../node_modules/three/examples/jsm/controls/OrbitControls.js'
export { THREE, OrbitControls }

.. and using a rollup like:

{
        input: 'workflow/three.index.js',
        output: {
            file: 'vendor/three.esm.js',
            format: 'esm',
        },
}

.. producing: Snap 01 17 21-14 31 05

But my goal is to use skypack's cdn, not a local module in my repo. Is there a solution to using three & examples on a cdn?

backspaces commented 3 years ago

This possibly solves the problem: rather than the default Three.js, specifically import the same module as OrbitControls uses:

import * as THREE from 'https://cdn.skypack.dev/three@0.120.0/build/three.module.js'
import { OrbitControls } from 'https://cdn.skypack.dev/three@0.120.0/examples/jsm/controls/OrbitControls.js'

I presume that is similar to what the rest of us are doing? I.e. the equivalent of:

import * as THREE from '../node_modules/three/build/three.module.js'
import { OrbitControls } from '../node_modules/three/examples/jsm/controls/OrbitControls.js'
taseenb commented 3 years ago

This works for me on Webpack 4+

externals: [
      ({ context, request }, callback) => {
        if (request === 'three' || request.endsWith('three.module.js')) {
          return callback(null, {
            commonjs: 'three',
            commonjs2: 'three',
            amd: 'three',
            root: 'THREE'
          })
        }
        callback()
      }
    ]
Blakeinstein commented 3 years ago

Has anyone come up with a solution for this on parcel(v2)?

I am also using typescript so can't use cdns..

import * as THREE from "three";
import { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
acu192 commented 3 years ago

My solution is dumb, but here it is and it works for me:

I use npm to install THREE (thus can do the normal import * as THREE from "three"). That's not the dump part.

Then, for each of the "examples" that I need (like TrackballControls in your case), I just copy that file into my project and edit it slightly so that it imports ... from "three". That avoids bundling three.js twice. It's dumb, but it works for my case and it hasn't caused me any trouble so far. Your mileage may vary.

If the maintainers edited each "example" so that it imports ... from "three" (currently each "example" imports with a relative path instead) then I would not need to copy them into my project to do that edit myself. However, I don't know the other implications of doing that. Maybe it breaks other peoples' things I'm not aware of.

harryhjsh commented 3 years ago

Has anyone come up with a solution for this on parcel(v2)?

I'm just doing import * as THREE from 'three/build/three.module' on parcelv2, which seems to be similar to what everyone else is doing. You can get types to work (if they don't) by using named imports, or by just using import * as THREE from 'three' during development, and switching over to the three.module import later.

+1 that it'd be good if the npm-distributed version of three imported from itself rather than directly referencing a file to import from

gkjohnson commented 3 years ago

@mrdoob one solution to this might be to make a pre-publish script that converts the import statements in the jsm folder from ".../build/three.module.js" to "three" just for NPM. That way the imports can be bare "three" imports as the npm ecosystem would expect while the files in Github would have proper file path references in them for those that want to download and use the files statically.

I'm not sure how this would affect cdn use like unpkg.com, though. It looks like unpkg.com specifically supports a ?module url parameter to automatically resolve the bare import specifiers but it is marked as experimental on the home page.