Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions e2e/react_native_0_60x_multibundle/__tests__/external.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
validateBaseBundle,
validateHostBundle,
validateAppBundle,
validateAppBundleWithChunk,
} from '../../utils/validators';

const TEST_PROJECT_DIR = path.resolve(
Expand Down Expand Up @@ -100,14 +99,12 @@ describe('for external bundle', () => {
validateHostBundle(iosBundles.host);
validateAppBundle(iosBundles.app0, { platform: 'ios' });
validateAppBundle(iosBundles.app1, { platform: 'ios' });
validateAppBundleWithChunk(iosBundles.app1, iosBundles.app1Chunk);

const androidBundles = await fetchBundles('android');
validateBaseBundle(androidBundles.baseDll, { platform: 'android' });
validateHostBundle(androidBundles.host);
validateAppBundle(androidBundles.app0, { platform: 'android' });
validateAppBundle(androidBundles.app1, { platform: 'android' });
validateAppBundleWithChunk(androidBundles.app1, androidBundles.app1Chunk);

stopServer(server);
});
Expand Down Expand Up @@ -175,9 +172,6 @@ async function fetchBundles(platform: string) {
const app1 = await (
await fetch(`http://localhost:9000/app1.${platform}.bundle`)
).buffer();
const app1Chunk = await (
await fetch(`http://localhost:9000/0.app1.${platform}.bundle`)
).buffer();

await (
await fetch(
Expand All @@ -192,6 +186,5 @@ async function fetchBundles(platform: string) {
host,
app0,
app1,
app1Chunk,
};
}
31 changes: 12 additions & 19 deletions e2e/react_native_0_60x_multibundle/__tests__/start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
validateAppBundle,
validateBaseBundle,
validateHostBundle,
validateAppBundleWithChunk,
} from '../../utils/validators';

const TEST_PROJECT_DIR = path.resolve(
Expand Down Expand Up @@ -39,7 +38,6 @@ describe('packager server', () => {
validateHostBundle(bundles.host);
validateAppBundle(bundles.app0, { platform: 'ios' });
validateAppBundle(bundles.app1, { platform: 'ios' });
validateAppBundleWithChunk(bundles.app1, bundles.app1Chunk);
});

it('compile bundles for Android', async () => {
Expand All @@ -48,32 +46,27 @@ describe('packager server', () => {
validateHostBundle(bundles.host);
validateAppBundle(bundles.app0, { platform: 'android' });
validateAppBundle(bundles.app1, { platform: 'android' });
validateAppBundleWithChunk(bundles.app1, bundles.app1Chunk);
});
});

async function fetchBundles(platform: string) {
const host = await (await fetch(
`http://localhost:${PORT}/host.${platform}.bundle`
)).buffer();
const baseDll = await (await fetch(
`http://localhost:${PORT}/base_dll.${platform}.bundle`
)).buffer();
const app0 = await (await fetch(
`http://localhost:${PORT}/app0.${platform}.bundle`
)).buffer();
const app1 = await (await fetch(
`http://localhost:${PORT}/app1.${platform}.bundle`
)).buffer();
const app1Chunk = await (await fetch(
`http://localhost:${PORT}/0.app1.${platform}.bundle`
)).buffer();
const host = await (
await fetch(`http://localhost:${PORT}/host.${platform}.bundle`)
).buffer();
const baseDll = await (
await fetch(`http://localhost:${PORT}/base_dll.${platform}.bundle`)
).buffer();
const app0 = await (
await fetch(`http://localhost:${PORT}/app0.${platform}.bundle`)
).buffer();
const app1 = await (
await fetch(`http://localhost:${PORT}/app1.${platform}.bundle`)
).buffer();

return {
baseDll,
host,
app0,
app1,
app1Chunk,
};
}
16 changes: 0 additions & 16 deletions e2e/utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,6 @@ export function validateAppBundle(
expect(bundleBuffer.toString()).toMatch(/this\["\w+"\] =/);
}

export function validateAppBundleWithChunk(
bundleBuffer: Buffer,
chunkBuffer: Buffer
) {
const bundle = bundleBuffer.toString();
expect(bundle).toMatch('function asyncEval');
expect(bundle).toMatch(
/return asyncEval\(__webpack_require__\.p \+ "" \+ chunkId \+ "\.app1\.(ios|android)\.bundle"\);/g
);

const chunk = chunkBuffer.toString();
expect(chunk).toMatch('this["webpackChunkapp1"]');
expect(chunk).toMatch('./src/async.js');
expect(chunk).not.toMatch('base_dll');
}

export async function fetchAndValidateBundle(url: string) {
const res = await fetch(url);
const bundle = await res.text();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,112 +1,16 @@
import webpack from 'webpack';
import { RawSource } from 'webpack-sources';
import Concat from 'concat-with-sourcemaps';

const asyncEval = `
// Fetch and eval async chunks
function asyncEval(url) {
return fetch(url)
.then(res => res.text())
.then(src => eval(src + "\\n//# sourceURL=" + url));
}
`;

/**
* Adds React Native specific tweaks to bootstrap logic.
*/
export default class WebpackBasicBundlePlugin {
private bundle: boolean;
private sourceMap: boolean;
private preloadBundles: string[];

constructor({
bundle,
sourceMap,
preloadBundles,
}: {
bundle: boolean;
sourceMap?: boolean;
preloadBundles?: string[];
}) {
this.bundle = bundle;
this.sourceMap = Boolean(sourceMap);
constructor({ preloadBundles }: { preloadBundles?: string[] }) {
this.preloadBundles = preloadBundles || [];
}

apply(compiler: webpack.Compiler) {
if (this.bundle) {
// When creating basic bundle (non-RAM), async chunks will be concatenated into main bundle.
// This will allow easy switching between RAM bundle and non-RAM static bundle.
compiler.hooks.emit.tap('WebpackBasicBundlePlugin', compilation => {
// Skip if there is no async chunks.
if (compilation.chunks.length === 1) {
return;
}

const {
mainMap,
mainSource: mainSourceFilename,
asyncChunks,
} = this.getFilenamesFromChunks(compilation.chunks);

const sourceMappingRegex = /\/\/# sourceMappingURL=(.+)/;
const mainSource = compilation.assets[mainSourceFilename].source();
const sourceMappingMatch = new RegExp(sourceMappingRegex, 'g').exec(
mainSource
);

// Concatenate all chunks and its source maps. Chunks source needs to have source mapping URL
// removed, since it will be added at the end of the whole bundle.
const concat = new Concat(true, mainSourceFilename, '\n');
concat.add(
mainSourceFilename,
mainSource.replace(new RegExp(sourceMappingRegex, 'g'), ''),
this.sourceMap ? compilation.assets[mainMap].source() : undefined
);
asyncChunks.forEach(chunk => {
concat.add(
chunk.source,
(compilation.assets[chunk.source].source() as string).replace(
new RegExp(sourceMappingRegex, 'g'),
''
),
this.sourceMap ? compilation.assets[chunk.map].source() : undefined
);
});

// Add single source mapping url pointing to file with concatenated source maps.
if (sourceMappingMatch) {
concat.add(null, sourceMappingMatch[0]);
}

// Remove async chunks
const filesToRemove: string[] = compilation.chunks.reduce(
(acc, chunk) => {
if (chunk.name !== 'main') {
return [...acc, ...chunk.files];
}
return acc;
},
[]
);
Object.keys(compilation.assets).forEach(assetName => {
const remove = filesToRemove.some(file => assetName.endsWith(file));
if (remove) {
delete compilation.assets[assetName];
}
});

// Assign concatenated bundle to main asset
compilation.assets[mainSourceFilename] = new RawSource(
concat.content.toString('utf8')
);
if (this.sourceMap) {
// Assign concatenated source maps to main source map.
compilation.assets[mainMap] = new RawSource(concat.sourceMap || '');
}
});
}

compiler.hooks.compilation.tap('WebpackBasicBundlePlugin', compilation => {
(compilation.mainTemplate as any).hooks.bootstrap.tap(
'WebpackBasicBundlePlugin',
Expand All @@ -117,48 +21,9 @@ export default class WebpackBasicBundlePlugin {
`if (!this["${bundleName}"]) { this.bundleRegistryLoad("${bundleName}", true, true); }\n`
)}\n`
: '';
// Add asyncEval only when serving from packager server. When bundling async
// chunks will be concatenated into the bundle.
return `${preload}${this.bundle ? '' : asyncEval}\n${source}`;
}
);

(compilation.mainTemplate as any).hooks.requireEnsure.tap(
'WebpackBasicBundlePlugin',
(source: string) => {
// throw new Error(typeof source);
// The is no `importScripts` in react-native. Replace it with Promise based
// fetch + eval and return the promise so the webpack module system and bootstrapping
// logic is not broken.
// When creating static bundle, async chunks will be concatenated into the bundle,
// so by the time they are required, they should already be loaded into the module system.
return source.replace(
/importScripts\((.+)\)/gm,
this.bundle
? 'throw new Error("Invalid bundle: async chunk not loaded. ' +
'Please open an issue at https://github.com/callstack/haul")'
: 'return asyncEval($1)'
);
return `${preload}\n${source}`;
}
);
});
}

private getFilenamesFromChunks(
chunks: Array<{ name: string; files: string[] }>
) {
const mainChunk = chunks.find(chunk => chunk.name === 'main');
const otherChunks = chunks.filter(chunk => chunk.name !== 'main');

const mapRegex = /\.map$/;

return {
mainSource: mainChunk!.files.find(item => !mapRegex.test(item)) || '',
mainMap: mainChunk!.files.find(item => mapRegex.test(item)) || '',
asyncChunks: otherChunks.map(chunk => ({
source: chunk!.files.find(item => !mapRegex.test(item)) || '',
map: chunk!.files.find(item => mapRegex.test(item)) || '',
})),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,90 +43,20 @@ describe('WebpackBasicBundlePlugin', () => {
await del(path.join(__dirname, './__fixtures__/tmp-*'));
});

it('should prepare bundle for packager server', async () => {
it('should prepare bundle', async () => {
const bundle = await build({
...getConfig(),
plugins: [
new WebpackBasicBundlePlugin({
bundle: false,
}),
],
});

expect(bundle).toMatch(/function asyncEval/);
expect(bundle).toMatch(/return asyncEval\(.+\)/);
expect(bundle).toMatch(/console.log\('entry'\)/);
expect(bundle).not.toMatch(/console.log\('async'\)/);
expect(bundle).not.toMatch(/bundleRegistryLoad/);
});

it('should prepare bundle for packager server with preload bundles', async () => {
const bundle = await build({
...getConfig(),
plugins: [
new WebpackBasicBundlePlugin({
bundle: false,
preloadBundles: ['test_bundle'],
}),
],
});

expect(bundle).toMatch(/function asyncEval/);
expect(bundle).toMatch(/return asyncEval\(.+\)/);
expect(bundle).toMatch(/console.log\('entry'\)/);
expect(bundle).not.toMatch(/console.log\('async'\)/);
expect(bundle).toMatch(
/if \(!this\["test_bundle"\]\) { this.bundleRegistryLoad\("test_bundle", true, true\); }/
);
});

it('should prepare offline bundle', async () => {
const bundle = await build({
...getConfig(),
plugins: [
new WebpackBasicBundlePlugin({
bundle: true,
preloadBundles: ['test_bundle'],
}),
],
});

expect(bundle).not.toMatch(/asyncEval/);
expect(bundle).not.toMatch(/return asyncEval\(.+\)/);
expect(bundle).toMatch(/Invalid bundle: async chunk not loaded/);
expect(bundle).toMatch(/console.log\('entry'\)/);
expect(bundle).toMatch(/console.log\('async'\)/);
expect(bundle).toMatch(
/if \(!this\["test_bundle"\]\) { this.bundleRegistryLoad\("test_bundle", true, true\); }/
);
});

it('should prepare offline bundle and merge sourcemaps', async () => {
const bundle = await build({
...getConfig(),
plugins: [
new webpack.SourceMapDevToolPlugin({
test: /\.(js|jsx|css|ts|tsx|(js)?bundle)($|\?)/i,
module: true,
filename: '[file].map',
moduleFilenameTemplate: '[absolute-resource-path]',
}),
new WebpackBasicBundlePlugin({
bundle: true,
sourceMap: true,
preloadBundles: ['test_bundle'],
}),
],
});

expect(bundle).not.toMatch(/asyncEval/);
expect(bundle).not.toMatch(/return asyncEval\(.+\)/);
expect(bundle).toMatch(/Invalid bundle: async chunk not loaded/);
expect(bundle).toMatch(/console.log\('entry'\)/);
expect(bundle).toMatch(/console.log\('async'\)/);
expect((bundle.match(/sourceMappingURL/g) || []).length).toBe(1);
expect(bundle).toMatch(
/if \(!this\["test_bundle"\]\) { this.bundleRegistryLoad\("test_bundle", true, true\); }/
);
});
});
Loading