Skip to content

Instantly share code, notes, and snippets.

@DanielDornhardt
Created October 12, 2023 14:00
Show Gist options
  • Save DanielDornhardt/9b235fe400d5e87778e2fe5548aa6a08 to your computer and use it in GitHub Desktop.
Save DanielDornhardt/9b235fe400d5e87778e2fe5548aa6a08 to your computer and use it in GitHub Desktop.
Babel Plugin to De-Asyncify Meteor Client Code
/**
* This is a babel plugin to de-asyncify MeteorJS client code.
*
* Note: This is currently experimental.
*
* This is primarily meant to restore the full reactivity of the Tracker module by removing the Async code necessary
* for the server, and instead calling synchronous versions of the same which work on the client because of minimongo being able
* to work synchronously.
*
* It does the following transformations:
*
* - removes all async and await statements / declarations from client code
* - changes all calls to known / declared *Async functions (see below) to the sync ones, eg. findOneAsync => findOne etc.
*
* CAVEAT: This de-asyncifies ALL async functions & awaits on the client.
*
* In order to mitigate this where necessary, here are 2 options for now:
*
* a) use Promises instead of native async / await
*
* b) use the "tag" @dontDeasync in a comment before your async function to skip its deAsyncification! Awesome.
*
*
* like this:
*
* // @dontDeasync
* async function iNeedToStayAsync() {
* // ...
* }
*/
/**
* Replace these functions with their sync counterparts. Client code only.
*/
const functionReplaceMap = {
// Mongo.Collection:
'findOneAsync': 'findOne',
'insertAsync': 'insert',
'removeAsync': 'remove',
'updateAsync': 'update',
'upsertAsync': 'upsert',
// Collection.Cursor:
'countAsync': 'count',
'fetchAsync': 'fetch',
'forEachAsync': 'forEach',
'mapAsync': 'map',
// accounts-base:
'userAsync': 'user',
// callback-hook:forEachAsync
// forEachAsync
// ddp-server
// callAsync
};
module.exports = function (babel) {
const { types: t, caller } = babel;
let callerInfo
caller(function (c) {
callerInfo = c
})
// detect server/client for Meteor, we only need this for client code
let isServer = false
/**
* Utility function to check for the presence of the "@dont-deasync" tag in leading comments
*
* @param node
* @return {boolean}
*/
function hasDontDeasyncTag(path) {
const checkCommentForTag = (comment) => comment.value.includes('@dontDeasync');
let currentPath = path;
while (currentPath) {
const node = currentPath.node;
if (node.leadingComments && node.leadingComments.some(checkCommentForTag)) {
return true;
}
currentPath = currentPath.parentPath;
}
return false;
}
return {
name: "async-to-sync",
visitor: {
Program: {
enter(_, state) {
isServer = callerInfo?.arch.startsWith('os.')
},
},
Function(path) {
if (isServer) {
return
}
if (hasDontDeasyncTag(path)) {
return; // Skip this function if the tag is present
}
if (path.node.async) {
path.node.async = false;
}
},
AwaitExpression(path) {
if (isServer) {
return
}
if (hasDontDeasyncTag(path)) {
return; // Skip this function if the tag is present
}
path.replaceWith(path.node.argument);
},
CallExpression(path) {
if (isServer) {
return
}
if (hasDontDeasyncTag(path)) {
return; // Skip this function if the tag is present
}
if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property)) {
const calleeName = path.node.callee.property.name;
// Use hasOwnProperty so if hasOwnProperty is called in client code doesn't run into issues here
// because hasOwnProperty would be found in functionReplaceMap's own property chain &
// then tried to be replaced otherwise... O_O :D
if (functionReplaceMap.hasOwnProperty(calleeName)) {
path.node.callee.property.name = functionReplaceMap[calleeName];
} }
},
},
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment