Skip to content

Commit

Permalink
🏗 Transform aliased configured components (ampproject#26541)
Browse files Browse the repository at this point in the history
Partial for ampproject#26572.

`transform-inline-configure-component` finds `configureComponent(MyConstructor, {foo: 'bar'})` calls and:

1. Inlines imported `MyConstructor` in current scope.

2. Replaces `*.*.STATIC_CONG_.foo` access with its value as defined in config object `{foo: 'bar'}`.

3. Replaces `configureComponent()` call with inlined `MyConstructor`'s id.

For runtime use of `configureComponent` like:

```js
import {MyConstructor} from './my-ctor';
const TAG = 'amp-foo';
const MyWrappedConstructor = configureComponent(
  MyConstructor,
  {TAG, foo: 'foo'}
);
```

Composed class gets aliased, hoisted and its configuration replaced inline:

```js
import {something} from './imported-by-constructor';

class MyConstructor {
  constructor() {
    // references to config object are replaced here, from:
    // `console.log(this.STATIC_CONFIG_.foo)`
    console.log(_foo);

    // ids in global scope are kept:
    const TAG = TAG;

    // unset properties are replaced with undefined, so this would
    // get minified
    const something = undefined || 'default value';

    // destructuring keeps only the default value when prop undefined:
    // `const {bar = 'default value'} = this.STATIC_CONFIG_`
    const bar = 'default value';
  }
}

const _foo = 'foo';

const TAG = 'amp-foo';
const MyWrappedConstructor = MyConstructor;
```
  • Loading branch information
alanorozco authored Feb 26, 2020
1 parent 39b4ad5 commit 6e49702
Show file tree
Hide file tree
Showing 19 changed files with 606 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {dirname, relative, join} = require('path');
const {transformFileSync} = require('@babel/core');

/**
* @fileoverview
* Finds `configureComponent(MyConstructor, {foo: 'bar'})` calls and:
*
* 1. Inlines imported `MyConstructor` in current scope.
*
* 2. Replaces `*.STATIC_CONFIG_.foo` accesses to their value as
* defined in config object `{foo: 'bar'}`.
*
* 3. Replaces `configureComponent(...)` call with identifier for inlined, static
* `MyConstructor`.
*/

const calleeName = 'configureComponent';
const replacedMember = 'STATIC_CONFIG_';

/**
* Sub-plugin that transforms inlined file that exports wrapped constructor.
* @return {!Object}
*/
function transformRedefineInline({types: t}) {
const propValueNode = (propValues, key, opt_default) =>
propValues[key] || opt_default || t.identifier('undefined');

function unjsdoc({leadingComments}) {
if (!leadingComments) {
return;
}
for (let i = 0; i < leadingComments.length; i++) {
const comment = leadingComments[i];
comment.value = comment.value.replace(/^\*/, ' [removed]');
}
}

function unexport(path) {
if (path.node.declaration) {
path.replaceWith(path.node.declaration);
return;
}
path.remove();
}

return {
name: 'transform-redefine-inline',
visitor: {
ExportDefaultDeclaration: unexport,
ExportNamedDeclaration: unexport,
ExportAllDeclaration: unexport,
ImportDeclaration(path, {opts}) {
const {source} = path.node;
if (source.value.startsWith('.')) {
source.value = join(opts.from, source.value).replace(/^[^.]/, './$&');
}
},
MemberExpression(path, {opts}) {
// Handle x.y.{...}.$replacedMember prop accesses
if (!t.isIdentifier(path.node.property, {name: replacedMember})) {
return;
}

const assignment = path.find(
({parent, parentKey, parentPath}) =>
parentKey == 'left' &&
t.isAssignmentExpression(parent) &&
t.isExpressionStatement(parentPath.parent)
);

if (assignment) {
unjsdoc(assignment.parentPath.parent);
assignment.parentPath.parentPath.remove();
return;
}

if (
t.isMemberExpression(path.parent) &&
t.isIdentifier(path.parent.property)
) {
const {name} = path.parent.property;
path.parentPath.replaceWith(propValueNode(opts.propValues, name));
return;
}

if (
t.isVariableDeclarator(path.parent) &&
t.isObjectPattern(path.parent.id) &&
t.isVariableDeclaration(path.parentPath.parent)
) {
const assignments = path.parent.id.properties.map(({key, value}) =>
t.variableDeclarator(
value.left || value,
propValueNode(opts.propValues, key.name, value.right)
)
);
path.parentPath.replaceWithMultiple(assignments);
}
},
},
};
}

/**
* Transforms using transformRedefineInline sub-plugin.
* @param {string} sourceFilename
* @param {!Object} opts
* @return {!Object}
*/
const redefineInline = (sourceFilename, opts) =>
transformFileSync(sourceFilename.toString(), {
configFile: false,
code: false,
ast: true,
sourceType: 'module',
plugins: [[transformRedefineInline, opts]],
});

/**
* Replaces `configureComponent()` wrapping calls.
* @return {!Object}
*/
module.exports = function({types: t}) {
function getImportPath(nodes, name) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (
t.isImportDeclaration(node) &&
node.specifiers.find(({imported}) => t.isIdentifier(imported, {name}))
) {
return node.source.value;
}
}
}

return {
name: 'transform-inline-decl-extensions',
visitor: {
CallExpression(path, {file}) {
if (!t.isIdentifier(path.node.callee, {name: calleeName})) {
return;
}

const [importedId, propsObj] = path.node.arguments;
if (!t.isIdentifier(importedId) || !t.isObjectExpression(propsObj)) {
return;
}

const program = path.findParent(p => t.isProgram(p));

const importPath = getImportPath(program.node.body, importedId.name);
if (!importPath) {
return;
}

for (const name in program.scope.bindings) {
if (name) {
path.scope.rename(name, program.scope.generateUid(name));
}
}

const propValues = Object.create(null);

for (const {key, value} of propsObj.properties) {
const {name} = key;

if (t.isMemberExpression(value)) {
throw path.buildCodeFrameError(
`${replacedMember} properties must not be assigned to members. ` +
'Set necessary values to program-level constants.'
);
}

if (t.isIdentifier(value)) {
const binding = path.scope.getBinding(value.name);
if (!binding || !t.isProgram(binding.scope.block)) {
throw path.buildCodeFrameError(
`ids used in ${replacedMember} must be defined as ` +
'program-level constants.'
);
}
propValues[name] = value;
continue;
}

const id = program.scope.generateUidIdentifier(name);
program.scope.push({id, init: value, kind: 'const'});
propValues[name] = id;
}

const currentDirname = dirname(file.opts.filename);
const importedModule = join(currentDirname, importPath);

// TODO(go.amp.dev/issue/26948): sourcemaps
const importedInline = redefineInline(require.resolve(importedModule), {
propValues,
from: relative(currentDirname, dirname(importedModule)),
});

program.unshiftContainer('body', importedInline.ast.program.body);

path.replaceWith(t.identifier(importedId.name));
},
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {a, b, c} = x.STATIC_CONFIG_;
const {a: a1, b: b1, c: c1} = x.y.STATIC_CONFIG_;

export class Destructuring {
method() {
const {a, b: bRenamed, c} = this.STATIC_CONFIG_;
}
withDefaultValues() {
const {
a = 'default value for a',
b: renamedBbbb = 'default value for b',
c = 'default value for c',
d = 'default value for d',
e: renamedE = 'default value for e',
} = this.STATIC_CONFIG_;
}
unset() {
const {
a,
thisPropIsUnset,
thisPropIsUnsetToo,
} = this.STATIC_CONFIG_;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Destructuring} from './input-base-class';
foo(
configureComponent(Destructuring, {
a: 'value for a',
b: 'value for b',
c: 'value for c',
})
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"plugins": ["../../../../"],
"sourceType": "module"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const _a = 'value for a',
_b = 'value for b',
_c = 'value for c';

/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const a = _a,
b = _b,
c = _c;
const a1 = _a,
b1 = _b,
c1 = _c;

class Destructuring {
method() {
const a = _a,
bRenamed = _b,
c = _c;
}

withDefaultValues() {
const a = _a,
renamedBbbb = _b,
c = _c,
d = 'default value for d',
renamedE = 'default value for e';
}

unset() {
const a = _a,
thisPropIsUnset = undefined,
thisPropIsUnsetToo = undefined;
}

}

import { Destructuring as _Destructuring } from './input-base-class';
foo(_Destructuring);
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class DirectAccess {
setProps() {
this.STATIC_CONFIG_.foo;
somethingSomething(this.STATIC_CONFIG_.bar);
tacos(this.STATIC_CONFIG_.nestedObject.baz);
}

unsetProps() {
return this.STATIC_CONFIG_.thisPropIsUnset;
}

propsSetToIds() {
return this.STATIC_CONFIG_.scopedId;
}
}
Loading

0 comments on commit 6e49702

Please sign in to comment.