@cascateer/core 2.0.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/.github/workflows/publish.yml +35 -0
- package/package.json +31 -0
- package/src/api.ts +188 -0
- package/src/app.ts +36 -0
- package/src/component.ts +153 -0
- package/src/css.ts +48 -0
- package/src/dom.ts +35 -0
- package/src/fragment.ts +134 -0
- package/src/index.ts +9 -0
- package/src/jsx-dev-runtime.ts +4 -0
- package/src/jsx-runtime.ts +192 -0
- package/src/lib/Enumerable.ts +23 -0
- package/src/lib/ExtendableDictionary.ts +54 -0
- package/src/lib/chunkWith.ts +26 -0
- package/src/lib/index.ts +29 -0
- package/src/lib/keys.ts +7 -0
- package/src/lib/nthArg.ts +6 -0
- package/src/lib/property.ts +4 -0
- package/src/multicast.ts +79 -0
- package/src/observable/Future.ts +17 -0
- package/src/observable/Signal.ts +181 -0
- package/src/observable/TapObservable.ts +30 -0
- package/src/observable/TransformSubject.ts +39 -0
- package/src/observable/index.ts +4 -0
- package/src/operators/concat.ts +6 -0
- package/src/operators/exchangeWith.ts +23 -0
- package/src/operators/flatMap.ts +11 -0
- package/src/operators/index.ts +15 -0
- package/src/operators/multicast.ts +99 -0
- package/src/operators/sequence.ts +23 -0
- package/src/operators/tapSubscription.ts +11 -0
- package/src/operators/transform.ts +7 -0
- package/src/slice.ts +274 -0
- package/src/store.ts +257 -0
- package/src/terminal.ts +208 -0
- package/src/types.ts +31 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to NPM
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
- uses: actions/setup-node@v6
|
|
18
|
+
with:
|
|
19
|
+
node-version: "24"
|
|
20
|
+
registry-url: "https://registry.npmjs.org"
|
|
21
|
+
|
|
22
|
+
- name: Verify version matches tag
|
|
23
|
+
run: |
|
|
24
|
+
TAG="${GITHUB_REF#refs/tags/}"
|
|
25
|
+
VERSION="${TAG#v}"
|
|
26
|
+
FILE_VERSION=$(node -p "require('./package.json').version")
|
|
27
|
+
if [ "$VERSION" != "$FILE_VERSION" ]; then
|
|
28
|
+
echo "Version mismatch: tag=$VERSION, package.json=$FILE_VERSION"
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
- name: Publish to NPM
|
|
33
|
+
env:
|
|
34
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
35
|
+
run: npm publish --access public --provenance
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cascateer/core",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/cascateer/core.git"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"patch": "npm version patch && git push origin main --tags"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.ts",
|
|
13
|
+
"./jsx-runtime": "./src/jsx-runtime.ts",
|
|
14
|
+
"./jsx-dev-runtime": "./src/jsx-dev-runtime.ts",
|
|
15
|
+
"./lib": "./src/lib/index.ts",
|
|
16
|
+
"./operators": "./src/operators/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/lodash": "^4.17.20",
|
|
20
|
+
"@types/object-hash": "^3.0.6",
|
|
21
|
+
"@types/react": "^19.2.14",
|
|
22
|
+
"typescript": "~5.8.3",
|
|
23
|
+
"vite": "^8.0.7"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"lodash": "^4.17.21",
|
|
27
|
+
"object-hash": "^3.0.0",
|
|
28
|
+
"rxjs": "^7.8.2",
|
|
29
|
+
"uuid": "^13.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
constant,
|
|
3
|
+
Dictionary,
|
|
4
|
+
intersectionWith,
|
|
5
|
+
isEqual,
|
|
6
|
+
isFunction,
|
|
7
|
+
memoize,
|
|
8
|
+
} from "lodash";
|
|
9
|
+
import objectHash from "object-hash";
|
|
10
|
+
import {
|
|
11
|
+
BehaviorSubject,
|
|
12
|
+
combineLatest,
|
|
13
|
+
filter,
|
|
14
|
+
lastValueFrom,
|
|
15
|
+
map,
|
|
16
|
+
NextObserver,
|
|
17
|
+
Observable,
|
|
18
|
+
repeat,
|
|
19
|
+
shareReplay,
|
|
20
|
+
Subject,
|
|
21
|
+
tap,
|
|
22
|
+
UnaryFunction,
|
|
23
|
+
} from "rxjs";
|
|
24
|
+
import { asObservable, ExtendableDictionary, property } from "./lib";
|
|
25
|
+
import { TapObservable } from "./observable";
|
|
26
|
+
import { tapSubscription } from "./operators";
|
|
27
|
+
import {
|
|
28
|
+
Action,
|
|
29
|
+
Effect,
|
|
30
|
+
MaybeArray,
|
|
31
|
+
MaybeObservable,
|
|
32
|
+
TapEffect,
|
|
33
|
+
} from "./types";
|
|
34
|
+
|
|
35
|
+
interface TagsConstructor<Args, Result> {
|
|
36
|
+
(args: Args, result: Result): string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface MemoizableConfig<Args, Result> {
|
|
40
|
+
predicate: UnaryFunction<Args, MaybeObservable<{ data: Result }>>;
|
|
41
|
+
tags: TagsConstructor<Args, Result> | MaybeArray<string>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class Memoizable<Args, Result> {
|
|
45
|
+
predicate: UnaryFunction<Args, Observable<Result>>;
|
|
46
|
+
tags: TagsConstructor<Args, Result>;
|
|
47
|
+
|
|
48
|
+
subscribe: UnaryFunction<Observable<string[]>, TapEffect<Args, Result>>;
|
|
49
|
+
|
|
50
|
+
share: UnaryFunction<NextObserver<string[]>, Action<Args, Result>>;
|
|
51
|
+
|
|
52
|
+
constructor({ predicate, tags }: MemoizableConfig<Args, Result>) {
|
|
53
|
+
this.predicate = (args) =>
|
|
54
|
+
asObservable(predicate(args)).pipe(map(property("data")));
|
|
55
|
+
this.tags = isFunction(tags) ? tags : constant([tags].flat());
|
|
56
|
+
|
|
57
|
+
this.subscribe = (invalidatedTags) => {
|
|
58
|
+
const loading = new BehaviorSubject(false);
|
|
59
|
+
const effect: Effect<Args, Result> = memoize(
|
|
60
|
+
(args) =>
|
|
61
|
+
this.predicate(args).pipe(
|
|
62
|
+
tapSubscription(loading),
|
|
63
|
+
tap({
|
|
64
|
+
complete: () => loading.next(false),
|
|
65
|
+
}),
|
|
66
|
+
repeat({
|
|
67
|
+
delay: () =>
|
|
68
|
+
combineLatest([
|
|
69
|
+
effect(args).pipe(map((result) => this.tags(args, result))),
|
|
70
|
+
invalidatedTags,
|
|
71
|
+
]).pipe(
|
|
72
|
+
filter(([tags, invalidatedTags]) =>
|
|
73
|
+
isEqual(tags, intersectionWith(tags, invalidatedTags)),
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
}),
|
|
77
|
+
shareReplay({ bufferSize: 1, refCount: false }),
|
|
78
|
+
),
|
|
79
|
+
(args) => objectHash(args ?? null),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return (args) => new TapObservable(effect(args), loading);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
this.share = (invalidatedTags) => (args) =>
|
|
86
|
+
lastValueFrom(this.predicate(args)).then(
|
|
87
|
+
(result) => (invalidatedTags.next(this.tags(args, result)), result),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ApiEffect<Args, Result> extends TapEffect<Args, Result> {}
|
|
93
|
+
|
|
94
|
+
type ApiAdapterPropertyConstructor<Source, Type extends "effect" | "action"> = {
|
|
95
|
+
[T in Type]: <Args, Result>(
|
|
96
|
+
config: UnaryFunction<Source, MemoizableConfig<Args, Result>>,
|
|
97
|
+
) => T extends "effect" ? ApiEffect<Args, Result> : Action<Args, Result>;
|
|
98
|
+
}[Type];
|
|
99
|
+
|
|
100
|
+
export class ApiAdapter<
|
|
101
|
+
Effects extends Dictionary<ApiEffect<any, any>>,
|
|
102
|
+
Actions extends Dictionary<Action<any, any>>,
|
|
103
|
+
> {
|
|
104
|
+
constructor(
|
|
105
|
+
public effects: Effects,
|
|
106
|
+
public actions: Actions,
|
|
107
|
+
) {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class ExtendableApiAdapter<
|
|
111
|
+
Source,
|
|
112
|
+
Effects extends Dictionary<ApiEffect<any, any>>,
|
|
113
|
+
Actions extends Dictionary<Action<any, any>>,
|
|
114
|
+
> {
|
|
115
|
+
complete(): ApiAdapter<Effects, Actions> {
|
|
116
|
+
return new ApiAdapter(
|
|
117
|
+
this.extendableEffects.complete(),
|
|
118
|
+
this.extendableActions.complete(),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
constructor(
|
|
123
|
+
public context: {
|
|
124
|
+
source: Source;
|
|
125
|
+
invalidatedTags: Subject<string[]>;
|
|
126
|
+
},
|
|
127
|
+
private extendableEffects: ExtendableDictionary<
|
|
128
|
+
ApiEffect<any, any>,
|
|
129
|
+
Effects
|
|
130
|
+
>,
|
|
131
|
+
private extendableActions: ExtendableDictionary<Action<any, any>, Actions>,
|
|
132
|
+
) {}
|
|
133
|
+
|
|
134
|
+
provideEffects<MoreEffects extends Dictionary<ApiEffect<any, any>>>(
|
|
135
|
+
effects: UnaryFunction<
|
|
136
|
+
{ effect: ApiAdapterPropertyConstructor<Source, "effect"> },
|
|
137
|
+
MoreEffects
|
|
138
|
+
>,
|
|
139
|
+
) {
|
|
140
|
+
return new ExtendableApiAdapter(
|
|
141
|
+
this.context,
|
|
142
|
+
this.extendableEffects.extend(
|
|
143
|
+
() => () =>
|
|
144
|
+
effects({
|
|
145
|
+
effect: (config) =>
|
|
146
|
+
new Memoizable(config(this.context.source)).subscribe(
|
|
147
|
+
this.context.invalidatedTags,
|
|
148
|
+
),
|
|
149
|
+
}),
|
|
150
|
+
),
|
|
151
|
+
this.extendableActions,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
provideActions<MoreActions extends Dictionary<Action<any, any>>>(
|
|
156
|
+
actions: UnaryFunction<
|
|
157
|
+
{ action: ApiAdapterPropertyConstructor<Source, "action"> },
|
|
158
|
+
MoreActions
|
|
159
|
+
>,
|
|
160
|
+
) {
|
|
161
|
+
return new ExtendableApiAdapter(
|
|
162
|
+
this.context,
|
|
163
|
+
this.extendableEffects,
|
|
164
|
+
this.extendableActions.extend(
|
|
165
|
+
() => () =>
|
|
166
|
+
actions({
|
|
167
|
+
action: (config) =>
|
|
168
|
+
new Memoizable(config(this.context.source)).share(
|
|
169
|
+
this.context.invalidatedTags,
|
|
170
|
+
),
|
|
171
|
+
}),
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export class ApiProvider<Source> extends ExtendableApiAdapter<Source, {}, {}> {
|
|
178
|
+
constructor(source: Source) {
|
|
179
|
+
super(
|
|
180
|
+
{
|
|
181
|
+
source,
|
|
182
|
+
invalidatedTags: new Subject(),
|
|
183
|
+
},
|
|
184
|
+
new ExtendableDictionary({}),
|
|
185
|
+
new ExtendableDictionary({}),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Dictionary, mapValues } from "lodash";
|
|
2
|
+
import { UnaryFunction } from "rxjs";
|
|
3
|
+
import { createFragment } from ".";
|
|
4
|
+
import { property } from "./lib";
|
|
5
|
+
import { Slice, SliceAdapter, SliceProvider } from "./slice";
|
|
6
|
+
|
|
7
|
+
export class App<
|
|
8
|
+
Slices extends Dictionary<Slice<any, any, any, any, any, any, any, any>>,
|
|
9
|
+
> {
|
|
10
|
+
slices: Slices;
|
|
11
|
+
render: () => JSX.Element;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
slices: UnaryFunction<
|
|
15
|
+
{
|
|
16
|
+
SliceProvider: {
|
|
17
|
+
new (): SliceProvider;
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
SliceAdapter<Slices>
|
|
21
|
+
>,
|
|
22
|
+
render: UnaryFunction<Record<keyof Slices, JSX.Component>, JSX.Element>,
|
|
23
|
+
) {
|
|
24
|
+
this.slices = slices({ SliceProvider }).slices;
|
|
25
|
+
|
|
26
|
+
this.render = () => render(mapValues(this.slices, property("render")));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
appendTo(node: HTMLElement) {
|
|
30
|
+
return node.append(
|
|
31
|
+
createFragment({
|
|
32
|
+
children: this.render(),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/component.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Dictionary, kebabCase } from "lodash";
|
|
2
|
+
import { defer, share, UnaryFunction } from "rxjs";
|
|
3
|
+
import { createFragment } from ".";
|
|
4
|
+
import { cssStyleSheets } from "./css";
|
|
5
|
+
import { defineCustomElement } from "./dom";
|
|
6
|
+
import { ExtendableDictionary } from "./lib";
|
|
7
|
+
import { ComputedSignal } from "./observable";
|
|
8
|
+
import { asStoreEffects, StoreAdapter, StoreEffects } from "./store";
|
|
9
|
+
import { TerminalAdapter, TerminalEffect } from "./terminal";
|
|
10
|
+
import { Action, Effect } from "./types";
|
|
11
|
+
|
|
12
|
+
export class ComponentConstructor<Props extends JSX.Props> {
|
|
13
|
+
constructor(public predicate: UnaryFunction<string, JSX.Component<Props>>) {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createComponent(customElement?: string) {
|
|
17
|
+
const withTemplate =
|
|
18
|
+
<Styles extends Promise<unknown>[]>(...styles: Styles) =>
|
|
19
|
+
<
|
|
20
|
+
Deps extends Dictionary<Effect<any, any> | Action<any, any>>,
|
|
21
|
+
Props extends JSX.Props,
|
|
22
|
+
>(
|
|
23
|
+
constructor: (
|
|
24
|
+
deps: Deps,
|
|
25
|
+
...classNamesList: { -readonly [K in keyof Styles]: Awaited<Styles[K]> }
|
|
26
|
+
) => JSX.Component<Props>,
|
|
27
|
+
) =>
|
|
28
|
+
class extends ComponentConstructor<Props> {
|
|
29
|
+
constructor(deps: Deps) {
|
|
30
|
+
super(
|
|
31
|
+
(key) => (props) =>
|
|
32
|
+
createFragment({
|
|
33
|
+
children: defer(() =>
|
|
34
|
+
Promise.all(styles).then((cssModules) =>
|
|
35
|
+
cssStyleSheets(cssModules).then(async (cssStyleSheets) => {
|
|
36
|
+
const element = constructor(deps, ...cssModules)(props);
|
|
37
|
+
|
|
38
|
+
return customElement != null
|
|
39
|
+
? new (defineCustomElement(
|
|
40
|
+
`${key}-${kebabCase(customElement)}`,
|
|
41
|
+
))(element, cssStyleSheets)
|
|
42
|
+
: createFragment({
|
|
43
|
+
children: element,
|
|
44
|
+
}); /* TODO omit cssModules (whole workflow) */
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
47
|
+
).pipe(share()),
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
withStyles: <Styles extends Promise<unknown>[]>(...styles: Styles) => ({
|
|
55
|
+
withTemplate: withTemplate(...styles),
|
|
56
|
+
}),
|
|
57
|
+
withTemplate: withTemplate(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class ComponentsAdapter<
|
|
62
|
+
Components extends Dictionary<ComponentConstructor<any>>,
|
|
63
|
+
> {
|
|
64
|
+
constructor(public components: Components) {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class ExtendableComponentsAdapter<
|
|
68
|
+
StoreSignals extends Dictionary<ComputedSignal<any>>,
|
|
69
|
+
StoreActions extends Dictionary<Action<any, any>>,
|
|
70
|
+
TerminalEffects extends Dictionary<TerminalEffect<any, any>>,
|
|
71
|
+
TerminalActions extends Dictionary<Action<any, any>>,
|
|
72
|
+
Components extends Dictionary<ComponentConstructor<any>>,
|
|
73
|
+
> {
|
|
74
|
+
complete(): ComponentsAdapter<Components> {
|
|
75
|
+
return new ComponentsAdapter(this.extendableComponents.complete());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
public context: {
|
|
80
|
+
store: StoreAdapter<StoreSignals, StoreActions>;
|
|
81
|
+
terminal: TerminalAdapter<TerminalEffects, TerminalActions>;
|
|
82
|
+
},
|
|
83
|
+
private extendableComponents: ExtendableDictionary<
|
|
84
|
+
ComponentConstructor<any>,
|
|
85
|
+
Components
|
|
86
|
+
>,
|
|
87
|
+
) {}
|
|
88
|
+
|
|
89
|
+
provideComponents<
|
|
90
|
+
MoreComponents extends Dictionary<ComponentConstructor<any>>,
|
|
91
|
+
>(
|
|
92
|
+
components: UnaryFunction<
|
|
93
|
+
{
|
|
94
|
+
component: <Props extends JSX.Props>(
|
|
95
|
+
constructor: UnaryFunction<
|
|
96
|
+
{
|
|
97
|
+
store: {
|
|
98
|
+
effects: StoreEffects<StoreSignals>;
|
|
99
|
+
actions: StoreActions;
|
|
100
|
+
};
|
|
101
|
+
terminal: {
|
|
102
|
+
effects: TerminalEffects;
|
|
103
|
+
actions: TerminalActions;
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
ComponentConstructor<Props>
|
|
107
|
+
>,
|
|
108
|
+
) => ComponentConstructor<Props>;
|
|
109
|
+
},
|
|
110
|
+
MoreComponents
|
|
111
|
+
>,
|
|
112
|
+
) {
|
|
113
|
+
return new ExtendableComponentsAdapter(
|
|
114
|
+
this.context,
|
|
115
|
+
this.extendableComponents.extend(
|
|
116
|
+
() => () =>
|
|
117
|
+
components({
|
|
118
|
+
component: (constructor) =>
|
|
119
|
+
constructor({
|
|
120
|
+
store: {
|
|
121
|
+
effects: asStoreEffects(this.context.store.signals),
|
|
122
|
+
actions: this.context.store.actions,
|
|
123
|
+
},
|
|
124
|
+
terminal: {
|
|
125
|
+
effects: this.context.terminal.effects,
|
|
126
|
+
actions: this.context.terminal.actions,
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class ComponentsProvider<
|
|
136
|
+
StoreSignals extends Dictionary<ComputedSignal<any>>,
|
|
137
|
+
StoreActions extends Dictionary<Action<any, any>>,
|
|
138
|
+
TerminalEffects extends Dictionary<TerminalEffect<any, any>>,
|
|
139
|
+
TerminalActions extends Dictionary<Action<any, any>>,
|
|
140
|
+
> extends ExtendableComponentsAdapter<
|
|
141
|
+
StoreSignals,
|
|
142
|
+
StoreActions,
|
|
143
|
+
TerminalEffects,
|
|
144
|
+
TerminalActions,
|
|
145
|
+
{}
|
|
146
|
+
> {
|
|
147
|
+
constructor(context: {
|
|
148
|
+
store: StoreAdapter<StoreSignals, StoreActions>;
|
|
149
|
+
terminal: TerminalAdapter<TerminalEffects, TerminalActions>;
|
|
150
|
+
}) {
|
|
151
|
+
super(context, new ExtendableDictionary({}));
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/css.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isObject, isString, once, tap } from "lodash";
|
|
2
|
+
import { nonNullable } from "./lib";
|
|
3
|
+
|
|
4
|
+
const cssImports = once(() =>
|
|
5
|
+
Promise.all(
|
|
6
|
+
[
|
|
7
|
+
...Object.entries(import.meta.glob("/**/*.*css")),
|
|
8
|
+
...Object.entries(import.meta.glob("/**/*.*css", { query: "?inline" })),
|
|
9
|
+
].map(([url, load]) =>
|
|
10
|
+
load().then(async (module) => {
|
|
11
|
+
const DEFAULT_KEY = "default";
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
url,
|
|
15
|
+
module,
|
|
16
|
+
styleSheet:
|
|
17
|
+
isObject(module) &&
|
|
18
|
+
DEFAULT_KEY in module &&
|
|
19
|
+
isString(module[DEFAULT_KEY])
|
|
20
|
+
? await new CSSStyleSheet().replace(module[DEFAULT_KEY])
|
|
21
|
+
: null,
|
|
22
|
+
};
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
).then((imports) =>
|
|
26
|
+
imports.reduce(
|
|
27
|
+
(imports, { url, module, styleSheet }) =>
|
|
28
|
+
tap(imports, ({ urls, styleSheets }) => {
|
|
29
|
+
urls.set(module, url);
|
|
30
|
+
styleSheets.set(
|
|
31
|
+
url,
|
|
32
|
+
(styleSheets.get(url) ?? []).concat(styleSheet ?? []),
|
|
33
|
+
);
|
|
34
|
+
}),
|
|
35
|
+
{
|
|
36
|
+
urls: new Map<unknown, string>(),
|
|
37
|
+
styleSheets: new Map<string, CSSStyleSheet[]>(),
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const cssStyleSheets = (modules: unknown[]) =>
|
|
44
|
+
cssImports().then(({ urls, styleSheets }) =>
|
|
45
|
+
modules.flatMap(
|
|
46
|
+
(module) => styleSheets.get(nonNullable(urls.get(module))) ?? [],
|
|
47
|
+
),
|
|
48
|
+
);
|
package/src/dom.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { identity, isObject, memoize } from "lodash";
|
|
2
|
+
import { createFragment } from ".";
|
|
3
|
+
|
|
4
|
+
export class CustomElement extends HTMLElement {
|
|
5
|
+
constructor(children?: JSX.Children, styles: CSSStyleSheet[] = []) {
|
|
6
|
+
super();
|
|
7
|
+
|
|
8
|
+
const shadowRoot = this.attachShadow({ mode: "open" });
|
|
9
|
+
|
|
10
|
+
shadowRoot.adoptedStyleSheets.push(...styles);
|
|
11
|
+
shadowRoot.append(createFragment({ children }));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const defineCustomElement = memoize((key: string) => {
|
|
16
|
+
const constructor = class extends CustomElement {};
|
|
17
|
+
|
|
18
|
+
customElements.define(key, constructor);
|
|
19
|
+
|
|
20
|
+
return constructor;
|
|
21
|
+
}, identity);
|
|
22
|
+
|
|
23
|
+
export const defineCustomProperties = (
|
|
24
|
+
definitions: Partial<JSX.CSSCustomPropertyDefinitions>,
|
|
25
|
+
) => {
|
|
26
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
27
|
+
if (isObject(definition) && "inherits" in definition) {
|
|
28
|
+
CSS.registerProperty({
|
|
29
|
+
...definition,
|
|
30
|
+
inherits: Boolean(definition.inherits),
|
|
31
|
+
name,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
package/src/fragment.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { tap } from "lodash";
|
|
2
|
+
import {
|
|
3
|
+
combineLatest,
|
|
4
|
+
distinctUntilChanged,
|
|
5
|
+
map,
|
|
6
|
+
Observable,
|
|
7
|
+
of,
|
|
8
|
+
scan,
|
|
9
|
+
share,
|
|
10
|
+
shareReplay,
|
|
11
|
+
startWith,
|
|
12
|
+
Subscription,
|
|
13
|
+
switchMap,
|
|
14
|
+
} from "rxjs";
|
|
15
|
+
import { asArray, asObservable } from "./lib";
|
|
16
|
+
import { flatMap } from "./operators";
|
|
17
|
+
|
|
18
|
+
export type Primitive =
|
|
19
|
+
| string
|
|
20
|
+
| number
|
|
21
|
+
| bigint
|
|
22
|
+
| boolean
|
|
23
|
+
| symbol
|
|
24
|
+
| null
|
|
25
|
+
| undefined;
|
|
26
|
+
|
|
27
|
+
const isPrimitive = (value: unknown): value is Primitive =>
|
|
28
|
+
["string", "number", "bigint", "boolean", "symbol"].includes(typeof value) ||
|
|
29
|
+
value == null;
|
|
30
|
+
|
|
31
|
+
const insert = <T extends Node>(...nodes: T[]) => ({
|
|
32
|
+
before: (child: Node | null): T[] => {
|
|
33
|
+
for (const node of nodes) {
|
|
34
|
+
child?.parentNode?.insertBefore(node, child);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return nodes;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const remove = <T extends Node>(...nodes: T[]) => {
|
|
42
|
+
for (const node of nodes) {
|
|
43
|
+
node.parentNode?.removeChild(node);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
class AnchorFragment extends DocumentFragment {
|
|
48
|
+
appendAnchor(current?: Comment) {
|
|
49
|
+
return {
|
|
50
|
+
current,
|
|
51
|
+
next: this.appendChild(new Comment("anchor")),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
anchor = new Observable<Node[]>((subscriber) => {
|
|
56
|
+
const observer = tap(
|
|
57
|
+
new MutationObserver((records) =>
|
|
58
|
+
subscriber.next(records.flatMap((record) => [...record.removedNodes])),
|
|
59
|
+
),
|
|
60
|
+
(observer) => observer.observe(this, { childList: true }),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
unsubscribe: () => observer.disconnect(),
|
|
65
|
+
};
|
|
66
|
+
}).pipe(
|
|
67
|
+
share(),
|
|
68
|
+
scan((anchor, removedNodes) => {
|
|
69
|
+
if (removedNodes.includes(anchor.next)) {
|
|
70
|
+
if (anchor.current != null) {
|
|
71
|
+
remove(anchor.current);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return this.appendAnchor(anchor.next);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return anchor;
|
|
78
|
+
}, this.appendAnchor()),
|
|
79
|
+
flatMap((anchor) => anchor.current ?? []),
|
|
80
|
+
distinctUntilChanged(),
|
|
81
|
+
shareReplay(1),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
constructor() {
|
|
85
|
+
super();
|
|
86
|
+
|
|
87
|
+
this.anchor.subscribe();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class ObservableFragment extends AnchorFragment {
|
|
92
|
+
subscription: Subscription;
|
|
93
|
+
|
|
94
|
+
get nodes(): Observable<Node[]> {
|
|
95
|
+
return asObservable(this.content).pipe(
|
|
96
|
+
map(asArray),
|
|
97
|
+
switchMap((elements) =>
|
|
98
|
+
combineLatest(
|
|
99
|
+
elements.map((element) =>
|
|
100
|
+
asObservable(element).pipe(
|
|
101
|
+
switchMap((element) =>
|
|
102
|
+
element instanceof ObservableFragment
|
|
103
|
+
? element.nodes
|
|
104
|
+
: of(
|
|
105
|
+
isPrimitive(element)
|
|
106
|
+
? new Text(element?.toString())
|
|
107
|
+
: element,
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
startWith(),
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
map((nodes) => nodes.flat()),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
constructor(private content: JSX.Children = []) {
|
|
120
|
+
super();
|
|
121
|
+
|
|
122
|
+
this.subscription = combineLatest([this.anchor, this.nodes])
|
|
123
|
+
.pipe(
|
|
124
|
+
scan(
|
|
125
|
+
(currentNodes, [anchor, nextNodes]) => (
|
|
126
|
+
remove(...currentNodes),
|
|
127
|
+
insert(...nextNodes).before(anchor)
|
|
128
|
+
),
|
|
129
|
+
new Array<Node>(),
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
.subscribe();
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { ApiProvider } from "./api";
|
|
2
|
+
export { App } from "./app";
|
|
3
|
+
export { createComponent } from "./component";
|
|
4
|
+
export { defineCustomProperties } from "./dom";
|
|
5
|
+
export { createElement, createFragment } from "./jsx-runtime";
|
|
6
|
+
export { createSlice } from "./slice";
|
|
7
|
+
export { type StoreEffect } from "./store";
|
|
8
|
+
export { type TerminalEffect } from "./terminal";
|
|
9
|
+
export { type Action, type MaybeObservable } from "./types";
|