@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.
@@ -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
+ }
@@ -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
+ };
@@ -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";
@@ -0,0 +1,4 @@
1
+ import { createElement, createFragment } from "./jsx-runtime";
2
+
3
+ export const jsxDEV = createElement;
4
+ export const Fragment = createFragment;