-
Notifications
You must be signed in to change notification settings - Fork 392
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
Conversation
Benchmark comparisonBase commit:
|
@@ -76,7 +76,7 @@ function validatePairSetterGetter(decorators) { | |||
}); | |||
} | |||
|
|||
module.exports = function validate(klass, decorators) { | |||
module.exports = function validate(klass, decorators, importSpecifiers) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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', { |
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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 })) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 stringengineImportSpecifiers
: 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)) { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
@pmdartus, question for you: @kevinv11n thoughts? |
Re for |
ade0071
to
e4e92fe
Compare
@pmdartus second pass, now no longer need to keep a list fo imported identifiers |
Benchmark comparisonBase commit:
|
Benchmark comparisonBase commit:
|
Benchmark comparisonBase commit:
|
There was a problem hiding this 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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
existence check on adapterImportPath
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
120c13c
to
b46ef01
Compare
Benchmark comparisonBase commit:
|
There was a problem hiding this 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.
There was a problem hiding this 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'; |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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', ` |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
PR Checklist
What kind of change does this PR introduce? (add 'x' - [x])
Does this PR introduce a breaking change? (check one)
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.
More details on LWC semantic commit can be found here.
Other information: