Dynamic import of Javascript module from stream
Asked Answered
I

2

7

Goal: To support dynamic loading of Javascript modules contingent on some security or defined user role requirement such that even if the name of the module is identified in dev tools, it cannot be successfully imported via the console.

enter image description here

enter image description here

enter image description here

A JavaScript module can be easily uploaded to a cloud storage service like Firebase (#AskFirebase) and the code can be conditionally retrieved using a Firebase Cloud Function firebase.functions().httpsCallable("ghost"); based on the presence of a custom claim or similar test.

export const ghost = functions.https.onCall(async (data, context) => {
  if (! context.auth.token.restrictedAccess === true) {
    throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
  }

  const storage = new Storage();
  const bucketName = 'bucket-name.appspot.com';
  const srcFilename = 'RestrictedChunk.chunk.js';

  // Downloads the file
  const response = await storage
    .bucket(bucketName)
    .file(srcFilename).download();
  const code = String.fromCharCode.apply(String, response[0])

  return {source: code};

})

In the end, what I want to do...

...is take a webpack'ed React component, put it in the cloud, conditionally download it to the client after a server-side security check, and import() it into the user's client environment and render it.

Storing the Javascript in the cloud and conditionally downloading to the client are easy. Once I have the webpack'ed code in the client, I can use Function(downloadedRestrictedComponent) to add it to the user's environment much as one would use import('./RestrictedComponent') but what I can't figure out is how to get the default export from the component so I can actually render the thing.

import(pathToComponent) returns the loaded module, and as far as I know there is no option to pass import() a string or a stream, just a path to the module. And Function(downloadedComponent) will add the downloaded code into the client environment but I don't know how to access the module's export(s) to render the dynamically loaded React components.

Is there any way to dynamically import a Javascript module from a downloaded stream?

Edit to add: Thanks for the reply. Not familiar with the nuances of Blobs and URL.createObjectURL. Any idea why this would be not found?

const ghost = firebase.functions().httpsCallable("ghost");

const LoadableRestricted = Loadable({
  //  loader: () => import(/* webpackChunkName: "Restricted" */ "./Restricted"),
  loader: async () => {
    const ghostContents = await ghost();
    console.log(ghostContents);
    const myBlob = new Blob([ghostContents.data.source], {
      type: "application/javascript"
    });
    console.log(myBlob);
    const myURL = URL.createObjectURL(myBlob);
    console.log(myURL);
    return import(myURL);
  },
  render(loaded, props) {
    console.log(loaded);
    let Component = loaded.Restricted;
    return <Component {...props} />;
  },
  loading: Loading,
  delay: 2000
});

enter image description here

Indolence answered 19/10, 2018 at 15:37 Comment(7)
Did you ever find a complete solution that you liked?Delaware
Shaz or anyone else: Have a complete solution to this problem? I'd like to assign the +500 bounty, but noone has stepped up, and it expires outside of my control tomorrow.Delaware
@Delaware If I understand this correctly, you download a file into a directory ./RestrictedComponent.js and if the user has been authenticated, import that component? Is so, have you considered React.lazy?Ilona
@radihuq: React.lazy may help with the dynamic import aspect (thank you) but doesn't itself prevent unauthenticated users from retrieving the restricted JavaScript from the server by GET'ing from the endpoint apart from the client UI check.Delaware
@Delaware ah okay I see. Maybe server side rendering is the solution, or something like this but I don't have much experience with that. Good luck !Ilona
@radihuq: Server side rendering isn't an option as the module is needed locally for more than just page rendering. Thanks, though.Delaware
Did you ever overcome the Error: Cannot find module 'blob: ?Plutonic
A
4

Read the contents of the module file/stream into a BLOB. The use URL.createObjectURL() to create your dynamic URL to the BLOB. Now use import as you suggested above:

import(myBlobURL).then(module=>{/*doSomethingWithModule*/});
Adversity answered 19/10, 2018 at 15:48 Comment(5)
Thanks @Randy Casburn, edited with "Cannot find module..." error above.Indolence
never knew about this blob thing; so one (quite obvious) thing to consider is that the js code fed there would be compiled js. But webpack not only compiles JS but also replaces imports with its own import logic. Imagine this file you import is code-split and can yet trigger other imports...Adulthood
@wkrueger: Note that OP's module is post-Webpack. Also note my interest in a complete working code sample is not limited to Randy's blob-based approach. Thanks.Delaware
There is a Webpack related comment here under this answer https://mcmap.net/q/444296/-import-a-module-from-string-variableRb
@Rb - I would hope so after 6 years! LOLAdversity
I
-2

You can try using React.lazy:

import React, {lazy, Suspense} from 'react';

const Example = () => {

    const [userAuthenticated, setUserAuthenticated] = useState(true);

    if (userAthenticated) {

        const RestrictedComponent = lazy(() => import('./RestrictedComponent'));

        return (
            <div>
                <Suspense fallback={<div><p>Loading...</p></div>}>
                    <RestrictedComponent />
                </Suspense>
            </div>
        )
    }

    return (
        <div>
            <h1>404</h1>
            <p>Restricted</p>
        </div>
    );

}

export default Example;
Ilona answered 5/11, 2019 at 20:3 Comment(1)
The question is not related to React and is about importing streams and blobs instead of urlsIndusium

© 2022 - 2024 — McMap. All rights reserved.