airx 0.2.3 → 0.3.0

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,9 +38,11 @@ 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>;
47
+ export declare function createErrorRender(error: unknown): AirxComponentRender;
46
48
  export {};
@@ -1,13 +1,14 @@
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 {
9
- plugin: (...plugins: Plugin[]) => AirxApp;
10
8
  mount: (container: HTMLElement) => AirxApp;
9
+ /** @deprecated WIP */
10
+ plugin: (...plugins: Plugin[]) => AirxApp;
11
+ /** @deprecated WIP */
11
12
  renderToHTML: () => Promise<string>;
12
13
  }
13
- export declare function createApp(element: AirxElement<any>): AirxApp;
14
+ 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 = typeof process != 'undefined' && process?.env?.NODE_ENV === 'development';
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
  */
@@ -124,6 +104,29 @@ function Fragment(props) {
124
104
  function component(comp) {
125
105
  return comp;
126
106
  }
107
+ function createErrorRender(error) {
108
+ console.error(error);
109
+ const handleClick = () => {
110
+ // 点击输出错误是为了避免
111
+ // 页面上多个组件同时出错时
112
+ // 无法定位错误与之对应的组件
113
+ console.error(error);
114
+ };
115
+ const formattingError = () => {
116
+ if (error == null)
117
+ return 'Unknown rendering error';
118
+ if (error instanceof Error)
119
+ return error.message;
120
+ return JSON.stringify(error);
121
+ };
122
+ const errorBlockStyle = {
123
+ padding: '8px',
124
+ fontSize: '20px',
125
+ color: 'rgb(255,255,255)',
126
+ backgroundColor: 'rgb(255, 0, 0)',
127
+ };
128
+ return () => createElement('div', { style: errorBlockStyle, onClick: handleClick }, formattingError());
129
+ }
127
130
 
128
131
  class InnerAirxComponentContext {
129
132
  instance;
@@ -177,7 +180,15 @@ class InnerAirxComponentContext {
177
180
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
181
  provide(key, value) {
179
182
  this.providedMap.set(key, value);
180
- return v => this.providedMap.set(key, v);
183
+ return v => {
184
+ if (typeof v === 'function') {
185
+ const old = this.providedMap.get(key);
186
+ const func = v;
187
+ this.providedMap.set(key, func(old));
188
+ return;
189
+ }
190
+ this.providedMap.set(key, v);
191
+ };
181
192
  }
182
193
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
194
  inject(key) {
@@ -372,9 +383,11 @@ function reconcileChildren(appContext, parentInstance, childrenElementArray) {
372
383
  // 添加 ref 处理
373
384
  if ('ref' in instance.memoProps) {
374
385
  context.onMounted(() => {
375
- if (instance.domRef) { // 如果组件有自己的 dom
376
- instance.memoProps.ref.value = instance.domRef;
377
- return () => instance.memoProps.ref.value = null;
386
+ const ref = instance.memoProps.ref;
387
+ // 如果组件有自己的 dom 并且 ref state
388
+ if (instance.domRef && isState(ref)) {
389
+ ref.set(instance.domRef);
390
+ return () => ref.set(undefined);
378
391
  }
379
392
  });
380
393
  }
@@ -439,32 +452,65 @@ function performUnitOfWork(pluginContext, instance, onUpdateRequire) {
439
452
  }
440
453
  // airx 组件
441
454
  if (typeof element?.type === 'function') {
442
- const collector = createCollector();
443
- if (instance.render == null) {
455
+ if (instance.signalWatcher == null) {
456
+ // Watch 是惰性的,只有当 Signal 被读取时才会触发 --!
457
+ const signalWatcher = createWatch(async () => {
458
+ instance.needReRender = true;
459
+ onUpdateRequire?.(instance);
460
+ queueMicrotask(() => {
461
+ signalWatcher.watch();
462
+ const paddings = signalWatcher.getPending();
463
+ for (const padding of paddings)
464
+ padding.get();
465
+ });
466
+ });
467
+ instance.signalWatcher = signalWatcher;
468
+ instance.context.addDisposer(() => signalWatcher.unwatch());
469
+ }
470
+ if (instance.childrenRender == null) {
444
471
  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));
472
+ const beforeContext = globalContext.current;
473
+ globalContext.current = instance.context.getSafeContext();
474
+ let componentReturnValue = undefined;
475
+ try {
476
+ componentReturnValue = component(instance.memoProps);
477
+ }
478
+ catch (error) {
479
+ componentReturnValue = createErrorRender(error);
480
+ }
448
481
  if (typeof componentReturnValue !== 'function') {
449
- throw new Error('Component must return a render function');
482
+ const error = new Error('Component must return a render function');
483
+ componentReturnValue = createErrorRender(error.message);
450
484
  }
451
- globalContext$1.current = beforeContext;
452
- instance.render = componentReturnValue;
453
- const children = collector.collect(() => instance.render?.());
485
+ // restore context
486
+ globalContext.current = beforeContext;
487
+ instance.childrenRender = componentReturnValue;
488
+ const childrenComputed = createComputed(() => {
489
+ try {
490
+ return instance.childrenRender();
491
+ }
492
+ catch (error) {
493
+ return createErrorRender(error)();
494
+ }
495
+ });
496
+ instance.signalWatcher.watch(childrenComputed);
497
+ const children = childrenComputed.get();
454
498
  reconcileChildren(pluginContext, instance, childrenAsElements(children));
455
499
  }
456
500
  if (instance.needReRender) {
457
- const children = collector.collect(() => instance.render?.());
501
+ let children;
502
+ try {
503
+ // 如果是由于父组件导致的子组件渲染
504
+ // 直接使用 childrenComputed.get() 将读取到缓存值
505
+ // 因此这里使用 childrenRender 来更新 children 的值
506
+ children = instance.childrenRender();
507
+ }
508
+ catch (error) {
509
+ children = createErrorRender(error)();
510
+ }
458
511
  reconcileChildren(pluginContext, instance, childrenAsElements(children));
459
512
  delete instance.needReRender;
460
513
  }
461
- // 处理依赖触发的更新
462
- collector.complete().forEach(ref => {
463
- instance.context.addDisposer(watchSignal(ref, () => {
464
- instance.needReRender = true;
465
- onUpdateRequire?.(instance);
466
- }));
467
- });
468
514
  }
469
515
  // 浏览器组件/标签
470
516
  if (typeof element?.type === 'string') {
@@ -669,7 +715,7 @@ function render$1(pluginContext, element, onComplete) {
669
715
  Object.keys(prevProps)
670
716
  .filter(isProperty)
671
717
  .filter(isGone(prevProps, nextProps))
672
- .forEach(name => dom.setAttribute(name, ''));
718
+ .forEach(name => dom.removeAttribute(name));
673
719
  // Set new or changed properties
674
720
  Object.keys(nextProps)
675
721
  .filter(isProperty)
@@ -1137,7 +1183,7 @@ class BasicLogic {
1137
1183
  Object.keys(prevProps)
1138
1184
  .filter(isProperty)
1139
1185
  .filter(isGone(prevProps, nextProps))
1140
- .forEach(name => dom.setAttribute(name, ''));
1186
+ .forEach(name => dom.removeAttribute(name));
1141
1187
  // Set new or changed properties
1142
1188
  Object.keys(nextProps)
1143
1189
  .filter(isProperty)
@@ -1190,23 +1236,30 @@ class PluginContext {
1190
1236
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1191
1237
  function createApp(element) {
1192
1238
  const appContext = new PluginContext();
1239
+ const ensureAsElement = (element) => {
1240
+ if (typeof element === 'function') {
1241
+ return createElement(element, {});
1242
+ }
1243
+ return element;
1244
+ };
1193
1245
  const app = {
1194
1246
  plugin: (...plugins) => {
1195
1247
  appContext.registerPlugin(...plugins);
1196
1248
  return app;
1197
1249
  },
1198
1250
  mount: (container) => {
1199
- render(appContext, element, container);
1251
+ container.innerHTML = ''; // 先清空再说
1252
+ render(appContext, ensureAsElement(element), container);
1200
1253
  return app;
1201
1254
  },
1202
1255
  renderToHTML: () => {
1203
1256
  return new Promise(resolve => {
1204
- render$1(appContext, element, resolve);
1257
+ render$1(appContext, ensureAsElement(element), resolve);
1205
1258
  });
1206
1259
  }
1207
1260
  };
1208
1261
  return app;
1209
1262
  }
1210
1263
 
1211
- export { Fragment, component, createApp, createElement, createSignal, inject, onMounted, onUnmounted, provide, watchSignal };
1264
+ export { Fragment, component, createApp, createElement, inject, onMounted, onUnmounted, provide };
1212
1265
  //# sourceMappingURL=index.js.map