@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 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. That is why it is an address and not a fingerprint: a
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
- const manifest = validateManifest({ schemaVersion: 1, extensions });
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. */
@@ -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
- const manifest = validateManifest({ schemaVersion: 1, extensions: [...extensions.values()] }, loaderOptions);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-extension-loader",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Host-independent verified TurboWarp Plus extension loading core",
5
5
  "keywords": [
6
6
  "turbowarp",