effect-start 0.26.0 → 0.27.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 (58) hide show
  1. package/package.json +4 -2
  2. package/src/Entity.ts +6 -6
  3. package/src/FileRouterCodegen.ts +4 -4
  4. package/src/FileSystem.ts +4 -8
  5. package/src/RouteHook.ts +1 -1
  6. package/src/RouteSse.ts +3 -3
  7. package/src/SqlIntrospect.ts +2 -2
  8. package/src/Start.ts +102 -2
  9. package/src/Values.ts +11 -0
  10. package/src/bun/BunRoute.ts +1 -1
  11. package/src/bun/BunRuntime.ts +5 -5
  12. package/src/hyper/HyperHtml.ts +11 -7
  13. package/src/hyper/jsx.d.ts +1 -1
  14. package/src/lint/plugin.js +174 -4
  15. package/src/sql/SqlClient.ts +355 -0
  16. package/src/sql/bun/index.ts +117 -50
  17. package/src/sql/index.ts +1 -1
  18. package/src/sql/libsql/index.ts +91 -77
  19. package/src/sql/libsql/libsql.d.ts +4 -1
  20. package/src/sql/mssql/index.ts +141 -108
  21. package/src/sql/mssql/mssql.d.ts +1 -0
  22. package/src/testing/TestLogger.ts +4 -4
  23. package/src/x/tailwind/compile.ts +6 -14
  24. package/src/console/Console.ts +0 -42
  25. package/src/console/ConsoleErrors.ts +0 -213
  26. package/src/console/ConsoleLogger.ts +0 -56
  27. package/src/console/ConsoleMetrics.ts +0 -72
  28. package/src/console/ConsoleProcess.ts +0 -59
  29. package/src/console/ConsoleStore.ts +0 -187
  30. package/src/console/ConsoleTracer.ts +0 -107
  31. package/src/console/Simulation.ts +0 -814
  32. package/src/console/console.html +0 -340
  33. package/src/console/index.ts +0 -3
  34. package/src/console/routes/errors/route.tsx +0 -97
  35. package/src/console/routes/fiberDetail.tsx +0 -54
  36. package/src/console/routes/fibers/route.tsx +0 -45
  37. package/src/console/routes/git/route.tsx +0 -64
  38. package/src/console/routes/layout.tsx +0 -4
  39. package/src/console/routes/logs/route.tsx +0 -77
  40. package/src/console/routes/metrics/route.tsx +0 -36
  41. package/src/console/routes/route.tsx +0 -8
  42. package/src/console/routes/routes/route.tsx +0 -30
  43. package/src/console/routes/services/route.tsx +0 -21
  44. package/src/console/routes/system/route.tsx +0 -43
  45. package/src/console/routes/traceDetail.tsx +0 -22
  46. package/src/console/routes/traces/route.tsx +0 -81
  47. package/src/console/routes/tree.ts +0 -30
  48. package/src/console/ui/Errors.tsx +0 -76
  49. package/src/console/ui/Fibers.tsx +0 -321
  50. package/src/console/ui/Git.tsx +0 -182
  51. package/src/console/ui/Logs.tsx +0 -46
  52. package/src/console/ui/Metrics.tsx +0 -78
  53. package/src/console/ui/Routes.tsx +0 -125
  54. package/src/console/ui/Services.tsx +0 -273
  55. package/src/console/ui/Shell.tsx +0 -62
  56. package/src/console/ui/System.tsx +0 -131
  57. package/src/console/ui/Traces.tsx +0 -426
  58. package/src/sql/Sql.ts +0 -51
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "effect-start",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "src/",
7
7
  "!src/**/*.test.ts",
8
8
  "!src/**/*.test.tsx",
9
+ "!src/tower",
9
10
  "dist/",
10
11
  "README.md",
11
12
  "package.json"
@@ -16,7 +17,8 @@
16
17
  "./bun": "./src/bun/index.ts",
17
18
  "./client": "./src/client/index.ts",
18
19
  "./testing": "./src/testing/index.ts",
19
- "./console": "./src/console/index.ts",
20
+ "./tower": "./src/tower/index.ts",
21
+ "./sql": "./src/sql/index.ts",
20
22
  "./sql/*": "./src/sql/*/index.ts",
21
23
  "./x/*": "./src/x/*/index.ts",
22
24
  "./x/tailwind/plugin": "./src/x/tailwind/plugin.ts",
package/src/Entity.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import * as Effectable from "effect/Effectable"
1
2
  import * as Effect from "effect/Effect"
2
3
  import * as ParseResult from "effect/ParseResult"
3
- import * as Pipeable from "effect/Pipeable"
4
4
  import * as Predicate from "effect/Predicate"
5
5
  import * as Schema from "effect/Schema"
6
6
  import * as Stream from "effect/Stream"
@@ -24,7 +24,7 @@ export type Headers = {
24
24
  [header: string]: string | null | undefined
25
25
  }
26
26
 
27
- export interface Entity<T = unknown, E = never> extends Pipeable.Pipeable {
27
+ export interface Entity<T = unknown, E = never> extends Effect.Effect<Entity<T, E>, E> {
28
28
  readonly [TypeId]: TypeId
29
29
  readonly body: T
30
30
  readonly headers: Headers
@@ -57,7 +57,7 @@ export interface Entity<T = unknown, E = never> extends Pipeable.Pipeable {
57
57
  : Stream.Stream<Uint8Array, ParseResult.ParseError | E>
58
58
  }
59
59
 
60
- export interface Proto extends Pipeable.Pipeable {
60
+ export interface Proto extends Effect.Effect<Entity, never> {
61
61
  readonly [TypeId]: TypeId
62
62
  }
63
63
 
@@ -218,11 +218,11 @@ function getStream(self: Entity<unknown, unknown>): Stream.Stream<unknown, unkno
218
218
  return Stream.fromEffect(getBytes(self))
219
219
  }
220
220
 
221
- const Proto: Proto = Object.defineProperties(Object.create(null), {
221
+ const Proto: Proto = Object.defineProperties(Object.create(Effectable.CommitPrototype), {
222
222
  [TypeId]: { value: TypeId },
223
- pipe: {
223
+ commit: {
224
224
  value: function (this: Entity) {
225
- return Pipeable.pipeArguments(this, arguments)
225
+ return resolve(this)
226
226
  },
227
227
  },
228
228
  text: {
@@ -166,14 +166,14 @@ export function generateCode(fileRoutes: FileRouter.OrderedFileRoutes): string |
166
166
  }
167
167
 
168
168
  const routeEntries = entries
169
- .map(({ path, loaders }) => {
170
- const loadersCode = loaders.join(",\n ")
171
- return ` "${path}": [\n ${loadersCode},\n ]`
169
+ .map((v) => {
170
+ const loadersCode = v.loaders.join(",\n ")
171
+ return ` "${v.path}": [\n ${loadersCode},\n ]`
172
172
  })
173
173
  .join(",\n")
174
174
 
175
175
  return `/**
176
- * Auto-generated by effect-start.
176
+ * Auto-generated by effect-start on startup and changes. Do not edit manually.
177
177
  */
178
178
 
179
179
  export default {
package/src/FileSystem.ts CHANGED
@@ -344,14 +344,10 @@ export const make = (
344
344
 
345
345
  const fileStream = (
346
346
  file: File,
347
- {
348
- bufferSize = 16,
349
- bytesToRead: bytesToRead_,
350
- chunkSize: chunkSize_ = Size(64 * 1024),
351
- }: StreamOptions = {},
347
+ options: StreamOptions = {},
352
348
  ) => {
353
- const bytesToRead = bytesToRead_ !== undefined ? Size(bytesToRead_) : undefined
354
- const chunkSize = Size(chunkSize_)
349
+ const bytesToRead = options.bytesToRead !== undefined ? Size(options.bytesToRead) : undefined
350
+ const chunkSize = Size(options.chunkSize ?? 64 * 1024)
355
351
 
356
352
  function loop(
357
353
  totalBytesRead: bigint,
@@ -385,6 +381,6 @@ const fileStream = (
385
381
  }
386
382
 
387
383
  return Stream.bufferChunks(Stream.fromChannel(loop(BigInt(0))), {
388
- capacity: bufferSize,
384
+ capacity: options.bufferSize ?? 16,
389
385
  })
390
386
  }
package/src/RouteHook.ts CHANGED
@@ -34,7 +34,7 @@ export function filter<
34
34
 
35
35
  const mergedContext = filterResult ? { ...context, ...filterResult.context } : context
36
36
 
37
- return yield* Entity.resolve(next(mergedContext as Partial<BOut>))
37
+ return yield* next(mergedContext as Partial<BOut>)
38
38
  }),
39
39
  )
40
40
 
package/src/RouteSse.ts CHANGED
@@ -13,7 +13,7 @@ const HEARTBEAT = ": <3\n\n"
13
13
 
14
14
  export interface SseEvent {
15
15
  data?: string | undefined
16
- event?: string
16
+ type?: string
17
17
  retry?: number
18
18
  }
19
19
 
@@ -35,8 +35,8 @@ function formatSseEvent(event: SseEventInput): string {
35
35
 
36
36
  const e = event as SseEvent
37
37
  let result = ""
38
- if (e.event) {
39
- result += `event: ${e.event}\n`
38
+ if (e.type) {
39
+ result += `event: ${e.type}\n`
40
40
  }
41
41
  if (typeof e.data === "string") {
42
42
  for (const line of e.data.split("\n")) {
@@ -1,6 +1,6 @@
1
1
  import * as Effect from "effect/Effect"
2
2
  import * as Schema from "effect/Schema"
3
- import * as Sql from "./sql/Sql.ts"
3
+ import * as Sql from "./sql/SqlClient.ts"
4
4
 
5
5
  export interface Column {
6
6
  readonly tableSchema: string
@@ -496,7 +496,7 @@ export interface DatabaseReader {
496
496
  const escapeIdentifier = (id: string): string => `"${id.replace(/"/g, '""')}"`
497
497
 
498
498
  const concatSql = (
499
- sql: Sql.SqlQuery,
499
+ sql: Sql.Connection,
500
500
  fragments: Array<{ strings: ReadonlyArray<string>; values: Array<unknown> }>,
501
501
  ): Effect.Effect<ReadonlyArray<unknown>, Sql.SqlError> => {
502
502
  const strings: Array<string> = []
package/src/Start.ts CHANGED
@@ -2,8 +2,12 @@ import type * as FileSystem from "./FileSystem.ts"
2
2
  import * as Context from "effect/Context"
3
3
  import * as Deferred from "effect/Deferred"
4
4
  import * as Effect from "effect/Effect"
5
+ import * as ExecutionStrategy from "effect/ExecutionStrategy"
6
+ import * as Exit from "effect/Exit"
5
7
  import * as Function from "effect/Function"
6
8
  import * as Layer from "effect/Layer"
9
+ import * as Scope from "effect/Scope"
10
+ import * as SynchronizedRef from "effect/SynchronizedRef"
7
11
  import type * as ChildProcess from "./ChildProcess.ts"
8
12
  import * as BunRuntime from "./bun/BunRuntime.ts"
9
13
  import * as BunServer from "./bun/BunServer.ts"
@@ -44,8 +48,10 @@ export function layer<
44
48
  * @since 1.0.0
45
49
  * @category constructors
46
50
  */
47
- export function pack<const Layers extends readonly [Layer.Layer.Any, ...Array<Layer.Layer.Any>]>(
48
- ...layers: Layers
51
+ export function pack<
52
+ const Layers extends readonly [Layer.Layer.Any, ...Array<Layer.Layer.Any>],
53
+ >(
54
+ ...layers: Layers & OrderedPack<NoInfer<Layers>, NoInfer<Layers>>
49
55
  ): Layer.Layer<
50
56
  { [K in keyof Layers]: Layer.Layer.Success<Layers[K]> }[number],
51
57
  { [K in keyof Layers]: Layer.Layer.Error<Layers[K]> }[number],
@@ -64,6 +70,100 @@ export function pack<const Layers extends readonly [Layer.Layer.Any, ...Array<La
64
70
  return result as AnyLayer
65
71
  }
66
72
 
73
+ type Unsatisfied<Unmet, Success> =
74
+ Unmet extends Success ? Unmet : never
75
+
76
+ type OrderedPack<
77
+ Layers extends readonly Layer.Layer.Any[],
78
+ All extends readonly Layer.Layer.Any[],
79
+ > = Layers extends readonly [
80
+ infer Head extends Layer.Layer.Any,
81
+ ...infer Tail extends Layer.Layer.Any[],
82
+ ]
83
+ ? [
84
+ [
85
+ Unsatisfied<
86
+ Exclude<Layer.Layer.Context<Head>, { [K in keyof Tail]: Layer.Layer.Success<Tail[K]> }[number]>,
87
+ { [K in keyof All]: Layer.Layer.Success<All[K]> }[number]
88
+ >,
89
+ ] extends [never]
90
+ ? Head
91
+ : never,
92
+ ...OrderedPack<Tail, All>,
93
+ ]
94
+ : []
95
+
96
+ /**
97
+ * Like `pack`, but accepts layers in any order.
98
+ *
99
+ * ```ts
100
+ * // These all produce the same result:
101
+ * Start.build(LoggerLive, DatabaseLive, UserRepoLive)
102
+ * Start.build(UserRepoLive, DatabaseLive, LoggerLive)
103
+ * ```
104
+ *
105
+ * @since 1.0.0
106
+ * @category constructors
107
+ */
108
+ export function build<
109
+ const Layers extends readonly [Layer.Layer.Any, ...Array<Layer.Layer.Any>],
110
+ >(
111
+ ...layers: Layers
112
+ ): Layer.Layer<
113
+ { [K in keyof Layers]: Layer.Layer.Success<Layers[K]> }[number],
114
+ { [K in keyof Layers]: Layer.Layer.Error<Layers[K]> }[number],
115
+ Exclude<
116
+ { [K in keyof Layers]: Layer.Layer.Context<Layers[K]> }[number],
117
+ { [K in keyof Layers]: Layer.Layer.Success<Layers[K]> }[number]
118
+ >
119
+ > {
120
+ type AnyLayer = Layer.Layer<any, any, any>
121
+ const layerArray = layers as unknown as ReadonlyArray<AnyLayer>
122
+
123
+ return Layer.scopedContext(
124
+ Effect.gen(function* () {
125
+ const scope = yield* Effect.scope
126
+ const memoMap = yield* Layer.makeMemoMap
127
+ const memoMapRef = (memoMap as any).ref as SynchronizedRef.SynchronizedRef<Map<AnyLayer, any>>
128
+ let ctx = yield* Effect.context<any>()
129
+ const pending = new Set<AnyLayer>(layerArray)
130
+
131
+ for (let pass = 0; pass < layerArray.length && pending.size > 0; pass++) {
132
+ for (const layer of [...pending]) {
133
+ const childScope = yield* Scope.fork(scope, ExecutionStrategy.sequential)
134
+ const exit = yield* layer.pipe(
135
+ Layer.buildWithMemoMap(memoMap, childScope),
136
+ Effect.provide(ctx),
137
+ Effect.exit,
138
+ )
139
+ if (Exit.isSuccess(exit)) {
140
+ ctx = Context.merge(ctx, exit.value)
141
+ pending.delete(layer)
142
+ } else {
143
+ yield* Scope.close(childScope, exit)
144
+ yield* SynchronizedRef.update(memoMapRef, (map) => {
145
+ map.delete(layer)
146
+ return map
147
+ })
148
+ }
149
+ }
150
+ }
151
+
152
+ for (const layer of pending) {
153
+ const childScope = yield* Scope.fork(scope, ExecutionStrategy.sequential)
154
+ const built = yield* layer.pipe(
155
+ Layer.buildWithMemoMap(memoMap, childScope),
156
+ Effect.provide(ctx),
157
+ )
158
+ ctx = Context.merge(ctx, built)
159
+ }
160
+
161
+ return ctx
162
+ }),
163
+ // TODO: not sure how to properly type it yet
164
+ ) as any
165
+ }
166
+
67
167
  export type PlatformServices =
68
168
  | BunServer.BunServer
69
169
  | FileSystem.FileSystem
package/src/Values.ts CHANGED
@@ -73,9 +73,20 @@ export const firstValue = <T>(record: Record<string, T>): T | undefined => {
73
73
  return undefined
74
74
  }
75
75
 
76
+ export const isTemplateStringsArray = (value: unknown): value is TemplateStringsArray =>
77
+ Array.isArray(value) && "raw" in value
78
+
76
79
  export const concatBytes = (a: Uint8Array, b: Uint8Array): Uint8Array => {
77
80
  const result = new Uint8Array(a.byteLength + b.byteLength)
78
81
  result.set(a)
79
82
  result.set(b, a.byteLength)
80
83
  return result
81
84
  }
85
+
86
+ export const compact = (record: Record<string, unknown | undefined>): Record<string, unknown> => {
87
+ const out: Record<string, unknown> = {}
88
+ for (const [key, value] of Object.entries(record)) {
89
+ if (value !== undefined) out[key] = value
90
+ }
91
+ return out
92
+ }
@@ -113,7 +113,7 @@ export function htmlBundle(load: () => HTMLBundleModule | Promise<HTMLBundleModu
113
113
  }),
114
114
  })
115
115
 
116
- const childEntity = yield* Entity.resolve(next(context))
116
+ const childEntity = yield* next(context)
117
117
  const children = childEntity?.body ?? childEntity
118
118
 
119
119
  let childrenHtml = ""
@@ -7,19 +7,19 @@ const mainFiber = GlobalValue.globalValue(Symbol.for("effect-start/BunRuntime/ex
7
7
  MutableRef.make<Fiber.RuntimeFiber<unknown, unknown> | undefined>(undefined),
8
8
  )
9
9
 
10
- export const runMain = PlatformRuntime.makeRunMain(({ fiber, teardown }) => {
10
+ export const runMain = PlatformRuntime.makeRunMain((options) => {
11
11
  const prevFiber = MutableRef.get(mainFiber)
12
12
 
13
- MutableRef.set(mainFiber, fiber)
13
+ MutableRef.set(mainFiber, options.fiber)
14
14
 
15
15
  let receivedSignal = false
16
16
 
17
- fiber.addObserver((exit) => {
17
+ options.fiber.addObserver((exit) => {
18
18
  if (!receivedSignal) {
19
19
  process.removeListener("SIGINT", onSigint)
20
20
  process.removeListener("SIGTERM", onSigint)
21
21
  }
22
- teardown(exit, (code) => {
22
+ options.teardown(exit, (code) => {
23
23
  if (receivedSignal || code !== 0) {
24
24
  process.exit(code)
25
25
  }
@@ -30,7 +30,7 @@ export const runMain = PlatformRuntime.makeRunMain(({ fiber, teardown }) => {
30
30
  receivedSignal = true
31
31
  process.removeListener("SIGINT", onSigint)
32
32
  process.removeListener("SIGTERM", onSigint)
33
- fiber.unsafeInterruptAsFork(fiber.id())
33
+ options.fiber.unsafeInterruptAsFork(options.fiber.id())
34
34
  }
35
35
 
36
36
  process.on("SIGINT", onSigint)
@@ -53,6 +53,9 @@ let map = {
53
53
 
54
54
  const RAW_TEXT_TAGS = ["script", "style"]
55
55
 
56
+ // Prevents closing html tags in embedded css/js source
57
+ const escapeRawText = (text: string) => text.replaceAll("</", "<\\/")
58
+
56
59
  export function renderToString(
57
60
  node: JSX.Children,
58
61
  hooks?: { onNode?: (node: HyperNode.HyperNode) => void },
@@ -113,8 +116,7 @@ export function renderToString(
113
116
  for (const key in props) {
114
117
  if (
115
118
  key !== "children" &&
116
- key !== "innerHTML" && // Solid-specific
117
- key !== "dangerouslySetInnerHTML" && // React-specific
119
+ key !== "dangerouslySetInnerHTML" &&
118
120
  props[key] !== false &&
119
121
  props[key] != null
120
122
  ) {
@@ -140,17 +142,19 @@ export function renderToString(
140
142
  if (!EMPTY_TAGS.includes(type)) {
141
143
  stack.push(`</${type}>`)
142
144
 
143
- const html = props.dangerouslySetInnerHTML?.__html ?? props.innerHTML
145
+ const isRawText = RAW_TEXT_TAGS.includes(type)
146
+ const html = props.dangerouslySetInnerHTML?.__html
144
147
 
145
148
  if (html) {
146
- result += html
149
+ result += isRawText ? escapeRawText(html) : html
147
150
  } else {
148
151
  const children = props.children
149
152
 
150
153
  if (type === "script" && typeof children === "function") {
151
- result += `(${children.toString()})(window)`
152
- } else if (RAW_TEXT_TAGS.includes(type) && children != null) {
153
- result += Array.isArray(children) ? children.join("") : children
154
+ result += escapeRawText(`(${children.toString()})(window)`)
155
+ } else if (isRawText && children != null) {
156
+ const raw = Array.isArray(children) ? children.join("") : String(children)
157
+ result += escapeRawText(raw)
154
158
  } else if (Array.isArray(children)) {
155
159
  for (let i = children.length - 1; i >= 0; i--) {
156
160
  stack.push(children[i])
@@ -536,7 +536,7 @@ export namespace JSX {
536
536
  // [key: ClassKeys]: boolean;
537
537
 
538
538
  // properties
539
- innerHTML?: string
539
+ dangerouslySetInnerHTML?: { __html: string }
540
540
  textContent?: string | number
541
541
 
542
542
  // attributes
@@ -55,15 +55,41 @@ export default {
55
55
 
56
56
  const typePrefix = node.importKind === "type" ? "type " : ""
57
57
 
58
+ const sourceCode = context.sourceCode || context.getSourceCode()
59
+
58
60
  context.report({
59
61
  node,
60
62
  messageId: "preferNamespace",
61
63
  data: { source, baseName, typePrefix },
62
64
  fix(fixer) {
63
- return fixer.replaceText(
64
- node,
65
- `import ${typePrefix}* as ${baseName} from "${source}"`,
66
- )
65
+ const fixes = [
66
+ fixer.replaceText(
67
+ node,
68
+ `import ${typePrefix}* as ${baseName} from "${source}"`,
69
+ ),
70
+ ]
71
+
72
+ for (const specifier of node.specifiers) {
73
+ if (specifier.type !== "ImportSpecifier") continue
74
+ const localName = specifier.local.name
75
+ const importedName = specifier.imported.name
76
+
77
+ for (const variable of sourceCode.getDeclaredVariables(specifier)) {
78
+ for (const ref of variable.references) {
79
+ if (ref.identifier.range[0] === specifier.local.range[0]) continue
80
+ fixes.push(
81
+ fixer.replaceTextRange(
82
+ ref.identifier.range,
83
+ localName !== importedName
84
+ ? `${baseName}.${importedName}`
85
+ : `${baseName}.${localName}`,
86
+ ),
87
+ )
88
+ }
89
+ }
90
+ }
91
+
92
+ return fixes
67
93
  },
68
94
  })
69
95
  },
@@ -182,6 +208,150 @@ export default {
182
208
  },
183
209
  },
184
210
 
211
+ "no-destructured-params": {
212
+ meta: {
213
+ type: "suggestion",
214
+ docs: {
215
+ description: "Disallow destructuring objects in function parameters",
216
+ },
217
+ fixable: "code",
218
+ schema: [],
219
+ messages: {
220
+ noDestructuredParam:
221
+ "Avoid destructuring objects in function parameters. Use a single parameter and access properties on it instead.",
222
+ },
223
+ },
224
+ create(context) {
225
+ const sourceCode = context.sourceCode || context.getSourceCode()
226
+
227
+ function findUnusedName(scope, base) {
228
+ const names = new Set()
229
+ let current = scope
230
+ while (current) {
231
+ for (const v of current.variables) names.add(v.name)
232
+ current = current.upper
233
+ }
234
+ if (!names.has(base)) return base
235
+ for (let i = 2; ; i++) {
236
+ const candidate = base + i
237
+ if (!names.has(candidate)) return candidate
238
+ }
239
+ }
240
+
241
+ function isDefinition(node) {
242
+ if (node.type === "FunctionDeclaration") return true
243
+ const ancestors = sourceCode.getAncestors(node)
244
+ const parent = ancestors[ancestors.length - 1]
245
+ if (!parent) return false
246
+ if (parent.type === "MethodDefinition" || parent.type === "Property" && parent.method) return true
247
+ if (
248
+ parent.type === "VariableDeclarator" &&
249
+ parent.init === node
250
+ ) return true
251
+ return false
252
+ }
253
+
254
+ function checkParams(node) {
255
+ for (const param of node.params) {
256
+ const pattern =
257
+ param.type === "AssignmentPattern" ? param.left : param
258
+ if (pattern.type !== "ObjectPattern") continue
259
+
260
+ const baseName = isDefinition(node) ? "options" : "v"
261
+ const outerScope = sourceCode.getScope(node).upper || sourceCode.getScope(node)
262
+ const name = findUnusedName(outerScope, baseName)
263
+
264
+ const hasPropertyDefaults = pattern.properties.some(
265
+ (prop) =>
266
+ prop.type !== "RestElement" &&
267
+ prop.value.type === "AssignmentPattern",
268
+ )
269
+
270
+ context.report({
271
+ node: pattern,
272
+ messageId: "noDestructuredParam",
273
+ fix: hasPropertyDefaults ? undefined : (fixer) => {
274
+ const fixes = []
275
+
276
+ const typeAnnotation = pattern.typeAnnotation
277
+ ? sourceCode.getText(pattern.typeAnnotation)
278
+ : ""
279
+ const hasDefault = param.type === "AssignmentPattern"
280
+ const defaultValue = hasDefault
281
+ ? " = " + sourceCode.getText(param.right)
282
+ : ""
283
+
284
+ fixes.push(
285
+ fixer.replaceTextRange(
286
+ param.range,
287
+ name + typeAnnotation + defaultValue,
288
+ ),
289
+ )
290
+
291
+ const fnScope = sourceCode.getScope(node)
292
+ const localToKey = new Map()
293
+ for (const prop of pattern.properties) {
294
+ if (prop.type === "RestElement") continue
295
+ const keyName =
296
+ prop.key.type === "Identifier"
297
+ ? prop.key.name
298
+ : sourceCode.getText(prop.key)
299
+ const localNode =
300
+ prop.value.type === "AssignmentPattern"
301
+ ? prop.value.left
302
+ : prop.value
303
+ if (localNode.type === "Identifier") {
304
+ localToKey.set(localNode.name, keyName)
305
+ }
306
+ }
307
+
308
+ for (const variable of fnScope.variables) {
309
+ const keyName = localToKey.get(variable.name)
310
+ if (keyName === undefined) continue
311
+
312
+ for (const ref of variable.references) {
313
+ if (ref.identifier.range[0] >= pattern.range[0] &&
314
+ ref.identifier.range[1] <= pattern.range[1]) continue
315
+
316
+ const ancestors = sourceCode.getAncestors(ref.identifier)
317
+ const parent = ancestors[ancestors.length - 1]
318
+ if (
319
+ parent &&
320
+ parent.type === "Property" &&
321
+ parent.shorthand &&
322
+ parent.value === ref.identifier
323
+ ) {
324
+ fixes.push(
325
+ fixer.replaceTextRange(
326
+ parent.range,
327
+ keyName + ": " + name + "." + keyName,
328
+ ),
329
+ )
330
+ } else {
331
+ fixes.push(
332
+ fixer.replaceTextRange(
333
+ ref.identifier.range,
334
+ name + "." + keyName,
335
+ ),
336
+ )
337
+ }
338
+ }
339
+ }
340
+
341
+ return fixes
342
+ },
343
+ })
344
+ }
345
+ }
346
+
347
+ return {
348
+ FunctionDeclaration: checkParams,
349
+ FunctionExpression: checkParams,
350
+ ArrowFunctionExpression: checkParams,
351
+ }
352
+ },
353
+ },
354
+
185
355
  "test-assertion-newline": {
186
356
  meta: {
187
357
  type: "layout",