Skip to content
This repository was archived by the owner on May 17, 2019. It is now read-only.
3 changes: 2 additions & 1 deletion build/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ function Compiler(
clientChunkMetadata,
legacyClientChunkMetadata,
mergedClientChunkMetadata,
i18nManifest: new DeferredState(),
i18nManifest: new Map(),
i18nDeferredManifest: new DeferredState(),
legacyBuildEnabled,
};
const root = path.resolve(dir);
Expand Down
42 changes: 27 additions & 15 deletions build/get-webpack-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const JS_EXT_PATTERN = /\.jsx?$/;
/*::
import type {
ClientChunkMetadataState,
TranslationsManifest,
TranslationsManifestState,
LegacyBuildEnabledState,
} from "./types.js";
Expand All @@ -86,7 +87,8 @@ export type WebpackConfigOpts = {|
clientChunkMetadata: ClientChunkMetadataState,
legacyClientChunkMetadata: ClientChunkMetadataState,
mergedClientChunkMetadata: ClientChunkMetadataState,
i18nManifest: TranslationsManifestState,
i18nManifest: TranslationsManifest,
i18nDeferredManifest: TranslationsManifestState,
legacyBuildEnabled: LegacyBuildEnabledState,
},
fusionConfig: FusionRC,
Expand Down Expand Up @@ -451,10 +453,13 @@ function getWebpackConfig(opts /*: WebpackConfigOpts */) {
state.mergedClientChunkMetadata
),
runtime === 'client'
? new I18nDiscoveryPlugin(state.i18nManifest)
? new I18nDiscoveryPlugin(
state.i18nDeferredManifest,
state.i18nManifest
)
: new LoaderContextProviderPlugin(
translationsManifestContextKey,
state.i18nManifest
state.i18nDeferredManifest
),
!dev && zopfli && zopfliWebpackPlugin,
!dev && brotliWebpackPlugin,
Expand All @@ -465,17 +470,21 @@ function getWebpackConfig(opts /*: WebpackConfigOpts */) {
// in dev because the CLI will not exit with an error code if the option is enabled,
// so failed builds would look like successful ones.
watch && new webpack.NoEmitOnErrorsPlugin(),
new InstrumentedImportDependencyTemplatePlugin(
runtime !== 'client'
? // Server
state.mergedClientChunkMetadata
: /**
* Client
* Don't wait for the client manifest on the client.
* The underlying plugin handles client instrumentation on its own.
*/
void 0
),
runtime === 'server'
? // Server
new InstrumentedImportDependencyTemplatePlugin({
compilation: 'server',
clientChunkMetadata: state.mergedClientChunkMetadata,
})
: /**
* Client
* Don't wait for the client manifest on the client.
* The underlying plugin is able determine client chunk metadata on its own.
*/
new InstrumentedImportDependencyTemplatePlugin({
compilation: 'client',
i18nManifest: state.i18nManifest,
}),
dev && hmr && watch && new webpack.HotModuleReplacementPlugin(),
!dev && runtime === 'client' && new webpack.HashedModuleIdsPlugin(),
runtime === 'client' &&
Expand Down Expand Up @@ -527,7 +536,10 @@ function getWebpackConfig(opts /*: WebpackConfigOpts */) {
options.optimization.splitChunks
),
// need to re-apply template
new InstrumentedImportDependencyTemplatePlugin(void 0),
new InstrumentedImportDependencyTemplatePlugin({
compilation: 'client',
i18nManifest: state.i18nManifest,
}),
new ClientChunkMetadataStateHydratorPlugin(
state.legacyClientChunkMetadata
),
Expand Down
19 changes: 11 additions & 8 deletions build/plugins/i18n-discovery-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,30 @@ import type {TranslationsManifestState, TranslationsManifest} from "../types.js"

class I18nDiscoveryPlugin {
/*::
manifest: TranslationsManifestState;
discoveryState: TranslationsManifest;
manifestState: TranslationsManifestState;
manifest: TranslationsManifest;
*/
constructor(manifest /*: TranslationsManifestState*/) {
constructor(
manifestState /*: TranslationsManifestState*/,
manifest /*: TranslationsManifest*/
) {
this.manifestState = manifestState;
this.manifest = manifest;
this.discoveryState = new Map();
}
apply(compiler /*: any */) {
const name = this.constructor.name;
// "thisCompilation" is not run in child compilations
compiler.hooks.thisCompilation.tap(name, compilation => {
compilation.hooks.normalModuleLoader.tap(name, (context, module) => {
context[translationsDiscoveryKey] = this.discoveryState;
context[translationsDiscoveryKey] = this.manifest;
});
});
compiler.hooks.done.tap(name, () => {
this.manifest.resolve(this.discoveryState);
this.manifestState.resolve(this.manifest);
});
compiler.hooks.invalid.tap(name, filename => {
this.manifest.reset();
this.discoveryState.delete(filename);
this.manifestState.reset();
this.manifest.delete(filename);
});
}
}
Expand Down
89 changes: 77 additions & 12 deletions build/plugins/instrumented-import-dependency-template-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,25 @@
/* eslint-env node */

/*::
import type {ClientChunkMetadataState, ClientChunkMetadata} from "../types.js";
import type {
ClientChunkMetadataState,
ClientChunkMetadata,
TranslationsManifest,
} from "../types.js";

type InstrumentationPluginOpts =
| ClientPluginOpts
| ServerPluginOpts;

type ServerPluginOpts = {
compilation: "server",
clientChunkMetadata: ClientChunkMetadataState
};

type ClientPluginOpts = {
compilation: "client",
i18nManifest: TranslationsManifest
};
*/

const ImportDependency = require('webpack/lib/dependencies/ImportDependency');
Expand All @@ -34,9 +52,16 @@ const ImportDependencyTemplate = require('webpack/lib/dependencies/ImportDepende

class InstrumentedImportDependencyTemplate extends ImportDependencyTemplate {
/*:: clientChunkIndex: ?$PropertyType<ClientChunkMetadata, "fileManifest">; */

constructor(clientChunkMetadata /*: ?ClientChunkMetadata */) {
/*:: manifest: ?TranslationsManifest; */

constructor(
{
clientChunkMetadata,
translationsManifest,
} /*: {clientChunkMetadata?: ClientChunkMetadata, translationsManifest?: TranslationsManifest}*/
) {
super();
this.translationsManifest = translationsManifest;
if (clientChunkMetadata) {
this.clientChunkIndex = clientChunkMetadata.fileManifest;
}
Expand Down Expand Up @@ -69,13 +94,26 @@ class InstrumentedImportDependencyTemplate extends ImportDependencyTemplate {
chunkIds = getChunkGroupIds(depBlock.chunkGroup);
}

let translationKeys = [];
if (this.translationsManifest) {
const modules = getChunkGroupModules(dep);
for (const module of modules) {
if (this.translationsManifest.has(module)) {
const keys = this.translationsManifest.get(module).keys();
translationKeys.push(...keys);
}
}
}

// Add the following properties to the promise returned by import()
// - `__CHUNK_IDS`: the webpack chunk ids for the dynamic import
// - `__MODULE_ID`: the webpack module id of the dynamically imported module. Equivalent to require.resolveWeak(path)
// - `__I18N_KEYS`: the translation keys used in the client chunk group for this import()
const customContent = chunkIds
? `Object.defineProperties(${content}, {
"__CHUNK_IDS": {value:${JSON.stringify(chunkIds)}},
"__MODULE_ID": {value:${JSON.stringify(dep.module.id)}}
"__MODULE_ID": {value:${JSON.stringify(dep.module.id)}},
"__I18N_KEYS": {value:${JSON.stringify(translationKeys)}}
})`
: content;

Expand All @@ -90,10 +128,10 @@ class InstrumentedImportDependencyTemplate extends ImportDependencyTemplate {
*/

class InstrumentedImportDependencyTemplatePlugin {
/*:: clientChunkIndexState: ?ClientChunkMetadataState; */
/*:: opts: InstrumentationPluginOpts;*/

constructor(clientChunkIndexState /*: ?ClientChunkMetadataState*/) {
this.clientChunkIndexState = clientChunkIndexState;
constructor(opts /*: InstrumentationPluginOpts*/) {
this.opts = opts;
}

apply(compiler /*: any */) {
Expand All @@ -104,22 +142,30 @@ class InstrumentedImportDependencyTemplatePlugin {
* `make` is the subsequent lifeycle method, so we can override this value here.
*/
compiler.hooks.make.tapAsync(name, (compilation, done) => {
if (this.clientChunkIndexState) {
if (this.opts.compilation === 'server') {
// server
this.clientChunkIndexState.result.then(chunkIndex => {
this.opts.clientChunkMetadata.result.then(chunkIndex => {
compilation.dependencyTemplates.set(
ImportDependency,
new InstrumentedImportDependencyTemplate(chunkIndex)
new InstrumentedImportDependencyTemplate({
clientChunkMetadata: chunkIndex,
})
);
done();
});
} else {
} else if (this.opts.compilation === 'client') {
// client
compilation.dependencyTemplates.set(
ImportDependency,
new InstrumentedImportDependencyTemplate()
new InstrumentedImportDependencyTemplate({
translationsManifest: this.opts.i18nManifest,
})
);
done();
} else {
throw new Error(
'InstrumentationImportDependencyPlugin called without clientChunkIndexState or translationsManifest'
);
}
});
}
Expand All @@ -139,3 +185,22 @@ function getChunkGroupIds(chunkGroup) {
return [chunkGroup.id];
}
}

function getChunkGroupModules(dep) {
const modulesSet = new Set();
// For ConcatenatedModules in production build
if (dep.module && dep.module.dependencies) {
dep.module.dependencies.forEach(dependency => {
if (dependency.originModule) {
modulesSet.add(dependency.originModule.userRequest);
}
});
}
// For NormalModules
dep.block.chunkGroup.chunks.forEach(chunk => {
for (const module of chunk._modules) {
modulesSet.add(module.resource);
}
});
return modulesSet;
}
2 changes: 1 addition & 1 deletion test/e2e/assets/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ test('`fusion dev` works with assets', async () => {
const clientMain = await request(`${url}/_static/client-main.js`);
t.ok(clientMain, 'serves client-main from memory correctly');
t.ok(
clientMain.includes('"src", "src/main.js")'),
clientMain.includes('"src","src/main.js")'),
'transpiles __dirname and __filename'
);
t.ok(
Expand Down
24 changes: 24 additions & 0 deletions test/e2e/dynamic-import-translations/fixture/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// @noflow

import React from 'react';
import App from 'fusion-react';

function Root () {
const split = import('./split.js');
const splitWithChild = import('./split-with-child.js');
return (
<div>
<div data-testid="split">
{JSON.stringify(split.__I18N_KEYS)}
</div>
<div data-testid="split-with-child">
{JSON.stringify(splitWithChild.__I18N_KEYS)}
</div>
</div>
);
}

export default async function start() {
const app = new App(<Root />);
return app;
}
10 changes: 10 additions & 0 deletions test/e2e/dynamic-import-translations/fixture/src/split-child.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @noflow

import React from 'react';
import {Translate} from 'fusion-plugin-i18n-react';

export default function SplitRouteChild() {
return (
<Translate id="__SPLIT_CHILD__"/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @noflow

import React, {Component} from 'react';
import {withTranslations} from 'fusion-plugin-i18n-react';

import SplitRouteChild from './split-child.js';

function SplitRouteWithChild () {
return <SplitRouteChild />;
}

export default withTranslations(['__SPLIT_WITH_CHILD__'])(SplitRouteWithChild);
10 changes: 10 additions & 0 deletions test/e2e/dynamic-import-translations/fixture/src/split.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @noflow

import React, {Component} from 'react';
import {withTranslations} from 'fusion-plugin-i18n-react';

function SplitRoute () {
return <div />
}

export default withTranslations(['__SPLIT__'])(SplitRoute);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"__SPLIT__": "",
"__SPLIT_WITH_CHILD__": "",
"__SPLIT_CHILD__": ""
}
40 changes: 40 additions & 0 deletions test/e2e/dynamic-import-translations/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// @flow
/* eslint-env node */

const t = require('assert');
const path = require('path');
const puppeteer = require('puppeteer');

const {cmd, start} = require('../utils.js');

const dir = path.resolve(__dirname, './fixture');

test('`fusion build` app with split translations integration', async () => {
var env = Object.create(process.env);
env.NODE_ENV = 'production';

await cmd(`build --dir=${dir} --production`, {env});

const {proc, port} = await start(`--dir=${dir}`, {env, cwd: dir});
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.goto(`http://localhost:${port}/`, {waitUntil: 'load'});
const content = await page.content();
t.ok(
content.includes('<div data-testid="split">["__SPLIT__"]</div>'),
'translation keys are added to promise instrumentation'
);
t.ok(
content.includes(
'<div data-testid="split-with-child">' +
'["__SPLIT_CHILD__","__SPLIT_WITH_CHILD__"]' +
'</div>'
),
'translation keys contain keys from child imports'
);

browser.close();
proc.kill();
}, 100000);
2 changes: 1 addition & 1 deletion test/e2e/empty/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ test('generates error if missing default export', async () => {
// $FlowFixMe
t.fail('did not error');
} catch (e) {
t.ok(e.stderr.includes('initialize is not a function'));
t.ok(e.stderr.includes(' is not a function'));
} finally {
proc.kill();
}
Expand Down
Loading