diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c0ba960 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false + +[*.ts] +indent_size = 2 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e038f2e..7e0aeb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -490,6 +490,7 @@ dependencies = [ "fixedbitset", "serde", "serde_json", + "swc_atoms", "swc_common", "swc_ecma_codegen", "swc_ecma_parser", diff --git a/Cargo.toml b/Cargo.toml index 5054e37..6f3f6d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ typescript = [ "swc_ecma_transforms_base", "swc_ecma_transforms_typescript", "swc_ecma_visit", + "swc_atoms" ] [dependencies] @@ -33,6 +34,7 @@ swc_ecma_parser = { version = "0.117.2", optional = true } swc_ecma_transforms_base = { version = "0.103.4", optional = true } swc_ecma_transforms_typescript = { version = "0.145.1", optional = true } swc_ecma_visit = { version = "0.76.3", optional = true } +swc_atoms = { version = "0.4.3", optional = true } [dev-dependencies] bevy = { version = "0.8.0", default-features = false, features = ["render", "bevy_winit", "x11", "filesystem_watcher"] } diff --git a/assets/scripts/debug.ts b/assets/scripts/debug.ts index 7ac91b5..e4a769d 100644 --- a/assets/scripts/debug.ts +++ b/assets/scripts/debug.ts @@ -1,42 +1,53 @@ - -function filterComponentInfos(infos: ComponentInfo[], prefix: string): string[] { - return infos - .filter(info => info.name.startsWith(prefix)) - .map(info => info.name.replace(prefix, "")); +function filterComponentInfos( + infos: ComponentInfo[], + prefix: string +): string[] { + return infos + .filter((info) => info.name.startsWith(prefix)) + .map((info) => info.name.replace(prefix, "")); } - -let firstIteration = true; -let i = 0; -function run() { - if (firstIteration) { - firstIteration = false; - - info("Components: " + filterComponentInfos(world.components, "bevy_transform::")); - info("Resources: " + filterComponentInfos(world.resources, "breakout::").join(", ")); +class Debug implements BevyScript { + firstIteration = true; + i = 0; + update() { + if (this.firstIteration) { + this.firstIteration = false; + + info( + "Components: " + + filterComponentInfos(world.components, "bevy_transform::") + ); + info( + "Resources: " + + filterComponentInfos(world.resources, "breakout::").join(", ") + ); } - - i++; - if (i % 60 == 0) { - let ballId = world.components.find(info => info.name == "breakout::Ball").id; - let velocityId = world.components.find(info => info.name == "breakout::Velocity").id; - let transformId = world.components.find(info => info.name == "bevy_transform::components::transform::Transform").id; - - const ballQuery = world.query({ - components: [ballId, transformId, velocityId], - }); - for (const item of ballQuery) { - let [ball, transform, velocity] = item.components; - velocity = velocity[0]; - - info(velocity.toString()); - } + this.i++; + if (this.i % 60 == 0) { + let ballId = world.components.find( + (info) => info.name == "breakout::Ball" + ).id; + let velocityId = world.components.find( + (info) => info.name == "breakout::Velocity" + ).id; + let transformId = world.components.find( + (info) => + info.name == "bevy_transform::components::transform::Transform" + ).id; + + const ballQuery = world.query({ + components: [ballId, transformId, velocityId], + }); + for (const item of ballQuery) { + let [ball, transform, velocity] = item.components; + velocity = velocity[0]; + + info(velocity.toString()); + } } + } } -function init() { - return { - update: run, - }; -} +export default new Debug(); diff --git a/assets/scripts/headless.ts b/assets/scripts/headless.ts index 01c042d..92080dc 100644 --- a/assets/scripts/headless.ts +++ b/assets/scripts/headless.ts @@ -1,45 +1,49 @@ -function filterComponentInfos(infos: ComponentInfo[], prefix: string): string[] { - return infos - .filter(info => info.name.startsWith(prefix)) - .map(info => info.name.replace(prefix, "")); +class Headless implements BevyScript { + firstIteration = true; + update() { + if (this.firstIteration) { + this.firstIteration = false; + + // info("Components: " + world.components.map(info => info.name).join(", ")); + // info("Resources:"); + // info(world.resources.map(info => info.name)); + // info("Resources (headless): " + filterComponentInfos(world.resources, "headless::").join(", ")); + // info("Entitites: " + (world.entities.map(entity => `Entity(${entity.id}v${entity.generation})`).join(", "))); + info("----------"); + + let transformId = componentId( + "bevy_transform::components::transform::Transform" + ); + + let query = world.query({ + components: [transformId], + }); + let [transform1, transform2] = query.map((item) => item.components[0]); + let [translation1, translation2] = [ + transform1.translation, + transform2.translation, + ]; + + for (const s of [0.0, 0.25, 0.5, 0.75, 1.0]) { + info(translation1.lerp(translation2, s).toString()); + } + } + } } -function componentId(name) { - let id = world.components.find(info => info.name === name); - if (!id) throw new Error(`component id for ${name} not found`); - return id.id; - +function componentId(name: string): ComponentId { + let id = world.components.find((info) => info.name === name); + if (!id) throw new Error(`component id for ${name} not found`); + return id.id; } -let firstIteration = true; -function run() { - if (firstIteration) { - firstIteration = false; - - // info("Components: " + world.components.map(info => info.name).join(", ")); - // info("Resources:"); - // info(world.resources.map(info => info.name)); - // info("Resources (headless): " + filterComponentInfos(world.resources, "headless::").join(", ")); - // info("Entitites: " + (world.entities.map(entity => `Entity(${entity.id}v${entity.generation})`).join(", "))); - info("----------"); - - let transformId = componentId("bevy_transform::components::transform::Transform"); - - - let query = world.query({ - components: [transformId] - }); - let [transform1, transform2] = query.map(item => item.components[0]); - let [translation1, translation2] = [transform1.translation, transform2.translation]; - - for (const s of [0.0, 0.25, 0.5, 0.75, 1.0]) { - info(translation1.lerp(translation2, s).toString()); - } - } +function filterComponentInfos( + infos: ComponentInfo[], + prefix: string +): string[] { + return infos + .filter((info) => info.name.startsWith(prefix)) + .map((info) => info.name.replace(prefix, "")); } -function init() { - return { - update: run, - } -} \ No newline at end of file +export default new Headless(); diff --git a/lib.bevy.d.ts b/lib.bevy.d.ts new file mode 100644 index 0000000..2933850 --- /dev/null +++ b/lib.bevy.d.ts @@ -0,0 +1,53 @@ +// log.s +declare function trace(val: any): void; +declare function debug(val: any): void; +declare function info(val: any): void; +declare function warn(val: any): void; +declare function error(val: any): void; + +// ecs.js +declare interface BevyScript { + first?: () => void; + pre_update?: () => void; + update?: () => void; + post_update?: () => void; + last?: () => void; +} + +declare class ComponentId { + index: number; +} +declare class Entity { + id: number; + generation: number; +} + +declare interface ComponentInfo { + id: ComponentId; + name: string; + size: number; +} + +declare interface QueryDescriptor { + components: ComponentId[]; +} + +declare interface QueryItem { + entity: Entity; + components: any[]; +} + +declare type Primitive = number | string | boolean; +declare interface Value { + [path: string | number]: Value | Primitive | undefined; +} + +declare class World { + get components(): ComponentInfo[]; + get resources(): ComponentInfo[]; + get entities(): Entity[]; + + query(descriptor: QueryDescriptor): QueryItem[]; +} + +declare const world: World; diff --git a/src/asset.rs b/src/asset.rs index bdd367f..41876b1 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -26,7 +26,7 @@ impl AssetLoader for JsScriptLoader { .extension() .map_or(false, |ext| ext == "ts") { - crate::ts_to_js::ts_to_js(load_context.path(), &source)? + crate::transpile::transpile(load_context.path(), &source)? } else { source }; diff --git a/src/lib.rs b/src/lib.rs index e137149..c4af35c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ mod asset; mod runtime; #[cfg(feature = "typescript")] -mod ts_to_js; +mod transpile; use asset::{JsScript, JsScriptLoader}; use bevy::{asset::AssetStage, prelude::*}; diff --git a/src/runtime/js/ecs.js b/src/runtime/js/ecs.js new file mode 100644 index 0000000..f2fadfc --- /dev/null +++ b/src/runtime/js/ecs.js @@ -0,0 +1,72 @@ +"use strict"; + +((window) => { + + class ComponentId { + index; + } + + class Entity { + #bits; + id; + generation; + } + + class World { + get #rid() { return 0 } + + toString() { + return Deno.core.opSync("op_world_tostring", this.rid); + } + + get components() { + return Deno.core.opSync("op_world_components", this.rid); + } + + get resources() { + return Deno.core.opSync("op_world_resources", this.rid); + } + + get entities() { + return Deno.core.opSync("op_world_entities", this.rid); + } + + query(descriptor) { + return Deno.core.opSync("op_world_query", this.rid, descriptor) + .map(({ entity, components }) => ({ + entity, + components: components.map(wrapValueRef), + })); + } + } + + + const VALUE_REF_GET_INNER = Symbol("value_ref_get_inner"); + function wrapValueRef(valueRef) { + // leaf primitives + if (typeof valueRef !== "object") { return valueRef }; + const proxy = new Proxy(valueRef, { + ownKeys: (target) => { + return Deno.core.opSync("op_value_ref_keys", world.rid, target); + }, + get: (target, p, receiver) => { + switch (p) { + case VALUE_REF_GET_INNER: + return target; + case "toString": + return () => Deno.core.opSync("op_value_ref_to_string", world.rid, target); + default: + let valueRef = Deno.core.opSync("op_value_ref_get", world.rid, target, "." + p); + return wrapValueRef(valueRef); + } + }, + set: (target, p, value) => { + Deno.core.opSync("op_value_ref_set", world.rid, target, "." + p, value); + } + }); + return proxy; + } + + const world = new World(); + window.world = world; +})(globalThis); diff --git a/src/runtime/js/log.js b/src/runtime/js/log.js new file mode 100644 index 0000000..108519e --- /dev/null +++ b/src/runtime/js/log.js @@ -0,0 +1,19 @@ +"use strict"; + +((window) => { + window.trace = function (val) { + Deno.core.opSync("op_log", "trace", val); + } + window.debug = function (val) { + Deno.core.opSync("op_log", "debug", val) + } + window.info = function (val) { + Deno.core.opSync("op_log", "info", val) + } + window.warn = function (val) { + Deno.core.opSync("op_log", "warn", val) + } + window.error = function (val) { + Deno.core.opSync("op_log", "error", val) + } +})(globalThis); diff --git a/src/runtime/native/mod.rs b/src/runtime/native/mod.rs index 878e081..6748b58 100644 --- a/src/runtime/native/mod.rs +++ b/src/runtime/native/mod.rs @@ -73,16 +73,13 @@ impl JsRuntimeApi for JsRuntime { // Get the script source code let code = &script.source; - // Wrap the script in a closure, and make the return value the result of calling the - // script's `init()` function. + // Wrap the script in a closure let code = format!( r#" "strict_mode"; ((window) => {{ {code} - - return init(); }})(globalThis) "#, ); diff --git a/src/ts_to_js.rs b/src/transpile.rs similarity index 67% rename from src/ts_to_js.rs rename to src/transpile.rs index f4718a4..f77cf36 100644 --- a/src/ts_to_js.rs +++ b/src/transpile.rs @@ -9,13 +9,16 @@ use swc_common::{ comments::SingleThreadedComments, errors::{EmitterWriter, Handler}, sync::Lrc, - BytePos, Globals, Mark, SourceMap, GLOBALS, + BytePos, Globals, Mark, SourceMap, Span, GLOBALS, }; use swc_ecma_codegen::{text_writer::JsWriter, Emitter}; use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig}; use swc_ecma_transforms_base::{fixer::fixer, hygiene::hygiene, resolver}; use swc_ecma_transforms_typescript::strip; -use swc_ecma_visit::FoldWith; +use swc_ecma_visit::{ + swc_ecma_ast::{ModuleDecl, ModuleItem, ReturnStmt, Stmt}, + FoldWith, +}; struct SharedWriter(Arc>); @@ -29,7 +32,7 @@ impl Write for SharedWriter { } } -pub fn ts_to_js(path: &Path, js: &str) -> Result { +pub fn transpile(path: &Path, js: &str) -> Result { let cm: Lrc = Default::default(); cm.new_source_file(path.to_owned().into(), js.to_owned()); @@ -66,7 +69,7 @@ pub fn ts_to_js(path: &Path, js: &str) -> Result { .parse_module() .map_err(|e| e.into_diagnostic(&handler).emit()); - let module = match module { + let mut module = match module { Ok(module) if !had_error => module, _ => { let error_msg = @@ -77,13 +80,39 @@ pub fn ts_to_js(path: &Path, js: &str) -> Result { } }; + // Rewrite module import/exports + let globals = Globals::default(); let ts = GLOBALS.set(&globals, || { let unresolved_mark = Mark::new(); let top_level_mark = Mark::new(); - // Optionally transforms decorators here before the resolver pass - // as it might produce runtime declarations. + let mut body = Vec::new(); + std::mem::swap(&mut body, &mut module.body); + for item in body { + if let ModuleItem::ModuleDecl(decl) = item { + match decl { + ModuleDecl::ExportDefaultExpr(expr) => { + module.body.push(ModuleItem::Stmt(Stmt::Return(ReturnStmt { + span: Span::dummy_with_cmt(), + arg: Some(expr.expr), + }))) + } + ModuleDecl::ExportNamed(_) + | ModuleDecl::ExportDefaultDecl(_) + | ModuleDecl::ExportAll(_) + | ModuleDecl::TsNamespaceExport(_) + | ModuleDecl::ExportDecl(_) => { + anyhow::bail!("Only default expression exports are supported currently") + } + ModuleDecl::TsImportEquals(_) + | ModuleDecl::TsExportAssignment(_) + | ModuleDecl::Import(_) => anyhow::bail!("Imports are not yet supported"), + } + } else { + module.body.push(item); + } + } // Conduct identifier scope analysis let module = module.fold_with(&mut resolver(unresolved_mark, top_level_mark, true)); @@ -112,8 +141,8 @@ pub fn ts_to_js(path: &Path, js: &str) -> Result { emitter.emit_module(&module).unwrap(); } - String::from_utf8(buf) - })?; + Ok(String::from_utf8(buf)) + })??; Ok(ts) } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..218ce9c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "rootDir": "./assets", + "target": "es6", + "lib": [ + "es2015" + ] + }, + "include": [ + "./assets/**/*", + "./tslib" +, "lib.bevy.d.ts" ], +} \ No newline at end of file