@preact-mvc/core 0.1.0 → 0.1.1

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/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @preact-mvc/core
2
+
3
+ [![CI](https://github.com/jwhenry3/preact-mvc/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jwhenry3/preact-mvc/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/jwhenry3/preact-mvc/graph/badge.svg?branch=main)](https://codecov.io/gh/jwhenry3/preact-mvc)
5
+ [![Socket Badge](https://badge.socket.dev/npm/package/@preact-mvc/core)](https://badge.socket.dev/npm/package/@preact-mvc/core)
6
+
7
+ Angular-inspired architecture rendered by Preact: `@Module`/`@Component`
8
+ decorators, hierarchical dependency injection, class-based `View`
9
+ components, controllers, an injector-scoped router, and signal reactivity
10
+ via `@preact/signals` — all over plain JSX, with no template compiler.
11
+
12
+ **[preact-mvc — documentation](https://jwhenry3.github.io/preact-mvc/docs/overview/)**
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @preact-mvc/core preact @preact/signals
18
+ ```
19
+
20
+ Point JSX at Preact (`tsconfig.json`):
21
+
22
+ ```jsonc
23
+ {
24
+ "compilerOptions": {
25
+ "jsx": "react-jsx",
26
+ "jsxImportSource": "preact",
27
+ "experimentalDecorators": true,
28
+ "useDefineForClassFields": false
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## A view is a class
34
+
35
+ `View` subclasses are constructed once per mount inside their own injector
36
+ scope — `inject()` works in field initializers and constructors, `render()`
37
+ stays hook-free, and signals drive re-rendering:
38
+
39
+ ```tsx
40
+ import { Component, View, inject, signal, computed } from '@preact-mvc/core';
41
+
42
+ @Component()
43
+ export class CounterView extends View {
44
+ count = signal(0);
45
+ doubled = computed(() => this.count.value * 2);
46
+
47
+ render() {
48
+ return (
49
+ <section>
50
+ <button onClick={() => (this.count.value -= 1)}>−</button>
51
+ <span>{this.count.value}</span>
52
+ <button onClick={() => (this.count.value += 1)}>+</button>
53
+ <p>doubled: {this.doubled.value}</p>
54
+ </section>
55
+ );
56
+ }
57
+ }
58
+ ```
59
+
60
+ ## Modules are the app map
61
+
62
+ ```ts
63
+ import { Module, RouterModule } from '@preact-mvc/core';
64
+ import { AppShell } from './app.shell';
65
+ import { routes } from './app.routes';
66
+
67
+ @Module({
68
+ imports: [RouterModule.forRoot(routes)],
69
+ providers: [{ provide: API_URL, useValue: '/api' }],
70
+ bootstrap: [AppShell],
71
+ })
72
+ export class AppModule {}
73
+ ```
74
+
75
+ ```ts
76
+ // main.tsx
77
+ import { bootstrap } from '@preact-mvc/core';
78
+ import { AppModule } from './app/app.module';
79
+
80
+ bootstrap(AppModule, document.getElementById('app')!);
81
+ ```
82
+
83
+ `components`/`bootstrap` declarations are enforced ownership: a declared
84
+ view may only mount where its module's injector is an ancestor.
85
+ Undeclared views mount anywhere — structure is opt-in per declaration.
86
+
87
+ ## DI, controllers, router
88
+
89
+ - `@Injectable`/`@Controller` classes resolve through the element-injector
90
+ hierarchy — view providers → module providers → root, closest scope wins.
91
+ - `OnInit`/`OnDestroy` lifecycle plus `DestroyRef` make teardown
92
+ declaration-driven instead of `useEffect` cleanup.
93
+ - `RouterModule.forRoot(routes)` + `<RouterOutlet/>` + `<Link/>` give you
94
+ nested routes whose `module` boundaries get their own child injectors.
95
+
96
+ Published builds ship compiled `dist/` (`.js` + `.d.ts`).
@@ -0,0 +1,10 @@
1
+ import type { ComponentChildren } from 'preact';
2
+ import { Injector } from './injector';
3
+ import type { Provider, Token } from './injector';
4
+ export declare const InjectorContext: import("preact").Context<Injector>;
5
+ export declare function useInjector(): Injector;
6
+ export declare function useService<T>(token: Token<T>): T;
7
+ export declare function Provide(props: {
8
+ providers: Provider[];
9
+ children?: ComponentChildren;
10
+ }): import("preact").JSX.Element;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ import { createContext } from 'preact';
3
+ import { useContext, useRef } from 'preact/hooks';
4
+ import { Injector } from './injector';
5
+ // The current injector travels through the view tree as Preact context.
6
+ // Module and route boundaries swap in child injectors.
7
+ export const InjectorContext = createContext(null);
8
+ export function useInjector() {
9
+ const injector = useContext(InjectorContext);
10
+ if (!injector) {
11
+ throw new Error('No injector in context — is this view mounted under bootstrap()?');
12
+ }
13
+ return injector;
14
+ }
15
+ // Resolve a service/store from the view's injector scope.
16
+ export function useService(token) {
17
+ return useInjector().get(token);
18
+ }
19
+ // View-local provider scope: equivalent to a component-level providers array.
20
+ // Providers created here live and die with this subtree.
21
+ export function Provide(props) {
22
+ const parent = useInjector();
23
+ const ref = useRef(null);
24
+ if (!ref.current) {
25
+ ref.current = new Injector(parent, 'view providers');
26
+ for (const p of props.providers)
27
+ ref.current.provide(p);
28
+ }
29
+ return (_jsx(InjectorContext.Provider, { value: ref.current, children: props.children }));
30
+ }
@@ -0,0 +1,8 @@
1
+ import type { Type } from './injector';
2
+ export interface OnInit {
3
+ onInit(): void;
4
+ }
5
+ export interface OnDestroy {
6
+ onDestroy(): void;
7
+ }
8
+ export declare function useController<T extends object>(cls: Type<T>): T;
@@ -0,0 +1,21 @@
1
+ import { useLayoutEffect, useRef } from 'preact/hooks';
2
+ import { useInjector } from './context';
3
+ import { Injector } from './injector';
4
+ // Binds a controller class to a view instance. The controller is created once
5
+ // per mounted view inside its own child injector, so `inject()` in its
6
+ // constructor sees every provider from the enclosing module scope upward.
7
+ // Lifecycle maps to mount/unmount: onInit on mount, onDestroy on unmount.
8
+ export function useController(cls) {
9
+ const parent = useInjector();
10
+ const ref = useRef(null);
11
+ if (!ref.current) {
12
+ const injector = new Injector(parent, `controller ${cls.name}`);
13
+ ref.current = { injector, ctrl: injector.instantiate(cls) };
14
+ }
15
+ const { ctrl, injector } = ref.current;
16
+ useLayoutEffect(() => {
17
+ injector.activate();
18
+ return () => injector.deactivate();
19
+ }, []);
20
+ return ctrl;
21
+ }
@@ -0,0 +1,22 @@
1
+ import type { Provider, Type } from './injector';
2
+ export interface InjectableMeta {
3
+ providedIn?: 'root';
4
+ }
5
+ export declare function Injectable(meta?: InjectableMeta): <T extends Type<any>>(target: T) => T;
6
+ export declare function Controller(): <T extends Type<any>>(target: T) => T;
7
+ export interface ComponentMeta {
8
+ providers?: Provider[];
9
+ controllers?: Type<any>[];
10
+ imports?: any[];
11
+ }
12
+ export declare function Component(meta?: ComponentMeta): <T>(target: T) => T;
13
+ export interface ModuleMeta {
14
+ imports?: any[];
15
+ providers?: Provider[];
16
+ controllers?: Type<any>[];
17
+ components?: any[];
18
+ exports?: any[];
19
+ bootstrap?: any[];
20
+ }
21
+ export declare function ownerOf(view: any): any;
22
+ export declare function Module(meta: ModuleMeta): <T>(target: T) => T;
@@ -0,0 +1,38 @@
1
+ export function Injectable(meta = {}) {
2
+ return function (target) {
3
+ target.__injectable = meta;
4
+ return target;
5
+ };
6
+ }
7
+ export function Controller() {
8
+ return function (target) {
9
+ target.__controller = true;
10
+ return target;
11
+ };
12
+ }
13
+ export function Component(meta = {}) {
14
+ return function (target) {
15
+ target.__component = meta;
16
+ return target;
17
+ };
18
+ }
19
+ // View class -> the module that declared it. Built as module classes are
20
+ // decorated; consulted at mount time to enforce scope ownership.
21
+ const componentOwners = new Map();
22
+ export function ownerOf(view) {
23
+ return componentOwners.get(view);
24
+ }
25
+ export function Module(meta) {
26
+ return function (target) {
27
+ target.__module = meta;
28
+ for (const c of [...(meta.components ?? []), ...(meta.bootstrap ?? [])]) {
29
+ const prev = componentOwners.get(c);
30
+ if (prev && prev !== target) {
31
+ console.warn(`[framework] ${c.name ?? c} is declared by both ${prev.name} and ${target.name}; keeping ${prev.name}`);
32
+ continue;
33
+ }
34
+ componentOwners.set(c, target);
35
+ }
36
+ return target;
37
+ };
38
+ }
@@ -1,33 +1,13 @@
1
- // Signals — the reactivity primitive shared by models, controllers, and views.
2
1
  export { signal, computed, effect, batch, untracked } from './signals';
3
2
  export type { Signal, ReadonlySignal } from './signals';
4
-
5
- // Dependency injection
6
- export {
7
- Injector,
8
- InjectionToken,
9
- inject,
10
- runInInjectionContext,
11
- getCurrentInjector,
12
- DestroyRef,
13
- } from './injector';
3
+ export { Injector, InjectionToken, inject, runInInjectionContext, getCurrentInjector, DestroyRef, } from './injector';
14
4
  export type { Provider, Token, Type } from './injector';
15
-
16
- // Metadata
17
5
  export { Injectable, Controller, Module, Component } from './decorators';
18
6
  export type { InjectableMeta, ModuleMeta, ComponentMeta } from './decorators';
19
-
20
- // Class-based views
21
7
  export { View, installViewInterop } from './view';
22
-
23
- // Modules & bootstrap
24
8
  export { bootstrap, createModuleInjector, getModuleInjector, getModuleMeta } from './module';
25
-
26
- // View integration
27
9
  export { InjectorContext, useInjector, useService, Provide } from './context';
28
10
  export { useController } from './controller';
29
11
  export type { OnInit, OnDestroy } from './controller';
30
-
31
- // Router
32
12
  export { Router, ROUTES, ROUTER_BASE, RouterModule, RouterOutlet, Link, defaultBasePath } from './router';
33
13
  export type { Route, RouteMatch, RouterOptions } from './router';
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // Signals — the reactivity primitive shared by models, controllers, and views.
2
+ export { signal, computed, effect, batch, untracked } from './signals';
3
+ // Dependency injection
4
+ export { Injector, InjectionToken, inject, runInInjectionContext, getCurrentInjector, DestroyRef, } from './injector';
5
+ // Metadata
6
+ export { Injectable, Controller, Module, Component } from './decorators';
7
+ // Class-based views
8
+ export { View, installViewInterop } from './view';
9
+ // Modules & bootstrap
10
+ export { bootstrap, createModuleInjector, getModuleInjector, getModuleMeta } from './module';
11
+ // View integration
12
+ export { InjectorContext, useInjector, useService, Provide } from './context';
13
+ export { useController } from './controller';
14
+ // Router
15
+ export { Router, ROUTES, ROUTER_BASE, RouterModule, RouterOutlet, Link, defaultBasePath } from './router';
@@ -0,0 +1,47 @@
1
+ export type Type<T> = new (...args: any[]) => T;
2
+ export declare class InjectionToken<T> {
3
+ protected readonly _desc: string;
4
+ constructor(_desc: string);
5
+ toString(): string;
6
+ }
7
+ export type Token<T> = Type<T> | InjectionToken<T> | (abstract new (...args: any[]) => T);
8
+ export type Provider = Type<any> | {
9
+ provide: Token<any>;
10
+ useClass: Type<any>;
11
+ } | {
12
+ provide: Token<any>;
13
+ useValue: any;
14
+ } | {
15
+ provide: Token<any>;
16
+ useFactory: (...deps: any[]) => any;
17
+ deps?: Token<any>[];
18
+ };
19
+ export declare function runInInjectionContext<T>(injector: Injector, fn: () => T): T;
20
+ export declare function inject<T>(token: Token<T>): T;
21
+ export declare function getCurrentInjector(): Injector | null;
22
+ export declare class DestroyRef {
23
+ private readonly injector;
24
+ constructor(injector: Injector);
25
+ onDestroy(fn: () => void): void;
26
+ }
27
+ export declare class Injector {
28
+ readonly parent: Injector | null;
29
+ readonly name: string;
30
+ private records;
31
+ readonly modules: Set<any>;
32
+ constructor(parent?: Injector | null, name?: string);
33
+ get root(): Injector;
34
+ provide(provider: Provider): void;
35
+ has(token: Token<any>): boolean;
36
+ get<T>(token: Token<T>): T;
37
+ private resolveRecord;
38
+ private isRootProvided;
39
+ instantiate<T>(cls: Type<T>, ...args: any[]): T;
40
+ private managed;
41
+ private active;
42
+ private trackLifecycle;
43
+ activate(): void;
44
+ deactivate(): void;
45
+ private cleanups;
46
+ registerCleanup(fn: () => void): void;
47
+ }
@@ -0,0 +1,202 @@
1
+ export class InjectionToken {
2
+ constructor(_desc) {
3
+ this._desc = _desc;
4
+ }
5
+ toString() {
6
+ return `InjectionToken ${this._desc}`;
7
+ }
8
+ }
9
+ // --- Injection context -----------------------------------------------------
10
+ // While an Injector constructs a class (service, controller, factory), it
11
+ // becomes the "current" injector so `inject()` calls inside the constructor
12
+ // or field initializers resolve against it — the same model as Angular's
13
+ // injection context.
14
+ let currentInjector = null;
15
+ export function runInInjectionContext(injector, fn) {
16
+ const prev = currentInjector;
17
+ currentInjector = injector;
18
+ try {
19
+ return fn();
20
+ }
21
+ finally {
22
+ currentInjector = prev;
23
+ }
24
+ }
25
+ export function inject(token) {
26
+ if (!currentInjector) {
27
+ throw new Error('inject() called outside an injection context. It can only be used in the ' +
28
+ 'constructor/field initializers of a class instantiated by the framework.');
29
+ }
30
+ return currentInjector.get(token);
31
+ }
32
+ export function getCurrentInjector() {
33
+ return currentInjector;
34
+ }
35
+ // --- Injector ---------------------------------------------------------------
36
+ function tokenName(token) {
37
+ return typeof token === 'function' ? token.name : token.toString();
38
+ }
39
+ // Injectable handle to the enclosing scope's destruction — inject(DestroyRef)
40
+ // anywhere to register cleanup that runs when the scope is deactivated.
41
+ export class DestroyRef {
42
+ constructor(injector) {
43
+ this.injector = injector;
44
+ }
45
+ onDestroy(fn) {
46
+ this.injector.registerCleanup(fn);
47
+ }
48
+ }
49
+ export class Injector {
50
+ constructor(parent = null, name = 'injector') {
51
+ this.parent = parent;
52
+ this.name = name;
53
+ this.records = new Map();
54
+ // Every module whose providers AND declared components are visible in this
55
+ // scope: the module this injector was created for plus its transitive
56
+ // imports. Populated by registerModule(); read by view-mount enforcement.
57
+ this.modules = new Set();
58
+ // --- Scope lifecycle -----------------------------------------------------
59
+ // Every class this injector constructs (view, injected controllers,
60
+ // view-scoped providers) is managed together: activate() fires onInit on
61
+ // all of them at mount, deactivate() fires onDestroy at unmount — the same
62
+ // way Angular ties provider lifecycles to their injector's scope.
63
+ this.managed = [];
64
+ this.active = false;
65
+ this.cleanups = [];
66
+ this.records.set(DestroyRef, {
67
+ factory: () => null,
68
+ resolving: false,
69
+ resolved: true,
70
+ value: new DestroyRef(this),
71
+ });
72
+ }
73
+ get root() {
74
+ let node = this;
75
+ while (node.parent)
76
+ node = node.parent;
77
+ return node;
78
+ }
79
+ provide(provider) {
80
+ if (typeof provider === 'function') {
81
+ const cls = provider;
82
+ this.records.set(cls, {
83
+ factory: () => this.instantiate(cls),
84
+ resolving: false,
85
+ resolved: false,
86
+ value: undefined,
87
+ });
88
+ }
89
+ else if ('useValue' in provider) {
90
+ this.records.set(provider.provide, {
91
+ factory: () => provider.useValue,
92
+ resolving: false,
93
+ resolved: false,
94
+ value: undefined,
95
+ });
96
+ }
97
+ else if ('useClass' in provider) {
98
+ const cls = provider.useClass;
99
+ this.records.set(provider.provide, {
100
+ factory: () => this.instantiate(cls),
101
+ resolving: false,
102
+ resolved: false,
103
+ value: undefined,
104
+ });
105
+ }
106
+ else {
107
+ const { useFactory, deps = [] } = provider;
108
+ this.records.set(provider.provide, {
109
+ factory: () => useFactory(...deps.map((d) => this.get(d))),
110
+ resolving: false,
111
+ resolved: false,
112
+ value: undefined,
113
+ });
114
+ }
115
+ }
116
+ has(token) {
117
+ if (this.records.has(token))
118
+ return true;
119
+ if (token.__controller)
120
+ return true;
121
+ if (this.parent)
122
+ return this.parent.has(token);
123
+ return this.isRootProvided(token);
124
+ }
125
+ get(token) {
126
+ // Nearest explicit registration wins, walking up the scope chain. This is
127
+ // what makes declaring a controller/provider in a scope meaningful: every
128
+ // descendant resolves that shared instance.
129
+ let node = this;
130
+ while (node) {
131
+ const rec = node.records.get(token);
132
+ if (rec)
133
+ return node.resolveRecord(token, rec);
134
+ node = node.parent;
135
+ }
136
+ // Controllers are transient unless explicitly provided: one instance per
137
+ // requesting scope, so a view always gets its own controller.
138
+ if (token.__controller) {
139
+ this.provide(token);
140
+ return this.resolveRecord(token, this.records.get(token));
141
+ }
142
+ // providedIn: 'root' lazily registers at the top — local providers still
143
+ // shadow it because the chain walk runs first.
144
+ const root = this.root;
145
+ if (root.isRootProvided(token)) {
146
+ root.provide({ provide: token, useClass: token });
147
+ return root.get(token);
148
+ }
149
+ throw new Error(`No provider for ${tokenName(token)} (looked up from "${this.name}")`);
150
+ }
151
+ resolveRecord(token, rec) {
152
+ if (rec.resolving) {
153
+ throw new Error(`Circular dependency detected while resolving ${tokenName(token)}`);
154
+ }
155
+ if (!rec.resolved) {
156
+ rec.resolving = true;
157
+ try {
158
+ rec.value = rec.factory();
159
+ rec.resolved = true;
160
+ }
161
+ finally {
162
+ rec.resolving = false;
163
+ }
164
+ }
165
+ return rec.value;
166
+ }
167
+ isRootProvided(token) {
168
+ const meta = token.__injectable;
169
+ return meta?.providedIn === 'root';
170
+ }
171
+ instantiate(cls, ...args) {
172
+ const inst = runInInjectionContext(this, () => new cls(...args));
173
+ this.trackLifecycle(inst);
174
+ return inst;
175
+ }
176
+ trackLifecycle(inst) {
177
+ if (typeof inst?.onInit !== 'function' && typeof inst?.onDestroy !== 'function')
178
+ return;
179
+ this.managed.push(inst);
180
+ if (this.active)
181
+ inst.onInit?.();
182
+ }
183
+ activate() {
184
+ this.active = true;
185
+ for (const inst of this.managed)
186
+ inst.onInit?.();
187
+ }
188
+ deactivate() {
189
+ for (const inst of this.managed)
190
+ inst.onDestroy?.();
191
+ for (const cleanup of this.cleanups)
192
+ cleanup();
193
+ this.managed.length = 0;
194
+ this.cleanups.length = 0;
195
+ this.active = false;
196
+ }
197
+ // Register teardown (effect disposers, subscriptions, timers) that runs
198
+ // when this scope is deactivated. Exposed to classes via DestroyRef.
199
+ registerCleanup(fn) {
200
+ this.cleanups.push(fn);
201
+ }
202
+ }
@@ -0,0 +1,7 @@
1
+ import { Injector } from './injector';
2
+ import type { ModuleMeta } from './decorators';
3
+ export declare function getModuleMeta(mod: any): ModuleMeta | undefined;
4
+ export declare function registerModule(mod: any, injector: Injector, seen: Set<any>): void;
5
+ export declare function createModuleInjector(mod: any, parent: Injector): Injector;
6
+ export declare function getModuleInjector(mod: any, parent: Injector): Injector;
7
+ export declare function bootstrap(RootModule: any, el: HTMLElement): void;
package/dist/module.js ADDED
@@ -0,0 +1,69 @@
1
+ import { h, render } from 'preact';
2
+ import { Injector } from './injector';
3
+ import { InjectorContext } from './context';
4
+ import { installViewInterop } from './view';
5
+ // Modules are the unit of provider scoping. A module's providers (plus those
6
+ // of its imports, transitively) register into a single child Injector. The
7
+ // injector tree mirrors the view tree: root injector -> AppModule injector ->
8
+ // feature/lazy module injectors -> per-view injectors.
9
+ export function getModuleMeta(mod) {
10
+ return mod?.__module;
11
+ }
12
+ // Applies a module to an injector: registers all transitively imported
13
+ // modules (and their providers/controllers) into it and marks them visible
14
+ // on injector.modules. Used for module injectors and standalone-component
15
+ // `imports` alike.
16
+ export function registerModule(mod, injector, seen) {
17
+ if (seen.has(mod))
18
+ return;
19
+ seen.add(mod);
20
+ const meta = getModuleMeta(mod);
21
+ if (!meta) {
22
+ throw new Error(`${mod?.name ?? mod} is not a module — missing @Module() metadata`);
23
+ }
24
+ for (const imp of meta.imports ?? [])
25
+ registerModule(imp, injector, seen);
26
+ injector.modules.add(mod);
27
+ for (const provider of meta.providers ?? [])
28
+ injector.provide(provider);
29
+ // Module-declared controllers register at module scope: one shared
30
+ // instance for everything injected under that module.
31
+ for (const c of meta.controllers ?? [])
32
+ injector.provide(c);
33
+ }
34
+ export function createModuleInjector(mod, parent) {
35
+ const meta = getModuleMeta(mod);
36
+ if (!meta) {
37
+ throw new Error(`${mod?.name ?? mod} is not a module — missing @Module() metadata`);
38
+ }
39
+ const injector = new Injector(parent, `module ${mod.name ?? '(anonymous)'}`);
40
+ registerModule(mod, injector, new Set());
41
+ return injector;
42
+ }
43
+ // Lazy/feature modules get one injector per parent injector, cached so a
44
+ // route-mounted module's providers stay alive across navigations.
45
+ const moduleInjectorCache = new WeakMap();
46
+ export function getModuleInjector(mod, parent) {
47
+ let byModule = moduleInjectorCache.get(parent);
48
+ if (!byModule) {
49
+ byModule = new Map();
50
+ moduleInjectorCache.set(parent, byModule);
51
+ }
52
+ let injector = byModule.get(mod);
53
+ if (!injector) {
54
+ injector = createModuleInjector(mod, parent);
55
+ byModule.set(mod, injector);
56
+ }
57
+ return injector;
58
+ }
59
+ export function bootstrap(RootModule, el) {
60
+ installViewInterop();
61
+ const root = new Injector(null, 'root');
62
+ const appInjector = createModuleInjector(RootModule, root);
63
+ const meta = getModuleMeta(RootModule);
64
+ const App = meta.bootstrap?.[0];
65
+ if (!App) {
66
+ throw new Error('Root module must declare a bootstrap component: @Module({ bootstrap: [AppView] })');
67
+ }
68
+ render(h(InjectorContext.Provider, { value: appInjector }, h(App, {})), el);
69
+ }
@@ -0,0 +1,4 @@
1
+ export { Router, ROUTES, ROUTER_BASE, defaultBasePath } from './router';
2
+ export { RouterModule, routeComponent } from './module';
3
+ export { RouterOutlet } from './outlet';
4
+ export { Link } from './link';
@@ -0,0 +1,7 @@
1
+ import type { ComponentChildren } from 'preact';
2
+ import type { JSX } from 'preact';
3
+ export declare function Link(props: {
4
+ to: string;
5
+ class?: string;
6
+ children?: ComponentChildren;
7
+ }): JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ import { useService } from '../context';
3
+ import { Router } from './router';
4
+ export function Link(props) {
5
+ const router = useService(Router);
6
+ const onClick = (e) => {
7
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0)
8
+ return;
9
+ e.preventDefault();
10
+ // preact-router's document-level <a> interceptor routes any plain
11
+ // anchor click it sees — stop propagation so ours doesn't double-nav.
12
+ e.stopPropagation();
13
+ router.navigate(props.to);
14
+ };
15
+ return (_jsx("a", { href: router.href(props.to), class: props.class, onClick: onClick, children: props.children }));
16
+ }
@@ -0,0 +1,35 @@
1
+ import { Router } from './router';
2
+ import type { Route } from './router';
3
+ export interface RouterOptions {
4
+ /**
5
+ * Mount prefix the app is served under ('/demo/'), Angular's APP_BASE_HREF.
6
+ * Defaults to the document's own directory (document.baseURI), so apps
7
+ * hosted under a subpath need no config; '/' disables prefixing.
8
+ */
9
+ basePath?: string;
10
+ }
11
+ export declare class RouterModule {
12
+ static forRoot(routes: Route[], opts?: RouterOptions): {
13
+ __module: {
14
+ providers: ({
15
+ provide: import("..").InjectionToken<Route[]>;
16
+ useValue: Route[];
17
+ useFactory?: undefined;
18
+ deps?: undefined;
19
+ } | {
20
+ provide: import("..").InjectionToken<string>;
21
+ useValue: string;
22
+ useFactory?: undefined;
23
+ deps?: undefined;
24
+ } | {
25
+ provide: typeof Router;
26
+ useFactory: (r: Route[], base: string) => Router;
27
+ deps: import("..").InjectionToken<Route[]>[];
28
+ useValue?: undefined;
29
+ })[];
30
+ };
31
+ };
32
+ }
33
+ export declare function routeComponent(match: {
34
+ route: Route;
35
+ }): any;