@kubohiroya/turbowarp-extension-loader 0.5.0 → 0.6.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/README.ja.md +8 -1
- package/README.md +9 -1
- package/dist/packager.js +4 -1
- package/dist/project-authoring.d.ts +10 -0
- package/dist/project-authoring.js +8 -3
- package/dist/project-catalog.d.ts +10 -0
- package/dist/project-catalog.js +8 -2
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -218,10 +218,17 @@ Manifest は `uri` を持てます。その作品をダウンロードできる
|
|
|
218
218
|
{"schemaVersion": 1, "uri": "https://example.test/editor/turbowarp-plus-projects/66/66aa/0193f0a1-2b3c-7d4e-8f01-2233445566aa.sb3", "extensions": []}
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
1つの作品に1つの URI
|
|
221
|
+
1つの作品に1つの URI。何度編集しても変わりません。拡張を足したり形式を変えたりすると Manifest は
|
|
222
|
+
組み直されますが、住所はその向こう側で保たれます。だから指紋ではなく住所なのです。指紋は保存
|
|
222
223
|
するたびに変わるので、ブラウザーのキャッシュも開く人の選択の記録も、触るたびに同じ作品を別名で
|
|
223
224
|
綴じてしまいます。住所は中身が変わっても変わりません。
|
|
224
225
|
|
|
226
|
+
`prepareCatalogExtensionChanges` と `prepareExtensionAddition` は `uri` を受け取ります。作品が
|
|
227
|
+
最初の住所を得るのも、作者が選んだ住所へ移るのも、この経路です。何も言わなければ作品が持って
|
|
228
|
+
いる住所がそのまま残り、`readProjectAuthoringState` がその住所を報告するので、ホストは初回保存
|
|
229
|
+
かどうかを決める前に見分けられます。割り当てるかどうかはホストの判断です。作者が住所を選んだ
|
|
230
|
+
のか、既定が必要な初回保存なのかを知っているのはホストだけだからです。
|
|
231
|
+
|
|
225
232
|
`./project-authoring` の `createProjectUUID` と `defaultProjectURI` が、作者が住所を選んで
|
|
226
233
|
いないときに与える住所を組みます。
|
|
227
234
|
|
package/README.md
CHANGED
|
@@ -220,11 +220,19 @@ identifies it.
|
|
|
220
220
|
{"schemaVersion": 1, "uri": "https://example.test/editor/turbowarp-plus-projects/66/66aa/0193f0a1-2b3c-7d4e-8f01-2233445566aa.sb3", "extensions": []}
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
One work, one URI, across every edit
|
|
223
|
+
One work, one URI, across every edit — including the edits that change its extensions, which
|
|
224
|
+
rebuild the manifest around it. That is why it is an address and not a fingerprint: a
|
|
224
225
|
fingerprint changes on every save, so a browser cache and a record of what the opener chose would
|
|
225
226
|
file one work under a new name each time it was touched. An address does not change when the bytes
|
|
226
227
|
do.
|
|
227
228
|
|
|
229
|
+
`prepareCatalogExtensionChanges` and `prepareExtensionAddition` take a `uri` option, which is how
|
|
230
|
+
a project gets its first address and how it moves to one the author chose. Saying nothing keeps
|
|
231
|
+
the address the project already has, and `readProjectAuthoringState` reports that address, so a
|
|
232
|
+
host can tell a first save from a later one before deciding. Whether to assign one is the host's
|
|
233
|
+
call: only the host knows whether the author chose an address or whether this is a first save
|
|
234
|
+
that needs a default.
|
|
235
|
+
|
|
228
236
|
`createProjectUUID` and `defaultProjectURI` in `./project-authoring` build the address a project
|
|
229
237
|
is given when its author has not chosen one:
|
|
230
238
|
|
package/dist/packager.js
CHANGED
|
@@ -81,7 +81,10 @@ export async function convertProjectSources(input, ports, { zip: Zip, targets =
|
|
|
81
81
|
for (const id of Object.keys(targets))
|
|
82
82
|
if (!records.some(record => record.id === id))
|
|
83
83
|
fail('UNKNOWN_EXTENSION', `Unknown conversion target ${id}`);
|
|
84
|
-
|
|
84
|
+
// The project keeps its address through a conversion: it is the same work, served from the
|
|
85
|
+
// same place, however its extensions are now held.
|
|
86
|
+
const manifest = validateManifest({ schemaVersion: 1,
|
|
87
|
+
...(prepared.manifest.uri === undefined ? {} : { uri: prepared.manifest.uri }), extensions });
|
|
85
88
|
const lock = Object.freeze({ schemaVersion: 1, previousLock: project.turboWarpPlusLock ?? null, extensions: Object.freeze(lockEntries) });
|
|
86
89
|
project.turboWarpPlus = manifest;
|
|
87
90
|
project.turboWarpPlusLock = lock;
|
|
@@ -23,6 +23,16 @@ export interface ExtensionAdditionOptions {
|
|
|
23
23
|
signal?: AbortSignal;
|
|
24
24
|
loaderOptions?: LoaderOptions;
|
|
25
25
|
archiveOptions?: ArchiveLimits;
|
|
26
|
+
/**
|
|
27
|
+
* The address to give this project, or nothing to keep the one it has.
|
|
28
|
+
*
|
|
29
|
+
* Whether to assign one is the host's call, not this function's: only the host knows whether
|
|
30
|
+
* the author chose an address or whether this is a first save that needs a default. So this
|
|
31
|
+
* sets what it is given and leaves the existing address alone when it is given nothing —
|
|
32
|
+
* `readProjectAuthoringState` reports what a project already carries, so the host can tell
|
|
33
|
+
* the two apart before deciding.
|
|
34
|
+
*/
|
|
35
|
+
uri?: string;
|
|
26
36
|
}
|
|
27
37
|
export interface PreparedExtensionAddition {
|
|
28
38
|
readonly projectBytes: Uint8Array;
|
|
@@ -21,7 +21,7 @@ function snapshotProject(input) {
|
|
|
21
21
|
* Add a file-backed Embedded or URL-backed Remote extension without evaluating it.
|
|
22
22
|
* A host approval is required before the candidate is returned; the original is untouched.
|
|
23
23
|
*/
|
|
24
|
-
export async function prepareExtensionAddition(input, request, ports, { zip: Zip, signal: externalSignal, loaderOptions = {}, archiveOptions } = {}) {
|
|
24
|
+
export async function prepareExtensionAddition(input, request, ports, { zip: Zip, signal: externalSignal, loaderOptions = {}, archiveOptions, uri } = {}) {
|
|
25
25
|
if (typeof Zip !== 'function' || typeof Zip.loadAsync !== 'function')
|
|
26
26
|
throw new TypeError('A compatible JSZip constructor is required');
|
|
27
27
|
if (!plainObject(request) || Object.keys(request).some(key => !['id', 'source', 'bytes', 'execution'].includes(key))) {
|
|
@@ -94,10 +94,15 @@ export async function prepareExtensionAddition(input, request, ports, { zip: Zip
|
|
|
94
94
|
if (project.extensionURLs !== undefined && !plainObject(project.extensionURLs))
|
|
95
95
|
fail('INVALID_PROJECT', 'Project extensionURLs must be an object');
|
|
96
96
|
const previous = prepared.manifest?.extensions || [];
|
|
97
|
+
// Carried through every rebuild below: an address that changed when an extension was
|
|
98
|
+
// added would not be an address. The host may set one, which is how a project gets its
|
|
99
|
+
// first; saying nothing keeps what is there.
|
|
100
|
+
const address = uri ?? prepared.manifest?.uri;
|
|
101
|
+
const carried = address === undefined ? {} : { uri: address };
|
|
97
102
|
if (previous.some(spec => spec.id === draft.id) || (project.extensions || []).includes(draft.id) ||
|
|
98
103
|
Object.hasOwn(project.extensionURLs || {}, draft.id))
|
|
99
104
|
fail('DUPLICATE_EXTENSION', 'This extension ID already belongs to the project', draft.id);
|
|
100
|
-
validateManifest({ schemaVersion: 1, extensions: [...previous, draft] }, loaderOptions);
|
|
105
|
+
validateManifest({ schemaVersion: 1, ...carried, extensions: [...previous, draft] }, loaderOptions);
|
|
101
106
|
if (draft.source.type === 'embedded-file' && Object.hasOwn(files, draft.source.path)) {
|
|
102
107
|
fail('SOURCE_PATH_COLLISION', 'The embedded path already exists in this project', draft.id);
|
|
103
108
|
}
|
|
@@ -128,7 +133,7 @@ export async function prepareExtensionAddition(input, request, ports, { zip: Zip
|
|
|
128
133
|
}
|
|
129
134
|
const verified = new Uint8Array(bytes);
|
|
130
135
|
const sha256 = await wait(() => digest(verified));
|
|
131
|
-
const manifest = validateManifest({ schemaVersion: 1, extensions: [...previous, { ...draft, sha256 }] }, loaderOptions);
|
|
136
|
+
const manifest = validateManifest({ schemaVersion: 1, ...carried, extensions: [...previous, { ...draft, sha256 }] }, loaderOptions);
|
|
132
137
|
const spec = manifest.extensions.find(item => item.id === draft.id);
|
|
133
138
|
project.turboWarpPlus = manifest;
|
|
134
139
|
project.extensions = [...(project.extensions || []), spec.id];
|
|
@@ -23,6 +23,16 @@ export interface CatalogOptions {
|
|
|
23
23
|
signal?: AbortSignal;
|
|
24
24
|
loaderOptions?: LoaderOptions;
|
|
25
25
|
archiveOptions?: ArchiveLimits;
|
|
26
|
+
/**
|
|
27
|
+
* The address to give this project, or nothing to keep the one it has.
|
|
28
|
+
*
|
|
29
|
+
* Whether to assign one is the host's call, not this function's: only the host knows whether
|
|
30
|
+
* the author chose an address or whether this is a first save that needs a default. So this
|
|
31
|
+
* sets what it is given and leaves the existing address alone when it is given nothing —
|
|
32
|
+
* `readProjectAuthoringState` reports what a project already carries, so the host can tell
|
|
33
|
+
* the two apart before deciding.
|
|
34
|
+
*/
|
|
35
|
+
uri?: string;
|
|
26
36
|
}
|
|
27
37
|
export type CatalogPorts = Pick<LoaderPorts, 'fetcher' | 'trustProvider' | 'cache' | 'diagnostics'>;
|
|
28
38
|
/** Read project-owned distribution requests; these records never grant execution permission. */
|
package/dist/project-catalog.js
CHANGED
|
@@ -19,7 +19,7 @@ export async function readProjectAuthoringState(input, { zip, signal, archiveOpt
|
|
|
19
19
|
const prepared = await io.prepare(input, { signal });
|
|
20
20
|
return prepared.manifest;
|
|
21
21
|
}
|
|
22
|
-
export async function prepareCatalogExtensionChanges(input, requests, ports, { zip: Zip, signal: externalSignal, loaderOptions = {}, archiveOptions } = {}) {
|
|
22
|
+
export async function prepareCatalogExtensionChanges(input, requests, ports, { zip: Zip, signal: externalSignal, loaderOptions = {}, archiveOptions, uri } = {}) {
|
|
23
23
|
const limits = { ...DEFAULT_LOADER_LIMITS, ...loaderOptions };
|
|
24
24
|
for (const key of ['maxExtensions', 'maxBytes', 'maxTotalBytes', 'timeoutMs']) {
|
|
25
25
|
if (!Number.isSafeInteger(limits[key]) || limits[key] < 1)
|
|
@@ -173,7 +173,13 @@ export async function prepareCatalogExtensionChanges(input, requests, ports, { z
|
|
|
173
173
|
...(old?.publisher ? { publisher: old.publisher } : {}),
|
|
174
174
|
capabilities: old?.capabilities || [], dependencies: choice.dependencies });
|
|
175
175
|
}
|
|
176
|
-
|
|
176
|
+
// The project's own address survives a change to its extensions. Rebuilding the manifest
|
|
177
|
+
// without it would file the same work under a new name the next time it was saved, which
|
|
178
|
+
// is the one thing an address is for.
|
|
179
|
+
const address = uri ?? prepared.manifest?.uri;
|
|
180
|
+
const manifest = validateManifest({ schemaVersion: 1,
|
|
181
|
+
...(address === undefined ? {} : { uri: address }),
|
|
182
|
+
extensions: [...extensions.values()] }, loaderOptions);
|
|
177
183
|
for (const spec of manifest.extensions)
|
|
178
184
|
if (spec.source.type === 'embedded-file')
|
|
179
185
|
replacedPaths.delete(spec.source.path);
|