From 11ef41234daa0d7f277db4734fe96c2bfd3b39cf Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 16 Jul 2026 08:28:04 +1000 Subject: [PATCH 1/6] PR publish plugins --- package.json | 2 +- packages/plugins/.bin/README.md | 10 +++++ packages/plugins/{bin => .bin}/repl.mts | 0 .../{bin => .bin}/temporal-polyfill.mts | 0 packages/plugins/{bin => .bin}/tsconfig.json | 0 packages/plugins/astro/package.json | 2 +- packages/plugins/batch/package.json | 3 +- packages/plugins/finance/package.json | 2 +- packages/plugins/snap/package.json | 2 +- packages/plugins/sync/package.json | 2 +- packages/plugins/tsup.shared.ts | 4 +- packages/plugins/vitest.shared.ts | 41 +++++++++++++++++++ .../theme/components/CatalogList.vue | 35 +++++++++++++--- packages/tempo/vitest.config.ts | 12 ++---- vitest.config.ts | 11 +++-- 15 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 packages/plugins/.bin/README.md rename packages/plugins/{bin => .bin}/repl.mts (100%) rename packages/plugins/{bin => .bin}/temporal-polyfill.mts (100%) rename packages/plugins/{bin => .bin}/tsconfig.json (100%) create mode 100644 packages/plugins/vitest.shared.ts diff --git a/package.json b/package.json index 6c9cfcdd..b63f1f33 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "version:bump": "node bin/version-bump.mjs", "version:sync": "node bin/version-sync.mjs", "repl": "npm run repl --workspace=@magmacomputing/tempo", - "repl:plugins": "node packages/plugins/bin/repl.mjs", + "repl:plugins": "node packages/plugins/.bin/repl.mjs", "repl:dist": "npm run repl:dist --workspace=@magmacomputing/tempo", "core": "npm run core --workspace=@magmacomputing/tempo", "docs:dev": "npm run docs:dev --workspace=@magmacomputing/tempo", diff --git a/packages/plugins/.bin/README.md b/packages/plugins/.bin/README.md new file mode 100644 index 00000000..cf6ac9df --- /dev/null +++ b/packages/plugins/.bin/README.md @@ -0,0 +1,10 @@ +# Plugin Support Binaries + +This directory (`packages/plugins/bin/`) contains internal support scripts and utilities for developing and testing Tempo plugins within the monorepo. + +It includes: +- **REPL Environment (`repl.mts`)**: Scripts to initialize an interactive Node.js REPL session with Tempo and Temporal pre-loaded, making it easy to experiment with plugins from the CLI. +- **Polyfill Setup (`temporal-polyfill.mts`)**: Initialization scripts to ensure the `@js-temporal/polyfill` is correctly loaded into the global scope during testing or REPL sessions, allowing plugins to work with native `Temporal` APIs before they are officially adopted by all runtimes. +- **TypeScript Configuration (`tsconfig.json`)**: Specific compiler options for running these support scripts directly via tools like `tsx`. + +These files are meant for local monorepo development and testing purposes only. They are not published or distributed with any NPM packages. diff --git a/packages/plugins/bin/repl.mts b/packages/plugins/.bin/repl.mts similarity index 100% rename from packages/plugins/bin/repl.mts rename to packages/plugins/.bin/repl.mts diff --git a/packages/plugins/bin/temporal-polyfill.mts b/packages/plugins/.bin/temporal-polyfill.mts similarity index 100% rename from packages/plugins/bin/temporal-polyfill.mts rename to packages/plugins/.bin/temporal-polyfill.mts diff --git a/packages/plugins/bin/tsconfig.json b/packages/plugins/.bin/tsconfig.json similarity index 100% rename from packages/plugins/bin/tsconfig.json rename to packages/plugins/.bin/tsconfig.json diff --git a/packages/plugins/astro/package.json b/packages/plugins/astro/package.json index 0fb82d0c..b0f171f7 100644 --- a/packages/plugins/astro/package.json +++ b/packages/plugins/astro/package.json @@ -18,7 +18,7 @@ }, "scripts": { "build": "tsup && tsc", - "test": "vitest run -c ../../test/vitest.community.ts", + "test": "vitest run -c ../vitest.shared.ts", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build" }, "peerDependencies": { diff --git a/packages/plugins/batch/package.json b/packages/plugins/batch/package.json index 4ddff66d..d19d2716 100644 --- a/packages/plugins/batch/package.json +++ b/packages/plugins/batch/package.json @@ -17,8 +17,7 @@ }, "scripts": { "build": "tsup && tsc", - "pretest": "npm run build", - "test": "vitest run -c ../../test/vitest.community.ts", + "test": "vitest run -c ../vitest.shared.ts", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build" }, "tempo": { diff --git a/packages/plugins/finance/package.json b/packages/plugins/finance/package.json index 8eef90ad..36b60000 100644 --- a/packages/plugins/finance/package.json +++ b/packages/plugins/finance/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "tsup && tsc", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build", - "test": "vitest run -c ../../test/vitest.community.ts" + "test": "vitest run -c ../vitest.shared.ts" }, "keywords": [ "tempo", diff --git a/packages/plugins/snap/package.json b/packages/plugins/snap/package.json index 7d60cac4..dff9ccc0 100644 --- a/packages/plugins/snap/package.json +++ b/packages/plugins/snap/package.json @@ -17,7 +17,7 @@ }, "scripts": { "build": "tsup && tsc", - "test": "vitest run -c ../../test/vitest.community.ts", + "test": "vitest run -c ../vitest.shared.ts", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build" }, "peerDependencies": { diff --git a/packages/plugins/sync/package.json b/packages/plugins/sync/package.json index a0c3539c..ef860593 100644 --- a/packages/plugins/sync/package.json +++ b/packages/plugins/sync/package.json @@ -17,7 +17,7 @@ }, "scripts": { "build": "tsup && tsc", - "test": "vitest run -c ../../test/vitest.community.ts", + "test": "vitest run -c ../vitest.shared.ts", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build" }, "tempo": { diff --git a/packages/plugins/tsup.shared.ts b/packages/plugins/tsup.shared.ts index 6c7c5f52..84226202 100644 --- a/packages/plugins/tsup.shared.ts +++ b/packages/plugins/tsup.shared.ts @@ -108,10 +108,10 @@ export const sharedConfig: Options = { const pkgPath = path.resolve(process.cwd(), 'package.json'); if (fs.existsSync(pkgPath)) { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - const pricing = pkg?.tempo?.pricing; + const plan = pkg?.tempo?.plan; // If the package explicitly declares the 'community' pricing tier, skip the license wrapper - if (pricing === 'community') + if (plan === 'community') return; // Community plugin, do not apply license wrapper } diff --git a/packages/plugins/vitest.shared.ts b/packages/plugins/vitest.shared.ts new file mode 100644 index 00000000..a9b5f3c8 --- /dev/null +++ b/packages/plugins/vitest.shared.ts @@ -0,0 +1,41 @@ +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; +import swc from 'unplugin-swc'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const polyfill = resolve(__dirname, '../tempo/bin/temporal-polyfill.ts'); +const spy = resolve(__dirname, '../tempo/test/support/setup.console-spy.ts'); + +export default defineConfig({ + esbuild: false, + oxc: false, + plugins: [ + swc.vite({ + jsc: { + target: 'es2022', + parser: { syntax: 'typescript', decorators: true }, + transform: { decoratorVersion: '2023-11' }, + }, + }), + ], + test: { + globals: true, + environment: 'node', + include: ['test/**/*.{test,spec}.ts'], + setupFiles: [polyfill, spy], + alias: [ + { find: /^#library\/(browser|server|common)\/(.*)\.js$/, replacement: resolve(__dirname, '../library/src/$1/$2.ts') }, + { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, '../library/src/common/$1.ts') }, + { find: /^#tempo\/plugins\/plugin\.(util|type)\.js$/, replacement: resolve(__dirname, '../tempo/src/plugins/plugin.$1.ts') }, + { find: /^#tempo\/plugins\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, '../tempo/src/plugins/extend/plugin.$1.ts') }, + { find: /^#tempo\/(parse|format|mutate|duration)$/, replacement: resolve(__dirname, '../tempo/src/module/module.$1.ts') }, + { find: /^#tempo\/core$/, replacement: resolve(__dirname, '../tempo/src/core.index.ts') }, + { find: /^#tempo\/module$/, replacement: resolve(__dirname, '../tempo/src/module/module.index.ts') }, + { find: /^#tempo\/support$/, replacement: resolve(__dirname, '../tempo/src/support/support.index.ts') }, + { find: /^#tempo\/tempo\.class\.js$/, replacement: resolve(__dirname, '../tempo/src/tempo.index.ts') }, + { find: /^#tempo\/(.*)\.js$/, replacement: resolve(__dirname, '../tempo/src/$1.ts') }, + { find: /^#tempo\/(.*)$/, replacement: resolve(__dirname, '../tempo/src/$1.ts') } + ] + } +}); diff --git a/packages/tempo/.vitepress/theme/components/CatalogList.vue b/packages/tempo/.vitepress/theme/components/CatalogList.vue index 8d136539..1d75fab4 100644 --- a/packages/tempo/.vitepress/theme/components/CatalogList.vue +++ b/packages/tempo/.vitepress/theme/components/CatalogList.vue @@ -9,6 +9,7 @@ interface Plugin { price: number; status: string; plan: string; + version: string; } const plugins = ref([]) @@ -30,7 +31,8 @@ onMounted(async () => { packageName: fields.packageName?.stringValue || '', price: parseInt(fields.price?.integerValue || '0'), status: fields.status?.stringValue || 'active', - plan: fields.plan?.stringValue || 'community' + plan: fields.plan?.stringValue || 'community', + version: fields.release?.mapValue?.fields?.version?.stringValue || '', } }) } @@ -59,7 +61,10 @@ const copyInstall = (pkgName: string) => {

These plugins are free, open-source extensions that do not require a license token.

-

{{ plugin.name }}

+
+

{{ plugin.name }}

+ v{{ plugin.version }} +

{{ plugin.description }}

npm install {{ plugin.packageName }} @@ -89,7 +94,10 @@ const copyInstall = (pkgName: string) => {
Premium
-

{{ plugin.name }}

+
+

{{ plugin.name }}

+ v{{ plugin.version }} +

{{ plugin.description }}

npm install {{ plugin.packageName }} @@ -106,7 +114,10 @@ const copyInstall = (pkgName: string) => {

Coming Soon

-

{{ plugin.name }}

+
+

{{ plugin.name }}

+ v{{ plugin.version }} +

{{ plugin.description }}

@@ -133,11 +144,25 @@ const copyInstall = (pkgName: string) => { display: flex; flex-direction: column; } +.card-title { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.5rem; + margin-bottom: 0.5rem; +} .card h3 { margin-top: 0; - margin-bottom: 0.5rem; + margin-bottom: 0; font-size: 1.25rem; } +.card-version { + font-size: 0.75rem; + font-weight: 500; + color: var(--vp-c-text-3); + white-space: nowrap; + flex-shrink: 0; +} .card p { flex-grow: 1; font-size: 0.95rem; diff --git a/packages/tempo/vitest.config.ts b/packages/tempo/vitest.config.ts index 2733b855..2c2b0cf9 100644 --- a/packages/tempo/vitest.config.ts +++ b/packages/tempo/vitest.config.ts @@ -65,17 +65,15 @@ export default defineConfig({ { find: /^#tempo\/license$/, replacement: resolve(__dirname, './dist/plugin/license/license.validator.js') }, { find: /^#tempo\/core$/, replacement: resolve(__dirname, './dist/core.index.js') }, { find: /^#tempo\/term$/, replacement: resolve(__dirname, './dist/plugin/term/term.index.js') }, - { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './dist/module/module.duration.js') }, - { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './dist/module/module.$1.js') }, + { find: /^#tempo\/(parse|format|mutate|duration)$/, replacement: resolve(__dirname, './dist/module/module.$1.js') }, { find: /^#tempo\/module$/, replacement: resolve(__dirname, './dist/module/module.index.js') }, - { find: /^#tempo\/mutate$/, replacement: resolve(__dirname, './dist/module/module.mutate.js') }, + { find: /^#tempo\/support$/, replacement: resolve(__dirname, './dist/support/support.index.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/$1.js') }, { find: /^#tempo\/engine\/(.*)\.js$/, replacement: resolve(__dirname, './dist/engine/$1.js') }, { find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './dist/module/$1.js') }, { find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/term/$1.js') }, - { find: /^#tempo\/support$/, replacement: resolve(__dirname, './dist/support/support.index.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') }, @@ -100,17 +98,15 @@ export default defineConfig({ { 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\/(.*)$/, replacement: resolve(__dirname, './src/plugin/term/$1') }, - { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './src/module/module.duration.ts') }, - { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './src/module/module.$1.ts') }, + { find: /^#tempo\/(parse|format|mutate|duration)$/, replacement: resolve(__dirname, './src/module/module.$1.ts') }, { find: /^#tempo\/module$/, replacement: resolve(__dirname, './src/module/module.index.ts') }, - { find: /^#tempo\/mutate$/, replacement: resolve(__dirname, './src/module/module.mutate.ts') }, + { find: /^#tempo\/support$/, replacement: resolve(__dirname, './src/support/support.index.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/$1.ts') }, { find: /^#tempo\/engine\/(.*)\.js$/, replacement: resolve(__dirname, './src/engine/$1.ts') }, { find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './src/module/$1.ts') }, { find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/term/$1.ts') }, - { find: /^#tempo\/support$/, replacement: resolve(__dirname, './src/support/support.index.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') }, diff --git a/vitest.config.ts b/vitest.config.ts index 8ca9d92c..27fe1366 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,6 @@ -import { defineConfig } from 'vitest/config' import path, { dirname } from 'node:path' import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' import swc from 'unplugin-swc' const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -61,14 +61,13 @@ export default defineConfig({ alias: [ { find: /^#library\/(browser|server|common)\/(.*)\.js$/, replacement: path.resolve(__dirname, './packages/library/src/$1/$2.ts') }, { find: /^#library\/(.*)\.js$/, replacement: path.resolve(__dirname, './packages/library/src/common/$1.ts') }, - { find: /^#tempo\/plugins\/plugin\.util\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/plugins/plugin.util.ts') }, - { find: /^#tempo\/plugins\/plugin\.type\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/plugins/plugin.type.ts') }, + { find: /^#tempo\/plugins\/plugin\.(util|type)\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/plugins/plugin.$1.ts') }, { find: /^#tempo\/plugins\/plugin\.(.*)\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/plugins/extend/plugin.$1.ts') }, { find: /^#tempo\/core$/, replacement: path.resolve(__dirname, './packages/tempo/src/core.index.ts') }, - { find: /^#tempo\/(parse|format)$/, replacement: path.resolve(__dirname, './packages/tempo/src/discrete/discrete.$1.ts') }, - { find: /^#tempo\/discrete$/, replacement: path.resolve(__dirname, './packages/tempo/src/discrete/discrete.index.ts') }, - { find: /^#tempo\/tempo\.class\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/tempo.index.ts') }, + { find: /^#tempo\/(parse|format|mutate|duration)$/, replacement: path.resolve(__dirname, './packages/tempo/src/module/module.$1.ts') }, { find: /^#tempo\/support$/, replacement: path.resolve(__dirname, './packages/tempo/src/support/support.index.ts') }, + { find: /^#tempo\/module$/, replacement: path.resolve(__dirname, './packages/tempo/src/module/module.index.ts') }, + { find: /^#tempo\/tempo\.class\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/tempo.index.ts') }, { find: /^#tempo\/(.*)\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/$1.ts') }, { find: /^#tempo\/(.*)$/, replacement: path.resolve(__dirname, './packages/tempo/src/$1.ts') } ] From 2133ef06b2afc690d057b445bb46bb54a1c2540d Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Thu, 16 Jul 2026 15:37:45 +1000 Subject: [PATCH 2/6] PR rm tempo-plugin-docs --- .github/workflows/ci.yml | 61 ++++++-- package.json | 2 +- packages/plugins/.app/doc/index.md | 135 ++++++++++++++++++ packages/plugins/.bin/repl.mts | 39 +++-- packages/plugins/.bin/temporal-polyfill.mts | 11 +- packages/plugins/finance/package.json | 1 + packages/plugins/ticker/doc/index.md | 2 + packages/plugins/tsconfig.shared.json | 4 + packages/plugins/tsup.shared.ts | 18 ++- packages/plugins/vitest.shared.ts | 4 +- packages/tempo/bin/harvest-plugins.mjs | 3 +- .../doc/3-extending-tempo/tempo.extension.md | 2 +- .../doc/8-project-and-support/commercial.md | 4 +- packages/tempo/doc/9-plugins/.app.md | 135 ++++++++++++++++++ packages/tempo/doc/9-plugins/ticker.md | 2 + packages/tempo/package.json | 4 +- packages/tempo/plan/refactor-path-terms.md | 7 + .../tempo/plan/typescript-format-template.md | 70 +++++++++ packages/tempo/src/module/module.format.ts | 23 ++- packages/tempo/src/tempo.class.ts | 13 +- packages/tempo/src/tempo.type.ts | 74 ++++++++++ vitest.config.ts | 4 +- 22 files changed, 569 insertions(+), 49 deletions(-) create mode 100644 packages/plugins/.app/doc/index.md create mode 100644 packages/tempo/doc/9-plugins/.app.md create mode 100644 packages/tempo/plan/refactor-path-terms.md create mode 100644 packages/tempo/plan/typescript-format-template.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05df835c..b704acf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,31 +8,72 @@ env: on: push: - branches: - - main - - release-c-layout-order-planner + branches: [main] pull_request: - branches: - - main - - release-c-layout-order-planner + branches: [main] jobs: - test: + library: + name: Library Tests + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + - name: Install monorepo dependencies + run: npm ci + - name: Run library tests + run: npm test --workspace=@magmacomputing/library + + tempo: name: Standard Tests runs-on: ubuntu-latest timeout-minutes: 30 + needs: library steps: - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - name: Install monorepo dependencies run: npm ci - working-directory: ${{ github.workspace }} - name: Run standard tests run: npm test working-directory: packages/tempo + plugins: + name: Plugin Tests + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: tempo + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + - name: Install monorepo dependencies + run: npm ci + - name: Run plugin tests + run: npm test --workspaces --if-present + working-directory: packages/plugins + functions: + name: Functions Tests + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: tempo + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + - name: Install monorepo dependencies + run: npm ci + - name: Run functions tests + run: npm test --workspace=@magmacomputing/tempo-fns diff --git a/package.json b/package.json index b63f1f33..dec329b9 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "version:bump": "node bin/version-bump.mjs", "version:sync": "node bin/version-sync.mjs", "repl": "npm run repl --workspace=@magmacomputing/tempo", - "repl:plugins": "node packages/plugins/.bin/repl.mjs", + "repl:plugins": "tsx --import ./packages/plugins/.bin/temporal-polyfill.mts ./packages/plugins/.bin/repl.mts", "repl:dist": "npm run repl:dist --workspace=@magmacomputing/tempo", "core": "npm run core --workspace=@magmacomputing/tempo", "docs:dev": "npm run docs:dev --workspace=@magmacomputing/tempo", diff --git a/packages/plugins/.app/doc/index.md b/packages/plugins/.app/doc/index.md new file mode 100644 index 00000000..1fcbb96e --- /dev/null +++ b/packages/plugins/.app/doc/index.md @@ -0,0 +1,135 @@ +# Tempo Plugin Setup Guide + +This guide explains how to install and activate premium Tempo plugins. + +## 1. Tempo License Registry + +::: warning πŸ”‘ ACTION REQUIRED: Generate Your License Key +To use any premium plugins, you **must** first generate a cryptographic license key (JWT). + +
+ + Tempo License Registry + +
+ πŸ‘‰ Go to the Tempo License Registry πŸ‘ˆ
+ Manage your subscriptions and retrieve your license key. +
+
+::: + +## 2. Installation + +All premium plugins are distributed as public scoped packages on the standard npm registry (`npmjs.com`). This means no special token configuration or registry overrides are required in your `.npmrc`. You can install them directly using your preferred package manager: + +```bash +npm install @magmacomputing/tempo-plugin-ticker +``` + +## 3. Activation + +Although the packages are publicly installable, they require your valid license key at runtime. If no valid license key is detected, the premium features will fail-safe to `undefined`. + +Once you have your key from the registry, you can activate it using any of the following discovery methods: + +### Method A: Environment Variable (Recommended for Node/Server environments) +Set the `TEMPO_LICENSE_KEY` environment variable in your run environment: + +```bash +export TEMPO_LICENSE_KEY="eyJhbGciOiJSUzI1NiJ9..." +``` + +Then in your application, you can simply import Tempo and the plugin via side-effect. Because the license key is automatically discovered from the environment variable, no manual initialization is required, and the side-effect import registers the plugin automatically: + +```javascript +import { Tempo } from '@magmacomputing/tempo'; +import '@magmacomputing/tempo-plugin-ticker'; // Automatically registers TickerPlugi + +const t = new Tempo(); +console.log(t.tickers); // Unlocked and ready! +``` + +### Method B: Initialization Option (Recommended for applications) +Pass the key explicitly when initializing Tempo. Since static imports are hoisted, calling `Tempo.init()` after a side-effect import will clear any registered terms. To avoid hoisting issues, you can either pass the plugins directly into the `plugins` configuration array (cleanest and recommended), or call `Tempo.extend()` explicitly after initialization: + +#### Option 1: Pass via Init Options (Recommended) +```javascript +import { Tempo } from '@magmacomputing/tempo'; +import { TickerPlugi } from '@magmacomputing/tempo-plugin-ticker'; + +Tempo.init({ + license: 'eyJhbGciOiJSUzI1NiJ9...', + plugins: [TickerPlugi ] +}); +``` + +#### Option 2: Explicit Extension +```javascript +import { Tempo } from '@magmacomputing/tempo'; +import { TickerPlugi } from '@magmacomputing/tempo-plugin-ticker'; + +// 1. Initialize core Tempo with your license +Tempo.init({ + license: 'eyJhbGciOiJSUzI1NiJ9...' +}); + +// 2. Register the plugin +Tempo.extend(TickerPlugi ); +``` + +### Method C: Global Context (Fallback for specific bundlers/environments) + +Method C is a browser-native variation of Method A. While Method A targets the process-level environment (`process.env`) available in Node/SSR contexts, Method C sets the key on the JavaScript `globalThis` object (which maps to `window` in browsers), enabling the same auto-discovery behaviour. + +**Use this method in the following scenarios:** + +#### 1. Direct HTML Script Tags (No Bundler) +If you are loading Tempo directly from a CDN or local file using ` + + +``` + +#### 2. Frontend Bundlers without `process.env` Polyfills +Modern browser bundlers (e.g., Vite) do not inject Node's `process` object by default. If you prefer to avoid configuring build-time env replacements or `dotenv` plugins, assign the key to `globalThis` in your entry file *before* importing Tempo: + +```javascript +// entry.js β€” must run before any Tempo import +globalThis.TEMPO_LICENSE_KEY = import.meta.env.VITE_TEMPO_LICENSE_KEY; + +// Now safe to import +import { Tempo } from '@magmacomputing/tempo'; +import '@magmacomputing/tempo-plugin-ticker'; +``` + +#### 3. Micro-frontends / Shared Global Space +In architectures where multiple independently-bundled applications share a single browser tab, set the key once in the host container. All dynamically-loaded sub-applications will then auto-discover it without needing individual configuration: + +```javascript +// host-container.js +globalThis.TEMPO_LICENSE_KEY = 'eyJhbGciOiJSUzI1NiJ9...'; + +// sub-apps loaded later will automatically run in licensed mode +``` + +## 4. Network Requests & Offline Behavior + +To verify license validity and prevent abuse, Tempo's licensing engine performs background synchronization with our revocation registry: + +* **Outbound Request:** When a license key is active, Tempo asynchronously fetches a cryptographically signed revocation list (JWS). +* **Endpoint:** `https://registry.magmacomputing.com.au/tempo/v1/revoked.jws` (useful for configuring Content Security Policies (CSP) or egress firewall rules). +* **Frequency:** The revocation check occurs once every **7 days**. The last-checked state is cached to avoid redundant network traffic on subsequent startups. +* **Offline Resilience (Fail-Open):** If your application is offline, behind a strict firewall, or the registry server is temporarily unreachable, the validation **fails open**. Tempo logs a warning in the console but continues to grant access to premium features (relying on the local cryptographic expiration of the JWT). + +## 5. Commercialize Your Own Plugin + +Are you a developer who has built an incredibly useful, domain-specific Tempo plugin (e.g., medical billing cycles, legal discovery windows, complex religious calendars)? + +If you would like to monetize your logic without having to build your own licensing infrastructure, **we want to partner with you**. + +Get in touch with us with your proposed code and use-case. If it meets our quality and performance standards, we can publish it as an official Premium Extension secured behind the Tempo License Key system, under a mutually beneficial commercial revenue-sharing arrangement. + diff --git a/packages/plugins/.bin/repl.mts b/packages/plugins/.bin/repl.mts index 4e8e6116..f512d427 100644 --- a/packages/plugins/.bin/repl.mts +++ b/packages/plugins/.bin/repl.mts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import repl from 'node:repl'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { Tempo, enums } from '@magmacomputing/tempo'; @@ -9,19 +10,19 @@ let mockToken = process.env.TEMPO_LICENSE_KEY || undefined; // Fallback manual .env loading since tsx CLI proxy might drop Node native args if (!mockToken) { - try { - const envPath = path.join(__dirname, '../.env'); - if (fs.existsSync(envPath)) { - const envContent = fs.readFileSync(envPath, 'utf8'); - const match = envContent.match(/^TEMPO_LICENSE_KEY=(.*)$/m); - if (match) mockToken = match[1].trim(); - } - } catch (e) { /* ignore */ } + try { + const envPath = path.join(__dirname, '../.env'); + if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, 'utf8'); + const match = envContent.match(/^TEMPO_LICENSE_KEY=(.*)$/m); + if (match) mockToken = match[1].trim(); + } + } catch (e) { /* ignore */ } } if (process.env.TEST_MODE) { - if (!process.env.TEMPO_REVOCATION_URL) process.env.TEMPO_REVOCATION_URL = 'mock'; - if (!process.env.TEMPO_REVOCATION_JWS) process.env.TEMPO_REVOCATION_JWS = '{"revoked":[]}'; + if (!process.env.TEMPO_REVOCATION_URL) process.env.TEMPO_REVOCATION_URL = 'mock'; + if (!process.env.TEMPO_REVOCATION_JWS) process.env.TEMPO_REVOCATION_JWS = '{"revoked":[]}'; } Tempo.init(mockToken ? { license: mockToken } : {}); @@ -32,7 +33,9 @@ console.log(`\n\x1b[38;2;252;194;1m\x1b[1m ⏳ Tempo \x1b[0m\x1b[38;2;45;212;191 const packagesDir = path.join(__dirname, '..'); const plugins = fs.readdirSync(packagesDir, { withFileTypes: true }) .filter(dirent => dirent.isDirectory() && !dirent.name.startsWith('@')) - .map(dirent => dirent.name) + .map(dirent => dirent.name); + +const globals: Record = { Tempo, enums }; for (const plugin of plugins) { let indexPath = path.join(packagesDir, plugin, 'dist/index.mjs'); @@ -48,13 +51,13 @@ for (const plugin of plugins) { for (const key in mod) { if (key.endsWith('Term') || key.endsWith('Plugin') || key.endsWith('Module')) { console.log(`\x1b[32mβœ” Loaded plugin from ${plugin}:\x1b[0m ${key}`); + globals[key] = mod[key]; loadedCount++; } } - if (loadedCount === 0) { + if (loadedCount === 0) console.log(`\x1b[33m⚠ No standard *Term, *Plugin, or *Module exports found in ${plugin} dist.\x1b[0m`); - } } catch (err: any) { console.error(`\x1b[31mβœ– Failed to load plugin ${plugin}:\x1b[0m ${err.message}`); } @@ -65,6 +68,12 @@ for (const plugin of plugins) { console.log(''); -// Expose Tempo globally -Object.assign(globalThis, { Tempo, enums }); +// Inject all globals into globalThis, then open an interactive REPL session +Object.assign(globalThis, globals); + +const globalNames = Object.keys(globals).join(', '); +console.log(`\x1b[90mGlobals: \x1b[0m\x1b[33m${globalNames}\x1b[0m`); +console.log(`\x1b[90mType .exit or Ctrl+C twice to quit.\x1b[0m\n`); +const server = repl.start({ prompt: '\x1b[38;2;252;194;1m⏳ tempo>\x1b[0m ' }); +Object.assign(server.context, globals); diff --git a/packages/plugins/.bin/temporal-polyfill.mts b/packages/plugins/.bin/temporal-polyfill.mts index 3f34b09f..0e2e0e1d 100644 --- a/packages/plugins/.bin/temporal-polyfill.mts +++ b/packages/plugins/.bin/temporal-polyfill.mts @@ -1,5 +1,10 @@ import { Temporal } from '@js-temporal/polyfill'; -Object.assign(globalThis, { Temporal }); - -// console.log('βœ… Temporal Polyfill Loaded:', Temporal.Now.instant().toString()); \ No newline at end of file +if (typeof globalThis.Temporal === 'undefined') { + Object.defineProperty(globalThis, 'Temporal', { + value: Temporal, + enumerable: false, + configurable: true, + writable: true, + }); +} \ No newline at end of file diff --git a/packages/plugins/finance/package.json b/packages/plugins/finance/package.json index 36b60000..13a75fc9 100644 --- a/packages/plugins/finance/package.json +++ b/packages/plugins/finance/package.json @@ -1,6 +1,7 @@ { "name": "@magmacomputing/tempo-plugin-finance", "version": "1.0.0", + "type": "module", "description": "Tempo Community Plugin: Finance namespace and fiscal year utilities", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/packages/plugins/ticker/doc/index.md b/packages/plugins/ticker/doc/index.md index daf5c54a..8cc6da0b 100644 --- a/packages/plugins/ticker/doc/index.md +++ b/packages/plugins/ticker/doc/index.md @@ -8,6 +8,8 @@ This is a premium plugin for the [Tempo](https://github.com/magmacomputing/magma) library that provides a high-performance continuous execution loop (Ticker) based on temporal mathematics. +For details on how to unlock and use these features, see our [License Key Guide](../9-plugins/.app.md). + ::: info High Performance Loop Unlike standard `setInterval` or `requestAnimationFrame`, the Ticker plugin leverages Tempo's robust temporal core to ensure exact sub-millisecond precision, making it ideal for games, complex UI animations, and accurate state synchronization. ::: diff --git a/packages/plugins/tsconfig.shared.json b/packages/plugins/tsconfig.shared.json index c8c67b42..c29c3c5e 100644 --- a/packages/plugins/tsconfig.shared.json +++ b/packages/plugins/tsconfig.shared.json @@ -1,5 +1,9 @@ { "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"], + "paths": {} + }, "include": [ "tsup.shared.ts" ] diff --git a/packages/plugins/tsup.shared.ts b/packages/plugins/tsup.shared.ts index 84226202..0cbe3405 100644 --- a/packages/plugins/tsup.shared.ts +++ b/packages/plugins/tsup.shared.ts @@ -21,6 +21,9 @@ if (fs.existsSync(pkgPath)) { export const sharedConfig: Options = { format: ['esm', 'iife'], globalName: `Magma.plugins.${pkgName}`, + // @magmacomputing/tempo/plugin* is inlined only for IIFE (self-contained browser bundle). + // For ESM the 'esm-external' plugin below re-marks them as external so all plugins + // share a single runtime copy and avoid registerSerializable singleton collisions. noExternal: [/^@magmacomputing\/tempo\/(plugin|plugin-api)$/], outExtension({ format }) { return { @@ -34,6 +37,19 @@ export const sharedConfig: Options = { }, clean: true, esbuildPlugins: [ + { + // For ESM builds, keep @magmacomputing/tempo/plugin* external so all plugins + // share one runtime singleton and avoid registerSerializable collisions. + // IIFE builds are exempt β€” they need fully self-contained bundles. + name: 'esm-external', + setup(build) { + if (build.initialOptions?.format === 'iife') return; + + build.onResolve({ filter: /^@magmacomputing\/tempo\/(plugin|plugin-api)$/ }, () => { + return { external: true }; + }); + } + }, { name: 'auto-inject-version', setup(build) { @@ -110,7 +126,7 @@ export const sharedConfig: Options = { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); const plan = pkg?.tempo?.plan; - // If the package explicitly declares the 'community' pricing tier, skip the license wrapper + // If the package explicitly declares plan: 'community', skip the license wrapper if (plan === 'community') return; // Community plugin, do not apply license wrapper } diff --git a/packages/plugins/vitest.shared.ts b/packages/plugins/vitest.shared.ts index a9b5f3c8..6391b94b 100644 --- a/packages/plugins/vitest.shared.ts +++ b/packages/plugins/vitest.shared.ts @@ -27,8 +27,8 @@ export default defineConfig({ alias: [ { find: /^#library\/(browser|server|common)\/(.*)\.js$/, replacement: resolve(__dirname, '../library/src/$1/$2.ts') }, { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, '../library/src/common/$1.ts') }, - { find: /^#tempo\/plugins\/plugin\.(util|type)\.js$/, replacement: resolve(__dirname, '../tempo/src/plugins/plugin.$1.ts') }, - { find: /^#tempo\/plugins\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, '../tempo/src/plugins/extend/plugin.$1.ts') }, + { find: /^#tempo\/plugin\.(util|type)\.js$/, replacement: resolve(__dirname, '../tempo/src/plugin/plugin.$1.ts') }, + { find: /^#tempo\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, '../tempo/src/plugin/extend/plugin.$1.ts') }, { find: /^#tempo\/(parse|format|mutate|duration)$/, replacement: resolve(__dirname, '../tempo/src/module/module.$1.ts') }, { find: /^#tempo\/core$/, replacement: resolve(__dirname, '../tempo/src/core.index.ts') }, { find: /^#tempo\/module$/, replacement: resolve(__dirname, '../tempo/src/module/module.index.ts') }, diff --git a/packages/tempo/bin/harvest-plugins.mjs b/packages/tempo/bin/harvest-plugins.mjs index ee59b9f4..fb597c05 100644 --- a/packages/tempo/bin/harvest-plugins.mjs +++ b/packages/tempo/bin/harvest-plugins.mjs @@ -8,9 +8,8 @@ const __dirname = path.dirname(__filename); const pluginsDir = path.resolve(__dirname, '../../../packages/plugins'); const targetDir = path.resolve(__dirname, '../doc/9-plugins'); -if (!fs.existsSync(targetDir)) { +if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true }); -} const plugins = fs.readdirSync(pluginsDir); for (const plugin of plugins) { diff --git a/packages/tempo/doc/3-extending-tempo/tempo.extension.md b/packages/tempo/doc/3-extending-tempo/tempo.extension.md index 7d6ad407..de181c5d 100644 --- a/packages/tempo/doc/3-extending-tempo/tempo.extension.md +++ b/packages/tempo/doc/3-extending-tempo/tempo.extension.md @@ -147,4 +147,4 @@ const nextBiz = t.addBusinessDays(2); --- > [!TIP] Need something more complex? -> If you need to build advanced scheduling engines, AsyncGenerators, or precision arithmetic tools that you plan to distribute commercially, check out our **[Premium Plugin Registry β†—](https://magmacomputing.github.io/tempo-plugin-docs/)** for inspiration, or contact Magma Computing Solutions for professional plugin development. +> If you need to build advanced scheduling engines, AsyncGenerators, or precision arithmetic tools that you plan to distribute commercially, check out our **[Plugin Ecosystem β†—](./ecosystem.html)** for inspiration, or contact Magma Computing Solutions for professional plugin development. diff --git a/packages/tempo/doc/8-project-and-support/commercial.md b/packages/tempo/doc/8-project-and-support/commercial.md index 2145dc00..b839e3e2 100644 --- a/packages/tempo/doc/8-project-and-support/commercial.md +++ b/packages/tempo/doc/8-project-and-support/commercial.md @@ -35,9 +35,7 @@ For mission-critical applications, we provide priority support, security auditin In addition to our open-source core, we offer a suite of **Premium Plugins** published directly to the standard public NPM registry (`npmjs.com`), secured by a commercial License Key. These extensions provide advanced, proprietary logic for enterprise-scale requirements. -More details on browsing the 'Tempo Store' will be provided soon... - -For details on how to unlock and use these features, see our [License Key Guide](https://magmacomputing.github.io/tempo-plugin-docs/). +For details on how to unlock and use these features, see our [License Key Guide](../9-plugins/.app.md). --- diff --git a/packages/tempo/doc/9-plugins/.app.md b/packages/tempo/doc/9-plugins/.app.md new file mode 100644 index 00000000..87d7c1b6 --- /dev/null +++ b/packages/tempo/doc/9-plugins/.app.md @@ -0,0 +1,135 @@ +# Tempo Plugin Setup Guide + +This guide explains how to install and activate premium Tempo plugins. + +## 1. Tempo License Registry + +::: warning πŸ”‘ ACTION REQUIRED: Generate Your License Key +To use any premium plugins, you **must** first generate a cryptographic license key (JWT). + +
+ + Tempo License Registry + +
+ πŸ‘‰ Go to the Tempo License Registry πŸ‘ˆ
+ Manage your subscriptions and retrieve your license key. +
+
+::: + +## 2. Installation + +All premium plugins are distributed as public scoped packages on the standard npm registry (`npmjs.com`). This means no special token configuration or registry overrides are required in your `.npmrc`. You can install them directly using your preferred package manager: + +```bash +npm install @magmacomputing/tempo-plugin-ticker +``` + +## 3. Activation + +Although the packages are publicly installable, they require your valid license key at runtime. If no valid license key is detected, the premium features will fail-safe to `undefined`. + +Once you have your key from the registry, you can activate it using any of the following discovery methods: + +### Method A: Environment Variable (Recommended for Node/Server environments) +Set the `TEMPO_LICENSE_KEY` environment variable in your run environment: + +```bash +export TEMPO_LICENSE_KEY="eyJhbGciOiJSUzI1NiJ9..." +``` + +Then in your application, you can simply import Tempo and the plugin via side-effect. Because the license key is automatically discovered from the environment variable, no manual initialization is required, and the side-effect import registers the plugin automatically: + +```javascript +import { Tempo } from '@magmacomputing/tempo'; +import '@magmacomputing/tempo-plugin-ticker'; // Automatically registers TickerModule + +const t = new Tempo(); +console.log(t.tickers); // Unlocked and ready! +``` + +### Method B: Initialization Option (Recommended for applications) +Pass the key explicitly when initializing Tempo. Since static imports are hoisted, calling `Tempo.init()` after a side-effect import will clear any registered terms. To avoid hoisting issues, you can either pass the plugins directly into the `plugins` configuration array (cleanest and recommended), or call `Tempo.extend()` explicitly after initialization: + +#### Option 1: Pass via Init Options (Recommended) +```javascript +import { Tempo } from '@magmacomputing/tempo'; +import { TickerModule } from '@magmacomputing/tempo-plugin-ticker'; + +Tempo.init({ + license: 'eyJhbGciOiJSUzI1NiJ9...', + plugins: [TickerModule] +}); +``` + +#### Option 2: Explicit Extension +```javascript +import { Tempo } from '@magmacomputing/tempo'; +import { TickerModule } from '@magmacomputing/tempo-plugin-ticker'; + +// 1. Initialize core Tempo with your license +Tempo.init({ + license: 'eyJhbGciOiJSUzI1NiJ9...' +}); + +// 2. Register the plugin +Tempo.extend(TickerModule); +``` + +### Method C: Global Context (Fallback for specific bundlers/environments) + +Method C is a browser-native variation of Method A. While Method A targets the process-level environment (`process.env`) available in Node/SSR contexts, Method C sets the key on the JavaScript `globalThis` object (which maps to `window` in browsers), enabling the same auto-discovery behaviour. + +**Use this method in the following scenarios:** + +#### 1. Direct HTML Script Tags (No Bundler) +If you are loading Tempo directly from a CDN or local file using ` + + +``` + +#### 2. Frontend Bundlers without `process.env` Polyfills +Modern browser bundlers (e.g., Vite) do not inject Node's `process` object by default. If you prefer to avoid configuring build-time env replacements or `dotenv` plugins, assign the key to `globalThis` in your entry file *before* importing Tempo: + +```javascript +// entry.js β€” must run before any Tempo import +globalThis.TEMPO_LICENSE_KEY = import.meta.env.VITE_TEMPO_LICENSE_KEY; + +// Now safe to import +import { Tempo } from '@magmacomputing/tempo'; +import '@magmacomputing/tempo-plugin-ticker'; +``` + +#### 3. Micro-frontends / Shared Global Space +In architectures where multiple independently-bundled applications share a single browser tab, set the key once in the host container. All dynamically-loaded sub-applications will then auto-discover it without needing individual configuration: + +```javascript +// host-container.js +globalThis.TEMPO_LICENSE_KEY = 'eyJhbGciOiJSUzI1NiJ9...'; + +// sub-apps loaded later will automatically run in licensed mode +``` + +## 4. Network Requests & Offline Behavior + +To verify license validity and prevent abuse, Tempo's licensing engine performs background synchronization with our revocation registry: + +* **Outbound Request:** When a license key is active, Tempo asynchronously fetches a cryptographically signed revocation list (JWS). +* **Endpoint:** `https://registry.magmacomputing.com.au/tempo/v1/revoked.jws` (useful for configuring Content Security Policies (CSP) or egress firewall rules). +* **Frequency:** The revocation check occurs once every **7 days**. The last-checked state is cached to avoid redundant network traffic on subsequent startups. +* **Offline Resilience (Fail-Open):** If your application is offline, behind a strict firewall, or the registry server is temporarily unreachable, the validation **fails open**. Tempo logs a warning in the console but continues to grant access to premium features (relying on the local cryptographic expiration of the JWT). + +## 5. Commercialize Your Own Plugin + +Are you a developer who has built an incredibly useful, domain-specific Tempo plugin (e.g., medical billing cycles, legal discovery windows, complex religious calendars)? + +If you would like to monetize your logic without having to build your own licensing infrastructure, **we want to partner with you**. + +Get in touch with us with your proposed code and use-case. If it meets our quality and performance standards, we can publish it as an official Premium Extension secured behind the Tempo License Key system, under a mutually beneficial commercial revenue-sharing arrangement. + diff --git a/packages/tempo/doc/9-plugins/ticker.md b/packages/tempo/doc/9-plugins/ticker.md index daf5c54a..8cc6da0b 100644 --- a/packages/tempo/doc/9-plugins/ticker.md +++ b/packages/tempo/doc/9-plugins/ticker.md @@ -8,6 +8,8 @@ This is a premium plugin for the [Tempo](https://github.com/magmacomputing/magma) library that provides a high-performance continuous execution loop (Ticker) based on temporal mathematics. +For details on how to unlock and use these features, see our [License Key Guide](../9-plugins/.app.md). + ::: info High Performance Loop Unlike standard `setInterval` or `requestAnimationFrame`, the Ticker plugin leverages Tempo's robust temporal core to ensure exact sub-millisecond precision, making it ideal for games, complex UI animations, and accurate state synchronization. ::: diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 28f1f51a..64dd549a 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -46,7 +46,9 @@ "dist/engine/engine.*.js", "src/engine/engine.*.ts", "dist/module/module.*.js", - "src/module/module.*.ts" + "src/module/module.*.ts", + "dist/lib/temporal.polyfill.js", + "dist/lib/temporal.library.js" ], "main": "dist/tempo.index.js", "types": "dist/tempo.index.d.ts", diff --git a/packages/tempo/plan/refactor-path-terms.md b/packages/tempo/plan/refactor-path-terms.md new file mode 100644 index 00000000..31d2f287 --- /dev/null +++ b/packages/tempo/plan/refactor-path-terms.md @@ -0,0 +1,7 @@ +What do we think about refactoring the inbuilt Terms for Tempo, down into the plugins/ workspace ? + +Would that +- reduce the build time for tempo ? +- make it easier to manage custom plugin terms +- make it easier to manage default library terms +- still allow for automated imports of the default terms ? diff --git a/packages/tempo/plan/typescript-format-template.md b/packages/tempo/plan/typescript-format-template.md new file mode 100644 index 00000000..07ca10d7 --- /dev/null +++ b/packages/tempo/plan/typescript-format-template.md @@ -0,0 +1,70 @@ +The goal of this step is to introduce 'IDE confirmation' on a User's .format() string. + + +``` typescript +// Define your allowed tokens +type ValidToken = 'dd' | 'mm' | 'yy' | 'yyyy' | 'mon' | 'dd:ord'; + +// A type helper that ensures the string contains a valid pattern +type ValidFormatString = S extends `${string}{${infer Token}}${infer Rest}` + ? Token extends ValidToken + ? ValidFormatString // Valid token, recursively check the rest of the string + : never // Illegal token found! + : string; // Base case: no more tokens found + +// Apply it directly to your class method +class Tempo { + format(formatStr: S & ValidFormatString): string { + // Runtime parsing logic goes here... + return ''; + } +} +``` + If a developer types new Tempo().format('{mon} {dy}'), their IDE will immediately highlight {dy} in red with a compilation error, preventing runtime failures before their code even hits production. + +Do we see value in doing this for parsing input ? +You can do the same thing for anchoring or parsing formats. If your anchor configuration option accepts an ISO date, you can strictly validate the YYYY-MM-DD shape at compile time +``` typescript +type ISODateFormat = `${number}${number}${number}${number}-${number}${number}-${number}${number}`; + +interface TempoOptions { + anchor?: ISODateFormat; // Will reject '2026/10/15' or 'tomorrow' +} +``` + +--- + +## Shipped in 3.9.0 + +The following was implemented in `packages/tempo/src/`: + +- **`TempoFormatTokens` interface** (`tempo.type.ts`) β€” an open, augmentable interface listing + every built-in `{token}` key. Plugins can extend it via declaration merging: + ```ts + declare module '@magmacomputing/tempo' { + interface TempoFormatTokens { 'fiscal.quarter': true } + } + ``` + +- **`ValidateFormat` type** (`tempo.type.ts`) β€” a recursive template-literal type that walks + every `{…}` pair and checks it against `TempoFormatTokens`. Key design decisions: + - `string extends S ? string : …` β€” skips validation when a variable (not a literal) is passed. + - `${_CoreToken}:${string}` β€” all modifier suffixes (`{dd:ord}`, `{tz:zzzzz}`) are accepted. + - `#${string}` β€” term-plugin keys (`{#season.key}`) are always accepted (dynamic, unknowable statically). + - Returns `❌ '{bad}' is not a valid Tempo format token` as the error type (not `never`) so the + bad token name is visible in the IDE tooltip. + +- **`format()` overload** (`module.format.ts`) β€” a new generic overload is inserted before the + `any` catch-all so string literals are resolved against `ValidateFormat` at call-site. + +--- + +## Excluded β€” future work + +| Feature | Reason excluded | Suggested PR | +|---|---|---| +| **`anchor` option validation** | The plan's `ISODateFormat` pattern (`${number}${number}…`) accepts any 8-digit sequence (e.g. `9999-99-99`). True ISO validation requires calendar-aware logic that can't be expressed purely in template literal types. Needs its own design. | 3.10.x | +| **Constructor `value` / parse-input validation** | Parse input is intentionally flexible (strings, numbers, Dates, Temporal objects). Constraining it at the type level would be too narrow and break valid use cases. | Not planned | +| **`registry.tokens` keys in `TempoFormatTokens`** | Custom tokens registered via `Tempo.init({ registry: { tokens: { myKey: fn } } })` are runtime values β€” TypeScript has no way to reflect them into the type system without explicit manual augmentation. Document the augmentation pattern instead. | 3.10.x (docs) | +| **Modifier validation** (`{dd:badModifier}`) | The modifier space (`:ord`, `:lower`, `:upper`, `:title`, `:locale`, `:z` … `:zzzzz`, etc.) varies per token. Per-token modifier unions would be possible but complex β€” a separate dedicated type work item. | 4.0 | +| **`Tempo.FORMAT` alias strings** | `tempo.format('display')` (a pre-registered alias key) works at runtime but the format alias string expands to a template containing tokens β€” validating the *expanded* alias at compile time would require inlining all `FORMAT` values into the type, which duplicates state and risks drift. | 4.0 | \ No newline at end of file diff --git a/packages/tempo/src/module/module.format.ts b/packages/tempo/src/module/module.format.ts index a4a17d78..5c5c2915 100644 --- a/packages/tempo/src/module/module.format.ts +++ b/packages/tempo/src/module/module.format.ts @@ -12,13 +12,26 @@ import { defineInterpreterModule } from '../plugin/plugin.util.js'; import { findTermPlugin } from '../plugin/term/term.util.js'; import type { Tempo } from '../tempo.class.js'; - declare module '../tempo.class.js' { interface Tempo { - /** applies a format to the instance. */ format(options: Intl.DateTimeFormatOptions & { timeZone?: string; calendar?: string }): string; - /** applies a format to the instance. */ format(fmt: BigIntPattern, options?: any): bigint; - /** applies a format to the instance. */ format(fmt: NumericPattern, options?: any): number; - /** applies a format to the instance. */ format(fmt?: any, options?: any): any; + /** applies a format to the instance. */ format(options: Intl.DateTimeFormatOptions & { timeZone?: string; calendar?: string }): string; + /** applies a format to the instance. */ format(fmt: BigIntPattern, options?: any): bigint; + /** applies a format to the instance. */ format(fmt: NumericPattern, options?: any): number; + /** + * Applies a format to the instance. + * Format strings are validated at compile time β€” any unrecognised `{token}` + * will produce an IDE error showing the bad token name. + * @see {@link import('../tempo.type.js').TempoFormatTokens} to extend the token set. + */ + format( + fmt: string extends S + ? S // variable string β€” no validation, accept as-is + : string extends import('../tempo.type.js').ValidateFormat + ? S // ValidateFormat is `string` β†’ all tokens valid, accept + : import('../tempo.type.js').ValidateFormat, // ValidateFormat is an error literal β†’ mismatch forces IDE error + options?: any + ): string | number | bigint; + /** applies a format to the instance. */ format(fmt?: any, options?: any): any; } } diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index bfb9f7ad..3fc0fbc7 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -1625,8 +1625,15 @@ export class Tempo { */ /** @internal */ get #Tempo() { return this.constructor as typeof Tempo; } - /** apply a custom format. */ format(fmt?: any, options?: any): any { return this.#resolve(() => interpret(this, 'FormatModule', () => `{${String(fmt)}}`, false, fmt, options)); } - + // /** apply a custom format. */ format(fmt?: any, options?: any): string | number | bigint { return this.#resolve(() => interpret(this, 'FormatModule', () => `{${String(fmt)}}`, false, fmt, options)); } +/** apply a custom format. */ format( + fmt: string extends S + ? S // variable string β€” no validation, accept as-is + : string extends t.ValidateFormat + ? S // ValidateFormat is `string` β†’ all tokens valid, accept + : t.ValidateFormat, // ValidateFormat is an error literal β†’ mismatch forces IDE error + options?: any + ): string | number | bigint { return this.#resolve(() => interpret(this, 'FormatModule', () => `{${String(fmt)}}`, false, fmt, options)); } /** time duration until another date-time */ until(dateTimeOrOpts?: t.DateTime | t.Options, opts?: t.Options): t.Duration; until(unit: t.Unit, opts?: t.Options): number; @@ -1854,4 +1861,4 @@ export namespace Tempo { export type Element = t.Element; export interface Params extends t.Params { } -} +} \ No newline at end of file diff --git a/packages/tempo/src/tempo.type.ts b/packages/tempo/src/tempo.type.ts index c67eff45..2226b623 100644 --- a/packages/tempo/src/tempo.type.ts +++ b/packages/tempo/src/tempo.type.ts @@ -187,6 +187,80 @@ export type Format = enums.Format; export type FormatRegistry = enums.FormatEnum; export type FormatType = enums.FormatType; +/** + * Augment this interface in your plugin or app to register additional + * format tokens for compile-time validation via `ValidateFormat`. + * + * @example + * // In a plugin's .d.ts: + * declare module '@magmacomputing/tempo' { + * interface TempoFormatTokens { + * 'fiscal.quarter': true; + * } + * } + */ +export interface TempoFormatTokens { + // ── year / week ─────────────────────────────────── + yyyy: true; yy: true; yw: true; + // ── week-of-year ───────────────────────────────── + ww: true; wy: true; yywy: true; yyww: true; + // ── era / eon ───────────────────────────────────── + era: true; eon: true; + // ── month ───────────────────────────────────────── + mm: true; mon: true; mmm: true; + // ── day ─────────────────────────────────────────── + dd: true; day: true; dow: true; wkd: true; www: true; + // ── hour / minute / second ──────────────────────── + hh: true; h24: true; h12: true; mer: true; + mi: true; ss: true; + // ── sub-second ──────────────────────────────────── + ms: true; us: true; ns: true; ff: true; + // ── composite date/time ─────────────────────────── + ymd: true; dmy: true; mdy: true; hms: true; + // ── legacy composites (deprecated, still supported) ── + ymd6: true; dmy6: true; mdy6: true; + // ── timestamp / zone / calendar ─────────────────── + ts: true; nano: true; tz: true; cal: true; +} + +/** All statically-known base token names (derived from TempoFormatTokens). */ +type _CoreToken = keyof TempoFormatTokens; + +/** + * Accepted token shapes inside a `{…}` brace pair: + * - a core token alone: `{yyyy}` + * - a core token with one or more modifiers: `{dd:ord}`, `{tz:zzzzz}` + * - a term-plugin key (always accepted, cannot be statically known): `{#season.key}` + */ +type _ValidToken = _CoreToken | `${_CoreToken}:${string}` | `#${string}`; + +/** + * Compile-time validator for Tempo format strings. + * + * - Recursively walks every `{token}` brace-pair in the string. + * - Accepts any token in `TempoFormatTokens`, modifier suffixes (`{dd:ord}`), + * and term-plugin keys (`{#season.key}`). + * - When `S` is a plain `string` variable (not a literal) validation is skipped + * to avoid false positives. + * - Returns a descriptive error literal when an unknown token is found, which + * surfaces as a readable type error in the IDE. + * + * @example + * // Valid β€” no IDE error + * tempo.format('{www}, {dd} {mmm} {yyyy}'); + * + * // Invalid β€” IDE highlights the bad token + * tempo.format('{mon} {dy}'); + * // ^^ Type '"❌ '{dy}' is not a valid Tempo format token"' + */ +export type ValidateFormat = + string extends S ? string // S widened to string (variable) β€” skip + : S extends `${string}{${infer T}}${infer Rest}` + ? T extends _ValidToken + ? ValidateFormat // valid token, recurse into tail + : `❌ '{${T}}' is not a valid Tempo format token` // bad token β€” surfaced as IDE error + : string; // no more braces β€” valid + export type WEEKDAY = enums.WEEKDAY export type WEEKDAYS = enums.WEEKDAYS export type MONTH = enums.MONTH diff --git a/vitest.config.ts b/vitest.config.ts index 27fe1366..b0bf6da8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -61,8 +61,8 @@ export default defineConfig({ alias: [ { find: /^#library\/(browser|server|common)\/(.*)\.js$/, replacement: path.resolve(__dirname, './packages/library/src/$1/$2.ts') }, { find: /^#library\/(.*)\.js$/, replacement: path.resolve(__dirname, './packages/library/src/common/$1.ts') }, - { find: /^#tempo\/plugins\/plugin\.(util|type)\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/plugins/plugin.$1.ts') }, - { find: /^#tempo\/plugins\/plugin\.(.*)\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/plugins/extend/plugin.$1.ts') }, + { find: /^#tempo\/plugin\.(util|type)\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/plugin/plugin.$1.ts') }, + { find: /^#tempo\/plugin\.(.*)\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/plugin/extend/plugin.$1.ts') }, { find: /^#tempo\/core$/, replacement: path.resolve(__dirname, './packages/tempo/src/core.index.ts') }, { find: /^#tempo\/(parse|format|mutate|duration)$/, replacement: path.resolve(__dirname, './packages/tempo/src/module/module.$1.ts') }, { find: /^#tempo\/support$/, replacement: path.resolve(__dirname, './packages/tempo/src/support/support.index.ts') }, From dae2394340febce34bb18eee3b4574d63d1e7659 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Fri, 17 Jul 2026 09:29:04 +1000 Subject: [PATCH 3/6] fix: update Tempo version to 3.9.1 and document dynamic import patterns for license keys. --- .github/workflows/ci.yml | 13 +- package.json | 2 +- packages/library/package.json | 2 +- packages/plugins/.app/doc/index.md | 36 ++- packages/plugins/ticker/doc/index.md | 2 +- packages/plugins/tsup.shared.ts | 29 +- packages/tempo/CHANGELOG.md | 2 +- packages/tempo/README.md | 2 +- packages/tempo/doc/9-plugins/.app.md | 24 +- packages/tempo/package.json | 2 +- packages/tempo/plan/refactor-path-terms.md | 301 ++++++++++++++++++++- packages/tempo/src/module/module.format.ts | 2 +- packages/tempo/src/tempo.class.ts | 10 +- 13 files changed, 374 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b704acf8..65392be1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,9 @@ on: pull_request: branches: [main] +permissions: + contents: none + jobs: library: name: Library Tests @@ -19,6 +22,8 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: node-version: '22' @@ -35,6 +40,8 @@ jobs: needs: library steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: node-version: '22' @@ -52,6 +59,8 @@ jobs: needs: tempo steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: node-version: '22' @@ -59,7 +68,7 @@ jobs: - name: Install monorepo dependencies run: npm ci - name: Run plugin tests - run: npm test --workspaces --if-present + run: npm test --if-present --workspace=@magmacomputing/tempo-plugin-snap --workspace=@magmacomputing/tempo-plugin-batch --workspace=@magmacomputing/tempo-plugin-finance --workspace=@magmacomputing/tempo-plugin-astro --workspace=@magmacomputing/tempo-plugin-sync --workspace=@magmacomputing/tempo-plugin-ticker working-directory: packages/plugins functions: @@ -69,6 +78,8 @@ jobs: needs: tempo steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-node@v4 with: node-version: '22' diff --git a/package.json b/package.json index dec329b9..d981b75c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tempo-monorepo", - "version": "3.9.0", + "version": "3.9.1", "private": true, "engines": { "node": ">=20.0.0" diff --git a/packages/library/package.json b/packages/library/package.json index 1e7172d7..702667b2 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "3.9.0", + "version": "3.9.1", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", diff --git a/packages/plugins/.app/doc/index.md b/packages/plugins/.app/doc/index.md index 1fcbb96e..8bb6784b 100644 --- a/packages/plugins/.app/doc/index.md +++ b/packages/plugins/.app/doc/index.md @@ -25,7 +25,7 @@ All premium plugins are distributed as public scoped packages on the standard np ```bash npm install @magmacomputing/tempo-plugin-ticker ``` - +7 ## 3. Activation Although the packages are publicly installable, they require your valid license key at runtime. If no valid license key is detected, the premium features will fail-safe to `undefined`. @@ -43,7 +43,7 @@ Then in your application, you can simply import Tempo and the plugin via side-ef ```javascript import { Tempo } from '@magmacomputing/tempo'; -import '@magmacomputing/tempo-plugin-ticker'; // Automatically registers TickerPlugi +import '@magmacomputing/tempo-plugin-ticker'; // Automatically registers TickerPlugin const t = new Tempo(); console.log(t.tickers); // Unlocked and ready! @@ -55,18 +55,18 @@ Pass the key explicitly when initializing Tempo. Since static imports are hoiste #### Option 1: Pass via Init Options (Recommended) ```javascript import { Tempo } from '@magmacomputing/tempo'; -import { TickerPlugi } from '@magmacomputing/tempo-plugin-ticker'; +import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker'; Tempo.init({ license: 'eyJhbGciOiJSUzI1NiJ9...', - plugins: [TickerPlugi ] + plugins: [TickerPlugin] }); ``` #### Option 2: Explicit Extension ```javascript import { Tempo } from '@magmacomputing/tempo'; -import { TickerPlugi } from '@magmacomputing/tempo-plugin-ticker'; +import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker'; // 1. Initialize core Tempo with your license Tempo.init({ @@ -74,7 +74,7 @@ Tempo.init({ }); // 2. Register the plugin -Tempo.extend(TickerPlugi ); +Tempo.extend(TickerPlugin); ``` ### Method C: Global Context (Fallback for specific bundlers/environments) @@ -95,15 +95,29 @@ If you are loading Tempo directly from a CDN or local file using ` + ``` diff --git a/packages/tempo-fns/bin/sync-docs.mjs b/packages/functions/bin/sync-docs.mjs similarity index 100% rename from packages/tempo-fns/bin/sync-docs.mjs rename to packages/functions/bin/sync-docs.mjs diff --git a/packages/tempo-fns/doc/.vitepress/config.mts b/packages/functions/doc/.vitepress/config.mts similarity index 92% rename from packages/tempo-fns/doc/.vitepress/config.mts rename to packages/functions/doc/.vitepress/config.mts index 0919b676..c1a9bc4d 100644 --- a/packages/tempo-fns/doc/.vitepress/config.mts +++ b/packages/functions/doc/.vitepress/config.mts @@ -1,9 +1,9 @@ import { defineConfig } from 'vitepress' export default defineConfig({ - title: "tempo-fns", + title: "functions", description: "The standard functional library for Temporal.", - base: "/magma/tempo-fns/", + base: "/magma/functions/", vite: { build: { target: 'es2022' @@ -18,7 +18,7 @@ export default defineConfig({ } }, themeConfig: { - logo: '/img/tempo-fns-logo.svg', + logo: '/img/functions-logo.svg', nav: [ { text: 'Home', link: '/' }, { text: 'Functions', link: '/functions/' } diff --git a/packages/tempo-fns/doc/CONTRIBUTING.md b/packages/functions/doc/CONTRIBUTING.md similarity index 93% rename from packages/tempo-fns/doc/CONTRIBUTING.md rename to packages/functions/doc/CONTRIBUTING.md index 5a23422a..8a621f1e 100644 --- a/packages/tempo-fns/doc/CONTRIBUTING.md +++ b/packages/functions/doc/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to `tempo-fns` +# Contributing to `functions` -Welcome to `@magmacomputing/tempo-fns`! We are thrilled that you want to contribute to the ecosystem. +Welcome to `@magmacomputing/functions`! We are thrilled that you want to contribute to the ecosystem. This library acts as a granular, tree-shakeable collection of utility functions designed to make working with dates easier for the modern web. Our goal is to provide a seamless bridge for users migrating from legacy tools (like Moment.js or `date-fns`), introducing them to the raw power of the Temporal API and the `@magmacomputing/tempo` ecosystem. @@ -15,7 +15,7 @@ We categorize utility functions into two distinct buckets. You must determine wh ### A. Temporal-Only Functions These functions rely strictly on standard date calculations and duck-typing native properties (like `.day`, `.month`, `.year`). - **Requirement**: They MUST accept raw `Temporal` primitives (or a polymorphic duck-typed interface) alongside `Tempo` instances. -- **Why?**: This allows standard `Temporal` API users to download and use `tempo-fns` *without* being forced to adopt `Tempo`. +- **Why?**: This allows standard `Temporal` API users to download and use `functions` *without* being forced to adopt `Tempo`. - **Example**: `isFirstDayOfMonth(input: TemporalLike | Tempo)` simply checks `input.day === 1`, which natively works for both objects! ### B. Tempo-Enhanced Functions @@ -28,7 +28,7 @@ These functions perform complex business intelligence calculations (like fiscal ## 2. No Polyfills Provided -**CRITICAL RULE**: `tempo-fns` is a pure utility library. **We do not bundle or provide a Temporal polyfill.** +**CRITICAL RULE**: `functions` is a pure utility library. **We do not bundle or provide a Temporal polyfill.** - It is the **consumer's responsibility** to ensure their environment supports Temporal natively (Node 26+, modern browsers) or to provide their own polyfill (like `@js-temporal/polyfill`). - Do not add any polyfills to `dependencies` in `package.json`. diff --git a/packages/tempo-fns/doc/README.md b/packages/functions/doc/README.md similarity index 68% rename from packages/tempo-fns/doc/README.md rename to packages/functions/doc/README.md index bd1a12fa..00c0dd96 100644 --- a/packages/tempo-fns/doc/README.md +++ b/packages/functions/doc/README.md @@ -2,10 +2,10 @@ - Tempo-fns Logo + Tempo-fns Logo -

@magmacomputing/tempo-fns

+

@magmacomputing/functions

The "date-fns" of the Temporal Era

@@ -16,16 +16,16 @@ A massive library of highly granular, fully tree-shakeable utility functions bui This package provides a bridge for developers transitioning from legacy date wrappers (like Moment or `date-fns`) into the modern Temporal API. -### Why `tempo-fns`? -1. **Tree-shakeable**: Import exactly what you need. `import { isFirstDayOfMonth } from '@magmacomputing/tempo-fns'` pulls in zero extra bloat. +### Why `functions`? +1. **Tree-shakeable**: Import exactly what you need. `import { isFirstDayOfMonth } from '@magmacomputing/functions'` pulls in zero extra bloat. 2. **Native Temporal**: Functions accept native `Temporal.ZonedDateTime` and `Temporal.PlainDate` objects. You don't *have* to use the `Tempo` class. -3. **Synergy**: If you *do* use the `Tempo` class wrapper, `tempo-fns` provides advanced business-intelligence utilities that inherently understand Tempo's Terms engine (e.g., `isSameFiscalQuarter`). +3. **Synergy**: If you *do* use the `Tempo` class wrapper, `functions` provides advanced business-intelligence utilities that inherently understand Tempo's Terms engine (e.g., `isSameFiscalQuarter`). ## Usage (NPM / Modern Bundlers) ```typescript import { Tempo } from '@magmacomputing/tempo'; -import { isFirstDayOfMonth, isSameFiscalQuarter } from '@magmacomputing/tempo-fns'; +import { isFirstDayOfMonth, isSameFiscalQuarter } from '@magmacomputing/functions'; const today = new Tempo(); @@ -36,12 +36,12 @@ if (isFirstDayOfMonth(today)) { ## Usage (Static CDN / Browser Global) -If you aren't using a bundler (like Vite, Webpack, or Rollup), we provide a pre-bundled script that exposes a `TempoFns` global object. +If you aren't using a bundler (like Vite, Webpack, or Rollup), we provide a pre-bundled script that exposes a `Functions` global object. ```html - + ``` diff --git a/packages/tempo-fns/doc/functions/business/index.md b/packages/functions/doc/functions/business/index.md similarity index 89% rename from packages/tempo-fns/doc/functions/business/index.md rename to packages/functions/doc/functions/business/index.md index 2bc1e037..41c858aa 100644 --- a/packages/tempo-fns/doc/functions/business/index.md +++ b/packages/functions/doc/functions/business/index.md @@ -16,7 +16,7 @@ function workingHoursUntil( **Example:** ```typescript import { Tempo } from '@magmacomputing/tempo'; -import { workingHoursUntil } from '@magmacomputing/tempo-fns/business'; +import { workingHoursUntil } from '@magmacomputing/functions/business'; Tempo.prototype.workingHoursUntil = workingHoursUntil; @@ -34,7 +34,7 @@ function isSameFiscalQuarter(date1: Tempo, date2: Tempo): boolean; ``` **Example:** ```typescript -import { isSameFiscalQuarter } from '@magmacomputing/tempo-fns/business'; +import { isSameFiscalQuarter } from '@magmacomputing/functions/business'; const d1 = new Tempo('2026-01-15'); const d2 = new Tempo('2026-03-31'); diff --git a/packages/tempo-fns/doc/functions/calendar/index.md b/packages/functions/doc/functions/calendar/index.md similarity index 84% rename from packages/tempo-fns/doc/functions/calendar/index.md rename to packages/functions/doc/functions/calendar/index.md index 622cfac7..862000df 100644 --- a/packages/tempo-fns/doc/functions/calendar/index.md +++ b/packages/functions/doc/functions/calendar/index.md @@ -11,7 +11,7 @@ function getISOWeekOfYear(zdt: Temporal.ZonedDateTime | Tempo): number; ``` **Example:** ```typescript -import { getISOWeekOfYear } from '@magmacomputing/tempo-fns/calendar'; +import { getISOWeekOfYear } from '@magmacomputing/functions/calendar'; getISOWeekOfYear(new Tempo('2026-01-01')); // Returns: 1 ``` @@ -24,7 +24,7 @@ function isFirstDayOfMonth(input: { day: number } | Tempo): boolean; ``` **Example:** ```typescript -import { isFirstDayOfMonth } from '@magmacomputing/tempo-fns/calendar'; +import { isFirstDayOfMonth } from '@magmacomputing/functions/calendar'; isFirstDayOfMonth(new Tempo('2026-03-01')); // Returns: true ``` diff --git a/packages/tempo-fns/doc/functions/duration/index.md b/packages/functions/doc/functions/duration/index.md similarity index 86% rename from packages/tempo-fns/doc/functions/duration/index.md rename to packages/functions/doc/functions/duration/index.md index d8f13d3f..666f6437 100644 --- a/packages/tempo-fns/doc/functions/duration/index.md +++ b/packages/functions/doc/functions/duration/index.md @@ -13,7 +13,7 @@ function normaliseFractionalDurations( ``` **Example:** ```typescript -import { normaliseFractionalDurations } from '@magmacomputing/tempo-fns/duration'; +import { normaliseFractionalDurations } from '@magmacomputing/functions/duration'; normaliseFractionalDurations({ hours: 1.5 }); // Returns: { hours: 1, minutes: 30 } diff --git a/packages/tempo-fns/doc/functions/index.md b/packages/functions/doc/functions/index.md similarity index 77% rename from packages/tempo-fns/doc/functions/index.md rename to packages/functions/doc/functions/index.md index 9952d874..c45322fb 100644 --- a/packages/tempo-fns/doc/functions/index.md +++ b/packages/functions/doc/functions/index.md @@ -1,10 +1,10 @@ # Introduction -Welcome to the `tempo-fns` documentation! This library provides a comprehensive suite of **pure, tree-shakable** functional utilities designed explicitly for the modern ECMAScript ecosystem. +Welcome to the `functions` documentation! This library provides a comprehensive suite of **pure, tree-shakable** functional utilities designed explicitly for the modern ECMAScript ecosystem. ## Built for Temporal -`tempo-fns` was engineered from the ground up to support modern date and time objects. For standard calendar operations, `tempo-fns` consumes and returns native `Temporal` instances. [Temporal](https://tc39.es/proposal-temporal/docs/) is the new global object coming to JavaScript that brings a modern, robust date and time API to the language, resolving decades of frustration with the legacy `Date` object. +`functions` was engineered from the ground up to support modern date and time objects. For standard calendar operations, `functions` consumes and returns native `Temporal` instances. [Temporal](https://tc39.es/proposal-temporal/docs/) is the new global object coming to JavaScript that brings a modern, robust date and time API to the language, resolving decades of frustration with the legacy `Date` object. Because these core utilities expect standard `Temporal` objects (like `Temporal.ZonedDateTime` or `Temporal.PlainDate`), you can use them natively in any modern JavaScript environment without requiring bulky adapters, parsers, or conversion layers. @@ -13,10 +13,10 @@ Because these core utilities expect standard `Temporal` objects (like `Temporal. ## Better Together with Tempo -While `tempo-fns` is a standalone library of pure functions, it was designed in parallel with the [Tempo](https://magmacomputing.github.io/magma/) core library. +While `functions` is a standalone library of pure functions, it was designed in parallel with the [Tempo](https://magmacomputing.github.io/magma/) core library. Tempo provides a powerful, immutable, and fully extensible wrapper over the Temporal API, offering features like semantic period parsing (e.g., `"tomorrow at noon"`), advanced recurring chronologies, and a premium asynchronous `Ticker` engine. -When you use `tempo-fns` *with* Tempo, you get the best of both worlds: the zero-cost instantiation and fluent chainable API of Tempo, seamlessly augmented by the hyper-specific, tree-shakable utility logic of `tempo-fns` (like SLA scheduling, fiscal calculations, or timezone hemisphere detection). +When you use `functions` *with* Tempo, you get the best of both worlds: the zero-cost instantiation and fluent chainable API of Tempo, seamlessly augmented by the hyper-specific, tree-shakable utility logic of `functions` (like SLA scheduling, fiscal calculations, or timezone hemisphere detection). [**Learn more about the Tempo Core Library β†’**](https://magmacomputing.github.io/magma/) diff --git a/packages/tempo-fns/doc/functions/scheduling/Interval.md b/packages/functions/doc/functions/scheduling/Interval.md similarity index 96% rename from packages/tempo-fns/doc/functions/scheduling/Interval.md rename to packages/functions/doc/functions/scheduling/Interval.md index c43d20b3..0a5b185e 100644 --- a/packages/tempo-fns/doc/functions/scheduling/Interval.md +++ b/packages/functions/doc/functions/scheduling/Interval.md @@ -10,7 +10,7 @@ The `Interval` class provides high-performance interval mathematics and represen ```typescript import { Tempo } from '@magmacomputing/tempo'; -import { Interval } from '@magmacomputing/tempo-fns'; +import { Interval } from '@magmacomputing/functions'; const start = new Tempo('2026-07-01'); const end = new Tempo('2026-07-31'); diff --git a/packages/tempo-fns/doc/functions/scheduling/cron.md b/packages/functions/doc/functions/scheduling/cron.md similarity index 87% rename from packages/tempo-fns/doc/functions/scheduling/cron.md rename to packages/functions/doc/functions/scheduling/cron.md index 3ad8eacc..f58c0428 100644 --- a/packages/tempo-fns/doc/functions/scheduling/cron.md +++ b/packages/functions/doc/functions/scheduling/cron.md @@ -1,6 +1,6 @@ # Cron Utils -The `tempo-fns` cron utilities provide zero-dependency, lightweight, and timezone-aware cron scheduling logic natively built for `Tempo`. +The `functions` cron utilities provide zero-dependency, lightweight, and timezone-aware cron scheduling logic natively built for `Tempo`. ## Available Functions @@ -10,7 +10,7 @@ Finds the next occurrence of a cron pattern starting from (and excluding) the cu ```typescript import { Tempo } from '@magmacomputing/tempo'; -import { nextCron } from '@magmacomputing/tempo-fns'; +import { nextCron } from '@magmacomputing/functions'; const start = new Tempo('2026-07-01T08:00:00Z'); // Every 5 minutes between 9 AM and 5 PM, Monday-Friday @@ -25,7 +25,7 @@ Finds the previous occurrence of a cron pattern starting from (and excluding) th ```typescript import { Tempo } from '@magmacomputing/tempo'; -import { prevCron } from '@magmacomputing/tempo-fns'; +import { prevCron } from '@magmacomputing/functions'; const start = new Tempo('2026-07-01T18:00:00Z'); // Every 5 minutes between 9 AM and 5 PM, Monday-Friday @@ -39,7 +39,7 @@ console.log(prev.format('{hhmiss}')); // '17:55:00' Exposes the internal tokenizer, returning a strongly typed `CronSchedule` dictionary representing allowed numeric values per-field. ```typescript -import { parseCron } from '@magmacomputing/tempo-fns'; +import { parseCron } from '@magmacomputing/functions'; const schedule = parseCron('*/5 9-17 * * 1-5'); console.log(schedule.minutes.allowed.has(5)); // true diff --git a/packages/tempo-fns/doc/functions/scheduling/index.md b/packages/functions/doc/functions/scheduling/index.md similarity index 96% rename from packages/tempo-fns/doc/functions/scheduling/index.md rename to packages/functions/doc/functions/scheduling/index.md index 49eb5710..44584537 100644 --- a/packages/tempo-fns/doc/functions/scheduling/index.md +++ b/packages/functions/doc/functions/scheduling/index.md @@ -12,7 +12,7 @@ function prevCron(tempo: Tempo, pattern: string): Tempo; ``` **Example:** ```typescript -import { nextCron } from '@magmacomputing/tempo-fns/scheduling'; +import { nextCron } from '@magmacomputing/functions/scheduling'; import { Tempo } from '@magmacomputing/tempo'; const now = new Tempo('2026-01-01T08:00:00'); diff --git a/packages/tempo-fns/doc/functions/timezone/index.md b/packages/functions/doc/functions/timezone/index.md similarity index 83% rename from packages/tempo-fns/doc/functions/timezone/index.md rename to packages/functions/doc/functions/timezone/index.md index f2e32c01..20e68442 100644 --- a/packages/tempo-fns/doc/functions/timezone/index.md +++ b/packages/functions/doc/functions/timezone/index.md @@ -14,7 +14,7 @@ function isDST( ``` **Example:** ```typescript -import { isDST } from '@magmacomputing/tempo-fns/timezone'; +import { isDST } from '@magmacomputing/functions/timezone'; isDST('2026-07-01T12:00:00', 'America/New_York'); // Returns: true ``` @@ -27,7 +27,7 @@ function getOffsets(timeZone: string, year?: number): number[]; ``` **Example:** ```typescript -import { getOffsets } from '@magmacomputing/tempo-fns/timezone'; +import { getOffsets } from '@magmacomputing/functions/timezone'; getOffsets('Australia/Sydney'); // Returns array of offset changes for the year @@ -41,7 +41,7 @@ function getHemisphere(timeZone?: string): 'N' | 'S' | 'E' | undefined; ``` **Example:** ```typescript -import { getHemisphere } from '@magmacomputing/tempo-fns/timezone'; +import { getHemisphere } from '@magmacomputing/functions/timezone'; getHemisphere('America/New_York'); // Returns: 'N' ``` @@ -54,7 +54,7 @@ function normalizeUtcOffset(zone: string): string; ``` **Example:** ```typescript -import { normalizeUtcOffset } from '@magmacomputing/tempo-fns/timezone'; +import { normalizeUtcOffset } from '@magmacomputing/functions/timezone'; normalizeUtcOffset('UTC+10'); // Returns: '+10:00' ``` diff --git a/packages/tempo-fns/doc/index.md b/packages/functions/doc/index.md similarity index 88% rename from packages/tempo-fns/doc/index.md rename to packages/functions/doc/index.md index 4afc1d3c..33f476a7 100644 --- a/packages/tempo-fns/doc/index.md +++ b/packages/functions/doc/index.md @@ -5,7 +5,7 @@ layout: home @@ -13,8 +13,8 @@ const getStartedUrl = withBase('/functions/')
- -

tempo-fns

+ +

functions

Pure, modern, and tree-shakable date-time utilities.

@@ -142,15 +142,15 @@ const getStartedUrl = withBase('/functions/') ## Designed for the ECMAScript Ecosystem -`tempo-fns` is a comprehensive suite of **pure, functional date-time utilities** built exclusively for the modern JavaScript and TypeScript ecosystem. +`functions` is a comprehensive suite of **pure, functional date-time utilities** built exclusively for the modern JavaScript and TypeScript ecosystem. Designed to seamlessly augment the new [ECMAScript Temporal API](https://tc39.es/proposal-temporal/docs/), this library delivers the advanced calendar, scheduling, and timezone logic that modern web applications demandβ€”without polluting your bundles. -### Why `tempo-fns`? +### Why `functions`? - **100% Pure Functions:** Every utility is a pure, standalone function. No mutated state, no side effects. - **Aggressively Tree-Shakable:** Keep your bundle footprint tiny. Only import exactly what you need. If you only need `isSameFiscalQuarter`, that's the only code that ships to your users. -- **Native Temporal Integration:** No legacy `Date` objects. `tempo-fns` is built from the ground up to consume, calculate, and return native `Temporal` instances. +- **Native Temporal Integration:** No legacy `Date` objects. `functions` is built from the ground up to consume, calculate, and return native `Temporal` instances. -Whether you're building complex financial SLA calculators, recurring cron-based scheduling systems, or simply need robust calendar math, `tempo-fns` provides the modern, professional tooling you need to ship with confidence. +Whether you're building complex financial SLA calculators, recurring cron-based scheduling systems, or simply need robust calendar math, `functions` provides the modern, professional tooling you need to ship with confidence.
diff --git a/packages/tempo-fns/doc/preview.html b/packages/functions/doc/preview.html similarity index 100% rename from packages/tempo-fns/doc/preview.html rename to packages/functions/doc/preview.html diff --git a/packages/tempo-fns/doc/public/img/tempo-fns-logo.svg b/packages/functions/doc/public/img/functions-logo.svg similarity index 100% rename from packages/tempo-fns/doc/public/img/tempo-fns-logo.svg rename to packages/functions/doc/public/img/functions-logo.svg diff --git a/packages/tempo-fns/doc/support.md b/packages/functions/doc/support.md similarity index 91% rename from packages/tempo-fns/doc/support.md rename to packages/functions/doc/support.md index ec042b42..09bcb016 100644 --- a/packages/tempo-fns/doc/support.md +++ b/packages/functions/doc/support.md @@ -4,7 +4,7 @@ We are building a vibrant ecosystem of developers who are passionate about moder ## Contributing -We welcome community contributions! Whether it's a new astrological utility or a fix to an existing cron parser, please check out our [Contributing Guide](https://github.com/magmacomputing/magma/blob/main/packages/tempo-fns/CONTRIBUTING.md) to get started. +We welcome community contributions! Whether it's a new astrological utility or a fix to an existing cron parser, please check out our [Contributing Guide](https://github.com/magmacomputing/magma/blob/main/packages/functions/CONTRIBUTING.md) to get started. ## Issues & Support diff --git a/packages/tempo-fns/img/tempo-fns-logo.svg b/packages/functions/img/functions-logo.svg similarity index 100% rename from packages/tempo-fns/img/tempo-fns-logo.svg rename to packages/functions/img/functions-logo.svg diff --git a/packages/tempo-fns/package.json b/packages/functions/package.json similarity index 88% rename from packages/tempo-fns/package.json rename to packages/functions/package.json index 903fbd30..d6b0cdad 100644 --- a/packages/tempo-fns/package.json +++ b/packages/functions/package.json @@ -16,15 +16,15 @@ "repository": { "type": "git", "url": "git+https://github.com/magmacomputing/magma.git", - "directory": "packages/tempo-fns" + "directory": "packages/functions" }, "type": "module", "sideEffects": false, "main": "dist/index.js", "types": "dist/index.d.ts", - "browser": "dist/tempo-fns.global.js", - "unpkg": "dist/tempo-fns.global.js", - "jsdelivr": "dist/tempo-fns.global.js", + "browser": "dist/functions.global.js", + "unpkg": "dist/functions.global.js", + "jsdelivr": "dist/functions.global.js", "exports": { ".": { "types": "./dist/index.d.ts", @@ -36,7 +36,7 @@ } }, "scripts": { - "test": "vitest run", + "test": "cross-env TEMPO_LICENSE_KEY=\"\" vitest run", "build": "npm run clean && npm run build:esm && npm run build:global", "build:esm": "node ../../node_modules/typescript-7/bin/tsc -b", "build:global": "rollup -c", @@ -60,4 +60,4 @@ "vitepress": "^1.6.4", "vue": "^3.5.39" } -} +} \ No newline at end of file diff --git a/packages/tempo-fns/rollup.config.js b/packages/functions/rollup.config.js similarity index 100% rename from packages/tempo-fns/rollup.config.js rename to packages/functions/rollup.config.js diff --git a/packages/tempo-fns/src/business/README.md b/packages/functions/src/business/README.md similarity index 89% rename from packages/tempo-fns/src/business/README.md rename to packages/functions/src/business/README.md index 2bc1e037..41c858aa 100644 --- a/packages/tempo-fns/src/business/README.md +++ b/packages/functions/src/business/README.md @@ -16,7 +16,7 @@ function workingHoursUntil( **Example:** ```typescript import { Tempo } from '@magmacomputing/tempo'; -import { workingHoursUntil } from '@magmacomputing/tempo-fns/business'; +import { workingHoursUntil } from '@magmacomputing/functions/business'; Tempo.prototype.workingHoursUntil = workingHoursUntil; @@ -34,7 +34,7 @@ function isSameFiscalQuarter(date1: Tempo, date2: Tempo): boolean; ``` **Example:** ```typescript -import { isSameFiscalQuarter } from '@magmacomputing/tempo-fns/business'; +import { isSameFiscalQuarter } from '@magmacomputing/functions/business'; const d1 = new Tempo('2026-01-15'); const d2 = new Tempo('2026-03-31'); diff --git a/packages/tempo-fns/src/business/isSameFiscalQuarter.ts b/packages/functions/src/business/isSameFiscalQuarter.ts similarity index 100% rename from packages/tempo-fns/src/business/isSameFiscalQuarter.ts rename to packages/functions/src/business/isSameFiscalQuarter.ts diff --git a/packages/tempo-fns/src/business/workingHoursUntil.ts b/packages/functions/src/business/workingHoursUntil.ts similarity index 98% rename from packages/tempo-fns/src/business/workingHoursUntil.ts rename to packages/functions/src/business/workingHoursUntil.ts index 31379e3f..aced60cb 100644 --- a/packages/tempo-fns/src/business/workingHoursUntil.ts +++ b/packages/functions/src/business/workingHoursUntil.ts @@ -101,7 +101,7 @@ export const preloadHolidays = async (region?: string, year: number = getTempora remoteHolidaysCache.set(cacheKey, holidays); } catch (error) { - console.warn(`[tempo-fns] Failed to fetch holidays for ${cacheKey}, falling back to none:`, error); + console.warn(`[functions] Failed to fetch holidays for ${cacheKey}, falling back to none:`, error); remoteHolidaysCache.set(cacheKey, []); } } diff --git a/packages/tempo-fns/src/calendar/README.md b/packages/functions/src/calendar/README.md similarity index 84% rename from packages/tempo-fns/src/calendar/README.md rename to packages/functions/src/calendar/README.md index 48a95f12..d2bb2306 100644 --- a/packages/tempo-fns/src/calendar/README.md +++ b/packages/functions/src/calendar/README.md @@ -11,7 +11,7 @@ function getISOWeekOfYear(zdt: Temporal.ZonedDateTime | Tempo): number; ``` **Example:** ```typescript -import { getISOWeekOfYear } from '@magmacomputing/tempo-fns/calendar'; +import { getISOWeekOfYear } from '@magmacomputing/functions/calendar'; getISOWeekOfYear(new Tempo('2026-01-01')); // Returns: 1 ``` @@ -24,7 +24,7 @@ function isFirstDayOfMonth(input: { day: number } | Tempo): boolean; ``` **Example:** ```typescript -import { isFirstDayOfMonth } from '@magmacomputing/tempo-fns/calendar'; +import { isFirstDayOfMonth } from '@magmacomputing/functions/calendar'; isFirstDayOfMonth(new Tempo('2026-03-01')); // Returns: true ``` @@ -37,7 +37,7 @@ function getPublicHolidays(year?: number, region?: string): Promise { ``` **Example:** ```typescript -import { Interval } from '@magmacomputing/tempo-fns/scheduling'; +import { Interval } from '@magmacomputing/functions/scheduling'; import { Temporal } from '@js-temporal/polyfill'; const start = Temporal.Instant.from('2026-01-01T08:00:00Z'); diff --git a/packages/tempo-fns/src/scheduling/cron.ts b/packages/functions/src/scheduling/cron.ts similarity index 100% rename from packages/tempo-fns/src/scheduling/cron.ts rename to packages/functions/src/scheduling/cron.ts diff --git a/packages/tempo-fns/src/support/README.md b/packages/functions/src/support/README.md similarity index 87% rename from packages/tempo-fns/src/support/README.md rename to packages/functions/src/support/README.md index 320acadc..cefb5f6b 100644 --- a/packages/tempo-fns/src/support/README.md +++ b/packages/functions/src/support/README.md @@ -15,4 +15,4 @@ A generic `fetch` wrapper that enforces a timeout via `AbortController`. Robust type guards that rely on `Symbol.toStringTag` duck-typing to safely assert the identity of a date object across cross-bundle boundaries. ### `getTemporal` -A dynamic runtime resolver for the `Temporal` API. Because `tempo-fns` is a utility library, it strictly avoids bundling the massive 800KB `@js-temporal/polyfill`. Instead, it intercepts `globalThis.Temporal` at runtime and gracefully throws a developer-friendly error if a polyfill has not been loaded by the consumer. +A dynamic runtime resolver for the `Temporal` API. Because `functions` is a utility library, it strictly avoids bundling the massive 800KB `@js-temporal/polyfill`. Instead, it intercepts `globalThis.Temporal` at runtime and gracefully throws a developer-friendly error if a polyfill has not been loaded by the consumer. diff --git a/packages/tempo-fns/src/support/assert.ts b/packages/functions/src/support/assert.ts similarity index 100% rename from packages/tempo-fns/src/support/assert.ts rename to packages/functions/src/support/assert.ts diff --git a/packages/tempo-fns/src/support/fetch.ts b/packages/functions/src/support/fetch.ts similarity index 100% rename from packages/tempo-fns/src/support/fetch.ts rename to packages/functions/src/support/fetch.ts diff --git a/packages/tempo-fns/src/support/index.ts b/packages/functions/src/support/index.ts similarity index 100% rename from packages/tempo-fns/src/support/index.ts rename to packages/functions/src/support/index.ts diff --git a/packages/tempo-fns/src/support/intl.ts b/packages/functions/src/support/intl.ts similarity index 100% rename from packages/tempo-fns/src/support/intl.ts rename to packages/functions/src/support/intl.ts diff --git a/packages/tempo-fns/src/support/temporal.ts b/packages/functions/src/support/temporal.ts similarity index 81% rename from packages/tempo-fns/src/support/temporal.ts rename to packages/functions/src/support/temporal.ts index 24560156..8afd5fce 100644 --- a/packages/tempo-fns/src/support/temporal.ts +++ b/packages/functions/src/support/temporal.ts @@ -3,7 +3,7 @@ export type { TemporalType as Temporal }; /** * Resolves the native Temporal API from the global scope at runtime. - * This guarantees that tempo-fns does not accidentally bundle the polyfill, + * This guarantees that functions does not accidentally bundle the polyfill, * while maintaining full type safety. */ export const getTemporal = (): typeof TemporalType => { @@ -13,5 +13,5 @@ export const getTemporal = (): typeof TemporalType => { if (typeof globalThis !== 'undefined' && 'Temporal' in globalThis) return (globalThis as any).Temporal; - throw new Error("[tempo-fns] Temporal API is not available in the global scope. Ensure a polyfill is loaded."); + throw new Error("[functions] Temporal API is not available in the global scope. Ensure a polyfill is loaded."); } diff --git a/packages/tempo-fns/src/timezone/README.md b/packages/functions/src/timezone/README.md similarity index 83% rename from packages/tempo-fns/src/timezone/README.md rename to packages/functions/src/timezone/README.md index f2e32c01..20e68442 100644 --- a/packages/tempo-fns/src/timezone/README.md +++ b/packages/functions/src/timezone/README.md @@ -14,7 +14,7 @@ function isDST( ``` **Example:** ```typescript -import { isDST } from '@magmacomputing/tempo-fns/timezone'; +import { isDST } from '@magmacomputing/functions/timezone'; isDST('2026-07-01T12:00:00', 'America/New_York'); // Returns: true ``` @@ -27,7 +27,7 @@ function getOffsets(timeZone: string, year?: number): number[]; ``` **Example:** ```typescript -import { getOffsets } from '@magmacomputing/tempo-fns/timezone'; +import { getOffsets } from '@magmacomputing/functions/timezone'; getOffsets('Australia/Sydney'); // Returns array of offset changes for the year @@ -41,7 +41,7 @@ function getHemisphere(timeZone?: string): 'N' | 'S' | 'E' | undefined; ``` **Example:** ```typescript -import { getHemisphere } from '@magmacomputing/tempo-fns/timezone'; +import { getHemisphere } from '@magmacomputing/functions/timezone'; getHemisphere('America/New_York'); // Returns: 'N' ``` @@ -54,7 +54,7 @@ function normalizeUtcOffset(zone: string): string; ``` **Example:** ```typescript -import { normalizeUtcOffset } from '@magmacomputing/tempo-fns/timezone'; +import { normalizeUtcOffset } from '@magmacomputing/functions/timezone'; normalizeUtcOffset('UTC+10'); // Returns: '+10:00' ``` diff --git a/packages/tempo-fns/src/timezone/getHemisphere.ts b/packages/functions/src/timezone/getHemisphere.ts similarity index 100% rename from packages/tempo-fns/src/timezone/getHemisphere.ts rename to packages/functions/src/timezone/getHemisphere.ts diff --git a/packages/tempo-fns/src/timezone/getOffsets.ts b/packages/functions/src/timezone/getOffsets.ts similarity index 100% rename from packages/tempo-fns/src/timezone/getOffsets.ts rename to packages/functions/src/timezone/getOffsets.ts diff --git a/packages/tempo-fns/src/timezone/isDST.ts b/packages/functions/src/timezone/isDST.ts similarity index 100% rename from packages/tempo-fns/src/timezone/isDST.ts rename to packages/functions/src/timezone/isDST.ts diff --git a/packages/tempo-fns/src/timezone/normalizeUtcOffset.ts b/packages/functions/src/timezone/normalizeUtcOffset.ts similarity index 100% rename from packages/tempo-fns/src/timezone/normalizeUtcOffset.ts rename to packages/functions/src/timezone/normalizeUtcOffset.ts diff --git a/packages/tempo-fns/test/business/isSameFiscalQuarter.test.ts b/packages/functions/test/business/isSameFiscalQuarter.test.ts similarity index 100% rename from packages/tempo-fns/test/business/isSameFiscalQuarter.test.ts rename to packages/functions/test/business/isSameFiscalQuarter.test.ts diff --git a/packages/tempo-fns/test/business/workingHoursUntil.test.ts b/packages/functions/test/business/workingHoursUntil.test.ts similarity index 100% rename from packages/tempo-fns/test/business/workingHoursUntil.test.ts rename to packages/functions/test/business/workingHoursUntil.test.ts diff --git a/packages/tempo-fns/test/calendar/isFirstDayOfMonth.test.ts b/packages/functions/test/calendar/isFirstDayOfMonth.test.ts similarity index 100% rename from packages/tempo-fns/test/calendar/isFirstDayOfMonth.test.ts rename to packages/functions/test/calendar/isFirstDayOfMonth.test.ts diff --git a/packages/tempo-fns/test/scheduling/cron.test.ts b/packages/functions/test/scheduling/cron.test.ts similarity index 100% rename from packages/tempo-fns/test/scheduling/cron.test.ts rename to packages/functions/test/scheduling/cron.test.ts diff --git a/packages/tempo-fns/test/setup.ts b/packages/functions/test/setup.ts similarity index 100% rename from packages/tempo-fns/test/setup.ts rename to packages/functions/test/setup.ts diff --git a/packages/tempo-fns/test/tsconfig.json b/packages/functions/test/tsconfig.json similarity index 100% rename from packages/tempo-fns/test/tsconfig.json rename to packages/functions/test/tsconfig.json diff --git a/packages/tempo-fns/tsconfig.json b/packages/functions/tsconfig.json similarity index 87% rename from packages/tempo-fns/tsconfig.json rename to packages/functions/tsconfig.json index d5ebbb72..30456753 100644 --- a/packages/tempo-fns/tsconfig.json +++ b/packages/functions/tsconfig.json @@ -15,6 +15,8 @@ "test" ], "references": [ - { "path": "../tempo/src" } + { + "path": "../tempo/src" + } ] -} +} \ No newline at end of file diff --git a/packages/tempo-fns/vitest.config.ts b/packages/functions/vitest.config.ts similarity index 100% rename from packages/tempo-fns/vitest.config.ts rename to packages/functions/vitest.config.ts diff --git a/packages/library/src/common/storage.library.ts b/packages/library/src/common/storage.library.ts index b16880d4..77ac6c47 100644 --- a/packages/library/src/common/storage.library.ts +++ b/packages/library/src/common/storage.library.ts @@ -12,7 +12,7 @@ const mockStorage: Storage = { key: (_index: number) => null, removeItem: (_key: string) => { }, setItem: (_key: string, _value: string) => { }, -}; +} /** safely attempt to retrieve a Storage object from the global context */ const getSafeStorage = (name: 'localStorage' | 'sessionStorage' = 'localStorage'): Storage => { @@ -21,7 +21,7 @@ const getSafeStorage = (name: 'localStorage' | 'sessionStorage' = 'localStorage' } catch { return mockStorage; } -}; +} let storage = context.type === CONTEXT.Browser ? getSafeStorage() diff --git a/packages/tempo/.vitepress/config.ts b/packages/tempo/.vitepress/config.ts index b0f6a219..837fe282 100644 --- a/packages/tempo/.vitepress/config.ts +++ b/packages/tempo/.vitepress/config.ts @@ -31,7 +31,7 @@ export default defineConfig({ { text: 'Guide', link: '/README' }, { text: 'API Reference', link: typedocSidebar[0].items[0].link }, { text: 'Releases', link: '/doc/8-project-and-support/releases/' }, - { text: 'Tempo-Fns', link: 'https://magmacomputing.github.io/magma/tempo-fns/' } + { text: 'Tempo-Fns', link: 'https://magmacomputing.github.io/magma/functions/' } ], sidebar: [ { diff --git a/packages/tempo/doc/3-extending-tempo/tempo.plugin.md b/packages/tempo/doc/3-extending-tempo/tempo.plugin.md index 4a219aba..bbd8759b 100644 --- a/packages/tempo/doc/3-extending-tempo/tempo.plugin.md +++ b/packages/tempo/doc/3-extending-tempo/tempo.plugin.md @@ -133,21 +133,21 @@ if (errorCondition) { This pattern ensures that Tempo remains robust in production environments while providing strict validation during development. -## Alternative: Standalone Functions (`tempo-fns`) +## Alternative: Standalone Functions (`functions`) The JavaScript ecosystem is divided between two architectural preferences: **Chained Fluent APIs** (like Tempo Plugins) and **Pure Standalone Functions** (for aggressive tree-shaking). -To support teams that mandate strict 0kb bundle-impacts and functional programming paradigms, Magma Computing provides the **`@magmacomputing/tempo-fns`** library. +To support teams that mandate strict 0kb bundle-impacts and functional programming paradigms, Magma Computing provides the **`@magmacomputing/functions`** library. ```typescript // The Pure, Tree-shakeable approach: -import { isFirstDayOfMonth } from '@magmacomputing/tempo-fns'; +import { isFirstDayOfMonth } from '@magmacomputing/functions'; import { Tempo } from '@magmacomputing/tempo/core'; if (isFirstDayOfMonth(new Tempo('2024-01-01'))) { ... } ``` -When building complex logic, consider whether it belongs as a core Plugin extension, or as a standalone utility in `tempo-fns` (or a hybrid wrapper of both!). +When building complex logic, consider whether it belongs as a core Plugin extension, or as a standalone utility in `functions` (or a hybrid wrapper of both!). ## Distributing Your Plugin From 7e0e362d4b895038ed87040fc3921143754a025c Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Fri, 17 Jul 2026 13:19:33 +1000 Subject: [PATCH 5/6] PR wrap-up --- .github/workflows/ci.yml | 4 + .gitignore | 2 +- packages/functions/package.json | 6 +- packages/plugins/.setup/README.md | 7 + .../plugins/{.app => .setup}/doc/index.md | 2 +- packages/plugins/ticker/doc/index.md | 2 +- packages/tempo/.vitepress/config.ts | 31 +- packages/tempo/CHANGELOG.md | 13 + packages/tempo/README.md | 13 + packages/tempo/bin/harvest-plugins.mjs | 48 ++- .../doc/1-getting-started/tempo.cookbook.md | 2 +- .../doc/8-project-and-support/commercial.md | 2 +- packages/tempo/doc/9-plugins/.app.md | 149 -------- packages/tempo/doc/9-plugins/astro.md | 77 ---- packages/tempo/doc/9-plugins/batch.md | 58 --- packages/tempo/doc/9-plugins/finance.md | 36 -- packages/tempo/doc/9-plugins/snap.md | 55 --- packages/tempo/doc/9-plugins/sync.md | 62 --- packages/tempo/doc/9-plugins/ticker.md | 353 ------------------ packages/tempo/src/tempo.type.ts | 5 +- packages/tempo/src/tempo.version.ts | 2 +- .../test/instance/instance.format.test.ts | 8 +- 22 files changed, 124 insertions(+), 813 deletions(-) create mode 100644 packages/plugins/.setup/README.md rename packages/plugins/{.app => .setup}/doc/index.md (99%) delete mode 100644 packages/tempo/doc/9-plugins/.app.md delete mode 100644 packages/tempo/doc/9-plugins/astro.md delete mode 100644 packages/tempo/doc/9-plugins/batch.md delete mode 100644 packages/tempo/doc/9-plugins/finance.md delete mode 100644 packages/tempo/doc/9-plugins/snap.md delete mode 100644 packages/tempo/doc/9-plugins/sync.md delete mode 100644 packages/tempo/doc/9-plugins/ticker.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98c3329f..5319296f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,8 @@ jobs: cache: 'npm' - name: Install monorepo dependencies run: npm ci + - name: Build Core + run: npm run build:tempo - name: Run plugin tests run: npm test --if-present --workspace=@magmacomputing/tempo-plugin-snap --workspace=@magmacomputing/tempo-plugin-batch --workspace=@magmacomputing/tempo-plugin-finance --workspace=@magmacomputing/tempo-plugin-astro --workspace=@magmacomputing/tempo-plugin-sync --workspace=@magmacomputing/tempo-plugin-ticker working-directory: packages/plugins @@ -86,5 +88,7 @@ jobs: cache: 'npm' - name: Install monorepo dependencies run: npm ci + - name: Build Core + run: npm run build:tempo - name: Run functions tests run: npm test --workspace=@magmacomputing/functions diff --git a/.gitignore b/.gitignore index b20c11d0..85eb44a4 100644 --- a/.gitignore +++ b/.gitignore @@ -49,7 +49,7 @@ Thumbs.db **/.vitepress/cache/ **/.vitepress/dist/ **/doc/api/ - +**/doc/9-plugins/ # Secrets and credentials .env .env.* diff --git a/packages/functions/package.json b/packages/functions/package.json index d6b0cdad..ec895bd5 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -22,9 +22,9 @@ "sideEffects": false, "main": "dist/index.js", "types": "dist/index.d.ts", - "browser": "dist/functions.global.js", - "unpkg": "dist/functions.global.js", - "jsdelivr": "dist/functions.global.js", + "browser": "dist/tempo-fns.global.js", + "unpkg": "dist/tempo-fns.global.js", + "jsdelivr": "dist/tempo-fns.global.js", "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/packages/plugins/.setup/README.md b/packages/plugins/.setup/README.md new file mode 100644 index 00000000..9a0aeaeb --- /dev/null +++ b/packages/plugins/.setup/README.md @@ -0,0 +1,7 @@ +# Setup Documentation + +> **Note**: This folder is **not** a standard Tempo Plugin. + +This directory is used strictly to house general documentation and configuration content that is harvested and displayed on the GitHub Pages documentation site. + +Specifically, it contains the broader guides (such as Tempo's Premium Licensing strategy) and other core ecosystem references that do not belong to any single specific plugin, but still participate in the VitePress `harvest-plugins` documentation pipeline. diff --git a/packages/plugins/.app/doc/index.md b/packages/plugins/.setup/doc/index.md similarity index 99% rename from packages/plugins/.app/doc/index.md rename to packages/plugins/.setup/doc/index.md index 8bb6784b..f9f020cc 100644 --- a/packages/plugins/.app/doc/index.md +++ b/packages/plugins/.setup/doc/index.md @@ -25,7 +25,7 @@ All premium plugins are distributed as public scoped packages on the standard np ```bash npm install @magmacomputing/tempo-plugin-ticker ``` -7 + ## 3. Activation Although the packages are publicly installable, they require your valid license key at runtime. If no valid license key is detected, the premium features will fail-safe to `undefined`. diff --git a/packages/plugins/ticker/doc/index.md b/packages/plugins/ticker/doc/index.md index 4979cfbc..cb334701 100644 --- a/packages/plugins/ticker/doc/index.md +++ b/packages/plugins/ticker/doc/index.md @@ -8,7 +8,7 @@ This is a premium plugin for the [Tempo](https://github.com/magmacomputing/magma) library that provides a high-performance continuous execution loop (Ticker) based on temporal mathematics. -For details on how to unlock and use these features, see our [License Key Guide](../../../tempo/doc/9-plugins/.app.md). +For details on how to unlock and use these features, see our [License Key Guide](../../.setup/doc/index.md). ::: info High Performance Loop Unlike standard `setInterval` or `requestAnimationFrame`, the Ticker plugin leverages Tempo's robust temporal core to ensure exact sub-millisecond precision, making it ideal for games, complex UI animations, and accurate state synchronization. diff --git a/packages/tempo/.vitepress/config.ts b/packages/tempo/.vitepress/config.ts index 837fe282..aaf500e7 100644 --- a/packages/tempo/.vitepress/config.ts +++ b/packages/tempo/.vitepress/config.ts @@ -31,7 +31,7 @@ export default defineConfig({ { text: 'Guide', link: '/README' }, { text: 'API Reference', link: typedocSidebar[0].items[0].link }, { text: 'Releases', link: '/doc/8-project-and-support/releases/' }, - { text: 'Tempo-Fns', link: 'https://magmacomputing.github.io/magma/functions/' } + { text: 'Functions β†—', link: 'https://magmacomputing.github.io/magma/functions/' } ], sidebar: [ { @@ -110,6 +110,7 @@ export default defineConfig({ { text: 'Project & Support', items: [ + { text: 'License Key Guide', link: '/doc/9-plugins/_setup.index' }, { text: 'Migration Guide', link: '/doc/8-project-and-support/migration-guide' }, { text: 'Release Notes', link: '/doc/8-project-and-support/releases/' }, { text: 'Professional Services', link: '/doc/8-project-and-support/commercial' } @@ -124,6 +125,34 @@ export default defineConfig({ copyright: 'Copyright Β© 2026-present Magma Computing' } }, + markdown: { + config: (md) => { + // Dynamically rewrite source links to the harvested plugin documents + const defaultRender = md.renderer.rules.link_open || function (tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); + } + + md.renderer.rules.link_open = function (tokens, idx, options, env, self) { + const token = tokens[idx]; + const hrefIndex = token.attrIndex('href'); + if (hrefIndex >= 0) { + const hrefAttr = token.attrs![hrefIndex]; + const href = hrefAttr[1]; + // Match paths like ../../../plugins/ticker/doc/index.md or similar + const match = href.match(/(?:\.\.\/)+plugins\/([^\/]+)\/doc\/([^/]+)\.md/); + if (match) { + // Normalise: replace leading dot with underscore (mirrors harvest-plugins.mjs) + // e.g. '.setup' -> '_setup', 'ticker' -> 'ticker' + // Using '_' (not stripping) avoids silent collision between '.setup/' and 'setup/'. + const pluginId = match[1].replace(/^\./, '_'); + // Rewrite the href to point to the absolute harvested VitePress path + hrefAttr[1] = `/doc/9-plugins/${pluginId}.${match[2]}.md`; + } + } + return defaultRender(tokens, idx, options, env, self); + } + } + }, vite: { build: { target: 'es2022', diff --git a/packages/tempo/CHANGELOG.md b/packages/tempo/CHANGELOG.md index 4a570d9b..ddea1533 100644 --- a/packages/tempo/CHANGELOG.md +++ b/packages/tempo/CHANGELOG.md @@ -6,6 +6,19 @@ 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). +## [3.9.1] - 2026-07-17 + +### Added +- **Format Type Validation**: Implemented compile-time IDE validation for Tempo's `.format()` string templates using a recursive template literal type validator (`ValidateFormat`). Augmented this with an extensible `TempoFormatTokens` interface, allowing plugins to register custom tokens (like `{term.quarter}`) without core modifications or type errors. + +### Changed +- **Documentation Navigation**: Restructured the VitePress navigation sidebar to include a dedicated **License Key Guide** section, prominently surfacing the guide for premium plugins. +- **Plugin Harvesting Pipeline**: Refactored the `harvest-plugins.mjs` build script to support modular multi-file plugin documentation. Implemented a robust `_` prefixing strategy (e.g., `.setup` -> `_setup`) and explicit directory collision detection to prevent silent overwrites in the flattened `9-plugins/` VitePress routing namespace. + +### Fixed +- **CI Workspace Resolution**: Fixed a critical build failure in the GitHub Actions CI where isolated test runners (`plugins`, `functions`) failed with `Failed to resolve entry for package "@magmacomputing/tempo"`. The CI now explicitly builds the core packages (`npm run build:tempo`) prior to executing downstream workspace tests. +- **CI Security Hardening**: Upgraded `.github/workflows/ci.yml` by explicitly setting `persist-credentials: false` on all checkout steps and implementing strict top-level `permissions` blocks following the principle of least privilege. + ## [3.9.0] - 2026-07-16 ### Added diff --git a/packages/tempo/README.md b/packages/tempo/README.md index d57563cf..7955dba4 100644 --- a/packages/tempo/README.md +++ b/packages/tempo/README.md @@ -115,6 +115,19 @@ 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/api/)** β€” Detailed technical documentation for every class and method. +--- + +## 🧰 The Tempo Ecosystem + +Tempo is the core library, but the ecosystem extends further: + +| Package | Description | +| :--- | :--- | +| **[`@magmacomputing/tempo`](https://www.npmjs.com/package/@magmacomputing/tempo)** | Core library β€” parsing, formatting, natural-language engine | +| **[`@magmacomputing/tempo-fns`](https://www.npmjs.com/package/@magmacomputing/tempo-fns)** | Pure functional utilities built on native Temporal & Tempo β€” tree-shakeable helpers for calendars, business logic, and scheduling   [![Docs](https://img.shields.io/badge/Docs-VitePress-brightgreen?logo=vitepress&style=flat-square)](https://magmacomputing.github.io/magma/functions/) | +| **[`@magmacomputing/tempo-plugin-*`](https://www.npmjs.com/search?q=%40magmacomputing%2Ftempo-plugin)** | Premium & community plugins β€” Ticker, Astro, Finance, Sync, Snap and more   [![Ecosystem](https://img.shields.io/badge/Browse-Plugin%20Ecosystem-blueviolet?logo=npm&style=flat-square)](./doc/3-extending-tempo/ecosystem.md) | + + --- ## πŸ’¬ Contact & Support diff --git a/packages/tempo/bin/harvest-plugins.mjs b/packages/tempo/bin/harvest-plugins.mjs index fb597c05..6593849d 100644 --- a/packages/tempo/bin/harvest-plugins.mjs +++ b/packages/tempo/bin/harvest-plugins.mjs @@ -8,15 +8,49 @@ const __dirname = path.dirname(__filename); const pluginsDir = path.resolve(__dirname, '../../../packages/plugins'); const targetDir = path.resolve(__dirname, '../doc/9-plugins'); -if (!fs.existsSync(targetDir)) - fs.mkdirSync(targetDir, { recursive: true }); +fs.rmSync(targetDir, { recursive: true, force: true }); +fs.mkdirSync(targetDir, { recursive: true }); + +// Track normalised plugin IDs to detect collisions early (e.g. both '.setup/' and 'setup/' present) +const usedPluginIds = new Map(); // pluginId -> original directory name const plugins = fs.readdirSync(pluginsDir); for (const plugin of plugins) { - const docPath = path.join(pluginsDir, plugin, 'doc', 'index.md'); - if (fs.existsSync(docPath)) { - let content = fs.readFileSync(docPath, 'utf8'); - fs.writeFileSync(path.join(targetDir, `${plugin}.md`), content); - console.log(`Harvested docs for plugin: ${plugin}`); + const docDir = path.join(pluginsDir, plugin, 'doc'); + if (!fs.existsSync(docDir)) continue; + + const files = fs.readdirSync(docDir).filter(f => f.endsWith('.md')); + if (files.length === 0) continue; + + // Normalise plugin directory name for use as a VitePress-safe filename segment: + // Leading dots are replaced with underscores to avoid router issues and prevent + // silent collision between e.g. '.setup/' and 'setup/' (would both strip to 'setup'). + const pluginId = plugin.replace(/^\./, '_'); + + if (usedPluginIds.has(pluginId)) { + const conflict = usedPluginIds.get(pluginId); + throw new Error( + `harvest-plugins: name collision detected!\n` + + ` Both '${conflict}' and '${plugin}' normalise to pluginId '${pluginId}'.\n` + + ` Rename one of the plugin directories to resolve the conflict.` + ); + } + usedPluginIds.set(pluginId, plugin); + + for (const file of files) { + let content = fs.readFileSync(path.join(docDir, file), 'utf8'); + + // Rewrite internal sibling links within the same plugin's doc/ folder + // Maps: ./other.md -> ./[pluginId].other.md + content = content.replace(/\]\(\.\/([^/]+)\.md\)/g, `](./${pluginId}.$1.md)`); + + // Rewrite cross-plugin relative links so they work in VitePress + // Maps: ../../[plugin-dir]/doc/[filename].md -> ./[normalised-pluginId].[filename].md + content = content.replace(/\]\(\.\.\/\.\.\/([^/]+)\/doc\/([^/]+)\.md\)/g, (_m, p, f) => `](./${p.replace(/^\./, '_')}.${f}.md)`); + + const basename = path.basename(file, '.md'); + const outName = `${pluginId}.${basename}.md`; + fs.writeFileSync(path.join(targetDir, outName), content); + console.log(`Harvested docs for plugin: ${plugin} (${file} -> ${outName})`); } } diff --git a/packages/tempo/doc/1-getting-started/tempo.cookbook.md b/packages/tempo/doc/1-getting-started/tempo.cookbook.md index f01fe452..f032b996 100644 --- a/packages/tempo/doc/1-getting-started/tempo.cookbook.md +++ b/packages/tempo/doc/1-getting-started/tempo.cookbook.md @@ -307,7 +307,7 @@ The examples below use the `using` and `await using` syntax, which require **Typ ### Ticker Plugin The Ticker engine is a premium feature. -πŸ‘‰ **Learn More:** [Ticker Plugin Documentation](../9-plugins/ticker.md) +πŸ‘‰ **Learn More:** [Ticker Plugin Documentation](../../../plugins/ticker/doc/index.md) ### Interval-Based Ticker (Recurring Billing) diff --git a/packages/tempo/doc/8-project-and-support/commercial.md b/packages/tempo/doc/8-project-and-support/commercial.md index b839e3e2..a0343abe 100644 --- a/packages/tempo/doc/8-project-and-support/commercial.md +++ b/packages/tempo/doc/8-project-and-support/commercial.md @@ -35,7 +35,7 @@ For mission-critical applications, we provide priority support, security auditin In addition to our open-source core, we offer a suite of **Premium Plugins** published directly to the standard public NPM registry (`npmjs.com`), secured by a commercial License Key. These extensions provide advanced, proprietary logic for enterprise-scale requirements. -For details on how to unlock and use these features, see our [License Key Guide](../9-plugins/.app.md). +For details on how to unlock and use these features, see our [License Key Guide](../../../plugins/.setup/doc/index.md). --- diff --git a/packages/tempo/doc/9-plugins/.app.md b/packages/tempo/doc/9-plugins/.app.md deleted file mode 100644 index 57436c6e..00000000 --- a/packages/tempo/doc/9-plugins/.app.md +++ /dev/null @@ -1,149 +0,0 @@ -# Tempo Plugin Setup Guide - -This guide explains how to install and activate premium Tempo plugins. - -## 1. Tempo License Registry - -::: warning πŸ”‘ ACTION REQUIRED: Generate Your License Key -To use any premium plugins, you **must** first generate a cryptographic license key (JWT). - -
- - Tempo License Registry - -
- πŸ‘‰ Go to the Tempo License Registry πŸ‘ˆ
- Manage your subscriptions and retrieve your license key. -
-
-::: - -## 2. Installation - -All premium plugins are distributed as public scoped packages on the standard npm registry (`npmjs.com`). This means no special token configuration or registry overrides are required in your `.npmrc`. You can install them directly using your preferred package manager: - -```bash -npm install @magmacomputing/tempo-plugin-ticker -``` - -## 3. Activation - -Although the packages are publicly installable, they require your valid license key at runtime. If no valid license key is detected, the premium features will fail-safe to `undefined`. - -Once you have your key from the registry, you can activate it using any of the following discovery methods: - -### Method A: Environment Variable (Recommended for Node/Server environments) -Set the `TEMPO_LICENSE_KEY` environment variable in your run environment: - -```bash -export TEMPO_LICENSE_KEY="eyJhbGciOiJSUzI1NiJ9..." -``` - -Then in your application, you can simply import Tempo and the plugin via side-effect. Because the license key is automatically discovered from the environment variable, no manual initialization is required, and the side-effect import registers the plugin automatically: - -```javascript -import { Tempo } from '@magmacomputing/tempo'; -import '@magmacomputing/tempo-plugin-ticker'; // Automatically registers TickerModule - -const t = new Tempo(); -console.log(t.tickers); // Unlocked and ready! -``` - -### Method B: Initialization Option (Recommended for applications) -Pass the key explicitly when initializing Tempo. Since static imports are hoisted, calling `Tempo.init()` after a side-effect import will clear any registered terms. To avoid hoisting issues, you can either pass the plugins directly into the `plugins` configuration array (cleanest and recommended), or call `Tempo.extend()` explicitly after initialization: - -#### Option 1: Pass via Init Options (Recommended) -```javascript -import { Tempo } from '@magmacomputing/tempo'; -import { TickerModule } from '@magmacomputing/tempo-plugin-ticker'; - -Tempo.init({ - license: 'eyJhbGciOiJSUzI1NiJ9...', - plugins: [TickerModule] -}); -``` - -#### Option 2: Explicit Extension -```javascript -import { Tempo } from '@magmacomputing/tempo'; -import { TickerModule } from '@magmacomputing/tempo-plugin-ticker'; - -// 1. Initialize core Tempo with your license -Tempo.init({ - license: 'eyJhbGciOiJSUzI1NiJ9...' -}); - -// 2. Register the plugin -Tempo.extend(TickerModule); -``` - -### Method C: Global Context (Fallback for specific bundlers/environments) - -Method C is a browser-native variation of Method A. While Method A targets the process-level environment (`process.env`) available in Node/SSR contexts, Method C sets the key on the JavaScript `globalThis` object (which maps to `window` in browsers), enabling the same auto-discovery behaviour. - -**Use this method in the following scenarios:** - -#### 1. Direct HTML Script Tags (No Bundler) -If you are loading Tempo directly from a CDN or local file using ` - - -``` - -#### 2. Frontend Bundlers without `process.env` Polyfills -Modern browser bundlers (e.g., Vite) do not inject Node's `process` object by default. If you prefer to avoid configuring build-time env replacements or `dotenv` plugins, assign the key to `globalThis` in your entry file and use dynamic imports to ensure the key is set before Tempo initializes: - -```javascript -// entry.js -globalThis.TEMPO_LICENSE_KEY = import.meta.env.VITE_TEMPO_LICENSE_KEY; - -// Use dynamic imports so the key is set before Tempo's static initializer runs -const { Tempo } = await import('@magmacomputing/tempo'); -const { TickerModule } = await import('@magmacomputing/tempo-plugin-ticker'); - -Tempo.init({ plugins: [TickerModule] }); -``` - -Alternatively, pass the license key explicitly via `Tempo.init()` after your static imports: - -```javascript -import { Tempo } from '@magmacomputing/tempo'; -import { TickerModule } from '@magmacomputing/tempo-plugin-ticker'; - -Tempo.init({ - license: import.meta.env.VITE_TEMPO_LICENSE_KEY, - plugins: [TickerModule] -}); -``` - -#### 3. Micro-frontends / Shared Global Space -In architectures where multiple independently-bundled applications share a single browser tab, set the key once in the host container. All dynamically-loaded sub-applications will then auto-discover it without needing individual configuration: - -```javascript -// host-container.js -globalThis.TEMPO_LICENSE_KEY = 'eyJhbGciOiJSUzI1NiJ9...'; - -// sub-apps loaded later will automatically run in licensed mode -``` - -## 4. Network Requests & Offline Behavior - -To verify license validity and prevent abuse, Tempo's licensing engine performs background synchronization with our revocation registry: - -* **Outbound Request:** When a license key is active, Tempo asynchronously fetches a cryptographically signed revocation list (JWS). -* **Endpoint:** `https://registry.magmacomputing.com.au/tempo/v1/revoked.jws` (useful for configuring Content Security Policies (CSP) or egress firewall rules). -* **Frequency:** The revocation check occurs once every **7 days**. The last-checked state is cached to avoid redundant network traffic on subsequent startups. -* **Offline Resilience (Fail-Open):** If your application is offline, behind a strict firewall, or the registry server is temporarily unreachable, the validation **fails open**. Tempo emits a debug-level log entry but continues to grant access to premium features (relying on the local cryptographic expiration of the JWT). - -## 5. Commercialize Your Own Plugin - -Are you a developer who has built an incredibly useful, domain-specific Tempo plugin (e.g., medical billing cycles, legal discovery windows, complex religious calendars)? - -If you would like to monetize your logic without having to build your own licensing infrastructure, **we want to partner with you**. - -Get in touch with us with your proposed code and use-case. If it meets our quality and performance standards, we can publish it as an official Premium Extension secured behind the Tempo License Key system, under a mutually beneficial commercial revenue-sharing arrangement. - diff --git a/packages/tempo/doc/9-plugins/astro.md b/packages/tempo/doc/9-plugins/astro.md deleted file mode 100644 index d8371908..00000000 --- a/packages/tempo/doc/9-plugins/astro.md +++ /dev/null @@ -1,77 +0,0 @@ -![Tempo Plugin](/plugin-logo.svg) - -# @magmacomputing/tempo-plugin-astro - -

- npm version npm peer dependency version License TypeScript Ready -

- -This is a Community plugin for the [Tempo](https://github.com/magmacomputing/magma) library that calculates the exact astronomical season (Equinoxes and Solstices) for any date using the **Jean Meeus polynomial algorithm**. - -> [!NOTE] -> **Mean-Polynomial Approximation (Ch. 27)** -> This plugin specifically implements the mean-polynomial calculation from Chapter 27 of Meeus' *Astronomical Algorithms*. To keep the library extremely lightweight, it omits the massive periodic correction tables required for exact apparent calculations. It is strictly enforced to support the mathematical range of **-1000 to +3000 AD**. - -Because it is a true astronomical calculation rather than a fixed calendar date, it precisely determines the exact minute the sun crosses the celestial equator. It is also **hemisphere-aware**: by configuring your Tempo instance with a `sphere` (e.g., `sphere: 'south'`), the plugin accurately flips the Vernal Equinox from Spring to Autumn. - -::: info Meteorological vs Astronomical -Unlike Tempo's built-in **Meteorological** `season` Term β€” which rigidly snaps to the 1st day of calendar months β€” this **Astronomical** plugin calculates the dynamic, true solar boundaries that shift slightly year-over-year. -::: -## Installation - -```bash -npm install @magmacomputing/tempo-plugin-astro -``` - -## Usage - -```typescript -import { Tempo } from '@magmacomputing/tempo'; -import { AstroTerm } from '@magmacomputing/tempo-plugin-astro'; - -// Pass the plugin to `Tempo.init` to register it into the runtime. -Tempo.init({ - plugins: [AstroTerm] -}); - -const t = new Tempo('2026-03-20'); - -// Get the Astronomical Event mapping -console.log(t.term.astro); -// Output: 'Vernal' -``` - -### Response Payload - -When resolving the term, the plugin intelligently returns the correct astronomical event and its corresponding traditional season based on your configured hemisphere (`sphere`): - -```javascript -{ - key: 'Vernal', // Flips to 'Autumnal' if sphere is set to 'south' - season: 'Spring', // Flips to 'Autumn' if sphere is set to 'south' - sphere: 'north', // Flips to 'south' if sphere is set to 'south' - event: 'Equinox', - group: 'astronomy', - year: 2026, - month: 3, - day: 20, - hour: 14, - minute: 45, - second: 0 -} -``` - -You can also access the full metadata object containing the sub-second precision fields via the `astronomy` term: - -```typescript -console.log(t.term.astronomy); -// Output: { key: 'Vernal', group: 'astronomy', year: 2026, month: 3, day: 20, hour: 14, minute: 45, ... } -``` - -::: tip Did you know? -**Seasons:** `t.term.astronomy.season` returns the *Astronomical* season calculated by the precise timing of solstices and equinoxes. This will often differ from `t.term.season.key` in the core library, which uses standard Meteorological/Civil calendar boundaries (e.g., 1st of the month). -::: - -## Licensing - -This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required. diff --git a/packages/tempo/doc/9-plugins/batch.md b/packages/tempo/doc/9-plugins/batch.md deleted file mode 100644 index 628f2c69..00000000 --- a/packages/tempo/doc/9-plugins/batch.md +++ /dev/null @@ -1,58 +0,0 @@ -![Tempo Plugin](/plugin-logo.svg) - -# @magmacomputing/tempo-plugin-batch - -

- npm version npm peer dependency version License TypeScript Ready -

- -This is a Community plugin for the [Tempo](https://github.com/magmacomputing/magma) library that parallelizes massive epoch mutation tasks across worker threads utilizing lock-free `SharedArrayBuffer` architecture for extreme throughput. - -::: tip Perfect For -Heavy data ETL pipelines, massive IoT telemetry ingestion, financial ledger chronometrics, and any parallel bulk date-processing workloads. -::: - -## Installation - -```bash -npm install @magmacomputing/tempo-plugin-batch -``` - -## Usage - -```typescript -import { Tempo } from '@magmacomputing/tempo'; -import { BatchPlugin } from '@magmacomputing/tempo-plugin-batch'; - -Tempo.init({ - plugins: [BatchPlugin] -}); - -// Assume `epochs` is a massive array of integers representing timestamps -const epochs = [1700000000000, 1700000001000, /* ... millions more ... */]; - -// Mutate millions of dates concurrently using the worker pool -// The engine automatically splits the payload and offloads to workers! -const batchResult = await Tempo.batch(epochs, { weeks: 1 }); - -console.log(batchResult); // Returns an array of mutated timestamp integers -``` - -### Rehydration - -By default, `Tempo.batch` returns an array of primitive `number` timestamps to maximize throughput over the thread boundary. If you need fully-fledged `Tempo` objects back, pass `{ rehydrate: true }`: - -```typescript -// Returns an array of Tempo instances instead of integers -const tempoInstances = await Tempo.batch(epochs, { weeks: 1 }, { rehydrate: true }); -``` - -### Graceful Degradation - -If the host environment does not support `SharedArrayBuffer` (or if it is blocked by CORS/COOP headers in the browser), the orchestrator intelligently and transparently falls back to using traditional `postMessage` structural cloning chunks to ensure execution never halts. - -## Licensing - -This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required. - -For commercial licensing options, please contact Magma Computing. diff --git a/packages/tempo/doc/9-plugins/finance.md b/packages/tempo/doc/9-plugins/finance.md deleted file mode 100644 index 067a7f14..00000000 --- a/packages/tempo/doc/9-plugins/finance.md +++ /dev/null @@ -1,36 +0,0 @@ -# @magmacomputing/tempo-plugin-finance - -

- npm version npm peer dependency version License TypeScript Ready -

- -A specialized namespace plugin for Tempo that provides fiscal year and financial date utilities. - -## Installation - -```bash -npm install @magmacomputing/tempo-plugin-finance -``` - -## Usage - -```typescript -import { Tempo } from '@magmacomputing/tempo'; -import { FinancePlugin } from '@magmacomputing/tempo-plugin-finance'; - -// Register the namespace -Tempo.extend(FinancePlugin); - -const t = new Tempo('2024-07-01'); - -// Evaluate static properties -console.log(t.finance.fiscalQuarter); // 3 -console.log(t.finance.taxYear); // 2024 - -// Evaluate functional closures -console.log(t.finance.isFiscalYearStart()); // false -``` - -## Licensing - -This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required. diff --git a/packages/tempo/doc/9-plugins/snap.md b/packages/tempo/doc/9-plugins/snap.md deleted file mode 100644 index d167810a..00000000 --- a/packages/tempo/doc/9-plugins/snap.md +++ /dev/null @@ -1,55 +0,0 @@ -![Tempo Plugin](/plugin-logo.svg) - -# @magmacomputing/tempo-plugin-snap - -

- npm version npm peer dependency version License TypeScript Ready -

- -A Community plugin for the [Tempo](https://github.com/magmacomputing/magma) library that provides robust time rounding and snapping functionality (e.g., snapping to the nearest 15 minutes or 1 hour block) for calendar and scheduling applications. - -By default, the plugin effortlessly snaps dates to a configurable minute-interval. This is particularly useful when building UI components like time-pickers, ensuring data boundaries align perfectly with application logic. - -### πŸ’‘ User Notes: Why Sub-Second Snapping? -While `hours` and `minutes` cover most UI use cases, sub-second precision (`ms`, `us`, `ns`) is invaluable for: -1. **Telemetry & Log Aggregation**: Snapping high-frequency jittery timestamps to the nearest `100ms` or `500ms` bucket for cleaner charts and analysis. -2. **Video & Audio Synchronization**: Multimedia frame rates require precise timing. Snap to the nearest `16ms` (approx 60fps) or `40ms` (25fps) to align data points with visual boundaries. -3. **Database & API Normalization**: Truncating or snapping Tempo's native nanosecond precision to the nearest `ms` before sending payloads ensures your local application state perfectly matches remote databases that don't support microseconds. -4. **Performance Benchmarking**: Grouping execution times into buckets (e.g., nearest `10ms`) for histograms. - -## Installation - -```bash -npm install @magmacomputing/tempo-plugin-snap -``` - -## Usage - -```typescript -import { Tempo } from '@magmacomputing/tempo'; -import { SnapPlugin } from '@magmacomputing/tempo-plugin-snap'; - -// Pass the plugin to `Tempo.init` to register it into the runtime. -Tempo.init({ - plugins: [SnapPlugin] -}); - -const t = new Tempo('2026-06-01T14:08:00Z'); - -// Snaps to the nearest 15 minutes by default -const snapped = t.snap(); -console.log(snapped.format('{hh}:{mi}')); // "14:15" - -// Or explicitly provide units and intervals -const snapHour = t.snap({ hh: 1 }); -const snapSecond = t.snap({ ss: 30 }); -const snapMs = t.snap({ ms: 100 }); - -// Force snapping direction instead of standard rounding -const snapUp = t.snap({ mi: 15, direction: 'up' }); -const snapDown = t.snap({ mi: 15, direction: 'down' }); -``` - -## Licensing - -This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required. diff --git a/packages/tempo/doc/9-plugins/sync.md b/packages/tempo/doc/9-plugins/sync.md deleted file mode 100644 index 7cea6e96..00000000 --- a/packages/tempo/doc/9-plugins/sync.md +++ /dev/null @@ -1,62 +0,0 @@ -![Tempo Plugin](/plugin-logo.svg) - -# @magmacomputing/tempo-plugin-sync - -

- npm version npm peer dependency version License TypeScript Ready -

- -This is a Community plugin for the [Tempo](https://github.com/magmacomputing/magma) library that provides lock-free, nanosecond-accurate cross-thread time synchronization using `SharedArrayBuffer` and `Atomics`. - -::: tip Perfect For -High-frequency trading platforms, real-time multiplayer game servers, distributed microservice tracing, and extreme-precision scientific telemetry. -::: - -## Installation - -```bash -npm install @magmacomputing/tempo-plugin-sync -``` - -## Usage - -```typescript -import { Tempo } from '@magmacomputing/tempo'; -import { SyncPlugin } from '@magmacomputing/tempo-plugin-sync'; - -Tempo.init({ - plugins: [SyncPlugin] -}); - -// Master Thread: Start the clock -const clock = Tempo.sync.startClock({ updateIntervalMs: 1 }); -const buffer = clock.buffer; // Pass this SharedArrayBuffer to your workers -``` - -### Reading from Worker Threads - -To read the synchronized time from inside a worker thread, pass the `SharedArrayBuffer` via `workerData` and instantiate an `AtomicReader`. - -```typescript -// worker.ts -import { workerData } from 'node:worker_threads'; -import { AtomicReader } from '@magmacomputing/tempo-plugin-sync'; - -// Hydrate the reader using the master buffer -const reader = new AtomicReader(workerData.buffer); - -// 1. Get raw milliseconds (O(1) Atomic Read) -const ms = reader.now(); - -// 2. Get high-precision BigInt nanoseconds -const ns = reader.nowNano(); - -// 3. Hydrate a brand new Tempo instance with exact precision -const t = reader.getTempo(); -``` - -## Licensing - -This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required. - -For commercial licensing options, please contact Magma Computing. diff --git a/packages/tempo/doc/9-plugins/ticker.md b/packages/tempo/doc/9-plugins/ticker.md deleted file mode 100644 index 8cc6da0b..00000000 --- a/packages/tempo/doc/9-plugins/ticker.md +++ /dev/null @@ -1,353 +0,0 @@ -![Tempo Plugin](/plugin-logo.svg) - -# @magmacomputing/tempo-plugin-ticker - -

- npm version npm peer dependency version License TypeScript Ready -

- -This is a premium plugin for the [Tempo](https://github.com/magmacomputing/magma) library that provides a high-performance continuous execution loop (Ticker) based on temporal mathematics. - -For details on how to unlock and use these features, see our [License Key Guide](../9-plugins/.app.md). - -::: info High Performance Loop -Unlike standard `setInterval` or `requestAnimationFrame`, the Ticker plugin leverages Tempo's robust temporal core to ensure exact sub-millisecond precision, making it ideal for games, complex UI animations, and accurate state synchronization. -::: - -## Installation - -```bash -npm install @magmacomputing/tempo-plugin-ticker -``` - -## Usage - -To use the Ticker, pass the plugin to `Tempo.init` to ensure it registers securely alongside your license. - -```typescript -import { Tempo } from '@magmacomputing/tempo'; -import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker'; - -// A valid premium license token is required to use this plugin -Tempo.init({ - license: 'YOUR_LICENSE_TOKEN', - plugins: [TickerPlugin] -}); - -// You can now access Ticker-based execution loops through the Tempo API: -const ticker = Tempo.ticker({ seconds: 1 }); -``` - -### Direct Access -If you need to access the [Reporting & Registry](#reporting-registry) API (like `Ticker.active`), you should import the `Ticker` namespace: - -```typescript -import { Ticker } from '@magmacomputing/tempo-plugin-ticker'; - -console.log(Ticker.active); -``` - -## πŸš€ Key Features - -The Ticker supports a unified **Options** object, enabling professional resource management and semantic duration-based intervals. - -### 1. Semantic Intervals (Duration Objects) -Instead of raw numeric seconds, you can use `DurationLike` objects or shorthand keys for clarity. This is especially powerful for variable-length intervals like **months**. - -```typescript -// Pulse exactly once a month -await using monthly = Tempo.ticker({ months: 1 }); - -// You can also use highly compact shorthand keys -await using concise = Tempo.ticker({ hh: 1, mi: 30 }); // every 1h 30m - -// Pulse every time a new #quarter begins -await using quarterly = Tempo.ticker({ '#quarter': 1 }); -``` - -### 2. Term-Based Intervals -Ticker intervals can now be driven by any registered **Term**. This is powerful for syncing with business cycles or daily shifts. - -```typescript -// Pulse at the start of every 'morning', 'afternoon', etc. -using shiftTicker = Tempo.ticker({ '#period': 1 }, (t) => { - console.log(`New period started: ${t.term.per}`); -}); -``` - -### 3. Stop Conditions (Resource Management) -Prevent memory leaks and runaway processes by setting a built-in termination condition. - -```typescript -// Pattern A: Stop after exactly 5 ticks (defaults to 1-second interval) -using tickerA = Tempo.ticker({ limit: 5 }, (t) => console.log(t)); - -// Pattern B: Stop when a specific virtual time is reached (Inclusive) -using tickerB = Tempo.ticker({ - seconds: 10, // Plural DurationLike property - until: '2024-12-25T12:00:00' -}, (t) => console.log(t)); - -// Pattern C: Stop immediately (Limit: 0 is strictly honored) -using tickerC = Tempo.ticker({ limit: 0 }); -``` - -### 4. Virtual Clock (Seeding) -To create a **Virtual Clock** that increments from a specific point rather than using the system time, use the `seed` option: - -```typescript -// Starts at '2024-01-01', then increments by 1 day per pulse -await using daily = Tempo.ticker({ - days: 1, - seed: '2024-01-01' -}); -``` - -### 5. Backwards Tickers (Countdowns) -By providing a **negative** interval, you can create a Ticker that moves backwards in time. - -```typescript -// Count down from 10 seconds, moving backwards 1s at a time -using countdown = Tempo.ticker({ seconds: -1, seed: "00:00:10" }, (t, stop) => { - console.log(t.format('{ss}')); - if (t.ss === 0) stop(); -}); -``` - -## Usage Patterns - -### 1. Resource Management (Recommended) - -Using the `using` and `await using` keywords ensures that Tickers are automatically stopped when they go out of scope. - -```typescript -// Pattern A: Automatic cleanup for callback-based ticker -{ - using ticker = Tempo.ticker((t) => render(t)); // Defaults to a 1-second pulse -} // interval stops automatically here - -// Pattern B: Automatic cleanup for async generator -{ - await using ticker = Tempo.ticker(1); - for await (const t of ticker) { - if (done) break; - } -} // generator is closed and interval stops here -``` - -### 2. Manual Control (Programmatic Stop) - -If you are not using the `using` or `await using` keywords, or if you need to stop the Ticker from outside its own loop (e.g., in a separate event handler), you can manually call the `stop()` method on the Ticker object. - -```typescript -// Pattern A: Stop a callback-based ticker -const tickerA = Tempo.ticker(1, (t) => console.log(t)); -// ... later -tickerA.stop(); - -// Pattern B: Stop an async generator externally -const tickerB = Tempo.ticker(1); - -(async () => { - for await (const t of tickerB) { - console.log(t.toString()); - } - console.log('Ticker has been gracefully stopped.'); -})(); - -// Close the generator from somewhere else -setTimeout(() => { - tickerB.stop(); -}, 5000); -``` -### 3. Event Listeners (.on) -Instead of (or in addition to) the constructor callback, you can register listeners for the `'pulse'`, `'stop'`, and `'catch'` events. -All listeners use the same callback signature: `(t, stop) => {}`. - -```typescript -const ticker = Tempo.ticker(1); -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, 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. - -```typescript -const ticker = Tempo.ticker({ seconds: 1 }); // Still has a 1s duration logic -// ... -ticker.pulse(); // Manually advance and notify listeners -``` - -## 🧟 Zombie Tickers (Warning) {#zombie-tickers-warning} - -In a Node.js environment, `Tempo.ticker()` uses background timers (`setTimeout`) to drive its pulses. If you do not explicitly stop a Ticker, it becomes a **"Zombie Ticker"** that continues to run indefinitely, even if the variable that created it has gone out of scope. - -### The Risks: -- **Process Hangs**: Node.js will not exit a process if there are active timers. Undisposed Tickers are a common cause of "mysterious hangs" at the end of test runs. -- **Test Inconsistency**: Leaked Tickers can continue to fire while subsequent tests are running, leading to flaky assertions and "impossible" state changes. -- **Memory Leaks**: Each active Ticker maintains closures that prevent garbage collection of the `Tempo` instance and its listeners. - -### The Solution: -Always use the **Disposer Pattern** (`using` or `await using`) or a `try...finally` block to guarantee cleanup: - -```typescript -// βœ…βœ… BEST: Automatic cleanup via 'using' -{ - using ticker = Tempo.ticker(1); - // ... logic ... -} // Stays clean: ticker stopped automatically here - -// βœ… GOOD: Manual cleanup in finally block (Required for captured variables) -let ticker; -try { - ticker = Tempo.ticker(1, (t) => { ... }); - // ... assertions ... -} finally { - ticker?.stop(); // Prevents "Zombie Tickers" even if assertions fail -} -``` - -::: warning -If you are using `const` or `let` without a `finally` block, an assertion failure will skip the `stop()` call, leaving a live timer in the event loop. Always prefer the `using` keyword or `try...finally` for industrial-grade resource management. -::: - -### `Ticker` Object -The object returned by `Tempo.ticker()` (or an instance of the `Ticker` class) implements the following interface: - -| Method / Property | Description | -| :--- | :--- | -| `on(event, cb)` | Registers a listener for the `'pulse'`, `'stop'`, or `'catch'` events. | -| `pulse()` | Manually triggers a pulse, advances state, and notifies listeners. Returns the new `Tempo`. | -| `info` | Read-only getter returning `{ next, ticks, limit, interval, stopped }`. | -| `stop()` | Stops the Ticker, clears active timers, and immediately resolves any pending async iteration Promises. | -| `[Symbol.dispose]` | Standard cleanup for `using` blocks. | -| `[Symbol.asyncDispose]` | Standard async cleanup for `await using` blocks. | -| `[Symbol.asyncIterator]` | Standard async iteration support (for `for await` loops). | - -## Reporting & Registry {#reporting-registry} - -The `Ticker` class maintains a static registry of all currently active Tickers. This is useful for debugging, monitoring, or cleanup checks. - -### `Ticker.active` -A static getter that returns an array of [`Ticker.Snapshot`](#tickersnapshot) objects for all active (non-stopped) Tickers. - -```typescript -import { Ticker } from '@magmacomputing/tempo-plugin-ticker'; - -// Get a report of all running tickers -const reports = Ticker.active; - -reports.forEach(({ ticker, next, ticks }) => { - console.log(`Ticker ${ticker} next pulse: ${next}, ticks so far: ${ticks}`); -}); -``` - -#### `Ticker.Snapshot` -```typescript -type Snapshot = { - ticker: Instance; // The Ticker instance (Proxy) itself - next: Tempo; // The next Tempo value to be emitted - ticks: number; // Number of pulses emitted so far - limit?: number; // The configured limit (if any) - interval: object; // The duration-based interval - stopped: boolean; // Whether the ticker is stopped -} -``` - -## 🎯 One-Shot Ticker (Meeting Alerts) - -You can use the Ticker as a "one-shot" timer for specific events by simply specifying a **seed** value. This is perfect for setting up a single alert (e.g., for a meeting) that cleans itself up immediately after firing. - -::: tip -**Seed-Only Logic**: Providing a `seed` (as a string or in an options object) without any other duration-based keys (`seconds`, `minutes`, etc.) or a `limit` implies a `limit: 1`. - -Effectively, `Tempo.ticker('Fri 10am')` and `Tempo.ticker({ seed: 'Fri 10am' })` and `Tempo.ticker({ seed: 'Fri 10am', limit: 1 })` are all treated as one-shot Tickers. - -**Inclusive Boundaries**: Termination conditions (`limit` and `until`) are **inclusive**. A Ticker with `limit: 1` will pulse exactly once before stopping. -::: - -```typescript -// Pattern A: Implicit one-shot via string seed -Tempo.ticker('Friday 10am', (t) => { - console.log(`Meeting alert: ${t.format('{hh}:{mi}')}`); -}); - -// Pattern B: Explicit one-shot via options -const event = { meeting: 'Friday 10am' }; - -Tempo.ticker({ - seed: { value: 'meeting', event } -}, (t) => { - console.log(`Meeting alert: ${t.format('{hh}:{mi}')}`); -}); -``` - -::: warning -**Future Seeds**: If the `seed` is in the future, the Ticker will remain dormant (waiting) until that time is reached. **Most Tickers emit an initial pulse immediately** (at the `seed` time or "now"), but a future seed will delay that first pulse until the specified time. -::: - -::: danger -**Persistence**: Ticker timers exist only **in-memory**. If the driving process (e.g., Node.js) terminates, any scheduled future pulses (including those from future seeds) are lost. For critical long-term scheduling, consider an external persistent job runner. -::: - -::: warning -While `limit: 1` handles the stop condition automatically, always remember that if you are using long-running Tickers without a limit, you **must** use the [Disposer Pattern](#zombie-tickers-warning) or manual `stop()` to avoid memory leaks and zombie processes. -::: - -## 🧭 Advanced: Syncing Multiple Clocks - -If you need to show multiple timezones on a dashboard, avoid creating multiple Tickers. Instead, use a single **Master Ticker** to drive all views. This prevents "drift" between the clocks and is much more efficient. - -### Using Signals (Recommended) - -Signals (from Preact, Solid, or Vue) are perfect for this "one source, many views" pattern. - -```typescript -// 1. Master source of truth -const now = signal(new Tempo()); - -// 2. Drive the master from a single ticker -using _ = Tempo.ticker(1, (t) => now.value = t); - -// 3. Derived timezones update automatically and stay 100% in sync -const sydney = computed(() => now.value.set({ timeZone: 'Australia/Sydney' })); -const london = computed(() => now.value.set({ timeZone: 'Europe/London' })); -``` - -### Using Async Generators (Framework-Agnostic) - -If you are not using a reactive framework, you can use the same pattern with an `AsyncGenerator` to derive all clocks from a single pulse. - -```typescript -// One generator, one interval, zero drift. -await using master = Tempo.ticker(1); - -for await (const t of master) { - const clocks = { - sydney: t.set({ timeZone: 'Australia/Sydney' }), - ny: t.set({ timeZone: 'America/New_York' }), - london: t.set({ timeZone: 'Europe/London' }) - }; - - renderDashboard(clocks); -} -``` - -## Licensing - -This is a **Premium** plugin. Usage requires an active, cryptographically signed Tempo license token with the `ticker` scope enabled. - -::: tip -
- - Tempo License Registry - -
- πŸ‘‰ Go to the Tempo License Registry πŸ‘ˆ
- Manage your subscriptions and retrieve your license key. -
-
-::: diff --git a/packages/tempo/src/tempo.type.ts b/packages/tempo/src/tempo.type.ts index 2226b623..571e38e5 100644 --- a/packages/tempo/src/tempo.type.ts +++ b/packages/tempo/src/tempo.type.ts @@ -232,7 +232,7 @@ type _CoreToken = keyof TempoFormatTokens; * - a core token with one or more modifiers: `{dd:ord}`, `{tz:zzzzz}` * - a term-plugin key (always accepted, cannot be statically known): `{#season.key}` */ -type _ValidToken = _CoreToken | `${_CoreToken}:${string}` | `#${string}`; +type _ValidToken = _CoreToken | `${_CoreToken}:${string}` | `#${string}` | `term.${string}`; /** * Compile-time validator for Tempo format strings. @@ -255,10 +255,11 @@ type _ValidToken = _CoreToken | `${_CoreToken}:${string}` | `#${string}`; */ export type ValidateFormat = string extends S ? string // S widened to string (variable) β€” skip + : S extends `${string}\\${string}` ? string // escaped braces present β€” skip validation : S extends `${string}{${infer T}}${infer Rest}` ? T extends _ValidToken ? ValidateFormat // valid token, recurse into tail - : `❌ '{${T}}' is not a valid Tempo format token` // bad token β€” surfaced as IDE error + : `❌ '{${T}}' is not a valid Tempo format token` // bad token β€” surfaced as IDE error : string; // no more braces β€” valid export type WEEKDAY = enums.WEEKDAY diff --git a/packages/tempo/src/tempo.version.ts b/packages/tempo/src/tempo.version.ts index 0e940446..bcd131b2 100644 --- a/packages/tempo/src/tempo.version.ts +++ b/packages/tempo/src/tempo.version.ts @@ -5,4 +5,4 @@ * ⚠️ This file is auto-updated by `npm run build:version` (see `bin/update-version.mjs`). * Do NOT edit manually β€” your changes will be overwritten on the next build. */ -export const TEMPO_VERSION = '3.9.0'; +export const TEMPO_VERSION = '3.9.1'; diff --git a/packages/tempo/test/instance/instance.format.test.ts b/packages/tempo/test/instance/instance.format.test.ts index 9c3ee940..166e317c 100644 --- a/packages/tempo/test/instance/instance.format.test.ts +++ b/packages/tempo/test/instance/instance.format.test.ts @@ -60,7 +60,7 @@ describe(`${label} format method`, () => { // The format module should extract locale, shift the ZonedDateTime, and gracefully // fallback or pass through the options without crashing on the Temporal timeZone mismatch constraint. - const result = t.format(arabicConfig); + const result = t.format(arabicConfig as any); expect(result).toBe('Ψ§Ω„Ψ£Ψ±Ψ¨ΨΉΨ§Ψ‘ΨŒ Ω’Ω₯ Ψ―ΩŠΨ³Ω…Ψ¨Ψ± Ω’Ω Ω’Ω€'); }); @@ -77,7 +77,7 @@ describe(`${label} format method`, () => { day: 'numeric' } - const result = t.format(japaneseConfig); + const result = t.format(japaneseConfig as any); expect(result).toBe('δ»€ε’Œ6εΉ΄12月25ζ—₯'); }); @@ -93,8 +93,8 @@ describe(`${label} format method`, () => { } } } as Tempo.Options - expect(t.format('{myDay}', options)).toBe('19'); - expect(t.format('{myDay:upper}', options)).toBe('20 UPPER'); + expect(t.format('{myDay}' as string, options)).toBe('19'); + expect(t.format('{myDay:upper}' as string, options)).toBe('20 UPPER'); }); test('prevents core tokens from being overridden by registry', () => { From 395df436b2a0b44e6adbf1d0bcd0c819eeaf1b0f Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Fri, 17 Jul 2026 16:18:13 +1000 Subject: [PATCH 6/6] PR last review (for today) --- .github/workflows/ci.yml | 4 ++-- .github/workflows/deploy-docs.yml | 2 +- packages/functions/CHANGELOG.md | 2 +- packages/functions/README.md | 16 +++++++------- packages/functions/bin/sync-docs.mjs | 2 +- packages/functions/doc/.vitepress/config.mts | 2 +- packages/functions/doc/CONTRIBUTING.md | 8 +++---- packages/functions/doc/README.md | 16 +++++++------- .../functions/doc/functions/business/index.md | 4 ++-- .../functions/doc/functions/calendar/index.md | 18 ++++++++++++++-- .../functions/doc/functions/duration/index.md | 2 +- packages/functions/doc/functions/index.md | 11 ++++------ .../doc/functions/scheduling/Interval.md | 2 +- .../doc/functions/scheduling/cron.md | 6 +++--- .../doc/functions/scheduling/index.md | 16 +++++++------- .../functions/doc/functions/timezone/index.md | 8 +++---- packages/functions/doc/index.md | 10 ++++----- packages/functions/doc/support.md | 2 +- packages/functions/rollup.config.js | 2 +- packages/functions/src/business/README.md | 4 ++-- .../src/business/workingHoursUntil.ts | 2 +- packages/functions/src/calendar/README.md | 6 +++--- packages/functions/src/duration/README.md | 2 +- .../duration/normaliseFractionalDurations.ts | 3 ++- packages/functions/src/scheduling/README.md | 4 ++-- packages/functions/src/scheduling/cron.ts | 20 +++++++++--------- packages/functions/src/timezone/README.md | 8 +++---- packages/functions/src/timezone/isDST.ts | 6 +++++- .../src/timezone/normalizeUtcOffset.ts | 2 +- .../library/src/common/storage.library.ts | 4 ++-- packages/plugins/finance/src/index.ts | 4 ++-- packages/plugins/snap/src/index.ts | 4 ++-- packages/tempo/.vitepress/config.ts | 4 ++-- packages/tempo/bin/harvest-plugins.mjs | 4 ++-- packages/tempo/src/module/module.format.ts | 4 ++-- packages/tempo/src/tempo.class.ts | 1 + packages/tempo/src/tempo.type.ts | 6 ++++++ .../test/instance/instance.format.test.ts | 21 ++++++++++++------- 38 files changed, 136 insertions(+), 106 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5319296f..81465082 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: branches: [main] permissions: - contents: none + contents: read jobs: library: @@ -91,4 +91,4 @@ jobs: - name: Build Core run: npm run build:tempo - name: Run functions tests - run: npm test --workspace=@magmacomputing/functions + run: npm test --workspace=@magmacomputing/tempo-fns diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index aa297121..9a32cd7e 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -52,7 +52,7 @@ jobs: run: npm run docs:build --workspace=@magmacomputing/tempo - name: Build Tempo-Fns Docs - run: npm run docs:build --workspace=@magmacomputing/functions + run: npm run docs:build --workspace=@magmacomputing/tempo-fns - name: Merge Documentation Sites run: | diff --git a/packages/functions/CHANGELOG.md b/packages/functions/CHANGELOG.md index 69b1900d..8beebe24 100644 --- a/packages/functions/CHANGELOG.md +++ b/packages/functions/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2026-07-12 ### Added -- **Initial Release**: Launch of `@magmacomputing/functions`, providing a comprehensive suite of pure functional utilities designed for the JavaScript Temporal API. +- **Initial Release**: Launch of `@magmacomputing/tempo-fns`, providing a comprehensive suite of pure functional utilities designed for the JavaScript Temporal API. - **Business Functions**: `workingHoursUntil`, `isSameFiscalQuarter`. - **Calendar Functions**: `getISOWeekOfYear`, `isFirstDayOfMonth`. - **Duration Functions**: `normaliseFractionalDurations`. diff --git a/packages/functions/README.md b/packages/functions/README.md index 00c0dd96..a3494a26 100644 --- a/packages/functions/README.md +++ b/packages/functions/README.md @@ -2,30 +2,30 @@ - Tempo-fns Logo + @magmacomputing/tempo-fns -

@magmacomputing/functions

+

@magmacomputing/tempo-fns

The "date-fns" of the Temporal Era

-A massive library of highly granular, fully tree-shakeable utility functions built directly on top of the JavaScript Temporal API. +A library of highly granular, fully tree-shakeable utility functions built directly on top of the JavaScript Temporal API. This package provides a bridge for developers transitioning from legacy date wrappers (like Moment or `date-fns`) into the modern Temporal API. -### Why `functions`? -1. **Tree-shakeable**: Import exactly what you need. `import { isFirstDayOfMonth } from '@magmacomputing/functions'` pulls in zero extra bloat. +### Why `tempo-fns`? +1. **Tree-shakeable**: Import exactly what you need. `import { isFirstDayOfMonth } from '@magmacomputing/tempo-fns'` pulls in zero extra bloat. 2. **Native Temporal**: Functions accept native `Temporal.ZonedDateTime` and `Temporal.PlainDate` objects. You don't *have* to use the `Tempo` class. -3. **Synergy**: If you *do* use the `Tempo` class wrapper, `functions` provides advanced business-intelligence utilities that inherently understand Tempo's Terms engine (e.g., `isSameFiscalQuarter`). +3. **Synergy**: If you *do* use the `Tempo` class wrapper, `tempo-fns` provides advanced business-intelligence utilities that inherently understand Tempo's Terms engine (e.g., `isSameFiscalQuarter`). ## Usage (NPM / Modern Bundlers) ```typescript import { Tempo } from '@magmacomputing/tempo'; -import { isFirstDayOfMonth, isSameFiscalQuarter } from '@magmacomputing/functions'; +import { isFirstDayOfMonth, isSameFiscalQuarter } from '@magmacomputing/tempo-fns'; const today = new Tempo(); @@ -39,7 +39,7 @@ if (isFirstDayOfMonth(today)) { If you aren't using a bundler (like Vite, Webpack, or Rollup), we provide a pre-bundled script that exposes a `Functions` global object. ```html - + +