@zerotal/core 1.0.0

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.
Files changed (201) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +128 -0
  4. package/package.json +72 -0
  5. package/src/application/Application.ts +1671 -0
  6. package/src/application/BootDoctor.ts +108 -0
  7. package/src/application/DevErrorPage.ts +567 -0
  8. package/src/application/ExceptionHandler.ts +183 -0
  9. package/src/application/currentApp.ts +73 -0
  10. package/src/assets/assets.ts +79 -0
  11. package/src/assets/index.ts +16 -0
  12. package/src/auth/AuthenticatedUser.ts +18 -0
  13. package/src/build/PackageLinter.ts +146 -0
  14. package/src/build/PackageScaffold.ts +127 -0
  15. package/src/build/codemod.ts +64 -0
  16. package/src/build/index.ts +12 -0
  17. package/src/command/Command.ts +254 -0
  18. package/src/command/CommandRunner.ts +593 -0
  19. package/src/command/OutputWriter.ts +61 -0
  20. package/src/command/builtin/CompileCommand.ts +46 -0
  21. package/src/command/builtin/CssBuildCommand.ts +71 -0
  22. package/src/command/builtin/KeyGenerateCommand.ts +58 -0
  23. package/src/command/builtin/LintPackagesCommand.ts +72 -0
  24. package/src/command/builtin/MakeCommandCommand.ts +85 -0
  25. package/src/command/builtin/MakeControllerCommand.ts +95 -0
  26. package/src/command/builtin/MakeEventCommand.ts +85 -0
  27. package/src/command/builtin/MakeJobCommand.ts +53 -0
  28. package/src/command/builtin/MakeListenerCommand.ts +35 -0
  29. package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
  30. package/src/command/builtin/MakeNotificationCommand.ts +48 -0
  31. package/src/command/builtin/MakeObserverCommand.ts +78 -0
  32. package/src/command/builtin/MakePackageCommand.ts +45 -0
  33. package/src/command/builtin/MakePolicyCommand.ts +66 -0
  34. package/src/command/builtin/MakeProviderCommand.ts +75 -0
  35. package/src/command/builtin/MakeRequestCommand.ts +47 -0
  36. package/src/command/builtin/MakeResourceCommand.ts +61 -0
  37. package/src/command/builtin/MakeTestCommand.ts +120 -0
  38. package/src/command/builtin/ReloadCommand.ts +52 -0
  39. package/src/command/builtin/ReplCommand.ts +174 -0
  40. package/src/command/builtin/RouteListCommand.ts +188 -0
  41. package/src/command/builtin/ServeCommand.ts +321 -0
  42. package/src/command/builtin/StartCommand.ts +3 -0
  43. package/src/command/builtin/StatusCommand.ts +71 -0
  44. package/src/command/builtin/TestCommand.ts +172 -0
  45. package/src/command/builtin/WorkerCommand.ts +27 -0
  46. package/src/command/builtin/index.ts +53 -0
  47. package/src/command/scaffold/worker.ts.txt +12 -0
  48. package/src/command/scaffold/zerotal.ts.txt +26 -0
  49. package/src/command/startZerotal.ts +55 -0
  50. package/src/config/AppConfig.ts +253 -0
  51. package/src/config/ConfigLoader.ts +117 -0
  52. package/src/config/ConfigManager.ts +169 -0
  53. package/src/config/index.ts +46 -0
  54. package/src/config/registry.ts +59 -0
  55. package/src/config/validation.ts +117 -0
  56. package/src/container/Container.ts +606 -0
  57. package/src/container/ContextualBindingBuilder.ts +57 -0
  58. package/src/container/ScopedResolver.ts +117 -0
  59. package/src/container/index.ts +32 -0
  60. package/src/container/inject.ts +55 -0
  61. package/src/container/types.ts +71 -0
  62. package/src/context/RequestContext.ts +91 -0
  63. package/src/contracts/auth.ts +24 -0
  64. package/src/contracts/index.ts +23 -0
  65. package/src/contracts/session.ts +70 -0
  66. package/src/contracts/transaction.ts +26 -0
  67. package/src/conventions/ConventionLoader.ts +128 -0
  68. package/src/conventions/builtinConcerns.ts +131 -0
  69. package/src/crypt/Crypt.ts +141 -0
  70. package/src/crypt/URLSigner.ts +96 -0
  71. package/src/datetime/Carbon.ts +1396 -0
  72. package/src/datetime/CarbonInterval.ts +421 -0
  73. package/src/datetime/clock.ts +28 -0
  74. package/src/datetime/index.ts +23 -0
  75. package/src/datetime/temporal-shim.ts +1 -0
  76. package/src/dev/BuildOutput.ts +131 -0
  77. package/src/dev/CssPlugins.ts +184 -0
  78. package/src/dev/DevBuildHook.ts +74 -0
  79. package/src/dev/DevOrchestrator.ts +213 -0
  80. package/src/dev/DevReloadMiddleware.ts +101 -0
  81. package/src/dev/DevReloadServer.ts +85 -0
  82. package/src/dev/DevWsServer.ts +45 -0
  83. package/src/dev/index.ts +19 -0
  84. package/src/dev/reloadClient.ts +39 -0
  85. package/src/env/Def.ts +232 -0
  86. package/src/env/EnvSchema.ts +105 -0
  87. package/src/env/index.ts +34 -0
  88. package/src/env/t.ts +128 -0
  89. package/src/errors/ConfigError.ts +12 -0
  90. package/src/errors/ContainerErrors.ts +143 -0
  91. package/src/errors/HttpError.ts +127 -0
  92. package/src/errors/ValidationError.ts +19 -0
  93. package/src/errors/ZerotalError.ts +25 -0
  94. package/src/errors/index.ts +46 -0
  95. package/src/events/CallQueuedListener.ts +66 -0
  96. package/src/events/Emitter.ts +280 -0
  97. package/src/events/EventFake.ts +160 -0
  98. package/src/events/FrameworkEvents.ts +252 -0
  99. package/src/facade/Facade.ts +101 -0
  100. package/src/facade/facades/App.ts +155 -0
  101. package/src/facade/facades/Artisan.ts +63 -0
  102. package/src/facade/facades/Config.ts +21 -0
  103. package/src/facade/facades/Events.ts +19 -0
  104. package/src/facade/facades/index.ts +28 -0
  105. package/src/global.d.ts +9 -0
  106. package/src/hash/Hash.ts +60 -0
  107. package/src/health/Health.ts +221 -0
  108. package/src/health/index.ts +27 -0
  109. package/src/helpers/Collection.ts +435 -0
  110. package/src/helpers/config.ts +59 -0
  111. package/src/helpers/fluent.ts +52 -0
  112. package/src/helpers/html.ts +11 -0
  113. package/src/helpers/index.ts +266 -0
  114. package/src/helpers/make.ts +35 -0
  115. package/src/helpers/markdown.ts +73 -0
  116. package/src/helpers/pageElements.ts +27 -0
  117. package/src/helpers/request.ts +62 -0
  118. package/src/helpers/response.ts +411 -0
  119. package/src/helpers/str.ts +208 -0
  120. package/src/http/Http.ts +298 -0
  121. package/src/http/HttpClient.ts +289 -0
  122. package/src/http/Resource.ts +171 -0
  123. package/src/http/UploadedFile.ts +204 -0
  124. package/src/http/Uri.ts +490 -0
  125. package/src/http/index.ts +46 -0
  126. package/src/http/negotiate.ts +213 -0
  127. package/src/http/originGuard.ts +76 -0
  128. package/src/http/sniffContentType.ts +105 -0
  129. package/src/http/url.ts +204 -0
  130. package/src/http/withHeaders.ts +24 -0
  131. package/src/index.ts +250 -0
  132. package/src/lock/LockManager.ts +228 -0
  133. package/src/lock/config.ts +49 -0
  134. package/src/lock/drivers/LockDriver.ts +32 -0
  135. package/src/lock/drivers/MemoryLockDriver.ts +52 -0
  136. package/src/lock/drivers/RedisLockDriver.ts +58 -0
  137. package/src/lock/drivers/SqliteLockDriver.ts +85 -0
  138. package/src/lock/errors.ts +20 -0
  139. package/src/lock/facades/Lock.ts +114 -0
  140. package/src/lock/index.ts +53 -0
  141. package/src/logger/Log.ts +35 -0
  142. package/src/logger/LogManager.ts +430 -0
  143. package/src/logger/LoggerMiddleware.ts +125 -0
  144. package/src/logger/channels/ConsoleChannel.ts +139 -0
  145. package/src/logger/channels/DailyChannel.ts +74 -0
  146. package/src/logger/channels/NullChannel.ts +17 -0
  147. package/src/logger/channels/SingleChannel.ts +34 -0
  148. package/src/logger/channels/StackChannel.ts +29 -0
  149. package/src/logger/config.ts +90 -0
  150. package/src/logger/format.ts +96 -0
  151. package/src/logger/frameworkLog.ts +93 -0
  152. package/src/logger/index.ts +68 -0
  153. package/src/logger/renderTable.ts +111 -0
  154. package/src/logger/types.ts +212 -0
  155. package/src/macros/config.macro.ts +50 -0
  156. package/src/metrics/HttpMetrics.ts +114 -0
  157. package/src/metrics/index.ts +18 -0
  158. package/src/middleware/BaseMiddleware.ts +72 -0
  159. package/src/middleware/CorsMiddleware.ts +152 -0
  160. package/src/middleware/RateLimiter.ts +255 -0
  161. package/src/middleware/SecureHeadersMiddleware.ts +127 -0
  162. package/src/middleware/ThrottleMiddleware.ts +252 -0
  163. package/src/middleware/WebhookMiddleware.ts +204 -0
  164. package/src/pipeline/ContextRegistry.ts +42 -0
  165. package/src/pipeline/HttpContext.ts +865 -0
  166. package/src/pipeline/Pipeline.ts +150 -0
  167. package/src/pipeline/currentPage.ts +46 -0
  168. package/src/pipeline/types.ts +80 -0
  169. package/src/provider/LockProvider.ts +64 -0
  170. package/src/provider/LogProvider.ts +137 -0
  171. package/src/provider/ServiceProvider.ts +84 -0
  172. package/src/provider/StorageProvider.ts +45 -0
  173. package/src/router/FileRouter.ts +526 -0
  174. package/src/router/Route.ts +76 -0
  175. package/src/router/RouteHandler.ts +335 -0
  176. package/src/router/Router.ts +1247 -0
  177. package/src/router/domain.ts +65 -0
  178. package/src/security/index.ts +22 -0
  179. package/src/storage/FakeDisk.ts +233 -0
  180. package/src/storage/StorageFilesMiddleware.ts +150 -0
  181. package/src/storage/StorageManager.ts +173 -0
  182. package/src/storage/config.ts +47 -0
  183. package/src/storage/drivers/LocalDriver.ts +138 -0
  184. package/src/storage/drivers/S3Driver.ts +169 -0
  185. package/src/storage/errors.ts +135 -0
  186. package/src/storage/facades/Storage.ts +3 -0
  187. package/src/storage/global.d.ts +7 -0
  188. package/src/storage/index.ts +22 -0
  189. package/src/storage/root.ts +59 -0
  190. package/src/storage/types.ts +104 -0
  191. package/src/support/appKey.ts +38 -0
  192. package/src/support/cookie.ts +72 -0
  193. package/src/support/crypto.ts +52 -0
  194. package/src/support/deepMerge.ts +117 -0
  195. package/src/support/env.ts +71 -0
  196. package/src/support/network.ts +79 -0
  197. package/src/support/port.ts +197 -0
  198. package/src/support/str.ts +122 -0
  199. package/src/view/FileRouteResolver.ts +59 -0
  200. package/src/view/index.ts +144 -0
  201. package/src/view/jsx-runtime.ts +233 -0
@@ -0,0 +1,421 @@
1
+ /**
2
+ * `CarbonInterval` — an immutable duration value object backed by
3
+ * `Temporal.Duration`. Every modifier returns a new instance; arithmetic is
4
+ * calendar-aware (months of varying length resolve correctly).
5
+ */
6
+
7
+ import { Temporal } from "./temporal-shim.ts";
8
+
9
+ // ── Types ─────────────────────────────────────────────────────────────────────
10
+
11
+ /** A plain object describing a duration by its individual time-unit fields. */
12
+ export interface DurationLike {
13
+ years?: number;
14
+ months?: number;
15
+ weeks?: number;
16
+ days?: number;
17
+ hours?: number;
18
+ minutes?: number;
19
+ seconds?: number;
20
+ milliseconds?: number;
21
+ microseconds?: number;
22
+ nanoseconds?: number;
23
+ }
24
+
25
+ // ── CarbonInterval ────────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * An immutable, calendar-aware duration backed by `Temporal.Duration`; every
29
+ * modifier returns a new instance. Build durations fluently, do arithmetic on
30
+ * them, apply them to a {@link Carbon}, and render them for humans or as ISO 8601.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { Carbon, CarbonInterval } from "@zerotal/core/carbon";
35
+ *
36
+ * const span = CarbonInterval.days(1).andHours(2).andMinutes(30);
37
+ * span.forHumans(); // "1 day 2 hours 30 minutes"
38
+ * span.toISO(); // "P1DT2H30M"
39
+ * Carbon.now().add(span); // apply to a date
40
+ *
41
+ * CarbonInterval.fromISO("PT90M").cascade(); // → 1 hour 30 minutes
42
+ * ```
43
+ */
44
+ export class CarbonInterval {
45
+ /** @internal — the backing Temporal.Duration (treat as immutable) */
46
+ readonly _duration: Temporal.Duration;
47
+
48
+ constructor(duration: Temporal.Duration | DurationLike = {}) {
49
+ if (duration instanceof Temporal.Duration) {
50
+ this._duration = duration;
51
+ } else {
52
+ this._duration = new Temporal.Duration(
53
+ duration.years ?? 0,
54
+ duration.months ?? 0,
55
+ duration.weeks ?? 0,
56
+ duration.days ?? 0,
57
+ duration.hours ?? 0,
58
+ duration.minutes ?? 0,
59
+ duration.seconds ?? 0,
60
+ duration.milliseconds ?? 0,
61
+ duration.microseconds ?? 0,
62
+ duration.nanoseconds ?? 0,
63
+ );
64
+ }
65
+ }
66
+
67
+ // ── Internal wrap ─────────────────────────────────────────────────────────
68
+
69
+ private _wrap(duration: Temporal.Duration): CarbonInterval {
70
+ const interval = Object.create(CarbonInterval.prototype) as CarbonInterval;
71
+ (interval as unknown as { _duration: Temporal.Duration })._duration = duration;
72
+ return interval;
73
+ }
74
+
75
+ // ── Static factories ──────────────────────────────────────────────────────
76
+
77
+ static years(amount: number): CarbonInterval {
78
+ return new CarbonInterval({ years: amount });
79
+ }
80
+ static months(amount: number): CarbonInterval {
81
+ return new CarbonInterval({ months: amount });
82
+ }
83
+ static weeks(amount: number): CarbonInterval {
84
+ return new CarbonInterval({ weeks: amount });
85
+ }
86
+ static days(amount: number): CarbonInterval {
87
+ return new CarbonInterval({ days: amount });
88
+ }
89
+ static hours(amount: number): CarbonInterval {
90
+ return new CarbonInterval({ hours: amount });
91
+ }
92
+ static minutes(amount: number): CarbonInterval {
93
+ return new CarbonInterval({ minutes: amount });
94
+ }
95
+ static seconds(amount: number): CarbonInterval {
96
+ return new CarbonInterval({ seconds: amount });
97
+ }
98
+ static milliseconds(amount: number): CarbonInterval {
99
+ return new CarbonInterval({ milliseconds: amount });
100
+ }
101
+ static microseconds(amount: number): CarbonInterval {
102
+ return new CarbonInterval({ microseconds: amount });
103
+ }
104
+ static nanoseconds(amount: number): CarbonInterval {
105
+ return new CarbonInterval({ nanoseconds: amount });
106
+ }
107
+
108
+ /**
109
+ * Wrap an existing Temporal.Duration.
110
+ *
111
+ * @example
112
+ * const dur = Temporal.Duration.from('P1Y2M3DT4H5M6S');
113
+ * const interval = CarbonInterval.fromDuration(dur);
114
+ */
115
+ static fromDuration(duration: Temporal.Duration): CarbonInterval {
116
+ return new CarbonInterval(duration);
117
+ }
118
+
119
+ /**
120
+ * Parse an ISO 8601 duration string.
121
+ *
122
+ * @example
123
+ * CarbonInterval.fromISO('P1Y2M3DT4H5M6S')
124
+ * CarbonInterval.fromISO('PT30M')
125
+ */
126
+ static fromISO(iso: string): CarbonInterval {
127
+ return new CarbonInterval(Temporal.Duration.from(iso));
128
+ }
129
+
130
+ /**
131
+ * Return the absolute (non-negative) version of all fields.
132
+ */
133
+ static abs(interval: CarbonInterval): CarbonInterval {
134
+ return interval.abs();
135
+ }
136
+
137
+ // ── Getters ───────────────────────────────────────────────────────────────
138
+
139
+ get years(): number {
140
+ return this._duration.years;
141
+ }
142
+ get months(): number {
143
+ return this._duration.months;
144
+ }
145
+ get weeks(): number {
146
+ return this._duration.weeks;
147
+ }
148
+ get days(): number {
149
+ return this._duration.days;
150
+ }
151
+ get hours(): number {
152
+ return this._duration.hours;
153
+ }
154
+ get minutes(): number {
155
+ return this._duration.minutes;
156
+ }
157
+ get seconds(): number {
158
+ return this._duration.seconds;
159
+ }
160
+ get milliseconds(): number {
161
+ return this._duration.milliseconds;
162
+ }
163
+ get microseconds(): number {
164
+ return this._duration.microseconds;
165
+ }
166
+ get nanoseconds(): number {
167
+ return this._duration.nanoseconds;
168
+ }
169
+
170
+ /** Sign of the duration: 1, -1, or 0. */
171
+ get sign(): -1 | 0 | 1 {
172
+ return this._duration.sign as -1 | 0 | 1;
173
+ }
174
+
175
+ get isZero(): boolean {
176
+ return this._duration.blank;
177
+ }
178
+
179
+ // ── Fluent builder ("and*" prefix) ────────────────────────────────────────
180
+
181
+ /**
182
+ * Return a new interval that also adds the given years.
183
+ *
184
+ * @example
185
+ * CarbonInterval.days(3).andHours(6).andMinutes(30)
186
+ */
187
+ andYears(amount: number): CarbonInterval {
188
+ return this.add(CarbonInterval.years(amount));
189
+ }
190
+ andMonths(amount: number): CarbonInterval {
191
+ return this.add(CarbonInterval.months(amount));
192
+ }
193
+ andWeeks(amount: number): CarbonInterval {
194
+ return this.add(CarbonInterval.weeks(amount));
195
+ }
196
+ andDays(amount: number): CarbonInterval {
197
+ return this.add(CarbonInterval.days(amount));
198
+ }
199
+ andHours(amount: number): CarbonInterval {
200
+ return this.add(CarbonInterval.hours(amount));
201
+ }
202
+ andMinutes(amount: number): CarbonInterval {
203
+ return this.add(CarbonInterval.minutes(amount));
204
+ }
205
+ andSeconds(amount: number): CarbonInterval {
206
+ return this.add(CarbonInterval.seconds(amount));
207
+ }
208
+ andMilliseconds(amount: number): CarbonInterval {
209
+ return this.add(CarbonInterval.milliseconds(amount));
210
+ }
211
+ andMicroseconds(amount: number): CarbonInterval {
212
+ return this.add(CarbonInterval.microseconds(amount));
213
+ }
214
+ andNanoseconds(amount: number): CarbonInterval {
215
+ return this.add(CarbonInterval.nanoseconds(amount));
216
+ }
217
+
218
+ // ── Arithmetic ────────────────────────────────────────────────────────────
219
+
220
+ /**
221
+ * Return a new interval that is the sum of this and another.
222
+ *
223
+ * @example
224
+ * CarbonInterval.hours(2).add(CarbonInterval.minutes(30))
225
+ */
226
+ add(other: CarbonInterval): CarbonInterval {
227
+ return this._wrap(this._duration.add(other._duration));
228
+ }
229
+
230
+ /**
231
+ * Return a new interval that is this minus another.
232
+ */
233
+ subtract(other: CarbonInterval): CarbonInterval {
234
+ return this._wrap(this._duration.subtract(other._duration));
235
+ }
236
+
237
+ /**
238
+ * Return a new interval with all fields negated.
239
+ */
240
+ negate(): CarbonInterval {
241
+ return this._wrap(this._duration.negated());
242
+ }
243
+
244
+ /**
245
+ * Return a new interval with all negative fields made positive.
246
+ */
247
+ abs(): CarbonInterval {
248
+ return this._wrap(this._duration.abs());
249
+ }
250
+
251
+ /**
252
+ * Multiply all fields by a scalar.
253
+ *
254
+ * @example
255
+ * CarbonInterval.hours(1).multiply(3) // → 3 hours
256
+ */
257
+ multiply(factor: number): CarbonInterval {
258
+ return new CarbonInterval({
259
+ years: Math.round(this.years * factor),
260
+ months: Math.round(this.months * factor),
261
+ weeks: Math.round(this.weeks * factor),
262
+ days: Math.round(this.days * factor),
263
+ hours: Math.round(this.hours * factor),
264
+ minutes: Math.round(this.minutes * factor),
265
+ seconds: Math.round(this.seconds * factor),
266
+ milliseconds: Math.round(this.milliseconds * factor),
267
+ microseconds: Math.round(this.microseconds * factor),
268
+ nanoseconds: Math.round(this.nanoseconds * factor),
269
+ });
270
+ }
271
+
272
+ // ── Normalisation ─────────────────────────────────────────────────────────
273
+
274
+ /**
275
+ * Cascade (normalize) excess sub-units up to higher units.
276
+ *
277
+ * Requires a reference ZonedDateTime to resolve calendar ambiguities
278
+ * (e.g. how many days are in a month). Defaults to "now" in UTC.
279
+ *
280
+ * @example
281
+ * CarbonInterval.seconds(90).cascade()
282
+ * // → CarbonInterval { minutes: 1, seconds: 30 }
283
+ *
284
+ * CarbonInterval.minutes(90).cascade()
285
+ * // → CarbonInterval { hours: 1, minutes: 30 }
286
+ */
287
+ cascade(relativeTo?: Temporal.ZonedDateTime): CarbonInterval {
288
+ const ref = relativeTo ?? Temporal.Now.zonedDateTimeISO("UTC");
289
+ const balanced = this._duration.round({
290
+ largestUnit: "years",
291
+ relativeTo: ref,
292
+ });
293
+ return this._wrap(balanced);
294
+ }
295
+
296
+ // ── Total values ──────────────────────────────────────────────────────────
297
+
298
+ /**
299
+ * Convert this interval to a total number of seconds.
300
+ * Calendar units (years, months) are approximated.
301
+ */
302
+ totalSeconds(): number {
303
+ return (
304
+ this.years * 365.25 * 24 * 3600 +
305
+ this.months * 30.4375 * 24 * 3600 +
306
+ this.weeks * 7 * 24 * 3600 +
307
+ this.days * 24 * 3600 +
308
+ this.hours * 3600 +
309
+ this.minutes * 60 +
310
+ this.seconds +
311
+ this.milliseconds / 1_000 +
312
+ this.microseconds / 1_000_000 +
313
+ this.nanoseconds / 1_000_000_000
314
+ );
315
+ }
316
+
317
+ totalMinutes(): number {
318
+ return this.totalSeconds() / 60;
319
+ }
320
+ totalHours(): number {
321
+ return this.totalSeconds() / 3600;
322
+ }
323
+ totalDays(): number {
324
+ return this.totalSeconds() / 86_400;
325
+ }
326
+ totalWeeks(): number {
327
+ return this.totalDays() / 7;
328
+ }
329
+
330
+ // ── Human-readable ────────────────────────────────────────────────────────
331
+
332
+ /**
333
+ * Return a human-readable description of the interval.
334
+ *
335
+ * @example
336
+ * CarbonInterval.days(1).andHours(2).andMinutes(30).forHumans()
337
+ * // → "1 day 2 hours 30 minutes"
338
+ *
339
+ * CarbonInterval.years(2).andMonths(3).forHumans({ join: ' and ' })
340
+ * // → "2 years and 3 months"
341
+ */
342
+ forHumans(options: { join?: string; short?: boolean } = {}): string {
343
+ const { join = " ", short = false } = options;
344
+
345
+ const parts: string[] = [];
346
+
347
+ const add = (value: number, singular: string, plural?: string) => {
348
+ if (value === 0) return;
349
+ const label = short
350
+ ? singular.slice(0, 3)
351
+ : value === 1
352
+ ? singular
353
+ : (plural ?? singular + "s");
354
+ parts.push(`${value} ${label}`);
355
+ };
356
+
357
+ add(Math.abs(this.years), "year");
358
+ add(Math.abs(this.months), "month");
359
+ add(Math.abs(this.weeks), "week");
360
+ add(Math.abs(this.days), "day");
361
+ add(Math.abs(this.hours), "hour");
362
+ add(Math.abs(this.minutes), "minute");
363
+ add(Math.abs(this.seconds), "second");
364
+ add(Math.abs(this.milliseconds), "millisecond");
365
+ add(Math.abs(this.microseconds), "microsecond");
366
+ add(Math.abs(this.nanoseconds), "nanosecond");
367
+
368
+ if (parts.length === 0) return short ? "0s" : "0 seconds";
369
+ return (this.sign < 0 ? "-" : "") + parts.join(join);
370
+ }
371
+
372
+ // ── Serialisation ─────────────────────────────────────────────────────────
373
+
374
+ /**
375
+ * Return the ISO 8601 duration string.
376
+ *
377
+ * @example
378
+ * CarbonInterval.days(1).andHours(2).toISO()
379
+ * // → "P1DT2H"
380
+ */
381
+ toISO(): string {
382
+ return this._duration.toString();
383
+ }
384
+
385
+ /** Alias for toISO(). Used by JSON.stringify. */
386
+ toJSON(): string {
387
+ return this.toISO();
388
+ }
389
+
390
+ toString(): string {
391
+ return this.forHumans();
392
+ }
393
+
394
+ /** Return the backing Temporal.Duration. */
395
+ toDuration(): Temporal.Duration {
396
+ return this._duration;
397
+ }
398
+
399
+ // ── Comparison ────────────────────────────────────────────────────────────
400
+
401
+ /**
402
+ * Compare two intervals by total seconds (approximate for calendar units).
403
+ * Returns -1, 0, or 1.
404
+ */
405
+ static compare(a: CarbonInterval, b: CarbonInterval): -1 | 0 | 1 {
406
+ const diff = a.totalSeconds() - b.totalSeconds();
407
+ if (diff < 0) return -1;
408
+ if (diff > 0) return 1;
409
+ return 0;
410
+ }
411
+
412
+ isLessThan(other: CarbonInterval): boolean {
413
+ return CarbonInterval.compare(this, other) < 0;
414
+ }
415
+ isGreaterThan(other: CarbonInterval): boolean {
416
+ return CarbonInterval.compare(this, other) > 0;
417
+ }
418
+ isEqualTo(other: CarbonInterval): boolean {
419
+ return CarbonInterval.compare(this, other) === 0;
420
+ }
421
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The clock every `Carbon` reads "now" from.
3
+ *
4
+ * Routing it through one place is what makes time testable: code that expires a
5
+ * token in seven days, or only sends a reminder after 24 hours, otherwise has no
6
+ * way to be tested except by waiting. Tests move this clock with
7
+ * `Carbon.setTestNow()` / `Carbon.travel()`; nothing else touches it.
8
+ *
9
+ * @module
10
+ */
11
+ import { Temporal } from "./temporal-shim.ts";
12
+
13
+ let _testInstant: Temporal.Instant | null = null;
14
+
15
+ /** The current zoned date-time in `tz` — the frozen instant when one is set. */
16
+ export function _nowIn(tz: string): Temporal.ZonedDateTime {
17
+ return _testInstant ? _testInstant.toZonedDateTimeISO(tz) : Temporal.Now.zonedDateTimeISO(tz);
18
+ }
19
+
20
+ /** Freeze the clock at `instant`, or release it with `null`. @internal */
21
+ export function _setTestInstant(instant: Temporal.Instant | null): void {
22
+ _testInstant = instant;
23
+ }
24
+
25
+ /** The frozen instant, or `null` when the clock is running normally. @internal */
26
+ export function _getTestInstant(): Temporal.Instant | null {
27
+ return _testInstant;
28
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Date/time primitives for Zerotal — a fluent, ergonomic date-time API built on
3
+ * `Temporal`. Exposes the immutable {@link Carbon} date-time value object and
4
+ * the {@link CarbonInterval} duration object. Isolated from the kernel barrel
5
+ * (the `@zerotal/core/carbon` subpath) so consumers that don't need dates
6
+ * avoid pulling in the Temporal polyfill.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { Carbon, CarbonInterval } from "@zerotal/core/carbon";
11
+ *
12
+ * const start = Carbon.now();
13
+ * const later = start.add(CarbonInterval.days(3).andHours(6));
14
+ * later.diffForHumans(start); // "in 3 days"
15
+ * later.format("YYYY-MM-DD HH:mm");
16
+ * ```
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ export { Carbon } from "./Carbon.ts";
21
+ export { CarbonInterval } from "./CarbonInterval.ts";
22
+ export type { DurationLike } from "./CarbonInterval.ts";
23
+ export type { CarbonInput } from "./Carbon.ts";
@@ -0,0 +1 @@
1
+ export { Temporal } from "@js-temporal/polyfill";
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Cleanup of the files a previous build left in an output directory.
3
+ *
4
+ * Code-splitting names every shared chunk after its content — `chunk-3f9a2c.js`
5
+ * — so each rebuild emits a fresh set and abandons the last one. Nothing
6
+ * overwrites those old names, so an output directory grows without limit across
7
+ * a dev session: hundreds of dead chunks, every one of them registered as a
8
+ * static route at startup and shipped in a production deploy.
9
+ */
10
+ import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
11
+ import { join, relative, resolve } from "node:path";
12
+
13
+ /** Bundler-generated code-split chunk, e.g. `chunk-2502z4dn.js` (+ `.map`). */
14
+ const CHUNK_NAME = /^chunk-[a-z0-9]+\.js(\.map)?$/i;
15
+
16
+ /** Where the per-directory record of "what the last build wrote" is kept. */
17
+ const MANIFEST_DIR = ".zerotal/build";
18
+
19
+ /**
20
+ * Delete what an earlier build wrote to `outdir` and this one did not.
21
+ *
22
+ * Two things qualify for removal, and nothing else: a file the previous build
23
+ * recorded as its own output, and a file named the way the bundler names
24
+ * code-split chunks. Everything else in the directory is left alone — an output
25
+ * directory is often `public/`, where a blanket wipe would take the app's
26
+ * images and favicon with it.
27
+ *
28
+ * Call this only after a *successful* build. Pruning after a failed one would
29
+ * delete the working output and leave nothing to serve in its place.
30
+ *
31
+ * @param outdir Absolute path the build wrote to.
32
+ * @param outputs The build's artifacts (`Bun.build()`'s `outputs`).
33
+ * @returns Paths removed, relative to `outdir`.
34
+ *
35
+ * @example
36
+ * const result = await Bun.build({ entrypoints, outdir, splitting: true });
37
+ * if (result.success) await pruneBuildOutput(outdir, result.outputs);
38
+ *
39
+ * @internal
40
+ */
41
+ export async function pruneBuildOutput(
42
+ outdir: string,
43
+ outputs: readonly { path: string }[],
44
+ ): Promise<string[]> {
45
+ const root = resolve(outdir);
46
+ const current = new Set(outputs.map((output) => _relative(root, output.path)));
47
+
48
+ const previous = await _readManifest(root);
49
+ const stale = new Set<string>();
50
+
51
+ for (const path of previous) {
52
+ if (!current.has(path)) stale.add(path);
53
+ }
54
+
55
+ // Chunks are swept by name as well as by manifest, so a directory that has
56
+ // been accumulating them since before any manifest existed still gets cleaned
57
+ // on the next build.
58
+ for (const path of await _listEntries(root)) {
59
+ if (current.has(path)) continue;
60
+ if (CHUNK_NAME.test(path.split("/").at(-1) ?? "")) stale.add(path);
61
+ }
62
+
63
+ const removed: string[] = [];
64
+ for (const path of stale) {
65
+ try {
66
+ await unlink(join(root, path));
67
+ removed.push(path);
68
+ } catch {
69
+ // Already gone, or held open by another process — either way the next
70
+ // build tries again, and a file we could not delete is not worth failing
71
+ // an otherwise good build over.
72
+ }
73
+ }
74
+
75
+ await _writeManifest(root, [...current].sort());
76
+ return removed.sort();
77
+ }
78
+
79
+ // ── Private ──────────────────────────────────────────────────────────────────
80
+
81
+ /** Path relative to `root`, with forward slashes so manifests are portable. */
82
+ function _relative(root: string, path: string): string {
83
+ return relative(root, resolve(path)).split("\\").join("/");
84
+ }
85
+
86
+ /**
87
+ * The manifest lives outside `outdir` — writing it inside would put a file the
88
+ * app then serves (and registers a route for) into the public directory.
89
+ */
90
+ function _manifestPath(root: string): string {
91
+ const slug = _relative(resolve(process.cwd()), root).replace(/[^a-z0-9]+/gi, "-") || "out";
92
+ // The slug alone can collide (two different roots normalising to the same
93
+ // name); the hash makes each directory's manifest its own.
94
+ const hash = Bun.hash(root).toString(36);
95
+ return join(process.cwd(), MANIFEST_DIR, `${slug}-${hash}.json`);
96
+ }
97
+
98
+ /** What the previous build recorded, or nothing on a first run. */
99
+ async function _readManifest(root: string): Promise<string[]> {
100
+ try {
101
+ const contents = (await Bun.file(_manifestPath(root)).json()) as unknown;
102
+ if (!Array.isArray(contents)) return [];
103
+ return contents.filter((entry): entry is string => typeof entry === "string");
104
+ } catch {
105
+ return [];
106
+ }
107
+ }
108
+
109
+ async function _writeManifest(root: string, files: string[]): Promise<void> {
110
+ const path = _manifestPath(root);
111
+ try {
112
+ await mkdir(join(process.cwd(), MANIFEST_DIR), { recursive: true });
113
+ await writeFile(path, JSON.stringify(files, null, 2));
114
+ } catch {
115
+ // A manifest that cannot be written costs precision on the next prune, not
116
+ // correctness: chunks are still swept by name.
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Every entry under `root`, relative and slash-normalised. Empty if unreadable
122
+ * (a first build has nothing to clean, and neither does a missing directory).
123
+ */
124
+ async function _listEntries(root: string): Promise<string[]> {
125
+ try {
126
+ const entries = await readdir(root, { recursive: true });
127
+ return entries.map((entry) => entry.split("\\").join("/"));
128
+ } catch {
129
+ return [];
130
+ }
131
+ }