@stefanobalocco/jfsmrouter 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.
package/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2019-2025, Stefano Balocco <stefano.balocco@gmail.com>
2
+ All rights reserved.
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are met:
5
+ * Redistributions of source code must retain the above copyright notice, this
6
+ list of conditions and the following disclaimer.
7
+ * Redistributions in binary form must reproduce the above copyright notice,
8
+ this list of conditions and the following disclaimer in the documentation
9
+ and/or other materials provided with the distribution.
10
+ * Neither the name of Stefano Balocco nor the names of its contributors may
11
+ be used to endorse or promote products derived from this software without
12
+ specific prior written permission.
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # jFSMRouter
2
+ JavaScript Finite State Machine Router
3
+
4
+ `jFSMRouter` (JavaScript Finite State Machine Router) is a JavaScript class that implements a **Finite State Machine** (FSM) integrated with a **hash-based router** for browsers. It allows centralized management of states, transitions, and routes.
5
+
6
+ ## 1. Singleton Instance
7
+
8
+ The class uses the Singleton pattern: only one global instance exists, which you can access with:
9
+
10
+ ```js
11
+ import router from 'https://cdn.jsdelivr.net/gh/StefanoBalocco/jFSMRouter@2.0.0/jFSMRouter.min.js';
12
+ ```
13
+
14
+ ## 2. States
15
+
16
+ States represent the logical steps of your application.
17
+
18
+ ### 2.1 Add a State
19
+
20
+ ```js
21
+ router.stateAdd('home'); // Adds the "home" state
22
+ ```
23
+
24
+ - Returns `true` if the state is successfully created.
25
+ - The first state added becomes the initial current state.
26
+
27
+ ### 2.2 Remove a State
28
+
29
+ ```js
30
+ router.stateDel('home'); // Removes the "home" state
31
+ ```
32
+
33
+ ### 2.3 Handle Entry and Exit Hooks
34
+
35
+ - **OnEnter**: functions called when entering a state.
36
+ - **OnLeave**: functions called when leaving a state.
37
+
38
+ ```js
39
+ function onEnter(prev, next) { console.log(`Entering ${next}`); }
40
+ function onLeave(curr, next) { console.log(`Leaving ${curr}`); }
41
+
42
+ router.stateOnEnterAdd('home', onEnter);
43
+ router.stateOnLeaveAdd('home', onLeave);
44
+ ```
45
+
46
+ To remove hooks:
47
+
48
+ ```js
49
+ router.stateOnEnterDel('home', onEnter);
50
+ router.stateOnLeaveDel('home', onLeave);
51
+ ```
52
+
53
+ ## 3. Transitions
54
+
55
+ Transitions define permissions and hooks between two states.
56
+
57
+ ### 3.1 Add a Transition
58
+
59
+ ```js
60
+ router.transitionAdd('home', 'about');
61
+ ```
62
+
63
+ ### 3.2 Remove a Transition
64
+
65
+ ```js
66
+ router.transitionDel('home', 'about');
67
+ ```
68
+
69
+ ### 3.3 Transition Hooks
70
+
71
+ - **OnBefore**: called before transitioning, can block it by returning `false`.
72
+ - **OnAfter**: called after the state change.
73
+
74
+ ```js
75
+ function before() { return confirm('Go to the About page?'); }
76
+ async function after() { console.log('Transition completed'); }
77
+
78
+ router.transitionOnBeforeAdd('home', 'about', before);
79
+ router.transitionOnAfterAdd('home', 'about', after);
80
+ ```
81
+
82
+ To remove hooks:
83
+
84
+ ```js
85
+ router.transitionOnBeforeDel('home', 'about', before);
86
+ router.transitionOnAfterDel('home', 'about', after);
87
+ ```
88
+
89
+ ## 4. Hash-Based Routing
90
+
91
+ Each route is associated with a valid state.
92
+
93
+ ### 4.1 Add a Route
94
+
95
+ ```js
96
+ // path: '/user/:id[09]'
97
+ router.routeAdd(
98
+ 'home', // required state
99
+ '/user/:id[09]', // path with variables
100
+ (pathDef, actual, vars) => { console.log(vars.id); },
101
+ () => true, // optional availability function
102
+ (pathDef, actual, vars) => { console.warn('Access denied'); } // 403
103
+ );
104
+ ```
105
+
106
+ - **path** may include variables like `:name[AZ09]`, `:num[09]`, `:str[AZ]`.
107
+ - **routeFunction**: callback called if all checks pass.
108
+ - **available**: sync or async function to allow/deny the route.
109
+ - **routeFunction403**: callback called in case of access denial (403).
110
+
111
+ ### 4.2 Remove a Route
112
+
113
+ ```js
114
+ router.routeDel('/user/:id[09]');
115
+ ```
116
+
117
+ ### 4.3 Special Routes
118
+
119
+ ```js
120
+ router.routeSpecialAdd(404, () => { /* page not found */ });
121
+ router.routeSpecialAdd(403, () => { /* access denied */ });
122
+ router.routeSpecialAdd(500, () => { /* internal error */ });
123
+ ```
124
+
125
+ ### 4.4 Manual trigger
126
+
127
+ To force navigation:
128
+
129
+ ```js
130
+ router.trigger('user/123'); // sets the hash and triggers routing
131
+ ```
132
+
133
+ ## 5. Internal Mechanism
134
+
135
+ - The `hashchange` listener calls `checkHash()`.
136
+ - More specific paths (higher weight) take priority.
137
+ - FSM handles the proper hook sequence: OnBefore → OnLeave → OnAfter → OnEnter.
138
+
139
+ ## 6. Complete Example
140
+
141
+ ```html
142
+ <!DOCTYPE html>
143
+ <html>
144
+ <head><title>jFSMRouter Demo</title></head>
145
+ <body>
146
+ <script type="module">
147
+ import router from 'https://example.org/jFSMRouter.js';
148
+
149
+ // Define states
150
+ router.stateAdd('home');
151
+ router.stateAdd('user');
152
+
153
+ // State hooks
154
+ router.stateOnEnterAdd('home', () => console.log('Entered Home'));
155
+ router.stateOnEnterAdd('user', (_, prev) => console.log(`User ${prev}→user`));
156
+
157
+ // Transitions
158
+ router.transitionAdd('home', 'user');
159
+
160
+ // Routing
161
+ router.routeSpecialAdd(404, () => document.body.innerHTML = '<h1>404 Not Found</h1>');
162
+ router.routeAdd(
163
+ 'home', '/home', () => alert('Welcome!')
164
+ );
165
+ router.routeAdd(
166
+ 'user', '/user/:id[09]', (pd, act, { id }) =>
167
+ document.body.innerHTML = `<h1>User ${id}</h1>`
168
+ );
169
+
170
+ // Initial startup (if hash already present)
171
+ router.checkHash();
172
+ </script>
173
+ </body>
174
+ </html>
175
+ ```
176
+
177
+ ---
178
+
179
+ **Notes**:
180
+ - Uses ES Module syntax for import.
181
+ - Hook handling supports both sync and async functions.
182
+ - Avoid duplicate variable IDs in a single path (throws `Duplicate path id` exception).
package/build.mjs ADDED
@@ -0,0 +1,102 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { minify } from 'terser';
5
+ import ts from 'typescript';
6
+
7
+ const __dirname = path.dirname( fileURLToPath( import.meta.url ) );
8
+
9
+ // ── Utilities ─────────────────────────────────────────────────────────────────
10
+
11
+ function compileTsc( configPath ) {
12
+ const absConfig = path.resolve( __dirname, configPath );
13
+ const configFile = ts.readConfigFile( absConfig, ts.sys.readFile );
14
+ if( configFile.error ) {
15
+ throw new Error( ts.formatDiagnosticsWithColorAndContext( [ configFile.error ], {
16
+ getCurrentDirectory: ts.sys.getCurrentDirectory,
17
+ getCanonicalFileName: f => f,
18
+ getNewLine: () => '\n'
19
+ } ) );
20
+ }
21
+ const parsed = ts.parseJsonConfigFileContent(
22
+ configFile.config,
23
+ ts.sys,
24
+ path.dirname( absConfig )
25
+ );
26
+ const program = ts.createProgram( parsed.fileNames, parsed.options );
27
+ const emitResult = program.emit();
28
+ const diagnostics = ts.getPreEmitDiagnostics( program ).concat( emitResult.diagnostics );
29
+ if( 0 < diagnostics.length ) {
30
+ const message = ts.formatDiagnosticsWithColorAndContext( diagnostics, {
31
+ getCurrentDirectory: ts.sys.getCurrentDirectory,
32
+ getCanonicalFileName: f => f,
33
+ getNewLine: () => '\n'
34
+ } );
35
+ throw new Error( message );
36
+ }
37
+ }
38
+
39
+ // ── Library ───────────────────────────────────────────────────────────────────
40
+
41
+ async function buildLibrary() {
42
+ console.log( 'Compiling TypeScript...' );
43
+ compileTsc( 'tsconfig.json' );
44
+ console.log( 'Minifying with terser...' );
45
+ const code = await fs.readFile( 'jFSMRouter.js', 'utf8' );
46
+ const result = await minify( code, {
47
+ module: true,
48
+ toplevel: true,
49
+ compress: true,
50
+ mangle: {
51
+ properties: {
52
+ regex: /^_/
53
+ }
54
+ }
55
+ } );
56
+ if( undefined === result.code ) {
57
+ throw new Error( 'Terser did not produce output.' );
58
+ }
59
+ await fs.writeFile( 'jFSMRouter.min.js', result.code );
60
+ console.log( '✓ Library built.' );
61
+ }
62
+
63
+ // ── Tests ─────────────────────────────────────────────────────────────────────
64
+
65
+ async function buildTests() {
66
+ console.log( 'Compiling tests...' );
67
+ compileTsc( 'tsconfig.tests.json' );
68
+ console.log( '✓ Tests compiled.' );
69
+ }
70
+
71
+ // ── Dispatch ──────────────────────────────────────────────────────────────────
72
+
73
+ const targets = {
74
+ library: buildLibrary,
75
+ tests: buildTests
76
+ };
77
+
78
+ const args = process.argv.slice( 2 );
79
+
80
+ if( 0 === args.length ) {
81
+ console.log( 'Usage: node build.mjs <target> [<target> ...]' );
82
+ console.log( 'Available targets: ' + Object.keys( targets ).join( ', ' ) );
83
+ process.exit( 0 );
84
+ }
85
+
86
+ const unknown = args.filter( a => !( a in targets ) );
87
+ if( 0 < unknown.length ) {
88
+ console.error( 'Unknown target(s): ' + unknown.join( ', ' ) );
89
+ process.exit( 1 );
90
+ }
91
+
92
+ async function main() {
93
+ const cL1 = args.length;
94
+ for( let iL1 = 0; iL1 < cL1; iL1++ ) {
95
+ await targets[ args[ iL1 ] ]();
96
+ }
97
+ }
98
+
99
+ main().catch( err => {
100
+ console.error( err );
101
+ process.exit( 1 );
102
+ } );
@@ -0,0 +1,53 @@
1
+ type Undefinedable<T> = T | undefined;
2
+ export type FunctionOnEnter = (currentState: string, nextState: string) => (void | Promise<void>);
3
+ export type FunctionOnLeave = (currentState: string, prevState: string) => (void | Promise<void>);
4
+ export type FunctionOnTransitionAfter = () => (void | Promise<void>);
5
+ export type FunctionOnTransitionBefore = () => (any | Promise<any>);
6
+ export type CheckAvailability = (path: string, hashPath: string, params?: {
7
+ [key: string]: string;
8
+ }) => (boolean | Promise<boolean>);
9
+ export type RouteFunction = (path: string, hashPath: string, params?: {
10
+ [key: string]: string;
11
+ }) => (void | Promise<void>);
12
+ declare class jFSMRouter {
13
+ private static _instance;
14
+ static get instance(): jFSMRouter;
15
+ private _regexDuplicatePathId;
16
+ private _regexSearchVariables;
17
+ private _routes;
18
+ private _routeFunction403;
19
+ private _routeFunction404;
20
+ private _routeFunction500;
21
+ private _routing;
22
+ private _queue;
23
+ private _inTransition;
24
+ private _currentState;
25
+ private _states;
26
+ private _transitions;
27
+ private _window;
28
+ private constructor();
29
+ private static _CheckRouteEquivalence;
30
+ stateAdd(state: string): boolean;
31
+ stateDel(state: string): boolean;
32
+ stateOnEnterAdd(state: string, func: FunctionOnEnter): boolean;
33
+ stateOnEnterDel(state: string, func: FunctionOnEnter): boolean;
34
+ stateOnLeaveAdd(state: string, func: FunctionOnLeave): boolean;
35
+ stateOnLeaveDel(state: string, func: FunctionOnLeave): boolean;
36
+ transitionAdd(from: string, to: string): boolean;
37
+ transitionDel(from: string, to: string): boolean;
38
+ transitionOnBeforeAdd(from: string, to: string, func: FunctionOnTransitionBefore): boolean;
39
+ transitionOnBeforeDel(from: string, to: string, func: FunctionOnTransitionBefore): boolean;
40
+ transitionOnAfterAdd(from: string, to: string, func: FunctionOnTransitionAfter): boolean;
41
+ transitionOnAfterDel(from: string, to: string, func: FunctionOnTransitionAfter): boolean;
42
+ get state(): Undefinedable<string>;
43
+ stateSet(nextState: string): Promise<boolean>;
44
+ checkTransition(nextState: string): boolean;
45
+ routeSpecialAdd(code: number, routeFunction: RouteFunction): boolean;
46
+ routeAdd(validState: string, path: string, routeFunction: RouteFunction, available?: CheckAvailability, routeFunction403?: RouteFunction): boolean;
47
+ routeDel(path: string): boolean;
48
+ trigger(path: string): void;
49
+ route(path: string): Promise<void>;
50
+ checkHash(): Promise<void>;
51
+ }
52
+ declare const _default: jFSMRouter;
53
+ export default _default;