@preact-mvc/core 0.1.0 → 0.1.2

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/src/view.ts DELETED
@@ -1,153 +0,0 @@
1
- import { Component as PreactComponent, h, options } from 'preact';
2
- import type { ComponentChildren, VNode } from 'preact';
3
- import { useLayoutEffect, useRef } from 'preact/hooks';
4
- import { InjectorContext, useInjector } from './context';
5
- import { Injector } from './injector';
6
- import { registerModule } from './module';
7
- import { ownerOf } from './decorators';
8
- import type { ComponentMeta } from './decorators';
9
-
10
- // --- Class-based views -----------------------------------------------------
11
- // A View subclass is never instantiated by Preact. The options.vnode hook
12
- // below swaps any vnode whose type is a View subclass for a host function
13
- // component, which constructs the class ONCE per mount inside a child
14
- // injector — so `inject()` in field initializers resolves against that view's
15
- // scope (component providers -> module providers -> root).
16
- //
17
- // render() runs on every render pass with no framework involvement: signals
18
- // read inside it subscribe the host component automatically.
19
-
20
- export abstract class View<P = {}> extends PreactComponent<P> {
21
- abstract render(): ComponentChildren;
22
-
23
- // Views are signal-driven; setState/forceUpdate would hit Preact internals
24
- // on an instance Preact never mounted. Fail loudly instead of corrupting.
25
- override setState(): never {
26
- throw new Error('View components are signal-driven — assign a signal instead of calling setState()');
27
- }
28
- override forceUpdate(): never {
29
- throw new Error('View components are signal-driven — assign a signal instead of calling forceUpdate()');
30
- }
31
- }
32
-
33
- function isViewClass(t: any): boolean {
34
- return typeof t === 'function' && t.prototype instanceof View;
35
- }
36
-
37
- // --- Error boundary ----------------------------------------------------------
38
- // Real Preact class component (not a View — it must be instantiated by Preact).
39
- // Wraps every view host so mount/render failures stay local instead of
40
- // aborting the whole render pass.
41
-
42
- class ViewErrorBoundary extends PreactComponent<
43
- { viewName: string; children?: ComponentChildren },
44
- { error: any }
45
- > {
46
- override state = { error: null as any };
47
-
48
- override componentDidCatch(error: any) {
49
- console.error(`[framework] ${this.props.viewName} failed:`, error);
50
- this.setState({ error });
51
- }
52
-
53
- override render() {
54
- const err = this.state.error;
55
- if (!err) return this.props.children;
56
- return h(
57
- 'div',
58
- {
59
- style: {
60
- border: '1px solid #e53935',
61
- background: '#ffebee',
62
- padding: '0.75rem',
63
- borderRadius: '4px',
64
- fontFamily: 'monospace',
65
- fontSize: '0.85rem',
66
- },
67
- },
68
- h('strong', null, `${this.props.viewName} failed to mount`),
69
- h('pre', { style: { whiteSpace: 'pre-wrap', margin: '0.5rem 0' } }, String(err?.message ?? err)),
70
- h('button', { onClick: () => this.setState({ error: null }) }, 'Retry')
71
- );
72
- }
73
- }
74
-
75
- // --- Host -------------------------------------------------------------------
76
-
77
- const hostCache = new Map<any, any>();
78
-
79
- function hostFor(cls: any): any {
80
- let host = hostCache.get(cls);
81
- if (host) return host;
82
-
83
- const meta = (cls.__component ?? {}) as ComponentMeta;
84
-
85
- const inner = function ViewHost(props: any) {
86
- const parent = useInjector();
87
- const ref = useRef<{ inj: Injector; inst: any } | null>(null);
88
- if (!ref.current) {
89
- // Module ownership: a view declared in a module's components/bootstrap
90
- // may only mount where that module's injector is an ancestor — Angular's
91
- // declaration rule enforced at mount. Undeclared views mount freely.
92
- const owner = ownerOf(cls);
93
- if (owner) {
94
- let node: Injector | null = parent;
95
- while (node && !node.modules.has(owner)) node = node.parent;
96
- if (!node) {
97
- throw new Error(
98
- `${cls.name} is declared by ${owner.name ?? 'a module'} but mounted outside its injector scope. ` +
99
- `Mount it under a route/module that includes ${owner.name ?? 'that module'}.`
100
- );
101
- }
102
- }
103
- const inj = new Injector(parent, `view ${cls.name}`);
104
- // Standalone scope: imported modules contribute providers (and mark
105
- // themselves visible for ownership checks); view-level providers and
106
- // declared controllers register on top, closest-scope-wins.
107
- const seen = new Set<any>();
108
- for (const imp of meta.imports ?? []) registerModule(imp, inj, seen);
109
- for (const p of meta.providers ?? []) inj.provide(p);
110
- for (const c of meta.controllers ?? []) inj.provide(c);
111
- ref.current = { inj, inst: inj.instantiate(cls, props) };
112
- }
113
- const { inj, inst } = ref.current;
114
- inst.props = props;
115
- // Mount activates the whole view scope: onInit for the view and every
116
- // controller/provider its injector constructed; unmount fires onDestroy.
117
- // useLayoutEffect (synchronous at commit) is deliberate: deferred
118
- // useEffect callbacks are dropped if the host unmounts before they flush,
119
- // which would silently lose onInit on same-tick mount+navigate.
120
- useLayoutEffect(() => {
121
- inj.activate();
122
- return () => inj.deactivate();
123
- }, []);
124
- // The view's injector becomes the scope for everything it renders, so
125
- // child components resolve through it — the element-injector hierarchy.
126
- return h(InjectorContext.Provider, { value: inj }, inst.render());
127
- };
128
- (inner as any).displayName = cls.name;
129
-
130
- host = function BoundedView(props: any) {
131
- return h(ViewErrorBoundary, { viewName: cls.name }, h(inner, props));
132
- };
133
- (host as any).displayName = cls.name;
134
- hostCache.set(cls, host);
135
- return host;
136
- }
137
-
138
- let installed = false;
139
-
140
- // Install the vnode interceptor. Must run before any view class is rendered;
141
- // called on module load and again defensively from bootstrap().
142
- export function installViewInterop(): void {
143
- if (installed) return;
144
- installed = true;
145
- const prev = (options as any).vnode as ((vnode: VNode) => void) | undefined;
146
- (options as any).vnode = (vnode: VNode) => {
147
- const t = vnode.type;
148
- if (isViewClass(t)) vnode.type = hostFor(t);
149
- prev?.(vnode);
150
- };
151
- }
152
-
153
- installViewInterop();
File without changes