@quenty/nevermore-cli 4.50.0 → 4.50.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +6 -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 +7 -3
  4. package/dist/commands/tools-command/ci-post-test-results.js.map +1 -1
  5. package/dist/utils/batch/changed-packages-utils.d.ts +8 -0
  6. package/dist/utils/batch/changed-packages-utils.d.ts.map +1 -1
  7. package/dist/utils/batch/changed-packages-utils.js +9 -11
  8. package/dist/utils/batch/changed-packages-utils.js.map +1 -1
  9. package/dist/utils/testing/parsers/annotation-workspace.d.ts +17 -0
  10. package/dist/utils/testing/parsers/annotation-workspace.d.ts.map +1 -0
  11. package/dist/utils/testing/parsers/annotation-workspace.js +47 -0
  12. package/dist/utils/testing/parsers/annotation-workspace.js.map +1 -0
  13. package/dist/utils/testing/parsers/index.d.ts +0 -1
  14. package/dist/utils/testing/parsers/index.d.ts.map +1 -1
  15. package/dist/utils/testing/parsers/index.js +0 -1
  16. package/dist/utils/testing/parsers/index.js.map +1 -1
  17. package/dist/utils/testing/parsers/jest-lua-parser.d.ts +14 -29
  18. package/dist/utils/testing/parsers/jest-lua-parser.d.ts.map +1 -1
  19. package/dist/utils/testing/parsers/jest-lua-parser.js +85 -100
  20. package/dist/utils/testing/parsers/jest-lua-parser.js.map +1 -1
  21. package/dist/utils/testing/parsers/jest-lua-parser.test.js +359 -56
  22. package/dist/utils/testing/parsers/jest-lua-parser.test.js.map +1 -1
  23. package/dist/utils/testing/parsers/roblox-path-resolver.d.ts +23 -17
  24. package/dist/utils/testing/parsers/roblox-path-resolver.d.ts.map +1 -1
  25. package/dist/utils/testing/parsers/roblox-path-resolver.js +89 -54
  26. package/dist/utils/testing/parsers/roblox-path-resolver.js.map +1 -1
  27. package/dist/utils/testing/parsers/roblox-path-resolver.test.js +76 -36
  28. package/dist/utils/testing/parsers/roblox-path-resolver.test.js.map +1 -1
  29. package/dist/utils/testing/runner/combined-project-generator.d.ts.map +1 -1
  30. package/dist/utils/testing/runner/combined-project-generator.js +5 -6
  31. package/dist/utils/testing/runner/combined-project-generator.js.map +1 -1
  32. package/dist/utils/testing/runner/test-project-slug.d.ts +11 -0
  33. package/dist/utils/testing/runner/test-project-slug.d.ts.map +1 -0
  34. package/dist/utils/testing/runner/test-project-slug.js +11 -0
  35. package/dist/utils/testing/runner/test-project-slug.js.map +1 -0
  36. package/package.json +6 -6
  37. package/src/commands/tools-command/ci-post-test-results.ts +7 -2
  38. package/src/utils/batch/changed-packages-utils.ts +21 -12
  39. package/src/utils/testing/parsers/annotation-workspace.ts +74 -0
  40. package/src/utils/testing/parsers/index.ts +0 -1
  41. package/src/utils/testing/parsers/jest-lua-parser.test.ts +433 -65
  42. package/src/utils/testing/parsers/jest-lua-parser.ts +131 -109
  43. package/src/utils/testing/parsers/roblox-path-resolver.test.ts +144 -61
  44. package/src/utils/testing/parsers/roblox-path-resolver.ts +117 -56
  45. package/src/utils/testing/runner/combined-project-generator.ts +9 -13
  46. package/src/utils/testing/runner/test-project-slug.ts +18 -0
  47. package/tsconfig.tsbuildinfo +1 -1
@@ -1,82 +1,143 @@
1
1
  /**
2
- * Converts Roblox instance paths from Jest-lua output back to
3
- * repo-relative local filesystem paths.
2
+ * Maps Roblox instance paths from Jest-lua output to existing repo-relative
3
+ * source files, through `sourcemap.json` or else the `<package dir>/src/`
4
+ * layout.
4
5
  *
5
- * Two strategies:
6
- *
7
- * 1. **Sourcemap** (preferred) — Uses the authoritative `sourcemap.json`
8
- * mapping from `rojo sourcemap --absolute`. Exact and handles all
9
- * project layouts.
10
- *
11
- * 2. **Heuristic** (fallback) — Assumes `src/{slug}/src/` layout and
12
- * hard-codes `.spec`/`.story` as known dotted suffixes. Works when no
13
- * sourcemap is available.
6
+ * A package's npm name (`@quenty/playermock`), directory (`src/player-mock`),
7
+ * Rojo project name (`playermock`) and test-place slug (`gameproduct` for
8
+ * `src/gameproductservice`) can all differ; see `AnnotationWorkspace`.
14
9
  */
15
10
 
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
16
13
  import type { SourcemapResolver } from '../../sourcemap/index.js';
14
+ import type {
15
+ AnnotationWorkspace,
16
+ AnnotationWorkspacePackage,
17
+ } from './annotation-workspace.js';
17
18
 
18
19
  const SERVER_SCRIPT_SERVICE_PREFIX = 'ServerScriptService.';
20
+ const NODE_MODULES_SEGMENT = '.node_modules.';
21
+ const QUENTY_SCOPE = '@quenty/';
22
+ const QUENTY_SCOPE_SEGMENT = '@quenty.';
23
+ const CHUNK_NAME_PATTERN = /^\[string "(.*)"\]$/;
24
+ // Rojo strips `.server`/`.client` from script instance names.
25
+ const SOURCE_EXTENSIONS = ['', '.server', '.client'].flatMap((kind) => [
26
+ `${kind}.lua`,
27
+ `${kind}.luau`,
28
+ `/init${kind}.lua`,
29
+ `/init${kind}.luau`,
30
+ ]);
31
+
32
+ export interface RobloxSourceResolveOptions {
33
+ sourcemapResolver?: SourcemapResolver;
34
+ workspace?: AnnotationWorkspace;
35
+ /** npm name of the package under test, e.g. `@quenty/gameproductservice` */
36
+ packageName?: string;
37
+ /** Repo root that holds the workspace. Defaults to cwd. */
38
+ repoRoot?: string;
39
+ }
40
+
41
+ export interface ResolvedSourceFile {
42
+ file: string;
43
+ /** False for dependency copies and sibling packages. */
44
+ isPackageUnderTest: boolean;
45
+ }
19
46
 
20
47
  /**
21
- * Resolve a Roblox instance path (from Jest-lua or a stack trace) to a
22
- * repo-relative filesystem path.
23
- *
24
- * When a `sourcemapResolver` is provided, it is tried first. Falls back to
25
- * the heuristic if the resolver doesn't have a mapping.
48
+ * Resolve an instance path (no `:LINE` suffix) to an existing repo-relative
49
+ * source file, or `undefined` when none matches.
26
50
  */
27
- export function resolveRobloxTestPath(
51
+ export function tryResolveRobloxSourceFile(
28
52
  instancePath: string,
29
- sourcemapResolver?: SourcemapResolver
30
- ): string {
31
- if (sourcemapResolver) {
32
- const resolved = sourcemapResolver.resolve(instancePath);
33
- if (resolved) return resolved;
53
+ options: RobloxSourceResolveOptions = {}
54
+ ): ResolvedSourceFile | undefined {
55
+ let unwrapped = _unwrapChunkName(instancePath);
56
+
57
+ // Roblox truncates long chunk names: `...@quenty.servic..."]`
58
+ if (unwrapped.endsWith('...')) return undefined;
59
+
60
+ if (unwrapped.startsWith(SERVER_SCRIPT_SERVICE_PREFIX)) {
61
+ unwrapped = unwrapped.slice(SERVER_SCRIPT_SERVICE_PREFIX.length);
34
62
  }
35
63
 
36
- return _resolveHeuristic(instancePath);
37
- }
64
+ const packagesByName = options.workspace?.packagesByName;
38
65
 
39
- /**
40
- * Heuristic fallback: assumes `src/{slug}/src/` layout.
41
- *
42
- * Handles optional `:LINE` suffixes and missing `ServerScriptService.` prefix.
43
- */
44
- function _resolveHeuristic(instancePath: string): string {
45
- // Strip :LINE suffix if present
46
- let path = instancePath.replace(/:\d+$/, '');
66
+ let workspacePackage: AnnotationWorkspacePackage | undefined;
67
+ let packageInstanceName: string;
68
+ let rest: string;
69
+ let isPackageUnderTest = false;
47
70
 
48
- // Strip ServerScriptService. prefix
49
- if (path.startsWith(SERVER_SCRIPT_SERVICE_PREFIX)) {
50
- path = path.slice(SERVER_SCRIPT_SERVICE_PREFIX.length);
71
+ const nodeModulesIndex = unwrapped.lastIndexOf(NODE_MODULES_SEGMENT);
72
+ if (nodeModulesIndex !== -1) {
73
+ const dependencyPath = unwrapped.slice(
74
+ nodeModulesIndex + NODE_MODULES_SEGMENT.length
75
+ );
76
+ if (!dependencyPath.startsWith(QUENTY_SCOPE_SEGMENT)) return undefined;
77
+ [packageInstanceName, rest] = _splitFirstSegment(
78
+ dependencyPath.slice(QUENTY_SCOPE_SEGMENT.length)
79
+ );
80
+ workspacePackage = packagesByName?.get(QUENTY_SCOPE + packageInstanceName);
81
+ } else {
82
+ [packageInstanceName, rest] = _splitFirstSegment(unwrapped);
83
+ const packageUnderTest = options.packageName
84
+ ? packagesByName?.get(options.packageName)
85
+ : undefined;
86
+ if (packageUnderTest?.testSlugs.includes(packageInstanceName)) {
87
+ workspacePackage = packageUnderTest;
88
+ isPackageUnderTest = true;
89
+ }
51
90
  }
52
91
 
53
- // First dot-separated segment is the package slug
54
- const dotIndex = path.indexOf('.');
55
- if (dotIndex === -1) {
56
- // Just a package name with no sub-path — return the src dir
57
- return `src/${path}/src`;
92
+ const dir = workspacePackage?.dir ?? `src/${packageInstanceName}`;
93
+ // The sourcemap names packages by Rojo project name, not folder.
94
+ const sourcemapNames = new Set([
95
+ workspacePackage?.projectName ?? packageInstanceName,
96
+ path.posix.basename(dir),
97
+ ]);
98
+ for (const name of sourcemapNames) {
99
+ const mapped = options.sourcemapResolver?.resolve(
100
+ SERVER_SCRIPT_SERVICE_PREFIX + name + (rest ? `.${rest}` : '')
101
+ );
102
+ if (mapped) return { file: mapped, isPackageUnderTest };
58
103
  }
59
104
 
60
- const packageSlug = path.slice(0, dotIndex);
61
- const remaining = path.slice(dotIndex + 1);
105
+ const repoRoot = options.repoRoot ?? process.cwd();
106
+ const base = _resolveHeuristic(dir, rest);
107
+ for (const extension of SOURCE_EXTENSIONS) {
108
+ const file = `${base}${extension}`;
109
+ if (fs.existsSync(path.join(repoRoot, file))) {
110
+ return { file, isPackageUnderTest };
111
+ }
112
+ }
62
113
 
63
- // Split into segments. In Rojo, dots in filenames (e.g. "Maid.spec.lua"
64
- // becomes instance "Maid.spec") are ambiguous with the hierarchy separator.
65
- // Rejoin known filename suffixes like ".spec" and ".story" with a dot
66
- // instead of a slash.
67
- const segments = remaining.split('.');
68
- const rejoined: string[] = [];
114
+ return undefined;
115
+ }
69
116
 
70
- for (let i = 0; i < segments.length; i++) {
71
- const seg = segments[i];
72
- // If this segment is a known file suffix, merge it with the previous
73
- if (i > 0 && (seg === 'spec' || seg === 'story')) {
74
- rejoined[rejoined.length - 1] += `.${seg}`;
117
+ function _unwrapChunkName(name: string): string {
118
+ const match = name.trim().match(CHUNK_NAME_PATTERN);
119
+ return match ? match[1] : name.trim();
120
+ }
121
+
122
+ function _splitFirstSegment(dottedPath: string): [string, string] {
123
+ const dotIndex = dottedPath.indexOf('.');
124
+ return dotIndex === -1
125
+ ? [dottedPath, '']
126
+ : [dottedPath.slice(0, dotIndex), dottedPath.slice(dotIndex + 1)];
127
+ }
128
+
129
+ function _resolveHeuristic(dir: string, rest: string): string {
130
+ if (!rest) return `${dir}/src`;
131
+
132
+ // Rojo turns `Maid.spec.lua` into instance `Maid.spec`, so rejoin suffixes.
133
+ const rejoined: string[] = [];
134
+ for (const segment of rest.split('.')) {
135
+ if (rejoined.length > 0 && (segment === 'spec' || segment === 'story')) {
136
+ rejoined[rejoined.length - 1] += `.${segment}`;
75
137
  } else {
76
- rejoined.push(seg);
138
+ rejoined.push(segment);
77
139
  }
78
140
  }
79
141
 
80
- const subPath = rejoined.join('/');
81
- return `src/${packageSlug}/src/${subPath}.lua`;
142
+ return `${dir}/src/${rejoined.join('/')}`;
82
143
  }
@@ -8,6 +8,10 @@ import { OutputHelper } from '@quenty/cli-output-helpers';
8
8
  import { type DeployTarget } from '@quenty/nevermore-deploy';
9
9
  import { type BatchTarget } from '../../batch/changed-packages-utils.js';
10
10
  import { recompressPlace } from '../../build/place-recompression.js';
11
+ import {
12
+ type TestRojoProject,
13
+ findTestProjectSlug,
14
+ } from './test-project-slug.js';
11
15
 
12
16
  /**
13
17
  * Budget for one combined place, as uploaded (recompressed). Under the upload
@@ -40,10 +44,6 @@ export interface CombinedProjectResult {
40
44
  buildContext: BuildContext;
41
45
  }
42
46
 
43
- interface RojoNode {
44
- [key: string]: unknown;
45
- }
46
-
47
47
  import { type StepProgress } from '@quenty/cli-output-helpers/reporting';
48
48
 
49
49
  /** Progress callbacks for granular phase reporting during combined builds. */
@@ -345,20 +345,16 @@ async function _extractSlugAsync(
345
345
  );
346
346
  }
347
347
 
348
- const project = JSON.parse(content) as {
349
- tree: { ServerScriptService?: RojoNode };
350
- };
351
- const serverScriptService = project.tree?.ServerScriptService;
352
- if (!serverScriptService) {
348
+ const project = JSON.parse(content) as TestRojoProject;
349
+ if (!project.tree?.ServerScriptService) {
353
350
  throw new Error(
354
351
  `Test project for ${batchTargetName} is missing ServerScriptService in tree`
355
352
  );
356
353
  }
357
354
 
358
- for (const key of Object.keys(serverScriptService)) {
359
- if (!key.startsWith('$') && key !== 'Script') {
360
- return key;
361
- }
355
+ const slug = findTestProjectSlug(project);
356
+ if (slug) {
357
+ return slug;
362
358
  }
363
359
 
364
360
  throw new Error(
@@ -0,0 +1,18 @@
1
+ export interface TestRojoProject {
2
+ tree?: { ServerScriptService?: Record<string, unknown> };
3
+ }
4
+
5
+ /**
6
+ * A package's instance name in its test place: the first non-`$`,
7
+ * non-`Script` key under ServerScriptService.
8
+ */
9
+ export function findTestProjectSlug(
10
+ project: TestRojoProject
11
+ ): string | undefined {
12
+ const serverScriptService = project.tree?.ServerScriptService;
13
+ if (!serverScriptService) return undefined;
14
+
15
+ return Object.keys(serverScriptService).find(
16
+ (key) => !key.startsWith('$') && key !== 'Script'
17
+ );
18
+ }