kea-typegen 3.6.8 → 3.7.1

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.
@@ -1,44 +1,271 @@
1
1
  import { spawn } from 'child_process'
2
+ import * as fs from 'fs'
3
+ import * as os from 'os'
2
4
  import * as path from 'path'
5
+ import * as ts from 'typescript'
6
+ import { planWatchTypegenPass } from '../watch'
3
7
 
4
- test(
5
- 'watch mode does not overlap successive typegen passes',
6
- async () => {
7
- const repoRoot = path.resolve(__dirname, '..', '..')
8
- const scriptPath = path.join(repoRoot, 'src/test-support/watch-mode-smoke.js')
8
+ function writeFile(filePath: string, source: string): void {
9
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
10
+ fs.writeFileSync(filePath, source)
11
+ }
9
12
 
10
- const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null; stdout: string; stderr: string }>(
11
- (resolve, reject) => {
12
- const child = spawn(process.execPath, [scriptPath], {
13
- cwd: repoRoot,
14
- env: { ...process.env, FORCE_COLOR: '0' },
15
- })
13
+ function createProgram(fileNames: string[]): ts.Program {
14
+ return ts.createProgram(fileNames, {
15
+ module: ts.ModuleKind.CommonJS,
16
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
17
+ target: ts.ScriptTarget.ES2020,
18
+ skipLibCheck: true,
19
+ })
20
+ }
16
21
 
17
- let stdout = ''
18
- let stderr = ''
22
+ test('watch mode does not overlap successive typegen passes', async () => {
23
+ const repoRoot = path.resolve(__dirname, '..', '..')
24
+ const scriptPath = path.join(repoRoot, 'src/test-support/watch-mode-smoke.js')
19
25
 
20
- child.stdout.on('data', (chunk) => {
21
- stdout += chunk.toString()
22
- })
26
+ const result = await new Promise<{
27
+ code: number | null
28
+ signal: NodeJS.Signals | null
29
+ stdout: string
30
+ stderr: string
31
+ }>((resolve, reject) => {
32
+ const child = spawn(process.execPath, [scriptPath], {
33
+ cwd: repoRoot,
34
+ env: { ...process.env, FORCE_COLOR: '0' },
35
+ })
23
36
 
24
- child.stderr.on('data', (chunk) => {
25
- stderr += chunk.toString()
26
- })
37
+ let stdout = ''
38
+ let stderr = ''
27
39
 
28
- child.on('error', reject)
29
- child.on('close', (code, signal) => resolve({ code, signal, stdout, stderr }))
30
- },
40
+ child.stdout.on('data', (chunk) => {
41
+ stdout += chunk.toString()
42
+ })
43
+
44
+ child.stderr.on('data', (chunk) => {
45
+ stderr += chunk.toString()
46
+ })
47
+
48
+ child.on('error', reject)
49
+ child.on('close', (code, signal) => resolve({ code, signal, stdout, stderr }))
50
+ })
51
+
52
+ expect(result.signal).toBeNull()
53
+ expect(result.code).toBe(0)
54
+
55
+ const payload = JSON.parse(result.stdout.trim())
56
+
57
+ expect(payload.started).toBeGreaterThan(1)
58
+ expect(payload.completed).toBeGreaterThan(0)
59
+ expect(payload.maxActive).toBe(1)
60
+ expect(payload.incrementalAfterEdit).toBe(true)
61
+ expect(payload.parsedLogicCountsAfterEdit).toContain(1)
62
+ expect(payload.sourceFilePathsAfterEdit.some(Boolean)).toBe(true)
63
+ expect(result.stderr).toBe('')
64
+ }, 30000)
65
+
66
+ test('watch planner limits a direct logic edit to the changed logic and dependent logics', () => {
67
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-watch-direct-logic-'))
68
+
69
+ try {
70
+ const logicDir = path.join(tempDir, 'src')
71
+ const firstLogicPath = path.join(logicDir, 'firstLogic.ts')
72
+ const secondLogicPath = path.join(logicDir, 'secondLogic.ts')
73
+ const unrelatedLogicPath = path.join(logicDir, 'unrelatedLogic.ts')
74
+ const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
75
+
76
+ writeFile(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
77
+ writeFile(
78
+ firstLogicPath,
79
+ [
80
+ "import { kea } from 'kea'",
81
+ '',
82
+ 'export const firstLogic = kea({',
83
+ ' actions: () => ({ first: true }),',
84
+ '})',
85
+ '',
86
+ ].join('\n'),
87
+ )
88
+ writeFile(
89
+ secondLogicPath,
90
+ [
91
+ "import { kea } from 'kea'",
92
+ "import { firstLogic } from './firstLogic'",
93
+ '',
94
+ 'export const secondLogic = kea({',
95
+ ' connect: { values: [firstLogic, ["first"]] },',
96
+ '})',
97
+ '',
98
+ ].join('\n'),
99
+ )
100
+ writeFile(
101
+ unrelatedLogicPath,
102
+ [
103
+ "import { kea } from 'kea'",
104
+ '',
105
+ 'export const unrelatedLogic = kea({',
106
+ ' actions: () => ({ unrelated: true }),',
107
+ '})',
108
+ '',
109
+ ].join('\n'),
110
+ )
111
+
112
+ const program = createProgram([firstLogicPath, secondLogicPath, unrelatedLogicPath])
113
+ const plan = planWatchTypegenPass(program, [
114
+ { fileName: firstLogicPath, eventKind: ts.FileWatcherEventKind.Changed },
115
+ ])
116
+
117
+ expect(plan).toEqual({
118
+ kind: 'files',
119
+ sourceFilePaths: [firstLogicPath, secondLogicPath].sort(),
120
+ reason: '2 affected Kea logic files',
121
+ })
122
+ } finally {
123
+ fs.rmSync(tempDir, { recursive: true, force: true })
124
+ }
125
+ })
126
+
127
+ test('watch planner regenerates logics affected by a helper type edit', () => {
128
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-watch-helper-'))
129
+
130
+ try {
131
+ const logicDir = path.join(tempDir, 'src')
132
+ const helperPath = path.join(logicDir, 'helper.ts')
133
+ const logicPath = path.join(logicDir, 'logic.ts')
134
+ const unrelatedLogicPath = path.join(logicDir, 'unrelatedLogic.ts')
135
+ const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
136
+
137
+ writeFile(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
138
+ writeFile(helperPath, 'export interface HelperType { value: string }\n')
139
+ writeFile(
140
+ logicPath,
141
+ [
142
+ "import { kea } from 'kea'",
143
+ "import type { HelperType } from './helper'",
144
+ '',
145
+ 'export const logic = kea({',
146
+ ' actions: () => ({ setValue: (value: HelperType) => ({ value }) }),',
147
+ '})',
148
+ '',
149
+ ].join('\n'),
150
+ )
151
+ writeFile(
152
+ unrelatedLogicPath,
153
+ [
154
+ "import { kea } from 'kea'",
155
+ '',
156
+ 'export const unrelatedLogic = kea({',
157
+ ' actions: () => ({ unrelated: true }),',
158
+ '})',
159
+ '',
160
+ ].join('\n'),
161
+ )
162
+
163
+ const program = createProgram([helperPath, logicPath, unrelatedLogicPath])
164
+ const plan = planWatchTypegenPass(program, [
165
+ { fileName: helperPath, eventKind: ts.FileWatcherEventKind.Changed },
166
+ ])
167
+
168
+ expect(plan).toEqual({
169
+ kind: 'files',
170
+ sourceFilePaths: [logicPath],
171
+ reason: '1 affected Kea logic file',
172
+ })
173
+ } finally {
174
+ fs.rmSync(tempDir, { recursive: true, force: true })
175
+ }
176
+ })
177
+
178
+ test('watch planner maps generated type changes back to source logics and dependents', () => {
179
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-watch-type-file-'))
180
+
181
+ try {
182
+ const logicDir = path.join(tempDir, 'src')
183
+ const firstLogicPath = path.join(logicDir, 'firstLogic.ts')
184
+ const firstLogicTypePath = path.join(logicDir, 'firstLogicType.ts')
185
+ const secondLogicPath = path.join(logicDir, 'secondLogic.ts')
186
+ const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
187
+
188
+ writeFile(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
189
+ writeFile(firstLogicTypePath, 'export interface firstLogicType {}\n')
190
+ writeFile(
191
+ firstLogicPath,
192
+ [
193
+ "import { kea } from 'kea'",
194
+ "import type { firstLogicType } from './firstLogicType'",
195
+ '',
196
+ 'export const firstLogic = kea<firstLogicType>({',
197
+ ' actions: () => ({ first: true }),',
198
+ '})',
199
+ '',
200
+ ].join('\n'),
201
+ )
202
+ writeFile(
203
+ secondLogicPath,
204
+ [
205
+ "import { kea } from 'kea'",
206
+ "import { firstLogic } from './firstLogic'",
207
+ '',
208
+ 'export const secondLogic = kea({',
209
+ ' connect: { actions: [firstLogic, ["first"]] },',
210
+ '})',
211
+ '',
212
+ ].join('\n'),
213
+ )
214
+
215
+ const program = createProgram([firstLogicPath, firstLogicTypePath, secondLogicPath])
216
+ const plan = planWatchTypegenPass(program, [
217
+ { fileName: firstLogicTypePath, eventKind: ts.FileWatcherEventKind.Changed },
218
+ ])
219
+
220
+ expect(plan).toEqual({
221
+ kind: 'files',
222
+ sourceFilePaths: [firstLogicPath, secondLogicPath].sort(),
223
+ reason: '2 affected Kea logic files',
224
+ })
225
+ } finally {
226
+ fs.rmSync(tempDir, { recursive: true, force: true })
227
+ }
228
+ })
229
+
230
+ test('watch planner falls back to a full pass for deleted files and resetContext changes', () => {
231
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-watch-full-'))
232
+
233
+ try {
234
+ const logicDir = path.join(tempDir, 'src')
235
+ const logicPath = path.join(logicDir, 'logic.ts')
236
+ const contextPath = path.join(logicDir, 'context.ts')
237
+ const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
238
+
239
+ writeFile(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
240
+ writeFile(
241
+ logicPath,
242
+ [
243
+ "import { kea } from 'kea'",
244
+ '',
245
+ 'export const logic = kea({',
246
+ ' actions: () => ({ value: true }),',
247
+ '})',
248
+ '',
249
+ ].join('\n'),
250
+ )
251
+ writeFile(
252
+ contextPath,
253
+ ["import { resetContext } from 'kea'", '', 'resetContext({ plugins: [] })', ''].join('\n'),
31
254
  )
32
255
 
33
- expect(result.signal).toBeNull()
34
- expect(result.code).toBe(0)
256
+ const program = createProgram([logicPath, contextPath])
35
257
 
36
- const payload = JSON.parse(result.stdout.trim())
258
+ expect(
259
+ planWatchTypegenPass(program, [{ fileName: logicPath, eventKind: ts.FileWatcherEventKind.Deleted }]),
260
+ ).toEqual({ kind: 'full', reason: 'deleted file detected' })
37
261
 
38
- expect(payload.started).toBeGreaterThan(1)
39
- expect(payload.completed).toBeGreaterThan(0)
40
- expect(payload.maxActive).toBe(1)
41
- expect(result.stderr).toBe('')
42
- },
43
- 30000,
44
- )
262
+ expect(
263
+ planWatchTypegenPass(program, [{ fileName: contextPath, eventKind: ts.FileWatcherEventKind.Changed }]),
264
+ ).toEqual({
265
+ kind: 'full',
266
+ reason: `resetContext changed: ${path.relative(process.cwd(), contextPath)}`,
267
+ })
268
+ } finally {
269
+ fs.rmSync(tempDir, { recursive: true, force: true })
270
+ }
271
+ })
@@ -153,3 +153,59 @@ test('writeTypeImports replaces existing kea generic without leaving a trailing
153
153
  fs.rmSync(tempDir, { recursive: true, force: true })
154
154
  }
155
155
  })
156
+
157
+ test('visitProgram limits project-aware single-file generation to the requested source file', () => {
158
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-single-file-'))
159
+
160
+ try {
161
+ const logicDir = path.join(tempDir, 'src')
162
+ const firstLogicPath = path.join(logicDir, 'firstLogic.ts')
163
+ const secondLogicPath = path.join(logicDir, 'secondLogic.ts')
164
+ const keaDtsPath = path.join(tempDir, 'node_modules', 'kea', 'index.d.ts')
165
+
166
+ fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
167
+ fs.mkdirSync(logicDir, { recursive: true })
168
+
169
+ fs.writeFileSync(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
170
+ fs.writeFileSync(
171
+ firstLogicPath,
172
+ [
173
+ "import { kea } from 'kea'",
174
+ '',
175
+ 'export const firstLogic = kea({',
176
+ ' actions: () => ({ first: true }),',
177
+ '})',
178
+ '',
179
+ ].join('\n'),
180
+ )
181
+ fs.writeFileSync(
182
+ secondLogicPath,
183
+ [
184
+ "import { kea } from 'kea'",
185
+ '',
186
+ 'export const secondLogic = kea({',
187
+ ' actions: () => ({ second: true }),',
188
+ '})',
189
+ '',
190
+ ].join('\n'),
191
+ )
192
+
193
+ const program = ts.createProgram([firstLogicPath, secondLogicPath], {
194
+ module: ts.ModuleKind.CommonJS,
195
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
196
+ target: ts.ScriptTarget.ES2020,
197
+ skipLibCheck: true,
198
+ })
199
+
200
+ const parsedLogics = visitProgram(program, {
201
+ rootPath: logicDir,
202
+ typesPath: logicDir,
203
+ sourceFilePath: secondLogicPath,
204
+ log: () => {},
205
+ })
206
+
207
+ expect(parsedLogics.map((logic) => logic.logicName)).toEqual(['secondLogic'])
208
+ } finally {
209
+ fs.rmSync(tempDir, { recursive: true, force: true })
210
+ }
211
+ })
@@ -18,7 +18,11 @@ yargs
18
18
  await runTypeGen({ ...includeKeaConfig(parsedToAppOptions(argv)), write: true, watch: true })
19
19
  })
20
20
  .option('config', { alias: 'c', describe: 'Path to tsconfig.json (otherwise auto-detected)', type: 'string' })
21
- .option('file', { alias: 'f', describe: "Single file to evaluate (can't be used with --config)", type: 'string' })
21
+ .option('file', {
22
+ alias: 'f',
23
+ describe: 'Single file to evaluate. Uses --config/.kearc compiler options when available.',
24
+ type: 'string',
25
+ })
22
26
  .option('root', {
23
27
  alias: 'r',
24
28
  describe: 'Root for logic paths. E.g: ./frontend/src',
@@ -51,6 +55,11 @@ yargs
51
55
  describe: 'Cache generated logic files into .typegen, use them if generating a logic type for the first time',
52
56
  type: 'boolean',
53
57
  })
58
+ .option('prettier', {
59
+ describe: 'Format generated logic type declarations with Prettier (use --no-prettier to skip)',
60
+ type: 'boolean',
61
+ default: true,
62
+ })
54
63
  .option('verbose', { describe: 'Slightly more verbose output log', type: 'boolean' })
55
64
  .demandCommand()
56
65
  .help()
@@ -40,8 +40,16 @@ import { printInternalExtraInput } from './printInternalExtraInput'
40
40
  import { convertToBuilders } from '../write/convertToBuilders'
41
41
  import { cacheWrittenFile } from '../cache'
42
42
 
43
+ const prettierConfigCache = new Map<string, Promise<prettier.Options | null>>()
44
+
43
45
  export async function runThroughPrettier(sourceText: string, filePath: string): Promise<string> {
44
- const options = await prettier.resolveConfig(filePath)
46
+ let configPromise = prettierConfigCache.get(filePath)
47
+ if (!configPromise) {
48
+ configPromise = prettier.resolveConfig(filePath, { useCache: true })
49
+ prettierConfigCache.set(filePath, configPromise)
50
+ }
51
+
52
+ const options = await configPromise
45
53
  if (options) {
46
54
  try {
47
55
  return await prettier.format(sourceText, { ...options, filepath: filePath })
@@ -107,11 +115,12 @@ export async function printToFiles(
107
115
  const logicStrings = []
108
116
  const requiredKeys = new Set(['Logic'])
109
117
  for (const parsedLogic of parsedLogics) {
110
- const logicTypeStirng = await runThroughPrettier(
111
- nodeToString(parsedLogic.interfaceDeclaration),
112
- typeFileName,
118
+ const logicTypeString = nodeToString(parsedLogic.interfaceDeclaration)
119
+ logicStrings.push(
120
+ appOptions.prettier === false
121
+ ? logicTypeString
122
+ : await runThroughPrettier(logicTypeString, typeFileName),
113
123
  )
114
- logicStrings.push(logicTypeStirng)
115
124
  for (const string of parsedLogic.importFromKeaInLogicType.values()) {
116
125
  requiredKeys.add(string)
117
126
  }
@@ -1,12 +1,15 @@
1
1
  const fs = require('fs')
2
+ const os = require('os')
2
3
  const path = require('path')
3
4
 
4
5
  require('ts-node/register/transpile-only')
5
6
 
6
7
  const repoRoot = path.resolve(__dirname, '..', '..')
7
- const projectDir = fs.mkdtempSync(path.join(repoRoot, 'tmp-watch-smoke-'))
8
+ const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-watch-smoke-'))
8
9
  const tsconfigPath = path.join(projectDir, 'tsconfig.json')
10
+ const keaDtsPath = path.join(projectDir, 'node_modules', 'kea', 'index.d.ts')
9
11
  const fileCount = 200
12
+ const logicPaths = []
10
13
 
11
14
  const noop = () => {}
12
15
  console.info = noop
@@ -18,6 +21,10 @@ let completed = 0
18
21
  let settleTimer
19
22
  let timeoutTimer
20
23
  let finished = false
24
+ let editTriggered = false
25
+ let incrementalAfterEdit = false
26
+ const parsedLogicCountsAfterEdit = []
27
+ const sourceFilePathsAfterEdit = []
21
28
 
22
29
  function cleanup() {
23
30
  try {
@@ -41,16 +48,31 @@ function finish(code) {
41
48
  completed,
42
49
  active,
43
50
  maxActive,
51
+ incrementalAfterEdit,
52
+ parsedLogicCountsAfterEdit,
53
+ sourceFilePathsAfterEdit,
44
54
  }) + '\n',
45
55
  )
46
56
  process.exit(code)
47
57
  }
48
58
 
49
59
  function scheduleFinishIfSettled() {
50
- if (started > 1 && active === 0) {
51
- clearTimeout(settleTimer)
52
- settleTimer = setTimeout(() => finish(0), 300)
60
+ if (active !== 0) {
61
+ return
53
62
  }
63
+
64
+ clearTimeout(settleTimer)
65
+ settleTimer = setTimeout(() => {
66
+ if (!editTriggered && started > 1) {
67
+ editTriggered = true
68
+ fs.appendFileSync(logicPaths[0], '\n// trigger incremental watch pass\n')
69
+ return
70
+ }
71
+
72
+ if (editTriggered && incrementalAfterEdit) {
73
+ finish(0)
74
+ }
75
+ }, 300)
54
76
  }
55
77
 
56
78
  process.on('uncaughtException', (error) => {
@@ -82,9 +104,13 @@ fs.writeFileSync(
82
104
  ),
83
105
  )
84
106
 
107
+ fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
108
+ fs.writeFileSync(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
109
+
85
110
  for (let i = 0; i < fileCount; i++) {
86
111
  const dir = path.join(projectDir, 'src', `group${String(i % 10).padStart(2, '0')}`)
87
112
  const filePath = path.join(dir, `logic${i}.ts`)
113
+ logicPaths.push(filePath)
88
114
 
89
115
  fs.mkdirSync(dir, { recursive: true })
90
116
  fs.writeFileSync(
@@ -109,11 +135,20 @@ const printModule = require(path.join(repoRoot, 'src/print/print'))
109
135
  const originalPrintToFiles = printModule.printToFiles
110
136
 
111
137
  printModule.printToFiles = async function (...args) {
138
+ const appOptions = args[1]
139
+ const parsedLogics = args[2]
140
+
112
141
  active += 1
113
142
  started += 1
114
143
  maxActive = Math.max(maxActive, active)
115
144
  clearTimeout(settleTimer)
116
145
 
146
+ if (editTriggered) {
147
+ parsedLogicCountsAfterEdit.push(parsedLogics.length)
148
+ sourceFilePathsAfterEdit.push(appOptions.sourceFilePath || null)
149
+ incrementalAfterEdit = incrementalAfterEdit || (parsedLogics.length === 1 && !!appOptions.sourceFilePath)
150
+ }
151
+
117
152
  await new Promise((resolve) => setTimeout(resolve, 25))
118
153
 
119
154
  try {
@@ -1,13 +1,15 @@
1
1
  const fs = require('fs')
2
2
  const Module = require('module')
3
+ const os = require('os')
3
4
  const path = require('path')
4
5
 
5
6
  require('ts-node/register/transpile-only')
6
7
 
7
8
  const repoRoot = path.resolve(__dirname, '..', '..')
8
- const projectDir = fs.mkdtempSync(path.join(repoRoot, 'tmp-write-smoke-'))
9
+ const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kea-typegen-write-smoke-'))
9
10
  const tsconfigPath = path.join(projectDir, 'tsconfig.json')
10
11
  const logicFilePath = path.join(projectDir, 'src', 'logic.ts')
12
+ const keaDtsPath = path.join(projectDir, 'node_modules', 'kea', 'index.d.ts')
11
13
 
12
14
  const noop = () => {}
13
15
 
@@ -83,6 +85,7 @@ process.on('unhandledRejection', (error) => {
83
85
  })
84
86
 
85
87
  fs.mkdirSync(path.dirname(logicFilePath), { recursive: true })
88
+ fs.mkdirSync(path.dirname(keaDtsPath), { recursive: true })
86
89
  fs.writeFileSync(
87
90
  tsconfigPath,
88
91
  JSON.stringify(
@@ -102,6 +105,8 @@ fs.writeFileSync(
102
105
  ),
103
106
  )
104
107
 
108
+ fs.writeFileSync(keaDtsPath, 'export function kea<T = any>(input: any): T\n')
109
+
105
110
  fs.writeFileSync(
106
111
  logicFilePath,
107
112
  [