@quenty/nevermore-cli 4.38.0 → 4.40.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 (33) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/commands/batch-command/batch-deploy-command.js +1 -1
  3. package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
  4. package/dist/commands/deploy-command/index.d.ts.map +1 -1
  5. package/dist/commands/deploy-command/index.js +5 -2
  6. package/dist/commands/deploy-command/index.js.map +1 -1
  7. package/dist/commands/tools-command/strip-sourcemap-jest-command.d.ts +5 -1
  8. package/dist/commands/tools-command/strip-sourcemap-jest-command.d.ts.map +1 -1
  9. package/dist/commands/tools-command/strip-sourcemap-jest-command.js +40 -14
  10. package/dist/commands/tools-command/strip-sourcemap-jest-command.js.map +1 -1
  11. package/dist/utils/batch/changed-packages-utils.d.ts +8 -1
  12. package/dist/utils/batch/changed-packages-utils.d.ts.map +1 -1
  13. package/dist/utils/batch/changed-packages-utils.js +10 -2
  14. package/dist/utils/batch/changed-packages-utils.js.map +1 -1
  15. package/dist/utils/build/deploy-config.d.ts +14 -0
  16. package/dist/utils/build/deploy-config.d.ts.map +1 -1
  17. package/dist/utils/build/deploy-config.js +8 -0
  18. package/dist/utils/build/deploy-config.js.map +1 -1
  19. package/dist/utils/deploy/deploy-metadata.d.ts +7 -1
  20. package/dist/utils/deploy/deploy-metadata.d.ts.map +1 -1
  21. package/dist/utils/deploy/deploy-metadata.js +13 -1
  22. package/dist/utils/deploy/deploy-metadata.js.map +1 -1
  23. package/dist/utils/deploy/deploy-metadata.test.js +25 -0
  24. package/dist/utils/deploy/deploy-metadata.test.js.map +1 -1
  25. package/package.json +2 -2
  26. package/src/commands/batch-command/batch-deploy-command.ts +11 -7
  27. package/src/commands/deploy-command/index.ts +15 -7
  28. package/src/commands/tools-command/strip-sourcemap-jest-command.ts +46 -16
  29. package/src/utils/batch/changed-packages-utils.ts +18 -1
  30. package/src/utils/build/deploy-config.ts +22 -0
  31. package/src/utils/deploy/deploy-metadata.test.ts +28 -0
  32. package/src/utils/deploy/deploy-metadata.ts +17 -1
  33. package/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/nevermore-cli",
3
- "version": "4.38.0",
3
+ "version": "4.40.0",
4
4
  "description": "CLI interface for Nevermore",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -57,5 +57,5 @@
57
57
  "engines": {
58
58
  "node": ">=16"
59
59
  },
60
- "gitHead": "7d1c8f4960e49256679a62bdc6aec0d51b7d0d89"
60
+ "gitHead": "2c903e91ae2ea1702ae1ce94af20af83af81e4fe"
61
61
  }
@@ -236,13 +236,17 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
236
236
 
237
237
  const injected = await injectDeployMetadataAsync(
238
238
  builtPlace,
239
- buildDeployMetadataAttributes(deployGitInfo, {
240
- target: targetName,
241
- published: publish,
242
- timestamp: deployTimestamp,
243
- universeId: buildTarget.target.universeId,
244
- placeId: buildTarget.target.placeId,
245
- })
239
+ buildDeployMetadataAttributes(
240
+ deployGitInfo,
241
+ {
242
+ target: targetName,
243
+ published: publish,
244
+ timestamp: deployTimestamp,
245
+ universeId: buildTarget.target.universeId,
246
+ placeId: buildTarget.target.placeId,
247
+ },
248
+ buildTarget.manifestPlaces
249
+ )
246
250
  );
247
251
 
248
252
  let version: number;
@@ -34,6 +34,7 @@ import {
34
34
  loadDeployConfigAsync,
35
35
  resolveDeployConfigPath,
36
36
  resolveDeployTargetPlaces,
37
+ toManifestPlaceInfo,
37
38
  } from '../../utils/build/deploy-config.js';
38
39
  import { handleInitAsync } from './deploy-init.js';
39
40
  import {
@@ -256,6 +257,7 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
256
257
 
257
258
  const config = await loadDeployConfigAsync(resolveDeployConfigPath(cwd));
258
259
  const places = resolveDeployTargetPlaces(config, targetName);
260
+ const manifestPlaces = places.map(toManifestPlaceInfo);
259
261
  const isMultiPlace = places.length > 1;
260
262
 
261
263
  if (isMultiPlace) {
@@ -279,6 +281,7 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
279
281
  packageName,
280
282
  path: cwd,
281
283
  target: place,
284
+ manifestPlaces,
282
285
  };
283
286
  })
284
287
  : [
@@ -287,6 +290,7 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
287
290
  packageName,
288
291
  path: cwd,
289
292
  target: places[0]!,
293
+ manifestPlaces,
290
294
  },
291
295
  ];
292
296
 
@@ -402,13 +406,17 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
402
406
 
403
407
  const injected = await injectDeployMetadataAsync(
404
408
  builtPlace,
405
- buildDeployMetadataAttributes(deployGitInfo, {
406
- target: targetName,
407
- published: publish,
408
- timestamp: deployTimestamp,
409
- universeId: args.universeId ?? buildTarget.target.universeId,
410
- placeId: args.placeId ?? buildTarget.target.placeId,
411
- })
409
+ buildDeployMetadataAttributes(
410
+ deployGitInfo,
411
+ {
412
+ target: targetName,
413
+ published: publish,
414
+ timestamp: deployTimestamp,
415
+ universeId: args.universeId ?? buildTarget.target.universeId,
416
+ placeId: args.placeId ?? buildTarget.target.placeId,
417
+ },
418
+ buildTarget.manifestPlaces
419
+ )
412
420
  );
413
421
 
414
422
  let uploadResult: Awaited<ReturnType<typeof uploadPlaceAsync>>;
@@ -10,13 +10,17 @@ interface StripSourcemapJestArgs extends NevermoreGlobalArgs {
10
10
  }
11
11
 
12
12
  /**
13
- * Temporary workaround: remove Jest nodes from sourcemap.json.
13
+ * Temporary workaround: strip shadowing modules from Jest subtrees in sourcemap.json.
14
14
  *
15
15
  * luau-lsp's require-by-name resolution picks the first file matching a name
16
16
  * in the sourcemap. Jest vendors internal copies of modules (Promise, string,
17
17
  * symbol, etc.) that shadow Nevermore packages. The real loader handles this
18
18
  * correctly, but luau-lsp doesn't yet.
19
19
  *
20
+ * We drop only the Jest descendants whose name also exists elsewhere in the
21
+ * sourcemap (the shadowing copies), and keep the Jest-unique modules (Jest,
22
+ * JestGlobals, ...) so `require("Jest")` still resolves.
23
+ *
20
24
  * Long-term fix: smarter require resolution in luau-lsp (plugins or fork).
21
25
  */
22
26
  export const stripSourcemapJestCommand: CommandModule<
@@ -25,7 +29,7 @@ export const stripSourcemapJestCommand: CommandModule<
25
29
  > = {
26
30
  command: 'strip-sourcemap-jest',
27
31
  describe:
28
- 'Remove Jest nodes from sourcemap.json to avoid luau-lsp name conflicts',
32
+ "Strip modules from Jest subtrees in sourcemap.json that shadow names elsewhere, keeping require('Jest') resolvable",
29
33
  builder: (yargs) => {
30
34
  return yargs.option('sourcemap', {
31
35
  describe: 'Path to sourcemap.json',
@@ -46,31 +50,57 @@ export const stripSourcemapJestCommand: CommandModule<
46
50
 
47
51
  const sourcemap = JSON.parse(content) as SourcemapNode;
48
52
 
49
- let removed = 0;
53
+ // Every module name that lives OUTSIDE any Jest subtree. Jest's vendored copies of these
54
+ // are what shadow the real Nevermore modules under luau-lsp's first-match resolution.
55
+ const externalNames = new Set<string>();
56
+ function collectExternalNames(
57
+ node: SourcemapNode,
58
+ insideJest: boolean
59
+ ): void {
60
+ if (!node.children) return;
61
+ for (const child of node.children) {
62
+ const childInsideJest = insideJest || child.name === 'Jest';
63
+ if (!childInsideJest) {
64
+ externalNames.add(child.name);
65
+ }
66
+ collectExternalNames(child, childInsideJest);
67
+ }
68
+ }
69
+ collectExternalNames(sourcemap, false);
50
70
 
51
- function removeJestNodes(node: SourcemapNode): void {
71
+ // Within each Jest subtree, drop any node whose name also exists elsewhere (the shadowing
72
+ // copies), keeping the Jest-unique modules so require("Jest") still resolves. Also drop a
73
+ // Jest node nested inside another Jest: the jest-lua package folder ("Jest", whose
74
+ // init.lua exports `.Globals`) contains a leaf "Jest" submodule that lacks `.Globals`, and
75
+ // the duplicate name makes require("Jest") resolve to the wrong one for some packages.
76
+ let stripped = 0;
77
+ function stripShadowingNodes(
78
+ node: SourcemapNode,
79
+ insideJest: boolean
80
+ ): void {
52
81
  if (!node.children) return;
53
82
 
54
- node.children = node.children.filter((child) => {
55
- if (child.name === 'Jest') {
56
- removed++;
57
- return false;
58
- }
59
- return true;
60
- });
83
+ if (insideJest) {
84
+ node.children = node.children.filter((child) => {
85
+ if (externalNames.has(child.name) || child.name === 'Jest') {
86
+ stripped++;
87
+ return false;
88
+ }
89
+ return true;
90
+ });
91
+ }
61
92
 
62
93
  for (const child of node.children) {
63
- removeJestNodes(child);
94
+ stripShadowingNodes(child, insideJest || child.name === 'Jest');
64
95
  }
65
96
  }
66
-
67
- removeJestNodes(sourcemap);
97
+ stripShadowingNodes(sourcemap, false);
68
98
 
69
99
  fs.writeFileSync(sourcemapPath, JSON.stringify(sourcemap));
70
100
 
71
- if (removed > 0) {
101
+ if (stripped > 0) {
72
102
  OutputHelper.info(
73
- `Removed ${removed} Jest node(s) from ${sourcemapPath}`
103
+ `Stripped ${stripped} shadowing node(s) from Jest subtrees in ${sourcemapPath}`
74
104
  );
75
105
  }
76
106
  },
@@ -3,9 +3,11 @@ import { execa } from 'execa';
3
3
  import { OutputHelper } from '@quenty/cli-output-helpers';
4
4
  import {
5
5
  DeployTarget,
6
+ ManifestPlaceInfo,
6
7
  loadDeployConfigAsync,
7
8
  resolveDeployConfigPath,
8
9
  resolveDeployTargetPlaces,
10
+ toManifestPlaceInfo,
9
11
  } from '../build/deploy-config.js';
10
12
 
11
13
  export interface TargetPackage {
@@ -33,6 +35,13 @@ export interface BatchTarget {
33
35
  path: string;
34
36
  /** The single resolved place this BatchTarget represents. */
35
37
  target: DeployTarget;
38
+ /**
39
+ * Every place of this package's target (including `target` itself), stamped
40
+ * into the runtime manifest so a deployed place can resolve its siblings' IDs.
41
+ * Same for every BatchTarget fanned out from one package. Only the deploy
42
+ * paths populate this; test/script BatchTargets omit it.
43
+ */
44
+ manifestPlaces?: ManifestPlaceInfo[];
36
45
  }
37
46
 
38
47
  /**
@@ -119,19 +128,27 @@ export function flattenToBatchTargets(
119
128
  ): BatchTarget[] {
120
129
  const result: BatchTarget[] = [];
121
130
  for (const pkg of packages) {
131
+ const manifestPlaces = pkg.activeTargets.map(toManifestPlaceInfo);
122
132
  if (pkg.activeTargets.length === 1) {
123
133
  result.push({
124
134
  name: pkg.name,
125
135
  packageName: pkg.name,
126
136
  path: pkg.path,
127
137
  target: pkg.activeTargets[0]!,
138
+ manifestPlaces,
128
139
  });
129
140
  continue;
130
141
  }
131
142
  for (const target of pkg.activeTargets) {
132
143
  const suffix = target.name;
133
144
  const name = suffix ? `${pkg.name} - ${suffix}` : pkg.name;
134
- result.push({ name, packageName: pkg.name, path: pkg.path, target });
145
+ result.push({
146
+ name,
147
+ packageName: pkg.name,
148
+ path: pkg.path,
149
+ target,
150
+ manifestPlaces,
151
+ });
135
152
  }
136
153
  }
137
154
  return result;
@@ -36,6 +36,28 @@ export interface DeployConfig {
36
36
  targets: Record<string, DeployTargetConfig>;
37
37
  }
38
38
 
39
+ /**
40
+ * The subset of a resolved place that gets baked into the runtime manifest
41
+ * (see the nevermore-cli-manifest package). A deployed place carries the whole
42
+ * target's place table so it can resolve its siblings' IDs at runtime — e.g. a
43
+ * chapter place teleporting to another chapter without hard-coding place IDs.
44
+ */
45
+ export interface ManifestPlaceInfo {
46
+ /** Place name from a multi-place target (e.g. "chapter0"); absent for single-place targets. */
47
+ name?: string;
48
+ placeId: number;
49
+ universeId: number;
50
+ }
51
+
52
+ /** Project a resolved deploy place down to the fields stamped into the manifest. */
53
+ export function toManifestPlaceInfo(place: DeployTarget): ManifestPlaceInfo {
54
+ return {
55
+ name: place.name,
56
+ placeId: place.placeId,
57
+ universeId: place.universeId,
58
+ };
59
+ }
60
+
39
61
  function _isMultiPlace(
40
62
  target: DeployTargetConfig
41
63
  ): target is MultiPlaceTargetConfig {
@@ -84,6 +84,34 @@ describe('buildDeployMetadataAttributes', () => {
84
84
  );
85
85
  expect(attributes.Published).toBe(false);
86
86
  });
87
+
88
+ it('omits Places when no place table is given', () => {
89
+ const attributes = buildDeployMetadataAttributes({}, PLACE);
90
+ expect('Places' in attributes).toBe(false);
91
+ });
92
+
93
+ it('stamps Places as a JSON string preserving large IDs exactly', () => {
94
+ const attributes = buildDeployMetadataAttributes({}, PLACE, [
95
+ { name: 'chapter0', placeId: 97235312452456, universeId: 10192566764 },
96
+ { name: 'chapter1', placeId: 87639818897831, universeId: 10192566764 },
97
+ ]);
98
+ expect(typeof attributes.Places).toBe('string');
99
+ const parsed = JSON.parse(attributes.Places as string);
100
+ // Large place IDs must survive exactly (JSON text, not a float32 attribute).
101
+ expect(parsed).toEqual([
102
+ { name: 'chapter0', placeId: 97235312452456, universeId: 10192566764 },
103
+ { name: 'chapter1', placeId: 87639818897831, universeId: 10192566764 },
104
+ ]);
105
+ });
106
+
107
+ it('omits the name key for a single-place (nameless) target', () => {
108
+ const attributes = buildDeployMetadataAttributes({}, PLACE, [
109
+ { placeId: 123, universeId: 456 },
110
+ ]);
111
+ const parsed = JSON.parse(attributes.Places as string);
112
+ expect(parsed).toEqual([{ placeId: 123, universeId: 456 }]);
113
+ expect('name' in parsed[0]).toBe(false);
114
+ });
87
115
  });
88
116
 
89
117
  describe('packageUsesManifestAsync', () => {
@@ -7,6 +7,7 @@ import {
7
7
  } from '@quenty/nevermore-template-helpers';
8
8
  import { OutputHelper } from '@quenty/cli-output-helpers';
9
9
  import { type BuiltPlace } from '../build/build.js';
10
+ import { type ManifestPlaceInfo } from '../build/deploy-config.js';
10
11
 
11
12
  /** npm name of the package that ships the manifest module. */
12
13
  export const MANIFEST_PACKAGE_NAME = '@quenty/nevermoreclimanifest';
@@ -85,10 +86,16 @@ export interface DeployPlaceInfo {
85
86
  * round-trip through Lune serialization as float32, which silently corrupts IDs
86
87
  * above 2^24 (e.g. 123456789 -> 123456792). Strings are exact; the Luau reader
87
88
  * converts them back with `tonumber`.
89
+ *
90
+ * When `places` is given (the whole target's place table), it is stamped as a
91
+ * `Places` JSON-string attribute. The IDs inside stay numeric — a JSON string
92
+ * is exact text, and the Luau reader decodes it to 64-bit numbers, so the
93
+ * float32 hazard that forces the top-level IDs to be stringified doesn't apply.
88
94
  */
89
95
  export function buildDeployMetadataAttributes(
90
96
  git: GitDeployInfo,
91
- place: DeployPlaceInfo
97
+ place: DeployPlaceInfo,
98
+ places?: readonly ManifestPlaceInfo[]
92
99
  ): DeployMetadataAttributes {
93
100
  const attributes: DeployMetadataAttributes = {
94
101
  Deployed: true,
@@ -101,6 +108,15 @@ export function buildDeployMetadataAttributes(
101
108
  if (git.commit) attributes.Commit = git.commit;
102
109
  if (git.version) attributes.Version = git.version;
103
110
  if (git.branch) attributes.Branch = git.branch;
111
+ if (places && places.length > 0) {
112
+ attributes.Places = JSON.stringify(
113
+ places.map((p) => ({
114
+ name: p.name,
115
+ placeId: p.placeId,
116
+ universeId: p.universeId,
117
+ }))
118
+ );
119
+ }
104
120
  return attributes;
105
121
  }
106
122