fragment/js_babel/fragment_js_babel.js

/**
 *  JavascriptBabelFragment
 *  Write typescript code and execute it in Codestrates
 * 
 *  Copyright 2024 Janus B. Kristensen, CAVI,
 *  Center for Advanced Visualization and Interaction, Aarhus University
 *    
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
**/

/* global webstrate, Fragment, wpm */

wpm.onRemoved(() => {
    Fragment.unRegisterFragmentType(JavascriptBabelFragment);
});

/**
 * A fragment containing Javascript code enchanced by Babel
 *
 * Supports auto - executes require() on load
 * @extends Fragments.Fragment
 * @hideconstructor
 * @memberof Fragments
 */
class JavascriptBabelFragment extends Fragment {
    constructor(html) {
        super(html);
        this.lastRaw = null;
    }
    
    isUniqueModules(){
        return true;
        return this.element.getAttribute("unique-modules");
    }

    /**
     * Evaluates the JavaScript code inside this fragment and returns the export object
     * @example
     * let exportedObject = Fragment.one("#myJSFragment").require();
     * @returns {Promise<Object>}
     */
    async require() {
        let self = this;
        
        // Check if we already have a compiled version in memory and use that
        if ((!this.isUniqueModules()) && this.currentModule && this.raw == this.lastRaw){
            return this.currentModule;
        }
        
        let processedCode = null;
        if (typeof AutoDOMCache !== "undefined"){
            processedCode = await AutoDOMCache.get(self);
        }
             
        if (processedCode==null){
            // Support for css-like imports from other fragments
            function fragmentImports({types,template}){
                return {
                    visitor: {
                        ImportDeclaration(path,state){
                            if (path?.node?.source?.value?.startsWith("#")){
                                let fragmentString = path.node.source.value.replaceAll("\"","\\\"");
                                let specifierString = "throw new Error('Couldnt parse import specifier, try something simpler like import {Thing} from \"...\"')";
                                if (path.node.specifiers.length===1 && path.node.specifiers[0].type==="ImportNamespaceSpecifier"){
                                    specifierString = "var "+path.node.specifiers[0].local.name+" = await fragment.require();";
                                    path.scope.removeBinding(path.node.specifiers[0].local.name); // Fix Babel not removing old bindings from scope
                                } else if (path.node.specifiers.length===1 && path.node.specifiers[0].type==="ImportDefaultSpecifier"){
                                    specifierString = "var "+path.node.specifiers[0].local.name+" = (await fragment.require()).default;";
                                    path.scope.removeBinding(path.node.specifiers[0].local.name); // Fix Babel not removing old bindings from scope
                                } else {
                                    let specifiers = path.node.specifiers.map(specifier=>{
                                        let assignment = "var "+specifier.local.name+"=wrapper."+specifier.imported.name;
                                        path.scope.removeBinding(specifier.local.name); // Fix Babel not removing old bindings from scope
                                        return assignment;
                                    });
                                    specifierString = "let wrapper = await fragment.require();"+specifiers.join(";");
                                }

                                let replacementProgram = Babel.transform(`
                                    {try {
                                        let fragment = Fragment.one("`+fragmentString+`");
                                        if (!fragment) throw new Error("Couldn't find fragment");
                                        `+specifierString+`
                                    } catch (ex){
                                        throw new Error("Unable to import '`+path.node.source.value.replaceAll("\"","")+"' on line "+path.node.loc.start.line+`: "+ex);
                                    }}
                                `, {ast:true,code:false});
                                path.replaceWith(replacementProgram.ast.program.body[0]);
                            }
                        }
                    }
                };
            }
            
            let compiled = Babel.transform(self.raw,{presets:["react"], ast:true, code:true, plugins:[fragmentImports]});
            processedCode = compiled.code;
            
            // Store in cache for later too
            if (processedCode && typeof AutoDOMCache !== "undefined"){
                 await AutoDOMCache.set(self, processedCode);
            }            
        }

        if (processedCode==null){
            throw new Error("Code is null?");
        }

        processedCode = "let fragmentSelfReference = Fragment.one('code-fragment[transient-fragment-uuid=\""+this.uuid+"\"');"+processedCode;       
        if (this.isUniqueModules()){
            processedCode += "/*"+Math.random()+"*/";
        }
        
        
        let output = await import(`data:text/javascript,${encodeURIComponent(processedCode)}`);
        this.currentModule = output;
        this.lastRaw = this.raw;
        return output;
    }

    onFragmentsLoaded() {
        if(this.auto && !Fragment.disableAutorun) {
            this.require({
                autoRun: true
            });
        }
    }

    supportsAuto() {
        return true;
    }
    
    supportsRun() {
        return true;
    }

    static type() {
        return "text/javascript+babel";
    }
}; window.JavascriptBabelFragment = JavascriptBabelFragment;

Fragment.registerFragmentType(JavascriptBabelFragment);