From ed344ce8cffb5a9dabdc6021c781d9c6bb01a700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramirez=20Vargas=2C=20Jos=C3=A9=20Pablo?= Date: Wed, 10 Sep 2025 21:24:12 -0600 Subject: [PATCH 1/3] feat: Add the initFull() function Fixes #63. --- package.json | 2 +- src/lib/core/Logger.test.ts | 2 +- src/lib/core/options.test.ts | 88 +++------------ src/lib/core/options.ts | 51 +-------- src/lib/core/trace.svelte.ts | 14 +-- src/lib/core/trace.test.ts | 25 ++--- src/lib/index.test.ts | 205 +---------------------------------- src/lib/index.ts | 58 +--------- src/lib/init.test.ts | 166 ++++++++++++++++++++++++++++ src/lib/init.ts | 53 +++++++++ src/lib/types.ts | 82 +++++++++++++- src/testing/test-utils.ts | 3 +- 12 files changed, 331 insertions(+), 418 deletions(-) create mode 100644 src/lib/init.test.ts create mode 100644 src/lib/init.ts diff --git a/package.json b/package.json index 0e2118e..d5d10a2 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/WJSoftware/wjfe-n-savant" + "url": "git+https://github.com/WJSoftware/wjfe-n-savant.git" }, "license": "MIT", "bugs": { diff --git a/src/lib/core/Logger.test.ts b/src/lib/core/Logger.test.ts index caa13a8..0d7d3a4 100644 --- a/src/lib/core/Logger.test.ts +++ b/src/lib/core/Logger.test.ts @@ -3,7 +3,7 @@ import { logger, resetLogger, setLogger } from "./Logger.js"; import type { ILogger } from "$lib/types.js"; describe("logger", () => { - test("Should default to offLogger (not console)", () => { + test("Should default to offLogger when the library hasn't been initialized.", () => { expect(logger).not.toBe(globalThis.console); expect(logger.debug).toBeDefined(); expect(logger.log).toBeDefined(); diff --git a/src/lib/core/options.test.ts b/src/lib/core/options.test.ts index 04551f0..74656f1 100644 --- a/src/lib/core/options.test.ts +++ b/src/lib/core/options.test.ts @@ -4,11 +4,7 @@ import { resetRoutingOptions, routingOptions } from "./options.js"; describe("options", () => { test("The routing options' initial values are the expected ones.", () => { // Assert. - expect(routingOptions).toEqual({ full: false, hashMode: 'single', implicitMode: 'path' }); - }); - - test("Should have correct default value for full option.", () => { - expect(routingOptions.full).toBe(false); + expect(routingOptions).toEqual({ hashMode: 'single', implicitMode: 'path' }); }); test("Should have correct default value for hashMode option.", () => { @@ -19,20 +15,11 @@ describe("options", () => { expect(routingOptions.implicitMode).toBe('path'); }); - test("Should allow modification of full option.", () => { - const originalValue = routingOptions.full; - routingOptions.full = true; - expect(routingOptions.full).toBe(true); - - // Restore original value - routingOptions.full = originalValue; - }); - test("Should allow modification of hashMode option.", () => { const originalValue = routingOptions.hashMode; routingOptions.hashMode = 'multi'; expect(routingOptions.hashMode).toBe('multi'); - + // Restore original value routingOptions.hashMode = originalValue; }); @@ -41,77 +28,30 @@ describe("options", () => { const originalValue = routingOptions.implicitMode; routingOptions.implicitMode = 'hash'; expect(routingOptions.implicitMode).toBe('hash'); - - // Restore original value - routingOptions.implicitMode = originalValue; - }); - test("Should maintain object reference integrity after modifications.", () => { - const optionsRef = routingOptions; - routingOptions.full = !routingOptions.full; - - expect(optionsRef).toBe(routingOptions); - expect(optionsRef.full).toBe(routingOptions.full); - // Restore original value - routingOptions.full = false; + routingOptions.implicitMode = originalValue; }); test("Should contain all required properties as non-nullable.", () => { - expect(routingOptions.full).toBeDefined(); expect(routingOptions.hashMode).toBeDefined(); expect(routingOptions.implicitMode).toBeDefined(); - - expect(typeof routingOptions.full).toBe('boolean'); expect(typeof routingOptions.hashMode).toBe('string'); expect(typeof routingOptions.implicitMode).toBe('string'); }); - test("Should validate hashMode enum values.", () => { - const originalValue = routingOptions.hashMode; - - // Valid values - routingOptions.hashMode = 'single'; - expect(routingOptions.hashMode).toBe('single'); - - routingOptions.hashMode = 'multi'; - expect(routingOptions.hashMode).toBe('multi'); - - // Restore original value - routingOptions.hashMode = originalValue; - }); + describe('resetRoutingOptions', () => { + test("Should reset all options to defaults when resetRoutingOptions is called.", () => { + // Arrange. + const original = structuredClone(routingOptions); + routingOptions.hashMode = 'multi'; + routingOptions.implicitMode = 'hash'; - test("Should validate implicitMode enum values.", () => { - const originalValue = routingOptions.implicitMode; - - // Valid values - routingOptions.implicitMode = 'hash'; - expect(routingOptions.implicitMode).toBe('hash'); - - routingOptions.implicitMode = 'path'; - expect(routingOptions.implicitMode).toBe('path'); - - // Restore original value - routingOptions.implicitMode = originalValue; - }); - - test("Should reset all options to defaults when resetRoutingOptions is called.", () => { - // Arrange - Modify all options to non-default values - routingOptions.full = true; - routingOptions.hashMode = 'multi'; - routingOptions.implicitMode = 'hash'; - - // Verify they were changed - expect(routingOptions.full).toBe(true); - expect(routingOptions.hashMode).toBe('multi'); - expect(routingOptions.implicitMode).toBe('hash'); - - // Act - resetRoutingOptions(); + // Act. + resetRoutingOptions(); - // Assert - Verify all options are back to defaults - expect(routingOptions.full).toBe(false); - expect(routingOptions.hashMode).toBe('single'); - expect(routingOptions.implicitMode).toBe('path'); + // Assert. + expect(routingOptions).deep.equal(original); + }); }); }); diff --git a/src/lib/core/options.ts b/src/lib/core/options.ts index d1680d5..7bffe47 100644 --- a/src/lib/core/options.ts +++ b/src/lib/core/options.ts @@ -1,58 +1,11 @@ -/** - * Library's routing options. - */ -export type RoutingOptions = { - /** - * Whether to initialize the routing library with all features. - * @default false - */ - full?: boolean; - /** - * Whether to use a single or multiple hash mode. In single hash mode, the hash value is always one path; in multi - * mode, the hash value can be multiple paths. - * - * The multiple paths option shapes the hash value as: `'#id1=/path/of/id1;id2=/path/of/id2;...'`. - * - * @default 'single' - */ - hashMode?: 'single' | 'multi'; - /** - * Mode routers operate when their `hash` property is not set (left `undefined`). - * - * In short: It tells the library what type of routing is assumed when no `hash` property is specified in `Router`, - * `Route`, `Fallback`, `Link`, or `RouterTrace` components. - * - * When set to `'path'`, create components for hash routing by setting the `hash` property to `true` or a string - * identifier; when set to `'hash'`, create components for path routing by setting the `hash` property to `false`. - * - * @default 'path' - * - * @example - * ```svelte - * // In main.ts: - * init({ implicitMode: 'hash' }); - * - * // In App.svelte: - * - * - * - * - * - * ``` - * - * Even though the `hash` property is not set in the `Router` or `Route` components, the library will treat both - * as hash-routing components because the `implicitMode` option was set to `'hash'`. - */ - implicitMode?: 'hash' | 'path'; -} +import type { RoutingOptions } from "$lib/types.js"; /** * Default routing options used for rollback. */ const defaultRoutingOptions: Required = { - full: false, hashMode: 'single', - implicitMode: 'path' + implicitMode: 'path', }; /** diff --git a/src/lib/core/trace.svelte.ts b/src/lib/core/trace.svelte.ts index dd8b791..adc171e 100644 --- a/src/lib/core/trace.svelte.ts +++ b/src/lib/core/trace.svelte.ts @@ -1,18 +1,6 @@ +import type { TraceOptions } from "$lib/types.js"; import type { RouterEngine } from "./RouterEngine.svelte.js"; -/** - * Library's tracing options. - */ -export type TraceOptions = { - /** - * Whether to trace the router hierarchy. - * - * This consumes extra RAM and a bit more CPU cycles. Disable it on production builds. - * @default false - */ - routerHierarchy?: boolean; -}; - /** * Weak references to all router engines that are created. */ diff --git a/src/lib/core/trace.test.ts b/src/lib/core/trace.test.ts index 100b67b..a0c37f0 100644 --- a/src/lib/core/trace.test.ts +++ b/src/lib/core/trace.test.ts @@ -1,21 +1,16 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; -import { getAllChildRouters, resetTraceOptions, setTraceOptions, traceOptions } from "./trace.svelte.js"; +import { getAllChildRouters, registerRouter, resetTraceOptions, setTraceOptions, traceOptions } from "./trace.svelte.js"; import { RouterEngine } from "./RouterEngine.svelte.js"; import { init } from "$lib/index.js"; -const hoistedVars = vi.hoisted(() => ({ - registerRouter: vi.fn(), -})); -vi.mock('./trace.svelte.js', async (originalImport) => { - const origModule = await originalImport() as any; +vi.mock(import('./trace.svelte.js'), async (importActual) => { + const actual = await importActual(); return { - ...origModule, - registerRouter: (...args: any[]) => { - hoistedVars.registerRouter.apply(null, args); - return origModule.registerRouter.apply(null, args); - } - } + ...actual, + registerRouter: vi.fn(actual.registerRouter) + }; }); + describe('setTraceOptions', () => { test.each([ true, @@ -64,14 +59,14 @@ describe('registerRouter', () => { cleanup(); }); beforeEach(() => { - hoistedVars.registerRouter.mockClear(); + vi.resetAllMocks(); }); test("Should be called when a new router engine is created.", () => { // Act. new RouterEngine(); // Assert. - expect(hoistedVars.registerRouter).toHaveBeenCalled(); + expect(registerRouter).toHaveBeenCalled(); }); }); @@ -84,7 +79,7 @@ describe('getAllChildRouters', () => { cleanup(); }); beforeEach(() => { - hoistedVars.registerRouter.mockClear(); + vi.clearAllMocks(); }); test("Should return the children of the specified router.", () => { // Arrange. diff --git a/src/lib/index.test.ts b/src/lib/index.test.ts index 1f0567e..b39f89e 100644 --- a/src/lib/index.test.ts +++ b/src/lib/index.test.ts @@ -1,16 +1,8 @@ -import { describe, expect, test, beforeEach, afterEach } from "vitest"; -import { logger } from "./core/Logger.js"; +import { describe, expect, test } from "vitest"; import { routingOptions } from "./core/options.js"; -import { traceOptions } from "./core/trace.svelte.js"; import { location } from "./core/Location.js"; describe('index', () => { - let cleanup: (() => void) | null = null; - - afterEach(() => { - cleanup?.(); - }); - test("Should export exactly the expected objects.", async () => { // Arrange. const expectedList = [ @@ -22,6 +14,7 @@ describe('index', () => { 'location', 'RouterTrace', 'init', + 'initFull', 'getRouterContext', 'setRouterContext', ]; @@ -38,208 +31,14 @@ describe('index', () => { } }); - test("Should use offLogger as default uninitialized logger.", () => { - // Assert. - expect(logger.debug).toBeDefined(); - expect(logger.log).toBeDefined(); - expect(logger.warn).toBeDefined(); - expect(logger.error).toBeDefined(); - - // The default logger should be offLogger (no-op functions) - // We can't test the exact identity, but we can test that it's not the console - expect(logger).not.toBe(globalThis.console); - }); - test("Should have default routing options in uninitialized state.", () => { // Assert. - expect(routingOptions.full).toBe(false); expect(routingOptions.hashMode).toBe('single'); expect(routingOptions.implicitMode).toBe('path'); }); - test("Should have default trace options in uninitialized state.", () => { - // Assert. - expect(traceOptions.routerHierarchy).toBe(false); - }); - test("Should have no location in uninitialized state.", () => { // Assert. expect(location).toBeUndefined(); }); - - test("Should initialize with console logger when logger option is true (default).", async () => { - // Arrange. - const { init } = await import('./index.js'); - - // Act. - cleanup = init(); - - // Assert. - expect(logger).toBe(globalThis.console); - }); - - test("Should initialize with custom options and rollback properly.", async () => { - // Arrange. - const { init } = await import('./index.js'); - const customLogger = { - debug: () => {}, - log: () => {}, - warn: () => {}, - error: () => {} - }; - - // Capture initial state - const initialLoggerIsOffLogger = logger !== globalThis.console; - const initialRoutingOptions = { - full: routingOptions.full, - hashMode: routingOptions.hashMode, - implicitMode: routingOptions.implicitMode - }; - const initialTraceOptions = { - routerHierarchy: traceOptions.routerHierarchy - }; - - // Act - Initialize with custom options - cleanup = init({ - full: true, - hashMode: 'multi', - implicitMode: 'hash', - logger: customLogger, - trace: { - routerHierarchy: true - } - }); - - // Assert - Check that options were applied - expect(logger).toBe(customLogger); - expect(routingOptions.full).toBe(true); - expect(routingOptions.hashMode).toBe('multi'); - expect(routingOptions.implicitMode).toBe('hash'); - expect(traceOptions.routerHierarchy).toBe(true); - expect(location).toBeDefined(); - - // Act - Cleanup - cleanup(); - cleanup = null; - - // Assert - Check that everything was rolled back - expect(logger !== globalThis.console).toBe(initialLoggerIsOffLogger); // Back to offLogger - expect(routingOptions.full).toBe(initialRoutingOptions.full); - expect(routingOptions.hashMode).toBe(initialRoutingOptions.hashMode); - expect(routingOptions.implicitMode).toBe(initialRoutingOptions.implicitMode); - expect(traceOptions.routerHierarchy).toBe(initialTraceOptions.routerHierarchy); - expect(location).toBeNull(); - }); - - test("Should rollback routing options to defaults.", async () => { - // Arrange. - const { init } = await import('./index.js'); - - // Act - Initialize with non-default options - cleanup = init({ - full: true, - hashMode: 'multi', - implicitMode: 'hash' - }); - - // Verify options were applied - expect(routingOptions.full).toBe(true); - expect(routingOptions.hashMode).toBe('multi'); - expect(routingOptions.implicitMode).toBe('hash'); - - // Act - Cleanup - cleanup(); - cleanup = null; - - // Assert - Check that routing options were reset to defaults - expect(routingOptions.full).toBe(false); - expect(routingOptions.hashMode).toBe('single'); - expect(routingOptions.implicitMode).toBe('path'); - }); - - test("Should rollback logger to offLogger.", async () => { - // Arrange. - const { init } = await import('./index.js'); - - // Act - Initialize with console logger (default) - cleanup = init(); - - // Verify logger was set to console - expect(logger).toBe(globalThis.console); - - // Act - Cleanup - cleanup(); - cleanup = null; - - // Assert - Check that logger was reset to offLogger - expect(logger).not.toBe(globalThis.console); - expect(logger.debug).toBeDefined(); - expect(logger.log).toBeDefined(); - expect(logger.warn).toBeDefined(); - expect(logger.error).toBeDefined(); - }); - - test("Should rollback trace options to defaults.", async () => { - // Arrange. - const { init } = await import('./index.js'); - - // Act - Initialize with non-default trace options - cleanup = init({ - trace: { - routerHierarchy: true - } - }); - - // Verify trace options were applied - expect(traceOptions.routerHierarchy).toBe(true); - - // Act - Cleanup - cleanup(); - cleanup = null; - - // Assert - Check that trace options were reset to defaults - expect(traceOptions.routerHierarchy).toBe(false); - }); - - test("Should handle multiple init/cleanup cycles properly.", async () => { - // Arrange. - const { init } = await import('./index.js'); - - // Act & Assert - First cycle - cleanup = init({ full: true }); - expect(routingOptions.full).toBe(true); - expect(logger).toBe(globalThis.console); - expect(location).toBeDefined(); - - cleanup(); - cleanup = null; - expect(routingOptions.full).toBe(false); - expect(logger).not.toBe(globalThis.console); - expect(location).toBeNull(); - - // Act & Assert - Second cycle - cleanup = init({ hashMode: 'multi' }); - expect(routingOptions.hashMode).toBe('multi'); - expect(logger).toBe(globalThis.console); - expect(location).toBeDefined(); - - cleanup(); - cleanup = null; - expect(routingOptions.hashMode).toBe('single'); - expect(logger).not.toBe(globalThis.console); - expect(location).toBeNull(); - }); - - test("Should handle cleanup without previous initialization gracefully.", async () => { - // Arrange. - const { init } = await import('./index.js'); - cleanup = init(); - - // Act - Call cleanup function - const cleanupFn = cleanup; - cleanup = null; - - // Should not throw when calling cleanup - expect(() => cleanupFn()).not.toThrow(); - }); }); diff --git a/src/lib/index.ts b/src/lib/index.ts index 417e119..f52d824 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,60 +1,4 @@ -import { setLocation } from "./core/Location.js"; -import { LocationFull } from "./core/LocationFull.js"; -import { LocationLite } from "./core/LocationLite.svelte.js"; -import { resetLogger, setLogger } from "./core/Logger.js"; -import { resetRoutingOptions, routingOptions, type RoutingOptions } from "./core/options.js"; -import { resetTraceOptions, setTraceOptions, type TraceOptions } from "./core/trace.svelte.js"; -import type { ILogger } from "./types.js"; - -/** - * Library's initialization options. - */ -export type InitOptions = RoutingOptions & { - /** - * Tracing options that generally should be off for production builds. - */ - trace?: TraceOptions; - /** - * Controls logging. If `true`, the default logger that logs to the console is used. If `false`, logging is - * turned off. If an object is provided, it is used as the logger. - * - * Logging is turned on by default. - * - * **TIP**: You can provide your own logger implementation to integrate with your application's logging system. - */ - logger?: boolean | ILogger; -} - -/** - * Initializes the routing library. In normal mode, only the following features are available: - * - * - URL and state tracking - * - Navigation - * - Event handling of the `popstate` event - * - Routers - * - Routes - * - Links - * - * Use `options.full` to enable the following features: - * - Raising the `beforeNavigate` and `navigationCancelled` events - * - Intercepting navigation from other libraries or routers - */ -export function init(options?: InitOptions): () => void { - setTraceOptions(options?.trace); - setLogger(options?.logger ?? true); - routingOptions.full = options?.full ?? routingOptions.full; - routingOptions.hashMode = options?.hashMode ?? routingOptions.hashMode; - routingOptions.implicitMode = options?.implicitMode ?? routingOptions.implicitMode; - const newLocation = setLocation(options?.full ? new LocationFull() : new LocationLite()); - return () => { - newLocation?.dispose(); - setLocation(null); - resetRoutingOptions(); - resetLogger(); - resetTraceOptions(); - }; -} - +export * from "$lib/init.js"; export * from "$lib/Link/Link.svelte"; export { default as Link } from "$lib/Link/Link.svelte"; export { default as LinkContext } from "$lib/LinkContext/LinkContext.svelte"; diff --git a/src/lib/init.test.ts b/src/lib/init.test.ts new file mode 100644 index 0000000..0db49a6 --- /dev/null +++ b/src/lib/init.test.ts @@ -0,0 +1,166 @@ +import { describe, test, expect, afterEach } from "vitest"; +import { init, initFull } from "./init.js"; +import { logger } from "./core/Logger.js"; +import { routingOptions } from "./core/options.js"; +import { traceOptions } from "./core/trace.svelte.js"; +import { location } from "$lib/core/Location.js"; +import { LocationLite } from "./core/LocationLite.svelte.js"; +import { LocationFull } from "./core/LocationFull.js"; + +let cleanup: (() => void) | undefined; + +[ + { + fn: init, + locationClass: LocationLite + }, + { + fn: initFull, + locationClass: LocationFull + } +].forEach((fnInfo) => { + describe(fnInfo.fn.name, () => { + afterEach(() => { + cleanup?.(); + }); + + test(`Should initialize the global location object to an instance of the ${fnInfo.locationClass.name} class.`, () => { + // Act. + cleanup = fnInfo.fn(); + + // Assert. + expect(location).toBeDefined(); + expect(location).toBeInstanceOf(fnInfo.locationClass); + }); + test("Should initialize with console logger when logger option is true (default).", async () => { + // Act. + cleanup = fnInfo.fn(); + + // Assert. + expect(logger).toBe(globalThis.console); + }); + test("Should initialize with custom options and rollback properly.", async () => { + // Arrange. + const customLogger = { + debug: () => { }, + log: () => { }, + warn: () => { }, + error: () => { } + }; + + // Capture initial state + const initialLoggerIsOffLogger = logger !== globalThis.console; + const initialRoutingOptions = { + hashMode: routingOptions.hashMode, + implicitMode: routingOptions.implicitMode + }; + const initialTraceOptions = { + routerHierarchy: traceOptions.routerHierarchy + }; + + // Act - Initialize with custom options + cleanup = fnInfo.fn({ + hashMode: 'multi', + implicitMode: 'hash', + logger: customLogger, + trace: { + routerHierarchy: true + } + }); + + // Assert - Check that options were applied + expect(logger).toBe(customLogger); + expect(routingOptions.hashMode).toBe('multi'); + expect(routingOptions.implicitMode).toBe('hash'); + expect(traceOptions.routerHierarchy).toBe(true); + expect(location).toBeDefined(); + + // Act - Cleanup + cleanup(); + cleanup = undefined; + + // Assert - Check that everything was rolled back + expect(logger !== globalThis.console).toBe(initialLoggerIsOffLogger); // Back to offLogger + expect(routingOptions.hashMode).toBe(initialRoutingOptions.hashMode); + expect(routingOptions.implicitMode).toBe(initialRoutingOptions.implicitMode); + expect(traceOptions.routerHierarchy).toBe(initialTraceOptions.routerHierarchy); + expect(location).toBeNull(); + }); + test("Should rollback routing options to defaults.", async () => { + // Arrange. + cleanup = fnInfo.fn({ + hashMode: 'multi', + implicitMode: 'hash' + }); + + // Verify options were applied + expect(routingOptions.hashMode).toBe('multi'); + expect(routingOptions.implicitMode).toBe('hash'); + + // Act - Cleanup + cleanup(); + cleanup = undefined; + + // Assert - Check that routing options were reset to defaults + expect(routingOptions.hashMode).toBe('single'); + expect(routingOptions.implicitMode).toBe('path'); + }); + describe('cleanup', () => { + test("Should rollback logger to its uninitialized value.", async () => { + // Arrange. + const uninitializedLogger = logger; + cleanup = fnInfo.fn({ + logger: { debug: () => { }, log: () => { }, warn: () => { }, error: () => { } } + }); + expect(logger).not.toBe(uninitializedLogger); + + // Act. + cleanup(); + cleanup = undefined; + + // Assert. + expect(logger).toBe(uninitializedLogger); + }); + test("Should rollback trace options to defaults.", async () => { + // Arrange. + cleanup = fnInfo.fn({ + trace: { + routerHierarchy: true + } + }); + expect(traceOptions.routerHierarchy).toBe(true); + + // Act. + cleanup(); + cleanup = undefined; + + // Assert. + expect(traceOptions.routerHierarchy).toBe(false); + }); + test("Should handle multiple init/cleanup cycles properly.", async () => { + // Arrange. + const uninitializedLogger = logger; + cleanup = fnInfo.fn({ + logger: { debug: () => { }, log: () => { }, warn: () => { }, error: () => { } } + }); + expect(logger).not.toBe(uninitializedLogger); + expect(location).toBeDefined(); + cleanup(); + cleanup = undefined; + expect(logger).toBe(uninitializedLogger); + expect(location).toBeNull(); + cleanup = fnInfo.fn({ hashMode: 'multi' }); + expect(routingOptions.hashMode).toBe('multi'); + expect(location).toBeDefined(); + + // Act. + cleanup(); + cleanup = undefined; + + // Assert. + expect(routingOptions.hashMode).toBe('single'); + expect(location).toBeNull(); + }); + }); + }); +}); diff --git a/src/lib/init.ts b/src/lib/init.ts new file mode 100644 index 0000000..18d7cb2 --- /dev/null +++ b/src/lib/init.ts @@ -0,0 +1,53 @@ +import { setLocation } from "./core/Location.js"; +import { LocationFull } from "./core/LocationFull.js"; +import { LocationLite } from "./core/LocationLite.svelte.js"; +import { resetLogger, setLogger } from "./core/Logger.js"; +import { resetRoutingOptions, routingOptions } from "./core/options.js"; +import { resetTraceOptions, setTraceOptions } from "./core/trace.svelte.js"; +import type { InitOptions, Location } from "./types.js"; + +function initInternal(options: InitOptions, location: Location) { + setTraceOptions(options?.trace); + setLogger(options?.logger ?? true); + routingOptions.hashMode = options?.hashMode ?? routingOptions.hashMode; + routingOptions.implicitMode = options?.implicitMode ?? routingOptions.implicitMode; + const newLocation = setLocation(location); + return () => { + newLocation?.dispose(); + setLocation(null); + resetRoutingOptions(); + resetLogger(); + resetTraceOptions(); + }; +} + +/** + * Initializes the routing library in normal mode. The following features are available: + * + * - URL and state tracking + * - Navigation + * - Event handling of the `popstate` and `hashchange` events + * - Routers + * - Routes + * - Links + * - Fallbacks + * - Link contexts + * + * Use `initFull()` to enable the following features: + * - Raising the `beforeNavigate` and `navigationCancelled` events + * - Intercepting navigation from other libraries or routers + * + * @returns A cleanup function that reverts the initialization process. + */ +export function init(options?: InitOptions) { + return initInternal(options ?? {}, new LocationLite()); +} + +/** + * Initializes the routing library in normal mode. The following features are available: + * + * @returns A cleanup function that reverts the initialization process. + */ +export function initFull(options?: InitOptions) { + return initInternal(options ?? {}, new LocationFull()); +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 2fbbf1a..f1560eb 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -210,7 +210,7 @@ export interface Location { * This event has the ability to cancel navigation by calling the `cancel` method on the event object. * * **IMPORTANT:** This is a feature only available when initializing the routing library with the - * {@link InitOptions.full} option. + * {@link initFull} function. * @param event The event to listen for. * @param callback The callback to invoke when the event occurs. * @returns A function that removes the event listener. @@ -221,8 +221,8 @@ export interface Location { * * This event occurs when navigation is cancelled by a handler of the `beforeNavigate` event. * - * **IMPORTANT:** This is a feature only available when initializing the routing library with the - * {@link InitOptions.full} option. + * **IMPORTANT:** This is a feature only available when initializing the routing library with the + * {@link initFull} function. * @param event The event to listen for. * @param callback The callback to invoke when the event occurs. * @returns A function that removes the event listener. @@ -343,3 +343,79 @@ export interface ILogger { */ error: (...args: any[]) => void; }; + +/** + * Library's routing options. + */ +export type RoutingOptions = { + /** + * Whether to use a single or multiple hash mode. In single hash mode, the hash value is always one path; in multi + * mode, the hash value can be multiple paths. + * + * The multiple paths option shapes the hash value as: `'#id1=/path/of/id1;id2=/path/of/id2;...'`. + * + * @default 'single' + */ + hashMode?: 'single' | 'multi'; + /** + * Mode routers operate when their `hash` property is not set (left `undefined`). + * + * In short: It tells the library what type of routing is assumed when no `hash` property is specified in `Router`, + * `Route`, `Fallback`, `Link`, or `RouterTrace` components. + * + * When set to `'path'`, create components for hash routing by setting the `hash` property to `true` or a string + * identifier; when set to `'hash'`, create components for path routing by setting the `hash` property to `false`. + * + * @default 'path' + * + * @example + * ```svelte + * // In main.ts: + * init({ implicitMode: 'hash' }); + * + * // In App.svelte: + * + * + * + * + * + * ``` + * + * Even though the `hash` property is not set in the `Router` or `Route` components, the library will treat both + * as hash-routing components because the `implicitMode` option was set to `'hash'`. + */ + implicitMode?: 'hash' | 'path'; +} + +/** + * Library's tracing options. + */ +export type TraceOptions = { + /** + * Whether to trace the router hierarchy. + * + * This consumes extra RAM and a bit more CPU cycles. Disable it on production builds. + * @default false + */ + routerHierarchy?: boolean; +}; + + +/** + * Library's initialization options. + */ +export type InitOptions = RoutingOptions & { + /** + * Tracing options that generally should be off for production builds. + */ + trace?: TraceOptions; + /** + * Controls logging. If `true`, the default logger that logs to the console is used. If `false`, logging is + * turned off. If an object is provided, it is used as the logger. + * + * Logging is turned on by default. + * + * **TIP**: You can provide your own logger implementation to integrate with your application's logging system. + */ + logger?: boolean | ILogger; +} diff --git a/src/testing/test-utils.ts b/src/testing/test-utils.ts index 94b94e0..b4ba54b 100644 --- a/src/testing/test-utils.ts +++ b/src/testing/test-utils.ts @@ -1,8 +1,7 @@ -import { type Hash, type RouteInfo } from "$lib/types.js"; +import type { Hash, RouteInfo, RoutingOptions } from "$lib/types.js"; import { RouterEngine } from "$lib/core/RouterEngine.svelte.js"; import { getRouterContextKey } from "../lib/Router/Router.svelte"; import { createRawSnippet } from "svelte"; -import type { RoutingOptions } from "$lib/core/options.js"; import { resolveHashValue } from "$lib/core/resolveHashValue.js"; import { vi } from "vitest"; From a7a95db6a4b7b33423c6933cec82038ef88a3b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramirez=20Vargas=2C=20Jos=C3=A9=20Pablo?= Date: Wed, 10 Sep 2025 21:56:00 -0600 Subject: [PATCH 2/3] tests: Add missing tests --- src/lib/init.test.ts | 66 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/src/lib/init.test.ts b/src/lib/init.test.ts index 0db49a6..d62b71f 100644 --- a/src/lib/init.test.ts +++ b/src/lib/init.test.ts @@ -86,26 +86,36 @@ let cleanup: (() => void) | undefined; expect(traceOptions.routerHierarchy).toBe(initialTraceOptions.routerHierarchy); expect(location).toBeNull(); }); - test("Should rollback routing options to defaults.", async () => { + test("Should throw an error when called a second time without proper prior cleanup.", () => { // Arrange. - cleanup = fnInfo.fn({ - hashMode: 'multi', - implicitMode: 'hash' - }); - - // Verify options were applied - expect(routingOptions.hashMode).toBe('multi'); - expect(routingOptions.implicitMode).toBe('hash'); + cleanup = fnInfo.fn(); - // Act - Cleanup - cleanup(); - cleanup = undefined; + // Act. + const act = () => fnInfo.fn(); - // Assert - Check that routing options were reset to defaults - expect(routingOptions.hashMode).toBe('single'); - expect(routingOptions.implicitMode).toBe('path'); + // Assert. + expect(act).toThrow(); }); describe('cleanup', () => { + test("Should rollback routing options to defaults.", async () => { + // Arrange. + cleanup = fnInfo.fn({ + hashMode: 'multi', + implicitMode: 'hash' + }); + + // Verify options were applied + expect(routingOptions.hashMode).toBe('multi'); + expect(routingOptions.implicitMode).toBe('hash'); + + // Act - Cleanup + cleanup(); + cleanup = undefined; + + // Assert - Check that routing options were reset to defaults + expect(routingOptions.hashMode).toBe('single'); + expect(routingOptions.implicitMode).toBe('path'); + }); test("Should rollback logger to its uninitialized value.", async () => { // Arrange. const uninitializedLogger = logger; @@ -164,3 +174,29 @@ let cleanup: (() => void) | undefined; }); }); }); + +describe('init + initFull', () => { + afterEach(() => { + cleanup?.(); + cleanup = undefined; + }); + test.each([ + { + fn1: init, + fn2: initFull, + }, + { + fn1: initFull, + fn2: init, + }, + ])("Should throw an error when calling $fn2.name without prior cleaning of a call to $fn1.name .", ({ fn1, fn2 }) => { + // Arrange. + cleanup = fn1(); + + // Act. + const act = () => fn2(); + + // Assert. + expect(act).toThrow(); + }); +}); \ No newline at end of file From feb337c5df40707d41c6fa44028581c602cdc8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pablo=20Ram=C3=ADrez=20Vargas?= Date: Wed, 10 Sep 2025 22:03:02 -0600 Subject: [PATCH 3/3] chore(jsdoc): Complete description of the initFull() function. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/init.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/init.ts b/src/lib/init.ts index 18d7cb2..708ea99 100644 --- a/src/lib/init.ts +++ b/src/lib/init.ts @@ -44,7 +44,10 @@ export function init(options?: InitOptions) { } /** - * Initializes the routing library in normal mode. The following features are available: + * Initializes the routing library in full mode. All features of normal mode are available, plus the following: + * + * - Raising the `beforeNavigate` and `navigationCancelled` events + * - Intercepting navigation from other libraries or routers * * @returns A cleanup function that reverts the initialization process. */