Using modules in Meteor.js with Typescript - meteor

Folks, I'm trying to do something that I thought ought to be simple, but I must be doing something wrong. I'm trying to simply have a clear structure in my meteor application which uses Typescript.
Here are my requirements:
All interfaces are available in both client and server
Some class implementations are only available on the server
I don't want to rely on file load order for my application to work properly
I need my own module to not clash with global objects (such as the Position class for example)
I need to have one monolithic include file for server, one for both client and server and one for client (don't want to have 10s of includes on top of my files)
The setup that I have right now is this
server
server-book.ts
client
shared
collections.ts
definitions
server
include.d.ts (includes all .d.ts files in this folder)
server-book.d.ts (server specific implementation of book)
client
shared
include.d.ts (includes all .d.ts files here)
book.d.ts (book interface definition)
collections.d.ts
In each .d.ts file I have
module MyModule {
interface Bla {}
};
In each .ts file that defines a class I have:
module MyModule {
export class MyBla implements Bla {};
}
All .d.ts files generated for classes are generated by tsc -d.
No .ts files are being included via ///<reference> rather only .d.ts files.
Now, when I run this, I get an error that MyModule is undefined:
/// <reference path="shared/include.d.ts"/>
/// <reference path="server/include.d.ts"/>
Meteor.startup(() => {
var temp = new MyModule.ServerBook();
});
The error occurs right on MyModule.
What am I doing wrong? What should be the proper setup here?
Thanks!

I have dealt with this issue on my blog. I decided to use the evil eval command, since it gave me the easiest possibility of using modules till something more sophisticated appears.
File /lib/foo.ts is position in the subdirectory since it has to be loaded before Bar.
eval('var Hugo = (this.Hugo || (this.Hugo = {})'); // this will override the automatically emitted var Hugo and assigns it with globally defined Hugo module
module Hugo {
export class Foo {
foo():string {
return 'foo'
}
}
}
File /bar.ts
/// <reference path="lib/foo.ts"/>
eval('var Hugo = (this.Hugo || (this.Hugo = {})'); // this will override the automatically emitted var Hugo and assigns it with globally defined Hugo module
module Hugo {
export class Bar extends Foo {
bar () : string {
return 'bar';
}
}
}
File /test.ts
/// <reference path="lib/foo.ts"/>
/// <reference path="bar.ts"/>
var m = new Hugo.Bar();
console.log(m.bar());
console.log(m.foo());
As mentioned here, for classes, the solution is even simpler:
class ExportedClass {
variable : int;
}
this.ExportedClass = ExportedClass;

Definition files should use the declare keyword. You would normally get an error if you didn't use this keyword.
declare module MyModule {
export interface Bla {}
}
And
declare module MyModule {
export class MyBla implements Bla {
}
}
It is also worth checking that the ServerBook class has the export keyword (just like MyBla in your examples).

After lot of trial and errors, here are my findings so far :
Using typescript "module" keyword doesn't get well with Meteor. I think at the moment you cannot use it (or the workarounds are too complicated for me).
However, here is what you can do :
Let say that you have package A where you want to define a class ClassToExport which you want to make public.
class ClassToExport {
getFoo(){
return "foo";
}
}
Please note that you can't write this.ClassToExport = ClassToExport and
api.export('ClassToExport') or else ClassToExport won't be available in the global scope of package A, hence the need for a module/namespace for exporting your class, which we will see next.
Now, for the class to be available for the consumers of your package, you have to create a namespace, which will be the equivalent of the "module" typescript keyword for internal module.
So let's write :
declare var packageA; //so that the compiler doesn't complain about undeclared var
packageA = packageA || {}; //so that this namespace can be reused for the entire package
packageA.ClassToExport = ClassToExport; //the actual export
Now, don't forget to write
api.export('packageA') in the package.js of package A
If you have a package B where you want to use ClassToExport, you write in package B:
var cte = new packageA.ClassToExport();
without forgetting to api.use package A in package B's package.js
If you don't want to write the namespace each time you use the class, you can also write var ClassToExport = packageA.ClassToExport; at the top of your using file.
If you need a global class for you package only, without exporting it, then you can do instead just :
this.ClassToExport = ClassToExport
and again don't write api.export('ClassToExport'), or it won't be available in the package anymore.
This way, i think the features (export/ import) of internal typescript modules are there.

If you are not afraid of gulp build, I have prepared a typescript boilerplate project which allows you to comfortably use typescript from within your app, not depending on packages.
https://github.com/tomitrescak/meteor-boilerplate-typescript

Random idea, what about extend Meteor instead of Window.
Meteor.yournamespace = Meteor.yournamespace || {};
Meteor.yournamespace.myclass = new MyClass();
or
Meteor.yournamespace.MyClass = MyClass();
I think this is less invasive than go directly to the window object IMHO. my two cents.
now you can do Meteor.yournamespace.MyClass :P
--EDIT
Then you could create a meteor-extend.d.ts file and do something like:
/// <reference path="main.d.ts" />
declare module Meteor {
var yournamespace: any;
}
Now you can remove the <any> before Meteor and Typescript will not complaint.

Related

Extend and override default lighthouse directive

Is there anyway that I can override a directive like: src/Schema/Directives/WhereDirective.php for instance this doesn't support some methods on my custom builder, I know I can make another directive and extend this like #myWhere but that's dirty, would be nice to be able to override the #where itself.
I've searched around but nothing was found about this sadly!
I edit my composer.json and manipulate the class mappings. In this example, I wanted to override some cache classes.
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/",
"Nuwave\\Lighthouse\\Cache\\": "lighthouseV6/cache/"
},
"exclude-from-classmap": [
"vendor/nuwave/lighthouse/src/Cache/CacheKeyAndTags.php",
"vendor/nuwave/lighthouse/src/Cache/CacheKeyAndTagsGenerator.php",
"vendor/nuwave/lighthouse/src/Cache/CacheDirective.php"
]
},
Then create a folder "lighthouseV6/cache" in the root of the project and copy the classes I wanted to override from "vendor/nuwave/lighthouse/src/Cache" inside it.
I found the solution. according to https://lighthouse-php.com/5/custom-directives/getting-started.html#register-directives
When Lighthouse encounters a directive within the schema, it starts looking for a matching class in the following order: 1. User-defined namespaces as configured in config/lighthouse.php, defaults to App\GraphQL\Directives 2. The RegisterDirectiveNamespaces event is dispatched to gather namespaces defined by plugins, extensions or other listeners 3. Lighthouse's built-in directive namespace.
So it did seem like override could be possible, and it was.
I haven't tried first method (App\GraphQL\Directive...) but that probably would work too, I went with the second method the RegisterDirectiveNamespaces event, since I was writing a package.
Make all your directives in the same folder under one namespace eg:
namespace SteveMoretz\Something\GraphQL\Directives;
Now in a service provider (Can be your package's service provider or AppServiceProvider or any service provider you get the idea.) register that namespace your directives are under.
use Illuminate\Contracts\Events\Dispatcher;
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;
class ScoutGraphQLServiceProvider {
public function register(Dispatcher $dispatcher) {
$dispatcher->listen(
RegisterDirectiveNamespaces::class,
static function (): string {
return "SteveMoretz\Something\GraphQL\Directives";
}
);
}
}
That's it so for an example I have overridden the #where directive, first I created a file named as original WhereDirective.php then put these contents in it:
<?php
namespace SteveMoretz\Something\GraphQL\Directives;
use Nuwave\Lighthouse\Scout\ScoutBuilderDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Directives\WhereDirective as WhereDirectiveOriginal;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
class WhereDirective extends WhereDirectiveOriginal
{
public function handleBuilder($builder, $value): object
{
$clause = $this->directiveArgValue('clause', 'where');
// do some other stuff too... my custom logic
return $builder->{$clause}(
$this->directiveArgValue('key', $this->nodeName()),
$this->directiveArgValue('operator', '='),
$value
);
}
}
Now whenever we use #where my custom directive runs instead of the original one, but be careful what you do in this directive don't alter the whole directive try to extend the original and add more options to it, otherwise you would end up confusing yourself later!

How to release or distribute an application that uses mikro-orm?

In the configuration I have to specify the paths to .js and .ts files defining entities:
MikroORM.init({
...
entitiesDirs: ["build/entities"],
entitiesDirsTs: ["src/entities"],
});
So, when I will go to release or distribute the application. Will I need distribute the typescript code too? or will I need distribute only the cache generated? or will I need distribute both? or... none?
As of MikroORM v2.2
Now you can work with default metadata provider, it will require entity source files only if you do not provide entity or type options in your decorators (you can use entity callback to use reference to entity class instead of using string name in type, handle for refactoring via IDE like webstorm).
Original answer:
You should ship the typescript code too, and let the cache regenerate on the server - cache would be rebuilt anyway as it checks absolute path to cached entity for invalidation.
You could implement your own cache adapter or metadata provider to get around this, if you don't want to ship the typescript code.
This is how you could implement custom metadata provider that simply throws error when the type option is missing:
import { MetadataProvider, Utils } from 'mikro-orm';
import { EntityMetadata } from 'mikro-orm/dist/decorators';
export class SimpleMetadataProvider extends MetadataProvider {
async loadEntityMetadata(meta: EntityMetadata, name: string): Promise<void> {
// init types and column names
Object.values(meta.properties).forEach(prop => {
if (prop.entity) {
prop.type = Utils.className(prop.entity());
} else if (!prop.type) {
throw new Error(`type is missing for ${meta.name}.${prop.name}`)
}
});
}
}
Then provide this class when initializing:
const orm = await MikroORM.init({
// ...
metadataProvider: SimpleMetadataProvider,
});
The value of type should be JS types, like string/number/Date... You can observe your cached metadata to be sure what values should be there.
Also keep in mind that without TS metadata provider, you will need to specify entity type in #ManyToOne decorator too (either via entity callback, or as a string via type).

Recompile after imported file changed in meteor app

I'm creating babel plugin to manage import paths according to some conditions based on file existence. On initial build everything works fine, but when I add or remove file that would impact result of the conditions, compiler doesn't recompile file that is importing changed file, therefore conditions are still resolved as if referenced file existed, but compiler is not able to find it.
I believe this is due to cache. So how can I manage/clear cache? Of course I would like to keep as much cacheing as possible. So, maybe, is there way to programmatically remove from cache only files that were impacted by my plugin?
Basically logic of plugin is as follows:
const fs = require("fs");
const fs_path = require("path");
function shouldResoulveImport(node) {
/* some checks, returns bool */
}
module.exports = function(babel) {
return {
visitor: {
ImportDeclaration(path, state) {
if (!shouldResoulveImport(path.node)) {
return;
}
const existing_file = /* some checking for another file existence */;
if (existing_file) {
path.node.source.value = existing_file;
}
}
}
};
};
What i try to achieve is to use some default file for import, but when same file in other particular directory exists then change the import for the other file.
I use meteor 1.7 with babel 7 beta and react.
I've checked with babel support and it seems to be issue of meteor.
Thanks.
There was no way to achieving it in this manner.
So I decided to use try on import overriding file and exporting it further. And if the import fails, it means, that file doesn't exists so default file export its default content:
...
let toExport;
try {
import Overrides from "overriding_path";
toExport = Overrides;
} catch (e) {
toExport = OriginalExport;
}
export default toExport;
Later just build plugin that instead of modifying import modifies file.

Meteor: Importing a JS file isn't working

I have created a JS file inside the lib folder which has a JSON Object assigned to a variable and i am trying to use that variable in the Client folder, in of the template helper function but i get error while running saying the variable isn't defined.
How to solve this ? How to use this variable in both Client and Server ?
deviceMap.js -> inside lib folder
var deviceMap = {
"123456": {
"name": "ABC",
"department": "dept1"
}
}
Template.tmp1.helpers({
console.log(deviceMap);
});
Thank you
Prior to meteor 1.3, the only way to share variables between files is through the global namespace.
Replace:
var deviceMap =
with:
deviceMap =
and your variable will be global instead of file scoped. You may also want to consider namespacing your variable like: DeviceMaps.departments or something.

Is there a way to integrate handlebars-helpers with hapi?

I would like to use the handlebars-helpers node module with my handlebars templates. I'm using hapi as my framework which supports handlebars. I haven't found any documentation or examples that shows how to use handlebars-helpers with hapi using handlebars as the view engine.
Is it possible and if so, what is the solution?
after start hapi server
// hapi v17
try {
await server.start();
} catch (err) {
throw err;
}
//......
// add some handlebars helpers
let hbs = server.realm.plugins.vision.manager._engines.hbs;
// console.log('handlebars_helpers', handlebars_helpers);
if (handlebars_helpers) {
for (let key in handlebars_helpers) {
if (key) {
// console.log('key', key, helpers[key]);
hbs.module.helpers[key] = handlebars_helpers[key];
}
}
}
//check your helper is registered
// console.log('hbs.module.helpers', hbs.module.helpers);
// add some handlebars helpers
I don't think it's currently possible according to hapijs api docs for views:
http://hapijs.com/api#serverviewsoptions
helpersPath - the directory path where helpers are located. Helpers are functions used within templates to perform transformations and other data manipulations using the template context or other inputs. Each '.js' file in the helpers directory is loaded and the file name is used as the helper name. The files must export a single method with the signature function(context) and return a string. Sub-folders are not supported and are ignored. Defaults to no helpers support (empty path). Note that jade does not support loading helpers this way.
Looks as though handlebars-helpers has a different signature than what is required by hapi

Resources