@quenty/nevermore-cli 4.48.0 → 4.49.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.
- package/CHANGELOG.md +6 -0
- package/dist/commands/batch-command/batch-test-command.d.ts +1 -0
- package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-test-command.js +11 -0
- package/dist/commands/batch-command/batch-test-command.js.map +1 -1
- package/dist/utils/build/place-recompression.d.ts +25 -0
- package/dist/utils/build/place-recompression.d.ts.map +1 -0
- package/dist/utils/build/place-recompression.js +166 -0
- package/dist/utils/build/place-recompression.js.map +1 -0
- package/dist/utils/build/place-recompression.test.d.ts +2 -0
- package/dist/utils/build/place-recompression.test.d.ts.map +1 -0
- package/dist/utils/build/place-recompression.test.js +68 -0
- package/dist/utils/build/place-recompression.test.js.map +1 -0
- package/dist/utils/job-context/batch-script-job-context.d.ts +20 -8
- package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.js +94 -22
- package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.d.ts +12 -0
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +37 -1
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/dist/utils/testing/batch-run.e2e.test.js +78 -13
- package/dist/utils/testing/batch-run.e2e.test.js.map +1 -1
- package/dist/utils/testing/runner/combined-project-generator.d.ts +33 -5
- package/dist/utils/testing/runner/combined-project-generator.d.ts.map +1 -1
- package/dist/utils/testing/runner/combined-project-generator.js +116 -19
- package/dist/utils/testing/runner/combined-project-generator.js.map +1 -1
- package/dist/utils/testing/runner/combined-project-generator.test.d.ts +2 -0
- package/dist/utils/testing/runner/combined-project-generator.test.d.ts.map +1 -0
- package/dist/utils/testing/runner/combined-project-generator.test.js +26 -0
- package/dist/utils/testing/runner/combined-project-generator.test.js.map +1 -0
- package/package.json +2 -2
- package/src/commands/batch-command/batch-test-command.ts +14 -0
- package/src/utils/build/place-recompression.test.ts +104 -0
- package/src/utils/build/place-recompression.ts +236 -0
- package/src/utils/job-context/batch-script-job-context.ts +140 -31
- package/src/utils/open-cloud/open-cloud-client.ts +58 -1
- package/src/utils/testing/batch-run.e2e.test.ts +93 -13
- package/src/utils/testing/runner/combined-project-generator.test.ts +30 -0
- package/src/utils/testing/runner/combined-project-generator.ts +198 -30
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
* read and on what each package's result came out as.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
17
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
18
18
|
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
19
19
|
import { BatchScriptJobContext } from '../job-context/batch-script-job-context.js';
|
|
20
20
|
import {
|
|
21
21
|
type BuiltPlace,
|
|
22
22
|
type Deployment,
|
|
23
|
+
type DeployPlaceOptions,
|
|
23
24
|
type JobContext,
|
|
24
25
|
type JobLogs,
|
|
25
26
|
type RunScriptOptions,
|
|
@@ -29,19 +30,33 @@ import { type TaskLogMessage } from '../open-cloud/open-cloud-client.js';
|
|
|
29
30
|
import { type BatchTarget } from '../batch/changed-packages-utils.js';
|
|
30
31
|
import { renderBatchOutcome } from './parsers/batch-log-renderer.js';
|
|
31
32
|
|
|
32
|
-
const { SLUG_MAP } = vi.hoisted(() =>
|
|
33
|
-
SLUG_MAP
|
|
33
|
+
const { SLUG_MAP, combined } = vi.hoisted(() => {
|
|
34
|
+
const SLUG_MAP = new Map([
|
|
34
35
|
['@quenty/alpha', 'alpha'],
|
|
35
36
|
['@quenty/beta', 'beta'],
|
|
36
|
-
])
|
|
37
|
-
|
|
37
|
+
]);
|
|
38
|
+
return {
|
|
39
|
+
SLUG_MAP,
|
|
40
|
+
// Which packages share a place. One place unless a test splits them.
|
|
41
|
+
combined: {
|
|
42
|
+
chunks: [[...SLUG_MAP]] as Array<Array<[string, string]>>,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
});
|
|
38
46
|
|
|
39
47
|
// The only mocked collaborator: building the combined place shells out to rojo
|
|
40
48
|
// and lune, which is a different subject from what this test is about. What it
|
|
41
|
-
// produces that matters downstream is the slug map.
|
|
42
|
-
vi.mock('./runner/combined-project-generator.js', () => ({
|
|
49
|
+
// produces that matters downstream is the slug map, and how it was split.
|
|
50
|
+
vi.mock('./runner/combined-project-generator.js', async (importOriginal) => ({
|
|
51
|
+
...(await importOriginal<
|
|
52
|
+
typeof import('./runner/combined-project-generator.js')
|
|
53
|
+
>()),
|
|
43
54
|
generateCombinedProjectAsync: vi.fn(async () => ({
|
|
44
|
-
|
|
55
|
+
chunks: combined.chunks.map((entries, index) => ({
|
|
56
|
+
rbxlPath: `/tmp/batch-${index + 1}.rbxl`,
|
|
57
|
+
slugMap: new Map(entries),
|
|
58
|
+
sizeBytes: 1024,
|
|
59
|
+
})),
|
|
45
60
|
slugMap: SLUG_MAP,
|
|
46
61
|
primaryTarget: { placeId: 1, universeId: 2 },
|
|
47
62
|
buildContext: { cleanupAsync: async () => {} },
|
|
@@ -119,11 +134,17 @@ const TASK_MESSAGES: TaskLogMessage[] = [
|
|
|
119
134
|
{ message: SUMMARY_JSON, messageType: 'OUTPUT' },
|
|
120
135
|
];
|
|
121
136
|
|
|
122
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* A transport that runs nothing and reports the messages it was handed — or,
|
|
139
|
+
* with `messagesFor`, the messages for whichever packages the script ran.
|
|
140
|
+
*/
|
|
123
141
|
class FakeTransport implements JobContext {
|
|
124
142
|
public scriptContent?: string;
|
|
143
|
+
public scriptContents: string[] = [];
|
|
144
|
+
public deployedPaths: string[] = [];
|
|
125
145
|
public deployCount = 0;
|
|
126
146
|
public runCount = 0;
|
|
147
|
+
public messagesFor?: (script: string) => TaskLogMessage[];
|
|
127
148
|
|
|
128
149
|
constructor(
|
|
129
150
|
private readonly _messages: TaskLogMessage[],
|
|
@@ -134,8 +155,11 @@ class FakeTransport implements JobContext {
|
|
|
134
155
|
throw new Error('the batch context builds the combined place itself');
|
|
135
156
|
}
|
|
136
157
|
|
|
137
|
-
async deployBuiltPlaceAsync(
|
|
158
|
+
async deployBuiltPlaceAsync(
|
|
159
|
+
options: DeployPlaceOptions
|
|
160
|
+
): Promise<Deployment> {
|
|
138
161
|
this.deployCount++;
|
|
162
|
+
this.deployedPaths.push(options.builtPlace.rbxlPath);
|
|
139
163
|
return {};
|
|
140
164
|
}
|
|
141
165
|
|
|
@@ -145,6 +169,7 @@ class FakeTransport implements JobContext {
|
|
|
145
169
|
): Promise<ScriptRunResult> {
|
|
146
170
|
this.runCount++;
|
|
147
171
|
this.scriptContent = options.scriptContent;
|
|
172
|
+
this.scriptContents.push(options.scriptContent);
|
|
148
173
|
return {
|
|
149
174
|
success: true,
|
|
150
175
|
taskState: 'COMPLETE',
|
|
@@ -153,15 +178,17 @@ class FakeTransport implements JobContext {
|
|
|
153
178
|
}
|
|
154
179
|
|
|
155
180
|
async getLogsAsync(): Promise<JobLogs> {
|
|
156
|
-
const
|
|
181
|
+
const messages =
|
|
182
|
+
this.messagesFor?.(this.scriptContent ?? '') ?? this._messages;
|
|
183
|
+
const text = messages.map((m) => m.message).join('\n');
|
|
157
184
|
return {
|
|
158
185
|
text,
|
|
159
|
-
messages
|
|
186
|
+
messages,
|
|
160
187
|
stats: {
|
|
161
188
|
requests: 1,
|
|
162
189
|
pages: 1,
|
|
163
190
|
entries: 1,
|
|
164
|
-
messages:
|
|
191
|
+
messages: messages.length,
|
|
165
192
|
chars: text.length,
|
|
166
193
|
},
|
|
167
194
|
};
|
|
@@ -210,6 +237,10 @@ async function runPackageAsync(
|
|
|
210
237
|
}
|
|
211
238
|
|
|
212
239
|
describe('an aggregated batch run, end to end', () => {
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
combined.chunks = [[...SLUG_MAP]];
|
|
242
|
+
});
|
|
243
|
+
|
|
213
244
|
it('renders the whole run: every line, in order, grouped and titled', async () => {
|
|
214
245
|
const { batch } = createBatch();
|
|
215
246
|
|
|
@@ -422,4 +453,53 @@ describe('an aggregated batch run, end to end', () => {
|
|
|
422
453
|
// being scraped out of the section that went missing.
|
|
423
454
|
expect(outcome.results.get('@quenty/alpha')?.testCounts?.total).toBe(68);
|
|
424
455
|
});
|
|
456
|
+
|
|
457
|
+
it('uploads and runs each place of a split batch, and merges what they report', async () => {
|
|
458
|
+
combined.chunks = [
|
|
459
|
+
[['@quenty/alpha', 'alpha']],
|
|
460
|
+
[['@quenty/beta', 'beta']],
|
|
461
|
+
];
|
|
462
|
+
const { batch, transport } = createBatch();
|
|
463
|
+
// Each place prints only the sections and summary of its own packages.
|
|
464
|
+
transport.messagesFor = (script) => {
|
|
465
|
+
const slug = script.includes('["alpha"]') ? 'alpha' : 'beta';
|
|
466
|
+
const entry = SUMMARY_ENTRIES.find((e) => e.slug === slug)!;
|
|
467
|
+
const verdict = entry.success ? 'PASS' : 'FAIL';
|
|
468
|
+
return [
|
|
469
|
+
{ message: `===BATCH_TEST_BEGIN ${slug}===`, messageType: 'OUTPUT' },
|
|
470
|
+
{
|
|
471
|
+
message: `===BATCH_TEST_END ${slug} ${verdict} ${entry.durationMs}===`,
|
|
472
|
+
messageType: 'OUTPUT',
|
|
473
|
+
},
|
|
474
|
+
{ message: '===BATCH_TEST_SUMMARY===', messageType: 'OUTPUT' },
|
|
475
|
+
{ message: JSON.stringify([entry]), messageType: 'OUTPUT' },
|
|
476
|
+
];
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const outcome = await batch.getExecutionOutcomeAsync();
|
|
480
|
+
const alpha = await runPackageAsync(batch, '@quenty/alpha');
|
|
481
|
+
const beta = await runPackageAsync(batch, '@quenty/beta');
|
|
482
|
+
|
|
483
|
+
expect(transport.deployedPaths).toEqual([
|
|
484
|
+
'/tmp/batch-1.rbxl',
|
|
485
|
+
'/tmp/batch-2.rbxl',
|
|
486
|
+
]);
|
|
487
|
+
expect(transport.runCount).toBe(2);
|
|
488
|
+
expect(transport.scriptContents[0]).toContain('["alpha"]');
|
|
489
|
+
expect(transport.scriptContents[1]).toContain('["beta"]');
|
|
490
|
+
|
|
491
|
+
expect(alpha.success).toBe(true);
|
|
492
|
+
expect(alpha.testResults).toMatchObject({ passed: 68, total: 68 });
|
|
493
|
+
expect(beta.success).toBe(false);
|
|
494
|
+
expect(beta.testResults).toMatchObject({ passed: 3, failed: 1 });
|
|
495
|
+
expect(outcome.stats?.requests).toBe(2);
|
|
496
|
+
expect(
|
|
497
|
+
renderBatchOutcome(outcome, { useGroups: true, color: false })
|
|
498
|
+
).toEqual(
|
|
499
|
+
expect.arrayContaining([
|
|
500
|
+
'::group::@quenty/alpha - ✅ Passed (68/68) (1.5s)',
|
|
501
|
+
'::group::@quenty/beta - ❌ FAILED (3/4) (900ms)',
|
|
502
|
+
])
|
|
503
|
+
);
|
|
504
|
+
});
|
|
425
505
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { partitionBySize } from './combined-project-generator.js';
|
|
3
|
+
|
|
4
|
+
describe('partitionBySize', () => {
|
|
5
|
+
const identity = (n: number) => n;
|
|
6
|
+
|
|
7
|
+
it('keeps everything in one group when it fits', () => {
|
|
8
|
+
expect(partitionBySize([10, 20, 30], identity, 60)).toEqual([[10, 20, 30]]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('starts a new group, in order, before the budget would be exceeded', () => {
|
|
12
|
+
expect(partitionBySize([40, 30, 20, 50, 10], identity, 60)).toEqual([
|
|
13
|
+
[40],
|
|
14
|
+
[30, 20],
|
|
15
|
+
[50, 10],
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('gives an item over the budget a group of its own', () => {
|
|
20
|
+
expect(partitionBySize([10, 100, 10], identity, 60)).toEqual([
|
|
21
|
+
[10],
|
|
22
|
+
[100],
|
|
23
|
+
[10],
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns no groups for no items', () => {
|
|
28
|
+
expect(partitionBySize([], identity, 60)).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -7,11 +7,32 @@ import {
|
|
|
7
7
|
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
|
+
import { recompressPlace } from '../../build/place-recompression.js';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Budget for one combined place, as uploaded (recompressed). Under the upload
|
|
14
|
+
* limit (`PLACE_UPLOAD_MAX_BYTES`), and under the 100 MB Roblox documents as
|
|
15
|
+
* supported, since split places run in parallel and cost little.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_MAX_CHUNK_BYTES = 90 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
/** One combined place: the packages that are uploaded and executed together. */
|
|
20
|
+
export interface CombinedPlaceChunk {
|
|
21
|
+
/** Absolute path to this chunk's combined .rbxl file. */
|
|
13
22
|
rbxlPath: string;
|
|
14
|
-
/** batchTarget.name → ServerScriptService slug
|
|
23
|
+
/** batchTarget.name → ServerScriptService slug, for this chunk's packages only. */
|
|
24
|
+
slugMap: Map<string, string>;
|
|
25
|
+
/** Size on disk — recompressed whenever the batch has a size budget. */
|
|
26
|
+
sizeBytes: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CombinedProjectResult {
|
|
30
|
+
/**
|
|
31
|
+
* The combined places, in package order. Usually one; more when even
|
|
32
|
+
* recompressed the packages together exceed the per-upload budget.
|
|
33
|
+
*/
|
|
34
|
+
chunks: CombinedPlaceChunk[];
|
|
35
|
+
/** batchTarget.name → ServerScriptService slug mapping, across every chunk. */
|
|
15
36
|
slugMap: Map<string, string>;
|
|
16
37
|
/** First batch target's deploy target (provides placeId/universeId for upload). */
|
|
17
38
|
primaryTarget: DeployTarget;
|
|
@@ -35,26 +56,36 @@ export interface CombinedBuildProgress {
|
|
|
35
56
|
|
|
36
57
|
/** Per-package build info collected during phase 1. */
|
|
37
58
|
interface PackageBuildInfo {
|
|
59
|
+
name: string;
|
|
38
60
|
slug: string;
|
|
39
61
|
rbxlPath: string;
|
|
40
62
|
scriptPath: string;
|
|
63
|
+
sizeBytes: number;
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
/**
|
|
44
|
-
* Build
|
|
67
|
+
* Build combined .rbxl files containing all testable packages.
|
|
45
68
|
*
|
|
46
69
|
* Two-phase approach to avoid rojo's symlink deduplication:
|
|
47
70
|
* Phase 1: Build each package's .rbxl individually via rojo
|
|
48
|
-
* Phase 2: Use Lune to merge the individual builds into
|
|
71
|
+
* Phase 2: Use Lune to merge the individual builds into one place, split
|
|
72
|
+
* into more only if it measures over `maxChunkBytes`
|
|
49
73
|
*/
|
|
50
74
|
export async function generateCombinedProjectAsync(options: {
|
|
51
75
|
batchTargets: BatchTarget[];
|
|
52
76
|
repoRoot: string;
|
|
53
77
|
batchPlaceId?: number;
|
|
54
78
|
batchUniverseId?: number;
|
|
79
|
+
maxChunkBytes?: number;
|
|
55
80
|
progress?: CombinedBuildProgress;
|
|
56
81
|
}): Promise<CombinedProjectResult> {
|
|
57
|
-
const {
|
|
82
|
+
const {
|
|
83
|
+
batchTargets,
|
|
84
|
+
batchPlaceId,
|
|
85
|
+
batchUniverseId,
|
|
86
|
+
maxChunkBytes = DEFAULT_MAX_CHUNK_BYTES,
|
|
87
|
+
progress,
|
|
88
|
+
} = options;
|
|
58
89
|
|
|
59
90
|
if (batchTargets.length === 0) {
|
|
60
91
|
throw new Error(
|
|
@@ -116,50 +147,187 @@ export async function generateCombinedProjectAsync(options: {
|
|
|
116
147
|
}
|
|
117
148
|
const scriptPath = path.resolve(buildTarget.path, target.scriptTemplate);
|
|
118
149
|
|
|
119
|
-
|
|
150
|
+
const { size: sizeBytes } = await fs.stat(rbxlPath);
|
|
151
|
+
|
|
152
|
+
builds.push({
|
|
153
|
+
name: buildTarget.name,
|
|
154
|
+
slug,
|
|
155
|
+
rbxlPath,
|
|
156
|
+
scriptPath,
|
|
157
|
+
sizeBytes,
|
|
158
|
+
});
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
// ── Phase 2: Merge via Lune ──
|
|
123
162
|
|
|
124
|
-
const combinedRbxlPath = buildContext.resolvePath('batch-test.rbxl');
|
|
125
163
|
const luneScriptPath = resolvePackagePath(
|
|
126
164
|
import.meta.url,
|
|
127
165
|
'build-scripts',
|
|
128
166
|
'combine-test-places.lua'
|
|
129
167
|
);
|
|
130
168
|
|
|
131
|
-
// Build args: <outputPath> <slug1> <rbxl1> <script1> <slug2> <rbxl2> <script2> ...
|
|
132
|
-
const luneArgs: string[] = [combinedRbxlPath];
|
|
133
|
-
for (const build of builds) {
|
|
134
|
-
luneArgs.push(build.slug, build.rbxlPath, build.scriptPath);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
OutputHelper.verbose(
|
|
138
|
-
`Merging ${builds.length} targets into combined .rbxl...`
|
|
139
|
-
);
|
|
140
169
|
progress?.onCombineStart?.();
|
|
141
|
-
progress?.onStepProgress?.({
|
|
142
|
-
kind: 'steps',
|
|
143
|
-
completed: 0,
|
|
144
|
-
total: builds.length,
|
|
145
|
-
});
|
|
146
|
-
await buildContext.executeLuneTransformScriptAsync(
|
|
147
|
-
luneScriptPath,
|
|
148
|
-
...luneArgs
|
|
149
|
-
);
|
|
150
170
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
171
|
+
// One place first, split only if it measures over budget. Sizes do not add
|
|
172
|
+
// up: recompression dedupes the modules the packages share, so a merge of
|
|
173
|
+
// many packages costs little more than one, and only measuring it says so.
|
|
174
|
+
const totalBytes = builds.reduce((sum, b) => sum + b.sizeBytes, 0);
|
|
175
|
+
let groups = [builds];
|
|
176
|
+
let chunks: CombinedPlaceChunk[];
|
|
177
|
+
for (;;) {
|
|
178
|
+
chunks = await _mergeGroupsAsync(
|
|
179
|
+
buildContext,
|
|
180
|
+
luneScriptPath,
|
|
181
|
+
groups,
|
|
182
|
+
Number.isFinite(maxChunkBytes),
|
|
183
|
+
progress
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const largest = Math.max(...chunks.map((c) => c.sizeBytes));
|
|
187
|
+
if (largest <= maxChunkBytes || groups.length === builds.length) {
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const groupCount = Math.max(
|
|
192
|
+
groups.length + 1,
|
|
193
|
+
Math.ceil((groups.length * largest) / maxChunkBytes)
|
|
194
|
+
);
|
|
195
|
+
OutputHelper.info(
|
|
196
|
+
`Combined place${groups.length === 1 ? ' is' : 's reach'} ` +
|
|
197
|
+
`${formatMegabytes(largest)}, over the ${formatMegabytes(
|
|
198
|
+
maxChunkBytes
|
|
199
|
+
)} per-upload budget — splitting ${builds.length} targets into ` +
|
|
200
|
+
`about ${groupCount} places.`
|
|
201
|
+
);
|
|
202
|
+
groups = partitionBySize(
|
|
203
|
+
builds,
|
|
204
|
+
(b) => b.sizeBytes,
|
|
205
|
+
totalBytes / groupCount
|
|
206
|
+
);
|
|
207
|
+
}
|
|
154
208
|
|
|
155
209
|
return {
|
|
156
|
-
|
|
210
|
+
chunks,
|
|
157
211
|
slugMap,
|
|
158
212
|
primaryTarget,
|
|
159
213
|
buildContext,
|
|
160
214
|
};
|
|
161
215
|
}
|
|
162
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Merge each group into its own place, recompressed when it will be measured
|
|
219
|
+
* against a budget — which is also the form it is uploaded in, so the upload
|
|
220
|
+
* has nothing left to do.
|
|
221
|
+
*/
|
|
222
|
+
async function _mergeGroupsAsync(
|
|
223
|
+
buildContext: BuildContext,
|
|
224
|
+
luneScriptPath: string,
|
|
225
|
+
groups: PackageBuildInfo[][],
|
|
226
|
+
recompress: boolean,
|
|
227
|
+
progress: CombinedBuildProgress | undefined
|
|
228
|
+
): Promise<CombinedPlaceChunk[]> {
|
|
229
|
+
progress?.onStepProgress?.({
|
|
230
|
+
kind: 'steps',
|
|
231
|
+
completed: 0,
|
|
232
|
+
total: groups.length,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const chunks: CombinedPlaceChunk[] = [];
|
|
236
|
+
for (const [index, group] of groups.entries()) {
|
|
237
|
+
const combinedRbxlPath = buildContext.resolvePath(
|
|
238
|
+
groups.length === 1 ? 'batch-test.rbxl' : `batch-test-${index + 1}.rbxl`
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Build args: <outputPath> <slug1> <rbxl1> <script1> <slug2> <rbxl2> <script2> ...
|
|
242
|
+
const luneArgs: string[] = [combinedRbxlPath];
|
|
243
|
+
for (const build of group) {
|
|
244
|
+
luneArgs.push(build.slug, build.rbxlPath, build.scriptPath);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
OutputHelper.verbose(
|
|
248
|
+
`Merging ${group.length} targets into ${path.basename(
|
|
249
|
+
combinedRbxlPath
|
|
250
|
+
)}...`
|
|
251
|
+
);
|
|
252
|
+
await buildContext.executeLuneTransformScriptAsync(
|
|
253
|
+
luneScriptPath,
|
|
254
|
+
...luneArgs
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
let sizeBytes: number;
|
|
258
|
+
if (recompress) {
|
|
259
|
+
const merged = await fs.readFile(combinedRbxlPath);
|
|
260
|
+
const result = recompressPlace(merged);
|
|
261
|
+
if (result.kind === 'recompressed') {
|
|
262
|
+
await fs.writeFile(combinedRbxlPath, result.data);
|
|
263
|
+
}
|
|
264
|
+
sizeBytes = result.data.length;
|
|
265
|
+
OutputHelper.verbose(
|
|
266
|
+
`Combined ${group.length} targets at ${combinedRbxlPath} ` +
|
|
267
|
+
`(${formatMegabytes(merged.length)} as built, ` +
|
|
268
|
+
`${formatMegabytes(sizeBytes)} to upload)`
|
|
269
|
+
);
|
|
270
|
+
} else {
|
|
271
|
+
({ size: sizeBytes } = await fs.stat(combinedRbxlPath));
|
|
272
|
+
OutputHelper.verbose(
|
|
273
|
+
`Combined ${
|
|
274
|
+
group.length
|
|
275
|
+
} targets at ${combinedRbxlPath} (${formatMegabytes(sizeBytes)})`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
chunks.push({
|
|
280
|
+
rbxlPath: combinedRbxlPath,
|
|
281
|
+
slugMap: new Map(group.map((b) => [b.name, b.slug])),
|
|
282
|
+
sizeBytes,
|
|
283
|
+
});
|
|
284
|
+
progress?.onStepProgress?.({
|
|
285
|
+
kind: 'steps',
|
|
286
|
+
completed: index + 1,
|
|
287
|
+
total: groups.length,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return chunks;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Split items, in order, into consecutive groups whose summed size stays within
|
|
296
|
+
* `maxBytes`. An item larger than the budget on its own gets a group to itself
|
|
297
|
+
* rather than being dropped — the upload is what reports it as too big.
|
|
298
|
+
*/
|
|
299
|
+
export function partitionBySize<T>(
|
|
300
|
+
items: T[],
|
|
301
|
+
sizeOf: (item: T) => number,
|
|
302
|
+
maxBytes: number
|
|
303
|
+
): T[][] {
|
|
304
|
+
const groups: T[][] = [];
|
|
305
|
+
let current: T[] = [];
|
|
306
|
+
let currentBytes = 0;
|
|
307
|
+
|
|
308
|
+
for (const item of items) {
|
|
309
|
+
const size = sizeOf(item);
|
|
310
|
+
if (current.length > 0 && currentBytes + size > maxBytes) {
|
|
311
|
+
groups.push(current);
|
|
312
|
+
current = [];
|
|
313
|
+
currentBytes = 0;
|
|
314
|
+
}
|
|
315
|
+
current.push(item);
|
|
316
|
+
currentBytes += size;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (current.length > 0) {
|
|
320
|
+
groups.push(current);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return groups;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** In Roblox's MB, which are 1024 * 1024 bytes. */
|
|
327
|
+
export function formatMegabytes(bytes: number): string {
|
|
328
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
329
|
+
}
|
|
330
|
+
|
|
163
331
|
/**
|
|
164
332
|
* Extract the slug from a test rojo project.
|
|
165
333
|
* The slug is the first non-`$`, non-`Script` key under ServerScriptService.
|