Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wire-service): imported function identifier as adapter id for @wire #26

Merged
merged 6 commits into from
Jan 30, 2018

Conversation

yungcheng
Copy link
Contributor

PR Checklist

What kind of change does this PR introduce? (add 'x' - [x])

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

Does this PR introduce a breaking change? (check one)

  • Yes
  • No

If yes, please describe the impact and migration path for existing applications:
Please check if your PR fulfills the following requirements:

The PR fulfills these requirements:

  • Tests for the changes have been added (for bug fixes / features)

  • Both unit and integration tests pass

  • Docs have been added / updated (for bug fixes / features)

  • The PR title follows conventional commit format:
    commit-type(optional scope): commit description.

    Supported commit types: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test.
    Supported scope: The scope should be the name of the npm package affected (engine, compiler, wire-service, etc.)
    
  • More details on LWC semantic commit can be found here.

Other information:

@salesforce-best-lwc-internal
Copy link

Benchmark comparison

Base commit: 0a8e11f | Target commit: ade0071

benchmark base(0a8e11f) target(ade0071) trend
append-1k.benchmark:benchmark-table/add/1k 335.93 (± 9.16 ms) 330.90 (± 9.16 ms) 👌
clear-1k.benchmark:benchmark-table/clear/1k 51.34 (± 1.54 ms) 50.42 (± 1.54 ms) 👌
create-1k.benchmark:benchmark-table/create/1k 199.85 (± 3.13 ms) 191.38 (± 3.13 ms) 👍
update-10th-1k.benchmark:benchmark-table/update-10th/1k 182.35 (± 2.63 ms) 193.34 (± 2.63 ms) 👎

@@ -76,7 +76,7 @@ function validatePairSetterGetter(decorators) {
});
}

module.exports = function validate(klass, decorators) {
module.exports = function validate(klass, decorators, importSpecifiers) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ImportSpecifiers is not used here.

@@ -7,7 +7,7 @@ function isTrackDecorator(decorator) {
return decorator.name === TRACK_DECORATOR;
}

function validate(klass, decorators) {
function validate(klass, decorators, importSpecifiers) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

importSpecifiers is not used here.


// TODO: deprecate type (string as adapter id once consumer has migrated to use imported identifier)
if (id.isStringLiteral()) {
Object.defineProperty(ret, 'type', {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Object.defineProperty is not needed here a simple property set would do the trick.

throw id.buildCodeFrameError(
`@wire expects a string as first parameter.`
);
if (!id.isIdentifier()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should simplify the logic here:

if (!id.isStringLiteral() || !id.isIdentifier()) {
	// throw because unexpected adapter
}

if (id.isIdentifier() && !importSpecifiers.includes(id.node.name)) {
	// throw because adapter should be imported
}

@@ -56,7 +56,7 @@ function getImportsStatements(path, sourceName) {

return programPath.get('body').filter(node => (
node.isImportDeclaration() &&
node.get('source').isStringLiteral({ value: sourceName })
(!sourceName || node.get('source').isStringLiteral({ value: sourceName }))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The association of AND and OR condition is not obvious, can you break it down into multiple lines?

@@ -68,7 +68,7 @@ function getImportSpecifiers(path, sourceName) {
return [...acc, ...importStatement.get('specifiers')];
}, []).reduce((acc, specifier) => {
// Get the list of specifiers with their name
const imported = specifier.get('imported').node.name;
const imported = specifier.get('imported').node && specifier.get('imported').node.name;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If specifier.get('imported').node is undefined it will add to the array { name: undefined, path: specifier }. The name property should always be a string.

Do you have an example where specifier.get('imported').node returns undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import * as target from '...';, it is discovered from a test

@@ -120,6 +120,7 @@ function removeImportSpecifiers(specifiers) {
module.exports = function decoratorVisitor({ types: t }) {
return {
Program(path, state) {
const importSpecifiers = getImportSpecifiers(path).map(i => i.name);
const engineImportSpecifiers = getImportSpecifiers(path, LWC_PACKAGE_ALIAS);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of doing 2 passes to retrieve imports from engine and all the imports. I would be faster to retrieve all the imports and then filter based on the module source.

@@ -120,6 +120,7 @@ function removeImportSpecifiers(specifiers) {
module.exports = function decoratorVisitor({ types: t }) {
return {
Program(path, state) {
const importSpecifiers = getImportSpecifiers(path).map(i => i.name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the scope you have:

  • importSpecifiers: list of imported properties as string
  • engineImportSpecifiers: list of imported properties as a data structure containing the specifier and the name

The naming of the variables should reflect that they are not referencing the same king of data structures.

);
} else {
// identifier has to be imported
if (!importSpecifiers.includes(id.node.name)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not enough to ensure that the wires are imported. For example:

import { record } from 'lds';

export function factory() {
    // Shadow imported identifier
	const record = 'another-adapter';
    return class Test {
          @api(record, {}) wiredRecord;
    }
}

In order to solve this issue, you need to locate where the identifier is defined. The scope property on the path holds this kind of information.

const wireDef = { adapter: unknownFunc };
expect(() => {
target.getAdapter(wireDef, "target");
}).toThrow();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably assert the thrown message.

@yungcheng
Copy link
Contributor Author

@pmdartus, question for you:
for import record from 'data-service';, does lwc-compiler need to output data-service in the dependencies metadata? wire-service doesn't really need the hard dependency because it has the ability to lookup adapter, but I assume this is something we want to enforce to enable static analysis?

@kevinv11n thoughts?

@kevinv11n
Copy link
Contributor

kevinv11n commented Jan 23, 2018

Re for import record from 'data-service';, does lwc-compiler need to output data-service in the dependencies metadata -- I'm working through the current + roadmap'ed UIAPI/LDS resources to figure out how we want this code organized. We may need to adjust things a bit.

@yungcheng yungcheng changed the title feat(wire-service): enable using imported function identifier as adapter id for @wire feat(wire-service): imported function identifier as adapter id for @wire Jan 24, 2018
@yungcheng
Copy link
Contributor Author

@pmdartus second pass, now no longer need to keep a list fo imported identifiers

@salesforce-best-lwc-internal
Copy link

Benchmark comparison

Base commit: b5fc0ed | Target commit: e4e92fe

benchmark base(b5fc0ed) target(e4e92fe) trend
append-1k.benchmark:benchmark-table/add/1k 328.81 (± 11.44 ms) 337.14 (± 11.44 ms) 👎
clear-1k.benchmark:benchmark-table/clear/1k 48.70 (± 1.38 ms) 49.53 (± 1.38 ms) 👎
create-1k.benchmark:benchmark-table/create/1k 195.45 (± 3.58 ms) 195.54 (± 3.58 ms) 👌
update-10th-1k.benchmark:benchmark-table/update-10th/1k 183.68 (± 3.84 ms) 185.25 (± 3.84 ms) 👌

@salesforce-best-lwc-internal
Copy link

Benchmark comparison

Base commit: 9dcac43 | Target commit: bca355a

benchmark base(9dcac43) target(bca355a) trend
append-1k.benchmark:benchmark-table/add/1k 347.06 (± 9.45 ms) 353.71 (± 9.45 ms) 👌
clear-1k.benchmark:benchmark-table/clear/1k 49.52 (± 2.04 ms) 54.46 (± 2.04 ms) 👎
create-1k.benchmark:benchmark-table/create/1k 201.60 (± 7.61 ms) 227.78 (± 7.61 ms) 👎
update-10th-1k.benchmark:benchmark-table/update-10th/1k 186.22 (± 6.43 ms) 188.85 (± 6.43 ms) 👌

@salesforce-best-lwc-internal
Copy link

Benchmark comparison

Base commit: 9dcac43 | Target commit: 120c13c

benchmark base(9dcac43) target(120c13c) trend
append-1k.benchmark:benchmark-table/add/1k 347.06 (± 10.22 ms) 336.04 (± 10.22 ms) 👍
clear-1k.benchmark:benchmark-table/clear/1k 49.52 (± 1.27 ms) 51.91 (± 1.27 ms) 👎
create-1k.benchmark:benchmark-table/create/1k 201.60 (± 4.21 ms) 199.54 (± 4.21 ms) 👌
update-10th-1k.benchmark:benchmark-table/update-10th/1k 186.22 (± 4.39 ms) 190.07 (± 4.39 ms) 👌

Copy link
Collaborator

@apapko apapko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few minor comments. It would probably be a good idea to open up a user story/issue to keep track of existing todos:
// TODO: deprecate type once consumers have migrate to use function identifier for adapter id.

);
}

let adapterModule;
if (id.isIdentifier()) {
const adapterImportPath = path.scope.getBinding(id.node.name).path;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check if getBinding doesn't produce undefined prior accessing path?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should always be defined because id exists, getBinding returns where the id is declared.

let adapterModule;
if (id.isIdentifier()) {
const adapterImportPath = path.scope.getBinding(id.node.name).path;
if (adapterImportPath.isImportSpecifier()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

existence check on adapterImportPath

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, this should always exist


// TODO: deprecate type (string as adapter id once consumer has migrated to use imported identifier)
if (id.isStringLiteral()) {
wiredValue.type = id.node.value;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we can extract id.node reference into a local variable, there are multiple accesses in this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could, but path.node is the common pattern to access a node in babel plugin

}
};

return {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there will be an error if someone calls next / error/ or complete before calling observable(); Your observer is undefined until subscribe is invoked.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not exposed, the only thing that the module exposes is serviceTodo

@salesforce-best-lwc-internal
Copy link

Benchmark comparison

Base commit: 839cdfe | Target commit: b46ef01

benchmark base(839cdfe) target(b46ef01) trend

Copy link
Contributor

@caridy caridy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with this baby steps. Eventually, to be able to do real rollups, we will need to add to the metadata the module specifier of the adapter used.

@yungcheng yungcheng merged commit 2c5c540 into master Jan 30, 2018
@yungcheng yungcheng deleted the vince/update-wire-syntax branch January 30, 2018 20:39
Copy link
Contributor

@kevinv11n kevinv11n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, a handle of nits and a question.

test('decorator expects a string as first parameter', `
test('decorator expects a function identifier as first parameter', `
import { wire } from 'engine';
import { record } from 'data-service';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: s/record/getRecord/. That's the format we're using per https://salesforce.quip.com/tpa5AVyZ21kc

}
`, `
import { wire } from 'engine';
import { record } from 'data-service';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as 36

loc: {
line: 4,
column: 6,
},
});

test('decorator expects an oject as second parameter', `
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not you but we have a typo in object (missing b)

@@ -1,9 +1,9 @@
import { Element } from 'engine';

import { serviceTodo } from 'todo';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: s/serviceTodo/getTodo/

});
it("returns with a known function identifier adapter id", () => {
const wireDef = { adapter: knownFunc };
target.getAdapter(wireDef, "target");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no assert?

throw new Error(`Unknown wire adapter id '${wireDef.type}' in ${wireTarget}'s @wire('${wireDef.type}', ...)`);
}
} else if (wireDef.adapter) {
adapter = ADAPTERS.get(wireDef.adapter.name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this handle identically named functions from multiple modules? I don't believe Function.name handles that.

Once we move from a string key to a function we'll need to adjust wire-adapters.js comments. The code looks like it should be fine because it's exclusively using a Map.

Copy link
Contributor Author

@yungcheng yungcheng Jan 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea this is baby step and if you import same function identifier from different modules then it won't work

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, want to file that as an issue or tackle it now? FWIW I think it's fine for the time being because we can ensure uniqueness in LDS.

I've some big questions around how this will work with query rollup. For me, the topic is sufficiently complex that I need to spike with functioning code to be confident in the design.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants