@quenty/nevermore-cli 4.18.0 → 4.19.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 (68) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/commands/tools-command/ci-post-test-results.d.ts.map +1 -1
  3. package/dist/commands/tools-command/ci-post-test-results.js +18 -0
  4. package/dist/commands/tools-command/ci-post-test-results.js.map +1 -1
  5. package/dist/commands/tools-command/strip-sourcemap-jest-command.d.ts.map +1 -1
  6. package/dist/commands/tools-command/strip-sourcemap-jest-command.js.map +1 -1
  7. package/dist/utils/linting/parsers/moonwave-parser.d.ts.map +1 -1
  8. package/dist/utils/linting/parsers/moonwave-parser.js +2 -6
  9. package/dist/utils/linting/parsers/moonwave-parser.js.map +1 -1
  10. package/dist/utils/sourcemap/index.d.ts +4 -0
  11. package/dist/utils/sourcemap/index.d.ts.map +1 -0
  12. package/dist/utils/sourcemap/index.js +3 -0
  13. package/dist/utils/sourcemap/index.js.map +1 -0
  14. package/dist/utils/sourcemap/sourcemap-loader.d.ts +10 -0
  15. package/dist/utils/sourcemap/sourcemap-loader.d.ts.map +1 -0
  16. package/dist/utils/sourcemap/sourcemap-loader.js +29 -0
  17. package/dist/utils/sourcemap/sourcemap-loader.js.map +1 -0
  18. package/dist/utils/sourcemap/sourcemap-resolver.d.ts +30 -0
  19. package/dist/utils/sourcemap/sourcemap-resolver.d.ts.map +1 -0
  20. package/dist/utils/sourcemap/sourcemap-resolver.js +58 -0
  21. package/dist/utils/sourcemap/sourcemap-resolver.js.map +1 -0
  22. package/dist/utils/sourcemap/sourcemap-resolver.test.d.ts +2 -0
  23. package/dist/utils/sourcemap/sourcemap-resolver.test.d.ts.map +1 -0
  24. package/dist/utils/sourcemap/sourcemap-resolver.test.js +106 -0
  25. package/dist/utils/sourcemap/sourcemap-resolver.test.js.map +1 -0
  26. package/dist/utils/sourcemap/sourcemap-types.d.ts +13 -0
  27. package/dist/utils/sourcemap/sourcemap-types.d.ts.map +1 -0
  28. package/dist/utils/sourcemap/sourcemap-types.js +2 -0
  29. package/dist/utils/sourcemap/sourcemap-types.js.map +1 -0
  30. package/dist/utils/testing/parsers/index.d.ts +3 -0
  31. package/dist/utils/testing/parsers/index.d.ts.map +1 -0
  32. package/dist/utils/testing/parsers/index.js +3 -0
  33. package/dist/utils/testing/parsers/index.js.map +1 -0
  34. package/dist/utils/testing/parsers/jest-lua-parser.d.ts +41 -0
  35. package/dist/utils/testing/parsers/jest-lua-parser.d.ts.map +1 -0
  36. package/dist/utils/testing/parsers/jest-lua-parser.js +222 -0
  37. package/dist/utils/testing/parsers/jest-lua-parser.js.map +1 -0
  38. package/dist/utils/testing/parsers/jest-lua-parser.test.d.ts +2 -0
  39. package/dist/utils/testing/parsers/jest-lua-parser.test.d.ts.map +1 -0
  40. package/dist/utils/testing/parsers/jest-lua-parser.test.js +297 -0
  41. package/dist/utils/testing/parsers/jest-lua-parser.test.js.map +1 -0
  42. package/dist/utils/testing/parsers/roblox-path-resolver.d.ts +24 -0
  43. package/dist/utils/testing/parsers/roblox-path-resolver.d.ts.map +1 -0
  44. package/dist/utils/testing/parsers/roblox-path-resolver.js +71 -0
  45. package/dist/utils/testing/parsers/roblox-path-resolver.js.map +1 -0
  46. package/dist/utils/testing/parsers/roblox-path-resolver.test.d.ts +2 -0
  47. package/dist/utils/testing/parsers/roblox-path-resolver.test.d.ts.map +1 -0
  48. package/dist/utils/testing/parsers/roblox-path-resolver.test.js +76 -0
  49. package/dist/utils/testing/parsers/roblox-path-resolver.test.js.map +1 -0
  50. package/dist/utils/testing/test-log-parser.d.ts.map +1 -1
  51. package/dist/utils/testing/test-log-parser.js +2 -1
  52. package/dist/utils/testing/test-log-parser.js.map +1 -1
  53. package/package.json +6 -6
  54. package/src/commands/tools-command/ci-post-test-results.ts +26 -0
  55. package/src/commands/tools-command/strip-sourcemap-jest-command.ts +1 -7
  56. package/src/utils/linting/parsers/moonwave-parser.ts +2 -7
  57. package/src/utils/sourcemap/index.ts +3 -0
  58. package/src/utils/sourcemap/sourcemap-loader.ts +33 -0
  59. package/src/utils/sourcemap/sourcemap-resolver.test.ts +150 -0
  60. package/src/utils/sourcemap/sourcemap-resolver.ts +75 -0
  61. package/src/utils/sourcemap/sourcemap-types.ts +12 -0
  62. package/src/utils/testing/parsers/index.ts +2 -0
  63. package/src/utils/testing/parsers/jest-lua-parser.test.ts +350 -0
  64. package/src/utils/testing/parsers/jest-lua-parser.ts +279 -0
  65. package/src/utils/testing/parsers/roblox-path-resolver.test.ts +129 -0
  66. package/src/utils/testing/parsers/roblox-path-resolver.ts +85 -0
  67. package/src/utils/testing/test-log-parser.ts +3 -1
  68. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roblox-path-resolver.test.d.ts","sourceRoot":"","sources":["../../../../src/utils/testing/parsers/roblox-path-resolver.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolveRobloxTestPath } from './roblox-path-resolver.js';
3
+ import { SourcemapResolver } from '../../sourcemap/index.js';
4
+ describe('resolveRobloxTestPath', () => {
5
+ it('resolves a standard ServerScriptService path', () => {
6
+ expect(resolveRobloxTestPath('ServerScriptService.observablecollection.Shared.ObservableList.spec')).toBe('src/observablecollection/src/Shared/ObservableList.spec.lua');
7
+ });
8
+ it('resolves a nested subdirectory path', () => {
9
+ expect(resolveRobloxTestPath('ServerScriptService.maid.Shared.Maid.spec')).toBe('src/maid/src/Shared/Maid.spec.lua');
10
+ });
11
+ it('resolves a deeply nested path', () => {
12
+ expect(resolveRobloxTestPath('ServerScriptService.blend.Client.Blend.Spring.SpringObject.spec')).toBe('src/blend/src/Client/Blend/Spring/SpringObject.spec.lua');
13
+ });
14
+ it('handles missing ServerScriptService prefix as fallback', () => {
15
+ expect(resolveRobloxTestPath('observablecollection.Shared.ObservableList.spec')).toBe('src/observablecollection/src/Shared/ObservableList.spec.lua');
16
+ });
17
+ it('strips :LINE suffix', () => {
18
+ expect(resolveRobloxTestPath('ServerScriptService.observablecollection.Shared.ObservableList.spec:45')).toBe('src/observablecollection/src/Shared/ObservableList.spec.lua');
19
+ });
20
+ it('handles path without ServerScriptService prefix and with :LINE suffix', () => {
21
+ expect(resolveRobloxTestPath('maid.Shared.Maid.spec:23')).toBe('src/maid/src/Shared/Maid.spec.lua');
22
+ });
23
+ it('handles a bare package slug', () => {
24
+ expect(resolveRobloxTestPath('ServerScriptService.maid')).toBe('src/maid/src');
25
+ });
26
+ it('handles a single-level spec path', () => {
27
+ expect(resolveRobloxTestPath('ServerScriptService.maid.Maid.spec')).toBe('src/maid/src/Maid.spec.lua');
28
+ });
29
+ describe('with sourcemap resolver', () => {
30
+ const repoRoot = '/repo';
31
+ const sourcemap = {
32
+ name: 'Nevermore',
33
+ className: 'DataModel',
34
+ children: [
35
+ {
36
+ name: 'mypkg',
37
+ className: 'Folder',
38
+ filePaths: [`${repoRoot}/src/mypkg/default.project.json`],
39
+ children: [
40
+ {
41
+ name: 'Shared',
42
+ className: 'Folder',
43
+ children: [
44
+ {
45
+ name: 'MyModule',
46
+ className: 'ModuleScript',
47
+ filePaths: [`${repoRoot}/src/mypkg/src/Shared/MyModule.lua`],
48
+ children: [
49
+ {
50
+ name: 'MyModule.spec',
51
+ className: 'ModuleScript',
52
+ filePaths: [
53
+ `${repoRoot}/src/mypkg/src/Shared/MyModule.spec.lua`,
54
+ ],
55
+ },
56
+ ],
57
+ },
58
+ ],
59
+ },
60
+ ],
61
+ },
62
+ ],
63
+ };
64
+ const resolver = SourcemapResolver.fromSourcemap(sourcemap, repoRoot);
65
+ it('uses sourcemap when path is found', () => {
66
+ expect(resolveRobloxTestPath('ServerScriptService.mypkg.Shared.MyModule.MyModule.spec', resolver)).toBe('src/mypkg/src/Shared/MyModule.spec.lua');
67
+ });
68
+ it('falls back to heuristic when sourcemap has no mapping', () => {
69
+ expect(resolveRobloxTestPath('ServerScriptService.unknownpkg.Shared.Foo.spec', resolver)).toBe('src/unknownpkg/src/Shared/Foo.spec.lua');
70
+ });
71
+ it('strips :LINE suffix with sourcemap', () => {
72
+ expect(resolveRobloxTestPath('ServerScriptService.mypkg.Shared.MyModule.MyModule.spec:42', resolver)).toBe('src/mypkg/src/Shared/MyModule.spec.lua');
73
+ });
74
+ });
75
+ });
76
+ //# sourceMappingURL=roblox-path-resolver.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roblox-path-resolver.test.js","sourceRoot":"","sources":["../../../../src/utils/testing/parsers/roblox-path-resolver.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"test-log-parser.d.ts","sourceRoot":"","sources":["../../../src/utils/testing/test-log-parser.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAiB/D"}
1
+ {"version":3,"file":"test-log-parser.d.ts","sourceRoot":"","sources":["../../../src/utils/testing/test-log-parser.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAiB/D"}
@@ -1,9 +1,10 @@
1
+ import { OutputHelper } from '@quenty/cli-output-helpers';
1
2
  /**
2
3
  * Analyze test output for Jest failures and Luau runtime errors.
3
4
  * Shared by both Open Cloud log fetching and local run-in-roblox output.
4
5
  */
5
6
  export function parseTestLogs(rawOutput) {
6
- const cleanLogs = rawOutput.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
7
+ const cleanLogs = OutputHelper.stripAnsi(rawOutput);
7
8
  // Check for Jest-style test failures
8
9
  const failedSuites = cleanLogs.match(/Test Suites:\s*(\d+)\s+failed/);
9
10
  const failedTests = cleanLogs.match(/Tests:\s*(\d+)\s+failed/);
@@ -1 +1 @@
1
- {"version":3,"file":"test-log-parser.js","sourceRoot":"","sources":["../../../src/utils/testing/test-log-parser.ts"],"names":[],"mappings":"AAKA;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,SAAiB;IAC7C,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAC;IAElE,qCAAqC;IACrC,MAAM,YAAY,GAAG,SAAS,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACtE,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/D,MAAM,eAAe,GACnB,CAAC,YAAY,IAAI,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;QACnD,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAEpD,+CAA+C;IAC/C,MAAM,eAAe,GAAG,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAExD,OAAO;QACL,OAAO,EAAE,CAAC,eAAe,IAAI,CAAC,eAAe;QAC7C,IAAI,EAAE,SAAS;KAChB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"test-log-parser.js","sourceRoot":"","sources":["../../../src/utils/testing/test-log-parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAO1D;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,SAAiB;IAC7C,MAAM,SAAS,GAAG,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAEpD,qCAAqC;IACrC,MAAM,YAAY,GAAG,SAAS,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACtE,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/D,MAAM,eAAe,GACnB,CAAC,YAAY,IAAI,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;QACnD,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAEpD,+CAA+C;IAC/C,MAAM,eAAe,GAAG,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAExD,OAAO;QACL,OAAO,EAAE,CAAC,eAAe,IAAI,CAAC,eAAe;QAC7C,IAAI,EAAE,SAAS;KAChB,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/nevermore-cli",
3
- "version": "4.18.0",
3
+ "version": "4.19.0",
4
4
  "description": "CLI interface for Nevermore",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -25,10 +25,10 @@
25
25
  "Quenty"
26
26
  ],
27
27
  "dependencies": {
28
- "@quenty/cli-output-helpers": "1.7.0",
29
- "@quenty/nevermore-cli-helpers": "1.5.0",
30
- "@quenty/nevermore-template-helpers": "1.8.0",
31
- "@quenty/studio-bridge": "0.4.0",
28
+ "@quenty/cli-output-helpers": "1.8.0",
29
+ "@quenty/nevermore-cli-helpers": "1.6.0",
30
+ "@quenty/nevermore-template-helpers": "1.9.0",
31
+ "@quenty/studio-bridge": "0.5.0",
32
32
  "execa": "^9.6.1",
33
33
  "find-git-root": "^1.0.4",
34
34
  "inquirer": "^13.2.0",
@@ -57,5 +57,5 @@
57
57
  "engines": {
58
58
  "node": ">=16"
59
59
  },
60
- "gitHead": "e283e4923e6b052d9b4db9a29f012b91e7a3440c"
60
+ "gitHead": "3e227cd352996f0ee771361ca5140ce7e366de00"
61
61
  }
@@ -1,5 +1,10 @@
1
1
  import { CommandModule } from 'yargs';
2
2
  import { OutputHelper } from '@quenty/cli-output-helpers';
3
+ import {
4
+ type Diagnostic,
5
+ emitAnnotations,
6
+ writeAnnotationSummaryAsync,
7
+ } from '@quenty/cli-output-helpers/reporting';
3
8
  import { NevermoreGlobalArgs } from '../../args/global-args.js';
4
9
  import {
5
10
  type IStateTracker,
@@ -9,6 +14,8 @@ import {
9
14
  LoadedStateTracker,
10
15
  createTestCommentConfig,
11
16
  } from '../../utils/testing/reporting/index.js';
17
+ import { parseJestLuaOutput } from '../../utils/testing/parsers/index.js';
18
+ import { tryLoadSourcemapResolver } from '../../utils/sourcemap/index.js';
12
19
 
13
20
  type ErrorReporter = Reporter & {
14
21
  setError(error: string): void;
@@ -86,6 +93,25 @@ export const ciPostTestResultsCommand: CommandModule<
86
93
  return;
87
94
  }
88
95
 
96
+ // Emit test failure annotations
97
+ const sourcemapResolver = tryLoadSourcemapResolver(process.cwd());
98
+ const allDiagnostics: Diagnostic[] = [];
99
+ for (const failure of state.getFailures()) {
100
+ const diagnostics = parseJestLuaOutput(failure.logs, {
101
+ packageName: failure.packageName,
102
+ sourcemapResolver,
103
+ });
104
+ allDiagnostics.push(...diagnostics);
105
+ }
106
+
107
+ if (allDiagnostics.length > 0) {
108
+ OutputHelper.info(
109
+ `Found ${allDiagnostics.length} test failure(s). Emitting annotations...`
110
+ );
111
+ emitAnnotations(allDiagnostics);
112
+ await writeAnnotationSummaryAsync('Test Failures', allDiagnostics);
113
+ }
114
+
89
115
  const reporters = _createGithubReporters(state, 1);
90
116
  for (const r of reporters) {
91
117
  await r.stopAsync();
@@ -3,13 +3,7 @@ import * as path from 'path';
3
3
  import { CommandModule } from 'yargs';
4
4
  import { OutputHelper } from '@quenty/cli-output-helpers';
5
5
  import { NevermoreGlobalArgs } from '../../args/global-args.js';
6
-
7
- interface SourcemapNode {
8
- name: string;
9
- className?: string;
10
- children?: SourcemapNode[];
11
- filePaths?: string[];
12
- }
6
+ import type { SourcemapNode } from '../../utils/sourcemap/index.js';
13
7
 
14
8
  interface StripSourcemapJestArgs extends NevermoreGlobalArgs {
15
9
  sourcemap?: string;
@@ -21,18 +21,13 @@
21
21
  */
22
22
 
23
23
  import { type Diagnostic, type DiagnosticSeverity } from '@quenty/cli-output-helpers/reporting';
24
+ import { OutputHelper } from '@quenty/cli-output-helpers';
24
25
  import {
25
26
  LERNA_PREFIX_PATTERN,
26
27
  LERNA_PREFIX_PATTERN_NC,
27
28
  resolvePackagePath,
28
29
  } from './lerna-utils.js';
29
30
 
30
- /** Strip all ANSI escape sequences from a string. */
31
- const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
32
- function _stripAnsi(text: string): string {
33
- return text.replace(ANSI_PATTERN, '');
34
- }
35
-
36
31
  /**
37
32
  * Matches the severity header line.
38
33
  * Optional lerna prefix (captures package name).
@@ -64,7 +59,7 @@ export function parseMoonwaveOutput(raw: string): Diagnostic[] {
64
59
  let pending: PendingDiagnostic | undefined;
65
60
 
66
61
  for (const rawLine of lines) {
67
- const line = _stripAnsi(rawLine);
62
+ const line = OutputHelper.stripAnsi(rawLine);
68
63
 
69
64
  // Skip the "aborting due to diagnostic error" summary line
70
65
  if (line.includes('aborting due to')) continue;
@@ -0,0 +1,3 @@
1
+ export type { SourcemapNode } from './sourcemap-types.js';
2
+ export { SourcemapResolver } from './sourcemap-resolver.js';
3
+ export { tryLoadSourcemapResolver } from './sourcemap-loader.js';
@@ -0,0 +1,33 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import type { SourcemapNode } from './sourcemap-types.js';
4
+ import { SourcemapResolver } from './sourcemap-resolver.js';
5
+
6
+ /**
7
+ * Try to load a `SourcemapResolver` from `sourcemap.json` in the given
8
+ * directory.
9
+ *
10
+ * Returns `undefined` if the file doesn't exist or can't be parsed — callers
11
+ * should fall back to heuristic resolution.
12
+ */
13
+ export function tryLoadSourcemapResolver(
14
+ repoRoot: string
15
+ ): SourcemapResolver | undefined {
16
+ const sourcemapPath = path.join(repoRoot, 'sourcemap.json');
17
+
18
+ let content: string;
19
+ try {
20
+ content = fs.readFileSync(sourcemapPath, 'utf-8');
21
+ } catch {
22
+ return undefined;
23
+ }
24
+
25
+ let root: SourcemapNode;
26
+ try {
27
+ root = JSON.parse(content) as SourcemapNode;
28
+ } catch {
29
+ return undefined;
30
+ }
31
+
32
+ return SourcemapResolver.fromSourcemap(root, repoRoot);
33
+ }
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SourcemapResolver } from './sourcemap-resolver.js';
3
+ import type { SourcemapNode } from './sourcemap-types.js';
4
+
5
+ /**
6
+ * Minimal sourcemap tree that mirrors a typical Nevermore project:
7
+ *
8
+ * Nevermore (root)
9
+ * └─ observablecollection
10
+ * └─ Shared
11
+ * └─ ObservableList
12
+ * └─ ObservableList.spec
13
+ */
14
+ function _createTestSourcemap(repoRoot: string): SourcemapNode {
15
+ return {
16
+ name: 'Nevermore',
17
+ className: 'DataModel',
18
+ children: [
19
+ {
20
+ name: 'observablecollection',
21
+ className: 'Folder',
22
+ filePaths: [`${repoRoot}/src/observablecollection/default.project.json`],
23
+ children: [
24
+ {
25
+ name: 'Shared',
26
+ className: 'Folder',
27
+ children: [
28
+ {
29
+ name: 'ObservableList',
30
+ className: 'ModuleScript',
31
+ filePaths: [
32
+ `${repoRoot}/src/observablecollection/src/Shared/ObservableList.lua`,
33
+ ],
34
+ children: [
35
+ {
36
+ name: 'ObservableList.spec',
37
+ className: 'ModuleScript',
38
+ filePaths: [
39
+ `${repoRoot}/src/observablecollection/src/Shared/ObservableList.spec.lua`,
40
+ ],
41
+ },
42
+ ],
43
+ },
44
+ ],
45
+ },
46
+ ],
47
+ },
48
+ {
49
+ name: 'maid',
50
+ className: 'Folder',
51
+ filePaths: [`${repoRoot}/src/maid/default.project.json`],
52
+ children: [
53
+ {
54
+ name: 'Shared',
55
+ className: 'Folder',
56
+ children: [
57
+ {
58
+ name: 'Maid',
59
+ className: 'ModuleScript',
60
+ filePaths: [`${repoRoot}/src/maid/src/Shared/Maid.lua`],
61
+ children: [
62
+ {
63
+ name: 'Maid.spec',
64
+ className: 'ModuleScript',
65
+ filePaths: [
66
+ `${repoRoot}/src/maid/src/Shared/Maid.spec.lua`,
67
+ ],
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ },
73
+ ],
74
+ },
75
+ ],
76
+ };
77
+ }
78
+
79
+ describe('SourcemapResolver', () => {
80
+ const repoRoot = '/repo';
81
+ const resolver = SourcemapResolver.fromSourcemap(
82
+ _createTestSourcemap(repoRoot),
83
+ repoRoot
84
+ );
85
+
86
+ it('resolves a spec file instance path', () => {
87
+ expect(
88
+ resolver.resolve(
89
+ 'ServerScriptService.observablecollection.Shared.ObservableList.ObservableList.spec'
90
+ )
91
+ ).toBe('src/observablecollection/src/Shared/ObservableList.spec.lua');
92
+ });
93
+
94
+ it('resolves a module instance path', () => {
95
+ expect(
96
+ resolver.resolve(
97
+ 'ServerScriptService.observablecollection.Shared.ObservableList'
98
+ )
99
+ ).toBe('src/observablecollection/src/Shared/ObservableList.lua');
100
+ });
101
+
102
+ it('strips :LINE suffix before lookup', () => {
103
+ expect(
104
+ resolver.resolve(
105
+ 'ServerScriptService.maid.Shared.Maid.Maid.spec:23'
106
+ )
107
+ ).toBe('src/maid/src/Shared/Maid.spec.lua');
108
+ });
109
+
110
+ it('returns undefined for unknown paths', () => {
111
+ expect(
112
+ resolver.resolve('ServerScriptService.nonexistent.Shared.Foo')
113
+ ).toBeUndefined();
114
+ });
115
+
116
+ it('skips nodes that only have .project.json (no lua files)', () => {
117
+ // The "observablecollection" folder node only has a .project.json filePath.
118
+ // It should not be indexed.
119
+ expect(
120
+ resolver.resolve('ServerScriptService.observablecollection')
121
+ ).toBeUndefined();
122
+ });
123
+
124
+ it('resolves paths in a different package', () => {
125
+ expect(
126
+ resolver.resolve('ServerScriptService.maid.Shared.Maid')
127
+ ).toBe('src/maid/src/Shared/Maid.lua');
128
+ });
129
+
130
+ it('supports a custom root alias', () => {
131
+ const customResolver = SourcemapResolver.fromSourcemap(
132
+ _createTestSourcemap(repoRoot),
133
+ repoRoot,
134
+ 'ReplicatedStorage'
135
+ );
136
+
137
+ expect(
138
+ customResolver.resolve(
139
+ 'ReplicatedStorage.maid.Shared.Maid'
140
+ )
141
+ ).toBe('src/maid/src/Shared/Maid.lua');
142
+
143
+ // ServerScriptService should NOT work with a different alias
144
+ expect(
145
+ customResolver.resolve(
146
+ 'ServerScriptService.maid.Shared.Maid'
147
+ )
148
+ ).toBeUndefined();
149
+ });
150
+ });
@@ -0,0 +1,75 @@
1
+ import * as path from 'path';
2
+ import type { SourcemapNode } from './sourcemap-types.js';
3
+
4
+ /**
5
+ * Builds a lookup index from a Rojo sourcemap tree so that dotted Roblox
6
+ * instance paths (e.g. `ServerScriptService.maid.Shared.Maid.spec`) can be
7
+ * resolved to repo-relative filesystem paths.
8
+ */
9
+ export class SourcemapResolver {
10
+ private readonly _index: Map<string, string>;
11
+
12
+ private constructor(index: Map<string, string>) {
13
+ this._index = index;
14
+ }
15
+
16
+ /**
17
+ * Build a resolver from a parsed sourcemap root node.
18
+ *
19
+ * @param root - The top-level sourcemap node (typically named after the project)
20
+ * @param repoRoot - Absolute path to the repo root, used to convert absolute
21
+ * `filePaths` entries to repo-relative paths
22
+ * @param rootAlias - The Roblox service name that the root node maps to in
23
+ * test output. Defaults to `"ServerScriptService"`.
24
+ */
25
+ static fromSourcemap(
26
+ root: SourcemapNode,
27
+ repoRoot: string,
28
+ rootAlias = 'ServerScriptService'
29
+ ): SourcemapResolver {
30
+ const index = new Map<string, string>();
31
+ _walkNode(root, rootAlias, repoRoot, index);
32
+ return new SourcemapResolver(index);
33
+ }
34
+
35
+ /**
36
+ * Resolve a Roblox instance path to a repo-relative filesystem path.
37
+ *
38
+ * Strips any trailing `:LINE` suffix before lookup.
39
+ *
40
+ * @returns The repo-relative path, or `undefined` if the instance path is
41
+ * not in the sourcemap.
42
+ */
43
+ resolve(instancePath: string): string | undefined {
44
+ const cleaned = instancePath.replace(/:\d+$/, '');
45
+ return this._index.get(cleaned);
46
+ }
47
+ }
48
+
49
+ /** Recursively walk the sourcemap tree, populating the index map. */
50
+ function _walkNode(
51
+ node: SourcemapNode,
52
+ dottedPath: string,
53
+ repoRoot: string,
54
+ index: Map<string, string>
55
+ ): void {
56
+ const luaFile = _findLuaFilePath(node.filePaths);
57
+ if (luaFile) {
58
+ const relative = path.relative(repoRoot, luaFile);
59
+ index.set(dottedPath, relative);
60
+ }
61
+
62
+ if (!node.children) return;
63
+
64
+ for (const child of node.children) {
65
+ _walkNode(child, `${dottedPath}.${child.name}`, repoRoot, index);
66
+ }
67
+ }
68
+
69
+ /** Return the first `.lua` or `.luau` file path from a node's filePaths. */
70
+ function _findLuaFilePath(filePaths?: string[]): string | undefined {
71
+ if (!filePaths) return undefined;
72
+ return filePaths.find(
73
+ (fp) => fp.endsWith('.lua') || fp.endsWith('.luau')
74
+ );
75
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A node in the Rojo sourcemap tree (generated by `rojo sourcemap --absolute`).
3
+ *
4
+ * Each node represents a Roblox instance and optionally maps to one or more
5
+ * filesystem paths.
6
+ */
7
+ export interface SourcemapNode {
8
+ name: string;
9
+ className?: string;
10
+ children?: SourcemapNode[];
11
+ filePaths?: string[];
12
+ }
@@ -0,0 +1,2 @@
1
+ export { resolveRobloxTestPath } from './roblox-path-resolver.js';
2
+ export { parseJestLuaOutput } from './jest-lua-parser.js';