@preact-mvc/core 0.1.3 → 0.1.5
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/decorators.d.ts +10 -0
- package/dist/decorators.js +38 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -1
- package/dist/injector.js +4 -4
- package/dist/lazy.d.ts +50 -0
- package/dist/lazy.js +148 -0
- package/dist/module.d.ts +1 -0
- package/dist/module.js +5 -1
- package/dist/pmvc.d.ts +14 -0
- package/dist/pmvc.js +22 -0
- package/dist/router/lazy.d.ts +48 -0
- package/dist/router/lazy.js +88 -0
- package/dist/router/module.js +2 -7
- package/dist/router/outlet.d.ts +4 -1
- package/dist/router/outlet.js +32 -9
- package/dist/router/router.d.ts +20 -0
- package/dist/router/router.js +26 -2
- package/dist/view.d.ts +3 -0
- package/dist/view.js +10 -5
- package/package.json +1 -1
package/dist/decorators.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { Provider, Type } from './injector';
|
|
2
|
+
/** A class's own decorator metadata — never the inherited static chain. */
|
|
3
|
+
export declare function ownMeta<T>(target: any, key: string): T | undefined;
|
|
4
|
+
/** Identity stamp: a subclass of a @Controller stays a controller. */
|
|
5
|
+
export declare function isController(t: any): boolean;
|
|
6
|
+
/** Injectable metadata — reads the chain on purpose (providedIn inherits). */
|
|
7
|
+
export declare function getInjectableMeta(t: any): InjectableMeta | undefined;
|
|
8
|
+
/** Identity read: the class carries @Injectable metadata. */
|
|
9
|
+
export declare function isInjectable(t: any): boolean;
|
|
10
|
+
/** Component metadata — own-class only; a subclass re-declares its own. */
|
|
11
|
+
export declare function getComponentMeta(t: any): ComponentMeta | undefined;
|
|
2
12
|
export interface InjectableMeta {
|
|
3
13
|
providedIn?: 'root';
|
|
4
14
|
}
|
package/dist/decorators.js
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
// Decorators attach metadata to the class constructor. They work with
|
|
2
|
+
// experimentalDecorators (esbuild honors the tsconfig flag) and can also be
|
|
3
|
+
// invoked manually: `Module({ ... })(MyModule)`.
|
|
4
|
+
//
|
|
5
|
+
// Two stamp families:
|
|
6
|
+
// identity (__view, __controller) — booleans answering "is this class a
|
|
7
|
+
// kind of thing"; they inherit through the constructor chain, so a
|
|
8
|
+
// subclass keeps the kind.
|
|
9
|
+
// config (__module, __component, __injectable) — metadata describing one
|
|
10
|
+
// class; readers use ownMeta so a subclass never borrows its parent's
|
|
11
|
+
// config (Angular's rule: @NgModule/@Component metadata does not
|
|
12
|
+
// inherit). __injectable is the exception at read time: the injector
|
|
13
|
+
// resolves providedIn through the chain, matching Angular's ɵprov.
|
|
14
|
+
/** A class's own decorator metadata — never the inherited static chain. */
|
|
15
|
+
export function ownMeta(target, key) {
|
|
16
|
+
return target != null && Object.prototype.hasOwnProperty.call(target, key)
|
|
17
|
+
? target[key]
|
|
18
|
+
: undefined;
|
|
19
|
+
}
|
|
20
|
+
// --- Flag accessors -----------------------------------------------------------
|
|
21
|
+
// The read surface for the stamps below. Callers go through these instead of
|
|
22
|
+
// touching `__x` properties, so the own-vs-inherited rules live in one place.
|
|
23
|
+
/** Identity stamp: a subclass of a @Controller stays a controller. */
|
|
24
|
+
export function isController(t) {
|
|
25
|
+
return typeof t === 'function' && !!t.__controller;
|
|
26
|
+
}
|
|
27
|
+
/** Injectable metadata — reads the chain on purpose (providedIn inherits). */
|
|
28
|
+
export function getInjectableMeta(t) {
|
|
29
|
+
return t?.__injectable;
|
|
30
|
+
}
|
|
31
|
+
/** Identity read: the class carries @Injectable metadata. */
|
|
32
|
+
export function isInjectable(t) {
|
|
33
|
+
return getInjectableMeta(t) != null;
|
|
34
|
+
}
|
|
35
|
+
/** Component metadata — own-class only; a subclass re-declares its own. */
|
|
36
|
+
export function getComponentMeta(t) {
|
|
37
|
+
return ownMeta(t, '__component');
|
|
38
|
+
}
|
|
1
39
|
export function Injectable(meta = {}) {
|
|
2
40
|
return function (target) {
|
|
3
41
|
target.__injectable = meta;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,12 @@ 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
7
|
export { View, installViewInterop } from './view';
|
|
8
|
-
export { bootstrap, createModuleInjector, getModuleInjector
|
|
8
|
+
export { bootstrap, createModuleInjector, getModuleInjector } from './module';
|
|
9
|
+
export { pmvc } from './pmvc';
|
|
9
10
|
export { InjectorContext, useInjector, useService, Provide } from './context';
|
|
10
11
|
export { useController } from './controller';
|
|
11
12
|
export type { OnInit, OnDestroy } from './controller';
|
|
13
|
+
export { lazyView, lazyModule } from './lazy';
|
|
14
|
+
export type { LazyResource, LazyResourceState } from './lazy';
|
|
12
15
|
export { Router, ROUTES, ROUTER_BASE, RouterModule, RouterOutlet, Link, defaultBasePath } from './router';
|
|
13
16
|
export type { Route, RouteMatch, RouterOptions } from './router';
|
package/dist/index.js
CHANGED
|
@@ -7,9 +7,13 @@ export { Injectable, Controller, Module, Component } from './decorators';
|
|
|
7
7
|
// Class-based views
|
|
8
8
|
export { View, installViewInterop } from './view';
|
|
9
9
|
// Modules & bootstrap
|
|
10
|
-
export { bootstrap, createModuleInjector, getModuleInjector
|
|
10
|
+
export { bootstrap, createModuleInjector, getModuleInjector } from './module';
|
|
11
|
+
// Class-flag utilities — pmvc.isView(x), pmvc.isController(x), ...
|
|
12
|
+
export { pmvc } from './pmvc';
|
|
11
13
|
// View integration
|
|
12
14
|
export { InjectorContext, useInjector, useService, Provide } from './context';
|
|
13
15
|
export { useController } from './controller';
|
|
16
|
+
// Lazy loading — usable in route configs and straight in JSX
|
|
17
|
+
export { lazyView, lazyModule } from './lazy';
|
|
14
18
|
// Router
|
|
15
19
|
export { Router, ROUTES, ROUTER_BASE, RouterModule, RouterOutlet, Link, defaultBasePath } from './router';
|
package/dist/injector.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isController, getInjectableMeta } from './decorators';
|
|
1
2
|
export class InjectionToken {
|
|
2
3
|
constructor(_desc) {
|
|
3
4
|
this._desc = _desc;
|
|
@@ -116,7 +117,7 @@ export class Injector {
|
|
|
116
117
|
has(token) {
|
|
117
118
|
if (this.records.has(token))
|
|
118
119
|
return true;
|
|
119
|
-
if (token
|
|
120
|
+
if (isController(token))
|
|
120
121
|
return true;
|
|
121
122
|
if (this.parent)
|
|
122
123
|
return this.parent.has(token);
|
|
@@ -135,7 +136,7 @@ export class Injector {
|
|
|
135
136
|
}
|
|
136
137
|
// Controllers are transient unless explicitly provided: one instance per
|
|
137
138
|
// requesting scope, so a view always gets its own controller.
|
|
138
|
-
if (token
|
|
139
|
+
if (isController(token)) {
|
|
139
140
|
this.provide(token);
|
|
140
141
|
return this.resolveRecord(token, this.records.get(token));
|
|
141
142
|
}
|
|
@@ -165,8 +166,7 @@ export class Injector {
|
|
|
165
166
|
return rec.value;
|
|
166
167
|
}
|
|
167
168
|
isRootProvided(token) {
|
|
168
|
-
|
|
169
|
-
return meta?.providedIn === 'root';
|
|
169
|
+
return getInjectableMeta(token)?.providedIn === 'root';
|
|
170
170
|
}
|
|
171
171
|
instantiate(cls, ...args) {
|
|
172
172
|
const inst = runInInjectionContext(this, () => new cls(...args));
|
package/dist/lazy.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FunctionComponent } from 'preact';
|
|
2
|
+
export type LazyResourceState<R> = {
|
|
3
|
+
status: 'loading';
|
|
4
|
+
} | {
|
|
5
|
+
status: 'ready';
|
|
6
|
+
value: R;
|
|
7
|
+
} | {
|
|
8
|
+
status: 'error';
|
|
9
|
+
error: unknown;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* A deferred loader. `.state` reads kick the import on first access and
|
|
13
|
+
* subscribe the reader (a component render or a computed); `.ready` is the
|
|
14
|
+
* same resolution as a promise.
|
|
15
|
+
*/
|
|
16
|
+
export interface LazyResource<R> {
|
|
17
|
+
readonly state: LazyResourceState<R>;
|
|
18
|
+
readonly ready: Promise<R>;
|
|
19
|
+
}
|
|
20
|
+
export declare function isComponent(v: any): boolean;
|
|
21
|
+
export declare function pickComponent(ns: any, label: string): any;
|
|
22
|
+
export declare function pickLazyFeature(ns: any, label: string): any;
|
|
23
|
+
export declare function pickModule(ns: any, label: string): any;
|
|
24
|
+
/**
|
|
25
|
+
* `lazyView(() => import('./hello.view'))` — a lazy standalone component.
|
|
26
|
+
* Usable as a Route `component` or directly in JSX: the facade suspends on
|
|
27
|
+
* the import, then mounts the resolved view with the same props.
|
|
28
|
+
*/
|
|
29
|
+
export declare function lazyView(load: () => Promise<any>): FunctionComponent<any>;
|
|
30
|
+
/**
|
|
31
|
+
* `lazyModule(() => import('./admin.module'))` — a lazy feature module.
|
|
32
|
+
* Usable as a Route `module` (the outlet resolves the real module and its
|
|
33
|
+
* injector) or `component`, or directly in JSX: mounts the module's
|
|
34
|
+
* bootstrap view inside the module injector once the chunk lands.
|
|
35
|
+
*/
|
|
36
|
+
export declare function lazyModule(load: () => Promise<any>): FunctionComponent<any>;
|
|
37
|
+
/**
|
|
38
|
+
* Read a lazyView/lazyModule facade's resource state — kicks the loader on
|
|
39
|
+
* first read and subscribes the reader (outlet render, router.pending
|
|
40
|
+
* computed). Non-facade values answer null.
|
|
41
|
+
*/
|
|
42
|
+
export declare function lazyStateOf(v: any): LazyResourceState<any> | null;
|
|
43
|
+
/**
|
|
44
|
+
* Sync unwrap for the Route `module` field: a lazyModule facade answers
|
|
45
|
+
* with the resolved module class once loaded (undefined while pending or
|
|
46
|
+
* after a failure). Reading the resource state subscribes the caller.
|
|
47
|
+
*/
|
|
48
|
+
export declare function moduleRefOf(m: any): any;
|
|
49
|
+
/** Async unwrap — awaits the ref's own resolution (kicking it if needed). */
|
|
50
|
+
export declare function lazyModuleReady(m: any): Promise<any> | null;
|
package/dist/lazy.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
import { h } from 'preact';
|
|
3
|
+
import { signal } from '@preact/signals';
|
|
4
|
+
import { InjectorContext, useInjector } from './context';
|
|
5
|
+
import { getModuleInjector, getModuleMeta } from './module';
|
|
6
|
+
import { isView } from './view';
|
|
7
|
+
function lazyResource(run, label) {
|
|
8
|
+
let s;
|
|
9
|
+
let promise;
|
|
10
|
+
const start = () => {
|
|
11
|
+
if (!s) {
|
|
12
|
+
s = signal({ status: 'loading' });
|
|
13
|
+
promise = run().then((value) => {
|
|
14
|
+
s.value = { status: 'ready', value };
|
|
15
|
+
return value;
|
|
16
|
+
}, (error) => {
|
|
17
|
+
console.error(`[framework] ${label} failed to load:`, error);
|
|
18
|
+
s.value = { status: 'error', error };
|
|
19
|
+
throw error;
|
|
20
|
+
});
|
|
21
|
+
// The state object carries failures to renderers; keep the promise
|
|
22
|
+
// from also reporting an unhandled rejection.
|
|
23
|
+
promise.catch(() => { });
|
|
24
|
+
}
|
|
25
|
+
return { s: s, promise: promise };
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
get state() {
|
|
29
|
+
return start().s.value;
|
|
30
|
+
},
|
|
31
|
+
get ready() {
|
|
32
|
+
return start().promise;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// --- Export pickers ---------------------------------------------------------
|
|
37
|
+
// A lazy import resolves to: the ES module namespace ({ default: X, Y }),
|
|
38
|
+
// a value picked by .then(m => m.X), or a bare Route[]. Candidates are
|
|
39
|
+
// ordered default-first so `export default` always wins.
|
|
40
|
+
function exportCandidates(ns) {
|
|
41
|
+
if (Array.isArray(ns) || typeof ns !== 'object' || ns === null)
|
|
42
|
+
return [ns];
|
|
43
|
+
return 'default' in ns ? [ns.default, ...Object.values(ns)] : Object.values(ns);
|
|
44
|
+
}
|
|
45
|
+
export function isComponent(v) {
|
|
46
|
+
// Detection is an is-a question, so these read the chain: a subclass of a
|
|
47
|
+
// stamped component is still renderable (config reads stay own-only).
|
|
48
|
+
return (typeof v === 'function' &&
|
|
49
|
+
(isView(v) || !!v.__component || !!v.__lazyView));
|
|
50
|
+
}
|
|
51
|
+
export function pickComponent(ns, label) {
|
|
52
|
+
for (const c of exportCandidates(ns)) {
|
|
53
|
+
if (isComponent(c))
|
|
54
|
+
return c;
|
|
55
|
+
}
|
|
56
|
+
// Unstamped function components resolve only where unambiguous: the
|
|
57
|
+
// loader's direct return value (.then(m => m.X)) or a default export.
|
|
58
|
+
if (typeof ns === 'function')
|
|
59
|
+
return ns;
|
|
60
|
+
if (typeof ns === 'object' && ns !== null && typeof ns.default === 'function') {
|
|
61
|
+
return ns.default;
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`${label} resolved no view — expected a View class, @Component class, ` +
|
|
64
|
+
`or function component (default export or .then(m => m.X))`);
|
|
65
|
+
}
|
|
66
|
+
export function pickLazyFeature(ns, label) {
|
|
67
|
+
for (const c of exportCandidates(ns)) {
|
|
68
|
+
if (getModuleMeta(c) || Array.isArray(c))
|
|
69
|
+
return c;
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`${label} resolved no feature — expected the module to export an ` +
|
|
72
|
+
`@Module class or a Route[] (default or named export)`);
|
|
73
|
+
}
|
|
74
|
+
export function pickModule(ns, label) {
|
|
75
|
+
const v = pickLazyFeature(ns, label);
|
|
76
|
+
if (Array.isArray(v)) {
|
|
77
|
+
throw new Error(`${label} resolved a Route[] — expected an @Module class`);
|
|
78
|
+
}
|
|
79
|
+
return v;
|
|
80
|
+
}
|
|
81
|
+
// --- Facades ----------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* `lazyView(() => import('./hello.view'))` — a lazy standalone component.
|
|
84
|
+
* Usable as a Route `component` or directly in JSX: the facade suspends on
|
|
85
|
+
* the import, then mounts the resolved view with the same props.
|
|
86
|
+
*/
|
|
87
|
+
export function lazyView(load) {
|
|
88
|
+
const res = lazyResource(() => load().then((ns) => pickComponent(ns, 'lazyView')), 'lazyView');
|
|
89
|
+
const LazyView = ({ fallback, ...props }) => {
|
|
90
|
+
const st = res.state;
|
|
91
|
+
if (st.status === 'ready')
|
|
92
|
+
return h(st.value, props);
|
|
93
|
+
return st.status === 'loading' ? _jsx(_Fragment, { children: fallback ?? null }) : null;
|
|
94
|
+
};
|
|
95
|
+
LazyView.__lazyView = res;
|
|
96
|
+
LazyView.displayName = 'LazyView';
|
|
97
|
+
return LazyView;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `lazyModule(() => import('./admin.module'))` — a lazy feature module.
|
|
101
|
+
* Usable as a Route `module` (the outlet resolves the real module and its
|
|
102
|
+
* injector) or `component`, or directly in JSX: mounts the module's
|
|
103
|
+
* bootstrap view inside the module injector once the chunk lands.
|
|
104
|
+
*/
|
|
105
|
+
export function lazyModule(load) {
|
|
106
|
+
const res = lazyResource(() => load().then((ns) => pickModule(ns, 'lazyModule')), 'lazyModule');
|
|
107
|
+
const LazyModule = ({ fallback, ...props }) => {
|
|
108
|
+
const parent = useInjector();
|
|
109
|
+
const st = res.state;
|
|
110
|
+
const mod = st.status === 'ready' ? st.value : undefined;
|
|
111
|
+
const Boot = mod ? getModuleMeta(mod)?.bootstrap?.[0] : null;
|
|
112
|
+
if (!Boot)
|
|
113
|
+
return st.status === 'loading' ? _jsx(_Fragment, { children: fallback ?? null }) : null;
|
|
114
|
+
const inj = getModuleInjector(mod, parent);
|
|
115
|
+
return h(InjectorContext.Provider, { value: inj }, h(Boot, props));
|
|
116
|
+
};
|
|
117
|
+
LazyModule.__lazyModule = res;
|
|
118
|
+
LazyModule.displayName = 'LazyModule';
|
|
119
|
+
return LazyModule;
|
|
120
|
+
}
|
|
121
|
+
// --- Route-field integration -------------------------------------------------
|
|
122
|
+
/**
|
|
123
|
+
* Read a lazyView/lazyModule facade's resource state — kicks the loader on
|
|
124
|
+
* first read and subscribes the reader (outlet render, router.pending
|
|
125
|
+
* computed). Non-facade values answer null.
|
|
126
|
+
*/
|
|
127
|
+
export function lazyStateOf(v) {
|
|
128
|
+
const res = typeof v === 'function' ? v.__lazyView ?? v.__lazyModule : null;
|
|
129
|
+
return res ? res.state : null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Sync unwrap for the Route `module` field: a lazyModule facade answers
|
|
133
|
+
* with the resolved module class once loaded (undefined while pending or
|
|
134
|
+
* after a failure). Reading the resource state subscribes the caller.
|
|
135
|
+
*/
|
|
136
|
+
export function moduleRefOf(m) {
|
|
137
|
+
if (typeof m === 'function' && m.__lazyModule) {
|
|
138
|
+
const st = m.__lazyModule.state;
|
|
139
|
+
return st.status === 'ready' ? st.value : undefined;
|
|
140
|
+
}
|
|
141
|
+
return m;
|
|
142
|
+
}
|
|
143
|
+
/** Async unwrap — awaits the ref's own resolution (kicking it if needed). */
|
|
144
|
+
export function lazyModuleReady(m) {
|
|
145
|
+
return typeof m === 'function' && m.__lazyModule
|
|
146
|
+
? m.__lazyModule.ready
|
|
147
|
+
: null;
|
|
148
|
+
}
|
package/dist/module.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Injector } from './injector';
|
|
2
2
|
import type { ModuleMeta } from './decorators';
|
|
3
3
|
export declare function getModuleMeta(mod: any): ModuleMeta | undefined;
|
|
4
|
+
export declare function isModule(mod: any): boolean;
|
|
4
5
|
export declare function registerModule(mod: any, injector: Injector, seen: Set<any>): void;
|
|
5
6
|
export declare function createModuleInjector(mod: any, parent: Injector): Injector;
|
|
6
7
|
export declare function getModuleInjector(mod: any, parent: Injector): Injector;
|
package/dist/module.js
CHANGED
|
@@ -2,12 +2,16 @@ import { h, render } from 'preact';
|
|
|
2
2
|
import { Injector } from './injector';
|
|
3
3
|
import { InjectorContext } from './context';
|
|
4
4
|
import { installViewInterop } from './view';
|
|
5
|
+
import { ownMeta } from './decorators';
|
|
5
6
|
// Modules are the unit of provider scoping. A module's providers (plus those
|
|
6
7
|
// of its imports, transitively) register into a single child Injector. The
|
|
7
8
|
// injector tree mirrors the view tree: root injector -> AppModule injector ->
|
|
8
9
|
// feature/lazy module injectors -> per-view injectors.
|
|
9
10
|
export function getModuleMeta(mod) {
|
|
10
|
-
return mod
|
|
11
|
+
return ownMeta(mod, '__module');
|
|
12
|
+
}
|
|
13
|
+
export function isModule(mod) {
|
|
14
|
+
return getModuleMeta(mod) != null;
|
|
11
15
|
}
|
|
12
16
|
// Applies a module to an injector: registers all transitively imported
|
|
13
17
|
// modules (and their providers/controllers) into it and marks them visible
|
package/dist/pmvc.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isView } from './view';
|
|
2
|
+
import { isController, isInjectable, getComponentMeta, getInjectableMeta } from './decorators';
|
|
3
|
+
import { getModuleMeta, isModule } from './module';
|
|
4
|
+
import { isComponent } from './lazy';
|
|
5
|
+
export declare const pmvc: Readonly<{
|
|
6
|
+
isView: typeof isView;
|
|
7
|
+
isController: typeof isController;
|
|
8
|
+
isModule: typeof isModule;
|
|
9
|
+
isInjectable: typeof isInjectable;
|
|
10
|
+
isComponent: typeof isComponent;
|
|
11
|
+
getModuleMeta: typeof getModuleMeta;
|
|
12
|
+
getComponentMeta: typeof getComponentMeta;
|
|
13
|
+
getInjectableMeta: typeof getInjectableMeta;
|
|
14
|
+
}>;
|
package/dist/pmvc.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isView } from './view';
|
|
2
|
+
import { isController, isInjectable, getComponentMeta, getInjectableMeta, } from './decorators';
|
|
3
|
+
import { getModuleMeta, isModule } from './module';
|
|
4
|
+
import { isComponent } from './lazy';
|
|
5
|
+
// The framework's class-flag read surface in one place: `pmvc.isView(x)`,
|
|
6
|
+
// `pmvc.isController(x)`. Classes are identified by `__x` stamps (see
|
|
7
|
+
// decorators.ts for the stamp families); these accessors encode the
|
|
8
|
+
// own-vs-inherited rules so callers never touch the raw fields:
|
|
9
|
+
//
|
|
10
|
+
// identity, inherited: isView, isController, isInjectable, isComponent
|
|
11
|
+
// config, own-class: isModule, getModuleMeta, getComponentMeta
|
|
12
|
+
// config, inherited: getInjectableMeta (Angular's ɵprov rule)
|
|
13
|
+
export const pmvc = Object.freeze({
|
|
14
|
+
isView,
|
|
15
|
+
isController,
|
|
16
|
+
isModule,
|
|
17
|
+
isInjectable,
|
|
18
|
+
isComponent,
|
|
19
|
+
getModuleMeta,
|
|
20
|
+
getComponentMeta,
|
|
21
|
+
getInjectableMeta,
|
|
22
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Signal } from '@preact/signals';
|
|
2
|
+
import type { Route } from './router';
|
|
3
|
+
export interface RouteResolution {
|
|
4
|
+
/** The view/component this route renders at its depth (null = componentless
|
|
5
|
+
* wrapper — the outlet passes through to the next depth). */
|
|
6
|
+
component: any;
|
|
7
|
+
/** Module whose injector scopes this level (its bootstrap view is the
|
|
8
|
+
* fallback component). */
|
|
9
|
+
module?: any;
|
|
10
|
+
/** Child routes — static `children`, or a Route[] resolved by loadChildren. */
|
|
11
|
+
children?: Route[];
|
|
12
|
+
}
|
|
13
|
+
export type LazyRouteState = {
|
|
14
|
+
status: 'loading';
|
|
15
|
+
} | {
|
|
16
|
+
status: 'ready';
|
|
17
|
+
resolved: RouteResolution;
|
|
18
|
+
} | {
|
|
19
|
+
status: 'error';
|
|
20
|
+
error: unknown;
|
|
21
|
+
};
|
|
22
|
+
export declare function isLazyRoute(route: Route): boolean;
|
|
23
|
+
/** Sync resolution — what the route's static fields already know. Used for
|
|
24
|
+
* eager routes and as the render answer while a lazy route's chunk loads
|
|
25
|
+
* (an eager shell mounts immediately; its lazy children extend the match).
|
|
26
|
+
* A `module: lazyModule(...)` field reads the facade's resource: pending
|
|
27
|
+
* resolves to no module yet, and the read subscribes the outlet so 'ready'
|
|
28
|
+
* swaps the real module in. */
|
|
29
|
+
export declare function resolveRouteSync(route: Route): RouteResolution;
|
|
30
|
+
/**
|
|
31
|
+
* True while the route's own render target is still resolving — powers
|
|
32
|
+
* `router.pending` and the outlet's `fallback`. Only matched routes are
|
|
33
|
+
* asked, so the reads merely subscribe work the outlet would kick anyway.
|
|
34
|
+
*/
|
|
35
|
+
export declare function routePending(route: Route): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* True while a matched route's `loadChildren` is holding back deeper
|
|
38
|
+
* matches — the pending match alone ends the chain, so an outlet sitting
|
|
39
|
+
* one depth past it is where the loaded child will appear. Mirrors the
|
|
40
|
+
* condition matching uses before consulting the resolution signal.
|
|
41
|
+
*/
|
|
42
|
+
export declare function routeChildPending(route: Route): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Per-route resolution state. First call kicks the loaders; the signal flips
|
|
45
|
+
* to 'ready'/'error' when they settle — `router.matches` re-runs because
|
|
46
|
+
* matching reads it for `loadChildren`, and outlets re-render on read.
|
|
47
|
+
*/
|
|
48
|
+
export declare function lazyRouteState(route: Route): Signal<LazyRouteState>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { signal } from '@preact/signals';
|
|
2
|
+
import { getModuleMeta } from '../module';
|
|
3
|
+
import { lazyModuleReady, lazyStateOf, moduleRefOf, pickComponent, pickLazyFeature, } from '../lazy';
|
|
4
|
+
export function isLazyRoute(route) {
|
|
5
|
+
return !!(route.loadComponent || route.loadChildren);
|
|
6
|
+
}
|
|
7
|
+
/** Sync resolution — what the route's static fields already know. Used for
|
|
8
|
+
* eager routes and as the render answer while a lazy route's chunk loads
|
|
9
|
+
* (an eager shell mounts immediately; its lazy children extend the match).
|
|
10
|
+
* A `module: lazyModule(...)` field reads the facade's resource: pending
|
|
11
|
+
* resolves to no module yet, and the read subscribes the outlet so 'ready'
|
|
12
|
+
* swaps the real module in. */
|
|
13
|
+
export function resolveRouteSync(route) {
|
|
14
|
+
const mod = moduleRefOf(route.module);
|
|
15
|
+
return {
|
|
16
|
+
component: route.component ?? getModuleMeta(mod)?.bootstrap?.[0] ?? null,
|
|
17
|
+
module: mod,
|
|
18
|
+
children: route.children,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* True while the route's own render target is still resolving — powers
|
|
23
|
+
* `router.pending` and the outlet's `fallback`. Only matched routes are
|
|
24
|
+
* asked, so the reads merely subscribe work the outlet would kick anyway.
|
|
25
|
+
*/
|
|
26
|
+
export function routePending(route) {
|
|
27
|
+
if (isLazyRoute(route) && lazyRouteState(route).value.status === 'loading') {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return (lazyStateOf(route.component)?.status === 'loading' ||
|
|
31
|
+
lazyStateOf(route.module)?.status === 'loading');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* True while a matched route's `loadChildren` is holding back deeper
|
|
35
|
+
* matches — the pending match alone ends the chain, so an outlet sitting
|
|
36
|
+
* one depth past it is where the loaded child will appear. Mirrors the
|
|
37
|
+
* condition matching uses before consulting the resolution signal.
|
|
38
|
+
*/
|
|
39
|
+
export function routeChildPending(route) {
|
|
40
|
+
return (!route.children?.length &&
|
|
41
|
+
!!route.loadChildren &&
|
|
42
|
+
lazyRouteState(route).value.status === 'loading');
|
|
43
|
+
}
|
|
44
|
+
const states = new WeakMap();
|
|
45
|
+
/**
|
|
46
|
+
* Per-route resolution state. First call kicks the loaders; the signal flips
|
|
47
|
+
* to 'ready'/'error' when they settle — `router.matches` re-runs because
|
|
48
|
+
* matching reads it for `loadChildren`, and outlets re-render on read.
|
|
49
|
+
*/
|
|
50
|
+
export function lazyRouteState(route) {
|
|
51
|
+
let s = states.get(route);
|
|
52
|
+
if (!s) {
|
|
53
|
+
s = signal({ status: 'loading' });
|
|
54
|
+
states.set(route, s);
|
|
55
|
+
resolveLazyRoute(route).then((resolved) => {
|
|
56
|
+
s.value = { status: 'ready', resolved };
|
|
57
|
+
}, (error) => {
|
|
58
|
+
console.error(`[framework] lazy route '${route.path}' failed to load:`, error);
|
|
59
|
+
s.value = { status: 'error', error };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return s;
|
|
63
|
+
}
|
|
64
|
+
async function resolveLazyRoute(route) {
|
|
65
|
+
const lazyMod = lazyModuleReady(route.module);
|
|
66
|
+
const [feature, comp, awaitedMod] = await Promise.all([
|
|
67
|
+
route.loadChildren ? route.loadChildren() : undefined,
|
|
68
|
+
route.loadComponent ? route.loadComponent() : undefined,
|
|
69
|
+
lazyMod ?? undefined,
|
|
70
|
+
]);
|
|
71
|
+
let mod = lazyMod ? awaitedMod : route.module;
|
|
72
|
+
let children = route.children;
|
|
73
|
+
if (route.loadChildren) {
|
|
74
|
+
const picked = pickLazyFeature(feature, `loadChildren for route '${route.path}'`);
|
|
75
|
+
if (Array.isArray(picked)) {
|
|
76
|
+
if (!children?.length)
|
|
77
|
+
children = picked;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
mod ?? (mod = picked);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const component = route.component ??
|
|
84
|
+
(route.loadComponent ? pickComponent(comp, `loadComponent for route '${route.path}'`) : undefined) ??
|
|
85
|
+
getModuleMeta(mod)?.bootstrap?.[0] ??
|
|
86
|
+
null;
|
|
87
|
+
return { component, module: mod, children };
|
|
88
|
+
}
|
package/dist/router/module.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getModuleMeta } from '../module';
|
|
2
1
|
import { ROUTES, ROUTER_BASE, Router, defaultBasePath } from './router';
|
|
2
|
+
import { resolveRouteSync } from './lazy';
|
|
3
3
|
// RouterModule.forRoot(routes) returns a module-shaped object — anything with
|
|
4
4
|
// __module metadata can sit in `imports`, so static config works the same as
|
|
5
5
|
// a decorated module class.
|
|
@@ -23,10 +23,5 @@ export class RouterModule {
|
|
|
23
23
|
// Resolve which view a matched route should render: an explicit component, or
|
|
24
24
|
// the module's declared bootstrap component.
|
|
25
25
|
export function routeComponent(match) {
|
|
26
|
-
|
|
27
|
-
if (route.component)
|
|
28
|
-
return route.component;
|
|
29
|
-
if (route.module)
|
|
30
|
-
return getModuleMeta(route.module)?.bootstrap?.[0] ?? null;
|
|
31
|
-
return null;
|
|
26
|
+
return resolveRouteSync(match.route).component;
|
|
32
27
|
}
|
package/dist/router/outlet.d.ts
CHANGED
package/dist/router/outlet.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
2
|
import { createContext } from 'preact';
|
|
3
3
|
import { useContext } from 'preact/hooks';
|
|
4
4
|
import PRRouter from 'preact-router';
|
|
5
5
|
import { useInjector, useService, InjectorContext } from '../context';
|
|
6
6
|
import { getModuleInjector } from '../module';
|
|
7
|
-
import {
|
|
7
|
+
import { lazyStateOf } from '../lazy';
|
|
8
|
+
import { isLazyRoute, lazyRouteState, resolveRouteSync, routeChildPending, } from './lazy';
|
|
8
9
|
import { Router } from './router';
|
|
9
10
|
/**
|
|
10
11
|
* Which depth of the matched chain this outlet renders — the root outlet
|
|
@@ -20,25 +21,47 @@ const OutletDepth = createContext(0);
|
|
|
20
21
|
* The sink renders nothing; url/matches props die on the component.
|
|
21
22
|
*/
|
|
22
23
|
const BridgeSink = () => null;
|
|
23
|
-
export function RouterOutlet() {
|
|
24
|
+
export function RouterOutlet(props) {
|
|
24
25
|
const router = useService(Router);
|
|
25
26
|
const parent = useInjector();
|
|
26
27
|
const depth = useContext(OutletDepth);
|
|
27
28
|
const matches = router.matches.value;
|
|
28
29
|
const match = matches?.[depth];
|
|
29
30
|
if (!match) {
|
|
31
|
+
// One depth past the chain while the last match's lazy children load —
|
|
32
|
+
// this outlet is where the loaded child will appear, so the fallback
|
|
33
|
+
// marks the spot. (Only loadChildren can hold back deeper matches.)
|
|
34
|
+
const chainLen = matches?.length ?? 0;
|
|
35
|
+
const last = chainLen ? matches[chainLen - 1] : undefined;
|
|
36
|
+
if (last && depth === chainLen && routeChildPending(last.route)) {
|
|
37
|
+
return _jsx(_Fragment, { children: props.fallback ?? null });
|
|
38
|
+
}
|
|
30
39
|
// The outermost outlet keeps the bridge mounted even with no match, so
|
|
31
40
|
// route() interop survives 404s.
|
|
32
41
|
return depth === 0 ? (_jsx(PRRouter, { onChange: (e) => router.syncUrl(e.url), children: _jsx(BridgeSink, { default: true }) })) : null;
|
|
33
42
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
// Lazy routes resolve through a per-route signal: while 'loading' the
|
|
44
|
+
// static fields still answer (an eager shell mounts while its lazy
|
|
45
|
+
// children load); 'ready' swaps in the resolved component/module.
|
|
46
|
+
const st = isLazyRoute(match.route) ? lazyRouteState(match.route).value : null;
|
|
47
|
+
const resolved = st?.status === 'ready' ? st.resolved : resolveRouteSync(match.route);
|
|
48
|
+
const Component = resolved.component;
|
|
37
49
|
// A route with `module` gets the module's injector — its providers are
|
|
38
50
|
// scoped to that feature. Nesting is automatic: a deeper outlet resolves
|
|
39
51
|
// its module under this level's injector.
|
|
40
|
-
const injector =
|
|
41
|
-
? getModuleInjector(
|
|
52
|
+
const injector = resolved.module
|
|
53
|
+
? getModuleInjector(resolved.module, parent)
|
|
42
54
|
: parent;
|
|
43
|
-
|
|
55
|
+
// This depth is pending while its own render target is unresolved:
|
|
56
|
+
// route-level loaders with nothing statically known yet, a lazyModule
|
|
57
|
+
// facade in the module field, or a lazyView/lazyModule facade suspending
|
|
58
|
+
// on mount. An eager shell under a pending loadChildren is NOT pending —
|
|
59
|
+
// it renders now and the deeper outlet's fallback covers the child.
|
|
60
|
+
const pending = (st?.status === 'loading' && !Component && !resolved.module) ||
|
|
61
|
+
lazyStateOf(Component)?.status === 'loading' ||
|
|
62
|
+
lazyStateOf(match.route.module)?.status === 'loading';
|
|
63
|
+
return (_jsxs(OutletDepth.Provider, { value: depth + 1, children: [depth === 0 && (_jsx(PRRouter, { onChange: (e) => router.syncUrl(e.url), children: _jsx(BridgeSink, { default: true }) })), _jsx(InjectorContext.Provider, { value: injector, children: pending ? (props.fallback ?? null) : Component ? (_jsx(Component, {})) : (
|
|
64
|
+
/* A componentless route is a pure wrapper — the chain continues
|
|
65
|
+
one depth down without needing a shell view. */
|
|
66
|
+
_jsx(RouterOutlet, { fallback: props.fallback })) })] }));
|
|
44
67
|
}
|
package/dist/router/router.d.ts
CHANGED
|
@@ -3,6 +3,20 @@ export interface Route {
|
|
|
3
3
|
path: string;
|
|
4
4
|
component?: any;
|
|
5
5
|
module?: any;
|
|
6
|
+
/**
|
|
7
|
+
* Lazy standalone component — a bundler split point. The loader resolves
|
|
8
|
+
* to a View class / @Component-stamped class / function component, picked
|
|
9
|
+
* from the module's default export or stamped named exports (or selected
|
|
10
|
+
* explicitly: `() => import('./x.view').then(m => m.XView)`).
|
|
11
|
+
*/
|
|
12
|
+
loadComponent?: () => Promise<any>;
|
|
13
|
+
/**
|
|
14
|
+
* Lazy feature — resolves to an @Module class (its bootstrap view renders
|
|
15
|
+
* inside the module injector, like `module`) or a Route[] spliced in as
|
|
16
|
+
* this route's children. Matching reads the resolution signal, so the
|
|
17
|
+
* chain extends as soon as the chunk lands.
|
|
18
|
+
*/
|
|
19
|
+
loadChildren?: () => Promise<any>;
|
|
6
20
|
children?: Route[];
|
|
7
21
|
}
|
|
8
22
|
export interface RouteMatch {
|
|
@@ -32,6 +46,12 @@ export declare class Router {
|
|
|
32
46
|
match: import("@preact/signals-core").ReadonlySignal<RouteMatch | null>;
|
|
33
47
|
/** Params merged across the whole chain (inner levels win on collision). */
|
|
34
48
|
params: import("@preact/signals-core").ReadonlySignal<any>;
|
|
49
|
+
/**
|
|
50
|
+
* True while any route in the matched chain is still resolving its lazy
|
|
51
|
+
* chunk (loadComponent/loadChildren/lazyView/lazyModule) — parents read
|
|
52
|
+
* it to render loading UI anywhere above the outlet.
|
|
53
|
+
*/
|
|
54
|
+
pending: import("@preact/signals-core").ReadonlySignal<boolean>;
|
|
35
55
|
constructor(routes: Route[], base?: string);
|
|
36
56
|
/**
|
|
37
57
|
* Static-host deep links: the site's 404 shim redirects
|
package/dist/router/router.js
CHANGED
|
@@ -8,6 +8,7 @@ import { computed, signal } from '@preact/signals';
|
|
|
8
8
|
import { route as prRoute } from 'preact-router';
|
|
9
9
|
import { Injectable } from '../decorators';
|
|
10
10
|
import { InjectionToken } from '../injector';
|
|
11
|
+
import { lazyRouteState, routePending } from './lazy';
|
|
11
12
|
export const ROUTES = new InjectionToken('ROUTES');
|
|
12
13
|
export const ROUTER_BASE = new InjectionToken('ROUTER_BASE');
|
|
13
14
|
/**
|
|
@@ -56,8 +57,25 @@ function matchSegments(routes, segs) {
|
|
|
56
57
|
const m = matchPrefix(route.path, segs);
|
|
57
58
|
if (m === null)
|
|
58
59
|
continue;
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
let children = route.children;
|
|
61
|
+
let pending = false;
|
|
62
|
+
if (!children?.length && route.loadChildren) {
|
|
63
|
+
// Reading the resolution signal inside the matches computed does two
|
|
64
|
+
// things: kicks the loader, and subscribes this computed so the chain
|
|
65
|
+
// re-matches the moment a lazy Route[] lands.
|
|
66
|
+
const st = lazyRouteState(route).value;
|
|
67
|
+
if (st.status === 'loading')
|
|
68
|
+
pending = true;
|
|
69
|
+
else if (st.status === 'ready')
|
|
70
|
+
children = st.resolved.children;
|
|
71
|
+
}
|
|
72
|
+
if (pending) {
|
|
73
|
+
// Lazy children unresolved — the parent claims the URL alone until the
|
|
74
|
+
// chunk lands and the computed re-matches into the loaded tree.
|
|
75
|
+
return [{ route, params: m.params }];
|
|
76
|
+
}
|
|
77
|
+
if (children?.length) {
|
|
78
|
+
const child = matchSegments(children, m.rest);
|
|
61
79
|
if (child !== null)
|
|
62
80
|
return [{ route, params: m.params }, ...child];
|
|
63
81
|
if (m.rest.length === 0)
|
|
@@ -86,6 +104,12 @@ let Router = class Router {
|
|
|
86
104
|
this.match = computed(() => this.matches.value?.[0] ?? null);
|
|
87
105
|
/** Params merged across the whole chain (inner levels win on collision). */
|
|
88
106
|
this.params = computed(() => Object.assign({}, ...(this.matches.value ?? []).map((m) => m.params)));
|
|
107
|
+
/**
|
|
108
|
+
* True while any route in the matched chain is still resolving its lazy
|
|
109
|
+
* chunk (loadComponent/loadChildren/lazyView/lazyModule) — parents read
|
|
110
|
+
* it to render loading UI anywhere above the outlet.
|
|
111
|
+
*/
|
|
112
|
+
this.pending = computed(() => !!this.matches.value?.some((m) => routePending(m.route)));
|
|
89
113
|
this.onPopState = () => {
|
|
90
114
|
this.syncUrl(location.pathname + location.search);
|
|
91
115
|
};
|
package/dist/view.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Component as PreactComponent } from 'preact';
|
|
2
2
|
import type { ComponentChildren } from 'preact';
|
|
3
3
|
export declare abstract class View<P = {}> extends PreactComponent<P> {
|
|
4
|
+
static readonly __view = true;
|
|
4
5
|
abstract render(): ComponentChildren;
|
|
5
6
|
setState(): never;
|
|
6
7
|
forceUpdate(): never;
|
|
7
8
|
}
|
|
9
|
+
/** Identity stamp read — a subclass of a View stays a view. */
|
|
10
|
+
export declare function isView(t: any): boolean;
|
|
8
11
|
export declare function installViewInterop(): void;
|
package/dist/view.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useLayoutEffect, useRef } from 'preact/hooks';
|
|
|
3
3
|
import { InjectorContext, useInjector } from './context';
|
|
4
4
|
import { Injector } from './injector';
|
|
5
5
|
import { registerModule } from './module';
|
|
6
|
-
import { ownerOf } from './decorators';
|
|
6
|
+
import { ownerOf, getComponentMeta } from './decorators';
|
|
7
7
|
// --- Class-based views -----------------------------------------------------
|
|
8
8
|
// A View subclass is never instantiated by Preact. The options.vnode hook
|
|
9
9
|
// below swaps any vnode whose type is a View subclass for a host function
|
|
@@ -23,8 +23,13 @@ export class View extends PreactComponent {
|
|
|
23
23
|
throw new Error('View components are signal-driven — assign a signal instead of calling forceUpdate()');
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// Identity stamp, not instanceof: statics inherit through the constructor
|
|
27
|
+
// chain, so subclasses (and subclasses of subclasses) read true, and a
|
|
28
|
+
// view class built against a second copy of this package still interops.
|
|
29
|
+
View.__view = true;
|
|
30
|
+
/** Identity stamp read — a subclass of a View stays a view. */
|
|
31
|
+
export function isView(t) {
|
|
32
|
+
return typeof t === 'function' && !!t.__view;
|
|
28
33
|
}
|
|
29
34
|
// --- Error boundary ----------------------------------------------------------
|
|
30
35
|
// Real Preact class component (not a View — it must be instantiated by Preact).
|
|
@@ -61,7 +66,7 @@ function hostFor(cls) {
|
|
|
61
66
|
let host = hostCache.get(cls);
|
|
62
67
|
if (host)
|
|
63
68
|
return host;
|
|
64
|
-
const meta = (cls
|
|
69
|
+
const meta = getComponentMeta(cls) ?? {};
|
|
65
70
|
const inner = function ViewHost(props) {
|
|
66
71
|
const parent = useInjector();
|
|
67
72
|
const ref = useRef(null);
|
|
@@ -125,7 +130,7 @@ export function installViewInterop() {
|
|
|
125
130
|
const prev = options.vnode;
|
|
126
131
|
options.vnode = (vnode) => {
|
|
127
132
|
const t = vnode.type;
|
|
128
|
-
if (
|
|
133
|
+
if (isView(t))
|
|
129
134
|
vnode.type = hostFor(t);
|
|
130
135
|
prev?.(vnode);
|
|
131
136
|
};
|
package/package.json
CHANGED