Skip to content

Commit fd59be4

Browse files
feat(core): add binary data support
see the issue #69 for details BREAKING CHANGE: *getFile()* will continue to support a simple string as return value but the object form is now : `{ type, getContentData }` instead of `{ type, content }`
1 parent 10517a4 commit fd59be4

8 files changed

+125
-40
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ Load .vue files dynamically at runtime from your html/js. No node.js environment
3939
const res = await fetch(url);
4040
if ( !res.ok )
4141
throw Object.assign(new Error(res.statusText + ' ' + url), { res });
42-
return await res.text();
42+
return {
43+
getContentData: asBinary => asBinary ? res.arrayBuffer() : res.text(),
44+
}
4345
},
4446
addStyle(textContent) {
4547

src/createVue2SFCModule.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export async function createSFCModule(source : string, filename : AbstractPath,
9393

9494
const compileTemplateOptions : TemplateCompileOptions = descriptor.template ? {
9595
// hack, since sourceMap is not configurable an we want to get rid of source-map dependency. see genSourcemap
96-
source: descriptor.template.src ? (await getResource({ refPath: filename, relPath: descriptor.template.src }, options).getContent()).content.toString() : descriptor.template.content,
96+
source: descriptor.template.src ? (await (await getResource({ refPath: filename, relPath: descriptor.template.src }, options).getContent()).getContentData(false)) as string : descriptor.template.content,
9797
filename: strFilename,
9898
compiler: vueTemplateCompiler as VueTemplateCompiler,
9999
compilerOptions: {
@@ -128,7 +128,7 @@ export async function createSFCModule(source : string, filename : AbstractPath,
128128

129129
// eg: https://github.com/vuejs/vue-loader/blob/v15.9.6/lib/index.js
130130

131-
const src = descriptor.script.src ? (await getResource({ refPath: filename, relPath: descriptor.script.src }, options).getContent()).content.toString() : descriptor.script.content;
131+
const src = descriptor.script.src ? (await (await getResource({ refPath: filename, relPath: descriptor.script.src }, options).getContent()).getContentData(false)) as string : descriptor.script.content;
132132

133133
const [ depsList, transformedScriptSource ] = await withCache(compiledCache, [ componentHash, src, additionalBabelParserPlugins, Object.keys(additionalBabelPlugins) ], async ({ preventCache }) => {
134134

@@ -186,7 +186,7 @@ export async function createSFCModule(source : string, filename : AbstractPath,
186186

187187
for ( const descStyle of descriptor.styles ) {
188188

189-
const src = descStyle.src ? (await getResource({ refPath: filename, relPath: descStyle.src }, options).getContent()).content.toString() : descStyle.content;
189+
const src = descStyle.src ? (await (await getResource({ refPath: filename, relPath: descStyle.src }, options).getContent()).getContentData(false)) as string : descStyle.content;
190190

191191
const style = await withCache(compiledCache, [ componentHash, src, descStyle.lang ], async ({ preventCache }) => {
192192

src/createVue3SFCModule.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export async function createSFCModule(source : string, filename : AbstractPath,
9999
const compileTemplateOptions : SFCTemplateCompileOptions = descriptor.template ? {
100100
// hack, since sourceMap is not configurable an we want to get rid of source-map dependency. see genSourcemap
101101
compiler: { ...vue_CompilerDOM, compile: (template, options) => vue_CompilerDOM.compile(template, { ...options, sourceMap: genSourcemap }) },
102-
source: descriptor.template.src ? (await getResource({ refPath: filename, relPath: descriptor.template.src }, options).getContent()).content.toString() : descriptor.template.content,
102+
source: descriptor.template.src ? (await (await getResource({ refPath: filename, relPath: descriptor.template.src }, options).getContent()).getContentData(false)) as string : descriptor.template.content,
103103
filename: descriptor.filename,
104104
isProd,
105105
scoped: hasScoped,
@@ -122,7 +122,7 @@ export async function createSFCModule(source : string, filename : AbstractPath,
122122
// doc: <script setup> cannot be used with the src attribute.
123123
// TBD: check if this is the right solution
124124
if ( descriptor.script?.src )
125-
descriptor.script.content = (await getResource({ refPath: filename, relPath: descriptor.script.src }, options).getContent()).content.toString();
125+
descriptor.script.content = (await (await getResource({ refPath: filename, relPath: descriptor.script.src }, options).getContent()).getContentData(false)) as string;
126126

127127
// TBD: handle <script setup src="...
128128

@@ -192,7 +192,7 @@ export async function createSFCModule(source : string, filename : AbstractPath,
192192
if ( descStyle.lang )
193193
await loadModuleInternal({ refPath: filename, relPath: descStyle.lang }, options);
194194

195-
const src = descStyle.src ? (await getResource({ refPath: filename, relPath: descStyle.src }, options).getContent()).content.toString() : descStyle.content;
195+
const src = descStyle.src ? (await (await getResource({ refPath: filename, relPath: descStyle.src }, options).getContent()).getContentData(false)) as string : descStyle.content;
196196

197197
const style = await withCache(compiledCache, [ componentHash, src ], async ({ preventCache }) => {
198198

src/index.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,34 @@ const defaultPathResolve : PathResolve = ({ refPath, relPath } : PathContext) =>
7474
*/
7575
function defaultGetResource(pathCx : PathContext, options : Options) : Resource {
7676

77-
const { pathResolve, getFile } = options;
77+
const { pathResolve, getFile, log } = options;
7878
const path = pathResolve(pathCx);
79+
const pathStr = path.toString();
7980
return {
80-
id: path.toString(),
81+
id: pathStr,
8182
path: path,
8283
getContent: async () => {
8384

8485
const res = await getFile(path);
85-
return typeof res === 'object' ? res : { content: res, type: Path.extname(path.toString()) };
86+
87+
if ( typeof res === 'string' || res instanceof ArrayBuffer ) {
88+
89+
return {
90+
type: Path.extname(pathStr),
91+
getContentData: async (asBinary) => {
92+
93+
if ( res instanceof ArrayBuffer !== asBinary )
94+
log?.('warn', `unexpected data type. ${ asBinary ? 'binary' : 'string' } is expected for "${ path }"`);
95+
96+
return res;
97+
},
98+
}
99+
}
100+
101+
return {
102+
type: res.type ?? Path.extname(pathStr),
103+
getContentData: res.getContentData,
104+
}
86105
}
87106
};
88107
}

src/tools.ts

+8-10
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
LoadingType,
4040
PathContext,
4141
AbstractPath,
42+
File,
4243
} from './types'
4344

4445
import { createSFCModule } from './createSFCModule'
@@ -278,19 +279,16 @@ export async function loadModuleInternal(pathCx : PathContext, options : Options
278279
return moduleCache[id] = module;
279280
}
280281

281-
const { content, type } = await getContent();
282-
283-
if ( typeof content !== 'string' )
284-
throw new TypeError(`Invalid module content (${ path }): ${ content }`);
282+
const { getContentData, type } = await getContent();
285283

286284
// note: null module is accepted
287285
let module : ModuleExport | undefined | null = undefined;
288286

289287
if ( handleModule !== undefined )
290-
module = await handleModule(type, content, path, options);
288+
module = await handleModule(type, getContentData, path, options);
291289

292290
if ( module === undefined )
293-
module = await defaultHandleModule(type, content, path, options);
291+
module = await defaultHandleModule(type, getContentData, path, options);
294292

295293
if ( module === undefined )
296294
throw new TypeError(`Unable to handle ${ type } files (${ path })`);
@@ -369,12 +367,12 @@ export async function loadDeps(refPath : AbstractPath, deps : AbstractPath[], op
369367
/**
370368
* Default implementation of handleModule
371369
*/
372-
async function defaultHandleModule(type : string, source : string, path : AbstractPath, options : Options) : Promise<ModuleExport | null> {
370+
async function defaultHandleModule(type : string, getContentData : File['getContentData'], path : AbstractPath, options : Options) : Promise<ModuleExport | null> {
373371

374372
switch (type) {
375-
case '.vue': return createSFCModule(source.toString(), path, options);
376-
case '.js': return createJSModule(source.toString(), false, path, options);
377-
case '.mjs': return createJSModule(source.toString(), true, path, options);
373+
case '.vue': return createSFCModule((await getContentData(false)) as string, path, options);
374+
case '.js': return createJSModule((await getContentData(false)) as string, false, path, options);
375+
case '.mjs': return createJSModule((await getContentData(false)) as string, true, path, options);
378376
}
379377

380378
return undefined;

src/types.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,18 @@ export type PathResolve = (pathCx : PathContext) => AbstractPath;
6565
* ...
6666
* ```
6767
*/
68-
export type ModuleHandler = (type : string, source : string, path : AbstractPath, options : Options) => Promise<ModuleExport | null>;
68+
export type ModuleHandler = (type : string, getContentData : File['getContentData'], path : AbstractPath, options : Options) => Promise<ModuleExport | null>;
69+
70+
71+
export type ContentData = string | ArrayBuffer
6972

7073

7174
/**
7275
* Represents a file content and the extension name.
7376
*/
7477
export type File = {
75-
/** The content data */
76-
content : string | ArrayBuffer,
78+
/** The content data accessor (request data as text of binary)*/
79+
getContentData : (asBinary : Boolean) => Promise<ContentData>,
7780
/** The content type (file extension name, eg. '.svg' ) */
7881
type : string,
7982
}
@@ -160,22 +163,28 @@ export type Options = {
160163
/**
161164
* Called by the library when it needs a file.
162165
* @param path The path of the file
163-
* @returns a Promise of the file content (UTF-8)
166+
* @returns a Promise of the file content or an accessor to the file content that handles text or binary data
164167
*
165168
* **example:**
166169
* ```javascript
167170
* ...
168171
* async getFile(url) {
169172
*
170173
* const res = await fetch(url);
174+
*
171175
* if ( !res.ok )
172176
* throw Object.assign(new Error(url+' '+res.statusText), { res });
177+
*
178+
* return {
179+
* getContentData: asBinary => asBinary ? res.arrayBuffer() : res.text(),
180+
* }
181+
*
173182
* return await res.text();
174183
* },
175184
* ...
176185
* ```
177186
*/
178-
getFile(path : AbstractPath) : Promise<File>,
187+
getFile(path : AbstractPath) : Promise<File | ContentData>,
179188

180189

181190
/**

tests/basic.test.js

+37-3
Original file line numberDiff line numberDiff line change
@@ -707,10 +707,10 @@ const { defaultFilesFactory, createPage } = require('./testsTools.js');
707707
'/optionsOverride.js': `
708708
export default (options) => {
709709
710-
options.handleModule = (extname, source, path, options) => {
710+
options.handleModule = async (type, getContentData, path, options) => {
711711
712-
switch (extname) {
713-
case '.svg': return 'data:image/svg+xml,' + source.toString();
712+
switch (type) {
713+
case '.svg': return 'data:image/svg+xml,' + await getContentData(false);
714714
}
715715
};
716716
};
@@ -721,6 +721,40 @@ const { defaultFilesFactory, createPage } = require('./testsTools.js');
721721
await expect(page.$eval('#app', el => el.innerHTML)).resolves.toMatch('[CDATA[10]]');
722722
});
723723

724+
test('should properly include png image', async () => {
725+
726+
const { page, output } = await createPage({
727+
files: {
728+
...files,
729+
730+
'/image.png': Buffer.from(Uint8Array.from(atob('iVBORw0KGgoAAAANSUhEUgAAAAUAAAAHCAAAAADlzNgyAAAAEklEQVQI12P8z8DAwMDEQJgEAConAQ0Jet0iAAAAAElFTkSuQmCC'), c => c.charCodeAt(0)).buffer),
731+
732+
'/main.vue': `
733+
<template>
734+
<div>
735+
<img :src="require('./image.png')">
736+
</div>
737+
</template>
738+
`,
739+
'/optionsOverride.js': `
740+
export default (options) => {
741+
742+
options.handleModule = async (type, getContentData, path, options) => {
743+
744+
switch (type) {
745+
case '.png':
746+
var data = await getContentData(true);
747+
return 'data:image/png;base64,' + btoa(String.fromCharCode(...new Uint8Array(data)));
748+
}
749+
};
750+
};
751+
`,
752+
}
753+
});
754+
755+
await expect(page.$eval('#app img', el => el.naturalWidth)).resolves.toBe(5); // img is 5x7 px
756+
});
757+
724758

725759
// https://github.com/vuejs/vue-template-es2015-compiler/blob/master/test.js
726760

tests/testsTools.js

+35-12
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,36 @@ const pendingPages = [];
1919

2020
async function createPage({ files, processors= {}}) {
2121

22-
async function getFile(url) {
22+
async function getRequestResource(url) {
2323

2424
const { origin, pathname } = new URL(url);
2525

2626
if ( origin !== local.origin )
2727
return null
2828

2929
let body = files[pathname]
30+
31+
if ( body === undefined ) {
32+
33+
return {
34+
status: 404,
35+
}
36+
}
37+
38+
if ( typeof body !== 'string' && !(body instanceof Buffer) )
39+
throw new Error('response body must be a string of a Buffer');
40+
3041
if (processors[pathname]) {
42+
3143
body = processors[pathname](body)
3244
}
3345

46+
const contentType = mime.lookup(Path.extname(pathname)) || '';
47+
const charset = mime.charset(contentType);
48+
3449
const res = {
35-
contentType: mime.lookup(Path.extname(pathname)) || '',
50+
contentType: contentType + (charset ? '; charset=' + charset : ''),
3651
body,
37-
status: files[pathname] === undefined ? 404 : 200,
3852
};
3953

4054
return res;
@@ -46,13 +60,9 @@ async function createPage({ files, processors= {}}) {
4660
await page.setRequestInterception(true);
4761
page.on('request', async interceptedRequest => {
4862
try {
49-
const file = await getFile(interceptedRequest.url(), 'utf-8');
50-
if (file) {
51-
return void interceptedRequest.respond({
52-
...file,
53-
contentType: file.contentType + '; charset=utf-8',
54-
});
55-
}
63+
const response = await getRequestResource(interceptedRequest.url());
64+
if (response)
65+
return void interceptedRequest.respond(response);
5666

5767
interceptedRequest.continue();
5868
} catch (ex) {
@@ -181,8 +191,21 @@ const defaultFilesFactory = ({ vueTarget }) => ({
181191
vue: Vue
182192
},
183193
184-
getFile(path) {
185-
return fetch(path).then(res => res.ok ? res.text() : Promise.reject(new HttpError(path, res)));
194+
async getFile(path) {
195+
//return fetch(path).then(res => res.ok ? res.text() : Promise.reject(new HttpError(path, res)));
196+
197+
const res = await fetch(path);
198+
if ( !res.ok )
199+
throw new HttpError(path, res);
200+
201+
return {
202+
//type: res.headers.get('content-type'),
203+
getContentData(asBinary) {
204+
205+
return asBinary ? res.arrayBuffer() : res.text();
206+
}
207+
}
208+
186209
},
187210
188211
addStyle(textContent) {

0 commit comments

Comments
 (0)