Skip to content

Commit

Permalink
feat: dynamic imports (experimental flag) (#1397)
Browse files Browse the repository at this point in the history
* refactor(engine): base api changes to support dynamic components

* feat: add compiler integration

* chore: minor lint whitelisting

* feat: add karma test

* chore: fix copyright

* fix(engine): fixing tests for PR #1397

* fix(engine): fixing unit tests for PR #1397

* fix(engine): fixing tests for PR #1397

* Update packages/@lwc/errors/src/compiler/error-info/template-transform.ts

Co-Authored-By: Pierre-Marie Dartus <p.dartus@salesforce.com>

* fix: option name

* fix: minor refactor for dynamic plugin

* fix: missing error type
  • Loading branch information
Diego Ferreiro Val committed Jul 10, 2019
1 parent cdf851b commit 969d124
Show file tree
Hide file tree
Showing 41 changed files with 803 additions and 48 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ lib/
coverage/
fixtures/
public/
__benchmarks_results__/
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
const pluginTestFactory = require('./utils/test-transform').pluginTest;
const testFunction = require('../index');
const pluginNoop = pluginTestFactory(testFunction);
const pluginTestStrict = pluginTestFactory(testFunction, {
dynamicImports: { loader: null, strictSpecifier: true },
});
const pluginTestLoader = pluginTestFactory(testFunction, {
dynamicImports: { loader: '@custom/loader', strictSpecifier: true },
});

describe('Dynamic imports', () => {
pluginNoop(
'passthough with no config',
`
export async function test() {
const id = "foo";
const x = await import(id);
return x + "yay";
}
`,
{
output: {
code: `
export async function test() {
const id = "foo";
const x = await import(id);
return x + "yay";
}
`,
},
}
);

pluginTestStrict(
'check validation for strict',
`
export async function test() {
const id = "foo";
const x = await import(id);
return x + "yay";
}
`,
{
error: {
message:
'Invalid import. The argument "id" must be a stringLiteral for dynamic imports when strict mode is enabled.',
loc: {
line: 3,
column: 23,
length: 2,
start: 72,
},
},
}
);

pluginTestStrict(
'unchanged dynamic import in strict mode',
`
export async function test() {
const x = await import("foo");
return x + "yay";
}
`,
{
output: {
code: `
export async function test() {
const x = await import("foo");
return x + "yay";
}
`,
},
}
);

pluginTestLoader(
'test custom loader',
`
export async function test() {
const x = await import("foo");
return x + "yay";
}
`,
{
output: {
code: `
import { load as _load } from "@custom/loader";
export async function test() {
const x = await _load("foo");
return x + "yay";
}
`,
},
}
);

pluginTestLoader(
'test custom loader multiple imports',
`
export async function test() {
const x = await import("foo");
const y = await import("bar");
return x + y + "yay";
}
`,
{
output: {
code: `
import { load as _load } from "@custom/loader";
export async function test() {
const x = await _load("foo");
const y = await _load("bar");
return x + y + "yay";
}
`,
},
}
);
});
71 changes: 71 additions & 0 deletions packages/@lwc/babel-plugin-component/src/dynamic-imports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
const moduleImports = require('@babel/helper-module-imports');
const { generateError } = require('./utils');
const { LWCClassErrors } = require('@lwc/errors');

function getImportSource(path) {
return path.parentPath.get('arguments.0');
}

function validateImport(sourcePath) {
if (!sourcePath.isStringLiteral()) {
throw generateError(sourcePath, {
errorInfo: LWCClassErrors.INVALID_DYNAMIC_IMPORT_SOURCE_STRICT,
messageArgs: [String(sourcePath)],
});
}
}
/*
* Expected API for this plugin:
* { dynamicImports: { loader: string, strictSpecifier: boolean } }
*/
module.exports = function() {
function getLoaderRef(path, loaderName, state) {
if (!state.loaderRef) {
state.loaderRef = moduleImports.addNamed(path, 'load', loaderName);
}
return state.loaderRef;
}

function addDynamicImportDependency(dependency, state) {
if (!state.dynamicImports) {
state.dynamicImports = [];
}

if (!state.dynamicImports.includes(dependency)) {
state.dynamicImports.push(dependency);
}
}

return {
Import(path, state) {
const { dynamicImports } = state.opts;
if (!dynamicImports) {
return;
}

const { loader, strictSpecifier } = dynamicImports;
const sourcePath = getImportSource(path);

if (strictSpecifier) {
validateImport(sourcePath);
}

if (loader) {
const loaderId = getLoaderRef(path, loader, state);
path.replaceWith(loaderId);
}

if (sourcePath.isStringLiteral()) {
addDynamicImportDependency(sourcePath.node.value, state);
} else {
state.unknownDynamicDependencies = true;
}
},
};
};
3 changes: 2 additions & 1 deletion packages/@lwc/babel-plugin-component/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
const component = require('./component');
const { decorators } = require('./decorators');
const { exit } = require('./program');
const dynamicImports = require('./dynamic-imports');
/**
* The transform is done in 2 passes:
* - First, apply in a single AST traversal the decorators and the component transformation.
Expand All @@ -21,6 +22,6 @@ module.exports = function LwcClassTransform(api) {
parserOpts.plugins.push('classProperties');
parserOpts.plugins.push('dynamicImport');
},
visitor: mergeVisitors([decorators(api), component(api), exit(api)]),
visitor: mergeVisitors([decorators(api), component(api), dynamicImports(api), exit(api)]),
};
};
32 changes: 32 additions & 0 deletions packages/@lwc/compiler/src/__tests__/dynamic-components.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { compile } from '../index';
import { readFixture, pretify } from './utils';

describe('dynamic imports', () => {
it('external dynamic import works', async () => {
const compilerResult = await compile({
name: 'dynamic_imports',
namespace: 'x',
files: {
'dynamic_imports.js': readFixture('dynamic_imports/dynamic_imports.js'),
'dynamic_imports.html': readFixture('dynamic_imports/dynamic_imports.html'),
},
outputConfig: {
compat: false,
format: 'es',
},
experimentalDynamicComponent: {
loader: '@custom/loader',
strictSpecifier: false,
},
});
const { result } = compilerResult;

expect(pretify(result.code)).toBe(pretify(readFixture('expected-dynamic-component.js')));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<x-foo lwc:dynamic={customCtor}></x-foo>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { LightningElement, track } from "lwc";
export default class DynamicCtor extends LightningElement {
@track customCtor;

connectedCallback() {
this.loadCtor();
}

async loadCtor() {
const ctor = await import("foo");
this.customCtor = ctor;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { registerTemplate, registerComponent, LightningElement, registerDecorators } from 'lwc';
import { load } from '@custom/loader';

function tmpl($api, $cmp, $slotset, $ctx) {
const {
dc: api_dynamic_component,
f: api_flatten
} = $api;
return api_flatten([api_dynamic_component("x-foo", $cmp.customCtor, {
context: {
lwc: {}
},
key: 0
}, [])]);
}
var _tmpl = registerTemplate(tmpl);
tmpl.stylesheets = [];
tmpl.stylesheetTokens = {
hostAttribute: "x-dynamic_imports_dynamic_imports-host",
shadowAttribute: "x-dynamic_imports_dynamic_imports"
};
class DynamicCtor extends LightningElement {
constructor(...args) {
super(...args);
this.customCtor = void 0;
}
connectedCallback() {
this.loadCtor();
}
async loadCtor() {
const ctor = await load("foo");
this.customCtor = ctor;
}
}
registerDecorators(DynamicCtor, {
track: {
customCtor: 1
}
});
var dynamic_imports = registerComponent(DynamicCtor, {
tmpl: _tmpl
});
export default dynamic_imports;
5 changes: 1 addition & 4 deletions packages/@lwc/compiler/src/babel-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,7 @@ export const BABEL_CONFIG_BASE = {
configFile: false,
sourceMaps: true,
parserOpts: {
plugins: [
['dynamicImport', {}], // we add this non standard since its already implemented in most browsers
['decorators', { decoratorsBeforeExport: true }],
],
plugins: [['dynamicImport', {}], ['decorators', { decoratorsBeforeExport: true }]],
},
presets: [],
};
Expand Down
20 changes: 20 additions & 0 deletions packages/@lwc/compiler/src/compiler/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const DEFAULT_OPTIONS = {
isExplicitImport: false,
};

const DEFAULT_DYNAMIC_CMP_CONFIG: NormalizedDynamicComponentConfig = {
loader: '',
strictSpecifier: true,
};

const DEFAULT_STYLESHEET_CONFIG: NormalizedStylesheetConfig = {
customProperties: {
allowDefinition: false,
Expand Down Expand Up @@ -52,6 +57,13 @@ export interface BundleFiles {
[filename: string]: string;
}

export type DynamicComponentConfig = Partial<NormalizedDynamicComponentConfig>;

export interface NormalizedDynamicComponentConfig {
loader: string;
strictSpecifier: boolean;
}

export interface CompilerOptions {
name: string;
namespace: string;
Expand All @@ -62,13 +74,15 @@ export interface CompilerOptions {
*/
baseDir?: string;
stylesheetConfig?: StylesheetConfig;
experimentalDynamicComponent?: DynamicComponentConfig;
outputConfig?: OutputConfig;
isExplicitImport?: boolean;
}

export interface NormalizedCompilerOptions extends CompilerOptions {
outputConfig: NormalizedOutputConfig;
stylesheetConfig: NormalizedStylesheetConfig;
experimentalDynamicComponent: NormalizedDynamicComponentConfig;
isExplicitImport: boolean;
}

Expand Down Expand Up @@ -205,10 +219,16 @@ export function normalizeOptions(options: CompilerOptions): NormalizedCompilerOp
},
};

const experimentalDynamicComponent = {
...DEFAULT_DYNAMIC_CMP_CONFIG,
...options.experimentalDynamicComponent,
};

return {
...DEFAULT_OPTIONS,
...options,
stylesheetConfig,
outputConfig,
experimentalDynamicComponent,
};
}
Loading

0 comments on commit 969d124

Please sign in to comment.