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 @@ - Tempo-fns Logo + @magmacomputing/tempo-fns

@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 @@ - Tempo-fns Logo + @magmacomputing/tempo-fns

@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

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). + +
+ + Tempo License Registry + +
+ ๐Ÿ‘‰ Go to the Tempo License Registry ๐Ÿ‘ˆ
+ Manage your subscriptions and retrieve your license key. +
+
+::: + +## 2. Installation + +All premium plugins are distributed as public scoped packages on the standard npm registry (`npmjs.com`). This means no special token configuration or registry overrides are required in your `.npmrc`. You can install them directly using your preferred package manager: + +```bash +npm install @magmacomputing/tempo-plugin-ticker +``` + +## 3. Activation + +Although the packages are publicly installable, they require your valid license key at runtime. If no valid license key is detected, the premium features will fail-safe to `undefined`. + +Once you have your key from the registry, you can activate it using any of the following discovery methods: + +### Method A: Environment Variable (Recommended for Node/Server environments) +Set the `TEMPO_LICENSE_KEY` environment variable in your run environment: + +```bash +export TEMPO_LICENSE_KEY="eyJhbGciOiJSUzI1NiJ9..." +``` + +Then in your application, you can simply import Tempo and the plugin via side-effect. Because the license key is automatically discovered from the environment variable, no manual initialization is required, and the side-effect import registers the plugin automatically: + +```javascript +import { Tempo } from '@magmacomputing/tempo'; +import '@magmacomputing/tempo-plugin-ticker'; // Automatically registers 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   [![Docs](https://img.shields.io/badge/Docs-VitePress-brightgreen?logo=vitepress&style=flat-square)](https://magmacomputing.github.io/magma/functions/) | +| **[`@magmacomputing/tempo-plugin-*`](https://www.npmjs.com/search?q=%40magmacomputing%2Ftempo-plugin)** | Premium & community plugins โ€” Ticker, Astro, Finance, Sync, Snap and more   [![Ecosystem](https://img.shields.io/badge/Browse-Plugin%20Ecosystem-blueviolet?logo=npm&style=flat-square)](./doc/3-extending-tempo/ecosystem.md) | + + --- ## ๐Ÿ’ฌ Contact & Support diff --git a/packages/tempo/bin/harvest-plugins.mjs b/packages/tempo/bin/harvest-plugins.mjs index 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 @@ -![Tempo Plugin](/plugin-logo.svg) - -# @magmacomputing/tempo-plugin-astro - -

- npm version npm peer dependency version License TypeScript Ready -

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

- npm version npm peer dependency version License TypeScript Ready -

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

- npm version npm peer dependency version License TypeScript Ready -

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

- npm version npm peer dependency version License TypeScript Ready -

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

- npm version npm peer dependency version License TypeScript Ready -

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

- npm version npm peer dependency version License TypeScript Ready -

- -This is a premium plugin for the [Tempo](https://github.com/magmacomputing/magma) library that provides a high-performance continuous execution loop (Ticker) based on temporal mathematics. - -::: info High Performance Loop -Unlike standard `setInterval` or `requestAnimationFrame`, the Ticker plugin leverages Tempo's robust temporal core to ensure exact sub-millisecond precision, making it ideal for games, complex UI animations, and accurate state synchronization. -::: - -## Installation - -```bash -npm install @magmacomputing/tempo-plugin-ticker -``` - -## Usage - -To use the Ticker, pass the plugin to `Tempo.init` to ensure it registers securely alongside your license. - -```typescript -import { Tempo } from '@magmacomputing/tempo'; -import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker'; - -// A valid premium license token is required to use this plugin -Tempo.init({ - license: 'YOUR_LICENSE_TOKEN', - plugins: [TickerPlugin] -}); - -// You can now access Ticker-based execution loops through the Tempo API: -const ticker = Tempo.ticker({ seconds: 1 }); -``` - -### Direct Access -If you need to access the [Reporting & Registry](#reporting-registry) API (like `Ticker.active`), you should import the `Ticker` namespace: - -```typescript -import { Ticker } from '@magmacomputing/tempo-plugin-ticker'; - -console.log(Ticker.active); -``` - -## ๐Ÿš€ Key Features - -The Ticker supports a unified **Options** object, enabling professional resource management and semantic duration-based intervals. - -### 1. Semantic Intervals (Duration Objects) -Instead of raw numeric seconds, you can use `DurationLike` objects or shorthand keys for clarity. This is especially powerful for variable-length intervals like **months**. - -```typescript -// Pulse exactly once a month -await using monthly = Tempo.ticker({ months: 1 }); - -// You can also use highly compact shorthand keys -await using concise = Tempo.ticker({ hh: 1, mi: 30 }); // every 1h 30m - -// Pulse every time a new #quarter begins -await using quarterly = Tempo.ticker({ '#quarter': 1 }); -``` - -### 2. Term-Based Intervals -Ticker intervals can now be driven by any registered **Term**. This is powerful for syncing with business cycles or daily shifts. - -```typescript -// Pulse at the start of every 'morning', 'afternoon', etc. -using shiftTicker = Tempo.ticker({ '#period': 1 }, (t) => { - console.log(`New period started: ${t.term.per}`); -}); -``` - -### 3. Stop Conditions (Resource Management) -Prevent memory leaks and runaway processes by setting a built-in termination condition. - -```typescript -// Pattern A: Stop after exactly 5 ticks (defaults to 1-second interval) -using tickerA = Tempo.ticker({ limit: 5 }, (t) => console.log(t)); - -// Pattern B: Stop when a specific virtual time is reached (Inclusive) -using tickerB = Tempo.ticker({ - seconds: 10, // Plural DurationLike property - until: '2024-12-25T12:00:00' -}, (t) => console.log(t)); - -// Pattern C: Stop immediately (Limit: 0 is strictly honored) -using tickerC = Tempo.ticker({ limit: 0 }); -``` - -### 4. Virtual Clock (Seeding) -To create a **Virtual Clock** that increments from a specific point rather than using the system time, use the `seed` option: - -```typescript -// Starts at '2024-01-01', then increments by 1 day per pulse -await using daily = Tempo.ticker({ - days: 1, - seed: '2024-01-01' -}); -``` - -### 5. Backwards Tickers (Countdowns) -By providing a **negative** interval, you can create a Ticker that moves backwards in time. - -```typescript -// Count down from 10 seconds, moving backwards 1s at a time -using countdown = Tempo.ticker({ seconds: -1, seed: "00:00:10" }, (t, stop) => { - console.log(t.format('{ss}')); - if (t.ss === 0) stop(); -}); -``` - -## Usage Patterns - -### 1. Resource Management (Recommended) - -Using the `using` and `await using` keywords ensures that Tickers are automatically stopped when they go out of scope. - -```typescript -// Pattern A: Automatic cleanup for callback-based ticker -{ - using ticker = Tempo.ticker((t) => render(t)); // Defaults to a 1-second pulse -} // interval stops automatically here - -// Pattern B: Automatic cleanup for async generator -{ - await using ticker = Tempo.ticker(1); - for await (const t of ticker) { - if (done) break; - } -} // generator is closed and interval stops here -``` - -### 2. Manual Control (Programmatic Stop) - -If you are not using the `using` or `await using` keywords, or if you need to stop the Ticker from outside its own loop (e.g., in a separate event handler), you can manually call the `stop()` method on the Ticker object. - -```typescript -// Pattern A: Stop a callback-based ticker -const tickerA = Tempo.ticker(1, (t) => console.log(t)); -// ... later -tickerA.stop(); - -// Pattern B: Stop an async generator externally -const tickerB = Tempo.ticker(1); - -(async () => { - for await (const t of tickerB) { - console.log(t.toString()); - } - console.log('Ticker has been gracefully stopped.'); -})(); - -// Close the generator from somewhere else -setTimeout(() => { - tickerB.stop(); -}, 5000); -``` -### 3. Event Listeners (.on) -Instead of (or in addition to) the constructor callback, you can register listeners for the `'pulse'`, `'stop'`, and `'catch'` events. -All listeners use the same callback signature: `(t, stop) => {}`. - -```typescript -const ticker = Tempo.ticker(1); -ticker.on('pulse', (t) => console.log('Listener A:', t.fmt.weekTime)); -ticker.on('pulse', (t) => console.log('Listener B:', t.fmt.weekTime)); -ticker.on('stop', (t) => console.log('Ticker stopped at:', t.fmt.weekTime)); -``` -For `'stop'` listeners, the `stop` callback argument is included for signature consistency; however, invoking it after stop has already occurred is a no-op. - -### 4. Manual Pulsing (.pulse) -In some scenarios, you may want to drive a Ticker manually (e.g., from a UI event or a WebSocket message) while still benefiting from the Ticker's internal state management and listeners. - -```typescript -const ticker = Tempo.ticker({ seconds: 1 }); // Still has a 1s duration logic -// ... -ticker.pulse(); // Manually advance and notify listeners -``` - -## ๐ŸงŸ Zombie Tickers (Warning) {#zombie-tickers-warning} - -In a Node.js environment, `Tempo.ticker()` uses background timers (`setTimeout`) to drive its pulses. If you do not explicitly stop a Ticker, it becomes a **"Zombie Ticker"** that continues to run indefinitely, even if the variable that created it has gone out of scope. - -### The Risks: -- **Process Hangs**: Node.js will not exit a process if there are active timers. Undisposed Tickers are a common cause of "mysterious hangs" at the end of test runs. -- **Test Inconsistency**: Leaked Tickers can continue to fire while subsequent tests are running, leading to flaky assertions and "impossible" state changes. -- **Memory Leaks**: Each active Ticker maintains closures that prevent garbage collection of the `Tempo` instance and its listeners. - -### The Solution: -Always use the **Disposer Pattern** (`using` or `await using`) or a `try...finally` block to guarantee cleanup: - -```typescript -// โœ…โœ… BEST: Automatic cleanup via 'using' -{ - using ticker = Tempo.ticker(1); - // ... logic ... -} // Stays clean: ticker stopped automatically here - -// โœ… GOOD: Manual cleanup in finally block (Required for captured variables) -let ticker; -try { - ticker = Tempo.ticker(1, (t) => { ... }); - // ... assertions ... -} finally { - ticker?.stop(); // Prevents "Zombie Tickers" even if assertions fail -} -``` - -::: warning -If you are using `const` or `let` without a `finally` block, an assertion failure will skip the `stop()` call, leaving a live timer in the event loop. Always prefer the `using` keyword or `try...finally` for industrial-grade resource management. -::: - -### `Ticker` Object -The object returned by `Tempo.ticker()` (or an instance of the `Ticker` class) implements the following interface: - -| Method / Property | Description | -| :--- | :--- | -| `on(event, cb)` | Registers a listener for the `'pulse'`, `'stop'`, or `'catch'` events. | -| `pulse()` | Manually triggers a pulse, advances state, and notifies listeners. Returns the new `Tempo`. | -| `info` | Read-only getter returning `{ next, ticks, limit, interval, stopped }`. | -| `stop()` | Stops the Ticker, clears active timers, and immediately resolves any pending async iteration Promises. | -| `[Symbol.dispose]` | Standard cleanup for `using` blocks. | -| `[Symbol.asyncDispose]` | Standard async cleanup for `await using` blocks. | -| `[Symbol.asyncIterator]` | Standard async iteration support (for `for await` loops). | - -## Reporting & Registry {#reporting-registry} - -The `Ticker` class maintains a static registry of all currently active Tickers. This is useful for debugging, monitoring, or cleanup checks. - -### `Ticker.active` -A static getter that returns an array of [`Ticker.Snapshot`](#tickersnapshot) objects for all active (non-stopped) Tickers. - -```typescript -import { Ticker } from '@magmacomputing/tempo-plugin-ticker'; - -// Get a report of all running tickers -const reports = Ticker.active; - -reports.forEach(({ ticker, next, ticks }) => { - console.log(`Ticker ${ticker} next pulse: ${next}, ticks so far: ${ticks}`); -}); -``` - -#### `Ticker.Snapshot` -```typescript -type Snapshot = { - ticker: Instance; // The Ticker instance (Proxy) itself - next: Tempo; // The next Tempo value to be emitted - ticks: number; // Number of pulses emitted so far - limit?: number; // The configured limit (if any) - interval: object; // The duration-based interval - stopped: boolean; // Whether the ticker is stopped -} -``` - -## ๐ŸŽฏ One-Shot Ticker (Meeting Alerts) - -You can use the Ticker as a "one-shot" timer for specific events by simply specifying a **seed** value. This is perfect for setting up a single alert (e.g., for a meeting) that cleans itself up immediately after firing. - -::: tip -**Seed-Only Logic**: Providing a `seed` (as a string or in an options object) without any other duration-based keys (`seconds`, `minutes`, etc.) or a `limit` implies a `limit: 1`. - -Effectively, `Tempo.ticker('Fri 10am')` and `Tempo.ticker({ seed: 'Fri 10am' })` and `Tempo.ticker({ seed: 'Fri 10am', limit: 1 })` are all treated as one-shot Tickers. - -**Inclusive Boundaries**: Termination conditions (`limit` and `until`) are **inclusive**. A Ticker with `limit: 1` will pulse exactly once before stopping. -::: - -```typescript -// Pattern A: Implicit one-shot via string seed -Tempo.ticker('Friday 10am', (t) => { - console.log(`Meeting alert: ${t.format('{hh}:{mi}')}`); -}); - -// Pattern B: Explicit one-shot via options -const event = { meeting: 'Friday 10am' }; - -Tempo.ticker({ - seed: { value: 'meeting', event } -}, (t) => { - console.log(`Meeting alert: ${t.format('{hh}:{mi}')}`); -}); -``` - -::: warning -**Future Seeds**: If the `seed` is in the future, the Ticker will remain dormant (waiting) until that time is reached. **Most Tickers emit an initial pulse immediately** (at the `seed` time or "now"), but a future seed will delay that first pulse until the specified time. -::: - -::: danger -**Persistence**: Ticker timers exist only **in-memory**. If the driving process (e.g., Node.js) terminates, any scheduled future pulses (including those from future seeds) are lost. For critical long-term scheduling, consider an external persistent job runner. -::: - -::: warning -While `limit: 1` handles the stop condition automatically, always remember that if you are using long-running Tickers without a limit, you **must** use the [Disposer Pattern](#zombie-tickers-warning) or manual `stop()` to avoid memory leaks and zombie processes. -::: - -## ๐Ÿงญ Advanced: Syncing Multiple Clocks - -If you need to show multiple timezones on a dashboard, avoid creating multiple Tickers. Instead, use a single **Master Ticker** to drive all views. This prevents "drift" between the clocks and is much more efficient. - -### Using Signals (Recommended) - -Signals (from Preact, Solid, or Vue) are perfect for this "one source, many views" pattern. - -```typescript -// 1. Master source of truth -const now = signal(new Tempo()); - -// 2. Drive the master from a single ticker -using _ = Tempo.ticker(1, (t) => now.value = t); - -// 3. Derived timezones update automatically and stay 100% in sync -const sydney = computed(() => now.value.set({ timeZone: 'Australia/Sydney' })); -const london = computed(() => now.value.set({ timeZone: 'Europe/London' })); -``` - -### Using Async Generators (Framework-Agnostic) - -If you are not using a reactive framework, you can use the same pattern with an `AsyncGenerator` to derive all clocks from a single pulse. - -```typescript -// One generator, one interval, zero drift. -await using master = Tempo.ticker(1); - -for await (const t of master) { - const clocks = { - sydney: t.set({ timeZone: 'Australia/Sydney' }), - ny: t.set({ timeZone: 'America/New_York' }), - london: t.set({ timeZone: 'Europe/London' }) - }; - - renderDashboard(clocks); -} -``` - -## Licensing - -This is a **Premium** plugin. Usage requires an active, cryptographically signed Tempo license token with the `ticker` scope enabled. - -::: tip -
- - Tempo License Registry - -
- ๐Ÿ‘‰ Go to the Tempo License Registry ๐Ÿ‘ˆ
- Manage your subscriptions and retrieve your license key. -
-
-::: diff --git a/packages/tempo/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') } ]