From 56be1a1024804a55afbdcaea5b03c610d53d0215 Mon Sep 17 00:00:00 2001 From: mantou132 <709922234@qq.com> Date: Mon, 30 Dec 2024 00:20:59 +0800 Subject: [PATCH] [gem] Improve hmr --- .zed/settings.json | 19 + crates/swc-plugin-gem/package.json | 2 +- crates/swc-plugin-gem/src/lib.rs | 2 +- crates/swc-plugin-gem/src/visitors/hmr.rs | 347 ++++++++++++++---- crates/swc-plugin-gem/tests/fixture.rs | 2 +- .../swc-plugin-gem/tests/fixture/hmr/input.ts | 13 +- .../tests/fixture/hmr/output.ts | 90 +++-- packages/gem/src/helper/hmr.ts | 326 +++++++++++++--- packages/gem/src/lib/decorators.ts | 3 +- 9 files changed, 634 insertions(+), 170 deletions(-) create mode 100644 .zed/settings.json diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000..86192e72 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,19 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#settings-files +{ + "auto_install_extension": { + "biome": true, + "emmet": true, + "toml": true, + "html": true, + "gem": true + }, + "tab_size": 2, + "languages": { + "Rust": { + "tab_size": 4 + } + } +} diff --git a/crates/swc-plugin-gem/package.json b/crates/swc-plugin-gem/package.json index 344c384e..3e635c99 100644 --- a/crates/swc-plugin-gem/package.json +++ b/crates/swc-plugin-gem/package.json @@ -1,6 +1,6 @@ { "name": "swc-plugin-gem", - "version": "0.1.5", + "version": "0.1.6", "description": "swc plugin for Gem", "keywords": [ "swc-plugin", diff --git a/crates/swc-plugin-gem/src/lib.rs b/crates/swc-plugin-gem/src/lib.rs index 5f8db122..395419de 100644 --- a/crates/swc-plugin-gem/src/lib.rs +++ b/crates/swc-plugin-gem/src/lib.rs @@ -74,7 +74,7 @@ pub fn process_transform(mut program: Program, data: TransformPluginProgramMetad }, Optional { enabled: config.hmr, - visitor: hmr_transform(), + visitor: hmr_transform(filename.clone()), }, )); diff --git a/crates/swc-plugin-gem/src/visitors/hmr.rs b/crates/swc-plugin-gem/src/visitors/hmr.rs index eab68925..809c13cc 100644 --- a/crates/swc-plugin-gem/src/visitors/hmr.rs +++ b/crates/swc-plugin-gem/src/visitors/hmr.rs @@ -1,62 +1,89 @@ //! https://rspack.dev/api/runtime-api/hmr //! -//! - 将私有字段转译成公开字段 -//! - 为函数成员(方法、getter、setter、字段,包括静态的)添加影子方法 -//! - 调用 HMR API +//! - 将私有成员转译成公开成员,同时修改所有私有成员访问 +//! - 为函数成员(方法、getter、setter、字段,包括静态的)添加影子方法, +//! 在运行时进行替换,不支持计算属性名 +//! - 调用 HMR API:模块中有元素定义就接受、否则冒泡 +//! - 收集所有非函数字段名称及其装饰器,在运行时进行比较和更新 -use std::{mem, vec}; +use std::{ + hash::{DefaultHasher, Hash, Hasher}, + mem, vec, +}; use once_cell::sync::Lazy; use regex::Regex; use swc_common::DUMMY_SP; use swc_core::{ + atoms::Atom, ecma::visit::{noop_visit_mut_type, VisitMut, VisitMutWith}, quote, }; use swc_ecma_ast::{ - BlockStmt, BlockStmtOrExpr, Callee, Class, ClassMember, ClassMethod, ClassProp, Expr, - ExprOrSpread, Function, Ident, IdentName, Lit, MemberExpr, MemberProp, ModuleItem, Param, - PropName, ThisExpr, + ArrayLit, ArrowExpr, BlockStmt, BlockStmtOrExpr, Callee, Class, ClassMember, ClassMethod, + ClassProp, Decorator, Expr, ExprOrSpread, Function, Ident, IdentName, Lit, MemberExpr, + MemberProp, MethodKind, ModuleItem, Param, Pat, PropName, RestPat, StaticBlock, ThisExpr, }; static DASH_REG: Lazy = Lazy::new(|| Regex::new(r"-").unwrap()); +static HASH_KEY_PREFIX: &str = "hash_"; + +fn hash_string(s: &str) -> u64 { + let mut hasher = DefaultHasher::new(); + s.hash(&mut hasher); + hasher.finish() +} #[derive(Default)] struct TransformVisitor { + filename: String, + class_index: usize, has_element: bool, - tag_name_stack: Vec, + class_stack: Vec, } impl TransformVisitor { fn get_current_tag_name(&self) -> &str { - self.tag_name_stack.last().unwrap() + self.class_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; + fn get_class_name(&mut self, node: &mut Class) -> String { + let tag_name = 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(); + 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() + Default::default() + }); + if let Some(tag_name) = tag_name { + return tag_name; + } + if self.filename.is_empty() { + return String::new(); + } + self.class_index += 1; + format!( + "{}{:x}", + HASH_KEY_PREFIX, + hash_string(&format!("{}{}", self.filename, self.class_index)) + ) + } } fn get_shadow_ident(origin_ident: &IdentName, key: &str, is_private: bool) -> IdentName { @@ -98,34 +125,90 @@ fn gen_shadow_member( }) } -fn gen_proxy_body(shadow_ident: &IdentName) -> BlockStmt { +fn gen_proxy_arg() -> Vec { + vec![Pat::Rest(RestPat { + arg: Box::new(Pat::Ident("args".into())), + dot3_token: DUMMY_SP, + span: DUMMY_SP, + type_ann: None, + })] +} + +fn gen_proxy_body(shadow_ident: &IdentName, is_getter: bool) -> 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 - )], + stmts: vec![if is_getter { + quote!( + " + return $expr.bind(this)(); + " as Stmt, + expr: Expr = this_expr + ) + } else { + quote!( + " + return $expr.bind(this)(...args); + " as Stmt, + expr: Expr = this_expr + ) + }], ..Default::default() } } +fn replace_to_proxy_function( + func: &mut Function, + shadow_ident: &IdentName, + is_getter: bool, +) -> (Option, Vec) { + if is_getter { + return ( + mem::replace(&mut func.body, gen_proxy_body(shadow_ident, true).into()), + vec![], + ); + } + ( + mem::replace(&mut func.body, gen_proxy_body(shadow_ident, false).into()), + mem::replace( + &mut func.params, + gen_proxy_arg().drain(..).map(|x| x.into()).collect(), + ), + ) +} + +fn replace_to_proxy_arrow( + func: &mut ArrowExpr, + shadow_ident: &IdentName, +) -> (Box, Vec) { + ( + mem::replace( + &mut func.body, + Box::new(BlockStmtOrExpr::BlockStmt(gen_proxy_body( + shadow_ident, + false, + ))), + ), + mem::replace(&mut func.params, gen_proxy_arg()) + .drain(..) + .map(|x| x.into()) + .collect(), + ) +} + 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 (body, params) = replace_to_proxy_function( + &mut method.function, + &shadow_ident, + method.kind == MethodKind::Getter, ); - let params = method.function.params.drain(..).collect(); let is_async = method.function.is_async; ( ClassMember::Method(ClassMethod { ..method }), @@ -145,11 +228,11 @@ fn transform_fn(node: ClassMember, key: &str) -> (ClassMember, Option (ClassMember, Option (ClassMember, Option (ClassMember, Option FieldProp { + FieldProp { + name: name.to_string(), + kind: kind.to_string(), + ..Default::default() + } + } + + fn set_static(mut self, is_static: bool) -> FieldProp { + self.is_static = is_static; + self + } +} + +fn get_field_type(sym: &Atom, decorators: &Vec) -> FieldProp { + let name = sym.as_str(); + for x in decorators { + if let Some(ident) = x.expr.as_ident() { + return match ident.sym.as_str() { + kind @ ("attribute" | "boolattribute" | "numattribute" | "property" | "emitter" + | "globalemitter" | "state" | "part" | "slot") => FieldProp::new(name, kind), + _ => FieldProp::new(name, "other"), + }; + } + } + FieldProp::new(name, "other") +} + +fn get_field(origin_member: &ClassMember) -> Option { + match origin_member { + ClassMember::ClassProp(prop) => { + if let Some(ident) = prop.key.as_ident() { + return Some( + get_field_type(&ident.sym, &prop.decorators).set_static(prop.is_static), + ); + } + None + } + ClassMember::PrivateProp(prop) => { + Some(get_field_type(&prop.key.name, &prop.decorators).set_static(prop.is_static)) + } + _ => None, + } +} + +fn gen_hmr_props(props: Vec>) -> ClassMember { + let elements = props + .iter() + .filter(|x| x.is_some()) + .map(|x| { + x.as_ref().map( + |FieldProp { + name, + kind, + is_static, + }| { + let p_name = Expr::Lit(Lit::Str(name.clone().into())); + let p_type = Expr::Lit(Lit::Str(kind.clone().into())); + let p_static = Expr::Lit(Lit::Bool((*is_static).into())); + ExprOrSpread { + spread: None, + expr: Box::new(quote!( + " + [$prop_name, $field_type, $is_static] + " as Expr, + prop_name: Expr = p_name, + field_type: Expr = p_type, + is_static: Expr = p_static, + )), + } + }, + ) + }) + .collect(); + let arr_expr = Expr::Array(ArrayLit { + elems: elements, + ..Default::default() + }); + ClassMember::StaticBlock(StaticBlock { + body: BlockStmt { + stmts: vec![quote!( + " + this._defined_fields_ = $arr_expr; + " as Stmt, + arr_expr: Expr = arr_expr + )], + ..Default::default() + }, + ..Default::default() + }) +} + +fn gen_register_class(name: &str) -> Decorator { + let name = Expr::Lit(Lit::Str(name.into())); + Decorator { + expr: Box::new(quote!( + " + (window._hmrRegisterClass ? _hmrRegisterClass($key) : Function.prototype) + " as Expr, + key: Expr = name, + )), + ..Default::default() + } +} + impl VisitMut for TransformVisitor { noop_visit_mut_type!(); @@ -275,26 +463,38 @@ impl VisitMut for TransformVisitor { } fn visit_mut_class(&mut self, node: &mut Class) { - let tag_name = get_tag_name(node); + let class_name = self.get_class_name(node); - if !tag_name.is_empty() { - self.tag_name_stack.push(tag_name.clone()); + if !class_name.is_empty() { + self.class_stack.push(class_name.clone()); node.visit_mut_children_with(self); - self.tag_name_stack.pop(); + self.class_stack.pop(); - self.has_element = true; + if !class_name.starts_with(HASH_KEY_PREFIX) { + self.has_element = true; + } + let mut props = vec![]; let mut body = vec![]; while let Some(item) = node.body.pop() { - let (member, append) = transform_fn(item, &tag_name); - body.push(member); + let (origin_member, append) = transform_fn(item, &class_name); + + // 不是函数成员,进行记录 + if append.is_none() { + props.push(get_field(&origin_member)); + } + + body.push(origin_member); - if let Some(member) = append { - body.push(member); + if let Some(shadow_member) = append { + body.push(shadow_member); } } body.reverse(); node.body = body; + + node.body.push(gen_hmr_props(props)); + node.decorators.push(gen_register_class(&class_name)); } } @@ -305,15 +505,7 @@ impl VisitMut for TransformVisitor { node.push(quote!( " if (module.hot) { - module.hot.accept(); - } - " as ModuleItem, - )); - } else { - node.push(quote!( - " - if (module.hot) { - module.hot.decline(); + module.hot.accept(); } " as ModuleItem, )); @@ -321,6 +513,9 @@ impl VisitMut for TransformVisitor { } } -pub fn hmr_transform() -> impl VisitMut { - TransformVisitor::default() +pub fn hmr_transform(filename: Option) -> impl VisitMut { + TransformVisitor { + filename: filename.unwrap_or_default(), + ..Default::default() + } } diff --git a/crates/swc-plugin-gem/tests/fixture.rs b/crates/swc-plugin-gem/tests/fixture.rs index 09ca61d6..d254a4f7 100644 --- a/crates/swc-plugin-gem/tests/fixture.rs +++ b/crates/swc-plugin-gem/tests/fixture.rs @@ -107,7 +107,7 @@ fn fixture_hmr(input: PathBuf) { test_fixture( get_syntax(), - &|_| visit_mut_pass(hmr_transform()), + &|_| visit_mut_pass(hmr_transform(None)), &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 index b1644df1..f2877b6e 100644 --- a/crates/swc-plugin-gem/tests/fixture/hmr/input.ts +++ b/crates/swc-plugin-gem/tests/fixture/hmr/input.ts @@ -1,6 +1,10 @@ // @ts-nocheck @customElement('my-element') class MyElement extends GemElement { + @emitter change; + get src() { + return 1; + } @effect([]) method(arg) { console.log('method'); @@ -8,14 +12,15 @@ class MyElement extends GemElement { @effect([]) field = (arg) => { console.log('field'); - } + }; @effect([]) #method(arg) { console.log('#method'); } - @effect([]) + @effect((i) => [i.#field]) #field = (arg) => { console.log('#field'); - } + }; #content; -} \ No newline at end of file + @state open; +} diff --git a/crates/swc-plugin-gem/tests/fixture/hmr/output.ts b/crates/swc-plugin-gem/tests/fixture/hmr/output.ts index 16ba3f92..0dc9e03f 100644 --- a/crates/swc-plugin-gem/tests/fixture/hmr/output.ts +++ b/crates/swc-plugin-gem/tests/fixture/hmr/output.ts @@ -1,33 +1,65 @@ // @ts-nocheck @customElement('my-element') +@(window._hmrRegisterClass ? _hmrRegisterClass("my-element") : Function.prototype) 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; + @emitter + change; + _hmr_public_my_element_src() { + return 1; + } + get src() { + return this._hmr_public_my_element_src.bind(this)(); + } + _hmr_public_my_element_method(arg) { + console.log('method'); + } + @effect([]) + method(...args) { + return this._hmr_public_my_element_method.bind(this)(...args); + } + _hmr_public_my_element_field(arg) { + console.log('field'); + } + @effect([]) + field = (...args)=>{ + return this._hmr_public_my_element_field.bind(this)(...args); + }; + _hmr_private_my_element_method(arg) { + console.log('#method'); + } + @effect([]) + _private_my_element_method(...args) { + return this._hmr_private_my_element_method.bind(this)(...args); + } + _hmr_private_my_element_field(arg) { + console.log('#field'); + } + @effect((i)=>[ + i._private_my_element_field + ]) + _private_my_element_field = (...args)=>{ + return this._hmr_private_my_element_field.bind(this)(...args); + }; + _private_my_element_content; + @state + open; + static{ + this._defined_fields_ = [ + [ + "open", + "state", + false + ], + [ + "_private_my_element_content", + "other", + false + ], + [ + "change", + "emitter", + false + ] + ]; + } } diff --git a/packages/gem/src/helper/hmr.ts b/packages/gem/src/helper/hmr.ts index 537429c8..4bec01d4 100644 --- a/packages/gem/src/helper/hmr.ts +++ b/packages/gem/src/helper/hmr.ts @@ -1,6 +1,6 @@ -import { cleanObject } from '../lib/utils'; -import { UpdateToken, type Metadata } from '../lib/element'; -import { type Store } from '../lib/store'; +import { GemElement, UpdateToken, type Metadata } from '../lib/element'; +import { property, attribute, numattribute, boolattribute, emitter, state } from '../lib/decorators'; +import type { Store } from '../lib/store'; import { Logger } from './logger'; @@ -8,87 +8,299 @@ 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]?.(); +const cache = new Map(); +function updateElement(name: string, fn: (ele: Element) => void) { + if (!cache.has(name)) { + const elements = []; + const temp: Element[] = [document.documentElement]; + while (!!temp.length) { + const element = temp.pop()!; + if (element.tagName.toLowerCase() === name) elements.push(element); + temp.push(...[...element.children, ...(element.shadowRoot?.children || [])].reverse()); } - temp.push(...[...element.children, ...(element.shadowRoot?.children || [])].reverse()); + cache.set(name, elements); + } + const elements = cache.get(name)!; + if (elements.length === 0) { + const ele = document.createElement(name); + Element.prototype.remove.apply(ele); + fn(ele); + } else { + elements.forEach(fn); } + queueMicrotask(() => cache.delete(name)); } 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 getHmrMethodKeys(obj: any) { + return Object.getOwnPropertyNames(obj).filter((key) => key.startsWith('_hmr_')); } -function checkNeedReload(existed: CustomElementConstructor, newClass: CustomElementConstructor) { - const { mode, penetrable, noBlocking, aria, observedStores } = getMetadata(existed); - const newMetadata = getMetadata(newClass); +/** 不支持修改 store */ +function checkMetadataNeedReload({ mode, penetrable, noBlocking, observedStores }: Metadata, newMetadata: Metadata) { + const stringifyStores = (stores?: Store[]) => + JSON.stringify( + stores?.map((store) => JSON.stringify({ ...store }, (_, v) => (typeof v === 'function' ? v.toString() : v))), + ); 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() + stringifyStores(observedStores) !== stringifyStores(newMetadata.observedStores) ) { return true; } } -window.customElements.define = (...rest: Parameters) => { - const [name, newClass] = rest; - const existed = customElements.get(name); +/** 不支持删减函数字段,因为原函数可能已经被绑定 */ +function checkClassNeedReload(existed: CustomElementConstructor, newClass: CustomElementConstructor) { + if ( + getHmrMethodKeys(existed).join() !== getHmrMethodKeys(newClass).join() || + getHmrMethodKeys(existed.prototype).join() !== getHmrMethodKeys(newClass.prototype).join() + ) { + return true; + } +} - if (!existed) { - nativeDefineElement(...rest); - return; +/** 不支持删减实例其他字段,因为新实例总是使用老 Class 定义 */ +function checkFieldsNeedReload({ instanceFields }: ReturnType) { + if (instanceFields.other?.add?.length || instanceFields.other?.remove?.length) { + return true; } +} - logger.info(`<${name}> update,`, { newClass }); +function diffArr(oldList: T[], newList: T[], fn: (i: T) => string = (e) => String(e)) { + const getMap = (list: T[]) => new Map(list.map((e) => [fn(e), e])); + const oldMap = getMap(oldList); + const newMap = getMap(newList); + const result = { remove: [] as T[], add: [] as T[], some: [] as T[], all: [] as T[] }; + getMap([...oldList, ...newList]).forEach((e, k) => { + result.all.push(e); + if (newMap.has(k) && oldMap.has(k)) { + result.some.push(e); + } else if (!newMap.has(k)) { + result.remove.push(e); + } else if (!oldMap.has(k)) { + result.add.push(e); + } + }); + return result; +} - if (checkNeedReload(existed, newClass)) { - location.reload(); - return; - } +function diffRecord(oldRecord: Partial>, newRecord: Partial>) { + const oldKeys = Object.keys(oldRecord); + const newKeys = Object.keys(newRecord); + return diffArr(oldKeys, newKeys); +} - const oldMetadata = getMetadata(existed); - const newMetadata = getMetadata(newClass); +type HasFieldsRecordClass = { _defined_fields_?: [string, string, boolean][] }; +function getFields(existed: CustomElementConstructor, newClass: CustomElementConstructor) { + const translate = (cls: HasFieldsRecordClass) => { + const fieldList = (cls._defined_fields_ || []).map(([name, type, isStatic]) => ({ name, type, isStatic })); + const fieldGroup = Object.groupBy(fieldList, ({ isStatic }) => (isStatic ? 'staticFields' : 'instanceFields')); + const groupBy = (list: typeof fieldGroup.staticFields) => Object.groupBy(list || [], ({ type }) => type); + return { + staticFields: groupBy(fieldGroup.staticFields), + instanceFields: groupBy(fieldGroup.instanceFields), + }; + }; + const oldFields = translate(existed as unknown as HasFieldsRecordClass); + const newFields = translate(newClass as unknown as HasFieldsRecordClass); + const result = Object.fromEntries( + Object.entries(oldFields).map(([classFieldType]: [keyof typeof oldFields, any]) => [ + classFieldType, + Object.fromEntries( + diffRecord(oldFields[classFieldType], newFields[classFieldType]).all.map((type) => [ + type, + diffArr(oldFields[classFieldType][type] || [], newFields[classFieldType][type] || [], ({ name }) => name), + ]), + ), + ]), + ); + return result as Record; +} - if (oldMetadata.adoptedStyleSheets) { - oldMetadata.adoptedStyleSheets.length = 0; - oldMetadata.adoptedStyleSheets.push(...(newMetadata.adoptedStyleSheets || [])); +const deleteProperty = Reflect.deleteProperty; +const getProperty = Reflect.get; +const getPrototypeOf = Reflect.getPrototypeOf; +const setProperty = (target: any, propertyKey: string | symbol, value: any) => + Reflect.defineProperty(target, propertyKey, { + configurable: true, + enumerable: true, + writable: true, + value, + }); +const safeProp = new Set(['part', 'slot', 'other']); +const descMap = { + attribute, + boolattribute, + numattribute, + property, + state, + emitter, +}; + +function setArrValue(obj: any, field: string, val: T[] = []) { + if (!Object.getOwnPropertyDescriptor(obj, field)) { + setProperty(obj, field, val); + } else { + const old = obj[field] as T[]; + old.length = 0; + old.push(...val); } +} - oldMetadata.observedStores?.forEach((store, index) => { - cleanObject(store); - Object.assign(store, newMetadata.observedStores![index]); - }); +declare global { + interface Window { + _hmrClassRegistry: Map; + _hmrRegisterClass: (name: string) => (cls: HasFieldsRecordClass, ctx: ClassDecoratorContext) => void; + } +} - Object.assign( - existed.prototype, - Object.fromEntries( - getHmrMethodKeys(newClass) - .filter((key) => existed.prototype[key].toString() !== newClass.prototype[key].toString()) - .map((key) => [key, newClass.prototype[key]]), - ), - ); +window._hmrClassRegistry = new Map(); +window._hmrRegisterClass = function (name: string) { + return function (cls: HasFieldsRecordClass, { addInitializer }: ClassDecoratorContext) { + addInitializer(() => { + const existed = window._hmrClassRegistry.get(name); + + if (!existed) { + window._hmrClassRegistry.set(name, cls); + return; + } + + logger.info(`class <${name}> update,`, { existed, cls }); + + let current: any[] = [existed, cls]; + while (current) { + const [oldClass, newClass] = current; + if (oldClass === newClass || current.some((e) => e === GemElement || e === Function.prototype)) break; + if (current.some((e) => !e) || checkClassNeedReload(oldClass, newClass)) { + location.reload(); + return; + } + + // 修正 `instanceof` + setProperty(newClass, Symbol.hasInstance, function hasInstance(instance: any) { + const isOldInstance = instance instanceof oldClass; + if (isOldInstance) return true; + setProperty(newClass, Symbol.hasInstance, undefined); + const isNew = instance instanceof newClass; + setProperty(newClass, Symbol.hasInstance, hasInstance); + return isNew; + }); + + const oldMetadata = getMetadata(oldClass); + const newMetadata = getMetadata(newClass); + + if (checkMetadataNeedReload(oldMetadata, newMetadata)) { + location.reload(); + return; + } + + const fields = getFields(oldClass, newClass); + + if (checkFieldsNeedReload(fields)) { + location.reload(); + return; + } + + Object.entries(fields.staticFields).forEach(([type, { remove, add }]) => { + remove.forEach((prop) => { + if (safeProp.has(type)) { + deleteProperty(oldClass, prop.name); + } + }); + add.forEach((prop) => { + if (safeProp.has(type)) { + setProperty(oldClass, prop.name, getProperty(newClass, prop.name)); + } + }); + }); + + const instanceFieldsEntries = Object.entries(fields.instanceFields); + // 删除的属性重新加回来要先清除占位值 + instanceFieldsEntries.forEach(([type, { add }]) => { + add.forEach((prop) => { + if (type in descMap) { + deleteProperty(oldClass.prototype, prop.name); + } + }); + }); + updateElement(name, (element) => { + instanceFieldsEntries.forEach(([type, { add, remove }]) => { + add.forEach((prop) => { + if (type in descMap) { + (descMap as any)[type](undefined, { + name: prop.name, + metadata: oldMetadata, + addInitializer: (fn: () => void) => fn.apply(element), + } as ClassMemberDecoratorContext); + } + }); + remove.forEach((prop) => { + setProperty(element, prop.name, undefined); + }); + }); + }); + // 删除的属性在 proto 上添加占位符 + instanceFieldsEntries.forEach(([type, { remove }]) => { + remove.forEach((prop) => { + if (type in descMap) { + // attribute 映射 prop 没有移除 + setProperty(oldClass.prototype, prop.name, undefined); + } + }); + }); - updateElements(name); + setArrValue(oldMetadata, 'definedParts', newMetadata.definedParts); + setArrValue(oldMetadata, 'definedSlots', newMetadata.definedSlots); + setArrValue(oldMetadata, 'observedProperties', newMetadata.observedProperties); + setArrValue(oldMetadata, 'observedAttributes', newMetadata.observedAttributes); + setArrValue(oldMetadata, 'definedEvents', newMetadata.definedEvents); + setArrValue(oldMetadata, 'definedCSSStates', newMetadata.definedCSSStates); + setArrValue(oldMetadata, 'adoptedStyleSheets', newMetadata.adoptedStyleSheets); + setArrValue(oldClass, '_defined_fields_', newClass._defined_fields_); + + [ + [oldClass, newClass], + [oldClass.prototype, newClass.prototype], + ].forEach(([existedObj, newObj]) => { + Object.assign( + existedObj, + Object.fromEntries( + getHmrMethodKeys(newObj) + .filter((key) => existedObj[key].toString() !== newObj[key].toString()) + .map((key) => [key, newObj[key]]), + ), + ); + }); + + current = [getPrototypeOf(oldClass), getPrototypeOf(newClass)]; + } + }); + }; +}; + +window.customElements.define = (name: string, cls: CustomElementConstructor) => { + const existed = customElements.get(name); + + if (!existed) { + nativeDefineElement(name, cls); + return; + } + + // 等待类更新 + queueMicrotask(() => { + logger.info(`<${name}> update,`, { cls }); + + updateElement(name, (element) => { + // 触发样式更新,支持 Light DOM + element.after(element); + // 重新渲染 + (element as any)[UpdateToken]?.(); + }); + }); }; diff --git a/packages/gem/src/lib/decorators.ts b/packages/gem/src/lib/decorators.ts index 5d1a0bf0..7255def9 100644 --- a/packages/gem/src/lib/decorators.ts +++ b/packages/gem/src/lib/decorators.ts @@ -27,7 +27,8 @@ function pushStaticField(context: ClassFieldDecoratorContext | ClassDecoratorCon } function clearField(instance: T, prop: string) { - const { value } = getOwnPropertyDescriptor(instance, prop)!; + // hmr 支持不存在属性 + const { value } = getOwnPropertyDescriptor(instance, prop) || {}; deleteProperty(instance, prop); (instance as any)[prop] = value; }