airx 0.2.2 → 0.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,50 +1,56 @@
1
- # airx
1
+ # airx
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/airx.svg)](https://www.npmjs.com/package/airx) [![build status](https://github.com/airxjs/airx/actions/workflows/check.yml/badge.svg?branch=main)](https://github.com/airxjs/airx/actions/workflows/check.yml)
4
4
 
5
5
  ☁️ Airx is a lightweight JSX web application framework.
6
6
 
7
- Airx is a front-end framework based on JSX, designed to provide a simple and straightforward solution for building web applications. While it does not include hooks like React, it offers a range of features to manage state and handle user interactions efficiently.
7
+ [中文文档](https://github.com/airxjs/airx/blob/main/README_CN.md)
8
+ |
9
+ [English Document](https://github.com/airxjs/airx/blob/main/README.md)
10
+
11
+ Airx is a frontend development framework based on `JSX` and `Signal`, aimed at providing a simple and direct solution for building web applications.
8
12
 
9
13
  ## Features
10
14
 
11
- - Create reaction values for managing dynamic data
12
- - Define components using JSX syntax
13
- - Lightweight and easy to learn
15
+ - Seamlessly integrates with [Signal](https://github.com/tc39/proposal-signals) and its ecosystem!
16
+ - Developed entirely using TypeScript, TypeScript-friendly
17
+ - Defines components using JSX functional syntax
18
+ - No hooks like React 😊
19
+ - Minimal API for easy learning
14
20
 
15
21
  ## Getting Started
16
22
 
17
- To get started with Airx, follow these steps:
23
+ To begin using Airx, follow these steps:
18
24
 
19
25
  1. Install Airx using npm or yarn:
20
26
 
21
- ```shell
22
- npm install airx
23
- ```
27
+ ```shell
28
+ npm install airx
29
+ ```
24
30
 
25
- 2. Import the necessary functions and components in your project:
31
+ 2. Import necessary functions and components into your project:
26
32
 
27
33
  ```javascript
28
34
  import * as airx from 'airx'
29
35
 
30
- // create a reaction value
31
- const outsideCount = airx.createSignal(1)
36
+ // All values based on Signal automatically trigger updates
37
+ const state = new Signal.State(1)
38
+ const computed = new Signal.Computed(() => state.get() + 100)
32
39
 
33
- // define a component
34
40
  function App() {
35
- // create a reaction value
36
- const innerCount = airx.createSignal(1)
41
+ const innerState = new Signal.State(1)
37
42
 
38
43
  const handleClick = () => {
39
- innerCount.value += 1
40
- outsideCount.value +=1
44
+ state.set(state.get() + 1)
45
+ innerState.set(innerState.get() + 1)
41
46
  }
42
47
 
43
- // return a render function
48
+ // Return a rendering function
44
49
  return () => (
45
50
  <button onClick={handleClick}>
46
- {innerCount.value}
47
- {outsideCount.value}
51
+ {state.get()}
52
+ {computed.get()}
53
+ {innerState.get()}
48
54
  </button>
49
55
  )
50
56
  }
@@ -53,18 +59,60 @@ const app = airx.createApp(<App />);
53
59
  app.mount(document.getElementById('app'));
54
60
  ```
55
61
 
62
+ ## API
63
+
64
+ We have only a few APIs because we pursue a minimal core design. In the future, we will also open up a plugin system.
65
+
66
+ ### createApp
67
+
68
+ Create an application instance.
69
+
70
+ ### provide
71
+
72
+ ```ts
73
+ function provide: <T = unknown>(key: unknown, value: T): ProvideUpdater<T>
74
+ ```
75
+
76
+ Inject a value downwards through the `context`, must be called synchronously directly or indirectly within a component.
77
+
78
+ ### inject
79
+
80
+ ```ts
81
+ function inject<T = unknown>(key: unknown): T | undefined
82
+ ```
83
+
84
+ Look up a specified value upwards through the `context`, must be called synchronously directly or indirectly within a component.
85
+
86
+ ### onMounted
87
+
88
+ ```ts
89
+ type MountedListener = () => (() => void) | void
90
+ function onMounted(listener: MountedListener): void
91
+ ```
92
+
93
+ Register a callback for when the DOM is mounted, must be called synchronously directly or indirectly within a component.
94
+
95
+ ### onUnmounted
96
+
97
+ ```ts
98
+ type UnmountedListener = () => void
99
+ function onUnmounted(listener: UnmountedListener): void
100
+ ```
101
+
102
+ Register a callback for when the DOM is unmounted, must be called synchronously directly or indirectly within a component.
103
+
56
104
  ## License
57
105
 
58
- This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
106
+ This project uses the MIT License. For detailed information, please refer to the [LICENSE](LICENSE) file.
59
107
 
60
- ## Contributing
108
+ ## Contribution
61
109
 
62
110
  Contributions are welcome! If you have any ideas, suggestions, or bug reports, please open an issue or submit a pull request.
63
111
 
64
- ## Acknowledgments
112
+ ## Acknowledgements
65
113
 
66
- We would like to thank all the contributors and supporters of the Airx project.
114
+ We want to thank all contributors and supporters of the Airx project.
67
115
 
68
116
  ---
69
117
 
70
- For more information, check out the [official documentation](https://github.com/airxjs/airx).
118
+ For more information, please refer to the [official documentation](https://github.com/airxjs/airx)
@@ -38,8 +38,9 @@ export interface AirxComponentLifecycle {
38
38
  onMounted: (listener: AirxComponentMountedListener) => void;
39
39
  onUnmounted: (listener: AirxComponentUnmountedListener) => void;
40
40
  }
41
+ type ProvideUpdater<T = unknown> = (newValue: T | ((old: T) => T)) => void;
41
42
  export type AirxComponentContext = AirxComponentLifecycle & {
42
- provide: <T = unknown>(key: unknown, value: T) => (newValue: T) => void;
43
+ provide: <T = unknown>(key: unknown, value: T) => ProvideUpdater<T>;
43
44
  inject: <T = unknown>(key: unknown) => T | undefined;
44
45
  };
45
46
  export declare function component<P = unknown>(comp: AirxComponent<P>): AirxComponent<P>;
@@ -1,8 +1,7 @@
1
- import { AirxElement } from './element';
2
1
  import { Plugin } from './render';
2
+ import { AirxComponent, AirxElement } from './element';
3
3
  export * from './types';
4
4
  export { Plugin } from './render';
5
- export { createSignal, Signal, watchSignal } from './reactive';
6
5
  export { Fragment, component, createElement, AirxComponent, AirxElement, AirxChildren, AirxComponentContext } from './element';
7
6
  export { inject, provide, onMounted, onUnmounted } from './render';
8
7
  export interface AirxApp {
@@ -10,4 +9,4 @@ export interface AirxApp {
10
9
  mount: (container: HTMLElement) => AirxApp;
11
10
  renderToHTML: () => Promise<string>;
12
11
  }
13
- export declare function createApp(element: AirxElement<any>): AirxApp;
12
+ export declare function createApp(element: AirxElement<any> | AirxComponent): AirxApp;
@@ -1,11 +1,11 @@
1
- const globalContext$1 = {
1
+ const globalContext = {
2
2
  current: null
3
3
  };
4
4
  function useContext() {
5
- if (globalContext$1.current == null) {
5
+ if (globalContext.current == null) {
6
6
  throw new Error('Unable to find a valid component context');
7
7
  }
8
- return globalContext$1.current;
8
+ return globalContext.current;
9
9
  }
10
10
  const onMounted = (listener) => {
11
11
  return useContext().onMounted(listener);
@@ -20,85 +20,65 @@ const provide = (key, value) => {
20
20
  return useContext().provide(key, value);
21
21
  };
22
22
 
23
- const isDev = localStorage.getItem('airx-dev');
23
+ const isPrintLogs = typeof process != 'undefined'
24
+ && process?.env?.NODE_ENV === 'development'
25
+ && process?.env?.AIRX_DEBUG === 'true';
24
26
  function createLogger(name) {
25
27
  function getPrintPrefix() {
26
28
  const date = new Date().toLocaleString();
27
29
  return `[${date}][${name}]`;
28
30
  }
29
31
  function debug(...args) {
30
- if (isDev)
32
+ if (isPrintLogs)
31
33
  console.log(getPrintPrefix(), ...args);
32
34
  }
33
35
  return { debug };
34
36
  }
35
37
 
36
- const airxElementSymbol = Symbol('airx-element');
37
- const airxReactiveDependenciesSymbol = Symbol('airx-dependencies');
38
-
39
- /** FIXME: 污染全局总是不好的 */
40
- const globalContext = {
41
- dependencies: new Set()
42
- };
43
- function createCollector() {
44
- const newDependencies = new Set();
45
- return {
46
- clear: () => newDependencies.clear(),
47
- complete: () => [...newDependencies.values()],
48
- collect: (process) => {
49
- const beforeDeps = globalContext.dependencies;
50
- globalContext.dependencies = newDependencies;
51
- const result = process();
52
- globalContext.dependencies = beforeDeps;
53
- return result;
54
- }
55
- };
56
- }
57
- function triggerRef(ref) {
58
- requestAnimationFrame(() => {
59
- const deps = Reflect.get(ref, airxReactiveDependenciesSymbol);
60
- for (const dep of deps) {
61
- dep();
62
- }
63
- });
38
+ let firstSignal = undefined;
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ const globalNS = (function () {
41
+ // the only reliable means to get the global object is
42
+ // `Function('return this')()`
43
+ // However, this causes CSP violations in Chrome apps.
44
+ if (typeof self !== 'undefined') {
45
+ return self;
46
+ }
47
+ if (typeof window !== 'undefined') {
48
+ return window;
49
+ }
50
+ if (typeof global !== 'undefined') {
51
+ return global;
52
+ }
53
+ throw new Error('unable to locate global object');
54
+ })();
55
+ // 通过函数包装来延迟加载 Signal
56
+ // 这对使用 Polyfill 的应用来说更友好
57
+ function getSignal() {
58
+ const globalSignal = globalNS['Signal'];
59
+ if (globalSignal == null)
60
+ throw new Error('Signal is undefined');
61
+ if (firstSignal == null)
62
+ firstSignal = globalSignal;
63
+ if (firstSignal !== globalSignal)
64
+ throw new Error('Signal have multiple instances');
65
+ return globalSignal;
64
66
  }
65
- function createRefObject(value) {
66
- const object = Object.create({ value });
67
- Reflect.defineProperty(object, airxReactiveDependenciesSymbol, {
68
- configurable: false,
69
- enumerable: false,
70
- writable: true,
71
- value: new Set()
72
- });
73
- return object;
67
+ function createWatch(notify) {
68
+ const signal = getSignal();
69
+ return new signal.subtle.Watcher(notify);
74
70
  }
75
- function watchSignal(ref, listener) {
76
- const deps = Reflect.get(ref, airxReactiveDependenciesSymbol);
77
- deps.add(listener);
78
- return () => { deps.delete(listener); };
71
+ function createComputed(computation, options) {
72
+ const signal = getSignal();
73
+ return new signal.Computed(computation, options);
79
74
  }
80
- function createSignal(obj) {
81
- const ref = createRefObject(obj);
82
- if (!globalContext.dependencies.has(ref)) {
83
- globalContext.dependencies.add(ref);
84
- }
85
- let value = ref.value;
86
- Reflect.defineProperty(ref, 'value', {
87
- get() {
88
- if (!globalContext.dependencies.has(ref)) {
89
- globalContext.dependencies.add(ref);
90
- }
91
- return value;
92
- },
93
- set(newValue) {
94
- value = newValue;
95
- triggerRef(ref);
96
- return value;
97
- }
98
- });
99
- return ref;
75
+ function isState(target) {
76
+ const signal = getSignal();
77
+ return target instanceof signal.State;
100
78
  }
101
79
 
80
+ const airxElementSymbol = Symbol('airx-element');
81
+
102
82
  /**
103
83
  * createElement 是用于创建 AirxElement 的工具函数
104
84
  */
@@ -177,7 +157,15 @@ class InnerAirxComponentContext {
177
157
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
158
  provide(key, value) {
179
159
  this.providedMap.set(key, value);
180
- return v => this.providedMap.set(key, v);
160
+ return v => {
161
+ if (typeof v === 'function') {
162
+ const old = this.providedMap.get(key);
163
+ const func = v;
164
+ this.providedMap.set(key, func(old));
165
+ return;
166
+ }
167
+ this.providedMap.set(key, v);
168
+ };
181
169
  }
182
170
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
171
  inject(key) {
@@ -372,9 +360,11 @@ function reconcileChildren(appContext, parentInstance, childrenElementArray) {
372
360
  // 添加 ref 处理
373
361
  if ('ref' in instance.memoProps) {
374
362
  context.onMounted(() => {
375
- if (instance.domRef) { // 如果组件有自己的 dom
376
- instance.memoProps.ref.value = instance.domRef;
377
- return () => instance.memoProps.ref.value = null;
363
+ const ref = instance.memoProps.ref;
364
+ // 如果组件有自己的 dom 并且 ref state
365
+ if (instance.domRef && isState(ref)) {
366
+ ref.set(instance.domRef);
367
+ return () => ref.set(undefined);
378
368
  }
379
369
  });
380
370
  }
@@ -439,32 +429,44 @@ function performUnitOfWork(pluginContext, instance, onUpdateRequire) {
439
429
  }
440
430
  // airx 组件
441
431
  if (typeof element?.type === 'function') {
442
- const collector = createCollector();
443
- if (instance.render == null) {
432
+ if (instance.signalWatcher == null) {
433
+ // Watch 是惰性的,只有当 Signal 被读取时才会触发 --!
434
+ const signalWatcher = createWatch(async () => {
435
+ instance.needReRender = true;
436
+ onUpdateRequire?.(instance);
437
+ queueMicrotask(() => {
438
+ signalWatcher.watch();
439
+ const paddings = signalWatcher.getPending();
440
+ for (const padding of paddings)
441
+ padding.get();
442
+ });
443
+ });
444
+ instance.signalWatcher = signalWatcher;
445
+ instance.context.addDisposer(() => signalWatcher.unwatch());
446
+ }
447
+ if (instance.childrenRender == null) {
444
448
  const component = element.type;
445
- const beforeContext = globalContext$1.current;
446
- globalContext$1.current = instance.context.getSafeContext();
447
- const componentReturnValue = collector.collect(() => component(instance.memoProps));
449
+ const beforeContext = globalContext.current;
450
+ globalContext.current = instance.context.getSafeContext();
451
+ const componentReturnValue = component(instance.memoProps);
448
452
  if (typeof componentReturnValue !== 'function') {
449
453
  throw new Error('Component must return a render function');
450
454
  }
451
- globalContext$1.current = beforeContext;
452
- instance.render = componentReturnValue;
453
- const children = collector.collect(() => instance.render?.());
455
+ globalContext.current = beforeContext;
456
+ instance.childrenRender = componentReturnValue;
457
+ const childrenComputed = createComputed(() => componentReturnValue());
458
+ instance.signalWatcher.watch(childrenComputed);
459
+ const children = childrenComputed.get();
454
460
  reconcileChildren(pluginContext, instance, childrenAsElements(children));
455
461
  }
456
462
  if (instance.needReRender) {
457
- const children = collector.collect(() => instance.render?.());
463
+ // 这里有个问题,如果是由于父组件导致的子组件渲染
464
+ // 直接使用 childrenComputed.get() 将读取到缓存值
465
+ // const children = instance.childrenRender?.()
466
+ const children = instance.childrenRender();
458
467
  reconcileChildren(pluginContext, instance, childrenAsElements(children));
459
468
  delete instance.needReRender;
460
469
  }
461
- // 处理依赖触发的更新
462
- collector.complete().forEach(ref => {
463
- instance.context.addDisposer(watchSignal(ref, () => {
464
- instance.needReRender = true;
465
- onUpdateRequire?.(instance);
466
- }));
467
- });
468
470
  }
469
471
  // 浏览器组件/标签
470
472
  if (typeof element?.type === 'string') {
@@ -669,7 +671,7 @@ function render$1(pluginContext, element, onComplete) {
669
671
  Object.keys(prevProps)
670
672
  .filter(isProperty)
671
673
  .filter(isGone(prevProps, nextProps))
672
- .forEach(name => dom.setAttribute(name, ''));
674
+ .forEach(name => dom.removeAttribute(name));
673
675
  // Set new or changed properties
674
676
  Object.keys(nextProps)
675
677
  .filter(isProperty)
@@ -1137,7 +1139,7 @@ class BasicLogic {
1137
1139
  Object.keys(prevProps)
1138
1140
  .filter(isProperty)
1139
1141
  .filter(isGone(prevProps, nextProps))
1140
- .forEach(name => dom.setAttribute(name, ''));
1142
+ .forEach(name => dom.removeAttribute(name));
1141
1143
  // Set new or changed properties
1142
1144
  Object.keys(nextProps)
1143
1145
  .filter(isProperty)
@@ -1190,23 +1192,30 @@ class PluginContext {
1190
1192
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1191
1193
  function createApp(element) {
1192
1194
  const appContext = new PluginContext();
1195
+ const ensureAsElement = (element) => {
1196
+ if (typeof element === 'function') {
1197
+ return createElement(element, {});
1198
+ }
1199
+ return element;
1200
+ };
1193
1201
  const app = {
1194
1202
  plugin: (...plugins) => {
1195
1203
  appContext.registerPlugin(...plugins);
1196
1204
  return app;
1197
1205
  },
1198
1206
  mount: (container) => {
1199
- render(appContext, element, container);
1207
+ container.innerHTML = ''; // 先清空再说
1208
+ render(appContext, ensureAsElement(element), container);
1200
1209
  return app;
1201
1210
  },
1202
1211
  renderToHTML: () => {
1203
1212
  return new Promise(resolve => {
1204
- render$1(appContext, element, resolve);
1213
+ render$1(appContext, ensureAsElement(element), resolve);
1205
1214
  });
1206
1215
  }
1207
1216
  };
1208
1217
  return app;
1209
1218
  }
1210
1219
 
1211
- export { Fragment, component, createApp, createElement, createSignal, inject, onMounted, onUnmounted, provide, watchSignal };
1220
+ export { Fragment, component, createApp, createElement, inject, onMounted, onUnmounted, provide };
1212
1221
  //# sourceMappingURL=index.js.map