Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using shared classes for single output client and common.js based node.js code #5094

Closed
andrewvarga opened this issue Oct 3, 2015 · 10 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@andrewvarga
Copy link

I have a general question about project structures / modules.
It's a project with 3 types of code:

  • client: classes used for the client web-app, built into a single js file (with --out).
  • server: code running on node
  • shared: classes used by both client and server, typically model classes with functions like validation, etc

My aim is this:

  • use 1 file per 1 class at the source level
  • deploy the client app into a single output file (for performance reasons: better start up time), the server can be deployed as many files (or if possible and makes sense from a perf. point of view it can be a single file too). These can be 2 separate compilations, the client with --out and the server with commonjs.
  • reuse the shared model classes (without duplication) in the client and the server
  • be able to use circular dependencies (although shared code will not depend on client/node code, only the other way around, circular dependencies can only occur in within the client code)

I can't seem to achieve these goals. The problem is this:

  • if I structure the shared code so that I only use the ///<reference.../> tags (like I do in the client) that won't be usable when compiling the server with common.js
  • if I use common.js style requires in the shared code then that won't work for the client's single file --out compilation

These are the options I think I have:

  • switch to using require js (amd) in the client as opposed to references and the single file output, and maybe use some post compile script to concatenate the modules to a single file. However circular dependencies may occur sometimes and that's a big issue I have with require.js
  • switch to using es6 style module syntax and use it on the client and in node with some polyfills/loaders until natively supported
  • switch to using commonjs style modules and load them with browserify on the client
  • do a pre-compilation step and transform the shared code to be eligible once for the client side single output compilation and once for the node.js side common.js compilation. However this can have many caveats and sounds like a hack.

What I'm really looking for is a way for each TypeScript class/file to be able to "say" what is the class it exports, but not specify how it will be consumed, ie. via commonjs or as a single file output. Probably that's not possible though in a simple way?

@andrewvarga andrewvarga changed the title Using shared classes for single output client and common.js based node.js code accidental duplicate Oct 3, 2015
@mhegazy mhegazy added the Question An issue which isn't directly actionable in code label Oct 5, 2015
@mhegazy
Copy link
Contributor

mhegazy commented Oct 6, 2015

what does your shared code look like? does it have any dependency on node modules? do you have issues using modules in general for your client, v.s. a single output file?

@mhegazy mhegazy reopened this Oct 6, 2015
@mhegazy
Copy link
Contributor

mhegazy commented Oct 6, 2015

First things first, i think you should have a two or three tsconfig files, each represent one of your build targets. so a directory structure like so:

src\client
src\client\clientClass1.ts
src\client\tsconfig.json

src\server
src\server\serverModule.ts
src\server\tsconfig.json

src\shared
src\shared\sharedLibrary.ts

Then a simple proposal is to use ES6 modules all the way. the compiler will emit your modules down to the appropriate format, so in your server tsconfig.json, you would specify "module": "commonjs" where as in your client you would specify "module": "amd" and use requirejs, or go commonjs and use browserify or webpack.. up to you. but the sources should be shared between he two.

the other option, which is more convoluted and will require a post build step to massage the code; if you can not use module loaders in your client code, is to isolate all module dependencies in your server code, then in the shared, they are just classes. Build the shared files without --module, and no exports or imports, but all inside a single namespace, say namespace MyApp { ... }; in your client code, you include them directly, and emit using --out. in your server code, you first emit the shared code to a single file, shared.js, and a single .d.ts shared.d.ts, augment these with some code to export them as a module, e.g. append exports = MyApp at the end of your shared.js and shared.d.ts, then import them from your server code.

@andrewvarga
Copy link
Author

Thanks for the reply!

My shared code is very simple: it has no dependency on node or anything outside it's folder, it is just a bunch of interfaces and classes (typically simple models with some methods to validate data).

A sample structure would be:

client
client/WebApp.ts - depends on LoginView, shared/User.ts and shared/Project.ts
client/LoginView - depends on shared/User.ts

server
server/Index.ts (main node.js file) - depends on shared/User.ts, server/Login.ts
server/Login.ts (login service handling class) - used to handle the login api request, doesn't depend on anything

shared
shared/User.ts (the user model) - depends on shared/Project.ts
shared/Project.ts (the project model) - doesn't depend on anything

The reason I would want to avoid any dynamic module loading on the client is that I will likely have a lot of module files (= classes in my case), loading them all dynamically would have a significant effect on startup time, and I will most likely need all the classes in my app that are referenced (directly or indirectly) by the main WebApp file anyway so it doesn't add much value to only load the modules dynamically. My aim is to have a single minified js file.

So your second suggestion sounds quite good. The only problem I have with is that it's a nice feature of the "--out" flag that it will only compile files that are referenced, so unused classes will not be compiled into the result. The same way it would be useful to only use the classes of the shared folder that are used by the server. However if this is not possible, it's not a biggie.

I'm thinking that it could also be possible to have a preprocess step on the shared classes before compiling them for the server code: I could just add some wrapper code to make them modules and so the server compile will work ok with that and it will only pick up the files that are used (required) by the server code).

@mhegazy
Copy link
Contributor

mhegazy commented Oct 6, 2015

I will likely have a lot of module files (= classes in my case), loading them all dynamically would have a significant effect on startup time,

We have a PR out (#5090) that will allow you to emit all your modules to a single file (assuming you are not targeting CommonJS). i think this will solve the startup speed, and will act like your bundler. you still need a loader to resolve dependencies, but i think the worst case scenario, you can have a microloader that is 20 lines long, if you have no external dependencies.

The only problem I have with is that it's a nice feature of the "--out" flag that it will only compile files that are referenced, so unused classes will not be compiled into the result. The same way it would be useful to only use the classes of the shared folder that are used by the serve

this is an interesting proposition. it should be fairly easy to do that, if you are using modules all the way, do you mind filing an issue for this suggestion.

I'm thinking that it could also be possible to have a preprocess step on the shared classes before compiling them for the server code:

you could do that, but i would reconsider modules. ES6 is now the standards, and modules will be the norm in the coming years, so i would definitely consider them for new projects, to avoid the migration work a year or two down the line.

@andrewvarga
Copy link
Author

That sounds good about that PR and using a microloader.

For the suggestion, do you mean that the PR you linked could be able to only output referenced modules or?

ES6 modules seems to be the standard, although I definitely remember a few years ago when everyone was all about amd and that that was going to be the future. I believe that it will in fact be ES6 modules, but my biggest concern about that is that I feel it has a slightly different purpose than how I'm organizing my code. I try to follow best practices used in Java, C#, C++ codebases ie. to use a lot of classes, even for the smallest things, each being in it's own physical file, so I really have a lot of classes. To me namespaces work well, and I prefer using references instead of imports/requires (a small annoyance with imports is that WebStorm only shows the import/require line if I click on a Class type as opposed to navigating me to the actual Class file.
That said I'm reconsidering this now, being future proof is important. The PR you mentioned will take care of being able to concatenate es6 modules into a single file right? Have any rough guesses when it could hit a beta/stable release?

@mhegazy
Copy link
Contributor

mhegazy commented Oct 6, 2015

For the suggestion, do you mean that the PR you linked could be able to only output referenced modules or?

Sorry about that. i was thinking of #4434. i think for your needs, you can get this working today. assuming that you have your code all organized in modules, if you pass your "entry module" to the compiler, it will follow the "import" statements and only emit the ones that are used. giving you a module-level tree shaking support.

To me namespaces work well, and I prefer using references instead of imports/requires (a small annoyance with imports is that WebStorm only shows the import/require line if I click on a Class type as opposed to navigating me to the actual Class file.

it is totally up to you, but again, if you are starting on a new project, i would give modules another look

The PR you mentioned will take care of being able to concatenate es6 modules into a single file right? Have any rough guesses when it could hit a beta/stable release?

hopefully merged within the next few days or so, once it is in master it should appear in the nightly npm release (npm install typescript@next) and you could give it a try. the next release is TypeScript 1.7, and we are trying to keep releases going out every 6-8 weeks, so it should not be too long :)

@andrewvarga andrewvarga changed the title accidental duplicate Using shared classes for single output client and common.js based node.js code Oct 19, 2015
@andrewvarga
Copy link
Author

I thought of another way, but I can't get it to work.
The idea is simple: on the server code keep using references and compile to a single file output (--out option), so no modules used for my own classes.
Of course I have to use modules like express, but those I would import like this:

var express = require('express');

The problem is the variable express should have proper typing, and I couldn't get it work.
I have the express typings referenced like this:

///<reference path="./lib/express/express.d.ts"/>

but this doesn't work:

var express: express = require('express');

Then I tried the methods suggested here:
#3180 (comment)

But even when I had a typings.d.ts file with the content:

///<reference path="./lib/express/express.d.ts"/>
declare var ex: typeof express;

and I try to use it like this:

///<reference path="./typings.d.ts"/>
var express: ex = require('express');

I get these errors:

Index.ts(9,18): error TS2304: Cannot find name 'ex'.
typings.d.ts(4,24): error TS2304: Cannot find name 'express'.

This is with TS 1.6.2 with compiler options: Index.ts --target ES5 --out index.js

@mhegazy
Copy link
Contributor

mhegazy commented Oct 26, 2015

I do not think this is going to work. the way the express typings are authored is to only be imported as a module.

@andrewvarga
Copy link
Author

That's ok. In the meanwhile I have found a solution for this entire topic which I think will work for me, but I'm still going to consider switching over to ES6 modules in the long term.
The solution is to simple:

  • Keep using references for all my classes to reach other classes with the out option on the client side
  • Use both --module commonjs and --out build/CombinedDependencies.js to compile the server side Index.ts file. With this there will be 2 files created because of this compilation setup: all the files that are referenced will be compiled into the single result file "CombinedDependencies.js", andthere will also be an Index.js file beside Index.ts resulting of the commonjs compile flag which will contain only the code generated from Index.ts's content. With a single build step I can append the content of "Index.js" to "CombinedDependencies.js" and I'm done.

@mhegazy
Copy link
Contributor

mhegazy commented Nov 13, 2015

Looks like this has been addressed. please reopen if you have other questions.

@mhegazy mhegazy closed this as completed Nov 13, 2015
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

2 participants