-
Notifications
You must be signed in to change notification settings - Fork 1.5k
ESM Scripts #5764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
ESM Scripts #5764
Changes from 5 commits
Commits
Show all changes
146 commits
Select commit
Hold shift + click to select a range
f520c67
Preliminary support for ES Modules
marklundin 40c664c
Fixed linting issues
marklundin fe3efec
Modified the ES Script format API. Added swap method
marklundin 9d24a07
Added HMR 'swap' to ES Modules
marklundin 274309d
Adding ASSET_BASE_URL to support module importing
marklundin 4841323
Added CORS flag to `npm run serve`
marklundin e768312
Added _ASSET_BASE_URL fix for dev time
marklundin 2a96c0c
Updated ESModule* naming convention to ScriptESM*
marklundin 762d18d
Fixed missing imports
marklundin f1696af
removed uneccessary cache
marklundin 6ef4dd1
Cleaned up module specifier import
marklundin d51230a
Added custom module import script + linting
marklundin 52c3471
Fixed issue with swapped modules being destroyed. Linting issues
marklundin cb05191
Added module import as template literal. See https://github.com/playc…
marklundin 02fb662
Added correct attributes assignment logic
marklundin 6c4c68b
Prevent module component iterating over all modules
marklundin e3b9d1e
Script ESM Modules can now be re-ordered in a component
marklundin 9f3ff07
Better documentation. Fixed few edge cases with default values
marklundin 4e6b960
Added examples
marklundin 3dc7ae3
added todo flag to cloning
marklundin 7f0955c
changed naming to EsmScript
marklundin 762caa1
Merge branch 'main' into esmodules
marklundin 1a30002
Fix linting
marklundin a66fe79
Nested attributes now copy over to modules with same hierarchy
marklundin 9e2da68
Merge branch 'main' into esmodules
marklundin bd3e347
removed dead code
marklundin 35c93e6
Fixed the ESM Script Component Systems 'clone' method
marklundin 1d45873
Fixed issue that now normalizes module path
marklundin a659d03
Removed render component on earthtile example
marklundin a56c825
updated component iteration to use for of loop
marklundin 99c5b1f
documentation update
marklundin b9f3eee
Merge branch 'main' into esmodules
marklundin ddd4199
added better documentation for `isValidAttributeDefinition`
marklundin 24a646f
Merge branch 'main' into esmodules
marklundin fafd773
Fixed types error
marklundin ecc3e3d
Addresses PR comments
marklundin eafe253
Merge branch 'main' into esmodules
marklundin ee0bd96
Updated example
marklundin 61990a4
Adding ignore to documentation
marklundin 2cd077e
added ignore
marklundin 893e2bf
Removed dynamic import and HMR swapping
marklundin 96e3272
Providing new Esm Script Type base class
marklundin ad9dd97
Fixed linting/types issues
marklundin 673aa2c
Confetti now extends base class
marklundin 177de66
Implemented PR feedback
marklundin 806201f
Added appEntity shorthand
marklundin 8fa2b26
Prevent redundant object creation in update methods
marklundin d429201
Implemented clone method
marklundin 0b76ad7
Removed Earthatile ESM example
marklundin 1392a62
fixed clone issue
marklundin 22eacf3
Testing nested scripts
marklundin 97649b3
correct imports
marklundin 8244e2b
Fixed issue with late initialized entities
marklundin d6db607
Added base test classes
marklundin c4049e1
test fixes
marklundin e0aa409
additional test
marklundin 443304a
Attributes now correctly resolve arrays and nested props
marklundin b047e98
merged esmscript test json
marklundin 99cca3f
included esm script tests
marklundin e34ca79
linting fixes
marklundin 3639e17
Added iniitialize
marklundin ef57d6c
Adding more tests
marklundin 9e011cb
Merge branch 'main' into esmodules
marklundin 9a02061
Raise an error if a module with the same name is added
marklundin fe34450
Ensured esm modules are added in correct order
marklundin 633533e
More tests
marklundin 0c0b8cc
Consistent 'ESM Script' naming
marklundin 500e6cd
removed redundant scene
marklundin 22db44d
Moving tests to ESM
marklundin ef4f2be
Removed redundant NAME field from examples. Sorted imports.
marklundin 0429a3c
documentation improvement.
marklundin d1ce21f
fixed casing issues
marklundin 9186e18
doc updates. removed some redundant code
marklundin dad139d
updated docs
marklundin e1cd169
Merge branch 'esmodules' into esmscript-tests
marklundin 6dd758f
Adding initialize tests
marklundin b76978d
Fix for undefined attributes in class def
marklundin 381caa5
Merge branch 'esmodules' into esmscript-tests
marklundin 291ba3a
Merge branch 'main' into esmodules
marklundin 230e412
Merge branch 'esmodules' into esmscript-tests
marklundin 0710453
Ported more test over to ES6
marklundin bff3bb0
Moving tests to ESM
marklundin 009ced3
Adding initialize tests
marklundin 9b5f29a
Ported more test over to ES6
marklundin a1b8672
Merge branch 'esmscript-tests' into esmodules
marklundin b2559d3
Add new ESM scripts and update EsmScriptComponent
marklundin 0c04a7f
remove esm test from es5 tests dir
marklundin 444d967
Added scene initialization tests
marklundin 24d1866
Tests: Attribute checks
marklundin 944a020
Added better warning/messaging w/ tests
marklundin f48643a
Simplified attribute population
marklundin 4c5321b
Enabled attributes test
marklundin db59d0f
linting fixes
marklundin c032b26
shimming console.error in tests to remove noise
marklundin cae0aea
Merge branch 'main' into esmodules
marklundin daeace9
Add experiemental api warning
marklundin 47ca448
Restrict to `warnOnce`
marklundin 0522633
Doc fixes
marklundin 4fe02bb
doc updates
marklundin c05ed1a
Jsdoc fixes as per @kungfooman feedbabackl
marklundin d0982e1
Added OrbitCamera ES Modules example. Removed Hello script Modules ex…
marklundin 6e8bc0c
Added clamping without events
marklundin 5789b07
removed redundant scripts from rollup
marklundin 407903a
removed legacy code from karma tests
marklundin 118e376
Fixed types issues
marklundin 8ea742b
docs + destroy test
marklundin 84c5c67
Attribute definition now a static class member
marklundin aa2688b
removed redundant attribute Map
marklundin 9a70f07
linting fix
marklundin bba2a7d
Adde enable/disable tests
marklundin 902d533
Updated orbit example with new static member attribute definition
marklundin 9937fa4
lint fixes
marklundin 7c6e1cc
Added script cache in preload
marklundin 36cbd0f
Removed redundant addComponent
marklundin 6f55849
removed redundant tests
marklundin 880579a
fix lint
marklundin 8f67918
removed unnecessary attr export
marklundin 3da4293
removed redundant exports
marklundin 3953e8d
removed unnecessary export. jsdoc fixes
marklundin 440a145
updated tests to use synchronous calls
marklundin de693ae
Changed compile time asset lookup to use runtime path
marklundin 1731a4b
removed dead code in tests
marklundin a083eab
Merge branch 'main' into esmodules
marklundin 89eb9b5
script cache is now scoped to the app
marklundin 97740a3
removed redundant export
marklundin 4bd907d
Update scripts/camera/orbit/orbit-camera-input-mouse.mjs
marklundin af90e32
Update scripts/camera/orbit/orbit-camera-input-mouse.mjs
marklundin 271b76c
Update scripts/camera/orbit/orbit-camera-input-touch.mjs
marklundin 333606f
Update scripts/camera/orbit/orbit-camera.mjs
marklundin aff2085
Update scripts/camera/orbit/orbit-camera.mjs
marklundin fdcf4d0
Update scripts/camera/orbit/orbit-camera.mjs
marklundin 70c9b88
Update scripts/camera/orbit/orbit-camera.mjs
marklundin ec3af91
Update scripts/camera/orbit/orbit-camera.mjs
marklundin 8e4918f
Update src/framework/components/esmscript/system.js
marklundin 1810d22
Update src/framework/components/esmscript/system.js
marklundin f12188b
Update scripts/camera/orbit/orbit-camera.mjs
marklundin 9042dae
Update scripts/camera/orbit/orbit-camera.mjs
marklundin 0099dc8
Update scripts/camera/orbit/orbit-camera.mjs
marklundin 3f83c35
Update scripts/camera/orbit/orbit-camera.mjs
marklundin cee0c70
Update scripts/camera/orbit/orbit-camera.mjs
marklundin 22cf09f
Update scripts/camera/orbit/orbit-camera.mjs
marklundin 972e7c4
Update scripts/camera/orbit/orbit-camera.mjs
marklundin c4153dd
lint fix
marklundin c552ee8
attributes are now correctly cloned, not assigned
marklundin 79ca774
ensure deep equal test
marklundin 026f33f
Scripts now have their own `enabled` prop. Removed component level co…
marklundin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,349 @@ | ||
import { Debug } from '../../../core/debug.js'; | ||
import { Component } from '../component.js'; | ||
|
||
const ASSET_BASE_URL = ''; | ||
|
||
/** | ||
* The ESModuleComponent allows you to extend the functionality of an Entity by attaching your own | ||
* module defined in JavaScript files to be executed with access to the Entity. For more | ||
* details on scripting see [Scripting](https://developer.playcanvas.com/user-manual/scripting/). | ||
* | ||
* @augments Component | ||
*/ | ||
class ESModuleComponent extends Component { | ||
/** | ||
* Create a new ScriptComponent instance. | ||
* | ||
* @param {import('./system.js').ESModuleComponentSystem} system - The ComponentSystem that | ||
* created this Component. | ||
* @param {Entity} entity - The Entity that this Component is attached to. | ||
*/ | ||
constructor(system, entity) { | ||
super(system, entity); | ||
|
||
/** | ||
* Holds all module instances for this component. | ||
* @private | ||
*/ | ||
|
||
this.modules = new Map(); | ||
|
||
|
||
this.moduleAttributes = new Map(); | ||
|
||
} | ||
|
||
/** | ||
* Fired when Component becomes enabled. Note: this event does not take in account entity or | ||
* any of its parent enabled state. | ||
* | ||
* @event ScriptComponent#enable | ||
* @example | ||
* entity.script.on('enable', function () { | ||
* // component is enabled | ||
* }); | ||
*/ | ||
|
||
/** | ||
* Fired when Component becomes disabled. Note: this event does not take in account entity or | ||
* any of its parent enabled state. | ||
* | ||
* @event ScriptComponent#disable | ||
* @example | ||
* entity.script.on('disable', function () { | ||
* // component is disabled | ||
* }); | ||
*/ | ||
|
||
/** | ||
* Fired when Component changes state to enabled or disabled. Note: this event does not take in | ||
* account entity or any of its parent enabled state. | ||
* | ||
* @event ScriptComponent#state | ||
* @param {boolean} enabled - True if now enabled, False if disabled. | ||
* @example | ||
* entity.script.on('state', function (enabled) { | ||
* // component changed state | ||
* }); | ||
*/ | ||
|
||
/** | ||
* Fired when Component is removed from entity. | ||
* | ||
* @event ScriptComponent#remove | ||
* @example | ||
* entity.script.on('remove', function () { | ||
* // entity has no more script component | ||
* }); | ||
*/ | ||
|
||
/** | ||
* Fired when a script instance is created and attached to component. | ||
* | ||
* @event ScriptComponent#create | ||
* @param {string} name - The name of the Script Type. | ||
* @param {import('../../script/script-type.js').ScriptType} scriptInstance - The instance of | ||
* the {@link ScriptType} that has been created. | ||
* @example | ||
* entity.script.on('create', function (name, scriptInstance) { | ||
* // new script instance added to component | ||
* }); | ||
*/ | ||
|
||
/** | ||
* Fired when a script instance is created and attached to component. | ||
* | ||
* @event ScriptComponent#create:[name] | ||
* @param {import('../../script/script-type.js').ScriptType} scriptInstance - The instance of | ||
* the {@link ScriptType} that has been created. | ||
* @example | ||
* entity.script.on('create:playerController', function (scriptInstance) { | ||
* // new script instance 'playerController' is added to component | ||
* }); | ||
*/ | ||
|
||
/** | ||
* Fired when a script instance is destroyed and removed from component. | ||
* | ||
* @event ScriptComponent#destroy | ||
* @param {string} name - The name of the Script Type. | ||
* @param {import('../../script/script-type.js').ScriptType} scriptInstance - The instance of | ||
* the {@link ScriptType} that has been destroyed. | ||
* @example | ||
* entity.script.on('destroy', function (name, scriptInstance) { | ||
* // script instance has been destroyed and removed from component | ||
* }); | ||
*/ | ||
|
||
/** | ||
* Fired when a script instance is destroyed and removed from component. | ||
* | ||
* @event ScriptComponent#destroy:[name] | ||
* @param {import('../../script/script-type.js').ScriptType} scriptInstance - The instance of | ||
* the {@link ScriptType} that has been destroyed. | ||
* @example | ||
* entity.script.on('destroy:playerController', function (scriptInstance) { | ||
* // script instance 'playerController' has been destroyed and removed from component | ||
* }); | ||
*/ | ||
|
||
/** | ||
* Fired when a script instance is moved in component. | ||
* | ||
* @event ScriptComponent#move | ||
* @param {string} name - The name of the Script Type. | ||
* @param {import('../../script/script-type.js').ScriptType} scriptInstance - The instance of | ||
* the {@link ScriptType} that has been moved. | ||
* @param {number} ind - New position index. | ||
* @param {number} indOld - Old position index. | ||
* @example | ||
* entity.script.on('move', function (name, scriptInstance, ind, indOld) { | ||
* // script instance has been moved in component | ||
* }); | ||
*/ | ||
|
||
/** | ||
* Fired when a script instance is moved in component. | ||
* | ||
* @event ScriptComponent#move:[name] | ||
* @param {import('../../script/script-type.js').ScriptType} scriptInstance - The instance of | ||
* the {@link ScriptType} that has been moved. | ||
* @param {number} ind - New position index. | ||
* @param {number} indOld - Old position index. | ||
* @example | ||
* entity.script.on('move:playerController', function (scriptInstance, ind, indOld) { | ||
* // script instance 'playerController' has been moved in component | ||
* }); | ||
*/ | ||
|
||
/** | ||
* Fired when a script instance had an exception. | ||
* | ||
* @event ScriptComponent#error | ||
* @param {import('../../script/script-type.js').ScriptType} scriptInstance - The instance of | ||
* the {@link ScriptType} that raised the exception. | ||
* @param {Error} err - Native JS Error object with details of an error. | ||
* @param {string} method - The method of the script instance that the exception originated from. | ||
* @example | ||
* entity.script.on('error', function (scriptInstance, err, method) { | ||
* // script instance caught an exception | ||
* }); | ||
*/ | ||
|
||
_onBeforeRemove() { | ||
this.modules.forEach(({ moduleInstance }) => { | ||
this.destroy(moduleInstance); | ||
}); | ||
} | ||
|
||
_onInitialize() { | ||
this.modules.forEach(({ moduleInstance }) => { | ||
moduleInstance.initialize?.(); | ||
}); | ||
} | ||
|
||
_onUpdate(dt) { | ||
this.modules.forEach(({ moduleInstance }) => { | ||
moduleInstance.update?.(dt); | ||
}); | ||
} | ||
|
||
/** | ||
* When an entity is cloned and it has entity script attributes that point to other entities in | ||
* the same subtree that is cloned, then we want the new script attributes to point at the | ||
* cloned entities. This method remaps the script attributes for this entity and it assumes | ||
* that this entity is the result of the clone operation. | ||
* | ||
* @param {ScriptESMComponent} oldScriptComponent - The source script component that belongs to | ||
* the entity that was being cloned. | ||
* @param {object} duplicatedIdsMap - A dictionary with guid-entity values that contains the | ||
* entities that were cloned. | ||
* @private | ||
*/ | ||
resolveDuplicatedEntityReferenceProperties(oldScriptComponent, duplicatedIdsMap) { | ||
|
||
// TODO - run over old script component, any entities found, re-point them | ||
} | ||
|
||
/** | ||
* Detect if script is attached to an entity. | ||
* | ||
* @param {string} moduleSpecifier - The module specifier to search for | ||
* @returns {boolean} If script is attached to an entity. | ||
* @example | ||
* if (entity.module.has('path/to/module.mjs')) { | ||
* // entity has script | ||
* } | ||
*/ | ||
has(moduleSpecifier) { | ||
return this.modules.has(moduleSpecifier); | ||
} | ||
|
||
/** | ||
* Get a script instance (if attached). | ||
* | ||
* @param {string} moduleSpecifier - The | ||
* name or type of {@link ScriptType}. | ||
* @returns {import('../../script/script-type.js').ScriptType|null} If script is attached, the | ||
* instance is returned. Otherwise null is returned. | ||
* @example | ||
* const controller = entity.module.get('module'); | ||
*/ | ||
get(moduleSpecifier) { | ||
return this.modules.get(moduleSpecifier); | ||
} | ||
|
||
/** | ||
* Create a script instance and attach to an entity script component. | ||
* | ||
* @param {string} moduleSpecifier - The module specifier used to import the ES module. | ||
* @param {object} [args] - Object with arguments for a script. | ||
* @param {boolean} [args.enabled] - If script instance is enabled after creation. Defaults to | ||
* true. | ||
* @param {object} [args.attributes] - Object with values for attributes (if any), where key is | ||
* name of an attribute. | ||
* @param {boolean} [args.preloading] - If script instance is created during preload. If true, | ||
* script and attributes must be initialized manually. Defaults to false. | ||
* @param {number} [args.ind] - The index where to insert the script instance at. Defaults to | ||
* -1, which means append it at the end. | ||
* @returns {*} Returns an instance of a | ||
* ES Module if successfully attached to an entity, or null if the import fails | ||
* @example | ||
* entity.module.create('moduleSpecifier', { | ||
* attributes: { | ||
* speed: 4 | ||
* } | ||
* }); | ||
*/ | ||
create(moduleSpecifier, args) { | ||
|
||
const { attributes: definedAttributes } = args; | ||
|
||
import(ASSET_BASE_URL + moduleSpecifier).then(({ default: ModuleClass, attributes }) => { | ||
|
||
this.addModule(moduleSpecifier, ModuleClass, attributes, definedAttributes); | ||
|
||
}).catch((err) => { | ||
Debug.error(`module '${moduleSpecifier}' does not exist`); | ||
return null; | ||
}); | ||
|
||
} | ||
|
||
addModule(moduleSpecifier, ModuleClass, attributes, definedAttributes) { | ||
|
||
if (!ModuleClass) | ||
throw new Error(`Please check your exports. The module '${moduleSpecifier}' does not contain a default export`); | ||
|
||
if (typeof ModuleClass !== 'function') | ||
throw new Error(`The module '${moduleSpecifier}' does not export a class or a function`); | ||
|
||
if (!attributes) Debug.warn(`The module '${moduleSpecifier}' does not export any attributes`); | ||
|
||
const moduleInstance = new ModuleClass(this.entity, attributes); | ||
|
||
const previousModuleInstance = this.modules.get(moduleSpecifier)?.moduleInstance; | ||
|
||
this.fire('create', moduleSpecifier, moduleInstance); | ||
this.fire('create:' + moduleSpecifier, moduleInstance); | ||
|
||
// Check if an existing module exists, ie. if this is a candidate to swap | ||
if (previousModuleInstance) { | ||
|
||
// Copy intrinsic state | ||
moduleInstance.enabled = previousModuleInstance.enabled; | ||
|
||
// Copy explicit state | ||
moduleInstance.swap?.(previousModuleInstance); | ||
|
||
// destroy previous module | ||
previousModuleInstance.destroy(); | ||
|
||
} else if (moduleInstance.enabled || !Object.hasOwn(moduleInstance, 'enabled')) { | ||
moduleInstance.initialize(definedAttributes); | ||
} | ||
|
||
let moduleEventPath = moduleSpecifier; | ||
const assets = this.system.app.assets; | ||
if (assets.prefix && moduleSpecifier.startsWith(assets.prefix)) { | ||
moduleEventPath = moduleEventPath.slice(assets.prefix.length); | ||
} | ||
|
||
const path = new URL(moduleEventPath, 'https://www.example.com'); | ||
path.searchParams.delete('t'); | ||
|
||
assets.once(`load:url:${path.pathname.slice(1) + path.search}`, (asset) => { | ||
const NewModuleClass = asset.resource; | ||
this.addModule(moduleSpecifier, NewModuleClass, attributes); | ||
}); | ||
|
||
this.modules.set(moduleSpecifier, { ModuleClass, attributes, moduleInstance }); | ||
|
||
return moduleInstance; | ||
} | ||
|
||
|
||
/** | ||
* Destroy the script instance that is attached to an entity. | ||
* | ||
* @param {string} moduleSpecifier - The | ||
* name or type of {@link ScriptType}. | ||
* @returns {boolean} If it was successfully destroyed. | ||
* @example | ||
* entity.script.destroy('playerController'); | ||
*/ | ||
destroy(moduleSpecifier) { | ||
|
||
const module = this.modules.get(moduleSpecifier); | ||
|
||
if (module) { | ||
module.destroy(); | ||
this.modules.delete(moduleSpecifier); | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
} | ||
|
||
export { ESModuleComponent }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
class ESModuleComponentData { | ||
constructor() { | ||
this.enabled = true; | ||
} | ||
} | ||
|
||
export { ESModuleComponentData }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.