-
Notifications
You must be signed in to change notification settings - Fork 34
Add translation key instrumentation for dynamic imports #769
base: master
Are you sure you want to change the base?
Changes from 8 commits
f39d2e7
adc025a
d7cc0e1
14fe62f
7236f02
af8fb96
130dbe6
c17ab57
142a2f2
58dc442
039274b
5b8b60a
bd44167
07b757a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,7 @@ const JS_EXT_PATTERN = /\.jsx?$/; | |
| /*:: | ||
| import type { | ||
| ClientChunkMetadataState, | ||
| TranslationsManifest, | ||
| TranslationsManifestState, | ||
| LegacyBuildEnabledState, | ||
| } from "./types.js"; | ||
|
|
@@ -86,7 +87,8 @@ export type WebpackConfigOpts = {| | |
| clientChunkMetadata: ClientChunkMetadataState, | ||
| legacyClientChunkMetadata: ClientChunkMetadataState, | ||
| mergedClientChunkMetadata: ClientChunkMetadataState, | ||
| i18nManifest: TranslationsManifestState, | ||
| i18nManifest: TranslationsManifest, | ||
| i18nDeferredManifest: TranslationsManifestState, | ||
| legacyBuildEnabled: LegacyBuildEnabledState, | ||
| }, | ||
| fusionConfig: FusionRC, | ||
|
|
@@ -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, | ||
|
|
@@ -465,17 +470,20 @@ 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 | ||
| runtime === 'server' | ||
| ? // Server | ||
| new InstrumentedImportDependencyTemplatePlugin( | ||
| state.mergedClientChunkMetadata | ||
| : /** | ||
| * Client | ||
| * Don't wait for the client manifest on the client. | ||
| * The underlying plugin handles client instrumentation on its own. | ||
| */ | ||
| void 0 | ||
| ), | ||
| ) | ||
| : /** | ||
| * Client | ||
| * Don't wait for the client manifest on the client. | ||
| * The underlying plugin handles client instrumentation on its own. | ||
| */ | ||
| new InstrumentedImportDependencyTemplatePlugin( | ||
| void 0, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that this constructor now takes more than one parameter, I think an object might make this more understandable. This was already a bit confusing (my bad) where the argument is expected to be undefined in the client case, but server-side should be provided. Adding another parameter now makes it even worse. Expressing the parameter type as a disjoint union of two different objects would help I think. For example: type Opts =
| ClientOpts
| ServerOpts;
type ServerOpts = {
compilation: "server",
clientChunkMetadata: ClientChunkMetadataState
};
type ClientOpts = {
compilation: "client",
i18nManifest: TranslationsManifest
};Also, this makes me think: is it odd that I think in the future, potentially we could align the server code to work more like the client code. I think this particular promise instrumentation might be useful even in a Suspense-powered i18n system.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I think that'll make the plugin easier to understand. I'll update to use those types.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was actually thinking the opposite for the promise instrumentation. I don't totally understand why
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay I recall that now. Since I didn't change how the translation manifest is added to the server bundle, the server still looks up the necessary translations based on the |
||
| state.i18nManifest | ||
| ), | ||
| dev && hmr && watch && new webpack.HotModuleReplacementPlugin(), | ||
| !dev && runtime === 'client' && new webpack.HashedModuleIdsPlugin(), | ||
| runtime === 'client' && | ||
|
|
@@ -527,7 +535,10 @@ function getWebpackConfig(opts /*: WebpackConfigOpts */) { | |
| options.optimization.splitChunks | ||
| ), | ||
| // need to re-apply template | ||
| new InstrumentedImportDependencyTemplatePlugin(void 0), | ||
| new InstrumentedImportDependencyTemplatePlugin( | ||
| void 0, | ||
| state.i18nManifest | ||
| ), | ||
| new ClientChunkMetadataStateHydratorPlugin( | ||
| state.legacyClientChunkMetadata | ||
| ), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,11 @@ | |
| /* eslint-env node */ | ||
|
|
||
| /*:: | ||
| import type {ClientChunkMetadataState, ClientChunkMetadata} from "../types.js"; | ||
| import type { | ||
| ClientChunkMetadataState, | ||
| ClientChunkMetadata, | ||
| TranslationsManifest, | ||
| } from "../types.js"; | ||
| */ | ||
|
|
||
| const ImportDependency = require('webpack/lib/dependencies/ImportDependency'); | ||
|
|
@@ -34,9 +38,14 @@ const ImportDependencyTemplate = require('webpack/lib/dependencies/ImportDepende | |
|
|
||
| class InstrumentedImportDependencyTemplate extends ImportDependencyTemplate { | ||
| /*:: clientChunkIndex: ?$PropertyType<ClientChunkMetadata, "fileManifest">; */ | ||
| /*:: manifest: ?TranslationsManifest; */ | ||
|
|
||
| constructor(clientChunkMetadata /*: ?ClientChunkMetadata */) { | ||
| constructor( | ||
| clientChunkMetadata /*: ?ClientChunkMetadata */, | ||
| translationsManifest /*: ?TranslationsManifest*/ | ||
| ) { | ||
| super(); | ||
| this.translationsManifest = translationsManifest; | ||
| if (clientChunkMetadata) { | ||
| this.clientChunkIndex = clientChunkMetadata.fileManifest; | ||
| } | ||
|
|
@@ -69,13 +78,45 @@ class InstrumentedImportDependencyTemplate extends ImportDependencyTemplate { | |
| chunkIds = getChunkGroupIds(depBlock.chunkGroup); | ||
| } | ||
|
|
||
| let translationKeys = []; | ||
| if (this.translationsManifest) { | ||
| const modulesSet = new Set(); | ||
|
|
||
| // Module dependencies | ||
| if (dep.module && dep.module.dependencies) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting, so these modules in Regardless, I think it might be worth encapsulating logic this into a separate function:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I originally didn't have to use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I can elaborate on this a little more. In dev, |
||
| dep.module.dependencies.map(d => { | ||
| if (d.originModule) { | ||
| modulesSet.add(d.originModule.userRequest); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Chunks | ||
| depBlock.chunkGroup.chunks.forEach(chunk => { | ||
| const modules = Array.from(chunk._modules.keys()); | ||
| modules.forEach(m => modulesSet.add(m.resource)); | ||
| }); | ||
|
|
||
| const modules = Array.from(modulesSet.keys()); | ||
| translationKeys = modules.reduce((acc, module) => { | ||
|
rtsao marked this conversation as resolved.
Outdated
|
||
| if (this.translationsManifest.has(module)) { | ||
| const keys = Array.from(this.translationsManifest.get(module).keys()); | ||
| return acc.concat(keys); | ||
| } else { | ||
| return acc; | ||
| } | ||
| }, []); | ||
| } | ||
|
|
||
| // 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 that are used in this bundle | ||
|
micburks marked this conversation as resolved.
Outdated
|
||
| 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; | ||
|
|
||
|
|
@@ -91,9 +132,14 @@ class InstrumentedImportDependencyTemplate extends ImportDependencyTemplate { | |
|
|
||
| class InstrumentedImportDependencyTemplatePlugin { | ||
| /*:: clientChunkIndexState: ?ClientChunkMetadataState; */ | ||
| /*:: translationsManifest: ?TranslationsManifest; */ | ||
|
|
||
| constructor(clientChunkIndexState /*: ?ClientChunkMetadataState*/) { | ||
| constructor( | ||
| clientChunkIndexState /*: ?ClientChunkMetadataState*/, | ||
| translationsManifest /*: ?TranslationsManifest*/ | ||
| ) { | ||
| this.clientChunkIndexState = clientChunkIndexState; | ||
| this.translationsManifest = translationsManifest; | ||
| } | ||
|
|
||
| apply(compiler /*: any */) { | ||
|
|
@@ -113,13 +159,20 @@ class InstrumentedImportDependencyTemplatePlugin { | |
| ); | ||
| done(); | ||
| }); | ||
| } else { | ||
| } else if (this.translationsManifest) { | ||
| // client | ||
| compilation.dependencyTemplates.set( | ||
| ImportDependency, | ||
| new InstrumentedImportDependencyTemplate() | ||
| new InstrumentedImportDependencyTemplate( | ||
| void 0, | ||
| this.translationsManifest | ||
| ) | ||
| ); | ||
| done(); | ||
| } else { | ||
| throw new Error( | ||
| 'InstrumentationImportDependencyPlugin called without clientChunkIndexState or translationsManifest' | ||
| ); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| 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; | ||
| } |
| 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); |
| 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__": "" | ||
| } |
| 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); |
Uh oh!
There was an error while loading. Please reload this page.