Skip to content

Commit

Permalink
feat: augments metadata gathering for @wire decorator (#631)
Browse files Browse the repository at this point in the history
## Details

- If a parameter of a @wire decorator contains an imported reference, this adds support for tracking of such metadata.
For example `@wire(getRecord, { recordId: id })`, where userId
is imported: `import id from '@salesforce/user/id'`;
- static 1st order references to a literal constant are now resolved, e.g.
  ```js
  const foo = '123';
  @wire(getRecord, { parameter: foo });
  ```
- In addition to strings, adds support for booleans and numbers 

- If a parameter is unresolved, adds it to metadata with type 'unresolved'

- Adds type to resolved references/expressions

- If an array contains an unresolved identifier/expression, marks the array as unresolved

- Updates to test suite and various bug fixes

## Does this PR introduce a breaking change?

* [ ] Yes
* [X] No
  • Loading branch information
sfdciuie committed Sep 14, 2018
1 parent fb35d0d commit 4920a4e
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 15 deletions.
@@ -1,5 +1,39 @@
const pluginTest = require('./utils/test-transform').pluginTest(require('../index'));

const wireMetadataParameterTest =
(testName, {declaration = '', wireParameters = '', expectedStaticParameters = {}, expectedVariableParameters = {}}) => {
pluginTest(
testName,
`
import { wire } from 'lwc';
import { getRecord } from 'recordDataService';
${declaration};
export default class Test {
@wire(getRecord, { ${wireParameters.join(',')} })
recordData;
}
`,
{
output: {
metadata: {
decorators: [{
type: 'wire',
targets: [
{
adapter: { name: 'getRecord', reference: 'recordDataService' },
name: 'recordData',
params: expectedVariableParameters,
static: expectedStaticParameters ,
type: 'property',
}
],
}]
},
},
},
);
};

describe('Transform property', () => {
pluginTest('transforms wired field', `
import { wire } from 'lwc';
Expand Down Expand Up @@ -126,7 +160,7 @@ Test.wire = {
}
};`
}
})
});

pluginTest('decorator accepts an optional config object as second parameter', `
import { wire } from 'lwc';
Expand Down Expand Up @@ -331,7 +365,7 @@ Test.wire = {

describe('Metadata', () => {
pluginTest(
'gather track metadata',
'gather wire metadata',
`
import { wire } from 'lwc';
import { getFoo } from 'data-service';
Expand All @@ -352,19 +386,106 @@ describe('Metadata', () => {
adapter: { name: 'getFoo', reference: 'data-service' },
name: 'wiredProp',
params: { key1: 'prop1' },
static: { key2: ['fixed'] },
static: { key2: { value: ['fixed'], type: 'array' } },
type: 'property',
},
{
adapter: { name: 'getFoo', reference: 'data-service' },
name: 'wiredMethod',
params: { key1: 'prop1' },
static: { key2: ['fixed'] },
static: { key2: { value: ['fixed'], type: 'array' } },
type: 'method',
}],
}]
},
},
},
);

wireMetadataParameterTest('when constant initialised to a string-literal should extract the value',
{ declaration: `const stringConstant = '123';`,
wireParameters: ['userId: stringConstant'],
expectedStaticParameters: { userId: { value: '123', type: 'string'} } });

wireMetadataParameterTest('when constant initialised to a number-literal should extract the value',
{ declaration: `const numberConstant = 100;`,
wireParameters: ['size: numberConstant'],
expectedStaticParameters: { size: { value: 100, type: 'number'} } });

wireMetadataParameterTest('when constant initialised to a boolean-literal should extract the value',
{ declaration: `const booleanConstant = true;`,
wireParameters: ['isRegistered: booleanConstant'],
expectedStaticParameters: { isRegistered: { value: true, type: 'boolean'} } });

wireMetadataParameterTest('when constant initialised as a reference to another should mark as unresolved',
{ declaration: `const stringConstant = '123'; const referenceConstant = stringConstant;`,
wireParameters: ['recordId: referenceConstant'],
expectedStaticParameters: { recordId: { type: 'unresolved', value: 'identifier' } } });

wireMetadataParameterTest('when importing a default export from a module should reference the name of the module',
{ declaration: `import id from '@salesforce/user/Id';`,
wireParameters: ['recordId: id'],
expectedStaticParameters: { recordId: { value: '@salesforce/user/Id', type: 'module' } } });

wireMetadataParameterTest('when parameter reference missing should mark as unresolved',
{ wireParameters: ['recordId: id'],
expectedStaticParameters: { recordId: { type: 'unresolved', value: 'identifier'} } });

wireMetadataParameterTest('when importing named export with "as" from a module should reference the name of the module',
{ declaration: `import { id as currentUserId } from '@salesforce/user/Id';`,
wireParameters: ['recordId: currentUserId'],
expectedStaticParameters: { recordId: { value: '@salesforce/user/Id', type: 'module' } } });

wireMetadataParameterTest('when importing a named export from a module should reference the name of the module',
{ declaration: `import { id } from '@salesforce/user/Id';`,
wireParameters: ['recordId: id'],
expectedStaticParameters: { recordId: { value: '@salesforce/user/Id', type: 'module' } } });

wireMetadataParameterTest('when importing from a relative module should reference the name of the module',
{ declaration: `import id from './someReference.js';`,
wireParameters: ['recordId: id'],
expectedStaticParameters: { recordId: { value: './someReference.js', type: 'module' } } });

wireMetadataParameterTest('when referencing a "let" variable should mark as unresolved',
{ declaration: `let userId = '123';`,
wireParameters: ['recordId: userId'],
expectedStaticParameters: { recordId: { type: 'unresolved', value: 'identifier'} } });

wireMetadataParameterTest('when referencing a member expression, should mark as unresolved',
{ declaration: `const data = {userId: '123'};`,
wireParameters: ['recordId: data.userId'],
expectedStaticParameters: { recordId: { type: 'unresolved', value: 'member_expression' } } });

wireMetadataParameterTest('when an inline string-literal initialization is used, should use value',
{ wireParameters: ['recordId: "123"'],
expectedStaticParameters: { recordId: { value: '123', type: 'string' } } });

wireMetadataParameterTest('when an inline numeric-literal initialization is used, should use value',
{ wireParameters: ['size: 100'],
expectedStaticParameters: { size: { value: 100, type: 'number' } } });

wireMetadataParameterTest('when an inline float-literal initialization is used, should use value',
{ wireParameters: ['underPrice: 50.50'],
expectedStaticParameters: { underPrice: { value: 50.50, type: 'number' } } });

wireMetadataParameterTest('when an inline boolean-literal initialization is used, should use value',
{ wireParameters: ['isRegistered: true'],
expectedStaticParameters: { isRegistered: { value: true, type: 'boolean' } } });

wireMetadataParameterTest('when $foo parameters are used, should use name of the parameter',
{ wireParameters: ['recordId: "$userId"'],
expectedVariableParameters: { recordId: 'userId' } });

wireMetadataParameterTest('when $foo parameters with dots are used, should use name of the parameter',
{ wireParameters: ['recordId: "$userRecord.Id"'],
expectedVariableParameters: { recordId: 'userRecord.Id' } });

wireMetadataParameterTest('when inline array with a non-string literal is used, should mark as unresolved',
{ declaration: `const bar = '123';`,
wireParameters: ['fields: ["foo", bar]'],
expectedStaticParameters: { fields: {type: 'unresolved', value: 'array_expression'} } });

wireMetadataParameterTest('when inline array with literals is used, should have the array',
{wireParameters: ['data: ["foo", 123, false]'],
expectedStaticParameters: { data: {type: 'array', value: ['foo', 123, false]} } });
});
Expand Up @@ -74,12 +74,57 @@ function buildWireConfigValue(t, wiredValues) {
}));
}

function getWiredStaticMetadata(properties) {
const ret = {};
const SUPPORTED_VALUE_TYPE_TO_METADATA_TYPE = {
StringLiteral: 'string',
NumericLiteral: 'number',
BooleanLiteral: 'boolean'
};

function getWiredStaticMetadata(properties, referenceLookup) {
const ret = {};
properties.forEach(s => {
if (s.key.type === 'Identifier' && s.value.type === 'ArrayExpression') {
ret[s.key.name] = s.value.elements.map(e => e.value);
let result = {};
const valueType = s.value.type;
if (s.key.type === 'Identifier') {
if (valueType === 'ArrayExpression') {
// @wire(getRecord, { fields: ['Id', 'Name'] })
// @wire(getRecord, { data: [123, false, 'string'] })
const elements = s.value.elements;
const hasUnsupportedElement =
elements.some(element => !SUPPORTED_VALUE_TYPE_TO_METADATA_TYPE[element.type]);
if (hasUnsupportedElement) {
result = {type: 'unresolved', value: 'array_expression'};
} else {
result = {type: 'array', value: elements.map(e => e.value)};
}
} else if (SUPPORTED_VALUE_TYPE_TO_METADATA_TYPE[valueType]) {
// @wire(getRecord, { companyName: ['Acme'] })
// @wire(getRecord, { size: 100 })
// @wire(getRecord, { isAdmin: true })
result = {type: SUPPORTED_VALUE_TYPE_TO_METADATA_TYPE[valueType], value: s.value.value};
} else if (valueType === 'Identifier') {
// References such as:
// 1. Modules
// import id from '@salesforce/user/id'
// @wire(getRecord, { userId: id })
//
// 2. 1st order constant references with string literals
// const userId = '123';
// @wire(getRecord, { userId: userId })
const reference = referenceLookup(s.value.name);
result = {value: reference.value, type: reference.type};
if (!result.type) {
result = {type: 'unresolved', value: 'identifier'}
}
} else if (valueType === 'MemberExpression') {
// @wire(getRecord, { userId: recordData.Id })
result = {type: 'unresolved', value: 'member_expression'};
}
}
if (!result.type) {
result = {type: 'unresolved'};
}
ret[s.key.name] = result;
});
return ret;
}
Expand All @@ -94,9 +139,39 @@ function getWiredParamMetadata(properties) {
return ret;
}

const scopedReferenceLookup = scope => name => {
const binding = scope.getBinding(name);

let type;
let value;

if (binding) {
if (binding.kind === 'module') {
// Resolves module import to the name of the module imported
// e.g. import { foo } from 'bar' gives value 'bar' for `name == 'foo'
let parentPathNode = binding.path.parentPath.node;
if (parentPathNode && parentPathNode.source) {
type = 'module';
value = parentPathNode.source.value;
}
} else if (binding.kind === 'const') {
// Resolves `const foo = 'text';` references to value 'text', where `name == 'foo'`
const init = binding.path.node.init;
if (init && SUPPORTED_VALUE_TYPE_TO_METADATA_TYPE[init.type]) {
type = SUPPORTED_VALUE_TYPE_TO_METADATA_TYPE[init.type];
value = init.value;
}
}
}
return {
type,
value
};
};

module.exports = function transform(t, klass, decorators) {
const metadata = [];
const wiredValues = decorators.filter(isWireDecorator).map(({ path }) => {
const wiredValues = decorators.filter(isWireDecorator).map(({path}) => {
const [id, config] = path.get('arguments');

const propertyName = path.parentPath.get('key.name').node;
Expand All @@ -107,17 +182,21 @@ module.exports = function transform(t, klass, decorators) {
const wiredValue = {
propertyName,
isClassMethod
}
};

if (config) {
wiredValue.static = getWiredStatic(config);
wiredValue.params = getWiredParams(t, config);
}

const referenceLookup = scopedReferenceLookup(path.scope);

if (id.isIdentifier()) {
const adapterName = id.node.name;
const reference = referenceLookup(adapterName);
wiredValue.adapter = {
name: id.node.name,
reference: path.scope.getBinding(id.node.name).path.parentPath.node.source.value
name: adapterName,
reference: reference.type === 'module' ? reference.value : undefined
}
}

Expand All @@ -128,7 +207,7 @@ module.exports = function transform(t, klass, decorators) {
};

if (config) {
wireMetadata.static = getWiredStaticMetadata(wiredValue.static);
wireMetadata.static = getWiredStaticMetadata(wiredValue.static, referenceLookup);
wireMetadata.params = getWiredParamMetadata(wiredValue.params);
}

Expand All @@ -154,4 +233,4 @@ module.exports = function transform(t, klass, decorators) {
targets: metadata
};
}
}
};
Expand Up @@ -265,7 +265,7 @@ describe("compiler metadata", () => {
},
name: "wiredMethod",
params: { name: "publicProp" },
static: { fields: ["one", "two"] }
static: { fields: { type: "array", value: ["one", "two"] } }
}
]
}
Expand Down

0 comments on commit 4920a4e

Please sign in to comment.