@preact-mvc/core 0.1.5 → 0.1.7
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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/module.js +12 -6
- package/dist/view.d.ts +2 -0
- package/dist/view.js +86 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { Injector, InjectionToken, inject, runInInjectionContext, getCurrentInje
|
|
|
4
4
|
export type { Provider, Token, Type } from './injector';
|
|
5
5
|
export { Injectable, Controller, Module, Component } from './decorators';
|
|
6
6
|
export type { InjectableMeta, ModuleMeta, ComponentMeta } from './decorators';
|
|
7
|
-
export { View, installViewInterop } from './view';
|
|
7
|
+
export { View, installViewInterop, errorHint } from './view';
|
|
8
8
|
export { bootstrap, createModuleInjector, getModuleInjector } from './module';
|
|
9
9
|
export { pmvc } from './pmvc';
|
|
10
10
|
export { InjectorContext, useInjector, useService, Provide } from './context';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { Injector, InjectionToken, inject, runInInjectionContext, getCurrentInje
|
|
|
5
5
|
// Metadata
|
|
6
6
|
export { Injectable, Controller, Module, Component } from './decorators';
|
|
7
7
|
// Class-based views
|
|
8
|
-
export { View, installViewInterop } from './view';
|
|
8
|
+
export { View, installViewInterop, errorHint } from './view';
|
|
9
9
|
// Modules & bootstrap
|
|
10
10
|
export { bootstrap, createModuleInjector, getModuleInjector } from './module';
|
|
11
11
|
// Class-flag utilities — pmvc.isView(x), pmvc.isController(x), ...
|
package/dist/module.js
CHANGED
|
@@ -4,8 +4,11 @@ import { InjectorContext } from './context';
|
|
|
4
4
|
import { installViewInterop } from './view';
|
|
5
5
|
import { ownMeta } from './decorators';
|
|
6
6
|
// Modules are the unit of provider scoping. A module's providers (plus those
|
|
7
|
-
// of its imports, transitively) register into a single
|
|
8
|
-
//
|
|
7
|
+
// of its imports, transitively) register into a single Injector. The
|
|
8
|
+
// bootstrap module registers into the ROOT injector itself — matching
|
|
9
|
+
// Angular, where eager module providers share the root scope — so
|
|
10
|
+
// providedIn: 'root' services can inject them. Only feature/lazy modules
|
|
11
|
+
// get child injectors: root injector (holding the app module's graph) ->
|
|
9
12
|
// feature/lazy module injectors -> per-view injectors.
|
|
10
13
|
export function getModuleMeta(mod) {
|
|
11
14
|
return ownMeta(mod, '__module');
|
|
@@ -63,11 +66,14 @@ export function getModuleInjector(mod, parent) {
|
|
|
63
66
|
export function bootstrap(RootModule, el) {
|
|
64
67
|
installViewInterop();
|
|
65
68
|
const root = new Injector(null, 'root');
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
// The bootstrapped module is eager: its providers and imports merge into
|
|
70
|
+
// the root injector, so providedIn: 'root' services (instantiated under
|
|
71
|
+
// root) can inject them — including forRoot() bundles like
|
|
72
|
+
// RouterModule.forRoot().
|
|
73
|
+
registerModule(RootModule, root, new Set());
|
|
74
|
+
const App = getModuleMeta(RootModule).bootstrap?.[0];
|
|
69
75
|
if (!App) {
|
|
70
76
|
throw new Error('Root module must declare a bootstrap component: @Module({ bootstrap: [AppView] })');
|
|
71
77
|
}
|
|
72
|
-
render(h(InjectorContext.Provider, { value:
|
|
78
|
+
render(h(InjectorContext.Provider, { value: root }, h(App, {})), el);
|
|
73
79
|
}
|
package/dist/view.d.ts
CHANGED
|
@@ -8,4 +8,6 @@ export declare abstract class View<P = {}> extends PreactComponent<P> {
|
|
|
8
8
|
}
|
|
9
9
|
/** Identity stamp read — a subclass of a View stays a view. */
|
|
10
10
|
export declare function isView(t: any): boolean;
|
|
11
|
+
/** Map a caught error to a known-cause hint, or null when unrecognized. */
|
|
12
|
+
export declare function errorHint(error: any): string | null;
|
|
11
13
|
export declare function installViewInterop(): void;
|
package/dist/view.js
CHANGED
|
@@ -35,6 +35,27 @@ export function isView(t) {
|
|
|
35
35
|
// Real Preact class component (not a View — it must be instantiated by Preact).
|
|
36
36
|
// Wraps every view host so mount/render failures stay local instead of
|
|
37
37
|
// aborting the whole render pass.
|
|
38
|
+
// Opaque upstream error signatures -> actionable guidance, appended to the
|
|
39
|
+
// boundary output and the console.error. Add entries when a cryptic thrown
|
|
40
|
+
// error has a known real cause.
|
|
41
|
+
const ERROR_HINTS = [
|
|
42
|
+
[
|
|
43
|
+
/Hook can only be invoked from render methods/,
|
|
44
|
+
'a hook ran outside a render pass. A common cause: a function component ' +
|
|
45
|
+
'mounts a Preact sub-tree with render() inside useEffect. Pending ' +
|
|
46
|
+
"useEffects flush inside options.__r on a re-render, and the nested " +
|
|
47
|
+
"diff corrupts preact/debug's hook tracking for the outer component. " +
|
|
48
|
+
'Mount sub-trees in useLayoutEffect instead.',
|
|
49
|
+
],
|
|
50
|
+
];
|
|
51
|
+
/** Map a caught error to a known-cause hint, or null when unrecognized. */
|
|
52
|
+
export function errorHint(error) {
|
|
53
|
+
const msg = String(error?.message ?? error);
|
|
54
|
+
for (const [re, hint] of ERROR_HINTS)
|
|
55
|
+
if (re.test(msg))
|
|
56
|
+
return hint;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
38
59
|
class ViewErrorBoundary extends PreactComponent {
|
|
39
60
|
constructor() {
|
|
40
61
|
super(...arguments);
|
|
@@ -42,12 +63,16 @@ class ViewErrorBoundary extends PreactComponent {
|
|
|
42
63
|
}
|
|
43
64
|
componentDidCatch(error) {
|
|
44
65
|
console.error(`[framework] ${this.props.viewName} failed:`, error);
|
|
66
|
+
const hint = errorHint(error);
|
|
67
|
+
if (hint)
|
|
68
|
+
console.error(`[framework] hint for ${this.props.viewName}:`, hint);
|
|
45
69
|
this.setState({ error });
|
|
46
70
|
}
|
|
47
71
|
render() {
|
|
48
72
|
const err = this.state.error;
|
|
49
73
|
if (!err)
|
|
50
74
|
return this.props.children;
|
|
75
|
+
const hint = errorHint(err);
|
|
51
76
|
return h('div', {
|
|
52
77
|
style: {
|
|
53
78
|
border: '1px solid #e53935',
|
|
@@ -57,7 +82,7 @@ class ViewErrorBoundary extends PreactComponent {
|
|
|
57
82
|
fontFamily: 'monospace',
|
|
58
83
|
fontSize: '0.85rem',
|
|
59
84
|
},
|
|
60
|
-
}, h('strong', null, `${this.props.viewName} failed
|
|
85
|
+
}, h('strong', null, `${this.props.viewName} failed`), h('pre', { style: { whiteSpace: 'pre-wrap', margin: '0.5rem 0' } }, String(err?.message ?? err)), hint ? h('p', { style: { margin: '0 0 0.5rem' } }, `hint: ${hint}`) : null, h('button', { onClick: () => this.setState({ error: null }) }, 'Retry'));
|
|
61
86
|
}
|
|
62
87
|
}
|
|
63
88
|
// --- Host -------------------------------------------------------------------
|
|
@@ -120,6 +145,42 @@ function hostFor(cls) {
|
|
|
120
145
|
hostCache.set(cls, host);
|
|
121
146
|
return host;
|
|
122
147
|
}
|
|
148
|
+
// --- preact/debug resilience --------------------------------------------------
|
|
149
|
+
// preact/hooks flushes a component's pending useEffects inside options.__r
|
|
150
|
+
// when it re-renders before the async afterPaint pass. A pending effect that
|
|
151
|
+
// calls render() runs a nested diff whose `diffed` clears preact/debug's
|
|
152
|
+
// hooksAllowed flag — and the flag stays cleared when the component's own
|
|
153
|
+
// render body resumes, so its next hook throws "Hook can only be invoked
|
|
154
|
+
// from render methods" falsely. The flag itself is module-local and cannot
|
|
155
|
+
// be repaired, so instead the guard's question is answered precisely:
|
|
156
|
+
// function component types are wrapped so `currentRender` is the instance
|
|
157
|
+
// executing its body, and the options.__h/_hook check tolerates a corrupt
|
|
158
|
+
// flag only when the hook's component provably IS that instance.
|
|
159
|
+
let currentRender = null;
|
|
160
|
+
const wrappedTypes = new WeakMap();
|
|
161
|
+
function isFunctionComponentType(t) {
|
|
162
|
+
return typeof t === 'function' && !(t.prototype && t.prototype.render);
|
|
163
|
+
}
|
|
164
|
+
function wrapFunctionComponent(type) {
|
|
165
|
+
let wrapped = wrappedTypes.get(type);
|
|
166
|
+
if (!wrapped) {
|
|
167
|
+
wrapped = function (props, ctx) {
|
|
168
|
+
const prev = currentRender;
|
|
169
|
+
currentRender = this;
|
|
170
|
+
try {
|
|
171
|
+
return type.call(this, props, ctx);
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
currentRender = prev;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
// Statics (displayName, metadata stamps) resolve off the original type.
|
|
178
|
+
Object.setPrototypeOf(wrapped, type);
|
|
179
|
+
wrapped.displayName = type.displayName || type.name;
|
|
180
|
+
wrappedTypes.set(type, wrapped);
|
|
181
|
+
}
|
|
182
|
+
return wrapped;
|
|
183
|
+
}
|
|
123
184
|
let installed = false;
|
|
124
185
|
// Install the vnode interceptor. Must run before any view class is rendered;
|
|
125
186
|
// called on module load and again defensively from bootstrap().
|
|
@@ -127,11 +188,35 @@ export function installViewInterop() {
|
|
|
127
188
|
if (installed)
|
|
128
189
|
return;
|
|
129
190
|
installed = true;
|
|
191
|
+
// preact/debug installs its hook guard on options.__h (dist build) or
|
|
192
|
+
// options._hook (source build). Component-type wrapping only engages when
|
|
193
|
+
// a guard exists to protect: production has no check that can corrupt.
|
|
194
|
+
// The guard must already be installed, so `import 'preact/debug'` has to
|
|
195
|
+
// precede framework imports (preset-vite injects it first).
|
|
196
|
+
const guardKeys = ['__h', '_hook'].filter((k) => typeof options[k] === 'function');
|
|
197
|
+
const debugLoaded = guardKeys.length > 0;
|
|
198
|
+
for (const key of guardKeys) {
|
|
199
|
+
const check = options[key];
|
|
200
|
+
options[key] = (comp, index, type) => {
|
|
201
|
+
try {
|
|
202
|
+
check(comp, index, type);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
if (!comp || comp !== currentRender)
|
|
206
|
+
throw err;
|
|
207
|
+
// comp is executing its own render body: the call is legal, the
|
|
208
|
+
// guard's flag was cleared by a nested diffed inside options.__r.
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
130
212
|
const prev = options.vnode;
|
|
131
213
|
options.vnode = (vnode) => {
|
|
132
214
|
const t = vnode.type;
|
|
133
215
|
if (isView(t))
|
|
134
216
|
vnode.type = hostFor(t);
|
|
217
|
+
else if (debugLoaded && isFunctionComponentType(t)) {
|
|
218
|
+
vnode.type = wrapFunctionComponent(t);
|
|
219
|
+
}
|
|
135
220
|
prev?.(vnode);
|
|
136
221
|
};
|
|
137
222
|
}
|
package/package.json
CHANGED