@sprucelabs/spruce-cli 24.0.0 → 24.1.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 (105) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/__tests__/behavioral/conversations/CreatingAConversationTopic.test.js +2 -2
  3. package/build/__tests__/behavioral/conversations/CreatingAConversationTopic.test.js.map +1 -1
  4. package/build/__tests__/behavioral/conversations/RegisteringConversationsOnBoot.test.js +1 -1
  5. package/build/__tests__/behavioral/conversations/RegisteringConversationsOnBoot.test.js.map +1 -1
  6. package/build/__tests__/behavioral/conversations/TestingAConversation.test.js +5 -5
  7. package/build/__tests__/behavioral/conversations/TestingAConversation.test.js.map +1 -1
  8. package/build/__tests__/behavioral/organization/CreatingAnOrg.test.js.map +1 -1
  9. package/build/__tests__/behavioral/tests/migrationToInstance/AbstractInstanceTest.d.ts +11 -0
  10. package/build/__tests__/behavioral/tests/migrationToInstance/AbstractInstanceTest.js +49 -0
  11. package/build/__tests__/behavioral/tests/migrationToInstance/AbstractInstanceTest.js.map +1 -0
  12. package/build/__tests__/behavioral/tests/migrationToInstance/InstanceBasedTesting.test.d.ts +5 -0
  13. package/build/__tests__/behavioral/tests/migrationToInstance/InstanceBasedTesting.test.js +34 -0
  14. package/build/__tests__/behavioral/tests/migrationToInstance/InstanceBasedTesting.test.js.map +1 -0
  15. package/build/__tests__/behavioral/tests/migrationToInstance/MigratingTests.test.d.ts +12 -0
  16. package/build/__tests__/behavioral/tests/migrationToInstance/MigratingTests.test.js +88 -0
  17. package/build/__tests__/behavioral/tests/migrationToInstance/MigratingTests.test.js.map +1 -0
  18. package/build/__tests__/behavioral/tests/migrationToInstance/StaticTestFinder.test.d.ts +13 -0
  19. package/build/__tests__/behavioral/tests/migrationToInstance/StaticTestFinder.test.js +109 -0
  20. package/build/__tests__/behavioral/tests/migrationToInstance/StaticTestFinder.test.js.map +1 -0
  21. package/build/__tests__/behavioral/tests/migrationToInstance/StaticTestsWhenAlreadyExists.test.d.ts +4 -0
  22. package/build/__tests__/behavioral/tests/migrationToInstance/StaticTestsWhenAlreadyExists.test.js +29 -0
  23. package/build/__tests__/behavioral/tests/migrationToInstance/StaticTestsWhenAlreadyExists.test.js.map +1 -0
  24. package/build/__tests__/behavioral/tests/migrationToInstance/StaticToInstanceMigrator.test.d.ts +26 -0
  25. package/build/__tests__/behavioral/tests/migrationToInstance/StaticToInstanceMigrator.test.js +259 -0
  26. package/build/__tests__/behavioral/tests/migrationToInstance/StaticToInstanceMigrator.test.js.map +1 -0
  27. package/build/__tests__/behavioral/tests/migrationToInstance/StaticToInstanceTestFileMigrator.test.d.ts +26 -0
  28. package/build/__tests__/behavioral/tests/migrationToInstance/StaticToInstanceTestFileMigrator.test.js +202 -0
  29. package/build/__tests__/behavioral/tests/migrationToInstance/StaticToInstanceTestFileMigrator.test.js.map +1 -0
  30. package/build/__tests__/behavioral/tests/migrationToInstance/support/AbstractInstanceTest.txt +24 -0
  31. package/build/__tests__/behavioral/tests/migrationToInstance/support/AbstractStaticTest.txt +24 -0
  32. package/build/__tests__/behavioral/tests/migrationToInstance/support/InstanceTest.txt +36 -0
  33. package/build/__tests__/behavioral/tests/migrationToInstance/support/InstanceTest2.txt +63 -0
  34. package/build/__tests__/behavioral/tests/migrationToInstance/support/InstanceTest3.txt +222 -0
  35. package/build/__tests__/behavioral/tests/migrationToInstance/support/StaticTest.txt +34 -0
  36. package/build/__tests__/behavioral/tests/migrationToInstance/support/StaticTest2.txt +62 -0
  37. package/build/__tests__/behavioral/tests/migrationToInstance/support/StaticTest3.txt +221 -0
  38. package/build/__tests__/behavioral/tests/migrationToInstance/testFileContentsGenerators.d.ts +2 -0
  39. package/build/__tests__/behavioral/tests/migrationToInstance/testFileContentsGenerators.js +51 -0
  40. package/build/__tests__/behavioral/tests/migrationToInstance/testFileContentsGenerators.js.map +1 -0
  41. package/build/__tests__/behavioral/versions/ResolvingVersions.test.js +0 -3
  42. package/build/__tests__/behavioral/versions/ResolvingVersions.test.js.map +1 -1
  43. package/build/__tests__/implementation/OrganizationStore.test.js +2 -2
  44. package/build/__tests__/implementation/OrganizationStore.test.js.map +1 -1
  45. package/build/__tests__/testDirsAndFiles/static_test_migration_1/src/__tests__/AnotherStaticTest.test.ts +33 -0
  46. package/build/__tests__/testDirsAndFiles/static_test_migration_1/src/__tests__/ShouldBeIgnored.ts +0 -0
  47. package/build/__tests__/testDirsAndFiles/static_test_migration_1/src/__tests__/StaticTest1.test.ts +33 -0
  48. package/build/__tests__/testDirsAndFiles/static_test_migration_2/src/DoNotInclude.ts +0 -0
  49. package/build/__tests__/testDirsAndFiles/static_test_migration_2/src/__tests__/AStaticTest.test.ts +33 -0
  50. package/build/__tests__/testDirsAndFiles/static_test_migration_2/src/__tests__/AnotherStaticTest1.test.ts +33 -0
  51. package/build/__tests__/testDirsAndFiles/static_test_migration_3/NotAFileThatShouldBeMigrated.ts +0 -0
  52. package/build/__tests__/testDirsAndFiles/static_test_migration_3/src/__tests__/AStaticTest2.test.ts +17 -0
  53. package/build/__tests__/testDirsAndFiles/static_test_migration_3/src/__tests__/AbstractWhateverTest.ts +3 -0
  54. package/build/features/test/actions/CreateAction.d.ts +1 -0
  55. package/build/features/test/actions/CreateAction.js +13 -0
  56. package/build/features/test/actions/CreateAction.js.map +1 -1
  57. package/build/features/test/actions/MigrateAction.d.ts +17 -0
  58. package/build/features/test/actions/MigrateAction.js +39 -0
  59. package/build/features/test/actions/MigrateAction.js.map +1 -0
  60. package/build/tests/staticToInstanceMigration/StaticTestFinder.d.ts +8 -0
  61. package/build/tests/staticToInstanceMigration/StaticTestFinder.js +23 -0
  62. package/build/tests/staticToInstanceMigration/StaticTestFinder.js.map +1 -0
  63. package/build/tests/staticToInstanceMigration/StaticToInstanceMigrator.d.ts +58 -0
  64. package/build/tests/staticToInstanceMigration/StaticToInstanceMigrator.js +48 -0
  65. package/build/tests/staticToInstanceMigration/StaticToInstanceMigrator.js.map +1 -0
  66. package/build/tests/staticToInstanceMigration/StaticToInstanceTestFileMigrator.d.ts +12 -0
  67. package/build/tests/staticToInstanceMigration/StaticToInstanceTestFileMigrator.js +138 -0
  68. package/build/tests/staticToInstanceMigration/StaticToInstanceTestFileMigrator.js.map +1 -0
  69. package/package.json +27 -27
  70. package/src/__tests__/behavioral/conversations/CreatingAConversationTopic.test.ts +2 -2
  71. package/src/__tests__/behavioral/conversations/RegisteringConversationsOnBoot.test.ts +1 -1
  72. package/src/__tests__/behavioral/conversations/TestingAConversation.test.ts +5 -5
  73. package/src/__tests__/behavioral/organization/CreatingAnOrg.test.ts +0 -1
  74. package/src/__tests__/behavioral/tests/migrationToInstance/AbstractInstanceTest.ts +77 -0
  75. package/src/__tests__/behavioral/tests/migrationToInstance/InstanceBasedTesting.test.ts +19 -0
  76. package/src/__tests__/behavioral/tests/migrationToInstance/MigratingTests.test.ts +99 -0
  77. package/src/__tests__/behavioral/tests/migrationToInstance/StaticTestFinder.test.ts +72 -0
  78. package/src/__tests__/behavioral/tests/migrationToInstance/StaticTestsWhenAlreadyExists.test.ts +31 -0
  79. package/src/__tests__/behavioral/tests/migrationToInstance/StaticToInstanceMigrator.test.ts +252 -0
  80. package/src/__tests__/behavioral/tests/migrationToInstance/StaticToInstanceTestFileMigrator.test.ts +193 -0
  81. package/src/__tests__/behavioral/tests/migrationToInstance/support/AbstractInstanceTest.txt +24 -0
  82. package/src/__tests__/behavioral/tests/migrationToInstance/support/AbstractStaticTest.txt +24 -0
  83. package/src/__tests__/behavioral/tests/migrationToInstance/support/InstanceTest.txt +36 -0
  84. package/src/__tests__/behavioral/tests/migrationToInstance/support/InstanceTest2.txt +63 -0
  85. package/src/__tests__/behavioral/tests/migrationToInstance/support/InstanceTest3.txt +222 -0
  86. package/src/__tests__/behavioral/tests/migrationToInstance/support/StaticTest.txt +34 -0
  87. package/src/__tests__/behavioral/tests/migrationToInstance/support/StaticTest2.txt +62 -0
  88. package/src/__tests__/behavioral/tests/migrationToInstance/support/StaticTest3.txt +221 -0
  89. package/src/__tests__/behavioral/tests/migrationToInstance/testFileContentsGenerators.ts +50 -0
  90. package/src/__tests__/behavioral/versions/ResolvingVersions.test.ts +1 -3
  91. package/src/__tests__/implementation/OrganizationStore.test.ts +6 -2
  92. package/src/__tests__/testDirsAndFiles/static_test_migration_1/src/__tests__/AnotherStaticTest.test.ts +33 -0
  93. package/src/__tests__/testDirsAndFiles/static_test_migration_1/src/__tests__/ShouldBeIgnored.ts +0 -0
  94. package/src/__tests__/testDirsAndFiles/static_test_migration_1/src/__tests__/StaticTest1.test.ts +33 -0
  95. package/src/__tests__/testDirsAndFiles/static_test_migration_2/src/DoNotInclude.ts +0 -0
  96. package/src/__tests__/testDirsAndFiles/static_test_migration_2/src/__tests__/AStaticTest.test.ts +33 -0
  97. package/src/__tests__/testDirsAndFiles/static_test_migration_2/src/__tests__/AnotherStaticTest1.test.ts +33 -0
  98. package/src/__tests__/testDirsAndFiles/static_test_migration_3/NotAFileThatShouldBeMigrated.ts +0 -0
  99. package/src/__tests__/testDirsAndFiles/static_test_migration_3/src/__tests__/AStaticTest2.test.ts +17 -0
  100. package/src/__tests__/testDirsAndFiles/static_test_migration_3/src/__tests__/AbstractWhateverTest.ts +3 -0
  101. package/src/features/test/actions/CreateAction.ts +17 -0
  102. package/src/features/test/actions/MigrateAction.ts +41 -0
  103. package/src/tests/staticToInstanceMigration/StaticTestFinder.ts +25 -0
  104. package/src/tests/staticToInstanceMigration/StaticToInstanceMigrator.ts +73 -0
  105. package/src/tests/staticToInstanceMigration/StaticToInstanceTestFileMigrator.ts +197 -0
@@ -0,0 +1,33 @@
1
+ import { fake } from '@sprucelabs/spruce-test-fixtures'
2
+ import AbstractSpruceTest, {
3
+ test,
4
+ assert,
5
+ errorAssert,
6
+ } from '@sprucelabs/test-utils'
7
+
8
+ @fake.login()
9
+ export default class StaticTestFinderTest extends AbstractSpruceTest {
10
+ @test()
11
+ protected static async throwsWithMissing() {
12
+ const finder = StaticTestFinder.Finder()
13
+ const err = await assert.doesThrowAsync(() => finder.find())
14
+ errorAssert.assertError(err, 'MISSING_PARAMETERS', {
15
+ parameters: ['lookupDir'],
16
+ })
17
+ }
18
+
19
+ @test()
20
+ protected static async yourNextTest() {
21
+ assert.isTrue(false)
22
+ }
23
+ }
24
+
25
+ class StaticTestFinder {
26
+ public static Finder() {
27
+ return new this()
28
+ }
29
+
30
+ public async find() {
31
+ // assertOptions({}, ['lookupDir'])
32
+ }
33
+ }
@@ -0,0 +1,33 @@
1
+ import { fake } from '@sprucelabs/spruce-test-fixtures'
2
+ import AbstractSpruceTest, {
3
+ test,
4
+ assert,
5
+ errorAssert,
6
+ } from '@sprucelabs/test-utils'
7
+
8
+ @fake.login()
9
+ export default class StaticTestFinderTest extends AbstractSpruceTest {
10
+ @test()
11
+ protected static async throwsWithMissing() {
12
+ const finder = StaticTestFinder.Finder()
13
+ const err = await assert.doesThrowAsync(() => finder.find())
14
+ errorAssert.assertError(err, 'MISSING_PARAMETERS', {
15
+ parameters: ['lookupDir'],
16
+ })
17
+ }
18
+
19
+ @test()
20
+ protected static async yourNextTest() {
21
+ assert.isTrue(false)
22
+ }
23
+ }
24
+
25
+ class StaticTestFinder {
26
+ public static Finder() {
27
+ return new this()
28
+ }
29
+
30
+ public async find() {
31
+ // assertOptions({}, ['lookupDir'])
32
+ }
33
+ }
@@ -0,0 +1,17 @@
1
+ import { fake } from "@sprucelabs/spruce-test-fixtures";
2
+ import {
3
+ test,
4
+ assert,
5
+ } from "@sprucelabs/test-utils";
6
+ import AbstractWhateverTest from "./AbstractWhateverTest";
7
+
8
+ @fake.login()
9
+ export default class StaticTestFinderTest extends AbstractWhateverTest {
10
+ @test()
11
+ protected static async throwsWithMissing() {}
12
+
13
+ @test()
14
+ protected static async yourNextTest() {
15
+ assert.isTrue(false);
16
+ }
17
+ }
@@ -0,0 +1,3 @@
1
+ import AbstractSpruceTest from "@sprucelabs/test-utils";
2
+
3
+ export default abstract class AbstractWhateverTest extends AbstractSpruceTest {}
@@ -1,4 +1,5 @@
1
1
  import pathUtil from 'path'
2
+ import globby from '@sprucelabs/globby'
2
3
  import { namesUtil } from '@sprucelabs/spruce-skill-utils'
3
4
  import { diskUtil } from '@sprucelabs/spruce-skill-utils'
4
5
  import { SpruceSchemas } from '#spruce/schemas/schemas.types'
@@ -51,6 +52,9 @@ export default class CreateAction extends AbstractAction<OptionsSchema> {
51
52
  'devDependencies.@sprucelabs/spruce-test-fixtures'
52
53
  )
53
54
 
55
+ let doesStaticTestExist =
56
+ await this.doesStaticTestAlreadyExist(resolvedDestination)
57
+
54
58
  const results = await writer.writeTest(resolvedDestination, {
55
59
  ...normalizedOptions,
56
60
  type,
@@ -58,6 +62,7 @@ export default class CreateAction extends AbstractAction<OptionsSchema> {
58
62
  parentTestClass,
59
63
  isTestFixturesInstalled,
60
64
  namePascal: namePascal ?? namesUtil.toPascal(nameCamel),
65
+ testType: doesStaticTestExist ? 'static' : 'instance',
61
66
  })
62
67
 
63
68
  return {
@@ -66,6 +71,18 @@ export default class CreateAction extends AbstractAction<OptionsSchema> {
66
71
  }
67
72
  }
68
73
 
74
+ private async doesStaticTestAlreadyExist(resolvedDestination: string) {
75
+ const matches = await globby(resolvedDestination + `/**/*.test.ts`)
76
+ let doesStaticTestExist = false
77
+
78
+ const match = matches[0]
79
+ if (match) {
80
+ const contents = diskUtil.readFile(matches[0])
81
+ doesStaticTestExist = contents.includes('static')
82
+ }
83
+ return doesStaticTestExist
84
+ }
85
+
69
86
  private async promptForSubDir(resolvedDestination: string) {
70
87
  const match = await this.ui.prompt({
71
88
  type: 'directory',
@@ -0,0 +1,41 @@
1
+ import { buildSchema } from '@sprucelabs/schema'
2
+ import { diskUtil } from '@sprucelabs/spruce-skill-utils'
3
+ import StaticTestFinderImpl from '../../../tests/staticToInstanceMigration/StaticTestFinder'
4
+ import StaticToInstanceMigratorImpl from '../../../tests/staticToInstanceMigration/StaticToInstanceMigrator'
5
+ import StaticToInstanceTestFileMigratorImpl from '../../../tests/staticToInstanceMigration/StaticToInstanceTestFileMigrator'
6
+ import AbstractAction from '../../AbstractAction'
7
+ import { FeatureActionResponse } from '../../features.types'
8
+
9
+ export default class MigrationAction extends AbstractAction<OptionsSchema> {
10
+ public optionsSchema = optionsSchema
11
+ public commandAliases = ['migrate.tests']
12
+ public invocationMessage = 'Migrating tests from static to instance... 🌲'
13
+
14
+ public async execute(): Promise<FeatureActionResponse> {
15
+ const testFinder = StaticTestFinderImpl.Finder()
16
+ const testFileMigrator = StaticToInstanceTestFileMigratorImpl.Migrator()
17
+ const migrator = StaticToInstanceMigratorImpl.Migrator({
18
+ testFinder,
19
+ testFileMigrator,
20
+ })
21
+
22
+ const path = diskUtil.resolvePath(this.cwd, 'src', '__tests__')
23
+ const { totalTestsSkipped, totalTestsUpdated } =
24
+ await migrator.run(path)
25
+
26
+ return {
27
+ headline: 'Migrated tests from static to instance based.',
28
+ summaryLines: [
29
+ `${totalTestsUpdated} test${totalTestsUpdated === 1 ? '' : 's'} updated`,
30
+ `${totalTestsSkipped} test${totalTestsSkipped === 1 ? '' : 's'} skipped`,
31
+ ],
32
+ }
33
+ }
34
+ }
35
+
36
+ const optionsSchema = buildSchema({
37
+ id: 'migrateTests',
38
+ fields: {},
39
+ })
40
+
41
+ type OptionsSchema = typeof optionsSchema
@@ -0,0 +1,25 @@
1
+ import globby from '@sprucelabs/globby'
2
+ import { assertOptions } from '@sprucelabs/schema'
3
+
4
+ export default class StaticTestFinderImpl implements StaticTestFinder {
5
+ public static Class?: new () => StaticTestFinder
6
+
7
+ public static Finder(): StaticTestFinder {
8
+ return new (this.Class ?? this)()
9
+ }
10
+
11
+ public async find(lookupDir: string) {
12
+ assertOptions({ lookupDir }, ['lookupDir'])
13
+
14
+ const matches = await globby([
15
+ `${lookupDir}/**/Abstract*Test.ts`,
16
+ `${lookupDir}/**/*.test.ts`,
17
+ ])
18
+
19
+ return matches
20
+ }
21
+ }
22
+
23
+ export interface StaticTestFinder {
24
+ find(lookupDir: string): Promise<string[]>
25
+ }
@@ -0,0 +1,73 @@
1
+ import { assertOptions } from '@sprucelabs/schema'
2
+ import { diskUtil } from '@sprucelabs/spruce-skill-utils'
3
+ import StaticTestFinder from './StaticTestFinder'
4
+ import { StaticToInstanceTestFileMigrator } from './StaticToInstanceTestFileMigrator'
5
+
6
+ export default class StaticToInstanceMigratorImpl
7
+ implements StaticToInstanceMigrator
8
+ {
9
+ public static diskUtil = diskUtil
10
+ public static Class?: new (
11
+ options: StaticToInstanceMigratorOptions
12
+ ) => StaticToInstanceMigrator
13
+
14
+ private testFinder: StaticTestFinder
15
+ private testFileMigrator: StaticToInstanceTestFileMigrator
16
+
17
+ protected constructor(options: StaticToInstanceMigratorOptions) {
18
+ const { testFinder, testFileMigrator } = options
19
+ this.testFinder = testFinder
20
+ this.testFileMigrator = testFileMigrator
21
+ }
22
+
23
+ public static Migrator(options: StaticToInstanceMigratorOptions) {
24
+ assertOptions(options, ['testFinder', 'testFileMigrator'])
25
+ return new (this.Class ?? this)(options)
26
+ }
27
+
28
+ public async run(lookupDir: string) {
29
+ assertOptions({ lookupDir }, ['lookupDir'])
30
+ const matches = await this.testFinder.find(lookupDir)
31
+
32
+ let totalTestsUpdated = 0
33
+ let totalTestsSkipped = 0
34
+
35
+ for (const match of matches) {
36
+ const contents = this.readFile(match)
37
+ const updated = this.testFileMigrator.migrate(contents)
38
+ if (contents === updated) {
39
+ totalTestsSkipped++
40
+ } else {
41
+ totalTestsUpdated++
42
+ this.writeFile(match, updated)
43
+ }
44
+ }
45
+
46
+ return {
47
+ totalTestsUpdated,
48
+ totalTestsSkipped,
49
+ }
50
+ }
51
+
52
+ private readFile(match: string) {
53
+ return StaticToInstanceMigratorImpl.diskUtil.readFile(match)
54
+ }
55
+
56
+ private writeFile(match: string, updated: string) {
57
+ StaticToInstanceMigratorImpl.diskUtil.writeFile(match, updated)
58
+ }
59
+ }
60
+
61
+ export interface StaticToInstanceMigratorOptions {
62
+ testFinder: StaticTestFinder
63
+ testFileMigrator: StaticToInstanceTestFileMigrator
64
+ }
65
+
66
+ export interface StaticToInstanceMigratorResults {
67
+ totalTestsUpdated: number
68
+ totalTestsSkipped: number
69
+ }
70
+
71
+ export interface StaticToInstanceMigrator {
72
+ run(lookupDir: string): Promise<StaticToInstanceMigratorResults>
73
+ }
@@ -0,0 +1,197 @@
1
+ import { assertOptions } from '@sprucelabs/schema'
2
+
3
+ export default class StaticToInstanceTestFileMigratorImpl
4
+ implements StaticToInstanceTestFileMigrator
5
+ {
6
+ public static Class?: new () => StaticToInstanceTestFileMigrator
7
+
8
+ public static Migrator() {
9
+ return new (this.Class ?? this)()
10
+ }
11
+
12
+ public migrate(contents: string) {
13
+ assertOptions({ contents }, ['contents'])
14
+
15
+ // 1a. Remove `static ` only when it appears immediately before a method
16
+ // that has the `@test()` decorator
17
+ // 1b. If the contents include `export default abstract class`,
18
+ // remove `static` from all methods
19
+ const includesAbstractExport = contents.includes(
20
+ 'export default abstract class'
21
+ )
22
+ let cleanedUp = includesAbstractExport
23
+ ? contents.replaceAll(' static ', ' ')
24
+ : contents.replace(
25
+ // Matches @test() or @seed(...) followed (on next line) by optional visibility and `static`.
26
+ /(@(?:test\(\)|seed\([^)]*\))\s*\n\s*(?:public|protected)\s+)static\s+/g,
27
+ '$1'
28
+ )
29
+
30
+ // 2. Add `@suite()` above `export default class` if it's not already present
31
+ if (!cleanedUp.includes('@suite')) {
32
+ cleanedUp = cleanedUp.replace(
33
+ /export default class/,
34
+ '@suite()\nexport default class'
35
+ )
36
+ }
37
+
38
+ // 3. Ensure `suite` is imported from `@sprucelabs/test-utils`
39
+ if (!this.hasSuiteImport(cleanedUp)) {
40
+ if (cleanedUp.includes('{ test')) {
41
+ cleanedUp = cleanedUp.replace('{ test', '{ test, suite')
42
+ } else if (cleanedUp.includes('test }')) {
43
+ cleanedUp = cleanedUp.replace('test }', 'test, suite }')
44
+ } else {
45
+ cleanedUp = cleanedUp.replace('test,', 'test,\n suite,')
46
+ }
47
+ }
48
+
49
+ const thisCallNames = this.findThisCalls(cleanedUp)
50
+ for (const name of thisCallNames) {
51
+ cleanedUp = this.removeStaticFromDeclaration(cleanedUp, name)
52
+ }
53
+
54
+ // 4. lifecicle methods
55
+ const methods = ['beforeEach', 'afterEach']
56
+ for (const method of methods) {
57
+ cleanedUp = cleanedUp.replace(
58
+ `protected static async ${method}()`,
59
+ `protected async ${method}()`
60
+ )
61
+ }
62
+
63
+ cleanedUp = this.fixNonNullAssertions(cleanedUp)
64
+
65
+ return cleanedUp
66
+ }
67
+
68
+ private hasSuiteImport(text: string): boolean {
69
+ const pattern = new RegExp(
70
+ `import\\s+(?:[\\s\\S]*?\\bsuite\\b[\\s\\S]*?)\\s+from\\s+['"]@sprucelabs/test-utils['"]`
71
+ )
72
+ return pattern.test(text)
73
+ }
74
+
75
+ private findThisCalls(contents: string): string[] {
76
+ // Matches `this.myProp` if followed by space, punctuation, parentheses, or end of string
77
+ const thisPropertyRegex = /this\.(\w+)(?=[\s.(),;]|$)/g
78
+ const names: string[] = []
79
+ let match: RegExpExecArray | null
80
+
81
+ while ((match = thisPropertyRegex.exec(contents)) !== null) {
82
+ const propName = match[1]
83
+ if (!names.includes(propName)) {
84
+ names.push(propName)
85
+ }
86
+ }
87
+
88
+ return names
89
+ }
90
+
91
+ private removeStaticFromDeclaration(
92
+ contents: string,
93
+ name: string
94
+ ): string {
95
+ /**
96
+ * 1) Remove `static` for methods/getters/setters, e.g.:
97
+ * private static async doSomething() => private async doSomething()
98
+ * private static get value() => private get value()
99
+ * private static set value(v) => private set value(v)
100
+ */
101
+ const methodPattern = new RegExp(
102
+ `((?:public|protected|private)?\\s+)?` + // group 1
103
+ `static\\s+` + // literal "static "
104
+ `(?:(async)\\s+)?` + // group 2: "async"?
105
+ `(?:(get|set)\\s+)?` + // group 3: "get" or "set"?
106
+ `(${name})\\s*\\(`, // group 4: the identifier + '('
107
+ 'g'
108
+ )
109
+ let updated = contents.replace(
110
+ methodPattern,
111
+ (match, g1, g2, g3, g4) => {
112
+ const asyncPart = g2 ? g2 + ' ' : ''
113
+ const accessorPart = g3 ? g3 + ' ' : ''
114
+ // Rebuild the declaration without "static"
115
+ return `${g1 ?? ''}${asyncPart}${accessorPart}${g4}(`
116
+ }
117
+ )
118
+
119
+ /**
120
+ * 2) Remove `static` from property declarations and add a non-null assertion.
121
+ * e.g.
122
+ * private static myProp: Type => private myProp!: Type
123
+ */
124
+ const propertyPattern = new RegExp(
125
+ `((?:public|protected|private)?\\s+)?` + // group 1: optional visibility
126
+ `static\\s+` + // literal "static "
127
+ `(${name})` + // group 2: the property name
128
+ `(?=[\\s=:\\[;]|$)`, // lookahead: space, '=', ':', '[', ';', or end-of-string
129
+ 'g'
130
+ )
131
+ updated = updated.replace(propertyPattern, (match, g1, g2) => {
132
+ // g1 = "private " / "public " / "protected " or empty
133
+ // g2 = property name
134
+ return `${g1 ?? ''}${g2}!`
135
+ })
136
+
137
+ return updated
138
+ }
139
+
140
+ private fixNonNullAssertions(contents: string): string {
141
+ const lines = contents.split('\n')
142
+
143
+ const propertyRegex =
144
+ /^(\s*)(public|protected|private)(\s+readonly)?\s+(\w+)\s*(!)?\s*:\s*([^=;]+)(=.*)?;?$/
145
+
146
+ const updatedLines = lines.map((originalLine) => {
147
+ // Skip lines containing "static"
148
+ if (originalLine.includes('static')) {
149
+ return originalLine
150
+ }
151
+
152
+ const match = originalLine.match(propertyRegex)
153
+ if (!match) {
154
+ return originalLine
155
+ }
156
+
157
+ let [
158
+ ,
159
+ leadingWhitespace,
160
+ visibility,
161
+ readonlyPart = '',
162
+ propName,
163
+ exclamation,
164
+ typeDecl,
165
+ assignment,
166
+ ] = match
167
+
168
+ // Trim trailing whitespace from the type
169
+ typeDecl = typeDecl.trim()
170
+
171
+ if (assignment) {
172
+ // Remove the bang if there's an assignment
173
+ exclamation = ''
174
+
175
+ // Remove trailing semicolon
176
+ assignment = assignment.replace(/;$/, '')
177
+
178
+ // Ensure we always have " = " at the start
179
+ // E.g. "=something" => " = something"
180
+ assignment = assignment.replace(/^=\s*/, ' = ')
181
+ } else {
182
+ // No assignment? Add bang
183
+ exclamation = '!'
184
+ }
185
+
186
+ // Rebuild line, preserving leading indentation
187
+ const rebuilt = `${leadingWhitespace}${visibility}${readonlyPart} ${propName}${exclamation}: ${typeDecl}${assignment ?? ''}`
188
+ return rebuilt
189
+ })
190
+
191
+ return updatedLines.join('\n')
192
+ }
193
+ }
194
+
195
+ export interface StaticToInstanceTestFileMigrator {
196
+ migrate(contents: string): string
197
+ }