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.
- package/CHANGELOG.md +21 -0
- package/dist/Array.d.ts +0 -17
- package/dist/Array.d.ts.map +1 -1
- package/dist/Array.js +2 -26
- package/dist/Chunk.d.ts +3 -27
- package/dist/Chunk.d.ts.map +1 -1
- package/dist/Chunk.js +4 -39
- package/dist/DateTime.d.ts +33 -0
- package/dist/DateTime.d.ts.map +1 -0
- package/dist/DateTime.js +18 -0
- package/dist/Effect.d.ts +0 -11
- package/dist/Effect.d.ts.map +1 -1
- package/dist/Effect.js +3 -18
- package/dist/Function.d.ts +0 -4
- package/dist/Function.d.ts.map +1 -1
- package/dist/Function.js +1 -7
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
- package/dist/Model/Repository/internal/internal.js +4 -5
- package/dist/Model/dsl.d.ts +4 -3
- package/dist/Model/dsl.d.ts.map +1 -1
- package/dist/Model/dsl.js +6 -4
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.js +5 -6
- package/dist/Option.d.ts +0 -2
- package/dist/Option.d.ts.map +1 -1
- package/dist/Option.js +1 -7
- package/dist/Pure.d.ts +4 -4
- package/dist/Pure.d.ts.map +1 -1
- package/dist/Pure.js +16 -16
- package/dist/Schema/ext.d.ts +0 -21
- package/dist/Schema/ext.d.ts.map +1 -1
- package/dist/Schema/ext.js +4 -10
- package/dist/Schema.d.ts +1 -1
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +2 -2
- package/dist/Set.d.ts.map +1 -1
- package/dist/Set.js +6 -5
- package/dist/client/apiClientFactory.d.ts.map +1 -1
- package/dist/client/apiClientFactory.js +7 -6
- package/dist/migration/nativeEffect.d.ts +21 -0
- package/dist/migration/nativeEffect.d.ts.map +1 -0
- package/dist/migration/nativeEffect.js +598 -0
- package/dist/utils.d.ts +0 -6
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -13
- package/dist/waitingGate.d.ts +38 -0
- package/dist/waitingGate.d.ts.map +1 -0
- package/dist/waitingGate.js +63 -0
- package/dist/withToast.d.ts.map +1 -1
- package/dist/withToast.js +42 -3
- package/package.json +2 -3
- package/src/Array.ts +4 -36
- package/src/Chunk.ts +4 -53
- package/src/DateTime.ts +473 -0
- package/src/Effect.ts +3 -22
- package/src/Function.ts +0 -7
- package/src/Model/Repository/internal/internal.ts +3 -4
- package/src/Model/dsl.ts +5 -4
- package/src/Model/query/new-kid-interpreter.ts +4 -5
- package/src/Option.ts +1 -8
- package/src/Pure.ts +15 -15
- package/src/Schema/ext.ts +3 -10
- package/src/Schema.ts +1 -1
- package/src/Set.ts +5 -4
- package/src/client/apiClientFactory.ts +8 -7
- package/src/migration/nativeEffect.ts +645 -0
- package/src/utils.ts +0 -17
- package/src/waitingGate.ts +82 -0
- package/src/withToast.ts +51 -5
- package/test/dateTimeLocal.test.ts +55 -0
- package/test/dist/dateTimeLocal.test.d.ts.map +1 -0
- package/test/dist/nativeEffectMigration.test.d.ts.map +1 -0
- package/test/dist/waitingGate.test.d.ts.map +1 -0
- package/test/nativeEffectMigration.test.ts +77 -0
- package/test/waitingGate.test.ts +64 -0
- package/dist/_ext/date.d.ts +0 -15
- package/dist/_ext/date.d.ts.map +0 -1
- package/dist/_ext/date.js +0 -16
- package/dist/_ext/misc.d.ts +0 -17
- package/dist/_ext/misc.d.ts.map +0 -1
- package/dist/_ext/misc.js +0 -19
- package/src/_ext/date.ts +0 -27
- 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>
|