effect-app 4.0.0 → 4.0.2

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 (83) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/Array.d.ts +0 -17
  3. package/dist/Array.d.ts.map +1 -1
  4. package/dist/Array.js +2 -26
  5. package/dist/Chunk.d.ts +3 -27
  6. package/dist/Chunk.d.ts.map +1 -1
  7. package/dist/Chunk.js +4 -39
  8. package/dist/DateTime.d.ts +33 -0
  9. package/dist/DateTime.d.ts.map +1 -0
  10. package/dist/DateTime.js +18 -0
  11. package/dist/Effect.d.ts +0 -11
  12. package/dist/Effect.d.ts.map +1 -1
  13. package/dist/Effect.js +3 -18
  14. package/dist/Function.d.ts +0 -4
  15. package/dist/Function.d.ts.map +1 -1
  16. package/dist/Function.js +1 -7
  17. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  18. package/dist/Model/Repository/internal/internal.js +4 -5
  19. package/dist/Model/dsl.d.ts +4 -3
  20. package/dist/Model/dsl.d.ts.map +1 -1
  21. package/dist/Model/dsl.js +6 -4
  22. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
  23. package/dist/Model/query/new-kid-interpreter.js +5 -6
  24. package/dist/Option.d.ts +0 -2
  25. package/dist/Option.d.ts.map +1 -1
  26. package/dist/Option.js +1 -7
  27. package/dist/Pure.d.ts +4 -4
  28. package/dist/Pure.d.ts.map +1 -1
  29. package/dist/Pure.js +16 -16
  30. package/dist/Schema/ext.d.ts +0 -21
  31. package/dist/Schema/ext.d.ts.map +1 -1
  32. package/dist/Schema/ext.js +4 -10
  33. package/dist/Schema.d.ts +1 -1
  34. package/dist/Schema.d.ts.map +1 -1
  35. package/dist/Schema.js +2 -2
  36. package/dist/Set.d.ts.map +1 -1
  37. package/dist/Set.js +6 -5
  38. package/dist/client/apiClientFactory.d.ts.map +1 -1
  39. package/dist/client/apiClientFactory.js +7 -6
  40. package/dist/migration/nativeEffect.d.ts +21 -0
  41. package/dist/migration/nativeEffect.d.ts.map +1 -0
  42. package/dist/migration/nativeEffect.js +598 -0
  43. package/dist/utils.d.ts +0 -6
  44. package/dist/utils.d.ts.map +1 -1
  45. package/dist/utils.js +1 -13
  46. package/dist/waitingGate.d.ts +38 -0
  47. package/dist/waitingGate.d.ts.map +1 -0
  48. package/dist/waitingGate.js +63 -0
  49. package/dist/withToast.d.ts.map +1 -1
  50. package/dist/withToast.js +42 -3
  51. package/package.json +2 -3
  52. package/src/Array.ts +4 -36
  53. package/src/Chunk.ts +4 -53
  54. package/src/DateTime.ts +473 -0
  55. package/src/Effect.ts +3 -22
  56. package/src/Function.ts +0 -7
  57. package/src/Model/Repository/internal/internal.ts +3 -4
  58. package/src/Model/dsl.ts +5 -4
  59. package/src/Model/query/new-kid-interpreter.ts +4 -5
  60. package/src/Option.ts +1 -8
  61. package/src/Pure.ts +15 -15
  62. package/src/Schema/ext.ts +3 -10
  63. package/src/Schema.ts +1 -1
  64. package/src/Set.ts +5 -4
  65. package/src/client/apiClientFactory.ts +8 -7
  66. package/src/migration/nativeEffect.ts +645 -0
  67. package/src/utils.ts +0 -17
  68. package/src/waitingGate.ts +82 -0
  69. package/src/withToast.ts +51 -5
  70. package/test/dateTimeLocal.test.ts +55 -0
  71. package/test/dist/dateTimeLocal.test.d.ts.map +1 -0
  72. package/test/dist/nativeEffectMigration.test.d.ts.map +1 -0
  73. package/test/dist/waitingGate.test.d.ts.map +1 -0
  74. package/test/nativeEffectMigration.test.ts +77 -0
  75. package/test/waitingGate.test.ts +64 -0
  76. package/dist/_ext/date.d.ts +0 -15
  77. package/dist/_ext/date.d.ts.map +0 -1
  78. package/dist/_ext/date.js +0 -16
  79. package/dist/_ext/misc.d.ts +0 -17
  80. package/dist/_ext/misc.d.ts.map +0 -1
  81. package/dist/_ext/misc.js +0 -19
  82. package/src/_ext/date.ts +0 -27
  83. package/src/_ext/misc.ts +0 -49
@@ -0,0 +1,645 @@
1
+ /**
2
+ * Rewrites source that imported effect-app helpers now provided by Effect.
3
+ *
4
+ * The rewrite is textual and keeps the rest of the file. It understands named
5
+ * imports, aliases, and `import * as` namespaces from the effect-app entry points
6
+ * listed in {@link nativeEffectReplacements}.
7
+ */
8
+
9
+ export interface NativeEffectReplacement {
10
+ readonly module: string
11
+ readonly name: string
12
+ /** Expression that replaces a reference to `name`. */
13
+ readonly expression: string
14
+ readonly note: string
15
+ readonly imports: ReadonlyArray<readonly [local: string, from: string]>
16
+ /** When set, a call `name(...args)` is rewritten instead of the bare identifier. */
17
+ readonly call?: "date-add" | "date-sub" | "chunk"
18
+ }
19
+
20
+ const struct = [["Struct", "effect/Struct"]] as const
21
+ const record = [["Record", "effect/Record"]] as const
22
+ const str = [["String", "effect/String"]] as const
23
+ const option = [["Option", "effect/Option"]] as const
24
+ const array = [["Array", "effect/Array"]] as const
25
+ const chunk = [["Chunk", "effect/Chunk"]] as const
26
+ const tuple = [["Tuple", "effect/Tuple"]] as const
27
+ const effect = [["Effect", "effect/Effect"]] as const
28
+ const dateTime = [["DateTime", "effect/DateTime"], ["pipeZonedLocal", "effect-app/DateTime"]] as const
29
+ const deferred = [["Deferred", "effect/Deferred"]] as const
30
+ const result = [["Result", "effect/Result"]] as const
31
+ const absurd = [["absurd", "effect/Function"]] as const
32
+
33
+ export const nativeEffectReplacements: ReadonlyArray<NativeEffectReplacement> = [
34
+ {
35
+ module: "effect-app/utils",
36
+ name: "typedKeysOf",
37
+ expression: "Struct.keys",
38
+ imports: struct,
39
+ note: "Struct.keys returns `(keyof T & string)[]`."
40
+ },
41
+ {
42
+ module: "effect-app/utils",
43
+ name: "typedValuesOf",
44
+ expression: "Record.values",
45
+ imports: record,
46
+ note:
47
+ "Record.values reads a record with one value type. When each value's type depends on its key, use Object.values."
48
+ },
49
+ {
50
+ module: "effect-app/utils",
51
+ name: "capitalize",
52
+ expression: "String.capitalize",
53
+ imports: str,
54
+ note: "Same first-character capitalization, including the empty string."
55
+ },
56
+ {
57
+ module: "effect-app/utils",
58
+ name: "uncapitalize",
59
+ expression: "String.uncapitalize",
60
+ imports: str,
61
+ note: "Same first-character uncapitalization, including the empty string."
62
+ },
63
+ {
64
+ module: "effect-app/utils",
65
+ name: "assertUnreachable",
66
+ expression: "absurd",
67
+ imports: absurd,
68
+ note: "absurd throws when an impossible value is reached. The error text no longer includes the value."
69
+ },
70
+ {
71
+ module: "effect-app/Function",
72
+ name: "tuple",
73
+ expression: "Tuple.make",
74
+ imports: tuple,
75
+ note: "Tuple.make returns a mutable tuple. It is assignable wherever a readonly tuple was expected."
76
+ },
77
+ {
78
+ module: "effect-app/Array",
79
+ name: "toNonEmptyArray",
80
+ expression: "Option.liftPredicate(Array.isReadonlyArrayNonEmpty)",
81
+ imports: [...option, ...array],
82
+ note: "Option of the same readonly array when it is non-empty."
83
+ },
84
+ {
85
+ module: "effect-app/Array",
86
+ name: "NEROArrayFromArray",
87
+ expression: "Option.liftPredicate(Array.isReadonlyArrayNonEmpty)",
88
+ imports: [...option, ...array],
89
+ note: "Same as toNonEmptyArray."
90
+ },
91
+ {
92
+ module: "effect-app/Array",
93
+ name: "NEAFromArray",
94
+ expression: "Option.liftPredicate(Array.isArrayNonEmpty)",
95
+ imports: [...option, ...array],
96
+ note: "Option of the same mutable array when it is non-empty."
97
+ },
98
+ {
99
+ module: "effect-app/Array",
100
+ name: "chunk_",
101
+ expression: "Array.chunksOf",
102
+ imports: [...array, ...chunk],
103
+ call: "chunk",
104
+ note: "Was a Chunk of mutable slices. Now a Chunk of Array.chunksOf groups. An empty input is an empty Chunk."
105
+ },
106
+ {
107
+ module: "effect-app/Array",
108
+ name: "_chunk_",
109
+ expression: "Array.chunksOf",
110
+ imports: array,
111
+ note: "The generator form is gone. Array.chunksOf returns the groups directly."
112
+ },
113
+ {
114
+ module: "effect-app/Option",
115
+ name: "toBool",
116
+ expression: "Option.isSome",
117
+ imports: option,
118
+ note: "isSome is the boolean test and also narrows to Some."
119
+ },
120
+ {
121
+ module: "effect-app/Effect",
122
+ name: "annotateLogscoped",
123
+ expression: "Effect.annotateLogsScoped",
124
+ imports: effect,
125
+ note: "The old name was a typo of Effect.annotateLogsScoped."
126
+ },
127
+ {
128
+ module: "effect-app/Effect",
129
+ name: "await_",
130
+ expression: "Deferred.await",
131
+ imports: deferred,
132
+ note: "Await a Deferred. Effect.await_ was Deferred.await."
133
+ },
134
+ {
135
+ module: "effect-app/Chunk",
136
+ name: "findFirstSimple",
137
+ expression: "Chunk.findFirst",
138
+ imports: chunk,
139
+ note: "The alias passed the chunk first. Chunk.findFirst is dual, so the data-first call still typechecks."
140
+ },
141
+ {
142
+ module: "effect-app/Chunk",
143
+ name: "findLastSimple",
144
+ expression: "Chunk.findLast",
145
+ imports: chunk,
146
+ note: "Alias of Chunk.findLast."
147
+ },
148
+ {
149
+ module: "effect-app/Chunk",
150
+ name: "ChunkPartition",
151
+ expression: "Chunk.partition",
152
+ imports: chunk,
153
+ note: "Alias of Chunk.partition."
154
+ },
155
+ {
156
+ module: "effect-app/Chunk",
157
+ name: "uniq",
158
+ expression:
159
+ "(equivalence) => (self) => Chunk.fromIterable(Array.dedupeWith(Chunk.toReadonlyArray(self), equivalence))",
160
+ imports: [...chunk, ...array],
161
+ note: "Keeps the first match under a custom equivalence. Equal values can use Chunk.dedupe instead."
162
+ },
163
+ {
164
+ module: "effect-app/Chunk",
165
+ name: "elem",
166
+ expression: "(equivalence, value) => Chunk.containsWith(equivalence)(value)",
167
+ imports: chunk,
168
+ note: "Equivalence is treated as symmetric. containsWith applies it as equivalence(needle, element)."
169
+ },
170
+ {
171
+ module: "effect-app/Schema",
172
+ name: "DateValid",
173
+ expression: "Date",
174
+ imports: [["Date", "effect-app/Schema"]],
175
+ note: "DateValid was an alias of effect-app Schema.Date. Schema.Date already rejects invalid dates."
176
+ },
177
+ {
178
+ module: "effect-app/_ext/misc",
179
+ name: "encaseMaybeInEffect_",
180
+ expression: "Effect.fromOption",
181
+ imports: effect,
182
+ note: "Effect.fromOption(option, onNone) fails with the lazy error on None."
183
+ },
184
+ {
185
+ module: "effect-app/_ext/misc",
186
+ name: "encaseMaybeEither_",
187
+ expression: "Result.fromOption",
188
+ imports: result,
189
+ note: "Result.fromOption(option, onNone)."
190
+ },
191
+ {
192
+ module: "effect-app/_ext/misc",
193
+ name: "toNullable",
194
+ expression: "(effect) => Effect.map(effect, Option.getOrNull)",
195
+ imports: [...effect, ...option],
196
+ note: "Maps a successful Option to its value or null."
197
+ },
198
+ {
199
+ module: "effect-app/_ext/misc",
200
+ name: "flatMapScoped",
201
+ expression: "(scoped, f) => Effect.scoped(Effect.flatMap(scoped, f))",
202
+ imports: effect,
203
+ note: "Closes the scope after the continuation."
204
+ },
205
+ {
206
+ module: "effect-app/_ext/misc",
207
+ name: "scope",
208
+ expression: "(scoped, effect) => Effect.scoped(Effect.andThen(scoped, effect))",
209
+ imports: effect,
210
+ note: "Runs the second effect inside the first effect's scope, then closes it."
211
+ },
212
+ ...([
213
+ ["DateAddDays", "days", "date-add"],
214
+ ["DateAddHours", "hours", "date-add"],
215
+ ["DateAddMinutes", "minutes", "date-add"],
216
+ ["DateAddSeconds", "seconds", "date-add"],
217
+ ["DateAddWeeks", "weeks", "date-add"],
218
+ ["DateAddMonths", "months", "date-add"],
219
+ ["DateAddYears", "years", "date-add"],
220
+ ["DateSubDays", "days", "date-sub"],
221
+ ["DateSubHours", "hours", "date-sub"],
222
+ ["DateSubMinutes", "minutes", "date-sub"],
223
+ ["DateSubSeconds", "seconds", "date-sub"],
224
+ ["DateSubWeeks", "weeks", "date-sub"],
225
+ ["DateSubMonths", "months", "date-sub"],
226
+ ["DateSubYears", "years", "date-sub"]
227
+ ] as const)
228
+ .map(([name, unit, call]): NativeEffectReplacement => ({
229
+ module: "effect-app/_ext/date",
230
+ name,
231
+ expression: unit,
232
+ imports: dateTime,
233
+ call,
234
+ note:
235
+ "pipeZonedLocal attaches the process timezone, runs DateTime.add or DateTime.subtract, and returns the instant with toDateUtc."
236
+ }))
237
+ ]
238
+
239
+ interface Binding {
240
+ readonly replacement: NativeEffectReplacement
241
+ readonly local: string
242
+ }
243
+
244
+ const byModule = new Map<string, Map<string, NativeEffectReplacement>>()
245
+ for (const replacement of nativeEffectReplacements) {
246
+ const existing = byModule.get(replacement.module) ?? new Map<string, NativeEffectReplacement>()
247
+ existing.set(replacement.name, replacement)
248
+ byModule.set(replacement.module, existing)
249
+ }
250
+
251
+ const moduleNames = new Set(byModule.keys())
252
+
253
+ interface Rendered {
254
+ readonly text: string
255
+ readonly end: number
256
+ }
257
+
258
+ /** Rewrite one source file. `source` is the file text. */
259
+ export const migrateEffectAppSource = (source: string): string => {
260
+ const imports = findImports(source)
261
+ const bindings = new Map<string, Binding>()
262
+ const namespaceBindings = new Map<string, ReadonlyMap<string, NativeEffectReplacement>>()
263
+ const edits: Array<{ start: number; end: number; text: string }> = []
264
+ const needed = new Map<string, Set<string>>()
265
+ const maskedSpans: Array<{ start: number; end: number }> = []
266
+
267
+ for (const statement of imports) {
268
+ const table = byModule.get(statement.module)
269
+ if (table === undefined) continue
270
+ maskedSpans.push({ start: statement.start, end: statement.end })
271
+
272
+ if (statement.namespace !== undefined) {
273
+ namespaceBindings.set(statement.namespace, table)
274
+ continue
275
+ }
276
+
277
+ const removed: Array<Specifier> = []
278
+ for (const specifier of statement.specifiers) {
279
+ const replacement = table.get(specifier.imported)
280
+ if (replacement === undefined) continue
281
+ bindings.set(specifier.local, { replacement, local: specifier.local })
282
+ removed.push(specifier)
283
+ rememberImports(needed, replacement)
284
+ }
285
+ if (removed.length === 0) continue
286
+ if (removed.length === statement.specifiers.length) {
287
+ edits.push({ start: statement.start, end: statement.end, text: "" })
288
+ } else {
289
+ const kept = statement.specifiers.filter((specifier) => !removed.includes(specifier))
290
+ edits.push({
291
+ start: statement.clauseStart,
292
+ end: statement.clauseEnd,
293
+ text: `{ ${kept.map((specifier) => specifier.text).join(", ")} }`
294
+ })
295
+ }
296
+ }
297
+
298
+ const mask = maskSpans(maskNonCode(source), maskedSpans)
299
+
300
+ for (const [local, binding] of bindings) {
301
+ for (const match of identifierSpans(mask, local)) {
302
+ const rendered = render(binding.replacement, source, match + local.length)
303
+ edits.push({ start: match, end: rendered.end, text: rendered.text })
304
+ }
305
+ }
306
+
307
+ for (const [namespace, table] of namespaceBindings) {
308
+ let used = false
309
+ for (const match of identifierSpans(mask, namespace)) {
310
+ const after = skipSpaces(source, match + namespace.length)
311
+ if (source[after] !== ".") continue
312
+ const propertyStart = after + 1
313
+ const property = readIdentifier(source, propertyStart)
314
+ if (property === undefined) continue
315
+ const replacement = table.get(property.name)
316
+ if (replacement === undefined) {
317
+ used = true
318
+ continue
319
+ }
320
+ rememberImports(needed, replacement)
321
+ const rendered = render(replacement, source, propertyStart + property.name.length)
322
+ edits.push({ start: match, end: rendered.end, text: rendered.text })
323
+ }
324
+ if (!used) {
325
+ const statement = imports.find((item) => item.namespace === namespace && byModule.has(item.module))
326
+ if (statement !== undefined) edits.push({ start: statement.start, end: statement.end, text: "" })
327
+ }
328
+ }
329
+
330
+ return insertImports(applyEdits(source, edits), needed)
331
+ }
332
+
333
+ const render = (replacement: NativeEffectReplacement, source: string, afterName: number): Rendered => {
334
+ const bare = { text: replacement.expression, end: afterName }
335
+ if (replacement.call === undefined) return bare
336
+ const open = skipSpaces(source, afterName)
337
+ if (source[open] !== "(") return bare
338
+ const args = splitArgs(source, open)
339
+ if (args === undefined) return bare
340
+ if (replacement.call === "chunk" && args.args.length === 2) {
341
+ return {
342
+ text: `Chunk.fromIterable(Array.chunksOf(${args.args[0]}, ${args.args[1]}))`,
343
+ end: args.end
344
+ }
345
+ }
346
+ if ((replacement.call === "date-add" || replacement.call === "date-sub") && args.args.length === 2) {
347
+ const method = replacement.call === "date-add" ? "add" : "subtract"
348
+ return {
349
+ text: `pipeZonedLocal(${args.args[0]}, DateTime.${method}({ ${replacement.expression}: ${args.args[1]} }))`,
350
+ end: args.end
351
+ }
352
+ }
353
+ return bare
354
+ }
355
+
356
+ const rememberImports = (needed: Map<string, Set<string>>, replacement: NativeEffectReplacement) => {
357
+ for (const [local, from] of replacement.imports) {
358
+ const names = needed.get(from) ?? new Set<string>()
359
+ names.add(local)
360
+ needed.set(from, names)
361
+ }
362
+ }
363
+
364
+ interface Specifier {
365
+ readonly imported: string
366
+ readonly local: string
367
+ readonly text: string
368
+ }
369
+
370
+ interface ImportStatement {
371
+ readonly start: number
372
+ readonly end: number
373
+ readonly clauseStart: number
374
+ readonly clauseEnd: number
375
+ readonly module: string
376
+ readonly namespace?: string
377
+ readonly specifiers: ReadonlyArray<Specifier>
378
+ }
379
+
380
+ const findImports = (source: string): ReadonlyArray<ImportStatement> => {
381
+ const found: Array<ImportStatement> = []
382
+ const pattern = /\bimport\s+(type\s+)?([\s\S]*?)\s+from\s+["']([^"']+)["']/g
383
+ for (const match of source.matchAll(pattern)) {
384
+ const clause = match[2] ?? ""
385
+ const moduleName = match[3] ?? ""
386
+ if (!moduleNames.has(moduleName)) continue
387
+ const start = lineStart(source, match.index ?? 0)
388
+ const end = lineEnd(source, (match.index ?? 0) + match[0].length)
389
+ const clauseStart = (match.index ?? 0) + match[0].indexOf(clause)
390
+ const namespace = /^\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(clause.trim())
391
+ found.push({
392
+ start,
393
+ end,
394
+ clauseStart,
395
+ clauseEnd: clauseStart + clause.length,
396
+ module: moduleName,
397
+ ...(namespace?.[1] === undefined ? {} : { namespace: namespace[1] }),
398
+ specifiers: namespace === null ? parseSpecifiers(clause) : []
399
+ })
400
+ }
401
+ return found
402
+ }
403
+
404
+ const parseSpecifiers = (clause: string): ReadonlyArray<Specifier> => {
405
+ const brace = clause.indexOf("{")
406
+ if (brace < 0) return []
407
+ const end = clause.lastIndexOf("}")
408
+ const body = clause.slice(brace + 1, end)
409
+ return body.split(",").flatMap((part) => {
410
+ const text = part.trim()
411
+ if (text === "" || text.startsWith("type ")) return []
412
+ const alias = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(text)
413
+ if (alias === null) return []
414
+ return [{ imported: alias[1]!, local: alias[2] ?? alias[1]!, text }]
415
+ })
416
+ }
417
+
418
+ const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
419
+
420
+ const identifierSpans = (mask: string, name: string): ReadonlyArray<number> => {
421
+ const spans: Array<number> = []
422
+ const pattern = new RegExp(`(?<![\\w$])${escapeRegExp(name)}(?![\\w$])`, "g")
423
+ for (const match of mask.matchAll(pattern)) spans.push(match.index ?? 0)
424
+ return spans
425
+ }
426
+
427
+ const readIdentifier = (source: string, index: number): { readonly name: string } | undefined => {
428
+ const match = /^[A-Za-z_$][\w$]*/.exec(source.slice(index))
429
+ return match === null ? undefined : { name: match[0] }
430
+ }
431
+
432
+ const splitArgs = (
433
+ source: string,
434
+ open: number
435
+ ): { readonly args: ReadonlyArray<string>; readonly end: number } | undefined => {
436
+ const balanced = readBalanced(source, open, "(", ")")
437
+ if (balanced === undefined) return undefined
438
+ const args: Array<string> = []
439
+ let depth = 0
440
+ let quote: "'" | "\"" | "`" | undefined
441
+ let start = 0
442
+ const body = balanced.body
443
+ for (let i = 0; i < body.length; i++) {
444
+ const char = body[i]
445
+ if (quote !== undefined) {
446
+ if (char === "\\") {
447
+ i++
448
+ continue
449
+ }
450
+ if (char === quote) quote = undefined
451
+ continue
452
+ }
453
+ if (char === "'" || char === "\"" || char === "`") {
454
+ quote = char
455
+ continue
456
+ }
457
+ if (char === "(" || char === "[" || char === "{") depth++
458
+ if (char === ")" || char === "]" || char === "}") depth--
459
+ if (char === "," && depth === 0) {
460
+ args.push(body.slice(start, i).trim())
461
+ start = i + 1
462
+ }
463
+ }
464
+ const last = body.slice(start).trim()
465
+ if (last !== "") args.push(last)
466
+ return { args, end: balanced.end }
467
+ }
468
+
469
+ const readBalanced = (
470
+ source: string,
471
+ open: number,
472
+ left: string,
473
+ right: string
474
+ ): { readonly body: string; readonly end: number } | undefined => {
475
+ if (source[open] !== left) return undefined
476
+ let depth = 0
477
+ let quote: "'" | "\"" | "`" | undefined
478
+ for (let i = open; i < source.length; i++) {
479
+ const char = source[i]
480
+ if (quote !== undefined) {
481
+ if (char === "\\") {
482
+ i++
483
+ continue
484
+ }
485
+ if (char === quote) quote = undefined
486
+ continue
487
+ }
488
+ if (char === "'" || char === "\"" || char === "`") {
489
+ quote = char
490
+ continue
491
+ }
492
+ if (char === left) depth++
493
+ if (char === right) {
494
+ depth--
495
+ if (depth === 0) return { body: source.slice(open + 1, i), end: i + 1 }
496
+ }
497
+ }
498
+ return undefined
499
+ }
500
+
501
+ const maskSpans = (mask: string, spans: ReadonlyArray<{ start: number; end: number }>): string => {
502
+ const chars = [...mask]
503
+ for (const span of spans) {
504
+ for (let i = span.start; i < span.end && i < chars.length; i++) {
505
+ if (chars[i] !== "\n") chars[i] = " "
506
+ }
507
+ }
508
+ return chars.join("")
509
+ }
510
+
511
+ const skipSpaces = (source: string, index: number): number => {
512
+ let i = index
513
+ while (i < source.length && (source[i] === " " || source[i] === "\n" || source[i] === "\t")) i++
514
+ return i
515
+ }
516
+
517
+ const lineStart = (source: string, index: number): number => {
518
+ const start = source.lastIndexOf("\n", index - 1)
519
+ return start < 0 ? 0 : start + 1
520
+ }
521
+
522
+ const lineEnd = (source: string, index: number): number => {
523
+ const end = source.indexOf("\n", index)
524
+ return end < 0 ? source.length : end + 1
525
+ }
526
+
527
+ const applyEdits = (source: string, edits: ReadonlyArray<{ start: number; end: number; text: string }>): string => {
528
+ const ordered = [...edits].sort((a, b) => b.start - a.start)
529
+ let next = source
530
+ for (const edit of ordered) next = next.slice(0, edit.start) + edit.text + next.slice(edit.end)
531
+ return next
532
+ }
533
+
534
+ const insertImports = (source: string, needed: ReadonlyMap<string, Set<string>>): string => {
535
+ const lines: Array<string> = []
536
+ for (const [from, names] of needed) {
537
+ for (const name of names) {
538
+ if (hasImport(source, name, from)) continue
539
+ lines.push(
540
+ namedImportNames.has(name)
541
+ ? `import { ${name} } from "${from}"`
542
+ : `import * as ${name} from "${from}"`
543
+ )
544
+ }
545
+ }
546
+ if (lines.length === 0) return source.replace(/^\n+/, "")
547
+ const lastImport = lastImportEnd(source)
548
+ const block = `${lines.sort().join("\n")}\n`
549
+ if (lastImport === 0) return `${block}${source.replace(/^\n+/, "")}`
550
+ return `${source.slice(0, lastImport)}${block}${source.slice(lastImport)}`.replace(/^\n+/, "")
551
+ }
552
+
553
+ const namedImportNames = new Set(["absurd", "Date", "pipeZonedLocal"])
554
+
555
+ const hasImport = (source: string, name: string, from: string): boolean => {
556
+ const fromPattern = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
557
+ const namespace = new RegExp(`import\\s+\\*\\s+as\\s+${name}\\s+from\\s+["']${fromPattern}["']`)
558
+ const named = new RegExp(`import\\s+(type\\s+)?\\{[^}]*\\b${name}\\b[^}]*}\\s+from\\s+["']${fromPattern}["']`)
559
+ const barrel = name === name[0]?.toUpperCase()
560
+ ? new RegExp(`import\\s+(type\\s+)?\\{[^}]*\\b${name}\\b[^}]*}\\s+from\\s+["']effect["']`)
561
+ : undefined
562
+ return namespace.test(source) || named.test(source) || barrel?.test(source) === true
563
+ }
564
+
565
+ const lastImportEnd = (source: string): number => {
566
+ const pattern = /\bimport\s+(type\s+)?[\s\S]*?\s+from\s+["'][^"']+["']/g
567
+ let end = 0
568
+ for (const match of source.matchAll(pattern)) {
569
+ end = lineEnd(source, (match.index ?? 0) + match[0].length)
570
+ }
571
+ return end
572
+ }
573
+
574
+ const maskNonCode = (source: string): string => {
575
+ const chars = [...source]
576
+ let i = 0
577
+ const blank = (from: number, to: number) => {
578
+ for (let j = from; j < to; j++) {
579
+ if (chars[j] !== "\n") chars[j] = " "
580
+ }
581
+ }
582
+ while (i < source.length) {
583
+ const two = source.slice(i, i + 2)
584
+ if (two === "//") {
585
+ const end = source.indexOf("\n", i)
586
+ blank(i, end < 0 ? source.length : end)
587
+ i = end < 0 ? source.length : end
588
+ continue
589
+ }
590
+ if (two === "/*") {
591
+ const end = source.indexOf("*/", i + 2)
592
+ const stop = end < 0 ? source.length : end + 2
593
+ blank(i, stop)
594
+ i = stop
595
+ continue
596
+ }
597
+ const char = source[i]
598
+ if (char === "'" || char === "\"") {
599
+ const stop = scanQuoted(source, i, char)
600
+ blank(i, stop)
601
+ i = stop
602
+ continue
603
+ }
604
+ if (char === "`") {
605
+ const stop = scanTemplate(source, i)
606
+ blank(i, stop)
607
+ i = stop
608
+ continue
609
+ }
610
+ i++
611
+ }
612
+ return chars.join("")
613
+ }
614
+
615
+ const scanQuoted = (source: string, start: number, quote: "'" | "\""): number => {
616
+ for (let i = start + 1; i < source.length; i++) {
617
+ if (source[i] === "\\") {
618
+ i++
619
+ continue
620
+ }
621
+ if (source[i] === quote) return i + 1
622
+ }
623
+ return source.length
624
+ }
625
+
626
+ const scanTemplate = (source: string, start: number): number => {
627
+ for (let i = start + 1; i < source.length; i++) {
628
+ if (source[i] === "\\") {
629
+ i++
630
+ continue
631
+ }
632
+ if (source[i] === "`") return i + 1
633
+ if (source.slice(i, i + 2) === "${") {
634
+ let depth = 1
635
+ i += 2
636
+ while (i < source.length && depth > 0) {
637
+ if (source[i] === "{") depth++
638
+ else if (source[i] === "}") depth--
639
+ i++
640
+ }
641
+ i--
642
+ }
643
+ }
644
+ return source.length
645
+ }
package/src/utils.ts CHANGED
@@ -47,11 +47,6 @@ export function toString(v: unknown) {
47
47
  return `${v}`
48
48
  }
49
49
 
50
- export const typedKeysOf = <T extends {}>(obj: T) => Object.keys(obj) as (keyof T)[]
51
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
52
- export const typedValuesOf = <T extends {}>(obj: T) => Object.values(obj) as ValueOf<T>[]
53
- type ValueOf<T> = T[keyof T]
54
-
55
50
  export type Constructor<T = any> = { new(...args: any[]): T }
56
51
  export type ThenArg<T> = T extends Promise<infer U> ? U
57
52
  : T extends (...args: any[]) => Promise<infer V> ? V
@@ -133,14 +128,6 @@ export function dropUndefined<A>(
133
128
  type GetTag<T> = T extends { _tag: infer K } ? K : never
134
129
  export const isOfType = <T extends { _tag: string }>(tag: GetTag<T>) => (e: { _tag: string }): e is T => e._tag === tag
135
130
 
136
- export function capitalize<T extends string>(string: T): Capitalize<T> {
137
- return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize<T>
138
- }
139
-
140
- export function uncapitalize<T extends string>(string: T): Uncapitalize<T> {
141
- return (string.charAt(0).toLowerCase() + string.slice(1)) as Uncapitalize<T>
142
- }
143
-
144
131
  export function pretty(o: unknown): string {
145
132
  return JSON.stringify(o, undefined, 2) ?? "undefined"
146
133
  }
@@ -565,10 +552,6 @@ export function exhaustiveMatch_<T extends string>(t: T) {
565
552
  return <Out extends Record<T, (t: T) => any>>(handlers: Out): ReturnType<Out[keyof Out]> => handlers[t](t)
566
553
  }
567
554
 
568
- export function assertUnreachable(x: never): never {
569
- throw new Error("Unknown case " + x)
570
- }
571
-
572
555
  export type OptPromise<T extends () => any> = (
573
556
  ...args: Parameters<T>
574
557
  ) => Promise<ReturnType<T>> | ReturnType<T>