External Module Usage Case
The biggest problem with how to use npm in Cocos Creator 3.x is the interaction between ESM and CJS modules. To understand how these two modules are defined in Cocos Creator, check out the section. In fact, the way ESM and CJS modules interact is described in the official Node.js documentation. Briefly summarized as follows:
CommonJS modules are exported by
module.exports
, and when importing CommonJS modules, use the default import method for ES modules or their sugar syntax counterparts.The
default
export of the ESM module points to themodule.exports
of the CJS module.The non-
default
part of the export is provided by Node.js as a separate ES module through static analysis.
Next, consider the following code snippet:
// foo.js
module.exports = {
a: 1,
b: 2,
}
module.exports.c = 3;
// test.mjs
// default points to module.exports
import foo from './foo.js'; // equivalent to import { default as foo } from './foo.js'
console.log(JSON.stringify(foo)); // {"a":1, "b":2, "c":3}
// Import all exports of the foo module
import * as module_foo from './foo.js'
import { a } from './foo.js'
console.log(a); // Error: a is not defined
// According to the third point above, c has a separate export
import { c } from './foo.js'
console.log(c); // 3
npm Module Usage Cases
First, get the package. Open a terminal in the project directory and execute npm i protobufjs
. If the project is a multi-person collaboration, the protobufjs
package can be written to the package.json
as a dependency by adding npm install --save protobufjs
to the above line to automatically write it to package.json
from the command line. Once executed, find the protobufjs
folder in the node_module
folder under the project directory.
Once the protobufjs
module package is installed, determine the module format.
- Look at the
main
field in thepackage.json
file and determine the entry fileindex.js
. - Look at the
type
field in thepackage.json
file and notice that there is notype
field.
Identify the module format, it can be inferred that this is a CJS module. By the way, you can see that each js file in the package corresponds to a .d.ts
file, which means that the protobufjs
package comes with a TypeScript
declaration file, which makes it easy to import the protobufjs
module and get the internal interface through code hints.
Next, notice it exported in index.js
:
"use strict";
Determine the module format and export method. The next step is how to use the protobufjs
module in the script assets.
First, create a test.ts
script under assets
. Then, write the following code in the header of the script.
// Most npm modules can be used by directly importing the module name.
import protobufjs from 'protobufjs';
console.log(protobufjs);
When run in Chrome, the console output is as follows:
Now, all the modules provided by protobufjs
are directly accessible. If only specific submodule functionality is needed, such as light
and minimal
, import the subpaths directly from the package:
// Using the light version
import protobuf from 'protobufjs/light.js';
// Using the minimal version
import protobuf from 'protobufjs/minimal.js';
TypeScript configuration
When importing modules without default export in TypeScript, it usually appears that Module '"/${project_path}/protobufjs"' has no default export.
. This is because currently protobufjs
only provides CommonJS modules, and Cocos Creator accesses CommonJS modules via “default import”, but CommonJS modules really don’t have a “default export”. In this case, set the "allowSyntheticDefaultImports"
option in the "compilerOptions"
field to true
by editing the tsconfig.json
file in the project directory. If the field is not present, manually fill it in.
This section is about how to compile a proto file into a JavaScript file. In fact, when looking through the protobufjs
documentation, notice that it provides command line tools to convert static modules and ts declaration files. This time, let’s take an empty 3.0 project, example, as an example to demonstrate the whole process.
First, install protobufjs
via npm and write it to the package.json
dependency in the project directory.
Next, create a new Proto directory in the project directory and define a few proto files:
// pkg1.proto
package pkg1;
syntax = "proto2";
message Bar {
required int32 bar = 1;
}
// pkg2.proto
package pkg2;
syntax = "proto2";
message Baz {
required int32 baz = 1;
}
// unpkg.proto is not part of any package
syntax = "proto2";
message Foo {
}
Next, define it in package.json
:
"scripts": {
"build-proto:pbjs": "pbjs --dependency protobufjs/minimal.js --target static-module --wrap commonjs --out ./Proto.js/proto.js ./Proto/*.proto",
"build-proto:pbts": "pbts --main --out ./Proto.js/proto.d.ts ./Proto.js/*.js"
},
The first directive build-proto:pbjs
roughly means to compile the proto file into js. The parameter --dependency protobufjs/minimal.js
is actually because the execution require
s to protobufjs
, but all we need to use is its submodule minimal.js
. Then, generate the js into the Proto.js folder.
The second command build-proto:pbts
generates the type declaration file based on the output of the first paragraph.
Following the above steps successfully completes the process of converting the proto file into a JavaScript file. Next, the JavaScript file can be introduced into the project script under assets
.
import proto from './Proto.js/proto.js';
console.log(proto.pkg1.Bar);
Here it is still important to state that if anyone gets an error when importing, indicating that proto is not exported by default, there are two solutions:
It can be solved by allowing default import fields for modules containing default exports in
tsconfig.json
"compilerOptions": {
"allowSyntheticDefaultImports": true,
}
Add the default export
Replace the original
build-proto:pbts
command with the following:"build-proto:pbts": "pbts --main --out ./Proto.js/proto.d.ts ./Proto.js/*.js && node ./Tools/wrap-pbts-result.js"
Eventually, it will be ready to run directly. For the full project content, please refer to: .
Similar to , install the lodash-es
package. The entry file is lodash.js
, and the entry file also automatically helps to export all submodules under it in ESM module format, and according to type
it also confirms that it is currently an ESM module. Therefore, it is possible to import any module directly. Taking the test.ts
script asset under assets
as an example, and introduce the submodules within lodash
:
import { array, add } from 'lodash-es';
At this point, notice that an error is reported at the code level, but it actually works. This is because there is a clear distinction between JavaScript, which is dynamically typed, and TypeScript, which is statically typed, this means there is no way to know the exact type of the exported module when using JavaScript, and the best thing to do is to declare a type definition file .d.ts
. Fortunately, when hovering over the error, it will prompt that the type declaration file for the lodash
module can be installed by running npm i --save-dev @types/lodash-es
. After installation, restart VS Code and notice the error message disappears, along with the code hint.
Install the web3
package in the same way as the protobufjs
case above. Using the same detection method, determine that web3
is a CJS module. Import in the same way:
import web3 from 'web3';
When going back to the editor, a bunch of errors are still present:
This is because the package is customized for Node and contains built-in modules from Node.js, which are not available in Creator, resulting in errors. Usually, these packages also take care of web users. When importing the package directly, the package entry points directly to the Node version, while the web version is placed in the dist/**.js
or dist/**.min.js
file under the package. In this case, it is necessary to import the custom version of the Web provided in the package as follows:
import web3 from 'web3/dist/web3.min.js';
At this point, the editor does not report errors anymore, but the code prompts: Could not find a declaration file for module 'web3/dist/web3.min.js'. '/${project_path}/node_modules/web3/dist/web3.min.js' implicitly has an 'any' type.
.
This is because even if these packages have a type specification, it is a type specification for the Node version of that entry. In general, the interface definitions are the same for both versions, just “borrow” the type description from the Node version. Just create an arbitrary new .d.ts
to the project and write:
This is a special case, take a look at the special features of this case. After installing the firebase
package in the way described above, and analyzing the package according to the package.json
file, notice the package is in CJS format. Based on the entry file, infer that this package is customized for Node (this can be tested according to Case 4: Using web3), so we have to use the Web custom version index.esm.js
. The problem is, index.esm.js
is an ESM module, and Creator recognizes this package as a CJS module, but it is also an ESM module, which naturally causes errors.
The proposed solution is for the user to package it as a separate js file as a non-npm module via a packaging tool like rollup
.