diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 05df835c..81465082 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,31 +8,87 @@ env:
on:
push:
- branches:
- - main
- - release-c-layout-order-planner
+ branches: [main]
pull_request:
- branches:
- - main
- - release-c-layout-order-planner
+ branches: [main]
+
+permissions:
+ contents: read
jobs:
- test:
+ library:
+ name: Library Tests
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - 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
+ with:
+ persist-credentials: false
+ - 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
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ 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
+ functions:
+ name: Functions Tests
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ needs: tempo
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ 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/tempo-fns
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 3c2d2941..9a32cd7e 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -5,7 +5,7 @@ on:
# Trigger only on changes to tempo package or this workflow
paths:
- 'packages/tempo/**'
- - 'packages/tempo-fns/**'
+ - 'packages/functions/**'
- '.github/workflows/deploy-docs.yml'
- 'package.json'
- 'package-lock.json'
@@ -56,8 +56,8 @@ jobs:
- name: Merge Documentation Sites
run: |
- mkdir -p packages/tempo/.vitepress/dist/tempo-fns
- cp -r packages/tempo-fns/doc/.vitepress/dist/* packages/tempo/.vitepress/dist/tempo-fns/
+ mkdir -p packages/tempo/.vitepress/dist/functions
+ cp -r packages/functions/doc/.vitepress/dist/* packages/tempo/.vitepress/dist/functions/
- name: Setup Pages
uses: actions/configure-pages@v4
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/package-lock.json b/package-lock.json
index 2ca7ec29..8f82610c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "tempo-monorepo",
- "version": "3.9.0",
+ "version": "3.9.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tempo-monorepo",
- "version": "3.9.0",
+ "version": "3.9.1",
"workspaces": [
"packages/*",
"packages/plugins/*"
@@ -1130,7 +1130,7 @@
"link": true
},
"node_modules/@magmacomputing/tempo-fns": {
- "resolved": "packages/tempo-fns",
+ "resolved": "packages/functions",
"link": true
},
"node_modules/@magmacomputing/tempo-plugin-astro": {
@@ -3434,57 +3434,57 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.5.38",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz",
- "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==",
+ "version": "3.5.40",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz",
+ "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.7",
- "@vue/shared": "3.5.38",
+ "@vue/shared": "3.5.40",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.5.38",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz",
- "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==",
+ "version": "3.5.40",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz",
+ "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/compiler-core": "3.5.38",
- "@vue/shared": "3.5.38"
+ "@vue/compiler-core": "3.5.40",
+ "@vue/shared": "3.5.40"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.5.38",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz",
- "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==",
+ "version": "3.5.40",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz",
+ "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.7",
- "@vue/compiler-core": "3.5.38",
- "@vue/compiler-dom": "3.5.38",
- "@vue/compiler-ssr": "3.5.38",
- "@vue/shared": "3.5.38",
+ "@vue/compiler-core": "3.5.40",
+ "@vue/compiler-dom": "3.5.40",
+ "@vue/compiler-ssr": "3.5.40",
+ "@vue/shared": "3.5.40",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
- "postcss": "^8.5.15",
+ "postcss": "^8.5.19",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.5.38",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz",
- "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==",
+ "version": "3.5.40",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz",
+ "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.38",
- "@vue/shared": "3.5.38"
+ "@vue/compiler-dom": "3.5.40",
+ "@vue/shared": "3.5.40"
}
},
"node_modules/@vue/devtools-api": {
@@ -3524,57 +3524,55 @@
}
},
"node_modules/@vue/reactivity": {
- "version": "3.5.38",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz",
- "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==",
+ "version": "3.5.40",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz",
+ "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/shared": "3.5.38"
+ "@vue/shared": "3.5.40"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.5.38",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz",
- "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==",
+ "version": "3.5.40",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz",
+ "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.38",
- "@vue/shared": "3.5.38"
+ "@vue/reactivity": "3.5.40",
+ "@vue/shared": "3.5.40"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.5.38",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz",
- "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==",
+ "version": "3.5.40",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz",
+ "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.38",
- "@vue/runtime-core": "3.5.38",
- "@vue/shared": "3.5.38",
+ "@vue/reactivity": "3.5.40",
+ "@vue/runtime-core": "3.5.40",
+ "@vue/shared": "3.5.40",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.5.38",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz",
- "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==",
+ "version": "3.5.40",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz",
+ "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/compiler-ssr": "3.5.38",
- "@vue/shared": "3.5.38"
- },
- "peerDependencies": {
- "vue": "3.5.38"
+ "@vue/compiler-ssr": "3.5.40",
+ "@vue/runtime-dom": "3.5.40",
+ "@vue/shared": "3.5.40"
}
},
"node_modules/@vue/shared": {
- "version": "3.5.38",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
- "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
+ "version": "3.5.40",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz",
+ "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==",
"dev": true,
"license": "MIT"
},
@@ -7937,9 +7935,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.15",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
- "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "version": "8.5.19",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
+ "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
"dev": true,
"funding": [
{
@@ -10253,17 +10251,17 @@
}
},
"node_modules/vue": {
- "version": "3.5.38",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz",
- "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==",
+ "version": "3.5.40",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz",
+ "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.38",
- "@vue/compiler-sfc": "3.5.38",
- "@vue/runtime-dom": "3.5.38",
- "@vue/server-renderer": "3.5.38",
- "@vue/shared": "3.5.38"
+ "@vue/compiler-dom": "3.5.40",
+ "@vue/compiler-sfc": "3.5.40",
+ "@vue/runtime-dom": "3.5.40",
+ "@vue/server-renderer": "3.5.40",
+ "@vue/shared": "3.5.40"
},
"peerDependencies": {
"typescript": "*"
@@ -11029,9 +11027,29 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "packages/functions": {
+ "name": "@magmacomputing/tempo-fns",
+ "version": "0.1.0",
+ "license": "MIT",
+ "devDependencies": {
+ "@rollup/plugin-node-resolve": "^16.0.3",
+ "rollup": "^4.62.2",
+ "vitepress": "^1.6.4",
+ "vue": "^3.5.39"
+ },
+ "peerDependencies": {
+ "@js-temporal/polyfill": "^0.5.1",
+ "@magmacomputing/tempo": "^3.7.0"
+ },
+ "peerDependenciesMeta": {
+ "@magmacomputing/tempo": {
+ "optional": true
+ }
+ }
+ },
"packages/library": {
"name": "@magmacomputing/library",
- "version": "3.9.0",
+ "version": "3.9.1",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.1"
@@ -11049,7 +11067,7 @@
},
"packages/plugins/astro": {
"name": "@magmacomputing/tempo-plugin-astro",
- "version": "2.1.1",
+ "version": "2.1.2",
"license": "MIT",
"devDependencies": {},
"peerDependencies": {
@@ -11072,7 +11090,7 @@
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
- "@magmacomputing/tempo": "^3.6.1",
+ "@magmacomputing/tempo": "^3.8.0",
"vitest": "^1.0.0"
},
"peerDependencies": {
@@ -11349,7 +11367,7 @@
},
"packages/plugins/snap": {
"name": "@magmacomputing/tempo-plugin-snap",
- "version": "1.3.0",
+ "version": "1.3.1",
"license": "MIT",
"devDependencies": {},
"peerDependencies": {
@@ -11367,7 +11385,7 @@
},
"packages/tempo": {
"name": "@magmacomputing/tempo",
- "version": "3.9.0",
+ "version": "3.9.1",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.1"
@@ -11391,157 +11409,6 @@
"engines": {
"node": ">=20.0.0"
}
- },
- "packages/tempo-fns": {
- "name": "@magmacomputing/tempo-fns",
- "version": "0.1.0",
- "license": "MIT",
- "devDependencies": {
- "@rollup/plugin-node-resolve": "^16.0.3",
- "rollup": "^4.62.2",
- "vitepress": "^1.6.4",
- "vue": "^3.5.39"
- },
- "peerDependencies": {
- "@js-temporal/polyfill": "^0.5.1",
- "@magmacomputing/tempo": "^3.7.0"
- },
- "peerDependenciesMeta": {
- "@magmacomputing/tempo": {
- "optional": true
- }
- }
- },
- "packages/tempo-fns/node_modules/@vue/compiler-core": {
- "version": "3.5.39",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz",
- "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.7",
- "@vue/shared": "3.5.39",
- "entities": "^7.0.1",
- "estree-walker": "^2.0.2",
- "source-map-js": "^1.2.1"
- }
- },
- "packages/tempo-fns/node_modules/@vue/compiler-dom": {
- "version": "3.5.39",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz",
- "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-core": "3.5.39",
- "@vue/shared": "3.5.39"
- }
- },
- "packages/tempo-fns/node_modules/@vue/compiler-sfc": {
- "version": "3.5.39",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz",
- "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.7",
- "@vue/compiler-core": "3.5.39",
- "@vue/compiler-dom": "3.5.39",
- "@vue/compiler-ssr": "3.5.39",
- "@vue/shared": "3.5.39",
- "estree-walker": "^2.0.2",
- "magic-string": "^0.30.21",
- "postcss": "^8.5.15",
- "source-map-js": "^1.2.1"
- }
- },
- "packages/tempo-fns/node_modules/@vue/compiler-ssr": {
- "version": "3.5.39",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz",
- "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-dom": "3.5.39",
- "@vue/shared": "3.5.39"
- }
- },
- "packages/tempo-fns/node_modules/@vue/reactivity": {
- "version": "3.5.39",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz",
- "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vue/shared": "3.5.39"
- }
- },
- "packages/tempo-fns/node_modules/@vue/runtime-core": {
- "version": "3.5.39",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz",
- "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vue/reactivity": "3.5.39",
- "@vue/shared": "3.5.39"
- }
- },
- "packages/tempo-fns/node_modules/@vue/runtime-dom": {
- "version": "3.5.39",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz",
- "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vue/reactivity": "3.5.39",
- "@vue/runtime-core": "3.5.39",
- "@vue/shared": "3.5.39",
- "csstype": "^3.2.3"
- }
- },
- "packages/tempo-fns/node_modules/@vue/server-renderer": {
- "version": "3.5.39",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz",
- "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-ssr": "3.5.39",
- "@vue/shared": "3.5.39"
- },
- "peerDependencies": {
- "vue": "3.5.39"
- }
- },
- "packages/tempo-fns/node_modules/@vue/shared": {
- "version": "3.5.39",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz",
- "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
- "dev": true,
- "license": "MIT"
- },
- "packages/tempo-fns/node_modules/vue": {
- "version": "3.5.39",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz",
- "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vue/compiler-dom": "3.5.39",
- "@vue/compiler-sfc": "3.5.39",
- "@vue/runtime-dom": "3.5.39",
- "@vue/server-renderer": "3.5.39",
- "@vue/shared": "3.5.39"
- },
- "peerDependencies": {
- "typescript": "*"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
}
}
}
diff --git a/package.json b/package.json
index 6c9cfcdd..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"
@@ -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/tempo-fns/CHANGELOG.md b/packages/functions/CHANGELOG.md
similarity index 100%
rename from packages/tempo-fns/CHANGELOG.md
rename to packages/functions/CHANGELOG.md
diff --git a/packages/tempo-fns/LICENSE b/packages/functions/LICENSE
similarity index 100%
rename from packages/tempo-fns/LICENSE
rename to packages/functions/LICENSE
diff --git a/packages/tempo-fns/README.md b/packages/functions/README.md
similarity index 79%
rename from packages/tempo-fns/README.md
rename to packages/functions/README.md
index bd1a12fa..a3494a26 100644
--- a/packages/tempo-fns/README.md
+++ b/packages/functions/README.md
@@ -2,7 +2,7 @@
-
+
|
@magmacomputing/tempo-fns
@@ -12,7 +12,7 @@
|
-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.
@@ -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/bin/sync-docs.mjs b/packages/functions/bin/sync-docs.mjs
similarity index 91%
rename from packages/tempo-fns/bin/sync-docs.mjs
rename to packages/functions/bin/sync-docs.mjs
index 95bf65fd..5556a1c6 100644
--- a/packages/tempo-fns/bin/sync-docs.mjs
+++ b/packages/functions/bin/sync-docs.mjs
@@ -7,7 +7,7 @@ const srcDir = path.resolve(__dirname, '../src');
const docFunctionsDir = path.resolve(__dirname, '../doc/functions');
const categories = fs.readdirSync(srcDir, { withFileTypes: true })
- .filter(dirent => dirent.isDirectory())
+ .filter(dirent => dirent.isDirectory() && !dirent.name.startsWith('.') && dirent.name !== 'support')
.map(dirent => dirent.name);
let syncedCount = 0;
diff --git a/packages/tempo-fns/doc/.vitepress/config.mts b/packages/functions/doc/.vitepress/config.mts
similarity index 94%
rename from packages/tempo-fns/doc/.vitepress/config.mts
rename to packages/functions/doc/.vitepress/config.mts
index 0919b676..dd4ad89f 100644
--- a/packages/tempo-fns/doc/.vitepress/config.mts
+++ b/packages/functions/doc/.vitepress/config.mts
@@ -3,7 +3,7 @@ import { defineConfig } from 'vitepress'
export default defineConfig({
title: "tempo-fns",
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 100%
rename from packages/tempo-fns/doc/CONTRIBUTING.md
rename to packages/functions/doc/CONTRIBUTING.md
diff --git a/packages/tempo-fns/doc/README.md b/packages/functions/doc/README.md
similarity index 79%
rename from packages/tempo-fns/doc/README.md
rename to packages/functions/doc/README.md
index bd1a12fa..a3494a26 100644
--- a/packages/tempo-fns/doc/README.md
+++ b/packages/functions/doc/README.md
@@ -2,7 +2,7 @@
-
+
|
@magmacomputing/tempo-fns
@@ -12,7 +12,7 @@
|
-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.
@@ -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 87%
rename from packages/tempo-fns/doc/functions/business/index.md
rename to packages/functions/doc/functions/business/index.md
index 2bc1e037..a3882723 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/tempo-fns';
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/tempo-fns';
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 56%
rename from packages/tempo-fns/doc/functions/calendar/index.md
rename to packages/functions/doc/functions/calendar/index.md
index 622cfac7..4f432038 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/tempo-fns';
getISOWeekOfYear(new Tempo('2026-01-01')); // Returns: 1
```
@@ -24,7 +24,21 @@ function isFirstDayOfMonth(input: { day: number } | Tempo): boolean;
```
**Example:**
```typescript
-import { isFirstDayOfMonth } from '@magmacomputing/tempo-fns/calendar';
+import { isFirstDayOfMonth } from '@magmacomputing/tempo-fns';
isFirstDayOfMonth(new Tempo('2026-03-01')); // Returns: true
```
+
+### `getPublicHolidays`
+Fetches a list of public holidays for a specific region and year from the Nager.Date API.
+
+```typescript
+function getPublicHolidays(year?: number, region?: string): Promise;
+```
+**Example:**
+```typescript
+import { getPublicHolidays } from '@magmacomputing/tempo-fns';
+
+const holidays = await getPublicHolidays(2026, 'US');
+console.log(holidays[0].name); // "New Year's Day"
+```
diff --git a/packages/tempo-fns/doc/functions/duration/index.md b/packages/functions/doc/functions/duration/index.md
similarity index 96%
rename from packages/tempo-fns/doc/functions/duration/index.md
rename to packages/functions/doc/functions/duration/index.md
index d8f13d3f..1e16d331 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/tempo-fns';
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 87%
rename from packages/tempo-fns/doc/functions/index.md
rename to packages/functions/doc/functions/index.md
index 9952d874..30d1e6a9 100644
--- a/packages/tempo-fns/doc/functions/index.md
+++ b/packages/functions/doc/functions/index.md
@@ -8,9 +8,6 @@ Welcome to the `tempo-fns` documentation! This library provides a comprehensive
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.
-> [!NOTE]
-> Advanced scheduling utilities (such as the `Interval` class) explicitly require `Tempo` instances as inputs, leveraging the `tempo.epoch.ns` high-performance nanosecond identifier for lightning-fast chronological comparisons.
-
## 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.
diff --git a/packages/tempo-fns/doc/functions/scheduling/Interval.md b/packages/functions/doc/functions/scheduling/Interval.md
similarity index 100%
rename from packages/tempo-fns/doc/functions/scheduling/Interval.md
rename to packages/functions/doc/functions/scheduling/Interval.md
diff --git a/packages/tempo-fns/doc/functions/scheduling/cron.md b/packages/functions/doc/functions/scheduling/cron.md
similarity index 96%
rename from packages/tempo-fns/doc/functions/scheduling/cron.md
rename to packages/functions/doc/functions/scheduling/cron.md
index 3ad8eacc..9eee297e 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
diff --git a/packages/tempo-fns/doc/functions/scheduling/index.md b/packages/functions/doc/functions/scheduling/index.md
similarity index 71%
rename from packages/tempo-fns/doc/functions/scheduling/index.md
rename to packages/functions/doc/functions/scheduling/index.md
index 49eb5710..de29dead 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/tempo-fns';
import { Tempo } from '@magmacomputing/tempo';
const now = new Tempo('2026-01-01T08:00:00');
@@ -24,7 +24,7 @@ Represents a continuous span of time with start and end boundaries, supporting s
```typescript
class Interval {
- constructor(start: T, end: T);
+ constructor(start: T | null, end: T | null);
contains(point: TemporalPoint): boolean;
overlaps(other: Interval): boolean;
@@ -35,15 +35,15 @@ class Interval {
```
**Example:**
```typescript
-import { Interval } from '@magmacomputing/tempo';
-import { Temporal } from '@js-temporal/polyfill'; // or native Temporal in Node 22+
+import { Interval } from '@magmacomputing/tempo-fns';
+import { Temporal } from '@js-temporal/polyfill';
-const start = Temporal.ZonedDateTime.from('2026-01-01T08:00[UTC]');
-const end = Temporal.ZonedDateTime.from('2026-01-01T17:00[UTC]');
+const start = Temporal.Instant.from('2026-01-01T08:00:00Z');
+const end = Temporal.Instant.from('2026-01-01T17:00:00Z');
const shift = new Interval(start, end);
-const meetingStart = Temporal.ZonedDateTime.from('2026-01-01T14:00[UTC]');
-const meetingEnd = Temporal.ZonedDateTime.from('2026-01-01T15:00[UTC]');
+const meetingStart = Temporal.Instant.from('2026-01-01T14:00:00Z');
+const meetingEnd = Temporal.Instant.from('2026-01-01T15:00:00Z');
const meeting = new Interval(meetingStart, meetingEnd);
console.log(shift.contains(meetingStart)); // true
diff --git a/packages/tempo-fns/doc/functions/timezone/index.md b/packages/functions/doc/functions/timezone/index.md
similarity index 82%
rename from packages/tempo-fns/doc/functions/timezone/index.md
rename to packages/functions/doc/functions/timezone/index.md
index f2e32c01..b06b9b09 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/tempo-fns';
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/tempo-fns';
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/tempo-fns';
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/tempo-fns';
normalizeUtcOffset('UTC+10'); // Returns: '+10:00'
```
diff --git a/packages/tempo-fns/doc/index.md b/packages/functions/doc/index.md
similarity index 96%
rename from packages/tempo-fns/doc/index.md
rename to packages/functions/doc/index.md
index 4afc1d3c..5a423510 100644
--- a/packages/tempo-fns/doc/index.md
+++ b/packages/functions/doc/index.md
@@ -5,7 +5,7 @@ layout: home
@@ -13,7 +13,7 @@ const getStartedUrl = withBase('/functions/')
-
![tempo-fns Logo]()
+
tempo-fns
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 83%
rename from packages/tempo-fns/doc/support.md
rename to packages/functions/doc/support.md
index ec042b42..b41c65a3 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](./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 94%
rename from packages/tempo-fns/package.json
rename to packages/functions/package.json
index 903fbd30..ec895bd5 100644
--- a/packages/tempo-fns/package.json
+++ b/packages/functions/package.json
@@ -16,7 +16,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/magmacomputing/magma.git",
- "directory": "packages/tempo-fns"
+ "directory": "packages/functions"
},
"type": "module",
"sideEffects": false,
@@ -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 92%
rename from packages/tempo-fns/rollup.config.js
rename to packages/functions/rollup.config.js
index f2d83fff..d4d577a8 100644
--- a/packages/tempo-fns/rollup.config.js
+++ b/packages/functions/rollup.config.js
@@ -5,7 +5,7 @@ export default {
output: {
file: 'dist/tempo-fns.global.js',
format: 'iife',
- name: 'TempoFns',
+ name: 'Functions',
globals: {
'@magmacomputing/tempo': 'Tempo'
}
diff --git a/packages/tempo-fns/src/business/README.md b/packages/functions/src/business/README.md
similarity index 87%
rename from packages/tempo-fns/src/business/README.md
rename to packages/functions/src/business/README.md
index 2bc1e037..a3882723 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/tempo-fns';
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/tempo-fns';
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 96%
rename from packages/tempo-fns/src/business/workingHoursUntil.ts
rename to packages/functions/src/business/workingHoursUntil.ts
index 31379e3f..d6d1d466 100644
--- a/packages/tempo-fns/src/business/workingHoursUntil.ts
+++ b/packages/functions/src/business/workingHoursUntil.ts
@@ -39,7 +39,7 @@ export const workingHoursUntil = function (start: Tempo | Temporal.ZonedDateTime
const endWorkHour = options?.endHour ?? 17;
if (startWorkHour >= endWorkHour)
- throw new Error('[Tempo-Fns] startHour must be strictly less than endHour');
+ throw new Error('[functions] startHour must be strictly less than endHour');
const holidaysList = [...(options?.holidays ?? [])];
if (options?.region) {
@@ -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 83%
rename from packages/tempo-fns/src/calendar/README.md
rename to packages/functions/src/calendar/README.md
index 48a95f12..4f432038 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/tempo-fns';
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/tempo-fns';
isFirstDayOfMonth(new Tempo('2026-03-01')); // Returns: true
```
@@ -37,7 +37,7 @@ function getPublicHolidays(year?: number, region?: string): Promise
) {
const whole = Math.trunc(v);
const frac = v - whole;
if (whole) payload[big] = whole; else delete payload[big];
- payload[small] = (payload[small] ?? 0) + Math.round(frac * factor);
+ const addition = small === 'nanoseconds' ? Math.round(frac * factor) : (frac * factor);
+ payload[small] = (payload[small] ?? 0) + addition;
}
}
diff --git a/packages/tempo-fns/src/index.ts b/packages/functions/src/index.ts
similarity index 100%
rename from packages/tempo-fns/src/index.ts
rename to packages/functions/src/index.ts
diff --git a/packages/tempo-fns/src/scheduling/README.md b/packages/functions/src/scheduling/README.md
similarity index 92%
rename from packages/tempo-fns/src/scheduling/README.md
rename to packages/functions/src/scheduling/README.md
index 21d22362..de29dead 100644
--- a/packages/tempo-fns/src/scheduling/README.md
+++ b/packages/functions/src/scheduling/README.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/tempo-fns';
import { Tempo } from '@magmacomputing/tempo';
const now = new Tempo('2026-01-01T08:00:00');
@@ -35,7 +35,7 @@ class Interval {
```
**Example:**
```typescript
-import { Interval } from '@magmacomputing/tempo-fns/scheduling';
+import { Interval } from '@magmacomputing/tempo-fns';
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 85%
rename from packages/tempo-fns/src/scheduling/cron.ts
rename to packages/functions/src/scheduling/cron.ts
index b019d974..19412d4a 100644
--- a/packages/tempo-fns/src/scheduling/cron.ts
+++ b/packages/functions/src/scheduling/cron.ts
@@ -23,7 +23,7 @@ function parseCronField(field: string, min: number, max: number): CronField {
const [range, stepStr] = part.split('/');
const step = parseInt(stepStr, 10);
if (isNaN(step) || step <= 0)
- throw new Error(`[Tempo-Fns] Invalid step value: ${stepStr}`);
+ throw new Error(`[tempo-fns] Invalid step value: ${stepStr}`);
let start = min;
let end = max;
@@ -32,7 +32,7 @@ function parseCronField(field: string, min: number, max: number): CronField {
start = parseInt(rangeParts[0], 10);
end = rangeParts.length > 1 ? parseInt(rangeParts[1], 10) : start;
if (start > end)
- throw new Error(`[Tempo-Fns] Invalid range: ${range}`);
+ throw new Error(`[tempo-fns] Invalid range: ${range}`);
}
for (let i = start; i <= end; i += step) {
@@ -41,7 +41,7 @@ function parseCronField(field: string, min: number, max: number): CronField {
} else if (part.includes('-')) {
const [start, end] = part.split('-').map(Number);
if (start > end)
- throw new Error(`[Tempo-Fns] Invalid range: ${part}`);
+ throw new Error(`[tempo-fns] Invalid range: ${part}`);
for (let i = start; i <= end; i++)
allowed.add(i);
@@ -55,7 +55,7 @@ function parseCronField(field: string, min: number, max: number): CronField {
export function parseCron(pattern: string): CronSchedule {
const fields = pattern.trim().split(/\s+/);
if (fields.length !== 5) {
- throw new Error('[Tempo-Fns] Invalid cron pattern. Expected 5 fields (min, hr, dom, mon, dow).');
+ throw new Error('[tempo-fns] Invalid cron pattern. Expected 5 fields (min, hr, dom, mon, dow).');
}
return {
@@ -86,7 +86,9 @@ export function nextCron(tempo: Tempo, pattern: string): Tempo {
// Start searching from the next minute, operating directly on Temporal.ZonedDateTime for performance
let current = tempo.toDateTime().add({ minutes: 1 }).with({ second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 });
- for (let i = 0; i < 5 * 366; i++) { // Max iterations to prevent infinite loops (5 years max approx)
+ const maxLimit = current.add({ years: 5 }).epochNanoseconds;
+ for (;;) { // Max iterations to prevent infinite loops (5 years max approx)
+ if (current.epochNanoseconds > maxLimit) throw new Error('[tempo-fns] Could not find next cron match within 5 years.');
if (!schedule.months.allowed.has(current.month)) {
current = current.add({ months: 1 }).with({ day: 1, hour: 0, minute: 0 });
continue;
@@ -109,8 +111,6 @@ export function nextCron(tempo: Tempo, pattern: string): Tempo {
return tempo.set(current);
}
-
- throw new Error('[Tempo-Fns] Could not find next cron match within 5 years.');
}
/**
@@ -121,7 +121,9 @@ export function prevCron(tempo: Tempo, pattern: string): Tempo {
// Start searching from the previous minute
let current = tempo.toDateTime().subtract({ minutes: 1 }).with({ second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 });
- for (let i = 0; i < 5 * 366; i++) {
+ const minLimit = current.subtract({ years: 5 }).epochNanoseconds;
+ for (;;) {
+ if (current.epochNanoseconds < minLimit) throw new Error('[tempo-fns] Could not find previous cron match within 5 years.');
if (!schedule.months.allowed.has(current.month)) {
current = current.subtract({ months: 1 });
current = current.with({ day: current.daysInMonth, hour: 23, minute: 59 });
@@ -145,6 +147,4 @@ export function prevCron(tempo: Tempo, pattern: string): Tempo {
return tempo.set(current);
}
-
- throw new Error('[Tempo-Fns] Could not find previous cron match within 5 years.');
}
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 82%
rename from packages/tempo-fns/src/timezone/README.md
rename to packages/functions/src/timezone/README.md
index f2e32c01..b06b9b09 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/tempo-fns';
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/tempo-fns';
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/tempo-fns';
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/tempo-fns';
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 80%
rename from packages/tempo-fns/src/timezone/isDST.ts
rename to packages/functions/src/timezone/isDST.ts
index f377c9c1..ede70745 100644
--- a/packages/tempo-fns/src/timezone/isDST.ts
+++ b/packages/functions/src/timezone/isDST.ts
@@ -10,7 +10,11 @@ export function isDST(date?: Temporal.ZonedDateTime | string, timeZone: string =
try {
zdt = TemporalAPI.Instant.from(date).toZonedDateTimeISO(timeZone);
} catch {
- zdt = TemporalAPI.PlainDate.from(date).toZonedDateTime(timeZone);
+ try {
+ zdt = TemporalAPI.PlainDateTime.from(date).toZonedDateTime(timeZone);
+ } catch {
+ zdt = TemporalAPI.PlainDate.from(date).toZonedDateTime(timeZone);
+ }
}
} else {
zdt = date ?? TemporalAPI.Now.zonedDateTimeISO(timeZone);
diff --git a/packages/tempo-fns/src/timezone/normalizeUtcOffset.ts b/packages/functions/src/timezone/normalizeUtcOffset.ts
similarity index 91%
rename from packages/tempo-fns/src/timezone/normalizeUtcOffset.ts
rename to packages/functions/src/timezone/normalizeUtcOffset.ts
index 9bdd0f01..fb066453 100644
--- a/packages/tempo-fns/src/timezone/normalizeUtcOffset.ts
+++ b/packages/functions/src/timezone/normalizeUtcOffset.ts
@@ -13,7 +13,7 @@ export function normalizeUtcOffset(zone: string): string {
const m = Number(minutes ?? '0');
// Temporal-valid range: -12:00 .. +14:00, minutes 0..59
- if (h > 14 || m > 59 || (sign === '+' && h === 14 && m !== 0) || (sign === '-' && h > 12)) return zone;
+ if (h > 14 || m > 59 || (sign === '+' && h === 14 && m !== 0) || (sign === '-' && (h > 12 || (h === 12 && m > 0)))) return zone;
return `${sign}${hours.padStart(2, '0')}:${minutes ?? '00'}`;
}
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/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/.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 63%
rename from packages/plugins/bin/repl.mts
rename to 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
new file mode 100644
index 00000000..0e2e0e1d
--- /dev/null
+++ b/packages/plugins/.bin/temporal-polyfill.mts
@@ -0,0 +1,10 @@
+import { Temporal } from '@js-temporal/polyfill';
+
+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/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/.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/.setup/doc/index.md b/packages/plugins/.setup/doc/index.md
new file mode 100644
index 00000000..f9f020cc
--- /dev/null
+++ b/packages/plugins/.setup/doc/index.md
@@ -0,0 +1,149 @@
+# 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).
+
+
+:::
+
+## 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 TickerPlugin
+
+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 { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker';
+
+Tempo.init({
+ license: 'eyJhbGciOiJSUzI1NiJ9...',
+ plugins: [TickerPlugin]
+});
+```
+
+#### Option 2: Explicit Extension
+```javascript
+import { Tempo } from '@magmacomputing/tempo';
+import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker';
+
+// 1. Initialize core Tempo with your license
+Tempo.init({
+ license: 'eyJhbGciOiJSUzI1NiJ9...'
+});
+
+// 2. Register the plugin
+Tempo.extend(TickerPlugin);
+```
+
+### 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 { TickerPlugin } = await import('@magmacomputing/tempo-plugin-ticker');
+
+Tempo.init({ plugins: [TickerPlugin] });
+```
+
+Alternatively, pass the license key explicitly via `Tempo.init()` after your static imports:
+
+```javascript
+import { Tempo } from '@magmacomputing/tempo';
+import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker';
+
+Tempo.init({
+ license: import.meta.env.VITE_TEMPO_LICENSE_KEY,
+ plugins: [TickerPlugin]
+});
+```
+
+#### 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/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/bin/temporal-polyfill.mts b/packages/plugins/bin/temporal-polyfill.mts
deleted file mode 100644
index 3f34b09f..00000000
--- a/packages/plugins/bin/temporal-polyfill.mts
+++ /dev/null
@@ -1,5 +0,0 @@
-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
diff --git a/packages/plugins/finance/package.json b/packages/plugins/finance/package.json
index 8eef90ad..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",
@@ -8,7 +9,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/finance/src/index.ts b/packages/plugins/finance/src/index.ts
index 77adcdc7..59906f5d 100644
--- a/packages/plugins/finance/src/index.ts
+++ b/packages/plugins/finance/src/index.ts
@@ -1,4 +1,4 @@
-import { defineNamespace } from '@magmacomputing/tempo/plugin';
+import { defineNamespace, type TempoPlugin } from '@magmacomputing/tempo/plugin';
import type { Tempo } from '@magmacomputing/tempo';
// -----------------------------------------------------------------------------
@@ -15,7 +15,7 @@ export const isFiscalYearStart = (tempo: Tempo) => () => tempo.mm === 1 && tempo
// Wrap the functions in a Namespace Plugin so they can be injected directly
// onto the Tempo instance (e.g., `t.finance.taxYear`) for a fluent experience.
// -----------------------------------------------------------------------------
-export const FinancePlugin = defineNamespace({
+export const FinancePlugin: TempoPlugin = defineNamespace({
name: 'finance',
resolvers: {
fiscalQuarter,
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/snap/src/index.ts b/packages/plugins/snap/src/index.ts
index 3aee0d97..ccdee7de 100644
--- a/packages/plugins/snap/src/index.ts
+++ b/packages/plugins/snap/src/index.ts
@@ -1,4 +1,4 @@
-import { definePlugin } from '@magmacomputing/tempo/plugin-api';
+import { definePlugin, type TempoPlugin } from '@magmacomputing/tempo/plugin-api';
import type { Tempo } from '@magmacomputing/tempo/core';
type SnapKey =
@@ -18,7 +18,7 @@ type OneKey =
type SnapOptions = OneKey & { direction?: 'up' | 'down' };
-export const SnapPlugin = definePlugin({
+export const SnapPlugin: TempoPlugin = definePlugin({
name: 'snap',
install(TempoClass: any) {
TempoClass.prototype.snap = function (this: Tempo, options?: SnapOptions) {
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/ticker/doc/index.md b/packages/plugins/ticker/doc/index.md
index daf5c54a..cb334701 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](../../.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/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 6c7c5f52..9b867627 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 {
@@ -108,16 +111,30 @@ 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 the package explicitly declares plan: 'community', skip the license wrapper
+ if (plan === 'community')
return; // Community plugin, do not apply license wrapper
}
return { path: path.resolve(__dirname, 'internal/license/src/plugin.api.ts') };
});
}
- }
+ },
+ {
+ // 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.
+ // NOTE: placed last so auto-inject-version and license-alias hooks run first.
+ name: 'esm-external',
+ setup(build) {
+ if (build.initialOptions?.format === 'iife') return;
+
+ build.onResolve({ filter: /^@magmacomputing\/tempo\/(plugin|plugin-api)$/ }, () => {
+ return { external: true };
+ });
+ }
+ },
]
}
diff --git a/packages/plugins/vitest.shared.ts b/packages/plugins/vitest.shared.ts
new file mode 100644
index 00000000..6391b94b
--- /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\/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') },
+ { 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/config.ts b/packages/tempo/.vitepress/config.ts
index b0f6a219..84162fc7 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: '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${match[3] || ''}`;
+ }
+ }
+ return defaultRender(tokens, idx, options, env, self);
+ }
+ }
+ },
vite: {
build: {
target: 'es2022',
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/CHANGELOG.md b/packages/tempo/CHANGELOG.md
index 28ad2857..ddea1533 100644
--- a/packages/tempo/CHANGELOG.md
+++ b/packages/tempo/CHANGELOG.md
@@ -6,7 +6,20 @@ 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.0] - 2026-07-14
+## [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
- **Era Parsing Engine**: Upgraded the `ParseModule` and Lexer to natively support parsing historical and future era dates with explicit markers (e.g. `200 BC`, `BC 200`, `2026 CE`). Supports both leading and trailing formats and flawlessly converts to the astronomical ISO 8601 year.
diff --git a/packages/tempo/README.md b/packages/tempo/README.md
index 23603de7..7955dba4 100644
--- a/packages/tempo/README.md
+++ b/packages/tempo/README.md
@@ -97,7 +97,7 @@ While the native Temporal API gives you perfect primitives (`ZonedDateTime`, `Pl
### The Missing Domain Layer
* **๐๏ธ Future Standard**: Built natively on the TC39 `Temporal` proposal. Inherit the reliability of the future standard.
-* **๐งฉ Premium Ecosystem**: Don't build temporal math from scratch. Drop in our cryptographically-secured plugins for Astronomical seasons (`astro`) and atomic state syncing (`sync`).
+* **๐งฉ Premium Ecosystem**: Don't build temporal math from scratch. Drop in our plugins for Astronomical seasons (`astro`) and atomic state syncing (`sync`).
* **๐ Zero-Bundle Localization**: Best-in-class multi-language parsing and formatting powered natively by the `Intl` APIโno massive static locale dictionaries required.
* **๐ฃ๏ธ Natural Language**: Resolve complex terms like "two days ago" with zero configuration.
* **๐ง Functional Aliases**: Extend the parser with custom logic using a powerful resolution context for relative date math.
@@ -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 [](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 [](./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 ee59b9f4..35a4576f 100644
--- a/packages/tempo/bin/harvest-plugins.mjs
+++ b/packages/tempo/bin/harvest-plugins.mjs
@@ -8,16 +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$2)`);
+
+ // 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, q) => `](./${p.replace(/^\./, '_')}.${f}.md${q || ''})`);
+
+ 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/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/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
diff --git a/packages/tempo/doc/8-project-and-support/commercial.md b/packages/tempo/doc/8-project-and-support/commercial.md
index 2145dc00..a0343abe 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](../../../plugins/.setup/doc/index.md).
---
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 @@
-
-
-# @magmacomputing/tempo-plugin-astro
-
-
-
-
-
-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 @@
-
-
-# @magmacomputing/tempo-plugin-batch
-
-
-
-
-
-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
-
-
-
-
-
-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 @@
-
-
-# @magmacomputing/tempo-plugin-snap
-
-
-
-
-
-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 @@
-
-
-# @magmacomputing/tempo-plugin-sync
-
-
-
-
-
-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 daf5c54a..00000000
--- a/packages/tempo/doc/9-plugins/ticker.md
+++ /dev/null
@@ -1,351 +0,0 @@
-
-
-# @magmacomputing/tempo-plugin-ticker
-
-
-
-
-
-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.
-
-::: 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
-
-:::
diff --git a/packages/tempo/package.json b/packages/tempo/package.json
index 28f1f51a..b9161d23 100644
--- a/packages/tempo/package.json
+++ b/packages/tempo/package.json
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo",
- "version": "3.9.0",
+ "version": "3.9.1",
"engines": {
"node": ">=20.0.0"
},
@@ -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..de7831d5
--- /dev/null
+++ b/packages/tempo/plan/refactor-path-terms.md
@@ -0,0 +1,296 @@
+# Plan: Refactor Built-in Terms into `packages/plugins/.std/`
+
+> **Status: DEFERRED** โ agreed design, awaiting execution after other work is complete.
+
+## Overview
+
+Move the four built-in data Terms (Quarter, Season, Zodiac, Timeline) from
+`packages/tempo/src/plugin/term/` into a new hidden directory
+`packages/plugins/.std/`.
+
+**Delivery:** Rollup inlines the `.std` source into `dist/term/` inside
+`@magmacomputing/tempo` at build time โ mirroring how `@magmacomputing/library`
+is inlined into `dist/lib/`.
+**No separate npm publish is required.**
+
+---
+
+## Naming: `.std` (hidden directory)
+
+The existing plugins workspace uses hidden directories for workspace infrastructure
+(`.app/`) and visible directories for published plugins (`snap/`, `batch/` etc.).
+`.std` inherits that convention: clearly internal, non-publishable, self-documenting.
+
+| Name | Problem | Verdict |
+|---|---|---|
+| `standard` | Contributor thinks it's publishable; user tries `npm i @magmacomputing/tempo-plugin-standard` โ 404 | โ |
+| `.std` | Mirrors `.app`, clearly non-publishable | โ
|
+
+---
+
+## Key Design Decisions (all resolved)
+
+### Option B โ `TermsModule` stays in `tempo.index.ts`
+
+`TermsModule` remains in `packages/tempo/src/tempo.index.ts`.
+`.std` exports **only** `StandardTerms` (the array of four terms) and the individual
+term exports. This means `.std` has **no need** for `getRuntime` or `onRegistryReset`,
+and `plugin-api` needs no new lifecycle surface exposure.
+
+If a Premium plugin author needs runtime lifecycle access in future, a dedicated
+mechanism will be designed at that time.
+
+### `plugin-api` additions (minimal)
+
+Only two pure utilities are added โ no runtime singleton exposure:
+- `resolveCycleWindow` (pure calculation function, already in `term.util.ts`)
+- `logWarn` (simple console wrapper)
+
+### Auto-loading safety
+
+A user dropping a custom term file into `.std/src/` will **not** have it
+auto-loaded. Rollup only bundles files reachable through the import graph.
+Un-imported files are tree-shaken away. The `StandardTerms` array in
+`.std/src/index.ts` is the sole entry gate.
+
+### `dist/term/` modification risk
+
+No greater than editing `dist/tempo.class.js`. Standard npm social contract applies.
+A `/* Generated โ do not edit */` header is sufficient deterrent.
+
+---
+
+## Proposed Changes
+
+### 1 โ New package: `packages/plugins/.std/`
+
+#### `packages/plugins/.std/package.json`
+
+```json
+{
+ "name": "@magmacomputing/tempo-std",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Standard built-in Terms for @magmacomputing/tempo (showcase implementation โ not published separately)",
+ "type": "module",
+ "peerDependencies": {
+ "@magmacomputing/tempo": "^3.9.x"
+ }
+}
+```
+
+`"private": true` โ hard guard against accidental `npm publish`; also self-documents intent.
+
+#### `packages/plugins/.std/src/index.ts`
+
+```ts
+export { QuarterTerm } from './term.quarter.js';
+export { SeasonTerm } from './term.season.js';
+export { ZodiacTerm } from './term.zodiac.js';
+export { TimelineTerm } from './term.timeline.js';
+
+export const StandardTerms = [QuarterTerm, SeasonTerm, ZodiacTerm, TimelineTerm];
+```
+
+`TermsModule` is **not** here โ it stays in `tempo.index.ts`.
+
+#### `packages/plugins/.std/src/term.quarter.ts` / `term.season.ts` / `term.zodiac.ts` / `term.timeline.ts`
+
+Move verbatim from `packages/tempo/src/plugin/term/`. Update imports:
+
+```diff
+- import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from './term.util.js';
+- import { logWarn } from '../../support/support.util.js';
+- import { COMPASS } from '../../support/support.enum.js';
+- import { isNumber } from '#library/assertion.library.js';
+- import type { Tempo } from '../../tempo.class.js';
+
++ import { defineTerm, getTermRange, defineRange, resolveCycleWindow, logWarn, COMPASS, isNumber } from '@magmacomputing/tempo/plugin-api';
++ import type { Tempo } from '@magmacomputing/tempo';
+```
+
+Module augmentation target:
+
+```diff
+- declare module '../../tempo.class.js' {
++ declare module '@magmacomputing/tempo' {
+ interface TempoTermRegistry { ... }
+ }
+```
+
+#### `packages/plugins/.std/README.md`
+
+Content must include:
+- โ ๏ธ Banner: "Not published separately"
+- Why it exists (source organization, Rollup-inlined delivery)
+- The four terms table (key, scope, description)
+- How end-users access them (`import '@magmacomputing/tempo'` or `/term/quarter` sub-path)
+- Clear instruction: **do not add custom terms here** โ create a new `packages/plugins/
/` instead
+- Build notes: source โ `packages/plugins/.std/src/`, output โ `packages/tempo/dist/term/`
+
+#### `packages/plugins/.std/test/`
+
+Move term tests from `packages/tempo/test/` (term-related files) alongside the source.
+
+---
+
+### 2 โ `packages/tempo/src/tsconfig.json`
+
+```diff
+ "paths": {
+ ...
++ "#tempo/std": [ "../../plugins/.std/src/index.ts" ],
+ }
+```
+
+No project reference needed โ `.std` is consumed as source (same pattern as `@magmacomputing/library`).
+
+---
+
+### 3 โ `packages/tempo/src/tempo.index.ts`
+
+`TermsModule` body unchanged. Only the import source changes:
+
+```diff
+- import { QuarterTerm } from './plugin/term/term.quarter.js';
+- import { SeasonTerm } from './plugin/term/term.season.js';
+- import { ZodiacTerm } from './plugin/term/term.zodiac.js';
+- import { TimelineTerm } from './plugin/term/term.timeline.js';
+
++ import { StandardTerms } from '#tempo/std';
+```
+
+And simplify the StandardTerms reference inside `TermsModule.install()`:
+
+```diff
+- TempoClass.extend([QuarterTerm, SeasonTerm, ZodiacTerm, TimelineTerm]);
++ TempoClass.extend(StandardTerms);
+```
+
+---
+
+### 4 โ `packages/tempo/src/plugin/term/term.index.ts` (slimmed)
+
+Remove all data term imports, `StandardTerms`, and `TermsModule`. Retain framework only:
+
+```ts
+// Framework utilities for external Term plugin authors
+export { defineTerm, defineRange, getTermRange, resolveCycleWindow } from './term.util.js';
+export type { TermPlugin, Range, ResolvedRange } from './term.type.js';
+```
+
+**Delete:**
+- `packages/tempo/src/plugin/term/term.quarter.ts`
+- `packages/tempo/src/plugin/term/term.season.ts`
+- `packages/tempo/src/plugin/term/term.zodiac.ts`
+- `packages/tempo/src/plugin/term/term.timeline.ts`
+
+---
+
+### 5 โ `packages/tempo/rollup.config.js`
+
+Extend the `entryFileNames` routing hook (after the existing `dist/lib/` branch):
+
+```diff
+ if (id.includes('magma/packages/library') || rel.startsWith('../library')) {
+ ...
+ return `lib/${dir}${name}.js`;
+ }
+
++ if (id.includes('magma/packages/plugins/.std') || rel.startsWith('../plugins/.std')) {
++ const match = normalizedRel.match(/plugins\/\.std\/src\/(.*)$/);
++ const modulePath = match ? path.dirname(match[1]) : '.';
++ const dir = modulePath === '.' ? '' : modulePath + '/';
++ return `term/${dir}${name}.js`;
++ }
+```
+
+Output: `dist/term/index.js`, `dist/term/term.quarter.js` โฆ `dist/term/term.timeline.js`
+
+---
+
+### 6 โ `packages/tempo/src/plugin-api.index.ts`
+
+Add two pure utilities (no runtime exposure):
+
+```diff
+- export { defineTerm, defineRange, getTermRange } from './plugin/term/term.index.js';
++ export { defineTerm, defineRange, getTermRange, resolveCycleWindow } from './plugin/term/term.index.js';
++ export { logWarn } from './support/support.util.js';
+```
+
+---
+
+### 7 โ `packages/tempo/package.json`
+
+#### `imports` โ one addition
+
+```diff
++ "#tempo/std": { "default": "./dist/term/index.js" },
+```
+
+#### `exports` โ additive only
+
+```diff
++ "./term/standard": { "types": "./dist/term/index.d.ts", "import": "./dist/term/index.js" },
++ "./term/quarter": { "types": "./dist/term/term.quarter.d.ts", "import": "./dist/term/term.quarter.js" },
++ "./term/season": { "types": "./dist/term/term.season.d.ts", "import": "./dist/term/term.season.js" },
++ "./term/zodiac": { "types": "./dist/term/term.zodiac.d.ts", "import": "./dist/term/term.zodiac.js" },
++ "./term/timeline": { "types": "./dist/term/term.timeline.d.ts", "import": "./dist/term/term.timeline.js" },
+```
+
+#### `sideEffects` โ two removals
+
+```diff
+- "**/plugin/term/term.index.js",
+- "**/plugin/term/term.index.ts",
+```
+
+#### `dependencies` โ no change
+
+`.std` source is inlined at build time. No new runtime npm dependency.
+
+---
+
+## Documentation Trawl
+
+Run after code changes are complete. Files requiring review:
+
+| File | Change |
+|---|---|
+| `doc/3-extending-tempo/tempo.term.md` L153 | `@magmacomputing/tempo/term/quarter` is now a **real** sub-path โ confirm/add note |
+| `doc/3-extending-tempo/tempo.modularity.md` L74 | Same โ confirm `/term/quarter` sub-path validity |
+| `doc/3-extending-tempo/tempo.term.md` (augmentation example) | Ensure `declare module '@magmacomputing/tempo'` (not `/core`) |
+| `doc/9-plugins/` | Consider adding `.std.md` entry or expanding `tempo.term.md` with "Built-in Standard Terms" section |
+| `doc/api/Variable.StandardTerms.md` | Delete stale file; regenerate via `npm run docs:api` |
+| `doc/api/Variable.TermsModule.md` | Regenerate |
+| `doc/api/Function.define*.md` + `getTermRange.md` | Regenerate |
+| `doc/api/Interface.TempoTermRegistry.md` | Regenerate |
+
+---
+
+## Verification Plan
+
+```bash
+# 1. Build
+cd packages/tempo && npm run build
+
+# Confirm dist/term/ has 5 files
+ls dist/term/
+# โ index.js term.quarter.js term.season.js term.zodiac.js term.timeline.js
+
+# Confirm dist/plugin/term/ has only framework files
+ls dist/plugin/term/
+# โ term.index.js term.util.js term.type.js
+
+# 2. Full test suite
+npm test
+
+# 3. REPL smoke test
+npm run repl
+# tempo.quarter / tempo.season / tempo.zodiac / tempo.timeOfDay all resolve
+# import { StandardTerms } from '@magmacomputing/tempo/term/standard' โ [4 terms]
+
+# 4. Plugin compat
+cd ../plugins/snap && npm run build # must still pass against updated plugin-api
+```
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..d7907c39 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: import('../tempo.type.js').FormatOptions): 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 (zero-argument โ returns a pre-built format proxy). */ format(): string | number | bigint;
}
}
@@ -35,7 +48,7 @@ declare module '../tempo.class.js' {
* const stamp = format().logStamp; // defaults to 'Now'
*/
export function format(obj?: any): any;
-export function format(obj: any, options: Intl.DateTimeFormatOptions & { timeZone?: string; calendar?: string }): string;
+export function format(obj: any, options: import('../tempo.type.js').FormatOptions): string;
export function format(obj: any, fmt: BigIntPattern, options?: any): bigint;
export function format(obj: any, fmt: NumericPattern, options?: any): number;
export function format(obj: any, fmt: string | symbol, options?: any): string;
diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts
index bfb9f7ad..c9d2aae1 100644
--- a/packages/tempo/src/tempo.class.ts
+++ b/packages/tempo/src/tempo.class.ts
@@ -1625,8 +1625,7 @@ 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)); }
/** 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;
@@ -1808,6 +1807,7 @@ export namespace Tempo {
export interface BaseOptions extends t.Internal.BaseOptions { }
export type Options = t.Options;
+ export interface FormatOptions extends t.FormatOptions { }
/** Configuration to use for #until() and #since() argument */
export type Unit = t.Unit;
@@ -1854,4 +1854,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..197df1a0 100644
--- a/packages/tempo/src/tempo.type.ts
+++ b/packages/tempo/src/tempo.type.ts
@@ -187,6 +187,81 @@ 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}` | `term.${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}\\${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
+ : string; // no more braces โ valid
+
export type WEEKDAY = enums.WEEKDAY
export type WEEKDAYS = enums.WEEKDAYS
export type MONTH = enums.MONTH
@@ -212,6 +287,12 @@ export interface RelativeTime {
/** Default style for relative time */ style?: Intl.RelativeTimeFormatStyle;
}
+export interface FormatOptions extends Intl.DateTimeFormatOptions {
+ timeZone?: string;
+ calendar?: string;
+ locale?: string | string[];
+}
+
export interface IntlOptions {
/** relative time formatting configuration */ relativeTimeFormat?: RelativeTime | ((value: number, unit: any) => string);
/** multi-unit duration formatting configuration */ durationFormat?: any | ((duration: any) => string);
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..00cfeb15 100644
--- a/packages/tempo/test/instance/instance.format.test.ts
+++ b/packages/tempo/test/instance/instance.format.test.ts
@@ -1,5 +1,12 @@
import { Tempo } from '#tempo';
+declare module '../../src/tempo.type.js' {
+ export interface TempoFormatTokens {
+ myDay: string;
+ }
+}
+
+
const label = 'instance.format:';
describe(`${label} format method`, () => {
@@ -7,7 +14,7 @@ describe(`${label} format method`, () => {
test('formats with standard tokens', () => {
const t = new Tempo('2024-05-20 15:30:00');
expect(t.format('{yyyy}-{mm}-{dd}')).toBe('2024-05-20');
- // hh is 24-hour hour. h12 is 12-hour hour.
+ // hh is 24-hour. h12 is 12-hour.
expect(t.format('{hh}:{mi}')).toBe('15:30');
});
@@ -56,7 +63,7 @@ describe(`${label} format method`, () => {
month: 'long',
day: 'numeric',
numberingSystem: 'arab'
- };
+ } as Tempo.FormatOptions;
// 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.
@@ -75,7 +82,7 @@ describe(`${label} format method`, () => {
year: 'numeric',
month: 'long',
day: 'numeric'
- }
+ } as Tempo.FormatOptions
const result = t.format(japaneseConfig);
expect(result).toBe('ไปคๅ6ๅนด12ๆ25ๆฅ');
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..b0bf6da8 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\.(.*)\.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)$/, 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') }
]