@quenty/nevermore-cli 4.45.0 → 4.47.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 +16 -0
- package/dist/commands/deploy-command/asset-command.d.ts +44 -0
- package/dist/commands/deploy-command/asset-command.d.ts.map +1 -0
- package/dist/commands/deploy-command/asset-command.js +182 -0
- package/dist/commands/deploy-command/asset-command.js.map +1 -0
- package/dist/commands/deploy-command/index.d.ts.map +1 -1
- package/dist/commands/deploy-command/index.js +62 -0
- package/dist/commands/deploy-command/index.js.map +1 -1
- package/dist/commands/init-command/init-game-command.js +1 -1
- package/dist/commands/init-command/init-game-command.js.map +1 -1
- package/dist/commands/init-command/init-package-command.d.ts.map +1 -1
- package/dist/commands/init-command/init-package-command.js +2 -1
- package/dist/commands/init-command/init-package-command.js.map +1 -1
- package/dist/commands/init-command/init-plugin-command.js +1 -1
- package/dist/commands/init-command/init-plugin-command.js.map +1 -1
- package/dist/utils/assets/asset-creator.d.ts +32 -0
- package/dist/utils/assets/asset-creator.d.ts.map +1 -0
- package/dist/utils/assets/asset-creator.js +62 -0
- package/dist/utils/assets/asset-creator.js.map +1 -0
- package/dist/utils/assets/asset-creator.test.d.ts +2 -0
- package/dist/utils/assets/asset-creator.test.d.ts.map +1 -0
- package/dist/utils/assets/asset-creator.test.js +71 -0
- package/dist/utils/assets/asset-creator.test.js.map +1 -0
- package/dist/utils/assets/asset-formats.d.ts +28 -0
- package/dist/utils/assets/asset-formats.d.ts.map +1 -0
- package/dist/utils/assets/asset-formats.js +72 -0
- package/dist/utils/assets/asset-formats.js.map +1 -0
- package/dist/utils/assets/asset-formats.test.d.ts +2 -0
- package/dist/utils/assets/asset-formats.test.d.ts.map +1 -0
- package/dist/utils/assets/asset-formats.test.js +43 -0
- package/dist/utils/assets/asset-formats.test.js.map +1 -0
- package/dist/utils/assets/asset-upload-plan.d.ts +27 -0
- package/dist/utils/assets/asset-upload-plan.d.ts.map +1 -0
- package/dist/utils/assets/asset-upload-plan.js +43 -0
- package/dist/utils/assets/asset-upload-plan.js.map +1 -0
- package/dist/utils/assets/asset-upload-plan.test.d.ts +2 -0
- package/dist/utils/assets/asset-upload-plan.test.d.ts.map +1 -0
- package/dist/utils/assets/asset-upload-plan.test.js +57 -0
- package/dist/utils/assets/asset-upload-plan.test.js.map +1 -0
- package/dist/utils/open-cloud/open-cloud-client.d.ts +77 -0
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +166 -6
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.test.js +258 -0
- package/dist/utils/open-cloud/open-cloud-client.test.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/deploy-command/asset-command.ts +279 -0
- package/src/commands/deploy-command/index.ts +85 -0
- package/src/commands/init-command/init-game-command.ts +1 -1
- package/src/commands/init-command/init-package-command.ts +2 -1
- package/src/commands/init-command/init-plugin-command.ts +1 -1
- package/src/utils/assets/asset-creator.test.ts +106 -0
- package/src/utils/assets/asset-creator.ts +96 -0
- package/src/utils/assets/asset-formats.test.ts +57 -0
- package/src/utils/assets/asset-formats.ts +102 -0
- package/src/utils/assets/asset-upload-plan.test.ts +75 -0
- package/src/utils/assets/asset-upload-plan.ts +79 -0
- package/src/utils/open-cloud/open-cloud-client.test.ts +361 -0
- package/src/utils/open-cloud/open-cloud-client.ts +316 -12
- package/templates/game-template/aftman.toml +1 -1
- package/templates/nevermore-library-package-template/test/scripts/Server/ServerMain.server.lua +1 -1
- package/templates/nevermore-service-package-template/test/scripts/Server/ServerMain.server.lua +1 -1
- package/templates/plugin-template/aftman.toml +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function formatAssetCreator(creator) {
|
|
2
|
+
return 'userId' in creator
|
|
3
|
+
? `user ${creator.userId}`
|
|
4
|
+
: `group ${creator.groupId}`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Decide who owns newly uploaded assets, so a bare
|
|
8
|
+
* `nevermore deploy asset upload icon.png` works.
|
|
9
|
+
*
|
|
10
|
+
* An explicit flag wins. Otherwise the owner of the surrounding project's
|
|
11
|
+
* universe is preferred over the API key's owner: an experience can only be
|
|
12
|
+
* relied on to load assets its own creator owns, so in a group's game the
|
|
13
|
+
* person holding the key is the wrong owner.
|
|
14
|
+
*/
|
|
15
|
+
export async function resolveAssetCreatorAsync(options) {
|
|
16
|
+
const { userId, groupId, universeId, source } = options;
|
|
17
|
+
if (userId != null && groupId != null) {
|
|
18
|
+
throw new Error('Pass --user-id or --group-id, not both.');
|
|
19
|
+
}
|
|
20
|
+
for (const [flag, id] of [
|
|
21
|
+
['--user-id', userId],
|
|
22
|
+
['--group-id', groupId],
|
|
23
|
+
]) {
|
|
24
|
+
if (id != null && (!Number.isSafeInteger(id) || id < 1)) {
|
|
25
|
+
throw new Error(`${flag} must be a positive integer.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (userId != null) {
|
|
29
|
+
return { creator: { userId }, reason: '--user-id' };
|
|
30
|
+
}
|
|
31
|
+
if (groupId != null) {
|
|
32
|
+
return { creator: { groupId }, reason: '--group-id' };
|
|
33
|
+
}
|
|
34
|
+
if (universeId != null) {
|
|
35
|
+
try {
|
|
36
|
+
return {
|
|
37
|
+
creator: await source.getUniverseOwnerAsync(universeId),
|
|
38
|
+
reason: `owns universe ${universeId}, from deploy.nevermore.json`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
options.onFallback?.(`Could not read who owns universe ${universeId} ` +
|
|
43
|
+
`(${err instanceof Error ? err.message : String(err)}); ` +
|
|
44
|
+
`uploading as the API key's owner instead.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return {
|
|
49
|
+
creator: { userId: await source.getApiKeyUserIdAsync() },
|
|
50
|
+
reason: 'owns the API key',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
throw new Error([
|
|
55
|
+
`Could not work out who should own the new assets: ${err instanceof Error ? err.message : String(err)}`,
|
|
56
|
+
'Check the key with "nevermore login --status", or name the owner:',
|
|
57
|
+
' --user-id <id> the Roblox user who should own them',
|
|
58
|
+
' --group-id <id> the Roblox group who should own them',
|
|
59
|
+
].join('\n'));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=asset-creator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-creator.js","sourceRoot":"","sources":["../../../src/utils/assets/asset-creator.ts"],"names":[],"mappings":"AAwBA,MAAM,UAAU,kBAAkB,CAAC,OAAqB;IACtD,OAAO,QAAQ,IAAI,OAAO;QACxB,CAAC,CAAC,QAAQ,OAAO,CAAC,MAAM,EAAE;QAC1B,CAAC,CAAC,SAAS,OAAO,CAAC,OAAO,EAAE,CAAC;AACjC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,OAAmC;IAEnC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAExD,IAAI,MAAM,IAAI,IAAI,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI;QACvB,CAAC,WAAW,EAAE,MAAM,CAAC;QACrB,CAAC,YAAY,EAAE,OAAO,CAAC;KACf,EAAE,CAAC;QACX,IAAI,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,8BAA8B,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,OAAO,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IACtD,CAAC;IACD,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QACpB,OAAO,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IACxD,CAAC;IAED,IAAI,UAAU,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC;YACH,OAAO;gBACL,OAAO,EAAE,MAAM,MAAM,CAAC,qBAAqB,CAAC,UAAU,CAAC;gBACvD,MAAM,EAAE,iBAAiB,UAAU,8BAA8B;aAClE,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,UAAU,EAAE,CAClB,oCAAoC,UAAU,GAAG;gBAC/C,IAAI,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK;gBACzD,2CAA2C,CAC9C,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,CAAC;QACH,OAAO;YACL,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC,oBAAoB,EAAE,EAAE;YACxD,MAAM,EAAE,kBAAkB;SAC3B,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb;YACE,qDACE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CACjD,EAAE;YACF,mEAAmE;YACnE,yDAAyD;YACzD,0DAA0D;SAC3D,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-creator.test.d.ts","sourceRoot":"","sources":["../../../src/utils/assets/asset-creator.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { formatAssetCreator, resolveAssetCreatorAsync, } from './asset-creator.js';
|
|
3
|
+
function makeSource(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
getUniverseOwnerAsync: vi.fn(async () => ({ groupId: 77 })),
|
|
6
|
+
getApiKeyUserIdAsync: vi.fn(async () => 4397833),
|
|
7
|
+
...overrides,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
describe('resolveAssetCreatorAsync', () => {
|
|
11
|
+
it('takes an explicit owner without asking Roblox anything', async () => {
|
|
12
|
+
const source = makeSource();
|
|
13
|
+
expect(await resolveAssetCreatorAsync({ userId: 5, universeId: 1, source })).toEqual({ creator: { userId: 5 }, reason: '--user-id' });
|
|
14
|
+
expect(await resolveAssetCreatorAsync({ groupId: 6, universeId: 1, source })).toEqual({ creator: { groupId: 6 }, reason: '--group-id' });
|
|
15
|
+
expect(source.getUniverseOwnerAsync).not.toHaveBeenCalled();
|
|
16
|
+
expect(source.getApiKeyUserIdAsync).not.toHaveBeenCalled();
|
|
17
|
+
});
|
|
18
|
+
it('refuses both owner flags, and an id that is not one', async () => {
|
|
19
|
+
const source = makeSource();
|
|
20
|
+
await expect(resolveAssetCreatorAsync({ userId: 5, groupId: 6, source })).rejects.toThrowError(/--user-id or --group-id, not both/);
|
|
21
|
+
await expect(resolveAssetCreatorAsync({ userId: NaN, source })).rejects.toThrowError(/--user-id must be a positive integer/);
|
|
22
|
+
});
|
|
23
|
+
it("prefers the project universe's owner over the key's", async () => {
|
|
24
|
+
// In a group's game the person holding the key is the wrong owner: the
|
|
25
|
+
// experience loads assets its own creator owns.
|
|
26
|
+
const source = makeSource();
|
|
27
|
+
expect(await resolveAssetCreatorAsync({ universeId: 123, source })).toEqual({
|
|
28
|
+
creator: { groupId: 77 },
|
|
29
|
+
reason: 'owns universe 123, from deploy.nevermore.json',
|
|
30
|
+
});
|
|
31
|
+
expect(source.getApiKeyUserIdAsync).not.toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
it("uses the API key's owner outside a project", async () => {
|
|
34
|
+
const source = makeSource();
|
|
35
|
+
expect(await resolveAssetCreatorAsync({ source })).toEqual({
|
|
36
|
+
creator: { userId: 4397833 },
|
|
37
|
+
reason: 'owns the API key',
|
|
38
|
+
});
|
|
39
|
+
expect(source.getUniverseOwnerAsync).not.toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
it("falls back to the key's owner, out loud, when the universe is unreadable", async () => {
|
|
42
|
+
const onFallback = vi.fn();
|
|
43
|
+
const source = makeSource({
|
|
44
|
+
getUniverseOwnerAsync: vi.fn(async () => {
|
|
45
|
+
throw new Error('Read universe 123 failed: 403 Forbidden');
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
const resolved = await resolveAssetCreatorAsync({
|
|
49
|
+
universeId: 123,
|
|
50
|
+
source,
|
|
51
|
+
onFallback,
|
|
52
|
+
});
|
|
53
|
+
expect(resolved.creator).toEqual({ userId: 4397833 });
|
|
54
|
+
expect(onFallback).toHaveBeenCalledWith(expect.stringMatching(/universe 123 \(.*403 Forbidden\)/));
|
|
55
|
+
});
|
|
56
|
+
it('says which flags to pass when no owner can be inferred', async () => {
|
|
57
|
+
const source = makeSource({
|
|
58
|
+
getApiKeyUserIdAsync: vi.fn(async () => {
|
|
59
|
+
throw new Error('Read API key owner failed: 401 Unauthorized');
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
await expect(resolveAssetCreatorAsync({ source })).rejects.toThrowError(/401 Unauthorized[\s\S]*--user-id <id>[\s\S]*--group-id <id>/);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('formatAssetCreator', () => {
|
|
66
|
+
it('names the kind of owner', () => {
|
|
67
|
+
expect(formatAssetCreator({ userId: 5 })).toBe('user 5');
|
|
68
|
+
expect(formatAssetCreator({ groupId: 6 })).toBe('group 6');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=asset-creator.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-creator.test.js","sourceRoot":"","sources":["../../../src/utils/assets/asset-creator.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EACL,kBAAkB,EAClB,wBAAwB,GAEzB,MAAM,oBAAoB,CAAC;AAE5B,SAAS,UAAU,CACjB,YAAyC,EAAE;IAE3C,OAAO;QACL,qBAAqB,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;QAC3D,oBAAoB,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC;QAChD,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAE5B,MAAM,CACJ,MAAM,wBAAwB,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CACrE,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;QAC3D,MAAM,CACJ,MAAM,wBAAwB,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CACtE,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QAE7D,MAAM,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QAC5D,MAAM,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACnE,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAE5B,MAAM,MAAM,CACV,wBAAwB,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAC5D,CAAC,OAAO,CAAC,YAAY,CAAC,mCAAmC,CAAC,CAAC;QAC5D,MAAM,MAAM,CACV,wBAAwB,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAClD,CAAC,OAAO,CAAC,YAAY,CAAC,sCAAsC,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACnE,uEAAuE;QACvE,gDAAgD;QAChD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAE5B,MAAM,CAAC,MAAM,wBAAwB,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,OAAO,CACzE;YACE,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;YACxB,MAAM,EAAE,+CAA+C;SACxD,CACF,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAE5B,MAAM,CAAC,MAAM,wBAAwB,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;YACzD,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;YAC5B,MAAM,EAAE,kBAAkB;SAC3B,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0EAA0E,EAAE,KAAK,IAAI,EAAE;QACxF,MAAM,UAAU,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,UAAU,CAAC;YACxB,qBAAqB,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE;gBACtC,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;YAC7D,CAAC,CAAC;SACH,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,MAAM,wBAAwB,CAAC;YAC9C,UAAU,EAAE,GAAG;YACf,MAAM;YACN,UAAU;SACX,CAAC,CAAC;QAEH,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QACtD,MAAM,CAAC,UAAU,CAAC,CAAC,oBAAoB,CACrC,MAAM,CAAC,cAAc,CAAC,kCAAkC,CAAC,CAC1D,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,MAAM,MAAM,GAAG,UAAU,CAAC;YACxB,oBAAoB,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE;gBACrC,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;YACjE,CAAC,CAAC;SACH,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,wBAAwB,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CACrE,6DAA6D,CAC9D,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzD,MAAM,CAAC,kBAAkB,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `assetType` values `nevermore deploy asset upload` can send to the Open
|
|
3
|
+
* Cloud Assets API. Mesh is left out on purpose: the API only accepts mesh data
|
|
4
|
+
* that was itself downloaded from Roblox, which is not something a file on disk
|
|
5
|
+
* can be recognized as.
|
|
6
|
+
*
|
|
7
|
+
* https://create.roblox.com/docs/cloud/guides/usage-assets
|
|
8
|
+
*/
|
|
9
|
+
export declare const ASSET_TYPES: readonly ["Animation", "Audio", "Decal", "Image", "Model", "Video"];
|
|
10
|
+
export type AssetType = typeof ASSET_TYPES[number];
|
|
11
|
+
export interface ResolvedAssetFormat {
|
|
12
|
+
assetType: AssetType;
|
|
13
|
+
contentType: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Work out how a file uploads: its asset type and the content type to send.
|
|
17
|
+
* The extension decides both unless `explicitType` picks one of the
|
|
18
|
+
* alternatives it allows (`Decal` for an image, `Animation` for an `.rbxm`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveAssetFormat(filePath: string, explicitType?: AssetType): ResolvedAssetFormat;
|
|
21
|
+
/**
|
|
22
|
+
* Whether an existing asset of this type can have its file replaced in place,
|
|
23
|
+
* keeping its id. The API documents this for models only, and marks images,
|
|
24
|
+
* audio and video "not available for updating" — so a changed image or sound
|
|
25
|
+
* is always a new asset with a new id.
|
|
26
|
+
*/
|
|
27
|
+
export declare function canUpdateAssetContent(assetType: AssetType): boolean;
|
|
28
|
+
//# sourceMappingURL=asset-formats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-formats.d.ts","sourceRoot":"","sources":["../../../src/utils/assets/asset-formats.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,qEAOd,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;AAoCnD,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,SAAS,GACvB,mBAAmB,CAuBrB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAEnE"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
/**
|
|
3
|
+
* The `assetType` values `nevermore deploy asset upload` can send to the Open
|
|
4
|
+
* Cloud Assets API. Mesh is left out on purpose: the API only accepts mesh data
|
|
5
|
+
* that was itself downloaded from Roblox, which is not something a file on disk
|
|
6
|
+
* can be recognized as.
|
|
7
|
+
*
|
|
8
|
+
* https://create.roblox.com/docs/cloud/guides/usage-assets
|
|
9
|
+
*/
|
|
10
|
+
export const ASSET_TYPES = [
|
|
11
|
+
'Animation',
|
|
12
|
+
'Audio',
|
|
13
|
+
'Decal',
|
|
14
|
+
'Image',
|
|
15
|
+
'Model',
|
|
16
|
+
'Video',
|
|
17
|
+
];
|
|
18
|
+
// An image defaults to Image rather than Decal: an Image id is usable directly
|
|
19
|
+
// as `rbxassetid://`, where a Decal is a wrapper whose id has to be resolved to
|
|
20
|
+
// the image inside it before an ImageLabel can show it.
|
|
21
|
+
const IMAGE_TYPES = ['Image', 'Decal'];
|
|
22
|
+
// A Roblox model file holds either, and nothing in its name says which.
|
|
23
|
+
const ROBLOX_MODEL_TYPES = ['Model', 'Animation'];
|
|
24
|
+
const ASSET_FILE_FORMATS = {
|
|
25
|
+
'.mp3': { contentType: 'audio/mpeg', assetTypes: ['Audio'] },
|
|
26
|
+
'.ogg': { contentType: 'audio/ogg', assetTypes: ['Audio'] },
|
|
27
|
+
'.wav': { contentType: 'audio/wav', assetTypes: ['Audio'] },
|
|
28
|
+
'.flac': { contentType: 'audio/flac', assetTypes: ['Audio'] },
|
|
29
|
+
'.png': { contentType: 'image/png', assetTypes: IMAGE_TYPES },
|
|
30
|
+
'.jpg': { contentType: 'image/jpeg', assetTypes: IMAGE_TYPES },
|
|
31
|
+
'.jpeg': { contentType: 'image/jpeg', assetTypes: IMAGE_TYPES },
|
|
32
|
+
'.bmp': { contentType: 'image/bmp', assetTypes: IMAGE_TYPES },
|
|
33
|
+
'.tga': { contentType: 'image/tga', assetTypes: IMAGE_TYPES },
|
|
34
|
+
'.fbx': { contentType: 'model/fbx', assetTypes: ['Model'] },
|
|
35
|
+
'.gltf': { contentType: 'model/gltf+json', assetTypes: ['Model'] },
|
|
36
|
+
'.glb': { contentType: 'model/gltf-binary', assetTypes: ['Model'] },
|
|
37
|
+
'.rbxm': { contentType: 'model/x-rbxm', assetTypes: ROBLOX_MODEL_TYPES },
|
|
38
|
+
'.rbxmx': { contentType: 'model/x-rbxm', assetTypes: ROBLOX_MODEL_TYPES },
|
|
39
|
+
'.mp4': { contentType: 'video/mp4', assetTypes: ['Video'] },
|
|
40
|
+
'.mov': { contentType: 'video/mov', assetTypes: ['Video'] },
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Work out how a file uploads: its asset type and the content type to send.
|
|
44
|
+
* The extension decides both unless `explicitType` picks one of the
|
|
45
|
+
* alternatives it allows (`Decal` for an image, `Animation` for an `.rbxm`).
|
|
46
|
+
*/
|
|
47
|
+
export function resolveAssetFormat(filePath, explicitType) {
|
|
48
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
49
|
+
const format = ASSET_FILE_FORMATS[extension];
|
|
50
|
+
if (!format) {
|
|
51
|
+
throw new Error(`Cannot upload "${filePath}": ${extension === '' ? 'a file with no extension' : `"${extension}"`} is not an asset format the Open Cloud Assets API accepts.\n` +
|
|
52
|
+
`Supported: ${Object.keys(ASSET_FILE_FORMATS).join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
if (explicitType != null && !format.assetTypes.includes(explicitType)) {
|
|
55
|
+
throw new Error(`Cannot upload "${filePath}" as ${explicitType}: ` +
|
|
56
|
+
`a "${extension}" file uploads as ${format.assetTypes.join(' or ')}.`);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
assetType: explicitType ?? format.assetTypes[0],
|
|
60
|
+
contentType: format.contentType,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether an existing asset of this type can have its file replaced in place,
|
|
65
|
+
* keeping its id. The API documents this for models only, and marks images,
|
|
66
|
+
* audio and video "not available for updating" — so a changed image or sound
|
|
67
|
+
* is always a new asset with a new id.
|
|
68
|
+
*/
|
|
69
|
+
export function canUpdateAssetContent(assetType) {
|
|
70
|
+
return assetType === 'Model';
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=asset-formats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-formats.js","sourceRoot":"","sources":["../../../src/utils/assets/asset-formats.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-formats.test.d.ts","sourceRoot":"","sources":["../../../src/utils/assets/asset-formats.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { canUpdateAssetContent, resolveAssetFormat } from './asset-formats.js';
|
|
3
|
+
describe('resolveAssetFormat', () => {
|
|
4
|
+
it('reads the asset type and content type off the extension', () => {
|
|
5
|
+
expect(resolveAssetFormat('models/City.fbx')).toEqual({
|
|
6
|
+
assetType: 'Model',
|
|
7
|
+
contentType: 'model/fbx',
|
|
8
|
+
});
|
|
9
|
+
expect(resolveAssetFormat('sfx/click.ogg')).toEqual({
|
|
10
|
+
assetType: 'Audio',
|
|
11
|
+
contentType: 'audio/ogg',
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
it('ignores the case of the extension', () => {
|
|
15
|
+
expect(resolveAssetFormat('ICON.PNG').contentType).toBe('image/png');
|
|
16
|
+
});
|
|
17
|
+
it('uploads an image as an Image unless told Decal', () => {
|
|
18
|
+
expect(resolveAssetFormat('icon.png').assetType).toBe('Image');
|
|
19
|
+
expect(resolveAssetFormat('icon.png', 'Decal')).toEqual({
|
|
20
|
+
assetType: 'Decal',
|
|
21
|
+
contentType: 'image/png',
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
it('uploads an .rbxm as a Model unless told Animation', () => {
|
|
25
|
+
expect(resolveAssetFormat('rig.rbxm').assetType).toBe('Model');
|
|
26
|
+
expect(resolveAssetFormat('walk.rbxm', 'Animation').assetType).toBe('Animation');
|
|
27
|
+
});
|
|
28
|
+
it('refuses a type the extension cannot upload as', () => {
|
|
29
|
+
expect(() => resolveAssetFormat('icon.png', 'Audio')).toThrowError(/a "\.png" file uploads as Image or Decal/);
|
|
30
|
+
});
|
|
31
|
+
it('refuses an extension the API does not accept, naming the ones it does', () => {
|
|
32
|
+
expect(() => resolveAssetFormat('notes.txt')).toThrowError(/"\.txt" is not an asset format[\s\S]*\.fbx/);
|
|
33
|
+
expect(() => resolveAssetFormat('README')).toThrowError(/a file with no extension/);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('canUpdateAssetContent', () => {
|
|
37
|
+
it('is only true for models', () => {
|
|
38
|
+
expect(canUpdateAssetContent('Model')).toBe(true);
|
|
39
|
+
expect(canUpdateAssetContent('Image')).toBe(false);
|
|
40
|
+
expect(canUpdateAssetContent('Audio')).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=asset-formats.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-formats.test.js","sourceRoot":"","sources":["../../../src/utils/assets/asset-formats.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE/E,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC;YACpD,SAAS,EAAE,OAAO;YAClB,WAAW,EAAE,WAAW;SACzB,CAAC,CAAC;QACH,MAAM,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC;YAClD,SAAS,EAAE,OAAO;YAClB,WAAW,EAAE,WAAW;SACzB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/D,MAAM,CAAC,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;YACtD,SAAS,EAAE,OAAO;YAClB,WAAW,EAAE,WAAW;SACzB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/D,MAAM,CAAC,kBAAkB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CACjE,WAAW,CACZ,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,YAAY,CAChE,0CAA0C,CAC3C,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CACxD,4CAA4C,CAC7C,CAAC;QACF,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CACrD,0BAA0B,CAC3B,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClD,MAAM,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnD,MAAM,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type AssetType } from './asset-formats.js';
|
|
2
|
+
export interface AssetUploadPlanOptions {
|
|
3
|
+
files: string[];
|
|
4
|
+
/** Applied to every file; each must be able to upload as it. */
|
|
5
|
+
type?: AssetType;
|
|
6
|
+
/** Display name. Single file only — defaults to the file's base name. */
|
|
7
|
+
name?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Replace this asset's content instead of creating one. Single file only. */
|
|
10
|
+
assetId?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface AssetUploadPlanEntry {
|
|
13
|
+
filePath: string;
|
|
14
|
+
assetType: AssetType;
|
|
15
|
+
contentType: string;
|
|
16
|
+
displayName: string;
|
|
17
|
+
description: string;
|
|
18
|
+
/** Set when the upload replaces an existing asset's content in place. */
|
|
19
|
+
assetId?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Turn the command line into one upload per file, rejecting anything that
|
|
23
|
+
* could not work before a single request is sent — a batch that fails on its
|
|
24
|
+
* fifth file has already created four assets.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildAssetUploadPlan(options: AssetUploadPlanOptions): AssetUploadPlanEntry[];
|
|
27
|
+
//# sourceMappingURL=asset-upload-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-upload-plan.d.ts","sourceRoot":"","sources":["../../../src/utils/assets/asset-upload-plan.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,gEAAgE;IAChE,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,sBAAsB,GAC9B,oBAAoB,EAAE,CA2CxB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { canUpdateAssetContent, resolveAssetFormat, } from './asset-formats.js';
|
|
3
|
+
/**
|
|
4
|
+
* Turn the command line into one upload per file, rejecting anything that
|
|
5
|
+
* could not work before a single request is sent — a batch that fails on its
|
|
6
|
+
* fifth file has already created four assets.
|
|
7
|
+
*/
|
|
8
|
+
export function buildAssetUploadPlan(options) {
|
|
9
|
+
const { files, type, name, description, assetId } = options;
|
|
10
|
+
if (files.length === 0) {
|
|
11
|
+
throw new Error('No files to upload.');
|
|
12
|
+
}
|
|
13
|
+
if (files.length > 1 && name != null) {
|
|
14
|
+
throw new Error(`--name names one asset, but ${files.length} files were given. ` +
|
|
15
|
+
`Drop it to name each asset after its file.`);
|
|
16
|
+
}
|
|
17
|
+
if (files.length > 1 && assetId != null) {
|
|
18
|
+
throw new Error(`--asset-id replaces one asset, but ${files.length} files were given.`);
|
|
19
|
+
}
|
|
20
|
+
if (assetId != null && (!Number.isSafeInteger(assetId) || assetId < 1)) {
|
|
21
|
+
throw new Error('--asset-id must be a positive integer.');
|
|
22
|
+
}
|
|
23
|
+
return files.map((filePath) => {
|
|
24
|
+
const { assetType, contentType } = resolveAssetFormat(filePath, type);
|
|
25
|
+
if (assetId != null && !canUpdateAssetContent(assetType)) {
|
|
26
|
+
throw new Error(`Cannot replace asset ${assetId} with "${filePath}": the Assets API ` +
|
|
27
|
+
`only replaces the content of a Model. Upload the ${assetType} ` +
|
|
28
|
+
`without --asset-id to create a new asset instead.`);
|
|
29
|
+
}
|
|
30
|
+
const displayName = name ?? path.basename(filePath, path.extname(filePath));
|
|
31
|
+
return {
|
|
32
|
+
filePath,
|
|
33
|
+
assetType,
|
|
34
|
+
contentType,
|
|
35
|
+
displayName,
|
|
36
|
+
// The API requires a description on create, so an omitted one falls back
|
|
37
|
+
// to the name rather than to text nobody chose.
|
|
38
|
+
description: description ?? displayName,
|
|
39
|
+
assetId,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=asset-upload-plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-upload-plan.js","sourceRoot":"","sources":["../../../src/utils/assets/asset-upload-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EACL,qBAAqB,EACrB,kBAAkB,GAEnB,MAAM,oBAAoB,CAAC;AAuB5B;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAA+B;IAE/B,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAE5D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACzC,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,+BAA+B,KAAK,CAAC,MAAM,qBAAqB;YAC9D,4CAA4C,CAC/C,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CACb,sCAAsC,KAAK,CAAC,MAAM,oBAAoB,CACvE,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,IAAI,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;QAC5B,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACtE,IAAI,OAAO,IAAI,IAAI,IAAI,CAAC,qBAAqB,CAAC,SAAS,CAAC,EAAE,CAAC;YACzD,MAAM,IAAI,KAAK,CACb,wBAAwB,OAAO,UAAU,QAAQ,oBAAoB;gBACnE,oDAAoD,SAAS,GAAG;gBAChE,mDAAmD,CACtD,CAAC;QACJ,CAAC;QAED,MAAM,WAAW,GAAG,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC5E,OAAO;YACL,QAAQ;YACR,SAAS;YACT,WAAW;YACX,WAAW;YACX,yEAAyE;YACzE,gDAAgD;YAChD,WAAW,EAAE,WAAW,IAAI,WAAW;YACvC,OAAO;SACR,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-upload-plan.test.d.ts","sourceRoot":"","sources":["../../../src/utils/assets/asset-upload-plan.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildAssetUploadPlan } from './asset-upload-plan.js';
|
|
3
|
+
describe('buildAssetUploadPlan', () => {
|
|
4
|
+
it('names each asset after its file, and describes it by its name', () => {
|
|
5
|
+
expect(buildAssetUploadPlan({ files: ['art/icons_1.png', 'models/City.fbx'] })).toEqual([
|
|
6
|
+
{
|
|
7
|
+
filePath: 'art/icons_1.png',
|
|
8
|
+
assetType: 'Image',
|
|
9
|
+
contentType: 'image/png',
|
|
10
|
+
displayName: 'icons_1',
|
|
11
|
+
description: 'icons_1',
|
|
12
|
+
assetId: undefined,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
filePath: 'models/City.fbx',
|
|
16
|
+
assetType: 'Model',
|
|
17
|
+
contentType: 'model/fbx',
|
|
18
|
+
displayName: 'City',
|
|
19
|
+
description: 'City',
|
|
20
|
+
assetId: undefined,
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
it('takes an explicit name and description for a single file', () => {
|
|
25
|
+
const [entry] = buildAssetUploadPlan({
|
|
26
|
+
files: ['City.fbx'],
|
|
27
|
+
name: 'Openfront City',
|
|
28
|
+
description: 'Unit model',
|
|
29
|
+
});
|
|
30
|
+
expect(entry?.displayName).toBe('Openfront City');
|
|
31
|
+
expect(entry?.description).toBe('Unit model');
|
|
32
|
+
});
|
|
33
|
+
it('refuses one name for several files', () => {
|
|
34
|
+
expect(() => buildAssetUploadPlan({ files: ['a.png', 'b.png'], name: 'Icon' })).toThrowError(/--name names one asset, but 2 files were given/);
|
|
35
|
+
});
|
|
36
|
+
it('plans a replacement when given the id of an existing model', () => {
|
|
37
|
+
const [entry] = buildAssetUploadPlan({
|
|
38
|
+
files: ['City.fbx'],
|
|
39
|
+
assetId: 123,
|
|
40
|
+
});
|
|
41
|
+
expect(entry?.assetId).toBe(123);
|
|
42
|
+
});
|
|
43
|
+
it('refuses to replace anything but a model', () => {
|
|
44
|
+
// The API has no in-place update for these, so letting the request through
|
|
45
|
+
// would only trade a clear message for whatever the server says instead.
|
|
46
|
+
expect(() => buildAssetUploadPlan({ files: ['icon.png'], assetId: 123 })).toThrowError(/only replaces the content of a Model/);
|
|
47
|
+
});
|
|
48
|
+
it('refuses one asset id for several files', () => {
|
|
49
|
+
expect(() => buildAssetUploadPlan({ files: ['a.fbx', 'b.fbx'], assetId: 123 })).toThrowError(/--asset-id replaces one asset, but 2 files were given/);
|
|
50
|
+
});
|
|
51
|
+
it('rejects the whole batch when one file cannot be uploaded', () => {
|
|
52
|
+
// Nothing is uploaded until every file is known to be uploadable: a batch
|
|
53
|
+
// that fails partway has already created assets that cannot be taken back.
|
|
54
|
+
expect(() => buildAssetUploadPlan({ files: ['a.png', 'notes.txt'] })).toThrowError(/"\.txt" is not an asset format/);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
//# sourceMappingURL=asset-upload-plan.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-upload-plan.test.js","sourceRoot":"","sources":["../../../src/utils/assets/asset-upload-plan.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -70,6 +70,56 @@ export interface OpenCloudClientOptions {
|
|
|
70
70
|
* written in deploy.nevermore.json.
|
|
71
71
|
*/
|
|
72
72
|
export type PlaceVersionType = 'saved' | 'published';
|
|
73
|
+
/** Who owns a newly created asset. */
|
|
74
|
+
export type AssetCreator = {
|
|
75
|
+
userId: number;
|
|
76
|
+
} | {
|
|
77
|
+
groupId: number;
|
|
78
|
+
};
|
|
79
|
+
export interface AssetFile {
|
|
80
|
+
filePath: string;
|
|
81
|
+
/** MIME type of the file. The Assets API picks its importer from it. */
|
|
82
|
+
contentType: string;
|
|
83
|
+
}
|
|
84
|
+
export interface CreateAssetOptions extends AssetFile {
|
|
85
|
+
/** `Model`, `Image`, `Audio`, ... as the Assets API spells them. */
|
|
86
|
+
assetType: string;
|
|
87
|
+
displayName: string;
|
|
88
|
+
description: string;
|
|
89
|
+
creator: AssetCreator;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The long-running operation every asset upload returns. Creating an asset is
|
|
93
|
+
* not synchronous — the file is imported and moderated first — so the id only
|
|
94
|
+
* exists once the operation reports `done`.
|
|
95
|
+
*/
|
|
96
|
+
export interface AssetOperation {
|
|
97
|
+
/** `operations/{operationId}` */
|
|
98
|
+
path: string;
|
|
99
|
+
done?: boolean;
|
|
100
|
+
response?: {
|
|
101
|
+
/** A string in practice, though the API's own schema calls it an integer. */
|
|
102
|
+
assetId?: string | number;
|
|
103
|
+
revisionId?: string;
|
|
104
|
+
moderationResult?: {
|
|
105
|
+
moderationState?: string;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
error?: {
|
|
109
|
+
code?: number | string;
|
|
110
|
+
message?: string;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export interface UploadedAsset {
|
|
114
|
+
assetId: number;
|
|
115
|
+
revisionId?: string;
|
|
116
|
+
/**
|
|
117
|
+
* `Approved`, `Reviewing`, ... exactly as the API reports it — which is not
|
|
118
|
+
* the `MODERATION_STATE_APPROVED` spelling its documentation shows. Audio
|
|
119
|
+
* routinely finishes uploading while still `Reviewing`.
|
|
120
|
+
*/
|
|
121
|
+
moderationState?: string;
|
|
122
|
+
}
|
|
73
123
|
export declare class OpenCloudClient {
|
|
74
124
|
private _apiKey;
|
|
75
125
|
private _apiKeySource;
|
|
@@ -124,5 +174,32 @@ export declare class OpenCloudClient {
|
|
|
124
174
|
* is capped so a never-published place fails fast instead of paging forever.
|
|
125
175
|
*/
|
|
126
176
|
resolveLatestPlaceVersionAsync(universeId: number, placeId: number, versionType: PlaceVersionType): Promise<number>;
|
|
177
|
+
/**
|
|
178
|
+
* Start uploading a file as a new asset via the Open Cloud Assets API
|
|
179
|
+
* (`asset:read` and `asset:write`). Returns the operation to hand to
|
|
180
|
+
* {@link pollAssetOperationAsync}; the asset has no id until that finishes.
|
|
181
|
+
*/
|
|
182
|
+
createAssetAsync(options: CreateAssetOptions): Promise<AssetOperation>;
|
|
183
|
+
/**
|
|
184
|
+
* Start replacing an existing asset's file, keeping its id — the upload
|
|
185
|
+
* becomes a new version of the same asset. The API only does this for models.
|
|
186
|
+
*/
|
|
187
|
+
updateAssetContentAsync(assetId: number, file: AssetFile): Promise<AssetOperation>;
|
|
188
|
+
/**
|
|
189
|
+
* Wait for an asset upload to finish and return the asset it produced.
|
|
190
|
+
*
|
|
191
|
+
* Throws if the operation reports an error or outlives `timeoutMs`. A timeout
|
|
192
|
+
* is not a failed upload: Roblox may still finish creating the asset, which
|
|
193
|
+
* is why the message says to look before uploading again.
|
|
194
|
+
*/
|
|
195
|
+
pollAssetOperationAsync(operation: AssetOperation, timeoutMs?: number): Promise<UploadedAsset>;
|
|
196
|
+
/**
|
|
197
|
+
* Who owns a universe: the user or group its assets should belong to if the
|
|
198
|
+
* experience is to use them. Needs no scope beyond a valid key.
|
|
199
|
+
*/
|
|
200
|
+
getUniverseOwnerAsync(universeId: number): Promise<AssetCreator>;
|
|
201
|
+
/** The user the API key acts as — whoever created it. */
|
|
202
|
+
getApiKeyUserIdAsync(): Promise<number>;
|
|
203
|
+
private _sendAssetAsync;
|
|
127
204
|
}
|
|
128
205
|
//# sourceMappingURL=open-cloud-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"open-cloud-client.d.ts","sourceRoot":"","sources":["../../../src/utils/open-cloud/open-cloud-client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"open-cloud-client.d.ts","sourceRoot":"","sources":["../../../src/utils/open-cloud/open-cloud-client.ts"],"names":[],"mappings":"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"}
|