From 0d346eb714161a0f5492ed80e2332e66fede25fa Mon Sep 17 00:00:00 2001 From: mantou132 <709922234@qq.com> Date: Thu, 12 Dec 2024 22:32:11 +0800 Subject: [PATCH] [swc] Support hmr --- crates/swc-plugin-gem/src/visitors/hmr.rs | 315 +++++++++++++++++- crates/swc-plugin-gem/src/visitors/memo.rs | 33 +- crates/swc-plugin-gem/src/visitors/minify.rs | 4 +- crates/swc-plugin-gem/tests/fixture.rs | 13 + .../swc-plugin-gem/tests/fixture/hmr/input.ts | 21 ++ .../tests/fixture/hmr/output.ts | 33 ++ .../tests/fixture/minify/input.ts | 4 + packages/gem-devtools/src/scripts/get-gem.ts | 13 +- packages/gem-devtools/src/scripts/preload.ts | 9 +- packages/gem/src/helper/hmr.ts | 93 +++++- packages/gem/src/helper/logger.ts | 14 +- packages/gem/src/lib/decorators.ts | 11 +- packages/gem/src/lib/element.ts | 2 +- .../src/test/gem-element/decorators.test.ts | 2 +- 14 files changed, 514 insertions(+), 53 deletions(-) create mode 100644 crates/swc-plugin-gem/tests/fixture/hmr/input.ts create mode 100644 crates/swc-plugin-gem/tests/fixture/hmr/output.ts diff --git a/crates/swc-plugin-gem/src/visitors/hmr.rs b/crates/swc-plugin-gem/src/visitors/hmr.rs index 8f52ae87..eab68925 100644 --- a/crates/swc-plugin-gem/src/visitors/hmr.rs +++ b/crates/swc-plugin-gem/src/visitors/hmr.rs @@ -1,28 +1,323 @@ //! https://rspack.dev/api/runtime-api/hmr -//! 为 HMR 添加一些特殊的转换 +//! +//! - 将私有字段转译成公开字段 +//! - 为函数成员(方法、getter、setter、字段,包括静态的)添加影子方法 +//! - 调用 HMR API +use std::{mem, vec}; + +use once_cell::sync::Lazy; +use regex::Regex; +use swc_common::DUMMY_SP; use swc_core::{ ecma::visit::{noop_visit_mut_type, VisitMut, VisitMutWith}, quote, }; -use swc_ecma_ast::ModuleItem; +use swc_ecma_ast::{ + BlockStmt, BlockStmtOrExpr, Callee, Class, ClassMember, ClassMethod, ClassProp, Expr, + ExprOrSpread, Function, Ident, IdentName, Lit, MemberExpr, MemberProp, ModuleItem, Param, + PropName, ThisExpr, +}; + +static DASH_REG: Lazy = Lazy::new(|| Regex::new(r"-").unwrap()); #[derive(Default)] -struct TransformVisitor {} +struct TransformVisitor { + has_element: bool, + tag_name_stack: Vec, +} + +impl TransformVisitor { + fn get_current_tag_name(&self) -> &str { + self.tag_name_stack.last().unwrap() + } +} + +fn get_tag_name(node: &mut Class) -> String { + node.decorators + .iter() + .find(|x| { + if let Some(call_expr) = x.expr.as_call() { + if let Callee::Expr(b) = &call_expr.callee { + if let Some(Ident { sym, .. }) = b.as_ident() { + if sym.as_str() == "customElement" { + return true; + } + } + } + } + false + }) + .map(|x| { + if let Some(ExprOrSpread { expr, .. }) = x.expr.as_call().unwrap().args.first() { + if let Some(Lit::Str(tag_name)) = expr.as_lit() { + return tag_name.value.to_string(); + } + } + Default::default() + }) + .unwrap_or_default() +} + +fn get_shadow_ident(origin_ident: &IdentName, key: &str, is_private: bool) -> IdentName { + format!( + "_hmr_{}_{}_{}", + if is_private { "private" } else { "public" }, + &DASH_REG.replace_all(key, "_"), + origin_ident.as_ref() + ) + .into() +} + +fn get_private_ident(origin_ident: &IdentName, key: &str) -> IdentName { + format!( + "_private_{}_{}", + &DASH_REG.replace_all(key, "_"), + origin_ident.as_ref() + ) + .into() +} + +fn gen_shadow_member( + shadow_ident: &IdentName, + is_static: bool, + body: Option, + params: Vec, + is_async: bool, +) -> ClassMember { + ClassMember::Method(ClassMethod { + is_static, + key: PropName::Ident(shadow_ident.clone()), + function: Box::new(Function { + is_async, + params, + body, + ..Default::default() + }), + ..Default::default() + }) +} + +fn gen_proxy_body(shadow_ident: &IdentName) -> BlockStmt { + let this_expr = Expr::Member(MemberExpr { + obj: Box::new(Expr::This(ThisExpr { span: DUMMY_SP })), + prop: MemberProp::Ident(shadow_ident.clone()), + ..Default::default() + }); + BlockStmt { + stmts: vec![quote!( + " + return $expr.bind(this)(...arguments); + " as Stmt, + expr: Expr = this_expr + )], + ..Default::default() + } +} + +fn transform_fn(node: ClassMember, key: &str) -> (ClassMember, Option) { + // TODO: 支持计算属性名 + match node { + ClassMember::Method(mut method) => { + if let Some(origin_ident) = method.key.as_ident() { + let shadow_ident = get_shadow_ident(origin_ident, key, false); + let body = mem::replace( + &mut method.function.body, + gen_proxy_body(&shadow_ident).into(), + ); + let params = method.function.params.drain(..).collect(); + let is_async = method.function.is_async; + ( + ClassMember::Method(ClassMethod { ..method }), + Some(gen_shadow_member( + &shadow_ident, + method.is_static, + body, + params, + is_async, + )), + ) + } else { + (ClassMember::Method(method), None) + } + } + ClassMember::PrivateMethod(mut method) => { + let origin_ident = IdentName::new(method.key.name, DUMMY_SP); + let private_ident = PropName::Ident(get_private_ident(&origin_ident, key)); + let shadow_ident = get_shadow_ident(&origin_ident, key, true); + let body = mem::replace( + &mut method.function.body, + gen_proxy_body(&shadow_ident).into(), + ); + let params = method.function.params.drain(..).collect(); + let is_async = method.function.is_async; + ( + ClassMember::Method(ClassMethod { + key: private_ident, + accessibility: method.accessibility, + is_abstract: method.is_abstract, + is_optional: method.is_optional, + is_override: method.is_override, + function: method.function, + is_static: method.is_static, + kind: method.kind, + span: method.span, + }), + Some(gen_shadow_member( + &shadow_ident, + method.is_static, + body, + params, + is_async, + )), + ) + } + ClassMember::ClassProp(mut prop) => { + if let Some(ref mut v) = prop.value { + if let Some(func) = v.as_mut_arrow() { + let origin_ident = prop.key.as_ident().unwrap(); + let shadow_ident = get_shadow_ident(origin_ident, key, false); + let body = mem::replace( + &mut func.body, + Box::new(BlockStmtOrExpr::BlockStmt(gen_proxy_body(&shadow_ident))), + ); + let params = func.params.drain(..).map(|x| x.into()).collect(); + let is_async = func.is_async; + if let BlockStmtOrExpr::BlockStmt(body) = *body { + return ( + ClassMember::ClassProp(ClassProp { ..prop }), + Some(gen_shadow_member( + &shadow_ident, + prop.is_static, + Some(body), + params, + is_async, + )), + ); + } + } + } + (ClassMember::ClassProp(prop), None) + } + ClassMember::PrivateProp(mut prop) => { + let origin_ident = IdentName::new(prop.key.name, DUMMY_SP); + let private_ident = PropName::Ident(get_private_ident(&origin_ident, key)); + if let Some(ref mut v) = prop.value { + if let Some(func) = v.as_mut_arrow() { + let shadow_ident = get_shadow_ident(&origin_ident, key, true); + let body = mem::replace( + &mut func.body, + Box::new(BlockStmtOrExpr::BlockStmt(gen_proxy_body(&shadow_ident))), + ); + let params = func.params.drain(..).map(|x| x.into()).collect(); + let is_async = func.is_async; + if let BlockStmtOrExpr::BlockStmt(body) = *body { + return ( + ClassMember::ClassProp(ClassProp { + key: private_ident, + accessibility: prop.accessibility, + is_optional: prop.is_optional, + is_override: prop.is_override, + is_static: prop.is_static, + span: prop.span, + decorators: prop.decorators, + definite: prop.definite, + readonly: prop.readonly, + value: prop.value, + type_ann: prop.type_ann, + ..Default::default() + }), + Some(gen_shadow_member( + &shadow_ident, + prop.is_static, + Some(body), + params, + is_async, + )), + ); + } + } + } + ( + ClassMember::ClassProp(ClassProp { + key: private_ident, + accessibility: prop.accessibility, + is_optional: prop.is_optional, + is_override: prop.is_override, + is_static: prop.is_static, + span: prop.span, + decorators: prop.decorators, + definite: prop.definite, + readonly: prop.readonly, + value: prop.value, + type_ann: prop.type_ann, + ..Default::default() + }), + None, + ) + } + _ => (node, None), + } +} impl VisitMut for TransformVisitor { noop_visit_mut_type!(); - fn visit_mut_module_items(&mut self, node: &mut Vec) { + fn visit_mut_member_expr(&mut self, node: &mut MemberExpr) { node.visit_mut_children_with(self); - node.push(quote!( - " - if (module.hot) { - module.hot.accept(); + if let Some(private_name) = node.prop.as_private_name() { + node.prop = MemberProp::Ident(get_private_ident( + &IdentName::new(private_name.name.clone(), DUMMY_SP), + self.get_current_tag_name(), + )); + } + } + + fn visit_mut_class(&mut self, node: &mut Class) { + let tag_name = get_tag_name(node); + + if !tag_name.is_empty() { + self.tag_name_stack.push(tag_name.clone()); + node.visit_mut_children_with(self); + self.tag_name_stack.pop(); + + self.has_element = true; + + let mut body = vec![]; + while let Some(item) = node.body.pop() { + let (member, append) = transform_fn(item, &tag_name); + body.push(member); + + if let Some(member) = append { + body.push(member); + } } - " as ModuleItem, - )); + body.reverse(); + node.body = body; + } + } + + fn visit_mut_module_items(&mut self, node: &mut Vec) { + node.visit_mut_children_with(self); + + if self.has_element { + node.push(quote!( + " + if (module.hot) { + module.hot.accept(); + } + " as ModuleItem, + )); + } else { + node.push(quote!( + " + if (module.hot) { + module.hot.decline(); + } + " as ModuleItem, + )); + } } } diff --git a/crates/swc-plugin-gem/src/visitors/memo.rs b/crates/swc-plugin-gem/src/visitors/memo.rs index 7e324103..e1fefdb9 100644 --- a/crates/swc-plugin-gem/src/visitors/memo.rs +++ b/crates/swc-plugin-gem/src/visitors/memo.rs @@ -4,9 +4,9 @@ use swc_core::{ ecma::visit::{noop_visit_mut_type, VisitMut, VisitMutWith}, }; use swc_ecma_ast::{ - AssignExpr, AssignOp, AssignTarget, BlockStmt, Callee, Class, ClassDecl, ClassExpr, - ClassMember, Decorator, Expr, ExprStmt, Function, MemberExpr, MemberProp, MethodKind, - PrivateMethod, PrivateName, PrivateProp, SimpleAssignTarget, Stmt, ThisExpr, + AssignExpr, AssignOp, AssignTarget, BlockStmt, Callee, Class, ClassMember, Decorator, Expr, + ExprStmt, Function, MemberExpr, MemberProp, MethodKind, PrivateMethod, PrivateName, + PrivateProp, SimpleAssignTarget, Stmt, ThisExpr, }; fn is_memo_getter(node: &mut PrivateMethod) -> bool { @@ -31,8 +31,12 @@ struct TransformVisitor { private_props: Vec<(Atom, Vec, String)>, } -impl TransformVisitor { - fn append_private_field(&mut self, node: &mut Box) { +impl VisitMut for TransformVisitor { + noop_visit_mut_type!(); + + fn visit_mut_class(&mut self, node: &mut Class) { + node.visit_mut_children_with(self); + while let Some((prop, decorators, getter_name)) = self.private_props.pop() { node.body.push(ClassMember::PrivateMethod(PrivateMethod { span: DUMMY_SP, @@ -87,22 +91,6 @@ impl TransformVisitor { })); } } -} - -impl VisitMut for TransformVisitor { - noop_visit_mut_type!(); - - fn visit_mut_class_decl(&mut self, node: &mut ClassDecl) { - node.visit_mut_children_with(self); - - self.append_private_field(&mut node.class); - } - - fn visit_mut_class_expr(&mut self, node: &mut ClassExpr) { - node.visit_mut_children_with(self); - - self.append_private_field(&mut node.class); - } fn visit_mut_private_method(&mut self, node: &mut PrivateMethod) { if is_memo_getter(node) { @@ -114,8 +102,7 @@ impl VisitMut for TransformVisitor { name: getter_name.clone().into(), }; - let mut decorators = Vec::new(); - decorators.append(&mut node.function.decorators); + let decorators = node.function.decorators.drain(..).collect(); self.private_props .push((name.clone(), decorators, getter_name)); diff --git a/crates/swc-plugin-gem/src/visitors/minify.rs b/crates/swc-plugin-gem/src/visitors/minify.rs index 8ec583b3..f453bfbf 100644 --- a/crates/swc-plugin-gem/src/visitors/minify.rs +++ b/crates/swc-plugin-gem/src/visitors/minify.rs @@ -7,6 +7,7 @@ use swc_ecma_ast::{Callee, KeyValueProp, TaggedTpl, Tpl, TplElement}; static HEAD_REG: Lazy = Lazy::new(|| Regex::new(r"(?s)\s*(\{)\s*").unwrap()); static TAIL_REG: Lazy = Lazy::new(|| Regex::new(r"(?s)(;|})\s+").unwrap()); static SPACE_REG: Lazy = Lazy::new(|| Regex::new(r"(?s)\s+").unwrap()); +static COMMENT_REG: Lazy = Lazy::new(|| Regex::new(r"(?s)/\\*.*\\*/").unwrap()); fn minify_tpl(tpl: &Tpl) -> Tpl { Tpl { @@ -16,7 +17,8 @@ fn minify_tpl(tpl: &Tpl) -> Tpl { .quasis .iter() .map(|x| { - let remove_head = &HEAD_REG.replace_all(x.raw.as_str(), "$1"); + let remove_comment = &COMMENT_REG.replace_all(x.raw.as_str(), ""); + let remove_head = &HEAD_REG.replace_all(remove_comment, "$1"); let remove_tail = &TAIL_REG.replace_all(remove_head, "$1"); let remove_space = SPACE_REG.replace_all(remove_tail, " "); TplElement { diff --git a/crates/swc-plugin-gem/tests/fixture.rs b/crates/swc-plugin-gem/tests/fixture.rs index 50a054ae..09ca61d6 100644 --- a/crates/swc-plugin-gem/tests/fixture.rs +++ b/crates/swc-plugin-gem/tests/fixture.rs @@ -100,3 +100,16 @@ fn fixture_preload(input: PathBuf) { Default::default(), ); } + +#[fixture("tests/fixture/hmr/input.ts")] +fn fixture_hmr(input: PathBuf) { + let output = input.parent().unwrap().join("output.ts"); + + test_fixture( + get_syntax(), + &|_| visit_mut_pass(hmr_transform()), + &input, + &output, + Default::default(), + ); +} diff --git a/crates/swc-plugin-gem/tests/fixture/hmr/input.ts b/crates/swc-plugin-gem/tests/fixture/hmr/input.ts new file mode 100644 index 00000000..b1644df1 --- /dev/null +++ b/crates/swc-plugin-gem/tests/fixture/hmr/input.ts @@ -0,0 +1,21 @@ +// @ts-nocheck +@customElement('my-element') +class MyElement extends GemElement { + @effect([]) + method(arg) { + console.log('method'); + } + @effect([]) + field = (arg) => { + console.log('field'); + } + @effect([]) + #method(arg) { + console.log('#method'); + } + @effect([]) + #field = (arg) => { + console.log('#field'); + } + #content; +} \ No newline at end of file diff --git a/crates/swc-plugin-gem/tests/fixture/hmr/output.ts b/crates/swc-plugin-gem/tests/fixture/hmr/output.ts new file mode 100644 index 00000000..16ba3f92 --- /dev/null +++ b/crates/swc-plugin-gem/tests/fixture/hmr/output.ts @@ -0,0 +1,33 @@ +// @ts-nocheck +@customElement('my-element') +class MyElement extends GemElement { + _hmr_public_my_element_method(arg) { + console.log('method'); + } + @effect([]) + method() { + return this._hmr_public_my_element_method.bind(this)(...arguments); + } + _hmr_public_my_element_field(arg) { + console.log('field'); + } + @effect([]) + field = () => { + return this._hmr_public_my_element_field.bind(this)(...arguments); + }; + _hmr_private_my_element_method(arg) { + console.log('#method'); + } + @effect([]) + _private_my_element_method() { + return this._hmr_private_my_element_method.bind(this)(...arguments); + } + _hmr_private_my_element_field(arg) { + console.log('#field'); + } + @effect([]) + _private_my_element_field = () => { + return this._hmr_private_my_element_field.bind(this)(...arguments); + }; + _private_my_element_content; +} diff --git a/crates/swc-plugin-gem/tests/fixture/minify/input.ts b/crates/swc-plugin-gem/tests/fixture/minify/input.ts index 6584b16c..b9c8afc0 100644 --- a/crates/swc-plugin-gem/tests/fixture/minify/input.ts +++ b/crates/swc-plugin-gem/tests/fixture/minify/input.ts @@ -1,7 +1,11 @@ // @ts-nocheck const style = css` :host { + /* + * comment1 + */ color: ${' red'}; + /* comment2 */ } ` const style2 = css({ diff --git a/packages/gem-devtools/src/scripts/get-gem.ts b/packages/gem-devtools/src/scripts/get-gem.ts index f33655e0..1442e69c 100644 --- a/packages/gem-devtools/src/scripts/get-gem.ts +++ b/packages/gem-devtools/src/scripts/get-gem.ts @@ -40,7 +40,7 @@ export const getSelectedGem = function (data: PanelStore): PanelStore | string { if (arg === null) return 'null'; switch (typeof arg) { case 'function': - if (arg instanceof Function) return funcToString(arg); + if (window.__GEM_DEVTOOLS__PRELOAD__.isFunction(arg)) return funcToString(arg); // eslint-disable-next-line no-fallthrough case 'object': return '{...}'; @@ -55,7 +55,7 @@ export const getSelectedGem = function (data: PanelStore): PanelStore | string { if (arg === null) return 'null'; switch (typeof arg) { case 'function': - if (arg instanceof Function) return funcToString(arg); + if (window.__GEM_DEVTOOLS__PRELOAD__.isFunction(arg)) return funcToString(arg); // eslint-disable-next-line no-fallthrough case 'object': { if (arg instanceof Element) { @@ -97,7 +97,14 @@ export const getSelectedGem = function (data: PanelStore): PanelStore | string { const getProps = (obj: any, set = new Set()) => { Object.getOwnPropertyNames(obj).forEach((key) => { - if (key !== 'constructor') set.add(key); + if ( + key !== 'constructor' && + !key.startsWith('_hmr_') && + !key.startsWith('_private_') && + !key.startsWith('_$lit') + ) { + set.add(key); + } }); const proto = Object.getPrototypeOf(obj); if (proto !== HTMLElement.prototype) getProps(proto, set); diff --git a/packages/gem-devtools/src/scripts/preload.ts b/packages/gem-devtools/src/scripts/preload.ts index fbadea95..b586d6c2 100644 --- a/packages/gem-devtools/src/scripts/preload.ts +++ b/packages/gem-devtools/src/scripts/preload.ts @@ -1,9 +1,11 @@ import type { Path } from '../store'; type DevToolsHookPreload = { + isFunction: (path: any) => boolean; readProp: (path: Path) => any; traverseDom: (callback: (element: Element) => void) => void; }; + declare global { interface Window { __GEM_DEVTOOLS__PRELOAD__: DevToolsHookPreload; @@ -15,6 +17,10 @@ declare let $0: any; export function preload() { window.__GEM_DEVTOOLS__PRELOAD__ = { + // 支持 state/store 函数 + isFunction(value) { + return value instanceof Function && Object.getOwnPropertyNames(value).join() === 'length,name'; + }, // [["shadowRoot", ""], "querySelector", "[ref=child-ref]"] // 只有 constructor 函数会当成对象读取 readProp(path) { @@ -31,8 +37,7 @@ export function preload() { return c.reduce((pp, cc) => pp || (cc === '' ? p : p[cc]), undefined); } else { const value = p[c]; - // 支持 state 函数 - return value instanceof Function && c !== 'constructor' ? value.bind(p) : value; + return window.__GEM_DEVTOOLS__PRELOAD__.isFunction(value) && c !== 'constructor' ? value.bind(p) : value; } } }, $0); diff --git a/packages/gem/src/helper/hmr.ts b/packages/gem/src/helper/hmr.ts index a7236e43..537429c8 100644 --- a/packages/gem/src/helper/hmr.ts +++ b/packages/gem/src/helper/hmr.ts @@ -1,13 +1,94 @@ -/* eslint-disable no-console */ +import { cleanObject } from '../lib/utils'; +import { UpdateToken, type Metadata } from '../lib/element'; +import { type Store } from '../lib/store'; + +import { Logger } from './logger'; + +const logger = new Logger('HMR'); const nativeDefineElement = window.customElements.define.bind(window.customElements); +function updateElements(name: string) { + const temp: Element[] = [document.documentElement]; + while (!!temp.length) { + const element = temp.pop()!; + if (element.tagName.toLowerCase() === name) { + // update style + element.after(element); + + // re render + (element as any)[UpdateToken]?.(); + } + temp.push(...[...element.children, ...(element.shadowRoot?.children || [])].reverse()); + } +} + +function getMetadata(cons: any): Metadata { + return (cons as any)[Symbol.metadata] || {}; +} + +function stringifyStores(stores?: Store[]) { + return JSON.stringify( + stores?.map((store) => JSON.stringify({ ...store }, (_, v) => (typeof v === 'function' ? v.toString() : v))), + ); +} + +function getHmrMethodKeys(cons: CustomElementConstructor) { + return Object.getOwnPropertyNames(cons.prototype).filter((key) => key.startsWith('_hmr_')); +} + +function checkNeedReload(existed: CustomElementConstructor, newClass: CustomElementConstructor) { + const { mode, penetrable, noBlocking, aria, observedStores } = getMetadata(existed); + const newMetadata = getMetadata(newClass); + if ( + mode !== newMetadata.mode || + penetrable !== newMetadata.penetrable || + noBlocking !== newMetadata.noBlocking || + JSON.stringify(aria) !== JSON.stringify(newMetadata.aria) || + stringifyStores(observedStores) !== stringifyStores(newMetadata.observedStores) || + getHmrMethodKeys(existed).join() !== getHmrMethodKeys(newClass).join() + ) { + return true; + } +} + window.customElements.define = (...rest: Parameters) => { - const [name, con] = rest; - if (!customElements.get(name)) { + const [name, newClass] = rest; + const existed = customElements.get(name); + + if (!existed) { nativeDefineElement(...rest); - } else { - // update - console.log(name, con); + return; + } + + logger.info(`<${name}> update,`, { newClass }); + + if (checkNeedReload(existed, newClass)) { + location.reload(); + return; + } + + const oldMetadata = getMetadata(existed); + const newMetadata = getMetadata(newClass); + + if (oldMetadata.adoptedStyleSheets) { + oldMetadata.adoptedStyleSheets.length = 0; + oldMetadata.adoptedStyleSheets.push(...(newMetadata.adoptedStyleSheets || [])); } + + oldMetadata.observedStores?.forEach((store, index) => { + cleanObject(store); + Object.assign(store, newMetadata.observedStores![index]); + }); + + Object.assign( + existed.prototype, + Object.fromEntries( + getHmrMethodKeys(newClass) + .filter((key) => existed.prototype[key].toString() !== newClass.prototype[key].toString()) + .map((key) => [key, newClass.prototype[key]]), + ), + ); + + updateElements(name); }; diff --git a/packages/gem/src/helper/logger.ts b/packages/gem/src/helper/logger.ts index a75de94c..cd1ef73f 100644 --- a/packages/gem/src/helper/logger.ts +++ b/packages/gem/src/helper/logger.ts @@ -1,20 +1,24 @@ export class Logger { - _type: string; + #type: string; + + get #prefix() { + return `[${this.#type}]`; + } constructor(type: string) { - this._type = type; + this.#type = type; } info = (...args: any[]) => { - console.log(`[${this._type}]:`, ...args); + console.log(this.#prefix, ...args); }; warn = (...args: any[]) => { - console.warn(`[${this._type}]:`, ...args); + console.warn(this.#prefix, ...args); }; error = (...args: any[]) => { - console.error(`[${this._type}]:`, ...args); + console.error(this.#prefix, ...args); }; } diff --git a/packages/gem/src/lib/decorators.ts b/packages/gem/src/lib/decorators.ts index 7cb9c4ff..5d1a0bf0 100644 --- a/packages/gem/src/lib/decorators.ts +++ b/packages/gem/src/lib/decorators.ts @@ -220,7 +220,16 @@ export function memo( if (kind === 'getter') { // 不能设置私有字段 https://github.com/tc39/proposal-decorators/issues/509 if (context.private) throw new GemError('not support'); - this.memo(() => defineProperty(this, name, { configurable: true, value: access.get(this) }), dep); + this.memo( + () => + defineProperty(this, name, { + configurable: true, + // 这里需要 bind(this) 是为了兼容 swc + // https://github.com/swc-project/swc/issues/9565#issuecomment-2539107736 + value: access.get.bind(this)(this), + }), + dep, + ); } else { this.memo((access.get(this) as any).bind(this) as any, dep); } diff --git a/packages/gem/src/lib/element.ts b/packages/gem/src/lib/element.ts index 1afea9ba..a1bf75a9 100644 --- a/packages/gem/src/lib/element.ts +++ b/packages/gem/src/lib/element.ts @@ -256,7 +256,7 @@ export type Metadata = Partial & { } >; // 实例化时使用到,DevTools 需要读取 - observedStores: Store[]; + observedStores?: Store[]; adoptedStyleSheets?: Sheet[]; // 以下静态字段仅供外部读取,没有实际作用 observedProperties?: string[]; diff --git a/packages/gem/src/test/gem-element/decorators.test.ts b/packages/gem/src/test/gem-element/decorators.test.ts index 5f15ec34..dedbcf0f 100644 --- a/packages/gem/src/test/gem-element/decorators.test.ts +++ b/packages/gem/src/test/gem-element/decorators.test.ts @@ -81,7 +81,7 @@ describe('装饰器', () => { store({ a: 1 }); await Promise.resolve(); const metadata: Metadata = Reflect.get(DecoratorGemElement, Symbol.metadata); - expect({ ...metadata.observedStores.at(0) }).to.eql({ a: 1 }); + expect({ ...metadata.observedStores?.at(0) }).to.eql({ a: 1 }); expect(metadata.observedAttributes).to.eql(['rank-attr', 'rank-disabled', 'rank-count']); expect(metadata.definedEvents).to.eql(['say-hi']); expect(metadata.definedCSSStates).to.eql(['open-state']);