ractivejs/rvc

Publish as npm package

Closed this issue · 27 comments

@Rich-Harris can you publish this as a npm package?

thought I already had! oops. done: https://www.npmjs.org/package/rvc

Thanks! How do I use it? require('rvc') throws an error as it tries to load Ractive using RequireJS.

Ha, I really should write some docs. The short version - to load a component inside an AMD module:

1. Set up paths config

require.config({
  // ...
  paths: {
    rvc: 'path/to/rvc',
    ractive: 'path/to/ractive'
  }
});

2. Use the plugin!path syntax

// assuming there's a component definition at path/to/component.html
// (note the extension is omitted):
define([ 'rvc!path/to/component' ], function ( Component ) {
  var view = new Component({ el: whatever });
});

Is that what you're trying to do? Or are you trying to compile a component in node.js?

Or are you trying to compile a component in node.js?

Yes. I should have said so. I'm already using this on the front end and I want to be able to reuse the same files on the back end, so I'm working on ractivejs/ractive#538.

Ah, okay. There's another repo, https://github.com/ractivejs/rcu, which is a sort of toolbelt for creating component loaders/builders. Is equally lacking in documentation but it goes something like this:

var rcu = require( 'rcu' ), Ractive = require( 'ractive' );

rcu.init( Ractive ); // otherwise nothing will work

fs.readFile( 'path/to/component.html', function ( err, result ) {
  if ( err ) throw err;

  // this next line gives you an object with five properties:
  //   `template` - the *parsed* template
  //   `css` - self-explanatory
  //   `imports` - any components referenced by e.g. `<link rel='ractive' href='./foo.html'>`
  //   `modules` - any modules referenced by e.g. `d3 = require('d3')`
  //   `script` - any JS code in a `<script>` element
  var parsed = rcu.parse( result.toString() );
  doSomethingWith( parsed );
});

Haven't tried actually rendering components in node.js using this method, only unadorned templates. Though it should be fine as long as the component itself doesn't have any browser-specific requirements... will be interested to hear how you get on.

Actually I take that back, you might run into problems rendering components as the rcu.createFunction method uses a filthy hack to make it possible to locate syntax errors in scripts - it turns the code into a data URI then injects a <script src='[datauri]'></script> on to the page. Might need to have a fallback (though I suppose it might be easier to just do new Function()).

Other methods exposed:

  • rcu.make( source, config, callback, errback ) - make the component class
  • rcu.resolve( relativePath, base ) - resolve paths, if they are relative
  • rcu.getName( 'path/to/foo.html' ) - returns 'foo' in this case

@Rich-Harris Here's what I did in another project to get to the errors.

try{
    var fn = new Function('return (' + js + ');')  //project specific append js to return
}
catch(e){
    eval(js)
    throw e; //should never get here, eval should throw, but...
}

@martypdx but how do you know where the syntax error occurs in the code? The stack trace includes the eval(js) call, but not the code itself. The data URI hack makes debugging much easier:

screen shot 2014-05-31 at 16 56 37

screen shot 2014-05-31 at 17 04 36

Oh, can we write the script out as a tmp file and then require it? For node I mean. Or do we not care if not in the client?

Yeah, that sounds like it could work

This example not fully baked, but you can create a module dynamically:

function requireFromString(src, filename) {
  var Module = module.constructor;
  var m = new Module();
  m._compile(src, filename);
  return m.exports;
}

var js = 'var ;'

var fn = requireFromString('var js = "' + js + '";\nmodule.exports = function(){ eval(js)};' )
console.log( fn() );

Nice!

@Rich-Harris what is the difference between load and build? I know that load calls make which is exactly why it doesn't work on node, but when I changed this to always use build it worked like a charm.

@MartinKolarik RequireJS plugins change their behaviour depending on whether they're being used at runtime or during optimisation. The job of r.js (the RequireJS optimiser) is to create a single file from a starting file (say, app.js) by concatenating its dependencies (and its dependencies' dependencies, recursively).

So during development, when AMD modules are typically loaded asynchronously, load() is used - this calls rcu.make(), which returns a Ractive subclass, ready to be instantiated. But in production, it would be inefficient to a) include rvc.js in the build, and b) do the necessary transformation at runtime. During optimisation, the build() function takes a component.html and turns it into pure JavaScript that can be inlined.

Hope that sheds some light?

@MartinKolarik Might also be instructive to look at broccoli-ractive - it's more environment-agnostic, and can optionally transform a component into an AMD, CommonJS, or ES6 module

@Rich-Harris

So during development, when AMD modules are typically loaded asynchronously, load() is used - this calls rcu.make(), which returns a Ractive subclass, ready to be instantiated.

Still can't see why we can't always use build. What is the advantage in using load? As I said, I changed the rvc to always use build and component in the following code is the ready-to-use Ractive subclass.

var requireJS = require('requireJS');

requireJS.config({
    paths: {
        rvc: 'node_modules/rvc/rvc',
        ractive: 'node_modules/ractive/ractive'
    }
});

requireJS(['rvc!views/index'], function(Component) {
    new Component({ ... });
});

@MartinKolarik load creates a subclass, build creates a string representing an AMD module that will return a subclass once executed. It's interesting that forcing rvc to use build at runtime works - presumably RequireJS executes the string as JavaScript. Wouldn't necessarily have expected that (I've never used RequireJS in node).

This method involves a fair bit of indirection though. Is there any reason to use RequireJS in this context other than to use rvc? We might be able to do things in a more nodey way by just using rcu directly, if we get rid of that browser requirement. (Though to be fair, if it works, it works... :)

@Rich-Harris Yeah, RequireJS must be executing it as typeof component returns function.

I'm using require not only for components, but also for other RequireJS modules, so RequireJS is needed anyway.

var SelectFilesView = require('rvc!search/components/select-files');
var tooltipDecorator = require('decorators/tooltip');// this is a standard RequireJS module

@Rich-Harris Any reason not to use var here?

@MartinKolarik nope! oversight on my part. thanks

@MartinKolarik In case you're interested:

Funnily enough, I need to use server-rendered components in the project I started yesterday. I didn't want to use RequireJS in node - I've learned over the last few days not to trust it, as big a fan of RequireJS as I normally am - but it occurred to me that the ractive-load plugin could work in node if we just replace XHR with fs.

So that's what I've done - ractive-load is now isomorphic, to use the cool kids' lingo:

load = require( 'ractive-load' );
load( 'foo.html', function ( Foo ) { ... });

@Rich-Harris thanks for info! Have you experienced any specific problems with RequireJS in node? I'm not a big fan of adding a big library to save 10 lines of code, but the ability to reuse the code wins (for me) in this case.

@MartinKolarik It's basically 'emulating' node things like module, but imperfectly. You don't have access to things like __dirname, for example - here's a quote from James Burke:

When using the RequireJS require/define APIs it does not support all of the Node idioms, like require.paths, since they do not work across JS environments. __dirname and __filename are those types of APIs.

In a similar vein:

I decided it was better to not try to keep up with node internals, which would require constant gardening

To me that's a big red flag that says 'don't use RequireJS in node'. It will inevitably find a way to break your expectations in a hard-to-debug way at the most inconvenient possible time. I've been bitten by the reverse problem with browserify's imperfect implementations of core node modules in the browser, and since then I steer clear of things that try to increase code portability by pretending to be things they're not, and bend code to match the environment rather than vice versa. But YMMV!

@Rich-Harris I see your point, but I don't think it'll be a problem in this case as I'm not trying to do any fancy stuff. That said, it might be a good idea to create our own implementation to handle define() calls and drop r.js; time will tell. Anyway, I'll publish ractive-express soon (for now with r.js).

Btw, just in case you missed it, I commented on dd234b6 yesterday. I see you solved the Module thing and published a new version, but the syntax error is still there.

@MartinKolarik ah, I did miss that. Will look into it. (That's a perfect example of a bug caused by using RequireJS in node!)

Great news about ractive-express.

@Rich-Harris I ended up supporting both RVC + RequireJS and ractive-load ;)