diff --git a/docs/theme/layouts/default.hbs b/docs/theme/layouts/default.hbs index 049bec258..ff804bd5a 100644 --- a/docs/theme/layouts/default.hbs +++ b/docs/theme/layouts/default.hbs @@ -95,6 +95,11 @@ PlatformIO (Abstract) +
  • + + DenoIO + +
  • NodeIO diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index db7203371..239bc210d 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -27,7 +27,7 @@ export { COPY_IDENTITY, } from './properties'; export { Graph, GraphEdge } from 'property-graph'; -export { PlatformIO, NodeIO, WebIO, ReaderContext, WriterContext } from './io'; +export { DenoIO, PlatformIO, NodeIO, WebIO, ReaderContext, WriterContext } from './io'; export { BufferUtils, ColorUtils, diff --git a/packages/core/src/io/deno-io.ts b/packages/core/src/io/deno-io.ts new file mode 100644 index 000000000..ccfc71ffd --- /dev/null +++ b/packages/core/src/io/deno-io.ts @@ -0,0 +1,69 @@ +import { PlatformIO } from '.'; + +declare global { + const Deno: { + readFile: (path: string) => Promise; + readTextFile: (path: string) => Promise; + }; +} + +interface Path { + resolve(directory: string, path: string): string; + dirname(uri: string): string; +} + +/** + * # DenoIO + * + * *I/O service for Deno.* + * + * The most common use of the I/O service is to read/write a {@link Document} with a given path. + * Methods are also available for converting in-memory representations of raw glTF files, both + * binary (*ArrayBuffer*) and JSON ({@link JSONDocument}). + * + * Usage: + * + * ```typescript + * import { DenoIO } from 'https://esm.sh/@gltf-transform/core'; + * import * as path from 'https://deno.land/std/path/mod.ts'; + * + * const io = new DenoIO(path); + * + * // Read. + * let document; + * document = io.read('model.glb'); // → Document + * document = io.readBinary(glb); // Uint8Array → Document + * + * // Write. + * const glb = io.writeBinary(document); // Document → Uint8Array + * ``` + * + * @category I/O + */ +export class DenoIO extends PlatformIO { + private _path: Path; + + constructor(path: unknown) { + super(); + this._path = path as Path; + } + + protected async readURI(uri: string, type: 'view'): Promise; + protected async readURI(uri: string, type: 'text'): Promise; + protected async readURI(uri: string, type: 'view' | 'text'): Promise { + switch (type) { + case 'view': + return Deno.readFile(uri); + case 'text': + return Deno.readTextFile(uri); + } + } + + protected resolve(directory: string, path: string): string { + return this._path.resolve(directory, path); + } + + protected dirname(uri: string): string { + return this._path.dirname(uri); + } +} diff --git a/packages/core/src/io/index.ts b/packages/core/src/io/index.ts index a5a5db99c..5582efbfe 100644 --- a/packages/core/src/io/index.ts +++ b/packages/core/src/io/index.ts @@ -1,4 +1,5 @@ export { NodeIO } from './node-io'; +export { DenoIO } from './deno-io'; export { PlatformIO } from './platform-io'; export { WebIO } from './web-io'; export { ReaderOptions } from './reader'; diff --git a/packages/core/src/io/node-io.ts b/packages/core/src/io/node-io.ts index 9b0b7d32d..79e5eea4a 100644 --- a/packages/core/src/io/node-io.ts +++ b/packages/core/src/io/node-io.ts @@ -1,7 +1,5 @@ import { Format } from '../constants'; import { Document } from '../document'; -import { JSONDocument } from '../json-document'; -import { GLTF } from '../types/gltf'; import { FileUtils } from '../utils/'; import { PlatformIO } from './platform-io'; @@ -37,12 +35,6 @@ export class NodeIO extends PlatformIO { private _fs; private _path; - /** @hidden */ - public lastReadBytes = 0; - - /** @hidden */ - public lastWriteBytes = 0; - /** Constructs a new NodeIO service. Instances are reusable. */ constructor() { super(); @@ -51,72 +43,39 @@ export class NodeIO extends PlatformIO { this._path = require('path'); } - /********************************************************************************************** - * Public. - */ - - /** Loads a local path and returns a {@link Document} instance. */ - public async read(uri: string): Promise { - return await this.readJSON(await this.readAsJSON(uri)); + protected async readURI(uri: string, type: 'view'): Promise; + protected async readURI(uri: string, type: 'text'): Promise; + protected async readURI(uri: string, type: 'view' | 'text'): Promise { + switch (type) { + case 'view': + return this._fs.readFile(uri); + case 'text': + return this._fs.readFile(uri, 'utf8'); + } } - /** Loads a local path and returns a {@link JSONDocument} struct, without parsing. */ - public async readAsJSON(uri: string): Promise { - const isGLB = !!(uri.match(/\.glb$/) || uri.match(/^data:application\/octet-stream;/)); - return isGLB ? this._readGLB(uri) : this._readGLTF(uri); + protected resolve(directory: string, path: string): string { + return this._path.resolve(directory, path); } - /** Writes a {@link Document} instance to a local path. */ - public async write(uri: string, doc: Document): Promise { - const isGLB = !!uri.match(/\.glb$/); - await (isGLB ? this._writeGLB(uri, doc) : this._writeGLTF(uri, doc)); + protected dirname(uri: string): string { + return this._path.dirname(uri); } /********************************************************************************************** - * Protected. + * Public. */ - /** @internal */ - private async _readResourcesExternal(jsonDoc: JSONDocument, dir: string): Promise { - const images = jsonDoc.json.images || []; - const buffers = jsonDoc.json.buffers || []; - const resources = [...images, ...buffers].map(async (resource: GLTF.IBuffer | GLTF.IImage) => { - if (resource.uri && !resource.uri.match(/data:/)) { - const absURI = this._path.resolve(dir, resource.uri); - jsonDoc.resources[resource.uri] = await this._fs.readFile(absURI); - this.lastReadBytes += jsonDoc.resources[resource.uri].byteLength; - } - }); - await Promise.all(resources); + /** Writes a {@link Document} instance to a local path. */ + public async write(uri: string, doc: Document): Promise { + const isGLB = !!uri.match(/\.glb$/); + await (isGLB ? this._writeGLB(uri, doc) : this._writeGLTF(uri, doc)); } /********************************************************************************************** * Private. */ - /** @internal */ - private async _readGLB(uri: string): Promise { - const buffer: Buffer = await this._fs.readFile(uri); - this.lastReadBytes = buffer.byteLength; - const jsonDoc = this._binaryToJSON(buffer); - // Read external resources first, before Data URIs are replaced. - await this._readResourcesExternal(jsonDoc, this._path.dirname(uri)); - await this._readResourcesInternal(jsonDoc); - return jsonDoc; - } - - /** @internal */ - private async _readGLTF(uri: string): Promise { - this.lastReadBytes = 0; - const jsonContent = await this._fs.readFile(uri, 'utf8'); - this.lastReadBytes += jsonContent.length; - const jsonDoc = { json: JSON.parse(jsonContent), resources: {} } as JSONDocument; - // Read external resources first, before Data URIs are replaced. - await this._readResourcesExternal(jsonDoc, this._path.dirname(uri)); - await this._readResourcesInternal(jsonDoc); - return jsonDoc; - } - /** @internal */ private async _writeGLTF(uri: string, doc: Document): Promise { this.lastWriteBytes = 0; diff --git a/packages/core/src/io/platform-io.ts b/packages/core/src/io/platform-io.ts index 0684602cf..e79e50fa1 100644 --- a/packages/core/src/io/platform-io.ts +++ b/packages/core/src/io/platform-io.ts @@ -23,7 +23,7 @@ type PublicWriterOptions = Partial>; * Methods are also available for converting in-memory representations of raw glTF files, both * binary (*ArrayBuffer*) and JSON ({@link JSONDocument}). * - * For platform-specific implementations, see {@link NodeIO} and {@link WebIO}. + * For platform-specific implementations, see {@link NodeIO}, {@link WebIO}, and {@link DenoIO}. * * @category I/O */ @@ -33,6 +33,12 @@ export abstract class PlatformIO { private _dependencies: { [key: string]: unknown } = {}; private _vertexLayout = VertexLayout.INTERLEAVED; + /** @hidden */ + public lastReadBytes = 0; + + /** @hidden */ + public lastWriteBytes = 0; + /** Sets the {@link Logger} used by this I/O instance. Defaults to Logger.DEFAULT_INSTANCE. */ public setLogger(logger: Logger): this { this._logger = logger; @@ -64,11 +70,148 @@ export abstract class PlatformIO { } /********************************************************************************************** - * Common. + * Abstract. + */ + + protected abstract readURI(uri: string, type: 'view'): Promise; + protected abstract readURI(uri: string, type: 'text'): Promise; + protected abstract readURI(uri: string, type: 'view' | 'text'): Promise; + + protected abstract resolve(directory: string, path: string): string; + protected abstract dirname(uri: string): string; + + /********************************************************************************************** + * Public Read API. + */ + + /** Reads a {@link Document} from the given URI. */ + public async read(uri: string): Promise { + return await this.readJSON(await this.readAsJSON(uri)); + } + + /** Loads a URI and returns a {@link JSONDocument} struct, without parsing. */ + public async readAsJSON(uri: string): Promise { + const isGLB = uri.match(/^data:application\/octet-stream;/) || FileUtils.extension(uri) === 'glb'; + return isGLB ? this._readGLB(uri) : this._readGLTF(uri); + } + + /** Converts glTF-formatted JSON and a resource map to a {@link Document}. */ + public async readJSON(jsonDoc: JSONDocument): Promise { + jsonDoc = this._copyJSON(jsonDoc); + this._readResourcesInternal(jsonDoc); + return GLTFReader.read(jsonDoc, { + extensions: Array.from(this._extensions), + dependencies: this._dependencies, + logger: this._logger, + }); + } + + /** Converts a GLB-formatted ArrayBuffer to a {@link JSONDocument}. */ + public async binaryToJSON(glb: Uint8Array): Promise { + const jsonDoc = this._binaryToJSON(BufferUtils.assertView(glb)); + this._readResourcesInternal(jsonDoc); + const json = jsonDoc.json; + + // Check for external references, which can't be resolved by this method. + if (json.buffers && json.buffers.some((bufferDef) => isExternalBuffer(jsonDoc, bufferDef))) { + throw new Error('Cannot resolve external buffers with binaryToJSON().'); + } else if (json.images && json.images.some((imageDef) => isExternalImage(jsonDoc, imageDef))) { + throw new Error('Cannot resolve external images with binaryToJSON().'); + } + + return jsonDoc; + } + + /** Converts a GLB-formatted ArrayBuffer to a {@link Document}. */ + public async readBinary(glb: Uint8Array): Promise { + return this.readJSON(await this.binaryToJSON(BufferUtils.assertView(glb))); + } + + /********************************************************************************************** + * Public Write API. */ - /** @internal */ - protected _readResourcesInternal(jsonDoc: JSONDocument): void { + /** Converts a {@link Document} to glTF-formatted JSON and a resource map. */ + public async writeJSON(doc: Document, _options: PublicWriterOptions = {}): Promise { + if (_options.format === Format.GLB && doc.getRoot().listBuffers().length > 1) { + throw new Error('GLB must have 0–1 buffers.'); + } + return GLTFWriter.write(doc, { + format: _options.format || Format.GLTF, + basename: _options.basename || '', + logger: this._logger, + vertexLayout: this._vertexLayout, + dependencies: { ...this._dependencies }, + extensions: Array.from(this._extensions), + } as Required); + } + + /** Converts a {@link Document} to a GLB-formatted ArrayBuffer. */ + public async writeBinary(doc: Document): Promise { + const { json, resources } = await this.writeJSON(doc, { format: Format.GLB }); + + const header = new Uint32Array([0x46546c67, 2, 12]); + + const jsonText = JSON.stringify(json); + const jsonChunkData = BufferUtils.pad(BufferUtils.encodeText(jsonText), 0x20); + const jsonChunkHeader = BufferUtils.toView(new Uint32Array([jsonChunkData.byteLength, 0x4e4f534a])); + const jsonChunk = BufferUtils.concat([jsonChunkHeader, jsonChunkData]); + header[header.length - 1] += jsonChunk.byteLength; + + const binBuffer = Object.values(resources)[0]; + if (!binBuffer || !binBuffer.byteLength) { + return BufferUtils.concat([BufferUtils.toView(header), jsonChunk]); + } + + const binChunkData = BufferUtils.pad(binBuffer, 0x00); + const binChunkHeader = BufferUtils.toView(new Uint32Array([binChunkData.byteLength, 0x004e4942])); + const binChunk = BufferUtils.concat([binChunkHeader, binChunkData]); + header[header.length - 1] += binChunk.byteLength; + + return BufferUtils.concat([BufferUtils.toView(header), jsonChunk, binChunk]); + } + + /********************************************************************************************** + * Internal. + */ + + private async _readGLTF(uri: string): Promise { + this.lastReadBytes = 0; + const jsonContent = await this.readURI(uri, 'text'); + this.lastReadBytes += jsonContent.length; + const jsonDoc: JSONDocument = { json: JSON.parse(jsonContent), resources: {} }; + // Read external resources first, before Data URIs are replaced. + await this._readResourcesExternal(jsonDoc, this.dirname(uri)); + this._readResourcesInternal(jsonDoc); + return jsonDoc; + } + + private async _readGLB(uri: string): Promise { + const view = await this.readURI(uri, 'view'); + this.lastReadBytes = view.byteLength; + const jsonDoc = this._binaryToJSON(view); + // Read external resources first, before Data URIs are replaced. + await this._readResourcesExternal(jsonDoc, this.dirname(uri)); + this._readResourcesInternal(jsonDoc); + return jsonDoc; + } + + private async _readResourcesExternal(jsonDoc: JSONDocument, dir: string): Promise { + const images = jsonDoc.json.images || []; + const buffers = jsonDoc.json.buffers || []; + const pendingResources: Array> = [...images, ...buffers].map( + async (resource: GLTF.IBuffer | GLTF.IImage): Promise => { + const uri = resource.uri; + if (!uri || uri.match(/data:/)) return Promise.resolve(); + + jsonDoc.resources[uri] = await this.readURI(this.resolve(dir, uri), 'view'); + this.lastReadBytes += jsonDoc.resources[uri].byteLength; + } + ); + await Promise.all(pendingResources); + } + + private _readResourcesInternal(jsonDoc: JSONDocument): void { // NOTICE: This method may be called more than once during the loading // process (e.g. WebIO.read) and should handle that safely. @@ -103,36 +246,6 @@ export abstract class PlatformIO { buffers.forEach(resolveResource); } - /********************************************************************************************** - * JSON. - */ - - /** Converts glTF-formatted JSON and a resource map to a {@link Document}. */ - public async readJSON(jsonDoc: JSONDocument): Promise { - jsonDoc = this._copyJSON(jsonDoc); - this._readResourcesInternal(jsonDoc); - return GLTFReader.read(jsonDoc, { - extensions: Array.from(this._extensions), - dependencies: this._dependencies, - logger: this._logger, - }); - } - - /** Converts a {@link Document} to glTF-formatted JSON and a resource map. */ - public async writeJSON(doc: Document, _options: PublicWriterOptions = {}): Promise { - if (_options.format === Format.GLB && doc.getRoot().listBuffers().length > 1) { - throw new Error('GLB must have 0–1 buffers.'); - } - return GLTFWriter.write(doc, { - format: _options.format || Format.GLTF, - basename: _options.basename || '', - logger: this._logger, - vertexLayout: this._vertexLayout, - dependencies: { ...this._dependencies }, - extensions: Array.from(this._extensions), - } as Required); - } - /** * Creates a shallow copy of glTF-formatted {@link JSONDocument}. * @@ -155,28 +268,8 @@ export abstract class PlatformIO { return jsonDoc; } - /********************************************************************************************** - * Binary -> JSON. - */ - - /** Converts a GLB-formatted ArrayBuffer to a {@link JSONDocument}. */ - public async binaryToJSON(glb: Uint8Array): Promise { - const jsonDoc = this._binaryToJSON(BufferUtils.assertView(glb)); - this._readResourcesInternal(jsonDoc); - const json = jsonDoc.json; - - // Check for external references, which can't be resolved by this method. - if (json.buffers && json.buffers.some((bufferDef) => isExternalBuffer(jsonDoc, bufferDef))) { - throw new Error('Cannot resolve external buffers with binaryToJSON().'); - } else if (json.images && json.images.some((imageDef) => isExternalImage(jsonDoc, imageDef))) { - throw new Error('Cannot resolve external images with binaryToJSON().'); - } - - return jsonDoc; - } - - /** @internal For internal use by WebIO and NodeIO. Does not warn about external resources. */ - protected _binaryToJSON(glb: Uint8Array): JSONDocument { + /** Internal version of binaryToJSON; does not warn about external resources. */ + private _binaryToJSON(glb: Uint8Array): JSONDocument { // Decode and verify GLB header. const header = new Uint32Array(glb.buffer, glb.byteOffset, 3); if (header[0] !== 0x46546c67) { @@ -214,40 +307,6 @@ export abstract class PlatformIO { return { json, resources: { [GLB_BUFFER]: binBuffer } }; } - - /********************************************************************************************** - * Binary. - */ - - /** Converts a GLB-formatted ArrayBuffer to a {@link Document}. */ - public async readBinary(glb: Uint8Array): Promise { - return this.readJSON(await this.binaryToJSON(BufferUtils.assertView(glb))); - } - - /** Converts a {@link Document} to a GLB-formatted ArrayBuffer. */ - public async writeBinary(doc: Document): Promise { - const { json, resources } = await this.writeJSON(doc, { format: Format.GLB }); - - const header = new Uint32Array([0x46546c67, 2, 12]); - - const jsonText = JSON.stringify(json); - const jsonChunkData = BufferUtils.pad(BufferUtils.encodeText(jsonText), 0x20); - const jsonChunkHeader = BufferUtils.toView(new Uint32Array([jsonChunkData.byteLength, 0x4e4f534a])); - const jsonChunk = BufferUtils.concat([jsonChunkHeader, jsonChunkData]); - header[header.length - 1] += jsonChunk.byteLength; - - const binBuffer = Object.values(resources)[0]; - if (!binBuffer || !binBuffer.byteLength) { - return BufferUtils.concat([BufferUtils.toView(header), jsonChunk]); - } - - const binChunkData = BufferUtils.pad(binBuffer, 0x00); - const binChunkHeader = BufferUtils.toView(new Uint32Array([binChunkData.byteLength, 0x004e4942])); - const binChunk = BufferUtils.concat([binChunkHeader, binChunkData]); - header[header.length - 1] += binChunk.byteLength; - - return BufferUtils.concat([BufferUtils.toView(header), jsonChunk, binChunk]); - } } function isExternalBuffer(jsonDocument: JSONDocument, bufferDef: GLTF.IBuffer): boolean { diff --git a/packages/core/src/io/web-io.ts b/packages/core/src/io/web-io.ts index 3d3f2d7ff..e690c9ac0 100644 --- a/packages/core/src/io/web-io.ts +++ b/packages/core/src/io/web-io.ts @@ -1,6 +1,3 @@ -import { Document } from '../document'; -import { JSONDocument } from '../json-document'; -import { GLTF } from '../types/gltf'; import { PlatformIO } from './platform-io'; const DEFAULT_INIT: RequestInit = {}; @@ -41,72 +38,31 @@ export class WebIO extends PlatformIO { super(); } - /********************************************************************************************** - * Public. - */ - - /** Loads a URI and returns a {@link Document} instance. */ - public async read(uri: string): Promise { - return this.readAsJSON(uri).then((jsonDoc) => this.readJSON(jsonDoc)); - } - - /** Loads a URI and returns a {@link JSONDocument} struct, without parsing. */ - public async readAsJSON(uri: string): Promise { - const isGLB = - uri.match(/^data:application\/octet-stream;/) || - new URL(uri, window.location.href).pathname.match(/\.glb$/); - return isGLB ? this._readGLB(uri) : this._readGLTF(uri); - } - - /********************************************************************************************** - * Protected. - */ - - /** @internal */ - private _readResourcesExternal(jsonDoc: JSONDocument, dir: string): Promise { - const images = jsonDoc.json.images || []; - const buffers = jsonDoc.json.buffers || []; - const pendingResources: Array> = [...images, ...buffers].map( - async (resource: GLTF.IBuffer | GLTF.IImage): Promise => { - const uri = resource.uri; - if (!uri || uri.match(/data:/)) return Promise.resolve(); - - const res = await fetch(_resolve(dir, uri), this._fetchConfig); - jsonDoc.resources[uri] = new Uint8Array(await res.arrayBuffer()); - } - ); - return Promise.all(pendingResources).then(() => undefined); + protected async readURI(uri: string, type: 'view'): Promise; + protected async readURI(uri: string, type: 'text'): Promise; + protected async readURI(uri: string, type: 'view' | 'text'): Promise { + const response = await fetch(uri, this._fetchConfig); + switch (type) { + case 'view': + return new Uint8Array(await response.arrayBuffer()); + case 'text': + return response.text(); + } } - /********************************************************************************************** - * Private. - */ - - /** @internal */ - private async _readGLTF(uri: string): Promise { - const json = await fetch(uri, this._fetchConfig).then((response) => response.json()); - const jsonDoc: JSONDocument = { json, resources: {} }; - // Read external resources first, before Data URIs are replaced. - await this._readResourcesExternal(jsonDoc, _dirname(uri)); - this._readResourcesInternal(jsonDoc); - return jsonDoc; + protected resolve(directory: string, path: string): string { + return _resolve(directory, path); } - /** @internal */ - private async _readGLB(uri: string): Promise { - const arrayBuffer = await fetch(uri, this._fetchConfig).then((response) => response.arrayBuffer()); - const jsonDoc = this._binaryToJSON(new Uint8Array(arrayBuffer)); - // Read external resources first, before Data URIs are replaced. - await this._readResourcesExternal(jsonDoc, _dirname(uri)); - this._readResourcesInternal(jsonDoc); - return jsonDoc; + protected dirname(uri: string): string { + return _dirname(uri); } } function _dirname(path: string): string { const index = path.lastIndexOf('/'); if (index === -1) return './'; - return path.substr(0, index + 1); + return path.substring(0, index + 1); } function _resolve(base: string, path: string) { diff --git a/packages/core/src/utils/file-utils.ts b/packages/core/src/utils/file-utils.ts index 50eab227c..e2a6953af 100644 --- a/packages/core/src/utils/file-utils.ts +++ b/packages/core/src/utils/file-utils.ts @@ -1,3 +1,7 @@ +// Need a placeholder domain to construct a URL from a relative path. We only +// access `url.pathname`, so the domain doesn't matter. +const NULL_DOMAIN = 'https://null.example'; + /** * # FileUtils * @@ -8,13 +12,15 @@ export class FileUtils { /** Extracts the basename from a path, e.g. "folder/model.glb" -> "model". */ static basename(path: string): string { + path = new URL(path, NULL_DOMAIN).pathname; const fileName = path.split(/[\\/]/).pop()!; - return fileName.substr(0, fileName.lastIndexOf('.')); + return fileName.substring(0, fileName.lastIndexOf('.')); } /** Extracts the extension from a path, e.g. "folder/model.glb" -> "glb". */ static extension(path: string): string { if (path.indexOf('data:') !== 0) { + path = new URL(path, NULL_DOMAIN).pathname; return path.split(/[\\/]/).pop()!.split(/[.]/).pop()!; } else if (path.indexOf('data:image/png') === 0) { return 'png'; diff --git a/packages/core/test/io/web-io.test.ts b/packages/core/test/io/web-io.test.ts index 8aac78701..9c61323b5 100644 --- a/packages/core/test/io/web-io.test.ts +++ b/packages/core/test/io/web-io.test.ts @@ -21,6 +21,9 @@ test('@gltf-transform/core::io | web read glb', (t) => { mockWindow('https://www.example.com/test'); mockFetch({ arrayBuffer: () => BufferUtils.createBufferFromDataURI(SAMPLE_GLB), + text: () => { + throw new Error('Do not call.'); + }, json: () => { throw new Error('Do not call.'); }, @@ -73,7 +76,10 @@ test('@gltf-transform/core::io | web read glb + resources', (t) => { mockWindow('https://www.example.com/test'); const fetchedPaths = mockFetch({ arrayBuffer: () => responses.pop(), - json: () => () => { + text: () => { + throw new Error('Do not call.'); + }, + json: () => { throw new Error('Do not call.'); }, }); @@ -106,16 +112,17 @@ test('@gltf-transform/core::io | web read gltf', (t) => { mockWindow('https://www.example.com/test'); const fetchedPaths = mockFetch({ arrayBuffer: () => images.pop(), - json: () => ({ - asset: { version: '2.0' }, - scenes: [{ name: 'Default Scene' }], - images: [ - { uri: 'image1.png' }, - { uri: '/abs/path/image2.png' }, - { uri: './rel/path/image3.png' }, - { uri: 'rel/path/image3.png' }, - ], - }), + text: () => + JSON.stringify({ + asset: { version: '2.0' }, + scenes: [{ name: 'Default Scene' }], + images: [ + { uri: 'image1.png' }, + { uri: '/abs/path/image2.png' }, + { uri: './rel/path/image3.png' }, + { uri: 'rel/path/image3.png' }, + ], + }), }); const io = new WebIO(); @@ -151,10 +158,11 @@ test('@gltf-transform/core::io | web read + data URIs', async (t) => { arrayBuffer: () => { throw new Error('Do not call.'); }, - json: () => ({ - asset: { version: '2.0' }, - images: uris.map((uri) => ({ uri })), - }), + text: () => + JSON.stringify({ + asset: { version: '2.0' }, + images: uris.map((uri) => ({ uri })), + }), }); const io = new WebIO(); @@ -181,6 +189,9 @@ test('@gltf-transform/core::io | web readJSON + data URIs', async (t) => { arrayBuffer: () => { throw new Error('Do not call.'); }, + text: () => { + throw new Error('Do not call.'); + }, json: () => () => { throw new Error('Do not call.'); },