diff --git a/package-lock.json b/package-lock.json index f76e5a2..8332c32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tempo-monorepo", - "version": "2.2.1", + "version": "2.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tempo-monorepo", - "version": "2.2.1", + "version": "2.2.2", "workspaces": [ "packages/*" ], @@ -19,6 +19,7 @@ "@types/jquery": "^4.0.0", "@types/node": "^25.5.2", "@vitest/ui": "^2.1.8", + "cross-env": "^7.0.3", "release-it": "^17.1.1", "rollup": "^4.60.1", "tslib": "^2.8.1", @@ -3247,6 +3248,25 @@ } } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -9952,7 +9972,7 @@ }, "packages/library": { "name": "@magmacomputing/library", - "version": "2.2.1", + "version": "2.2.2", "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -9971,14 +9991,14 @@ }, "packages/tempo": { "name": "@magmacomputing/tempo", - "version": "2.2.1", + "version": "2.2.2", "license": "MIT", "dependencies": { "tslib": "^2.8.1" }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.2.1", + "@magmacomputing/library": "2.2.2", "@rollup/plugin-alias": "^6.0.0", "magic-string": "^0.30.21", "typedoc": "^0.28.19", diff --git a/package.json b/package.json index 9b7af1b..5b617ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tempo-monorepo", - "version": "2.2.1", + "version": "2.2.2", "private": true, "description": "Magma Computing Monorepo", "repository": { @@ -21,10 +21,12 @@ "core": "npm run core --workspace=@magmacomputing/tempo", "docs:dev": "npm run docs:dev --workspace=@magmacomputing/tempo", "docs:build": "npm run docs:build --workspace=@magmacomputing/tempo", - "docs:preview": "npm run docs:preview --workspace=@magmacomputing/tempo" + "docs:preview": "npm run docs:preview --workspace=@magmacomputing/tempo", + "test:dist": "npm run build:library && npm run build:tempo && cross-env TEST_DIST=true vitest run" }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", + "cross-env": "^7.0.3", "@release-it/keep-a-changelog": "^5.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@types/google.maps": "^3.58.1", diff --git a/packages/library/package.json b/packages/library/package.json index 37fcc32..6cf917d 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "2.2.1", + "version": "2.2.2", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", diff --git a/packages/library/src/common/pledge.class.ts b/packages/library/src/common/pledge.class.ts index 85061d5..7d61d1c 100644 --- a/packages/library/src/common/pledge.class.ts +++ b/packages/library/src/common/pledge.class.ts @@ -17,8 +17,8 @@ declare module '#library/type.library.js' { * Wrap a Promise's resolve/reject/finally methods for later fulfilment. * with useful methods for tracking the state of the Promise, chaining fulfilment, etc. ``` - new Pledge({tag: string, onResolve?: () => void, onReject?: () => void, onSettle?: () => void}) - new Pledge(tag?: string) + new Pledge({tag: string, onResolve?: () => void, onReject?: () => void, onSettle?: () => void}) + new Pledge(tag?: string) ``` */ @Immutable diff --git a/packages/library/src/common/reflection.library.ts b/packages/library/src/common/reflection.library.ts index a37e9cf..d62b6ab 100644 --- a/packages/library/src/common/reflection.library.ts +++ b/packages/library/src/common/reflection.library.ts @@ -84,11 +84,34 @@ export const setAccessors = (obj: any = {}) => { } const ownAccessors = (obj: any = {}, type: 'get' | 'set') => { - const accessors = Object.getOwnPropertyDescriptors(obj.prototype || Object.getPrototypeOf(obj)); + const keys: PropertyKey[] = []; + const limit = 50; + let depth = 0; + + // 1. Walk the Instance Prototype chain (for instance accessors) + let proto = obj.prototype || Object.getPrototypeOf(obj); + while (proto && proto !== Object.prototype && ++depth < limit) { + const descriptors = Object.getOwnPropertyDescriptors(proto); + Reflect.ownKeys(descriptors).forEach(key => { + if (isFunction((descriptors as any)[key][type])) + keys.push(key); + }); + proto = Object.getPrototypeOf(proto); + } + + // 2. Walk the Constructor chain (for static accessors) + let constructor = isFunction(obj) ? obj : (obj as any).constructor; + depth = 0; + while (constructor && constructor !== Function.prototype && constructor !== Object.prototype && ++depth < limit) { + const descriptors = Object.getOwnPropertyDescriptors(constructor); + Reflect.ownKeys(descriptors).forEach(key => { + if (isFunction((descriptors as any)[key][type])) + keys.push(key); + }); + constructor = Object.getPrototypeOf(constructor); + } - return ownEntries(accessors) - .filter(([_, descriptor]) => isFunction(descriptor[type])) - .map(([key, _]) => key) + return distinct(keys as string[]); } /** diff --git a/packages/library/src/common/serialize.library.ts b/packages/library/src/common/serialize.library.ts index 35fa71a..7286cd5 100644 --- a/packages/library/src/common/serialize.library.ts +++ b/packages/library/src/common/serialize.library.ts @@ -2,10 +2,10 @@ import { curry } from '#library/function.library.js'; import { ownKeys, ownValues, ownEntries } from '#library/primitive.library.js'; import { isType, asType, isEmpty, isDefined, isUndefined, isNullish, isString, isObject, isArray, isFunction, isSymbolFor, isSymbol } from '#library/type.library.js'; +import sym from '#library/symbol.library.js'; import type { Obj, Type } from '#library/type.library.js'; - -export const Registry = new Map(); +export const Registry = (globalThis as any)[sym.$SerializerRegistry] ??= new Map(); /** register a Class for serialization */ export const registerSerializable = (name: string, cls: Function) => { diff --git a/packages/library/src/common/symbol.library.ts b/packages/library/src/common/symbol.library.ts index f855cd5..9095bad 100644 --- a/packages/library/src/common/symbol.library.ts +++ b/packages/library/src/common/symbol.library.ts @@ -18,6 +18,8 @@ const sym = { $Registry: Symbol.for('$LibraryRegistry'), /** key to identify the global registration hook */ $Register: Symbol.for('$LibraryRegister'), + /** key to identify the global serialization registry */ + $SerializerRegistry: Symbol.for('$LibrarySerializerRegistry'), } as const; /** identify and mark a Logify configuration object */ diff --git a/packages/library/vitest.config.ts b/packages/library/vitest.config.ts index a5a25cd..ad3c61e 100644 --- a/packages/library/vitest.config.ts +++ b/packages/library/vitest.config.ts @@ -3,16 +3,25 @@ import { dirname, resolve } from 'node:path'; import { defineConfig } from 'vitest/config'; const __dirname = dirname(fileURLToPath(import.meta.url)); +const isDist = process.env.TEST_DIST === 'true'; export default defineConfig({ test: { + name: 'Library: Full', globals: true, environment: 'node', + include: ['test/**/*.{test,spec}.ts'], + setupFiles: [resolve(__dirname, '../tempo/bin/setup.polyfill.ts')], }, resolve: { - alias: [ + alias: isDist ? [ + { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, './dist/common/$1.js') }, + { find: /^#library$/, replacement: resolve(__dirname, './dist/common.index.js') }, + { find: /^#browser\/(.*)\.js$/, replacement: resolve(__dirname, './dist/browser/$1.js') }, + { find: /^#server\/(.*)\.js$/, replacement: resolve(__dirname, './dist/server/$1.js') }, + ] : [ { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, './src/common/$1.ts') }, - { find: /^#library$/, replacement: resolve(__dirname, './src/common/index.ts') }, + { find: /^#library$/, replacement: resolve(__dirname, './src/common.index.ts') }, { find: /^#browser\/(.*)\.js$/, replacement: resolve(__dirname, './src/browser/$1.ts') }, { find: /^#server\/(.*)\.js$/, replacement: resolve(__dirname, './src/server/$1.ts') }, { find: /^#server\/(.*)$/, replacement: resolve(__dirname, './src/server/$1.ts') }, diff --git a/packages/tempo/.vitepress/config.ts b/packages/tempo/.vitepress/config.ts index 4bba792..52c2505 100644 --- a/packages/tempo/.vitepress/config.ts +++ b/packages/tempo/.vitepress/config.ts @@ -67,21 +67,26 @@ export default defineConfig({ }, vite: { build: { + target: 'es2022' + }, + esbuild: { target: 'esnext' }, resolve: { - // Include 'development' so workspace packages resolve from TypeScript source - // (no pre-built dist required when running docs:dev or docs:build). conditions: ['development', 'module', 'browser', 'import', 'default'], alias: [ + { + find: /^#library\/(.*)\.js$/, + replacement: fileURLToPath(new URL('../../library/dist/common/$1.js', import.meta.url)) + }, // More-specific path must come first so it is matched before the bare package. { find: /^@magmacomputing\/tempo\/ticker$/, - replacement: fileURLToPath(new URL('../src/plugin/extend/extend.ticker.ts', import.meta.url)) + replacement: fileURLToPath(new URL('../dist/plugin/extend/extend.ticker.js', import.meta.url)) }, { find: /^@magmacomputing\/tempo$/, - replacement: fileURLToPath(new URL('../src/tempo.index.ts', import.meta.url)) + replacement: fileURLToPath(new URL('../dist/tempo.index.js', import.meta.url)) }, ] }, diff --git a/packages/tempo/CHANGELOG.md b/packages/tempo/CHANGELOG.md index 747da50..77e87f6 100644 --- a/packages/tempo/CHANGELOG.md +++ b/packages/tempo/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.2] - 2026-04-18 + +### Fixed +- **Plugin Infrastructure Preservation**: Refactored the Rollup configuration to treat all library files as public entry points. This prevents critical utilities (like `defineExtension`) from being tree-shaken during the build process, ensuring that modular plugins can register correctly. +- **API Surface Hardening**: Explicitly exported all registration and utility helpers (`defineModule`, `defineTerm`, etc.) from the main entry point to guarantee their availability for third-party extensions. +- **Documentation Build Stability**: Updated the documentation configuration to utilize pre-compiled `dist/` assets. This resolves runtime `SyntaxError` issues in the browser caused by the presence of modern TC39 decorators in the raw TypeScript source files. +- **Decorator Transpilation**: Refactored utility functions to ensure standard function declarations are used where appropriate, improving the reliability of the transpilation phase. + ## [2.1.2] - 2026-04-14 ### Added diff --git a/packages/tempo/README.md b/packages/tempo/README.md index b33a499..7fbd7e6 100644 --- a/packages/tempo/README.md +++ b/packages/tempo/README.md @@ -1,22 +1,12 @@ - - Visit Tempo Documentation - - -
- - +
- - + - - - - @@ -27,14 +17,14 @@ **Tempo** is a premium, high-performance wrapper around the JavaScript `Temporal` API. It provides a modern, **immutable**, and **fluent** interface for date-time manipulation, and flexible parsing. It's designed as a better-performing, type-safe alternative to legacy libraries like **Moment.js**, **Day.js**, and **Luxon**.
-
- Tempo logo +
+ Tempo logo - Tempo -
- The Professional Date-Time Library for Temporal + +

Tempo

+

The Professional Date-Time Library for the Temporal API

+
- - - - - - + + + + + +
License: MITTemporalTypeScript ReadyNative ESMDocumentation
License: MITTemporalTypeScript ReadyNative ESMDocumentation
@@ -116,6 +106,17 @@ For environments without `importmap` support or simple prototypes, use the bundl console.log(t.toString()); ``` + +--- + +## 📚 Documentation + +For a deeper dive into the API, architecture, and advanced features: + +* **[Official Documentation Website](https://magmacomputing.github.io/magma/)** — Tutorials, interactive demos, and "Getting Started" guides. +* **[Full API Reference Guide](https://magmacomputing.github.io/magma/doc/tempo.api)** — Detailed technical documentation for every class and method. + +--- ## 🛠️ Quick Start ```javascript @@ -134,7 +135,6 @@ const startOfMonth = now.set({ start: 'month' }); console.log(now.format('{dd} {mmm} {yyyy}')); // using custom format with tokens: "24 Jan 2026" console.log(now.fmt.date); // using pre-built formats: "2026-01-24" ``` - Looking for the full API reference? Check out the **[Tempo API Guide](https://magmacomputing.github.io/magma/doc/tempo.api)**. ## 💬 Contact & Support diff --git a/packages/tempo/doc/tempo.ticker.md b/packages/tempo/doc/tempo.ticker.md index 0f5735a..879bfaf 100644 --- a/packages/tempo/doc/tempo.ticker.md +++ b/packages/tempo/doc/tempo.ticker.md @@ -1,6 +1,6 @@ # Tempo Ticker -`Tempo.ticker` is an optional plugin (provided in the project) that creates a reactive stream of `Tempo` instances at regular intervals. It is designed to be high-performance and lightweight, providing a simple way to build clocks, countdowns, or scheduled updates. +`Tempo.ticker` is an optional plugin (provided in the @magmacomputing/tempo/ticker module) that creates a reactive stream of `Tempo` instances at regular intervals. It is designed to be high-performance and lightweight, providing a simple way to build clocks, countdowns, or scheduled updates. ## Installation @@ -142,11 +142,11 @@ All listeners use the same callback signature: `(t, stop) => {}`. ```typescript const ticker = Tempo.ticker(1); -ticker.on('pulse', (t) => console.log('Listener A:', t)); -ticker.on('pulse', (t) => console.log('Listener B:', t)); -ticker.on('stop', (t, stop) => console.log('Ticker stopped at:', t, stop)); +ticker.on('pulse', (t) => console.log('Listener A:', t.fmt.weekTime)); +ticker.on('pulse', (t) => console.log('Listener B:', t.fmt.weekTime)); +ticker.on('stop', (t) => console.log('Ticker stopped at:', t.fmt.weekTime)); ``` -For `'stop'` listeners, `stop` is included for callback signature consistency; invoking it after stop has already occurred is a no-op. +For `'stop'` listeners, the `stop` callback argument is included for signature consistency; however, invoking it after stop has already occurred is a no-op. ### 4. Manual Pulsing (.pulse) In some scenarios, you may want to drive a ticker manually (e.g., from a UI event or a WebSocket message) while still benefiting from the ticker's internal state management and listeners. diff --git a/packages/tempo/index.md b/packages/tempo/index.md index 284c369..d4e865a 100644 --- a/packages/tempo/index.md +++ b/packages/tempo/index.md @@ -1,28 +1,5 @@ --- layout: home - -hero: - name: "Tempo" - text: "The Professional Date-Time Library" - tagline: "Fluent, Immutable, and Zero-Cost wrapper for the Temporal API." - image: - src: /logo.svg - alt: Tempo Logo - actions: - - theme: brand - text: Get Started - link: /README - - theme: alt - text: View on GitHub - link: https://github.com/magmacomputing/magma/tree/main/packages/tempo - -features: - - title: "Zero-Cost Constructor" - details: "Lazy evaluation and smart matching ensure instantiation overhead is near-zero, even with massive plugin lists." - - title: "Relational Math" - details: "Shift by semantic terms (Quarters, Seasons, Periods) while preserving your relative cycle offset." - - title: "Hardened & Modular" - details: "Built for resilience in complex monorepos with proxy-protected registries and decoupled diagnostics." --- -
-
-

Live Tempo.ticker Demo

-

- This analog clock is driven by a single - Tempo.ticker({ seconds: 1 }) call. - It starts the moment you land on this page and is automatically - disposed when you navigate away — no zombie timers, no memory leaks. -

-
- - - - - - - - - - - - - - - - - -

{{ timeStr }}

+
+
+
+
+ +

Tempo

+
+
+

The Professional Date-Time Library for the Temporal API

+ +
+
+
+
+ + + + + + + + + + + +
+

{{ timeStr }}

+

{{ tzStr }}

+
+
+
+
+

Zero-Cost Constructor

+

Lazy evaluation and smart matching ensure instantiation overhead is near-zero, even with massive plugin lists.

+
+
+

Relational Math

+

Shift by semantic terms (Quarters, Seasons, Periods) while preserving your relative cycle offset.

+
+
+

Hardened & Modular

+

Built for resilience in complex monorepos with proxy-protected registries and decoupled diagnostics.

+
+
+ diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 7775c5d..1075e90 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo", - "version": "2.2.1", + "version": "2.2.2", "description": "The Tempo core library", "author": "Magma Computing Solutions", "license": "MIT", @@ -147,7 +147,8 @@ } }, "scripts": { - "test": "vitest run --workspace ../../vitest.workspace.ts", + "test": "vitest run", + "test:dist": "cross-env TEST_DIST=true vitest run", "repl": "tsx --conditions=development -i --import ./bin/setup.polyfill.ts --import ./bin/repl.ts", "core": "tsx --conditions=development -i --import ./bin/setup.polyfill.ts --import ./bin/core.ts", "build": "tsc -b && npm run build:bundle && npm run build:resolve", @@ -157,22 +158,21 @@ "publish": "npm publish --access public", "prepublishOnly": "npm run build", "docs:api": "typedoc", - "docs:dev": "npm run docs:api && vitepress dev", - "docs:build": "npm run docs:api && vitepress build", + "docs:dev": "npm run build && npm run docs:api && vitepress dev", + "docs:build": "npm run build && npm run docs:api && vitepress build", "docs:preview": "vitepress preview" }, "files": [ - "doc/", - "dist/", - ".vitepress/dist/" + "dist/" ], "dependencies": { "tslib": "^2.8.1" }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.2.1", + "@magmacomputing/library": "2.2.2", "@rollup/plugin-alias": "^6.0.0", + "cross-env": "^7.0.3", "magic-string": "^0.30.21", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0", diff --git a/packages/tempo/rollup.config.js b/packages/tempo/rollup.config.js index 74460d1..a95af82 100644 --- a/packages/tempo/rollup.config.js +++ b/packages/tempo/rollup.config.js @@ -1,7 +1,12 @@ import path from 'node:path'; +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; import resolve from '@rollup/plugin-node-resolve'; import MagicString from 'magic-string'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const distPath = path.join(__dirname, 'dist'); + /** * Rollup Configuration for Tempo * @@ -9,9 +14,41 @@ import MagicString from 'magic-string'; * 2. Granular ESM: Multi-file for bundlers, keeps external dependencies external. */ +function getFiles(dir, suffix = '.js') { + const files = []; + if (!fs.existsSync(dir)) return files; + + try { + const items = fs.readdirSync(dir, { withFileTypes: true }); + for (const item of items) { + const fullPath = path.join(dir, item.name); + if (item.isDirectory()) { + files.push(...getFiles(fullPath, suffix)); + } else if (item.name.endsWith(suffix) && !item.name.endsWith('.bundle.js') && !item.name.endsWith('.entry.js')) { + files.push(fullPath); + } + } + } catch (e) { + console.error(`Rollup Build Warning: Could not read directory ${dir}. Ensure 'tsc' has run.`); + } + return files; +} + +// Generate a map of entry points from all files in dist (after tsc has run) +const entryPoints = Object.fromEntries( + getFiles(distPath).map(file => [ + path.relative(distPath, file).replace(/\.js$/, ''), + file + ]) +); + +// Force inclusion of the full library for testing/distribution parity +// We resolve this relative to this config file's directory +entryPoints['lib/common.index'] = path.resolve(__dirname, '../library/dist/common.index.js'); + export default [ { - input: 'dist/tempo.entry.js', + input: path.join(distPath, 'tempo.entry.js'), output: { file: 'dist/tempo.bundle.js', format: 'iife', @@ -26,23 +63,20 @@ export default [ ], }, { - input: { - 'tempo.index': 'dist/tempo.index.js', - 'library.index': 'dist/library.index.js' - }, + input: entryPoints, // Keep tslib external in ESM distribution for better bundler compatibility external: ['tslib'], output: { dir: 'dist', format: 'es', preserveModules: true, - preserveModulesRoot: 'dist', + preserveModulesRoot: distPath, sourcemap: false, indent: '\t', // Map library imports to lib/ for browser-ready granular ESM entryFileNames: (chunkInfo) => { if (!chunkInfo.facadeModuleId) return '[name].js'; - const rel = path.relative(process.cwd(), chunkInfo.facadeModuleId); + const rel = path.relative(__dirname, chunkInfo.facadeModuleId); return (rel.startsWith('..') || rel.includes('node_modules')) ? 'lib/' + path.basename(chunkInfo.facadeModuleId, '.js') + '.js' : '[name].js'; diff --git a/packages/tempo/src/plugin/plugin.util.ts b/packages/tempo/src/plugin/plugin.util.ts index 3a04fce..7ed6335 100644 --- a/packages/tempo/src/plugin/plugin.util.ts +++ b/packages/tempo/src/plugin/plugin.util.ts @@ -103,11 +103,11 @@ export function findTermPlugin(ident: string): TermPlugin | undefined { const id = (ident.startsWith('#') ? ident.slice(1) : ident).toLowerCase(); const [termPart] = id.split('.'); - return REGISTRY.terms.find(t => { + return REGISTRY.terms.find((t: TermPlugin) => { if (t.key?.toLowerCase() === termPart || t.scope?.toLowerCase() === termPart) return true; if (t.groups) { const list = Array.isArray(t.groups) ? t.groups : Object.values(t.groups).flat(Infinity) as Range[]; - return list.some(r => r.key?.toLowerCase() === id || r.key?.toLowerCase() === termPart); + return list.some((r: Range) => r.key?.toLowerCase() === id || r.key?.toLowerCase() === termPart); } return false; }); @@ -423,7 +423,7 @@ export function registerTerm(term: TermPlugin) { db.terms.push(term); } - if (!REGISTRY.terms.find(t => t.key === term.key)) { + if (!REGISTRY.terms.find((t: TermPlugin) => t.key === term.key)) { REGISTRY.terms.push(term); } diff --git a/packages/tempo/src/plugin/term/standard.index.ts b/packages/tempo/src/plugin/term/standard.index.ts index 96accf1..3674a60 100644 --- a/packages/tempo/src/plugin/term/standard.index.ts +++ b/packages/tempo/src/plugin/term/standard.index.ts @@ -1,5 +1,11 @@ import { Tempo } from '../../tempo.class.js'; +import { onRegistryReset } from '../../tempo.register.js'; import { TermsModule } from './term.index.js'; // Side-effect: Automatically register all standard terms Tempo.extend(TermsModule); + +// Resilience: Ensure terms are restored after a registry reset +onRegistryReset(() => { + Tempo.extend(TermsModule); +}); diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 1cb6147..cfbceee 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -748,7 +748,6 @@ export class Tempo { if (key.startsWith('usr.')) // only remove 'usr.' prefixed keys delete Token[key]; - Tempo.#terms = []; // clear registered terms Tempo.#termMap.clear(); // clear term lookup map registryReset(); // purge formats and numbers diff --git a/packages/tempo/src/tempo.register.ts b/packages/tempo/src/tempo.register.ts index a0496bc..143f11b 100644 --- a/packages/tempo/src/tempo.register.ts +++ b/packages/tempo/src/tempo.register.ts @@ -12,14 +12,16 @@ import type { TermPlugin, Extension } from './plugin/plugin.type.js'; import { STATE, REGISTRIES, DEFAULTS } from './tempo.enum.js'; /** @internal storage for plugin/module registry */ -const _terms = [] as TermPlugin[]; -const _extends = [] as Extension[]; +const _terms = (globalThis as any)[sym.$terms] ??= [] as TermPlugin[]; +const _extends = (globalThis as any)[sym.$extends] ??= [] as Extension[]; +const _modules = (globalThis as any)[sym.$modules] ??= {} as Record; +const _installed = (globalThis as any)[sym.$installed] ??= new Set(); const _REGISTRY = { terms: secureRef(_terms), extends: secureRef(_extends), - modules: secureRef({} as Record), - installed: new Set() + modules: secureRef(_modules), + installed: _installed } /** @@ -67,18 +69,22 @@ export function registryReset() { clearCache(state); }); - // 3. Clear Term and Extension arrays (managed via secureRef) + // 3. Clear all plugin/module storage via raw targets const internal = (_REGISTRY as any); - if (internal.terms[lib.$Target]) internal.terms[lib.$Target].length = 0; - if (internal.extends[lib.$Target]) internal.extends[lib.$Target].length = 0; - - // 4. Clear Modules and Installed Set + const terms = internal.terms[lib.$Target] ?? internal.terms; + const extensions = internal.extends[lib.$Target] ?? internal.extends; const modules = internal.modules[lib.$Target] ?? internal.modules; + + terms.length = 0; + extensions.length = 0; + for (const key in modules) delete modules[key]; internal.installed.clear(); // Trigger all registered reset hooks - resetHooks().forEach(hook => hook()); + const hooks = resetHooks(); + hooks.forEach(hook => hook()); + hooks.clear(); } /** update a global registry with new discoverable data */ diff --git a/packages/tempo/src/tempo.symbol.ts b/packages/tempo/src/tempo.symbol.ts index ed17ba0..227d370 100644 --- a/packages/tempo/src/tempo.symbol.ts +++ b/packages/tempo/src/tempo.symbol.ts @@ -22,6 +22,10 @@ export const sym = { /** internal key for tracking mutation recursion depth */ $mutateDepth: Symbol.for('$TempoMutateDepth'), /** internal key for re-validating the Master Guard */ $rebuildGuard: Symbol.for('$TempoRebuildGuard'), /** internal key for decentralized registry resets */ $reset: Symbol.for('$TempoReset'), + /** internal key for tracking installed plugins */ $installed: Symbol.for('$TempoInstalled'), + /** internal key for tracking registered terms */ $terms: Symbol.for('$TempoTerms'), + /** internal key for tracking registered extensions */ $extends: Symbol.for('$TempoExtends'), + /** internal key for tracking registered modules */ $modules: Symbol.for('$TempoModules'), } as const; /** diff --git a/packages/tempo/vitest.config.ts b/packages/tempo/vitest.config.ts index 63fd144..e7521e2 100644 --- a/packages/tempo/vitest.config.ts +++ b/packages/tempo/vitest.config.ts @@ -3,21 +3,41 @@ import { dirname, resolve } from 'node:path'; import { defineConfig } from 'vitest/config'; const __dirname = dirname(fileURLToPath(import.meta.url)); +const isDist = process.env.TEST_DIST === 'true'; +const polyfill = resolve(__dirname, './bin/setup.polyfill.ts'); export default defineConfig({ plugins: [], test: { globals: true, - pool: 'forks', // isolated child processes (no shared memory) + pool: 'forks', poolOptions: { forks: { - minForks: 1, // always keep at least 1 worker alive - maxForks: 2, // cap at 2 concurrent forks to limit load + minForks: 1, + maxForks: 2, }, }, + setupFiles: [polyfill], }, resolve: { - alias: [ + alias: isDist ? [ + { find: /^#tempo\/core$/, replacement: resolve(__dirname, './dist/core.index.js') }, + { find: /^#tempo\/term$/, replacement: resolve(__dirname, './dist/plugin/term/term.index.js') }, + { find: /^#tempo\/term\/standard$/, replacement: resolve(__dirname, './dist/plugin/term/standard.index.js') }, + { find: /^#tempo\/term\/(.*)$/, replacement: resolve(__dirname, './dist/plugin/term/$1.js') }, + { find: /^#tempo\/ticker$/, replacement: resolve(__dirname, './dist/plugin/extend/extend.ticker.js') }, + { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './dist/plugin/module/module.duration.js') }, + { find: /^#tempo\/format$/, replacement: resolve(__dirname, './dist/plugin/module/module.format.js') }, + { find: /^#tempo\/scripts\/(.*)\.js$/, replacement: resolve(__dirname, './scripts/$1.js') }, + { find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/plugin.$1.js') }, + { find: /^#tempo\/plugin\/extend\.(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/extend/extend.$1.js') }, + { find: /^#tempo\/plugin\/module\.(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/module/module.$1.js') }, + { find: /^#tempo\/plugin\/term\.(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/term/term.$1.js') }, + { find: /^#tempo\/(.*)\.js$/, replacement: resolve(__dirname, './dist/$1.js') }, + { find: /^#tempo$/, replacement: resolve(__dirname, './dist/tempo.index.js') }, + { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, '../library/dist/common/$1.js') }, + { find: /^#library$/, replacement: resolve(__dirname, '../library/dist/common.index.js') }, + ] : [ { find: /^#tempo\/core$/, replacement: resolve(__dirname, './src/core.index.ts') }, { find: /^#tempo\/term$/, replacement: resolve(__dirname, './src/plugin/term/term.index.ts') }, { find: /^#tempo\/term\/standard$/, replacement: resolve(__dirname, './src/plugin/term/standard.index.ts') }, @@ -25,18 +45,15 @@ export default defineConfig({ { find: /^#tempo\/ticker$/, replacement: resolve(__dirname, './src/plugin/extend/extend.ticker.ts') }, { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './src/plugin/module/module.duration.ts') }, { find: /^#tempo\/format$/, replacement: resolve(__dirname, './src/plugin/module/module.format.ts') }, - { find: /^#tempo\/scripts\/(.*)\.js$/, replacement: resolve(__dirname, './scripts/$1.ts') }, { find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/plugin.$1.ts') }, { find: /^#tempo\/plugin\/extend\.(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/extend/extend.$1.ts') }, { find: /^#tempo\/plugin\/module\.(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/module/module.$1.ts') }, { find: /^#tempo\/plugin\/term\.(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/term/term.$1.ts') }, - { find: /^#tempo\/(.*)\.js$/, replacement: resolve(__dirname, './src/$1.ts') }, { find: /^#tempo$/, replacement: resolve(__dirname, './src/tempo.index.ts') }, - { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, '../library/src/common/$1.ts') }, { find: /^#library$/, replacement: resolve(__dirname, '../library/src/common.index.ts') }, ] } -}) +}); diff --git a/packages/tempo/vitest.workspace.ts b/packages/tempo/vitest.workspace.ts new file mode 100644 index 0000000..e43460e --- /dev/null +++ b/packages/tempo/vitest.workspace.ts @@ -0,0 +1,23 @@ +import { defineWorkspace } from 'vitest/config' + +export default defineWorkspace([ + { + extends: 'vitest.config.ts', + test: { + name: 'Tempo: Full', + include: ['test/**/*.{test,spec}.ts'], + exclude: [ + '**/node_modules/**', + '**/test/**/*.core.test.ts', + '**/test/**/*.lazy.test.ts' + ], + } + }, + { + extends: 'vitest.config.ts', + test: { + name: 'Tempo: Core', + include: ['test/**/*.core.test.ts', 'test/**/*.lazy.test.ts'], + } + } +]) diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 8dbbbef..9e4c3d4 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -10,7 +10,7 @@ export default defineWorkspace([ extends: 'packages/tempo/vitest.config.ts', test: { name: 'Tempo: Full', - include: ['**/test/**/*.{test,spec}.ts'], + include: ['packages/tempo/test/**/*.{test,spec}.ts'], exclude: [ '**/node_modules/**', '**/test/**/*.core.test.ts', @@ -23,7 +23,16 @@ export default defineWorkspace([ extends: 'packages/tempo/vitest.config.ts', test: { name: 'Tempo: Core', - include: ['**/test/**/*.core.test.ts', '**/test/**/*.lazy.test.ts'], + include: ['packages/tempo/test/**/*.core.test.ts', 'packages/tempo/test/**/*.lazy.test.ts'], + exclude: ['**/node_modules/**'], + setupFiles: [polyfill], + } + }, + { + extends: 'packages/library/vitest.config.ts', + test: { + name: 'Library: Full', + include: ['packages/library/test/**/*.{test,spec}.ts'], exclude: ['**/node_modules/**'], setupFiles: [polyfill], }