Skip to content

Commit

Permalink
add normalizeModuleIds option
Browse files Browse the repository at this point in the history
supports #9
  • Loading branch information
Daniel Schaffer committed Jan 16, 2019
1 parent 023c2f5 commit f8b1a40
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 205 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ versions that don't support `<script type="module">`
* **`targets[browserProfile].noModule`** (`boolean`) - Determines whether
this target can be referenced by a `<script nomodule>` tag. Only
one target may have this property set to `true`.
* **`safari10NoModuleFix`** (`boolean` | `'inline'` | `'inline-data'` | `'inline-data-base64'`) - Embeds a polyfill/workaround
* **`safari10NoModuleFix`** (`boolean` | `'external'`, `'inline'` | `'inline-data'` | `'inline-data-base64'`) - Embeds a polyfill/workaround
to allow the `nomodule` attribute to function correctly in Safari 10.1.
See #9 for more information.
* `false` - disabled (default)
Expand All @@ -98,6 +98,12 @@ See #9 for more information.
* `'inline-data-base64'` - adds the nomodule fix using a script tag with a base64-encoded data url (`HtmlWebpackPlugin` only)
* `'external'` - adds the nomodule fix as a separate file linked with a `<script src>` tag

* **`normalizeModuleIds`**: (`boolean`) - **EXPERIMENTAL**. Removes the babel targeting query from module ids so they
use what the module id would be without using `BabelMultiTargetPlugin`, and adds a check to webpack's bootstrapping
code that stops bundle code from executing if it detects that webpack has already been bootstrapped elsewhere.
This has the effect of preventing duplicate modules from loading in instances where the browser loads both bundles
(e.g. Safari 10.1).

## Configuration Examples

### Basic Usage
Expand Down
2 changes: 1 addition & 1 deletion examples/webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ module.exports = (workingDir, options = {}) => ({
template: options.template || '../index.pug',
}),

new BabelMultiTargetPlugin({ safari10NoModuleFix: 'external', preventDuplicateChunkLoading: true }),
new BabelMultiTargetPlugin({ normalizeModuleIds: true }),
],

});
8 changes: 4 additions & 4 deletions src/babel.multi.target.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { BabelPresetOptions } from 'babel-loader';
import { TargetOptionsMap } from './babel.target.options';

export enum SafariNoModuleFix {
bundled = 'bundled',
external = 'external',
inline = 'inline',
inlineData = 'inline-data',
Expand Down Expand Up @@ -66,8 +65,9 @@ export interface Options {
safari10NoModuleFix?: SafariNoModuleFixOption;

/**
* EXPERIMENTAL. Adds gating logic to Webpack's bootstrapping code to prevent execution of duplicate chunks between
* targets.
* EXPERIMENTAL. Removes babel targeting query from module ids so they use what the module id would be without using
* the BabelMultiTargetPlugin. This has the effect of preventing duplicate modules from running in instances where
* the browser loads both bundles (e.g. Safari 10.1).
*/
preventDuplicateChunkLoading?: boolean;
normalizeModuleIds?: boolean;
}
6 changes: 3 additions & 3 deletions src/babel.multi.target.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { BabelTargetEntryOptionPlugin } from './babel.target.entry.option.plu
import { BrowserProfileName } from './browser.profile.name';
import { DEFAULT_TARGET_INFO } from './defaults';
import { NormalizeCssChunksPlugin } from './normalize.css.chunks.plugin';
import { PreventDuplicateChunksPlugin } from './prevent.duplicate.chunks.plugin';
import { NormalizeModuleIdsPlugin } from './normalize.module.ids.plugin';
import { SafariNoModuleFixPlugin } from './safari-nomodule-fix/safari.nomodule.fix.plugin';
import { TargetingPlugin } from './targeting.plugin';

Expand Down Expand Up @@ -75,8 +75,8 @@ export class BabelMultiTargetPlugin implements Plugin {
if (this.options.safari10NoModuleFix) {
new SafariNoModuleFixPlugin(this.options.safari10NoModuleFix).apply(compiler)
}
if (this.options.preventDuplicateChunkLoading) {
new PreventDuplicateChunksPlugin(this.targets).apply(compiler);
if (this.options.normalizeModuleIds) {
new NormalizeModuleIdsPlugin().apply(compiler);
}
}

Expand Down
122 changes: 122 additions & 0 deletions src/normalize.module.ids.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { AlterAssetTagsData, HtmlTag, HtmlWebpackPlugin } from 'html-webpack-plugin';
import { Compiler, Module, Plugin, compilation } from 'webpack';

import Compilation = compilation.Compilation;

import { BabelTarget } from './babel.target';

export class NormalizeModuleIdsPlugin implements Plugin {

public apply(compiler: Compiler): void {
this.applyModuleIdNormalizing(compiler);
this.applyConditionJsonpCallback(compiler);
this.applyHtmlWebpackTagOrdering(compiler);
}

private pluginName(desc?: string): string {
return `${NormalizeModuleIdsPlugin.name}${desc ? ': ' : ''}${desc || ''}`
}

private applyModuleIdNormalizing(compiler: Compiler): void {
compiler.hooks.compilation.tap(this.pluginName(), (compilation: Compilation) => {
if (compilation.name) {
return;
}
compilation.hooks.moduleIds.tap(this.pluginName(), (modules: Module[]) => {
modules.forEach(module => {
if (BabelTarget.isTaggedRequest(module.id)) {
const queryIndex = module.id.indexOf('?');
const ogId = module.id.substring(0, queryIndex);
const query = module.id.substring(queryIndex + 1);
const queryParts = query.split('&').filter((part: string) => !part.startsWith('babel-target'));
if (!queryParts.length) {
module.id = ogId;
} else {
module.id = `${ogId}?${queryParts.join('&')}`;
}
}
});
});
});
}

private applyConditionJsonpCallback(compiler: Compiler): void {
compiler.hooks.afterPlugins.tap(this.pluginName(), () => {
compiler.hooks.thisCompilation.tap(this.pluginName(), (compilation: Compilation) => {
if (compilation.name) {
return;
}
compilation.mainTemplate.hooks.beforeStartup.tap(this.pluginName('conditional jsonp callback'), (source: string) => {
const insertPointCode = 'var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);\n'
const insertPoint = source.indexOf(insertPointCode);
if (insertPoint < 0) {
return;
}
const before = source.substring(0, insertPoint);
const after = source.substring(insertPoint);
return `${before}if (jsonpArray.push.name === 'webpackJsonpCallback') return;\n${after}`;

});
});
return compiler;
});
}

private applyHtmlWebpackTagOrdering(compiler: Compiler): void {

compiler.hooks.afterPlugins.tap(this.pluginName(), () => {

const htmlWebpackPlugin: HtmlWebpackPlugin = compiler.options.plugins
// instanceof can act wonky since we don't actually keep our own dependency on html-webpack-plugin
// should we?
.find(plugin => plugin.constructor.name === 'HtmlWebpackPlugin') as any;

if (!htmlWebpackPlugin) {
return;
}

compiler.hooks.compilation.tap(this.pluginName(), (compilation: Compilation) => {

if (compilation.name) {
return;
}

compilation.hooks.htmlWebpackPluginAlterAssetTags.tapPromise(this.pluginName('reorder asset tags'),
async (htmlPluginData: AlterAssetTagsData) => {

const tags = htmlPluginData.body.slice(0);

// re-sort the tags so that es module tags are rendered first, otherwise maintaining the original order
htmlPluginData.body.sort((a: HtmlTag, b: HtmlTag) => {
const aIndex = tags.indexOf(a);
const bIndex = tags.indexOf(b);
if (a.tagName !== 'script' || b.tagName !== 'script' ||
!a.attributes || !b.attributes ||
!a.attributes.src || !b.attributes.src ||
(a.attributes.type !== 'module' && b.attributes.type !== 'module')) {
// use the original order
return aIndex - bIndex;
}

if (a.attributes.type === 'module') {
return -1;
}
return 1;
});

htmlPluginData.body.forEach((tag: HtmlTag) => {
if (tag.tagName === 'script' && tag.attributes && tag.attributes.nomodule) {
tag.attributes.defer = true;
}
});


return htmlPluginData;

});

});
});
}

}
67 changes: 0 additions & 67 deletions src/prevent.duplicate.chunks.plugin.ts

This file was deleted.

17 changes: 0 additions & 17 deletions src/safari-nomodule-fix/safari.nomodule.fix.dependency.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,16 @@
import { resolve } from 'path';
import * as webpack from 'webpack';
import { ReplaceSource } from 'webpack-sources';

import ModuleDependency = require('webpack/lib/dependencies/ModuleDependency');
import Dependency = webpack.compilation.Dependency;
import RuntimeTemplate = webpack.compilation.RuntimeTemplate;

const SAFARI_NOMODULE_FIX_FILENAME = 'safari.nomodule.fix';

class Template {
apply(dep: Dependency, source: ReplaceSource, runtime: RuntimeTemplate) {
// use the provided webpack runtime template to fabricate a require statement for the nomodule fix
// this actually executes the fix code
// const content = runtime.moduleRaw({
// module: dep.module,
// request: dep.request
// });
// source.replace(-100, -101, content + ';\n');
}
};

export class SafariNoModuleFixDependency extends ModuleDependency {

public readonly type = 'safari nomodule fix';
public static readonly context = __dirname;
public static readonly modulename = SAFARI_NOMODULE_FIX_FILENAME;
public static readonly filename = SAFARI_NOMODULE_FIX_FILENAME + '.js';
public static readonly path = resolve(__dirname, SAFARI_NOMODULE_FIX_FILENAME + '.js');
public static readonly Template = Template

constructor() {
super(resolve(__dirname, SAFARI_NOMODULE_FIX_FILENAME))
Expand Down

0 comments on commit f8b1a40

Please sign in to comment.