meteor / meteor-feature-requests

A tracker for Meteor issues that are requests for new functionality, not bugs.
Other
89 stars 3 forks source link

Support aliases for modules and npm packages #60

Open joncursi opened 7 years ago

joncursi commented 7 years ago

Migrated from: meteor/meteor#5869

I'm having a really hard time porting my React Native components over for usage on the web inside Meteor via react-native-web. React Native Web recommends in their setup guide creating an alias on node_modules that maps all react-native imports to react-native-web, but this is not possible right now with Meteor.

Instead of a simple configuration, you are forced to use sloppy solutions like this. This is bad for two reasons:

1) It's ugly code, and Flow and ESLint can't interpret it 2) Because you have to manually declare the module to import from, you lose out on the ability to use any 3rd party modules that have a specific import baked into their source code, such as react-native-image-progress. Which means you end up asking for bad feature requests on these third party modules to work around Meteor's limitations.

benjamn commented 7 years ago

Meteor's runtime module system already supports aliases! This means you can use meteorInstall to tell the module system to reinterpret any module identifier by resolving a different module identifier in the same context.

For example, with this code, any import that would have resolved to /node_modules/react-native will instead resolve react-native-web, as if imported from a module within the node_modules directory:

meteorInstall({
  node_modules: {
    "react-native": "react-native-web"
  }
});

However, this is only a runtime remapping, so you need to

  1. make sure react-native-web is getting bundled due to a require("react-native-web") import somewhere else in your code, and
  2. make sure that /node_modules/react-native is not included in the bundle, because it will take precedence over the alias (see this comment for an idea).

The second requirement is the tricky one, I'm afraid.

mitar commented 7 years ago

For same idea for Atmosphere packages, see #47.

joncursi commented 7 years ago

@benjamn Hmm... would you be able to provide a slightly deeper code example, or is this meteorInstall configuration documented somewhere where I can learn more about it? I tried inserting the code snippet you gave in both Meteor client and server startup code but my app keeps complaining with Uncaught Error: Cannot find module 'react-native'.

To give some more context on how I have my system setup: my setup isn't a mono-repo, it's actually three different modules / projects that are independently versioned:

The "core" project: houses all universal JavaScript code, and exports a common set of components. This is a private npm module that currently depends on react-native and react-native-web because it is consumed by both types of clients, and tries to figure out at run time which client is consuming it:

// project-core/lib/components/View.js
/* @flow */

import Platform from '../Platform';

const rnId = 'react-native';
const {
  View,
// $FlowFixMe
} = Platform.OS === 'web' ? require('react-native-web') : require(rnId); // eslint-disable-line

export default View;

The "native" project: houses all iOS and Android specific code; depends on core project to build out screens with a common set of JS and components. The genesis of this repo was initialized using react-native init; this project depends on the "core" project as well as react-native:

// project-native/imports/ui/SomeScreen.js
import View from 'project-core/lib/components/View'; // React Native manages this repository's imports
import SomethingElse from 'react-native-specific-package';

const Component = () => (
  <View>
    { ... }
  </View>
);

The "web" project: houses all iOS and Android specific code; depends on core project to build out screens with a common set of JS and components. The genesis of this repo was initialized using meteor create; this project depends on the "core" project as well as react web:

// project-web/imports/ui/SomeScreen.js
import View from 'project-core/lib/components/View'; // Meteor manages this repository's imports
import SomethingElse from 'react-web-specific-package';

const Component = () => (
  <View>
    { ... }
  </View>
);

I'm trying to figure out a better way to export universal components from project-core. After playing around with this for a few days, I have come to the conclusion that I have two needs here:

1) The Meteor project-web and the ReactNative project-native should be able to import / require the same component using the same exact import syntax / declaration. The goal is to merge these two codebases closer and closer together over time by moving more and more code / components into the "core" project, with the ultimate utopia of achieving a truly universal codebase in the future (i.e. everything lives in core, front-end client implementations are super minimal). (As you can see here, I am having a very hard time achieving this as the Meteor build system and react-native's build system handle the build process differently.)

2) Rather than using if / else checks all over the place in the core project to try and figure out which client project is consuming the code (and then import from the "appropriate" npm module: react-native or react-native-web), I would prefer to have all of my core code simply import from the react-native namespace. This has a couple of massive benefits:

A) This will work out of the box in React Native, and if Meteor does support aliasing (though I haven't been able to get it to work myself), it would run in Meteor as well with a small amount of configuration in project-web. Because I would be now importing from a single source, I wouldn't need to have the core project depend on react-native-web at all (this is now just a project-web dependency), and I can delete the ternary expressions, and Flow and ESLint will be way happier.

B) There are tons of 3rd party modules that are built around the react-native component primitives, such as react-native-image-progress. Because both client projects would know how to handle imports that come from the react-native namespace, either project can use any 3rd party module that depends only on this same set of component primitives without requiring any additional configuration / burden on these 3rd party modules to explicitly support web clients. Essentially, my web app would get access to these 3rd part modules "for free".


Would you be able to help me understand how I can modify the above setup to achieve these two needs?

benjamn commented 7 years ago

That meteorInstall code has to run before you import react-native for the first time. You might want to look at your code in DevTools without source maps to make sure things are happening in the order you expect.

zimme commented 7 years ago

If this technically is possible in Meteor, maybe this should be a FR for documentation?

joncursi commented 7 years ago

+1 for documentation or some sort of concrete example. Maybe a simple repo? I'm still having trouble with this. I've tried removing all client and server code and just writing this in a brand new Meteor app:

// client/main.js

meteorInstall({
  node_modules: {
    "react-native": "react-native-web"
  }
});

const TextComponentOne = require('react-native-web').Text;

const TextComponentTwo = require('react-native').Text; // error happens here

and it errors out with

Uncaught Error: Cannot find module 'Text'
screen shot 2017-06-12 at 3 08 08 am

Here is my entire app.js file that Meteor generates (it looks like meteorInstall comes first as you described?):

var require = meteorInstall({"client":{"template.main.js":function(){

///////////////////////////////////////////////////////////////////////
//                                                                   //
// client/template.main.js                                           //
//                                                                   //
///////////////////////////////////////////////////////////////////////
                                                                     //
Meteor.startup(function() {                                          // 1
  var attrs = {"bgcolor":"#fafafa"};                                 // 2
  for (var prop in attrs) {                                          // 3
    document.body.setAttribute(prop, attrs[prop]);                   // 4
  }                                                                  // 5
});                                                                  // 6
                                                                     // 7
///////////////////////////////////////////////////////////////////////

},"main.js":function(require){

///////////////////////////////////////////////////////////////////////
//                                                                   //
// client/main.js                                                    //
//                                                                   //
///////////////////////////////////////////////////////////////////////
                                                                     //
"use strict";                                                        //
                                                                     //
meteorInstall({                                                      // 5
  node_modules: {                                                    //
    "react-native": "react-native-web"                               //
  }                                                                  // 3
});                                                                  // 4
                                                                     //
var TextComponentOne = require('react-native-web').Text;                          // 11
                                                                     //
var TextComponentTwo = require('react-native').Text;                              //
///////////////////////////////////////////////////////////////////////

}}},{
  "extensions": [
    ".js",
    ".json",
    ".html",
    ".css",
    ".graphql",
    ".jsx"
  ]
});
require("./client/template.main.js");
require("./client/main.js");
artpolikarpov commented 7 years ago

@joncursi try to add import 'react-native-web'; just before your meteorInstall call.

Falieson commented 7 years ago

webpack has had import alias resolving for a long time now... relative imports can't possibly be a good idea for a maintainable application!

joncursi commented 7 years ago

@benjamn does meteorInstall work the same on the Meteor server?

Without SSR I finally got this to work on the client by inspecting the app.js and making sure meteorInstall came first. But now that I'm trying to use SSR via server-render, the Meteor server is complaining now about the same stuff:

Error: Cannot find module 'react-native'
W20170819-18:48:36.638(-4)? (STDERR)     at Function.Module._resolveFilename (module.js:485:15)
W20170819-18:48:36.638(-4)? (STDERR)     at Function.Module._load (module.js:437:25)
W20170819-18:48:36.638(-4)? (STDERR)     at Module.require (module.js:513:17)
W20170819-18:48:36.638(-4)? (STDERR)     at require (internal/module.js:11:18)
W20170819-18:48:36.639(-4)? (STDERR)     at Object.<anonymous> (/Users/joncursi/Sites/joncursi/redbird-web/node_modules/react-native-vector-icons/dist/lib/react-native.js:1:75)
W20170819-18:48:36.639(-4)? (STDERR)     at Module._compile (module.js:569:30)
W20170819-18:48:36.639(-4)? (STDERR)     at Object.Module._extensions..js (module.js:580:10)
W20170819-18:48:36.639(-4)? (STDERR)     at Module.load (module.js:503:32)
W20170819-18:48:36.639(-4)? (STDERR)     at tryModuleLoad (module.js:466:12)
W20170819-18:48:36.640(-4)? (STDERR)     at Function.Module._load (module.js:458:3)

The same app.js is working great on the client, but failing on the server. Any ideas?

Telokis commented 6 years ago

I don't know why but nothing changes at all when I use meteorInstall.

At the very top of client/main.jsx and server/main.js

import "preact-compat";

meteorInstall({
  node_modules: {
    "react-dom": "preact-compat",
    react: "preact-compat",
  },
});
sakulstra commented 6 years ago

It's also not working for me atm (but didn't do much debugging yet)

chiming in with another use/problem-case: we run into this problem at a regular basis as from time to time package A has a mission critical bug and pr-merging/releasing happens to slowly. So we will release a fork for the meantime and are forced to rewrite all import statements (at least this is what we did in the past as I didn't find the current solution in the docs). This approach get's especially annoying when package B relies on package A and both(A and A-fork) ends up in the build.

It would be cool if we had sth similar to jest moduleMapper or webpack aliases in place.

dhamaniasad commented 6 years ago

I tried using the meteorInstall method and could not get it to work. I found a Babel plugin that transformed imports and modified it to work with my specific use case (using preact).

You need to have meteor-babel installed (meteor npm install meteor-babel), and you'll need Babel 7. (the default if you install it now)

Here's the plugin, and it'll allow you to use plugins written for React with Preact and any other use case you might have for transforming imports / aliases.

{
  "plugins": [
    ["babel-plugin-meteoralias", {
        "aliases": [
            {
                "from": "react",
                "to": "preact-compat"
            },
            {
                "from": "react-dom",
                "to": "preact-compat"
            }
        ]
    }]
  ]
}

Edit: Just realized this is not working with code from files in your node_modules directory, only the top-level code that you've written :(

tafelito commented 6 years ago

I was able to make this work using it like this

Option 1 - using import inside Meteor.startup

client/main.js

import React from "react";
import "react-native-web";
import { Meteor } from "meteor/meteor";

meteorInstall({
  node_modules: {
    "react-native": "react-native-web", //
  },
});

Meteor.startup(() => {
  import { AppRegistry } from "react-native";
  import App from '../imports/ui/App';

  AppRegistry.registerComponent("App", () => App);

  AppRegistry.runApplication("App", {
    rootTag: document.getElementById("root"),
  });
});

Option 2 - using require

client/main.js

import React from "react";
import "react-native-web";
import { Meteor } from "meteor/meteor";

meteorInstall({
  node_modules: {
    "react-native": "react-native-web", //
  },
});

const AppRegistry = require("react-native").AppRegistry;
const App = require("../imports/ui/App").default;

Meteor.startup(() => {
  AppRegistry.registerComponent("App", () => App);

  AppRegistry.runApplication("App", {
    rootTag: document.getElementById("root"),
  });
});

Even though this is working, I still get a missing module for react-native when running the project

Is there any other way of doing it, or is it ok to do it like this?

Batistleman commented 5 years ago

Did you manage to solve this? I'm also still getting a warning when bundling...

Batistleman commented 5 years ago

@tafelito did you find a solution for this?

dagatsoin commented 5 years ago

I managed to use react-native-icon with this:

declare const __meteor_runtime_config__: any
declare const cordova: any
declare const meteorInstall: any

// This will make the meteor bundlers embed react-native-web
// tslint:disable-next-line: no-var-requires
import 'react-native-web'

// Then we can set an alias to replace react-native module by react-native-web
meteorInstall({
  node_modules: {
    "react-native": "react-native-web"
  }
})

import './importSpaceLift'

// tslint:disable-next-line: no-var-requires
require('/imports/startup/client')

document.addEventListener("deviceready", onDeviceReady, false)
function onDeviceReady() {
  console.log(cordova.file)
  console.log("/meteor/" + __meteor_runtime_config__.autoupdateVersionCordova)
}

in my tsconfig.json (remove unrevelant part)

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "jsx": "react",
    "downlevelIteration": true,
    "module": "commonjs",
    "target": "es5",
    "noImplicitAny": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "sourceMap": true,
    "skipLibCheck": true,
    "lib": ["es2017", "dom"],
    "types": ["meteor", "jest", "node"],
    "strictNullChecks": true,
    "baseUrl": "./", 
    "paths": { 
      "react-native": ["react-native-web"],
      "~/*": [ "./imports/*" ]
    } 
  },
  "include": [
    "client/**/*",
    "imports/**/*",
    "server/**/*",
  ],
  "exclude": [
    "node_modules"
  ]
}
trusktr commented 5 years ago

The meteorInstall trick is too tricky. Something in https://github.com/meteor/meteor-feature-requests/issues/353 would be better, so that it applies the same across all imports in an application, without nuances.

tafelito commented 5 years ago

@Batistleman

@tafelito did you find a solution for this?

It's working for me with either the 2 options I wrote above. The only minor "issue" with that is the warning you get, but as @benjamn said meteorInstall is only a runtime remapping, so until something like webpack aliases is implemented in meteor, that's the best solution I found

mrauhu commented 4 years ago

Meteor version >= 1.8.1 and NPM >= 6.9

Example: Inferno as React in Vue

<template>
  <AutoForm :schema="schema" :onSubmit="submit"/>
</template>

<script>
  import { ReactInVue } from 'vuera';
  import { AutoForm } from 'uniforms-bootstrap4';

  import schema from './schema';

  export default {
    name: 'Form',
    data() {
      return {
        schema,
      }
    },
    methods: {
      submit(data) {
        console.log('submit() - data', data);
      }
    },
    components: {
      AutoForm: ReactInVue(AutoForm),
    }
  };
</script>

Solution: NPM alias

meteor npm install --save inferno-compat react@npm:inferno-compat react-dom@npm:inferno-compat