5htp 0.3.1-1 → 0.3.2-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "5htp",
3
3
  "description": "Convenient TypeScript framework designed for Performance and Productivity.",
4
- "version": "0.3.1-1",
4
+ "version": "0.3.2-1",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp.git",
7
7
  "license": "MIT",
@@ -18,7 +18,6 @@
18
18
  "dependencies": {
19
19
  "@babel/cli": "^7.15.4",
20
20
  "@babel/plugin-proposal-class-properties": "^7.14.5",
21
- "@babel/plugin-proposal-decorators": "^7.15.4",
22
21
  "@babel/plugin-proposal-private-methods": "^7.14.5",
23
22
  "@babel/plugin-proposal-private-property-in-object": "^7.15.4",
24
23
  "@babel/plugin-transform-react-constant-elements": "^7.14.5",
@@ -22,9 +22,14 @@ import { app, App } from '../app';
22
22
  export const run = () => new Promise<void>(async () => {
23
23
 
24
24
  const compiler = new Compiler('dev', {
25
- before: () => {
25
+ before: (compiler) => {
26
26
 
27
- stopApp();
27
+ const changedFilesList = compiler.modifiedFiles ? [...compiler.modifiedFiles] : [];
28
+
29
+ if (changedFilesList.length === 0)
30
+ stopApp("Starting a new compilation");
31
+ else
32
+ stopApp("Need to recompile because files changed:\n" + changedFilesList.join('\n'));
28
33
 
29
34
  },
30
35
  after: () => {
@@ -43,7 +48,10 @@ export const run = () => new Promise<void>(async () => {
43
48
  poll: 1000,
44
49
 
45
50
  // Decrease CPU or memory usage in some file systems
46
- ignored: /node_modules\/(?!5htp\-core\/src\/)/,
51
+ // Ignore updated from:
52
+ // - Node modules except 5HTP core (framework dev mode)
53
+ // - Generated files during runtime (cause infinite loop. Ex: models.d.ts)
54
+ ignored: /(node_modules\/(?!5htp\-core\/src\/))|(\.generated\/)/
47
55
 
48
56
  //aggregateTimeout: 1000,
49
57
  }, async (error, stats) => {
@@ -69,7 +77,7 @@ export const run = () => new Promise<void>(async () => {
69
77
  });
70
78
 
71
79
  Keyboard.input('ctrl+c', () => {
72
- stopApp();
80
+ stopApp("CTRL+C Pressed");
73
81
  });
74
82
  });
75
83
 
@@ -79,9 +87,9 @@ export const run = () => new Promise<void>(async () => {
79
87
  ----------------------------------*/
80
88
  let cp: ChildProcess | undefined = undefined;
81
89
 
82
- async function startApp(app: App) {
90
+ async function startApp( app: App ) {
83
91
 
84
- stopApp();
92
+ stopApp('Restart asked');
85
93
 
86
94
  console.info(`Launching new server ...`);
87
95
  cp = spawn('node', ['' + app.paths.bin + '/server.js', '--preserve-symlinks'], {
@@ -92,9 +100,9 @@ async function startApp(app: App) {
92
100
  });
93
101
  }
94
102
 
95
- function stopApp() {
103
+ function stopApp( reason: string ) {
96
104
  if (cp !== undefined) {
97
- console.info(`Killing current server instance (ID: ${cp.pid}) ...`);
105
+ console.info(`Killing current server instance (ID: ${cp.pid}) for the following reason:`, reason);
98
106
  cp.kill();
99
107
  }
100
108
 
@@ -6,12 +6,13 @@
6
6
  import path from 'path';
7
7
  import type webpack from 'webpack';
8
8
  import * as types from '@babel/types'
9
+ import PresetReact from '@babel/preset-react';
9
10
 
10
11
  // Core
11
12
  import PluginIndexage from '../plugins/indexage';
12
13
 
13
14
  import cli from '@cli';
14
- import type { TAppSide, default as App } from '@cli/app';
15
+ import type { TAppSide, App } from '@cli/app';
15
16
 
16
17
  /*----------------------------------
17
18
  - REGLES
@@ -85,7 +86,7 @@ module.exports = (app: App, side: TAppSide, dev: boolean): webpack.RuleSetRule[]
85
86
  // NOTE: On résoud les plugins et presets directement ici
86
87
  // Autrement, babel-loader les cherchera dans projet/node_modules
87
88
 
88
- [require("@babel/plugin-proposal-decorators"), { "legacy": true }],
89
+ //[require("@babel/plugin-proposal-decorators"), { "legacy": true }],
89
90
 
90
91
  [require('@babel/plugin-proposal-class-properties'), { "loose": true }],
91
92
 
@@ -122,8 +123,6 @@ module.exports = (app: App, side: TAppSide, dev: boolean): webpack.RuleSetRule[]
122
123
  }]
123
124
  ]),
124
125
 
125
- //require("./plugins/pages")({ side }),
126
-
127
126
  require('./routes/routes')({ side, app, debug: false }),
128
127
 
129
128
  ...(side === 'client' ? [] : [
@@ -20,6 +20,20 @@ type TOptions = {
20
20
  app: App,
21
21
  debug?: boolean
22
22
  }
23
+ type TRouteDefinition = {
24
+ definition: types.CallExpression,
25
+ dataFetchers: types.ObjectProperty[],
26
+ contextName?: string
27
+ }
28
+
29
+ type TFileInfos = {
30
+ path: string,
31
+ process: boolean,
32
+ side: 'front'|'back',
33
+
34
+ importedServices: {[local: string]: string},
35
+ routeDefinitions: TRouteDefinition[],
36
+ }
23
37
 
24
38
  module.exports = (options: TOptions) => (
25
39
  [Plugin, options]
@@ -35,69 +49,29 @@ function Plugin(babel, { app, side, debug }: TOptions) {
35
49
  /*
36
50
  - Wrap route.get(...) with (app: Application) => { }
37
51
  - Inject chunk ID into client route options
52
+ - Transform api.fetch calls
38
53
  */
39
54
 
40
55
  const plugin: PluginObj<{
41
-
42
56
  filename: string,
43
- part: 'routes',
44
- side: 'front' | 'back',
45
- processFile: boolean,
46
-
47
- // Identifier => Name
48
- importedServices: {[local: string]: string},
49
- routeDefinitions: types.Expression[]
57
+ file: TFileInfos
50
58
  }> = {
51
59
  pre(state) {
52
-
53
60
  this.filename = state.opts.filename as string;
54
- this.processFile = true;
55
-
56
- // Relative path
57
- let relativeFileName: string | undefined;
58
- if (this.filename.startsWith( cli.paths.appRoot ))
59
- relativeFileName = this.filename.substring( cli.paths.appRoot.length );
60
- if (this.filename.startsWith( cli.paths.coreRoot ))
61
- relativeFileName = this.filename.substring( cli.paths.coreRoot.length );
62
- if (this.filename.startsWith('/node_modules/5htp-core/'))
63
- relativeFileName = this.filename.substring( '/node_modules/5htp-core/'.length );
64
-
65
- // The file isn't a route definition
66
- if (relativeFileName === undefined) {
67
- this.processFile = false;
68
- return false;
69
- }
70
-
71
- // Differenciate back / front
72
- if (relativeFileName.startsWith('/src/client/pages')) {
73
-
74
- this.side = 'front';
75
- this.part = 'routes';
76
-
77
- } else if (relativeFileName.startsWith('/src/server/routes')) {
78
61
 
79
- this.side = 'back';
80
- this.part = 'routes';
81
-
82
- } else
83
- this.processFile = false;
84
-
85
- // Init output
86
- this.importedServices = {}
87
- this.routeDefinitions = []
62
+ this.file = getFileInfos(this.filename);
88
63
  },
89
64
  visitor: {
90
-
91
65
  // Find @app imports
92
66
  // Test: import { Router } from '@app';
93
67
  // Replace by: nothing
94
68
  ImportDeclaration(path) {
95
-
96
- if (!this.processFile)
69
+
70
+ if (!this.file.process)
97
71
  return;
98
72
 
99
73
  if (path.node.source.value !== '@app')
100
- return;
74
+ return;
101
75
 
102
76
  for (const specifier of path.node.specifiers) {
103
77
 
@@ -107,11 +81,11 @@ function Plugin(babel, { app, side, debug }: TOptions) {
107
81
  if (specifier.imported.type !== 'Identifier')
108
82
  continue;
109
83
 
110
- this.importedServices[ specifier.local.name ] = specifier.imported.name;
84
+ this.file.importedServices[ specifier.local.name ] = specifier.imported.name;
111
85
  }
112
86
 
113
87
  // Remove this import
114
- path.replaceWithMultiple([]);
88
+ path.remove();
115
89
 
116
90
  },
117
91
 
@@ -120,18 +94,10 @@ function Plugin(babel, { app, side, debug }: TOptions) {
120
94
  // Replace by: nothing
121
95
  CallExpression(path) {
122
96
 
123
- if (!this.processFile)
124
- return;
125
-
126
- // Should be at the root of the document
127
- if (!(
128
- path.parent.type === 'ExpressionStatement'
129
- &&
130
- path.parentPath.parent.type === 'Program'
131
- ))
97
+ if (!this.file.process)
132
98
  return;
133
99
 
134
- // service.method()
100
+ // object.property()
135
101
  const callee = path.node.callee
136
102
  if (!(
137
103
  callee.type === 'MemberExpression'
@@ -140,117 +106,143 @@ function Plugin(babel, { app, side, debug }: TOptions) {
140
106
  &&
141
107
  callee.property.type === 'Identifier'
142
108
  &&
143
- (callee.object.name in this.importedServices)
144
- ))
109
+ // Should be at the root of the document
110
+ path.parent.type === 'ExpressionStatement'
111
+ &&
112
+ path.parentPath.parent.type === 'Program'
113
+ // And make reference to a service
114
+ &&
115
+ (callee.object.name in this.file.importedServices)
116
+ ))
145
117
  return;
146
118
 
147
- // Client route definition: Add chunk id
148
- let [routePath, ...routeArgs] = path.node.arguments;
149
- if (this.side === 'front' && callee.object.name === 'Router') {
150
-
151
- // Inject chunk id in options (2nd arg)
152
- const newRouteArgs = injectChunkId(routeArgs, this.filename);
153
- if (newRouteArgs === 'ALREADY_PROCESSED')
154
- return;
155
-
156
- routeArgs = newRouteArgs;
119
+ const routeDef: TRouteDefinition = {
120
+ definition: path.node,
121
+ dataFetchers: []
157
122
  }
158
123
 
159
- // Force babel to create new fresh nodes
160
- // If we directy use statementParent, it will not be included in the final compiler code
161
- const statementParent =
162
- t.callExpression(
163
- t.memberExpression(
164
- t.identifier( callee.object.name ),
165
- callee.property,
166
- ),
167
- [routePath, ...routeArgs]
168
- )
124
+ // Adjust
125
+ if (this.file.side === 'front') {
126
+ transformDataFetchers(path, this, routeDef);
127
+ }
169
128
 
170
- this.routeDefinitions.push( statementParent );
129
+ // Add to the list of route definitons to wrap
130
+ this.file.routeDefinitions.push(routeDef);
171
131
 
172
- // Delete this node
132
+ // Delete the route def since it will be replaced by a wrapper
173
133
  path.replaceWithMultiple([]);
134
+
174
135
  },
136
+ Program: {
137
+ exit(path, parent) {
175
138
 
176
- // Wrap declarations into a exported const app function
177
- /*
178
- export const __register = ({ Router }} => {
139
+ if (!this.file.process)
140
+ return;
141
+
142
+ const wrappedrouteDefs = wrapRouteDefs( this.file );
143
+ if (wrappedrouteDefs)
144
+ path.pushContainer('body', [wrappedrouteDefs])
145
+
146
+ }
147
+ }
148
+ }
149
+ }
179
150
 
180
- Router.page(..)
151
+ function getFileInfos( filename: string ): TFileInfos {
181
152
 
182
- }
183
- */
184
- Program: {
185
- exit: function(path, parent) {
153
+ const file: TFileInfos = {
154
+ process: true,
155
+ side: 'back',
156
+ path: filename,
157
+ importedServices: {},
158
+ routeDefinitions: []
159
+ }
186
160
 
187
- if (!this.processFile)
188
- return;
161
+ // Relative path
162
+ let relativeFileName: string | undefined;
163
+ if (filename.startsWith( cli.paths.appRoot ))
164
+ relativeFileName = filename.substring( cli.paths.appRoot.length );
165
+ if (filename.startsWith( cli.paths.coreRoot ))
166
+ relativeFileName = filename.substring( cli.paths.coreRoot.length );
167
+ if (filename.startsWith('/node_modules/5htp-core/'))
168
+ relativeFileName = filename.substring( '/node_modules/5htp-core/'.length );
169
+
170
+ // The file isn't a route definition
171
+ if (relativeFileName === undefined) {
172
+ file.process = false;
173
+ return file;
174
+ }
175
+
176
+ // Differenciate back / front
177
+ if (relativeFileName.startsWith('/src/client/pages')) {
178
+ file.side = 'front';
179
+ } else if (relativeFileName.startsWith('/src/server/routes')) {
180
+ file.side = 'back';
181
+ } else
182
+ file.process = false;
183
+
184
+ return file
185
+ }
189
186
 
190
- const importedServices = Object.entries(this.importedServices);
191
- if (importedServices.length === 0)
192
- return;
187
+ function transformDataFetchers(
188
+ path: NodePath<types.CallExpression>,
189
+ routerDefContext: PluginObj,
190
+ routeDef: TRouteDefinition
191
+ ) {
192
+ path.traverse({
193
+ CallExpression(path) {
193
194
 
194
- let exportValue: types.Expression | types.BlockStatement;
195
- if (this.side === 'front') {
196
-
197
- const routesDefCount = this.routeDefinitions.length;
198
- if (routesDefCount !== 1)
199
- throw new Error(`Frontend route definition files (/client/pages/**/**.ts) can contain only one route definition.
200
- ${routesDefCount} were given in ${this.filename}.`);
201
-
202
- exportValue = this.routeDefinitions[0];
203
-
204
- } else {
205
-
206
- exportValue = t.blockStatement([
207
- // Without spread = react jxx need additionnal loader
208
- ...this.routeDefinitions.map( def =>
209
- t.expressionStatement(def)
210
- ),
211
- ])
212
- }
213
-
214
- const exportDeclaration = t.exportNamedDeclaration(
215
- t.variableDeclaration('const', [
216
- t.variableDeclarator(
217
- t.identifier('__register'),
218
- t.arrowFunctionExpression(
219
- [
220
- t.objectPattern(
221
- importedServices.map(([ local, imported ]) =>
222
- t.objectProperty(
223
- t.identifier( local ),
224
- t.identifier( imported ),
225
- )
226
- )
227
- )
228
- ],
229
- exportValue
230
- )
231
- )
232
- ])
233
- )
195
+ const callee = path.node.callee
234
196
 
235
- // Sans
236
- // console.log('import app via', this.filename, this.importedServices);
237
- //debug && console.log( generate(exportDeclaration).code )
238
- path.pushContainer('body', [exportDeclaration])
239
- }
197
+ // api.load => move data fetchers to route.options.data
198
+ // So the router is able to load data before rendering the component
199
+ if (!(
200
+ callee.type === 'MemberExpression'
201
+ &&
202
+ callee.object.type === 'Identifier'
203
+ &&
204
+ callee.property.type === 'Identifier'
205
+ &&
206
+ callee.object.name === 'api'
207
+ &&
208
+ callee.property.name === 'fetch'
209
+ ))
210
+ return;
211
+
212
+ routeDef.dataFetchers.push(
213
+ ...path.node.arguments[0].properties
214
+ );
215
+
216
+ // Delete routerDefContext node
217
+ path.replaceWith(
218
+ t.memberExpression(
219
+ t.identifier( routeDef.contextName || 'context' ),
220
+ t.identifier('data'),
221
+ )
222
+ );
240
223
  }
241
- }
224
+ }, routerDefContext);
242
225
  }
243
226
 
244
- function injectChunkId(
227
+ function injectOptions(
228
+ routeDef: TRouteDefinition,
245
229
  routeArgs: types.CallExpression["arguments"],
246
230
  filename: string
247
231
  ): types.CallExpression["arguments"] | 'ALREADY_PROCESSED' {
248
232
 
249
- let [routeOptions, ...otherArgs] = routeArgs;
233
+ // Extract client route definition arguments
234
+ let routeOptions: types.ObjectExpression | undefined;
235
+ let renderer: types.ArrowFunctionExpression;
236
+ if (routeArgs.length === 1)
237
+ ([ renderer ] = routeArgs);
238
+ else
239
+ ([ routeOptions, renderer ] = routeArgs);
250
240
 
241
+ // Generate page chunk id
251
242
  const { filepath, chunkId } = cli.paths.getPageChunk(app, filename);
252
243
  debug && console.log(`[routes]`, filename, '=>', chunkId);
253
244
 
245
+ // Create new options to add in route.options
254
246
  const newProperties = [
255
247
  t.objectProperty(
256
248
  t.identifier('id'),
@@ -259,17 +251,39 @@ function Plugin(babel, { app, side, debug }: TOptions) {
259
251
  t.objectProperty(
260
252
  t.identifier('filepath'),
261
253
  t.stringLiteral(filepath)
262
- )
254
+ ),
263
255
  ]
264
256
 
265
- // No options object
266
- if (routeOptions.type !== 'ObjectExpression') {
257
+ // Add data fetchers
258
+ if (routeDef.dataFetchers.length !== 0) {
259
+
260
+ // (contollerParams) => fetchers
261
+ const dataFetchersFunc = t.arrowFunctionExpression(
262
+ renderer.params.map( param => t.cloneNode( param )),
263
+ t.objectExpression(
264
+ routeDef.dataFetchers.map( df => t.cloneNode( df ))
265
+ )
266
+ )
267
+
268
+ // Add the data fetchers to options.data
269
+ newProperties.push(
270
+ t.objectProperty(
271
+ t.identifier('data'),
272
+ dataFetchersFunc
273
+ )
274
+ );
275
+
276
+ // Expose the context variable in the renderer
277
+ exposeContextProperty( renderer, routeDef );
278
+ }
279
+
280
+ if (routeOptions?.properties === undefined)
267
281
  return [
268
282
  t.objectExpression(newProperties),
269
- ...routeArgs
283
+ renderer
270
284
  ]
271
- }
272
285
 
286
+ // Test if the route options were not already processed
273
287
  const wasAlreadyProcessed = routeOptions.properties.some( o =>
274
288
  o.type === 'ObjectProperty'
275
289
  &&
@@ -284,14 +298,128 @@ function Plugin(babel, { app, side, debug }: TOptions) {
284
298
  return 'ALREADY_PROCESSED';
285
299
  }
286
300
 
301
+ // Create the new options object
287
302
  return [
288
303
  t.objectExpression([
289
304
  ...routeOptions.properties,
290
305
  ...newProperties
291
306
  ]),
292
- ...otherArgs
307
+ renderer
293
308
  ]
294
309
  }
295
310
 
311
+ function exposeContextProperty(
312
+ renderer: types.ArrowFunctionExpression,
313
+ routeDef: TRouteDefinition
314
+ ) {
315
+ const contextParam = renderer.params[0];
316
+ if (contextParam?.type === 'ObjectPattern') {
317
+
318
+ for (const property of contextParam.properties) {
319
+ if (
320
+ property.type === 'ObjectProperty'
321
+ &&
322
+ property.key.type === 'Identifier'
323
+ &&
324
+ property.key.name === 'context'
325
+ &&
326
+ property.value.type === 'Identifier'
327
+ ) {
328
+
329
+ routeDef.contextName = property.value.name;
330
+ break;
331
+ }
332
+ }
333
+
334
+ if (!routeDef.contextName) {
335
+ routeDef.contextName = 'context';
336
+ contextParam.properties.push(
337
+ t.objectProperty( t.identifier('context'), t.identifier( routeDef.contextName ) )
338
+ );
339
+ }
340
+
341
+ } else if (contextParam?.type === 'Identifier') {
342
+ console.log("routeDef.contextName", routeDef.contextName);
343
+ routeDef.contextName = contextParam.name;
344
+ }
345
+ }
346
+
347
+ function wrapRouteDefs( file: TFileInfos ) {
348
+
349
+ const importedServicesList = Object.entries(file.importedServices);
350
+ if (importedServicesList.length === 0)
351
+ return;
352
+
353
+ let exportValue: types.Expression | types.BlockStatement;
354
+ if (file.side === 'front') {
355
+
356
+ // Limit to one route def per file
357
+ const routesDefCount = file.routeDefinitions.length;
358
+ if (routesDefCount !== 1)
359
+ throw new Error(`Frontend route definition files (/client/pages/**/**.ts) can contain only one route definition.
360
+ ${routesDefCount} were given in ${file.path}.`);
361
+
362
+ const routeDef = file.routeDefinitions[0];
363
+
364
+ // Client route definition: Add chunk id
365
+ let [routePath, ...routeArgs] = routeDef.definition.arguments;
366
+ const callee = routeDef.definition.callee;
367
+
368
+ if (callee.object.name === 'Router') {
369
+
370
+ // Inject chunk id in options (2nd arg)
371
+ const newRouteArgs = injectOptions(routeDef, routeArgs, file.path);
372
+ if (newRouteArgs === 'ALREADY_PROCESSED')
373
+ return;
374
+
375
+ routeArgs = newRouteArgs;
376
+ }
377
+
378
+ // Force babel to create new fresh nodes
379
+ // If we directy use statementParent, it will not be included in the final compiler code
380
+ exportValue = t.callExpression(
381
+ t.memberExpression(
382
+ t.identifier( callee.object.name ),
383
+ callee.property,
384
+ ),
385
+ [routePath, ...routeArgs]
386
+ )
387
+
388
+ } else {
389
+
390
+ exportValue = t.blockStatement([
391
+ // Without spread = react jxx need additionnal loader
392
+ ...file.routeDefinitions.map( def =>
393
+ t.expressionStatement(def.definition)
394
+ ),
395
+ ])
396
+ }
397
+
398
+ const exportDeclaration = t.exportNamedDeclaration(
399
+ t.variableDeclaration('const', [
400
+ t.variableDeclarator(
401
+ t.identifier('__register'),
402
+ t.arrowFunctionExpression(
403
+ [
404
+ t.objectPattern(
405
+ importedServicesList.map(([ local, imported ]) =>
406
+ t.objectProperty(
407
+ t.identifier( local ),
408
+ t.identifier( imported ),
409
+ )
410
+ )
411
+ )
412
+ ],
413
+ exportValue
414
+ )
415
+ )
416
+ ])
417
+ )
418
+
419
+ //file.side === 'front' && console.log( generate(exportDeclaration).code );
420
+
421
+ return exportDeclaration;
422
+ }
423
+
296
424
  return plugin;
297
425
  }
@@ -17,7 +17,7 @@ import createServerConfig from './server';
17
17
  import createClientConfig from './client';
18
18
  import { TCompileMode } from './common';
19
19
 
20
- type TCompilerCallback = () => void
20
+ type TCompilerCallback = (compiler: webpack.Compiler) => void
21
21
 
22
22
  /*----------------------------------
23
23
  - FONCTION
@@ -92,14 +92,36 @@ export default class Compiler {
92
92
  }
93
93
 
94
94
  private findServices( dir: string ) {
95
+
96
+ const blacklist = ['node_modules', '5htp-core']
95
97
  const files: string[] = [];
96
98
  const dirents = fs.readdirSync(dir, { withFileTypes: true });
97
- for (const dirent of dirents) {
98
- const res = path.resolve(dir, dirent.name);
99
- if (dirent.isDirectory()) {
100
- files.push( ...this.findServices(res) );
99
+
100
+ for (let dirent of dirents) {
101
+
102
+ let fileName = dirent.name;
103
+ let filePath = path.resolve(dir, fileName);
104
+
105
+ if (blacklist.includes( fileName ))
106
+ continue;
107
+
108
+ // Define is we should recursively find service in the current item
109
+ let iterate: boolean = false;
110
+ if (dirent.isSymbolicLink()) {
111
+
112
+ const realPath = path.resolve( dir, fs.readlinkSync(filePath) );
113
+ const destinationInfos = fs.lstatSync( realPath );
114
+ if (destinationInfos.isDirectory())
115
+ iterate = true;
116
+
117
+ } else if (dirent.isDirectory())
118
+ iterate = true;
119
+
120
+ // Update the list of found services
121
+ if (iterate) {
122
+ files.push( ...this.findServices(filePath) );
101
123
  } else if (dirent.name === 'service.json') {
102
- files.push( path.dirname(res) );
124
+ files.push( path.dirname(filePath) );
103
125
  }
104
126
  }
105
127
  return files;
@@ -113,9 +135,9 @@ export default class Compiler {
113
135
 
114
136
  // Index services
115
137
  const searchDirs = {
116
- '@server/services': path.join(cli.paths.core.src, 'server', 'services'),
117
- '@/server/services': path.join(app.paths.src, 'server', 'services'),
118
- // TODO: node_modules
138
+ '@server/services/': path.join(cli.paths.core.src, 'server', 'services'),
139
+ '@/server/services/': path.join(app.paths.src, 'server', 'services'),
140
+ '': path.join(app.paths.root, 'node_modules'),
119
141
  }
120
142
 
121
143
  for (const importationPrefix in searchDirs) {
@@ -128,7 +150,8 @@ export default class Compiler {
128
150
  const metasFile = path.join( serviceDir, 'service.json');
129
151
  const { id, name, parent, dependences } = require(metasFile);
130
152
 
131
- const importationPath = importationPrefix + serviceDir.substring( searchDir.length );
153
+ // The +1 is to remove the slash
154
+ const importationPath = importationPrefix + serviceDir.substring( searchDir.length + 1 );
132
155
 
133
156
  // Generate index & typings
134
157
  imported.push(`import type ${name} from "${importationPath}";`);
@@ -154,9 +177,9 @@ dependences: ${JSON.stringify(dependences)},
154
177
  path.join( app.paths.client.generated, 'services.d.ts'),
155
178
  `declare module "@app" {
156
179
 
157
- import { CrossPathClient } from '@/client/index';
180
+ import { ${appClassIdentifier} } from '@/client/index';
158
181
 
159
- const appClass: CrossPathClient;
182
+ const appClass: ${appClassIdentifier};
160
183
 
161
184
  export = appClass
162
185
  }`
@@ -200,8 +223,12 @@ declare module '@server/app' {
200
223
  import { Application } from "@server/app/index";
201
224
  import { ServicesContainer } from "@server/app/service/container";
202
225
 
226
+ abstract class ApplicationWithServices extends Application<
227
+ ServicesContainer<InstalledServices>
228
+ > {}
229
+
203
230
  export interface Exported {
204
- Application: typeof Application,
231
+ Application: typeof ApplicationWithServices,
205
232
  Services: ServicesContainer<InstalledServices>,
206
233
  }
207
234
 
@@ -237,9 +264,9 @@ declare module '@server/app' {
237
264
  let finished: (() => void);
238
265
  this.compiling[name] = new Promise((resolve) => finished = resolve);
239
266
 
240
- compiler.hooks.compile.tap(name, () => {
241
-
242
- this.callbacks.before && this.callbacks.before();
267
+ compiler.hooks.compile.tap(name, (compilation) => {
268
+
269
+ this.callbacks.before && this.callbacks.before( compiler );
243
270
 
244
271
  this.compiling[name] = new Promise((resolve) => finished = resolve);
245
272