const razorParentPath = path.findParent(ppath => ppath.isVariableDeclaration());
if (!razorParentPath.parentPath.isExportNamedDeclaration()) {
razorParentPath.remove(); // remove it unless its exported :)
}
So now that node is not removed, it is printing. i skip to the end insert query stage:
razor.replaceWith(graphqlOutput);
if exporting, the razor when it arrives at the insert query stage is now a ExportNamedDeclaration which is kind of indirect - having export const pageQuery = createQuery(); is nice but what if i need multiple declarations and only one of them is a createQuery or createFragment? i punted on it for now.
if (razor.isExportNamedDeclaration()) {
// allow 1 exportconst decs = razor.get('declaration').get('declarations');
if (decs.length > 1)
thrownewError(
'detected multiple export declarations in one line, you are restricted to 1 for now'
);
razor = decs[0].get('init');
}
interest in babel-blade is randomly heating up again (graphql summit is in town, but i dont think i did anything to promote it or anything)
Kent tried it out and came to me with this:
You know, if you need to enforce some limitations on what I can do to get my values out of arrays that's fine too
Like if you have to enforce that all array values must have a map that maps the array to the needed values or something that'd be fine with me
let result = pageQuery(data);
const stuff = result.stuff.map(({ a }) => ({ a }));
and it didnt work:
`
query pageQuery{
stuff {
map_2248: map(${({ a }) => ({
a
})})
}
}
`;
thats not right. basically i forgot/neglected to account for destructuring inside of array method internal functions.
also he was using an old astexplorer which didnt even have the map functions. because my docs were out of date. facepalm.
anyway, i fixed it by detecting destructuring inside workOnRHSParent:
if (!newblade && paramRef.type === 'ObjectPattern') {
// destructuring!// *******this is new*******
parseObjectPattern(paramRef, newSemanticPath);
// hoisted upfunctionparseObjectPattern(ref, semanticPath) {
/* --- we do conCall but the semanticVisitor should ideally perform no actions apart from rename */
conditionalCall(semanticVisitor, ref.type, 'LHS', ref, semanticPath);
/* --- we do conCall but should have no actions performed on it */if (ref.type === 'ObjectPattern') {
const properties = ref.get('properties');
properties.forEach(property => {
const newSemanticPath = [...semanticPath];
const key = property.get('key');
newSemanticPath.push([key.node.name, property]);
const value = property.get('value');
parseObjectPattern(value, newSemanticPath);
});
} elseif (ref.type === 'Identifier') {
const idname = ref.get('name');
semanticTrace(ref, idname.node, semanticVisitor, semanticPath);
}
}
// *******this is new*******
} else {
// kick off the traversal inside the internal function
semanticTrace(paramRef, newblade, semanticVisitor, newSemanticPath);
}
however this is a complete cut and paste of what is already inside TraceLHS. However i was unable to extract it out due to a weird bug where i am shadowing a function. sigh. tech debt.
anyway now you can do this:
let result = pageQuery(data);
const { temp } = result;
const stuff = result.stuff.map(({ a: { c }, b }) => ({ a }));
it's poorly documented and i am realizing i dont have a strong vision of what this looks like. its a good point to just abandon it for now since it wont be as effective a demo as i can do elsewhere.
so to handle array prototypes i theoretically need to go and reimplement the entire Array.prototype spec. however i just dont have time for that right now and it is likely not going to be that useful. so what i can do is have an Ignore list and a special Enabled List where I manually make sure that the blade is passed through to the internal inline function.
i used to have this bit of code inside my datastructure:
add(val) {
let child = this.get(val.name)
// eslint-disable-next-lineif (child && child._alias == val.alias) {
// intentional == here// child = child;
} else {
child = new BladeData(val)
this._children.push(child)
}
return child // return child so that more operations can be made
}
for both my razor and blade and this was important for an "idempotent add" to the children. i could call .add(chilData) repeatedly and there would be no extra children if their details matched up.
however this bit: child._alias == val.alias was screwing me up, i couldnt find out why i had put it in there. since i no longer have val.alias it is screwing up my idempotence. so i removed it. i have no idea if i will regret that but now i have working aliasing.
Deferred execution with Map()
So babel-blade does two passes - one pass reads in the data, and the next outputs the data. in particular, the output of the data modifies the AST to inject the graphql string as well as rename babel-blade inline function calls with aliases. it is this alias renaming that caused me some grief, because without it i can actually do one pass of the script since the graphql string injection doesnt really affect any other part of the AST.
this is my old notes on the babel strategy:
* 1. for each razor
* for each call of the razor that is assigned, that is a blade
* we call parseBlade on the blade
*
* 2. parseBlade
* only assignment targets are blades
* process each reference (variable declarator and standalone) at once
*
* 2.1 process reference
* lhs = process LHS
* rhs process RHS
* merge(lhs, rhs)
* parseBlade on all assign targets (if any)
*
* 2.2 process LHS
* get LHS (either id or objectpattern's key)
*
* 2.3 process RHS
* get RHS list
*
* 2.4 merge()
* added properties can come from either lhsorrhs. rhs taking precedence
* rhs.foreach - call add, return child, call add again
* lhs.foreach - ???
* ifrhsandlhs has no kids, just an assign, just tag the blade and early exit
* else, sequence is:
* .add from rhs, including args
* at the end of rhs, if there is an alias, assign the alias too
* iflhs has kids, keep .adding, including alias
*
* 3. once all blades are parsed, compose and insert the graphql query where the razor is referenced
In short, I used to do this:
read blades into an LHS and RHS queue
pop off the queue:
add into my datastructure (Razors and Blades)
rename aliases if necessary
once queues are depleted, inject graphql
Now i do this
declare an aliasReplaceQueue which is a Map()
semanticTraverse entire AST
read into datastructure
where renaming will be needed, push the node into aliasReplaceQueue
inject graphql
aliasReplaceQueue.forEach and rename
This seems like a much nicer approach (no separate lhs and rhs queues). with Map I get deduplication of nodes for free.
having confidence in prior work
i had another gnarly bug which i could not trace. the only message was Cannot read property '0' of null.
this is inside ASTexplorer where there is no real stepping through of code. the debugger statement is useful but only so useful when you have 600 lines of recursive code. you still need to know where to put it.
it got to a point where i started doubting the wrong piece of code, my print code for printing out the graphql query string (specifically, for fragments). i knew that this was battle tested from prior work, however I could make the error go away just by making this one tiny change that hopefully wouldnt cascade down. of course i made the change, and of course the error wasn't solved - it just flowed down to another test case where my fix was now the bug.
so i spent about 1-2 hours trying to fix both my bug and the bug that my fix caused and that is a hopeless task. eventually i decided to double check my hypothesis of what the bug really was, and did even more logging and tracing. this is how i eventually tracked it down to a totally different part of the code (the aliasReplaceQueue removal above) and solved it with just a simple conditional.
the lesson i guess is dont be so quick to suspect your prior work. if the fundamental assumptions havent changed, its more likely to be your new code that is buggy, than the old code.
woohoo
so i am basically done. i should stop here and clean up the code and work on my vscode extension as well as slide deck.
the last thing i wanna add is the array methods. so lets do this.
(later) ok i enabled some more methods:
const arrayPrototypeEnables = {
map: 1, // not 0 based as we will do null check against thisevery: 1,
filter: 1,
find: 1,
findIndex: 1,
forEach: 1,
reduce: 2, // so we dont touch the accumulator - may be issue in futurereduceRight: 2,
some: 1
};
problem with multiple aliases
my words come back to bite me. i modified my own code above (see "child alias comparison") and found the use case why i had them there originally:
the trackVisitor function is the one the blade implementor implements, but i think it is still doing too much work. time to revamp it.
the game plan
the new plan is to only work based on identifier. if you pass semanticTrace an identifier (lets call it origin), it will go through all the semantic children in your scope and call your callback function. so semanticTrace's job is to only pass you identifier nodes as well as a nice path down from your origin identifier.
now i have refs. for each ref, i need to call semanticChildCallback, then travel up their node tree until i hit some end state, and then semanticTrace the new identifiers that result from that.
One thing I have been punting on for a long while is how to deal with array methods. this is the problem. The chosen API i have for doing inline query arguments looks like this:
// `data` and `createdQuery` declared aboveconst DATA = createdQuery(data);
const list = DATA.list("id:234");
const listtitle = list.title;
this generates a graphql query that looks like:
query createdQuery {
list(id: 234) {
title
}
}
That's nice, but what happens when the field is an array and I want to map over it?
// `data` and `createdQuery` declared aboveconst DATA = createdQuery(data);
const list = DATA.list("id:234");
list.map(item =>console.log(item.title));
map looks like a GraphQL property to our babel plugin!
note: below generated graphql is not real, just trying to illustrate the problem
what we need is for our parser to "ignore" the map call, and then declare item a blade and re process it again.
previously to deal with this i simply blacklisted Array.prototype methods but that obviously wasn't very seamless.
fixing the babel parser
The key part comes here when we parse the RHS of an assignment:
const RHSVisitor = {
MemberExpression(childpath) {
let aliasPath, calleeArguments;
if (isCallee(childpath)) {
// if its a callee, extract its args and push it into RHS// will parse out fragments/args/directives later
calleeArguments = getCalleeArgs(childpath);
aliasPath = childpath;
}
// hacky dodge for array methods; just ignores them for now// we will have to make iteration methods also count as bladesfor (const prop of arrayPrototype) {
if (childpath.node.property.name === prop) {
return;
}
}
if (childpath.parentKey !== "arguments")
// else it will include membexps inside call arguments
RHS.push({
name: childpath.node.property.name,
calleeArguments,
aliasPath
});
}
};
the hacky return in there skips map and its brethren, but the visitor continues operation when I really need to interrupt it. according to the docs you can skip or stop traversal. Stop is the right call here because i dont really care about siblings (though i am probably writing the traversal wrong due to my inexperience; i look for references in scope and iterate through manually but maybe that is not strictly necessary. sad)
so editing the hacky bit above:
// hacky dodge for array methods; just ignores them for now// we will have to make iteration methods also count as bladesfor (const prop of arrayPrototype) {
// i will rewrite this later to use array.includesif (childpath.node.property.name === prop) {
childpath.stop();
return;
}
}
this breaks the above parsing appropriately and generates a shorter graphql query:
query createdQuery {
list(id: 234)
}
and now we have to parseBlade on the map child.
a first solution
Ok it took an hour or two but I figured it out. the trick is to use .get liberally so that you keep using the path instead of the node (thank you SO).
So this code:
// hacky dodge for the other array prototype methodsif (arrayPrototype.includes(childPropName)) {
childpath.stop();
const args = getCalleeArgs(childpath);
if (args.length) {
const mapBladeID = args[0].params[0].name;
const argPath = childpath.parentPath.get("arguments")[0];
parseBlade(argPath, mapBladeID, razorData);
}
return;
}
calls parseBlade correctly within the scope of the arguments of the arrow or normal function and generates the correct graphql. Given this:
// `data` and `createdQuery` declared aboveconst DATA = createdQuery(data);
const list = DATA.list("id:234");
list.map(item =>console.log(item.title));
generate this:
query createdQuery {
list(id: 234) {
title
}
}
so it treats .map correctly by "skipping over it".
Testing
ok so map works but nesting maybe doesnt work so well. this js:
query movieQuery {
movie_659a: movie(id: 234) {
credits {
actors {
year
leading
supporting
}
}
}
}
and is missing the films bit. time to investigate...
solving nested maps
this is happening because i'm not supplying the right slice of razorData when i parse. i may need to move my callsite of parseBlade lower down the LHS-RHS parsing.
but this was throwing some weird error: app-52921c056e00d2b70dc2-16.js:611 TypeError: blade.macro: Cannot read property 'name' of undefined probably due to some downstream printing issues. i need to just filter out the array prototype method names at the source and manually manipulate razorData.
ok i have to call a halt. i discovered an even worse flaw in my output that i havent tested enough. :( array methods will have to be on hold for now.
this js:
const DATA = movieQuery(data);
console.log(DATA.type);
generates:
type {
log_a057: log(${DATA.type})
}
so its not robust to console.log. i need to fix that first.