@final-commerce/common 1.1.4-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +224 -0
- package/bin/gate-done.mjs +161 -0
- package/bin/gate-i18n-sync.mjs +351 -0
- package/bin/gate-save.mjs +146 -0
- package/bin/gate-setup.mjs +227 -0
- package/bin/gate-start.mjs +101 -0
- package/bin/prepare-commit-msg.sh +16 -0
- package/commitlint/index.mjs +14 -0
- package/dist/index.d.mts +468 -0
- package/dist/index.d.ts +468 -0
- package/dist/index.js +2702 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2565 -0
- package/dist/index.mjs.map +1 -0
- package/dist/interfaces-6GnQyqYd.d.mts +598 -0
- package/dist/interfaces-6GnQyqYd.d.ts +598 -0
- package/dist/pos-types/index.d.mts +1782 -0
- package/dist/pos-types/index.d.ts +1782 -0
- package/dist/pos-types/index.js +144 -0
- package/dist/pos-types/index.js.map +1 -0
- package/dist/pos-types/index.mjs +114 -0
- package/dist/pos-types/index.mjs.map +1 -0
- package/eslint/backend-nestjs.mjs +30 -0
- package/eslint/frontend-react.mjs +27 -0
- package/eslint/library.mjs +19 -0
- package/lint-staged/index.mjs +4 -0
- package/package.json +160 -0
- package/prettier/index.json +10 -0
- package/typescript/backend-nestjs.json +13 -0
- package/typescript/base.json +13 -0
- package/typescript/frontend-react.json +15 -0
- package/typescript/library.json +12 -0
- package/vitest/backend.ts +20 -0
- package/vitest/frontend.ts +22 -0
- package/vitest/library.ts +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# @final-commerce/common
|
|
2
|
+
|
|
3
|
+
Shared utilities, types, and constants for Final Commerce applications. This package provides framework-agnostic functionality that works across both frontend (React, Vue, etc.) and backend (Node.js, NestJS) environments.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @final-commerce/common
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Imports
|
|
12
|
+
|
|
13
|
+
Everything is re-exported from the package root:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { CurrencyCode, toMinorUnits, formatCurrency, Amount } from '@final-commerce/common';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Sub-path imports also work:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { toMinorUnits } from '@final-commerce/common/dist/utils/currency.util';
|
|
23
|
+
import { CurrencyCode } from '@final-commerce/common/dist/enums';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Enums
|
|
29
|
+
|
|
30
|
+
### `CurrencyCode`
|
|
31
|
+
|
|
32
|
+
ISO 4217 currency codes. 41 currencies across three decimal groups:
|
|
33
|
+
|
|
34
|
+
| Decimal Places | Currencies |
|
|
35
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| **2** (standard) | USD, EUR, GBP, CAD, AUD, NZD, CHF, CNY, INR, MXN, BRL, ZAR, SGD, HKD, SEK, NOK, DKK, PLN, THB, MYR, PHP, IDR, AED, SAR, ILS, TRY, RUB |
|
|
37
|
+
| **0** (zero-decimal) | JPY, KRW, VND, CLP, ISK, HUF, TWD |
|
|
38
|
+
| **3** (three-decimal) | KWD, BHD, OMR, JOD, TND, LYD, IQD |
|
|
39
|
+
|
|
40
|
+
## Interfaces
|
|
41
|
+
|
|
42
|
+
### `Amount`
|
|
43
|
+
|
|
44
|
+
Represents a monetary value stored in the database.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
{ amount: 3122, currency: 'USD', minorUnits: 2 } // = $31.22
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Field | Type | Description |
|
|
51
|
+
| ------------ | -------------- | --------------------------------- |
|
|
52
|
+
| `amount` | `number` | Value in minor units (e.g. cents) |
|
|
53
|
+
| `currency` | `CurrencyCode` | ISO 4217 code |
|
|
54
|
+
| `minorUnits` | `number` | Decimal places (2, 0, or 3) |
|
|
55
|
+
|
|
56
|
+
### `CurrencyConfig`
|
|
57
|
+
|
|
58
|
+
| Currency | Minor Units | Symbol | Example |
|
|
59
|
+
| -------- | ----------- | ------ | -------- |
|
|
60
|
+
| USD | 2 | $ | $10.50 |
|
|
61
|
+
| EUR | 2 | € | €10.50 |
|
|
62
|
+
| GBP | 2 | £ | £10.50 |
|
|
63
|
+
| JPY | 0 | ¥ | ¥1000 |
|
|
64
|
+
| KWD | 3 | KD | KD10.500 |
|
|
65
|
+
|
|
66
|
+
| Field | Type | Description |
|
|
67
|
+
| ---------------- | ---------------------- | ------------------------------------ |
|
|
68
|
+
| `minorUnits` | `number` | Decimal places |
|
|
69
|
+
| `symbol` | `string` | Display symbol (`$`, `€`, `¥`, etc.) |
|
|
70
|
+
| `symbolPosition` | `'prefix' \| 'suffix'` | Where the symbol goes |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Utility Functions
|
|
75
|
+
|
|
76
|
+
### Conversion
|
|
77
|
+
|
|
78
|
+
| Function | In | Out | What it does |
|
|
79
|
+
| ---------------------------------------------- | ------------------ | ------ | ------------------------------------------------------------ |
|
|
80
|
+
| `toMinorUnits(amount, currency)` | `(10.50, 'USD')` | `1050` | Major to minor units |
|
|
81
|
+
| `fromMinorUnits(minorAmount, currency)` | `(1050, 'USD')` | `10.5` | Minor to major units |
|
|
82
|
+
| `stringifyMinorUnits(minorAmount, minorUnits)` | `(1050, 2)` | `10.5` | Minor to major using decimal count directly |
|
|
83
|
+
| `parseAmountToMinorUnits(str, currency)` | `('10.50', 'USD')` | `1050` | Parse string input to minor units. Returns `null` on failure |
|
|
84
|
+
|
|
85
|
+
### Formatting
|
|
86
|
+
|
|
87
|
+
| Function | In | Out | What it does |
|
|
88
|
+
| --------------------------------------------------- | --------------- | ---------- | ----------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `formatCurrency(minorAmount, currency, options?)` | `(1050, 'USD')` | `'$10.50'` | Display string with symbol and grouping |
|
|
90
|
+
| `formatMinorUnitsToString(minorAmount, minorUnits)` | `(1050, 2)` | `'10.5'` | Plain decimal string (no symbol, no trailing zeros). Useful for API payloads (e.g. WooCommerce) |
|
|
91
|
+
|
|
92
|
+
`formatCurrency` options:
|
|
93
|
+
|
|
94
|
+
- `includeSymbol` (default: `true`) — include currency symbol
|
|
95
|
+
- `useGrouping` (default: `true`) — thousands separators
|
|
96
|
+
|
|
97
|
+
### Arithmetic
|
|
98
|
+
|
|
99
|
+
All inputs and outputs are in minor units (integers).
|
|
100
|
+
|
|
101
|
+
| Function | Example | Result |
|
|
102
|
+
| ------------------------------------ | ------------- | ------ |
|
|
103
|
+
| `addAmounts(a, b)` | `(1050, 250)` | `1300` |
|
|
104
|
+
| `subtractAmounts(a, b)` | `(1050, 250)` | `800` |
|
|
105
|
+
| `multiplyAmount(amount, multiplier)` | `(1050, 2)` | `2100` |
|
|
106
|
+
| `divideAmount(amount, divisor)` | `(1050, 2)` | `525` |
|
|
107
|
+
| `calculatePercentage(amount, pct)` | `(10000, 15)` | `1500` |
|
|
108
|
+
|
|
109
|
+
`divideAmount` throws on division by zero. `calculatePercentage` takes percentage as a whole number (15 = 15%, not 0.15).
|
|
110
|
+
|
|
111
|
+
### Config / Validation
|
|
112
|
+
|
|
113
|
+
| Function | What it does |
|
|
114
|
+
| ---------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
115
|
+
| `getCurrencyConfig(currency)` | Returns `CurrencyConfig` for a code. Falls back to 2-decimal default for unknown currencies |
|
|
116
|
+
| `getMinorUnitMultiplier(currency)` | Returns `10^minorUnits` (100 for USD, 1 for JPY, 1000 for KWD) |
|
|
117
|
+
| `getMinorUnits(currency)` | Returns decimal place count for a currency |
|
|
118
|
+
| `isSupportedCurrency(currency)` | `true` if the currency has explicit config |
|
|
119
|
+
| `parseCurrencyCode(value)` | Validates a raw string is a valid `CurrencyCode`. Throws with supported codes list if invalid |
|
|
120
|
+
|
|
121
|
+
### Amount Object Helpers
|
|
122
|
+
|
|
123
|
+
Used in BuilderHub for converting between user input and the `Amount` storage format.
|
|
124
|
+
|
|
125
|
+
| Function | Example | Result |
|
|
126
|
+
| ------------------------------- | ---------------------------------------------------- | -------------------------------------------------- |
|
|
127
|
+
| `createAmount(value, currency)` | `('31.22', 'USD')` | `{ amount: 3122, currency: 'USD', minorUnits: 2 }` |
|
|
128
|
+
| `amountToString(amount)` | `({ amount: 3122, currency: 'USD', minorUnits: 2 })` | `'31.22'` |
|
|
129
|
+
|
|
130
|
+
`createAmount` accepts `string | number | null | undefined`. Returns `undefined` for null, undefined, empty string, or NaN input.
|
|
131
|
+
`amountToString` returns `''` for null/undefined input.
|
|
132
|
+
|
|
133
|
+
### Constants
|
|
134
|
+
|
|
135
|
+
`CURRENCY_CONFIG` — a `Record<CurrencyCode | string, CurrencyConfig>` mapping all 41 currencies to their config (symbol, decimal places, symbol position).
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Local Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm install
|
|
143
|
+
npm run build # build with tsup (CJS + ESM + types → dist/)
|
|
144
|
+
npm run dev # watch mode — rebuilds on file changes
|
|
145
|
+
npm test # vitest in watch mode
|
|
146
|
+
npm run test:run # single test run
|
|
147
|
+
npm run test:ui # vitest UI
|
|
148
|
+
npm run test:coverage
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Link locally to another project
|
|
152
|
+
|
|
153
|
+
Use `npm link` to test changes in a consuming project without publishing:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# 1. In this package (common/) — build first so dist/ is up to date
|
|
157
|
+
npm run build
|
|
158
|
+
npm link
|
|
159
|
+
|
|
160
|
+
# 2. In the consuming project (e.g. hub-api/) — clear node_modules to avoid stale/duplicate copies
|
|
161
|
+
rm -rf node_modules/@final-commerce/common
|
|
162
|
+
npm link @final-commerce/common
|
|
163
|
+
|
|
164
|
+
# 3. Verify the symlink points to your local copy
|
|
165
|
+
ls -la node_modules/@final-commerce/common
|
|
166
|
+
# Should show a symlink → /path/to/common
|
|
167
|
+
|
|
168
|
+
# 4. When done, unlink and restore the published version
|
|
169
|
+
npm unlink @final-commerce/common # in consumer
|
|
170
|
+
npm install # reinstall published version
|
|
171
|
+
npm unlink # in common/
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
> **If you change source files in common**, run `npm run build` again (or use `npm run dev` for watch mode). The symlink means the consumer always reads from common's `dist/`, so it picks up new builds immediately.
|
|
175
|
+
|
|
176
|
+
### Quick verification with `node -p`
|
|
177
|
+
|
|
178
|
+
After building, verify exports directly from the terminal:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Check the package loads
|
|
182
|
+
node -p "require('./dist/index.js')"
|
|
183
|
+
|
|
184
|
+
# Test a conversion
|
|
185
|
+
node -p "const c = require('./dist/index.js'); c.toMinorUnits(10.50, 'USD')"
|
|
186
|
+
# → 1050
|
|
187
|
+
|
|
188
|
+
# Test formatting
|
|
189
|
+
node -p "const c = require('./dist/index.js'); c.formatCurrency(1050, 'USD')"
|
|
190
|
+
# → $10.50
|
|
191
|
+
|
|
192
|
+
# Test zero-decimal currency
|
|
193
|
+
node -p "const c = require('./dist/index.js'); c.toMinorUnits(1000, 'JPY')"
|
|
194
|
+
# → 1000
|
|
195
|
+
|
|
196
|
+
# Test three-decimal currency
|
|
197
|
+
node -p "const c = require('./dist/index.js'); c.toMinorUnits(10.5, 'KWD')"
|
|
198
|
+
# → 10500
|
|
199
|
+
|
|
200
|
+
# Test Amount helpers
|
|
201
|
+
node -p "const c = require('./dist/index.js'); c.createAmount('31.22', 'USD')"
|
|
202
|
+
# → { amount: 3122, currency: 'USD', minorUnits: 2 }
|
|
203
|
+
|
|
204
|
+
node -p "const c = require('./dist/index.js'); c.amountToString({ amount: 3122, currency: 'USD', minorUnits: 2 })"
|
|
205
|
+
# → 31.22
|
|
206
|
+
|
|
207
|
+
# List all exported keys
|
|
208
|
+
node -p "Object.keys(require('./dist/index.js'))"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Verify from a linked project
|
|
212
|
+
|
|
213
|
+
After `npm link`, verify the link works in the consuming project:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# In the consuming project directory
|
|
217
|
+
node -p "require('@final-commerce/common')"
|
|
218
|
+
node -p "const c = require('@final-commerce/common'); c.formatCurrency(2500, 'EUR')"
|
|
219
|
+
# → €25.00
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
Private — Final Commerce internal use only.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gate-done — pre-push hook.
|
|
4
|
+
*
|
|
5
|
+
* Runs four sweeps inside a 90-second wall-clock budget:
|
|
6
|
+
*
|
|
7
|
+
* 1. npm audit — known CVE check against the installed dependency tree
|
|
8
|
+
* 2. gitleaks — credential / secret scanning (git log + staged)
|
|
9
|
+
* 3. vitest — full test suite with coverage threshold enforcement
|
|
10
|
+
* 4. bundle — size-limit or custom bundle-size script (if configured)
|
|
11
|
+
*
|
|
12
|
+
* Any sweep failure exits 1, blocking the push.
|
|
13
|
+
* Budget overrun also exits 1.
|
|
14
|
+
*
|
|
15
|
+
* Install as Husky pre-push hook:
|
|
16
|
+
* echo "npx gate-done" > .husky/pre-push
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { resolve } from 'node:path';
|
|
22
|
+
|
|
23
|
+
const BUDGET_MS = 90_000;
|
|
24
|
+
const t0 = Date.now();
|
|
25
|
+
|
|
26
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function remaining() {
|
|
29
|
+
return Math.max(0, BUDGET_MS - (Date.now() - t0));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function elapsed() {
|
|
33
|
+
return ((Date.now() - t0) / 1000).toFixed(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function checkBudget(label) {
|
|
37
|
+
if (remaining() <= 0) {
|
|
38
|
+
fail(`90s budget exhausted before "${label}" could complete. Push blocked.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run a command, streaming output. Returns the spawnSync result.
|
|
44
|
+
* Passes remaining budget as the process timeout so a runaway child
|
|
45
|
+
* cannot exceed the wall-clock limit on its own.
|
|
46
|
+
*/
|
|
47
|
+
function run(cmd) {
|
|
48
|
+
return spawnSync(cmd, {
|
|
49
|
+
shell: true,
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
stdio: 'inherit',
|
|
52
|
+
timeout: remaining(),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function capture(cmd) {
|
|
57
|
+
return spawnSync(cmd, { shell: true, encoding: 'utf8', timeout: remaining() });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fail(msg) {
|
|
61
|
+
process.stderr.write(`\x1b[31m✖ [${elapsed()}s] ${msg}\x1b[0m\n`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function warn(msg) {
|
|
66
|
+
process.stdout.write(`\x1b[33m⚠ [${elapsed()}s] ${msg}\x1b[0m\n`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function info(msg) {
|
|
70
|
+
process.stdout.write(`\x1b[36mℹ [${elapsed()}s] ${msg}\x1b[0m\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ok(msg) {
|
|
74
|
+
process.stdout.write(`\x1b[32m✔ [${elapsed()}s] ${msg}\x1b[0m\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── 1. npm audit ─────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
info('Sweep 1/4 — npm audit (known CVEs)…');
|
|
80
|
+
checkBudget('npm audit');
|
|
81
|
+
|
|
82
|
+
const auditResult = run('npm audit --audit-level=high');
|
|
83
|
+
if (auditResult.status !== 0) {
|
|
84
|
+
fail('npm audit found high or critical vulnerabilities. Run `npm audit` for details.');
|
|
85
|
+
}
|
|
86
|
+
ok('npm audit: no high/critical vulnerabilities.');
|
|
87
|
+
|
|
88
|
+
// ── 2. gitleaks ───────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
info('Sweep 2/4 — gitleaks secrets scan…');
|
|
91
|
+
checkBudget('gitleaks');
|
|
92
|
+
|
|
93
|
+
const hasGitleaks = capture('command -v gitleaks 2>/dev/null').status === 0;
|
|
94
|
+
|
|
95
|
+
if (!hasGitleaks) {
|
|
96
|
+
warn(
|
|
97
|
+
'gitleaks not found in PATH — secrets scan skipped.\n' +
|
|
98
|
+
' Install: https://github.com/gitleaks/gitleaks#installing',
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
// Scan the full git history visible from HEAD — correct mode for a pre-push hook.
|
|
102
|
+
// --no-git-serial scans refs in parallel for speed within the budget.
|
|
103
|
+
const glResult = run('gitleaks git --exit-code 1 .');
|
|
104
|
+
if (glResult.status !== 0) {
|
|
105
|
+
fail('gitleaks detected a potential secret. Remove it and retry the push.');
|
|
106
|
+
}
|
|
107
|
+
ok('gitleaks: no secrets detected.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── 3. vitest coverage ────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
info('Sweep 3/4 — Vitest test suite + coverage thresholds…');
|
|
113
|
+
checkBudget('vitest');
|
|
114
|
+
|
|
115
|
+
const vitestResult = run('npx vitest run --coverage');
|
|
116
|
+
|
|
117
|
+
if (vitestResult.status !== 0) {
|
|
118
|
+
fail('Vitest coverage gate failed — fix failing tests or low-coverage areas before pushing.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ok('Vitest: all tests passed, coverage thresholds met.');
|
|
122
|
+
|
|
123
|
+
// ── 4. bundle size ────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
info('Sweep 4/4 — bundle size audit…');
|
|
126
|
+
checkBudget('bundle audit');
|
|
127
|
+
|
|
128
|
+
const pkgPath = resolve(process.cwd(), 'package.json');
|
|
129
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
130
|
+
|
|
131
|
+
const hasBundleSizeScript = Boolean(pkg.scripts?.['bundle-size']);
|
|
132
|
+
const hasSizeLimit = Boolean(pkg.devDependencies?.['size-limit']) || Boolean(pkg.dependencies?.['size-limit']);
|
|
133
|
+
const hasBundlesize = Boolean(pkg.devDependencies?.['bundlesize']) || Boolean(pkg.dependencies?.['bundlesize']);
|
|
134
|
+
const hasBuildScript = Boolean(pkg.scripts?.build);
|
|
135
|
+
|
|
136
|
+
if (hasBundleSizeScript) {
|
|
137
|
+
const result = run('npm run bundle-size');
|
|
138
|
+
if (result.status !== 0) fail('bundle-size script reported a violation. Push blocked.');
|
|
139
|
+
ok('Bundle size: within limits.');
|
|
140
|
+
} else if (hasSizeLimit) {
|
|
141
|
+
const result = run('npx size-limit');
|
|
142
|
+
if (result.status !== 0) fail('size-limit reported a violation. Push blocked.');
|
|
143
|
+
ok('Bundle size: size-limit passed.');
|
|
144
|
+
} else if (hasBundlesize) {
|
|
145
|
+
const result = run('npx bundlesize');
|
|
146
|
+
if (result.status !== 0) fail('bundlesize reported a violation. Push blocked.');
|
|
147
|
+
ok('Bundle size: bundlesize passed.');
|
|
148
|
+
} else if (hasBuildScript) {
|
|
149
|
+
// No size tool configured — at minimum verify the build succeeds cleanly.
|
|
150
|
+
info('No size-limit/bundlesize configured — running build as a smoke-check.');
|
|
151
|
+
const result = run('npm run build');
|
|
152
|
+
if (result.status !== 0) fail('Build failed during gate-done. Push blocked.');
|
|
153
|
+
ok('Build: succeeded (no size budget configured).');
|
|
154
|
+
} else {
|
|
155
|
+
info('No build or bundle-size script found — skipping bundle sweep.');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── done ─────────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
ok(`gate-done completed in ${elapsed()}s (budget: 90s). Push proceeding.`);
|
|
161
|
+
process.exit(0);
|