Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid the assumption of Buffer in browser envs #3452

Merged
merged 8 commits into from Mar 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -80,7 +80,7 @@
* Modules that are completely tree-shaken will no longer be listed as part of any chunks in `generateBundle`
* The `experimentalOptimizeChunks` and `chunkGroupingSize` options have been removed
* [acorn](https://github.com/acornjs/acorn) plugins can only be used if they accept a passed-in acorn instance instead of importing it themselves. See https://github.com/acornjs/acorn/pull/870#issuecomment-527339830 for what needs to be done to make plugins compatible that do not support this yet (#3391)
* Emitted chunks now have the TypeScript type `UInt8Array` instead of `Buffer`. A `Buffer` can still be used, though (#3395)
* Emitted chunks now have the TypeScript type `Uint8Array` instead of `Buffer`. A `Buffer` can still be used, though (#3395)
* The TypeScript types no longer use ESTree types for AST nodes but a very generic type that does not contain information specific to certain node types (#3395)
* The signature of the `writeBundle` plugin hook has been changed to match `generateBundle`: The bundle object is now passed as second parameter instead of first and the first parameter is the output options (#3361)
* The following plugin hooks have been removed:
Expand Down
2 changes: 1 addition & 1 deletion docs/02-javascript-api.md
Expand Up @@ -29,7 +29,7 @@ async function build() {
// For assets, this contains
// {
// fileName: string, // the asset file name
// source: string | UInt8Array // the asset source
// source: string | Uint8Array // the asset source
// type: 'asset' // signifies that this is an asset
// }
console.log('Asset', chunkOrAsset);
Expand Down
10 changes: 5 additions & 5 deletions docs/05-plugin-development.md
Expand Up @@ -282,7 +282,7 @@ Called at the end of `bundle.generate()` or immediately before the files are wri
// AssetInfo
{
fileName: string,
source: string | UInt8Array,
source: string | Uint8Array,
type: 'asset',
}

Expand Down Expand Up @@ -457,7 +457,7 @@ Emits a new file that is included in the build output and returns a `referenceId
// EmittedAsset
{
type: 'asset',
source?: string | UInt8Array,
source?: string | Uint8Array,
name?: string,
fileName?: string
}
Expand Down Expand Up @@ -526,9 +526,9 @@ Resolve imports to module ids (i.e. file names) using the same plugins that Roll

If you pass `skipSelf: true`, then the `resolveId` hook of the plugin from which `this.resolve` is called will be skipped when resolving.

#### `this.setAssetSource(assetReferenceId: string, source: string | UInt8Array) => void`
#### `this.setAssetSource(assetReferenceId: string, source: string | Uint8Array) => void`

Set the deferred source of an asset. Note that you can also pass a Node `Buffer` as `source` as it is a sub-class of `UInt8Array`.
Set the deferred source of an asset. Note that you can also pass a Node `Buffer` as `source` as it is a sub-class of `Uint8Array`.

#### `this.warn(warning: string | RollupWarning, position?: number | { column: number; line: number }) => void`

Expand All @@ -550,7 +550,7 @@ The `position` argument is a character index where the warning was raised. If pr

☢️ These context utility functions have been deprecated and may be removed in a future Rollup version.

- `this.emitAsset(assetName: string, source: string) => string` - _**Use [`this.emitFile`](guide/en/#thisemitfileemittedfile-emittedchunk--emittedasset--string)**_ - Emits a custom file that is included in the build output, returning an `assetReferenceId` that can be used to reference the emitted file. You can defer setting the source if you provide it later via [`this.setAssetSource(assetReferenceId, source)`](guide/en/#thissetassetsourceassetreferenceid-string-source-string--uint8array--void). A string or `UInt8Array`/`Buffer` source must be set for each asset through either method or an error will be thrown on generate completion.
- `this.emitAsset(assetName: string, source: string) => string` - _**Use [`this.emitFile`](guide/en/#thisemitfileemittedfile-emittedchunk--emittedasset--string)**_ - Emits a custom file that is included in the build output, returning an `assetReferenceId` that can be used to reference the emitted file. You can defer setting the source if you provide it later via [`this.setAssetSource(assetReferenceId, source)`](guide/en/#thissetassetsourceassetreferenceid-string-source-string--uint8array--void). A string or `Uint8Array`/`Buffer` source must be set for each asset through either method or an error will be thrown on generate completion.

Emitted assets will follow the [`output.assetFileNames`](guide/en/#outputassetfilenames) naming scheme. You can reference the URL of the file in any code returned by a [`load`](guide/en/#load) or [`transform`](guide/en/#transform) plugin hook via `import.meta.ROLLUP_ASSET_URL_assetReferenceId`.

Expand Down
60 changes: 39 additions & 21 deletions src/utils/FileEmitter.ts
Expand Up @@ -26,7 +26,7 @@ interface OutputSpecificFileData {

function generateAssetFileName(
name: string | undefined,
source: string | Buffer,
source: string | Uint8Array,
output: OutputSpecificFileData
): string {
const emittedName = name || 'asset';
Expand Down Expand Up @@ -68,7 +68,7 @@ interface ConsumedChunk {
interface ConsumedAsset {
fileName: string | undefined;
name: string | undefined;
source: string | Buffer | undefined;
source: string | Uint8Array | undefined;
type: 'asset';
}

Expand Down Expand Up @@ -109,14 +109,14 @@ function getValidSource(
source: unknown,
emittedFile: { fileName?: string; name?: string },
fileReferenceId: string | null
): string | Buffer {
if (typeof source !== 'string' && !Buffer.isBuffer(source)) {
): string | Uint8Array {
if (!(typeof source === 'string' || source instanceof Uint8Array)) {
const assetName = emittedFile.fileName || emittedFile.name || fileReferenceId;
return error(
errFailedValidation(
`Could not set source for ${
typeof assetName === 'string' ? `asset "${assetName}"` : 'unnamed asset'
}, asset source needs to be a string of Buffer.`
}, asset source needs to be a string, Uint8Array or Buffer.`
)
);
}
Expand Down Expand Up @@ -313,13 +313,13 @@ export class FileEmitter {

private finalizeAsset(
consumedFile: ConsumedFile,
source: string | Buffer,
source: string | Uint8Array,
referenceId: string,
output: OutputSpecificFileData
): void {
const fileName =
consumedFile.fileName ||
this.findExistingAssetFileNameWithSource(output.bundle, source) ||
findExistingAssetFileNameWithSource(output.bundle, source) ||
generateAssetFileName(consumedFile.name, source, output);

// We must not modify the original assets to avoid interaction between outputs
Expand All @@ -340,21 +340,39 @@ export class FileEmitter {
type: 'asset'
};
}
}

private findExistingAssetFileNameWithSource(
bundle: OutputBundleWithPlaceholders,
source: string | Buffer
): string | null {
for (const fileName of Object.keys(bundle)) {
const outputFile = bundle[fileName];
if (
outputFile.type === 'asset' &&
(Buffer.isBuffer(source) && Buffer.isBuffer(outputFile.source)
? source.equals(outputFile.source)
: source === outputFile.source)
)
return fileName;
function findExistingAssetFileNameWithSource(
bundle: OutputBundleWithPlaceholders,
source: string | Uint8Array
): string | null {
for (const fileName of Object.keys(bundle)) {
const outputFile = bundle[fileName];
if (outputFile.type === 'asset' && areSourcesEqual(source, outputFile.source)) return fileName;
}
return null;
}

function areSourcesEqual(
sourceA: string | Uint8Array | Buffer,
sourceB: string | Uint8Array | Buffer
): boolean {
if (typeof sourceA === 'string') {
return sourceA === sourceB;
}
if (typeof sourceB === 'string') {
return false;
}
if ('equals' in sourceA) {
return sourceA.equals(sourceB);
}
if (sourceA.length !== sourceB.length) {
return false;
}
for (let index = 0; index < sourceA.length; index++) {
if (sourceA[index] !== sourceB[index]) {
return false;
}
return null;
}
return true;
}
103 changes: 51 additions & 52 deletions test/form/index.js
Expand Up @@ -12,12 +12,13 @@ runTestSuiteWithSamples('form', path.resolve(__dirname, 'samples'), (dir, config
(config.skip ? itOrDescribe.skip : config.solo ? itOrDescribe.only : itOrDescribe)(
path.basename(dir) + ': ' + config.description,
() => {
let promise;
const runRollupTest = (inputFile, bundleFile, defaultFormat) => {
let bundle;
const runRollupTest = async (inputFile, bundleFile, defaultFormat) => {
if (config.before) config.before();
process.chdir(dir);
return (
promise ||
(promise = rollup.rollup(
bundle =
bundle ||
(await rollup.rollup(
extend(
{
input: dir + '/main.js',
Expand All @@ -35,21 +36,20 @@ runTestSuiteWithSamples('form', path.resolve(__dirname, 'samples'), (dir, config
},
config.options || {}
)
))
).then(bundle =>
generateAndTestBundle(
bundle,
extend(
{
file: inputFile,
format: defaultFormat
},
(config.options || {}).output || {}
),
bundleFile,
config
)
));
await generateAndTestBundle(
bundle,
extend(
{
file: inputFile,
format: defaultFormat
},
(config.options || {}).output || {}
),
bundleFile,
config
);
if (config.after) config.after();
};

if (isSingleFormatTest) {
Expand All @@ -69,42 +69,41 @@ runTestSuiteWithSamples('form', path.resolve(__dirname, 'samples'), (dir, config
);
});

function generateAndTestBundle(bundle, outputOptions, expectedFile, { show }) {
return bundle.write(outputOptions).then(() => {
const actualCode = normaliseOutput(sander.readFileSync(outputOptions.file));
let expectedCode;
let actualMap;
let expectedMap;
async function generateAndTestBundle(bundle, outputOptions, expectedFile, { show }) {
await bundle.write(outputOptions);
const actualCode = normaliseOutput(sander.readFileSync(outputOptions.file));
let expectedCode;
let actualMap;
let expectedMap;

try {
expectedCode = normaliseOutput(sander.readFileSync(expectedFile));
} catch (err) {
expectedCode = 'missing file';
}
try {
expectedCode = normaliseOutput(sander.readFileSync(expectedFile));
} catch (err) {
expectedCode = 'missing file';
}

try {
actualMap = JSON.parse(sander.readFileSync(outputOptions.file + '.map').toString());
actualMap.sourcesContent = actualMap.sourcesContent
? actualMap.sourcesContent.map(normaliseOutput)
: null;
} catch (err) {
assert.equal(err.code, 'ENOENT');
}
try {
actualMap = JSON.parse(sander.readFileSync(outputOptions.file + '.map').toString());
actualMap.sourcesContent = actualMap.sourcesContent
? actualMap.sourcesContent.map(normaliseOutput)
: null;
} catch (err) {
assert.equal(err.code, 'ENOENT');
}

try {
expectedMap = JSON.parse(sander.readFileSync(expectedFile + '.map').toString());
expectedMap.sourcesContent = actualMap.sourcesContent
? expectedMap.sourcesContent.map(normaliseOutput)
: null;
} catch (err) {
assert.equal(err.code, 'ENOENT');
}
try {
expectedMap = JSON.parse(sander.readFileSync(expectedFile + '.map').toString());
expectedMap.sourcesContent = actualMap.sourcesContent
? expectedMap.sourcesContent.map(normaliseOutput)
: null;
} catch (err) {
assert.equal(err.code, 'ENOENT');
}

if (show) {
console.log(actualCode + '\n\n\n');
}
if (show) {
console.log(actualCode + '\n\n\n');
}

assert.equal(actualCode, expectedCode);
assert.deepEqual(actualMap, expectedMap);
});
assert.equal(actualCode, expectedCode);
assert.deepEqual(actualMap, expectedMap);
}
29 changes: 29 additions & 0 deletions test/form/samples/emit-uint8array-no-buffer/_config.js
@@ -0,0 +1,29 @@
const Buffer = global.Buffer;

module.exports = {
description:
'supports emitting assets as Uint8Arrays when Buffer is not available with deduplication',
before() {
delete global.Buffer;
},
after() {
global.Buffer = Buffer;
},
options: {
plugins: {
resolveId(id) {
if (id.startsWith('asset')) {
return id;
}
},
load(id) {
if (id.startsWith('asset')) {
return `export default import.meta.ROLLUP_FILE_URL_${this.emitFile({
type: 'asset',
source: Uint8Array.from(Array.from(id.slice(0, -1)).map(char => char.charCodeAt(0)))
})};`;
}
}
}
}
};
15 changes: 15 additions & 0 deletions test/form/samples/emit-uint8array-no-buffer/_expected/amd.js
@@ -0,0 +1,15 @@
define(['require'], function (require) { 'use strict';

var asset1a = new URL(require.toUrl('./assets/asset-dc5cb674'), document.baseURI).href;

var asset1b = new URL(require.toUrl('./assets/asset-dc5cb674'), document.baseURI).href;

var asset2a = new URL(require.toUrl('./assets/asset-52cbd095'), document.baseURI).href;

var asset2b = new URL(require.toUrl('./assets/asset-52cbd095'), document.baseURI).href;

var asset99a = new URL(require.toUrl('./assets/asset-c568a840'), document.baseURI).href;

console.log(asset1a, asset1b, asset2a, asset2b, asset99a);

});
@@ -0,0 +1 @@
asset2
@@ -0,0 +1 @@
asset99
@@ -0,0 +1 @@
asset1
13 changes: 13 additions & 0 deletions test/form/samples/emit-uint8array-no-buffer/_expected/cjs.js
@@ -0,0 +1,13 @@
'use strict';

var asset1a = (typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __dirname + '/assets/asset-dc5cb674').href : new URL('assets/asset-dc5cb674', document.currentScript && document.currentScript.src || document.baseURI).href);

var asset1b = (typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __dirname + '/assets/asset-dc5cb674').href : new URL('assets/asset-dc5cb674', document.currentScript && document.currentScript.src || document.baseURI).href);

var asset2a = (typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __dirname + '/assets/asset-52cbd095').href : new URL('assets/asset-52cbd095', document.currentScript && document.currentScript.src || document.baseURI).href);

var asset2b = (typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __dirname + '/assets/asset-52cbd095').href : new URL('assets/asset-52cbd095', document.currentScript && document.currentScript.src || document.baseURI).href);

var asset99a = (typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __dirname + '/assets/asset-c568a840').href : new URL('assets/asset-c568a840', document.currentScript && document.currentScript.src || document.baseURI).href);

console.log(asset1a, asset1b, asset2a, asset2b, asset99a);
11 changes: 11 additions & 0 deletions test/form/samples/emit-uint8array-no-buffer/_expected/es.js
@@ -0,0 +1,11 @@
var asset1a = new URL('assets/asset-dc5cb674', import.meta.url).href;

var asset1b = new URL('assets/asset-dc5cb674', import.meta.url).href;

var asset2a = new URL('assets/asset-52cbd095', import.meta.url).href;

var asset2b = new URL('assets/asset-52cbd095', import.meta.url).href;

var asset99a = new URL('assets/asset-c568a840', import.meta.url).href;

console.log(asset1a, asset1b, asset2a, asset2b, asset99a);
16 changes: 16 additions & 0 deletions test/form/samples/emit-uint8array-no-buffer/_expected/iife.js
@@ -0,0 +1,16 @@
(function () {
'use strict';

var asset1a = new URL('assets/asset-dc5cb674', document.currentScript && document.currentScript.src || document.baseURI).href;

var asset1b = new URL('assets/asset-dc5cb674', document.currentScript && document.currentScript.src || document.baseURI).href;

var asset2a = new URL('assets/asset-52cbd095', document.currentScript && document.currentScript.src || document.baseURI).href;

var asset2b = new URL('assets/asset-52cbd095', document.currentScript && document.currentScript.src || document.baseURI).href;

var asset99a = new URL('assets/asset-c568a840', document.currentScript && document.currentScript.src || document.baseURI).href;

console.log(asset1a, asset1b, asset2a, asset2b, asset99a);

}());