5htp 0.5.9 → 0.6.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/app/index.ts CHANGED
@@ -20,6 +20,23 @@ import type { TEnvConfig } from '../../core/server/app/container/config';
20
20
 
21
21
  export type TAppSide = 'server' | 'client'
22
22
 
23
+ type TServiceSetup = {
24
+ id: string,
25
+ name: string,
26
+ config: {},
27
+ subservices: TServiceSubservices,
28
+ type: 'service.setup'
29
+ }
30
+
31
+ type TServiceRef = {
32
+ refTo: string,
33
+ type: 'service.ref'
34
+ }
35
+
36
+ type TServiceSubservices = {
37
+ [key: string]: TServiceSetup | TServiceRef
38
+ }
39
+
23
40
  /*----------------------------------
24
41
  - SERVICE
25
42
  ----------------------------------*/
@@ -110,7 +127,7 @@ export class App {
110
127
 
111
128
  public registered = {}
112
129
 
113
- public use( referenceName: string ) {
130
+ public use( referenceName: string ): TServiceRef {
114
131
 
115
132
  // We don't check because all service are not regstered when we register subservices
116
133
  /*if (this.registered[referenceName] === undefined) {
@@ -119,37 +136,38 @@ export class App {
119
136
 
120
137
  return {
121
138
  refTo: referenceName,
139
+ type: 'service.ref'
122
140
  }
123
141
  }
124
142
 
125
143
  public setup(...args: [
126
144
  // { user: app.setup('Core/User') }
127
- serviceId: string,
128
- serviceConfig?: TServiceConfig,
129
- serviceSubservices?: TServiceSubservices
145
+ servicePath: string,
146
+ serviceConfig?: {},
130
147
  ] | [
131
148
  // app.setup('User', 'Core/User')
132
149
  serviceName: string,
133
- serviceId: string,
134
- serviceConfig?: TServiceConfig,
135
- serviceSubservices?: TServiceSubservices
136
- ]) {
150
+ servicePath: string,
151
+ serviceConfig?: {},
152
+ ]): TServiceSetup {
137
153
 
138
154
  // Registration to app root
139
155
  if (typeof args[1] === 'string') {
140
156
 
141
- const [name, id, config, subservices] = args;
157
+ const [name, id, config] = args;
142
158
 
143
- const service = { id, name, config, subservices }
159
+ const service = { id, name, config, type: 'service.setup' } as TServiceSetup
144
160
 
145
161
  this.registered[name] = service;
146
162
 
163
+ return service;
164
+
147
165
  // Scoped to a parent service
148
166
  } else {
149
167
 
150
- const [id, config, subservices] = args;
168
+ const [id, config] = args;
151
169
 
152
- const service = { id, config, subservices }
170
+ const service = { id, config, type: 'service.setup' } as TServiceSetup
153
171
 
154
172
  return service;
155
173
  }
@@ -157,9 +175,6 @@ export class App {
157
175
 
158
176
  public async warmup() {
159
177
 
160
- console.log("env", this.env);
161
-
162
-
163
178
  // Require all config files in @/server/config
164
179
  const configDir = path.resolve(cli.paths.appRoot, 'server', 'config');
165
180
  const configFiles = fs.readdirSync(configDir);
@@ -167,27 +182,6 @@ export class App {
167
182
  console.log("Loading config file:", configFile);
168
183
  require( path.resolve(configDir, configFile) );
169
184
  }
170
-
171
- // Wait 2 seconds
172
- await new Promise(resolve => setTimeout(resolve, 1000));
173
-
174
- // Load subservices
175
- // for (const serviceName in this.registered) {
176
- // const service = this.registered[serviceName];
177
-
178
- // const subservices = {}
179
- // if (service.subservices) {
180
- // const list = service.subservices( this.registered );
181
- // for (const subservice of list) {
182
-
183
- // subservices[subservice.name] = list[];
184
-
185
- // }
186
- // }
187
- // service.subservices = subservices;
188
- // }
189
-
190
- // console.log("SERVICES", this.registered);
191
185
  }
192
186
  }
193
187
 
@@ -312,22 +312,16 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
312
312
  new CssMinimizerPlugin()
313
313
  ]),
314
314
 
315
- // BUG: Essai de charger les plugins depuis app/node_modules
316
- // Et la specification via require() ne sembl epas être supportée ...
317
- // https://webpack.js.org/plugins/image-minimizer-webpack-plugin/
318
315
  /*new ImageMinimizerPlugin({
316
+ // (optional) add generators; you can also just import &format=webp (see below)
319
317
  generator: [
320
318
  {
321
- // You can apply generator using `?as=webp`, you can use any name and provide more options
322
319
  preset: "webp",
323
320
  implementation: ImageMinimizerPlugin.imageminGenerate,
324
- options: {
325
- // Please specify only one plugin here, multiple plugins will not work
326
- plugins: ["imagemin-webp"],
327
- },
328
- },
329
- ],
330
- }),*/
321
+ options: { plugins: ["imagemin-webp"] }
322
+ }
323
+ ]
324
+ })*/
331
325
  ],
332
326
  nodeEnv: 'production',
333
327
  sideEffects: true,
@@ -23,6 +23,8 @@ module.exports = {
23
23
  ]
24
24
  }
25
25
 
26
+ const routerMethods = ['get', 'post', 'put', 'delete', 'patch'];
27
+
26
28
  /*----------------------------------
27
29
  - PLUGIN
28
30
  ----------------------------------*/
@@ -133,7 +135,7 @@ function Plugin (babel) {
133
135
  &&
134
136
  i.node.callee.property.type === 'Identifier'
135
137
  &&
136
- ['get', 'post', 'put', 'delete'].includes(i.node.callee.property.name)
138
+ routerMethods.includes(i.node.callee.property.name)
137
139
  &&
138
140
  i.node.arguments.length >= 2 // url + au moins 1 middleware
139
141
  &&
@@ -4,7 +4,8 @@
4
4
 
5
5
  // Npm
6
6
  import * as types from '@babel/types'
7
- import type { PluginObj } from '@babel/core';
7
+ import type { NodePath, PluginObj } from '@babel/core';
8
+ import generate from '@babel/generator';
8
9
 
9
10
  // Core
10
11
  import cli from '@cli';
@@ -24,12 +25,19 @@ type TOptions = {
24
25
  * Extended source type: now includes "models"
25
26
  * so we can differentiate how we rewrite references.
26
27
  */
27
- type TImportSource = 'container' | 'application' | 'models';
28
+ type TImportSource = 'container' | 'services' | 'models' | 'request';
28
29
 
29
30
  module.exports = (options: TOptions) => (
30
31
  [Plugin, options]
31
32
  )
32
33
 
34
+ type TImportedIndex = {
35
+ local: string,
36
+ imported: string, // The original “imported” name
37
+ references: NodePath<types.Node>[], // reference paths
38
+ source: TImportSource // container | application | models
39
+ }
40
+
33
41
  /*----------------------------------
34
42
  - PLUGIN
35
43
  ----------------------------------*/
@@ -54,8 +62,6 @@ function Plugin(babel, { app, side, debug }: TOptions) {
54
62
  this.app.Models.client.MyModel.someCall();
55
63
 
56
64
  Processed files:
57
- @/server/config
58
- @/server/routes
59
65
  @/server/services
60
66
  */
61
67
 
@@ -68,42 +74,65 @@ function Plugin(babel, { app, side, debug }: TOptions) {
68
74
 
69
75
  // Count how many total imports we transform
70
76
  importedCount: number,
77
+ routeMethods: string[],
71
78
 
72
79
  // For every local identifier, store info about how it should be rewritten
73
- importedReferences: {
74
- [localName: string]: {
75
- imported: string, // The original “imported” name
76
- bindings: any, // reference paths
77
- source: TImportSource // container | application | models
78
- }
80
+ imported: {
81
+ [localName: string]: TImportedIndex
79
82
  }
80
-
81
- // Tally how many references per kind
82
- bySource: { [s in TImportSource]: number }
83
83
  }> = {
84
84
 
85
85
  pre(state) {
86
86
  this.filename = state.opts.filename as string;
87
- this.processFile = (
88
- this.filename.startsWith(cli.paths.appRoot + '/server/config')
89
- ||
90
- this.filename.startsWith(cli.paths.appRoot + '/server/routes')
91
- ||
92
- this.filename.startsWith(cli.paths.appRoot + '/server/services')
93
- );
94
-
95
- this.importedReferences = {};
96
- this.bySource = {
97
- container: 0,
98
- application: 0,
99
- models: 0
100
- };
87
+ this.processFile = this.filename.startsWith(cli.paths.appRoot + '/server/services');
88
+
89
+ this.imported = {};
90
+
101
91
  this.importedCount = 0;
102
92
  this.debug = debug || false;
93
+
94
+ this.routeMethods = [];
103
95
  },
104
96
 
105
97
  visitor: {
106
98
 
99
+ // Detect decored methods before other plugins remove decorators
100
+ Program(path) {
101
+
102
+ if (!this.processFile) return;
103
+
104
+ // Traverse the AST within the Program node
105
+ path.traverse({
106
+ ClassMethod: (subPath) => {
107
+ const { node } = subPath;
108
+ if (!node.decorators || node.key.type !== 'Identifier') return;
109
+
110
+ for (const decorator of node.decorators) {
111
+
112
+ const isRoute = (
113
+ // Handles the case of @Route without parameters
114
+ (
115
+ t.isIdentifier(decorator.expression) && decorator.expression.name === 'Route'
116
+ )
117
+ ||
118
+ // Handles the case of @Route() with parameters
119
+ (
120
+ t.isCallExpression(decorator.expression) &&
121
+ t.isIdentifier(decorator.expression.callee) &&
122
+ decorator.expression.callee.name === 'Route'
123
+ )
124
+ );
125
+
126
+ if (!isRoute) continue;
127
+
128
+ const methodName = node.key.name;
129
+ this.routeMethods.push( methodName );
130
+
131
+ }
132
+ }
133
+ });
134
+ },
135
+
107
136
  /**
108
137
  * Detect import statements from '@app' or '@models'
109
138
  */
@@ -111,7 +140,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
111
140
  if (!this.processFile) return;
112
141
 
113
142
  const source = path.node.source.value;
114
- if (source !== '@app' && source !== '@models') {
143
+ if (source !== '@app' && source !== '@models' && source !== '@request') {
115
144
  return;
116
145
  }
117
146
 
@@ -123,25 +152,32 @@ function Plugin(babel, { app, side, debug }: TOptions) {
123
152
  this.importedCount++;
124
153
 
125
154
  let importSource: TImportSource;
126
- if (source === '@app') {
127
- // Distinguish whether it's a container service or an application service
128
- if (app.containerServices.includes(specifier.imported.name)) {
129
- importSource = 'container';
130
- } else {
131
- importSource = 'application';
132
- }
133
- } else {
134
- // source === '@models'
135
- importSource = 'models';
155
+ switch (source) {
156
+ case '@app':
157
+ // Distinguish whether it's a container service or an application service
158
+ if (app.containerServices.includes(specifier.imported.name)) {
159
+ importSource = 'container';
160
+ } else {
161
+ importSource = 'services';
162
+ }
163
+ break;
164
+ case '@request':
165
+ importSource = 'request';
166
+ break;
167
+ case '@models':
168
+ // source === '@models'
169
+ importSource = 'models';
170
+ break;
171
+ default:
172
+ throw new Error(`Unknown import source: ${source}`);
136
173
  }
137
174
 
138
- this.importedReferences[specifier.local.name] = {
175
+ this.imported[specifier.local.name] = {
176
+ local: specifier.local.name,
139
177
  imported: specifier.imported.name,
140
- bindings: path.scope.bindings[specifier.local.name].referencePaths,
178
+ references: path.scope.bindings[specifier.local.name].referencePaths,
141
179
  source: importSource
142
180
  };
143
-
144
- this.bySource[importSource]++;
145
181
  }
146
182
 
147
183
  // Remove the original import line(s) and replace with any needed new import
@@ -151,7 +187,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
151
187
 
152
188
  // If this line had container references, add a default import for container
153
189
  // Example: import container from '<root>/server/app/container'
154
- if (source === '@app' && this.bySource.container > 0) {
190
+ if (source === '@app') {
155
191
  replaceWith.push(
156
192
  t.importDeclaration(
157
193
  [t.importDefaultSpecifier(t.identifier('container'))],
@@ -167,78 +203,124 @@ function Plugin(babel, { app, side, debug }: TOptions) {
167
203
  path.replaceWithMultiple(replaceWith);
168
204
  },
169
205
 
170
- /**
171
- * Rewrite references to the imports
172
- */
173
- Identifier(path) {
174
- if (!this.processFile || this.importedCount === 0) {
175
- return;
176
- }
206
+ // This visitor fires for every class method.
207
+ ClassMethod(path) {
177
208
 
178
- const name = path.node.name;
179
- const ref = this.importedReferences[name];
180
- if (!ref || !ref.bindings) {
181
- return;
182
- }
209
+ // Must be a server service
210
+ if (!this.processFile || path.replaced) return;
183
211
 
184
- // Find a specific binding that hasn't been replaced yet
185
- let foundBinding = undefined;
186
- for (const binding of ref.bindings) {
187
- if (!binding.replaced && path.getPathLocation() === binding.getPathLocation()) {
188
- foundBinding = binding;
189
- break;
212
+ // Must have a method name
213
+ if (path.node.key.type !== 'Identifier') return;
214
+
215
+ // Init context
216
+ const methodName = path.node.key.name;
217
+ let params = path.node.params;
218
+
219
+ // Prefix references
220
+ path.traverse({ Identifier: (subPath) => {
221
+
222
+ const { node } = subPath;
223
+ const name = node.name;
224
+ const ref = this.imported[name];
225
+ if (!ref || !ref.references) {
226
+ return;
190
227
  }
191
- }
192
- if (!foundBinding) {
193
- return;
194
- }
195
228
 
196
- // Mark as replaced to avoid loops
197
- foundBinding.replaced = true;
229
+ // Find a specific binding that hasn't been replaced yet
230
+ const foundBinding = ref.references.find(binding => {
231
+ return subPath.getPathLocation() === binding.getPathLocation();
232
+ });
198
233
 
199
- // Based on the source, replace the identifier with the proper MemberExpression
200
- if (ref.source === 'container') {
201
- // container.[identifier]
202
- // e.g. container.Environment
203
- path.replaceWith(
204
- t.memberExpression(
205
- t.identifier('container'),
206
- path.node
207
- )
208
- );
209
- }
210
- else if (ref.source === 'application') {
211
- // this.app.[identifier]
212
- // e.g. this.app.MyService
213
- path.replaceWith(
214
- t.memberExpression(
234
+ if (!foundBinding || foundBinding.replaced)
235
+ return;
236
+
237
+ // Mark as replaced to avoid loops
238
+ foundBinding.replaced = true;
239
+
240
+ // Based on the source, replace the identifier with the proper MemberExpression
241
+ if (ref.source === 'container') {
242
+ // container.[identifier]
243
+ // e.g. container.Environment
244
+ subPath.replaceWith(
215
245
  t.memberExpression(
216
- t.thisExpression(),
217
- t.identifier('app')
218
- ),
219
- path.node
220
- )
221
- );
222
- }
223
- else if (ref.source === 'models') {
224
- // this.app.Models.client.[identifier]
225
- // e.g. this.app.Models.client.MyModel
226
- path.replaceWith(
227
- t.memberExpression(
246
+ t.identifier('container'),
247
+ subPath.node
248
+ )
249
+ );
250
+ }
251
+ else if (ref.source === 'services') {
252
+ // this.app.[identifier]
253
+ // e.g. this.app.MyService
254
+ subPath.replaceWith(
255
+ t.memberExpression(
256
+ t.memberExpression(
257
+ t.thisExpression(),
258
+ t.identifier('app')
259
+ ),
260
+ subPath.node
261
+ )
262
+ );
263
+ }
264
+ else if (ref.source === 'models') {
265
+ // this.app.Models.client.[identifier]
266
+ // e.g. this.app.Models.client.MyModel
267
+ subPath.replaceWith(
228
268
  t.memberExpression(
229
269
  t.memberExpression(
230
270
  t.memberExpression(
231
- t.thisExpression(),
232
- t.identifier('app')
271
+ t.memberExpression(
272
+ t.thisExpression(),
273
+ t.identifier('app')
274
+ ),
275
+ t.identifier('Models')
233
276
  ),
234
- t.identifier('Models')
277
+ t.identifier('client')
235
278
  ),
236
- t.identifier('client')
237
- ),
238
- path.node
279
+ subPath.node
280
+ )
281
+ );
282
+ }
283
+ else if (ref.source === 'request') {
284
+ // this.app.Models.client.[identifier]
285
+ // e.g. this.app.Models.client.MyModel
286
+ subPath.replaceWith(
287
+ t.memberExpression(
288
+ t.identifier('context'),
289
+ subPath.node
290
+ )
291
+ );
292
+ }
293
+
294
+ } });
295
+
296
+ if (
297
+ this.routeMethods.includes(methodName)
298
+ &&
299
+ path.node.params.length < 2
300
+ ) {
301
+
302
+ // Expose router context variable via the second parameter
303
+ params = [
304
+ path.node.params[0] || t.objectPattern([]),
305
+ t.identifier('context'),
306
+ ];
307
+
308
+ // Apply changes
309
+ path.replaceWith(
310
+ t.classMethod(
311
+ path.node.kind,
312
+ path.node.key,
313
+ params,
314
+ path.node.body,
315
+ false,
316
+ false,
317
+ false,
318
+ path.node.async
239
319
  )
240
320
  );
241
321
  }
322
+
323
+ //console.log("ROUTE METHOD", this.filename, methodName, generate(path.node).code);
242
324
  }
243
325
  }
244
326
  };
@@ -109,10 +109,10 @@ module.exports = (app: App, side: TAppSide, dev: boolean): ImportTransformer =>
109
109
  }
110
110
  }
111
111
 
112
- console.log( generate(t.variableDeclaration("const", [t.variableDeclarator(
112
+ /*console.log( generate(t.variableDeclaration("const", [t.variableDeclarator(
113
113
  t.identifier(request.imported.name),
114
114
  t.objectExpression(pageLoaders)
115
- )])).code );
115
+ )])).code );*/
116
116
 
117
117
  return [
118
118
  ...imports,
@@ -42,19 +42,21 @@ module.exports = (options: TOptions) => (
42
42
  const clientServices = ['Router'];
43
43
  // Others will be called via app.<Service> (backend) or api.post(<path>, <params>) (frontend)
44
44
 
45
+ const routerMethods = ['get', 'post', 'put', 'delete', 'patch'];
46
+
45
47
  /*----------------------------------
46
48
  - PLUGIN
47
49
  ----------------------------------*/
48
50
  function Plugin(babel, { app, side, debug }: TOptions) {
49
51
 
50
- debug = true;
52
+ //debug = true;
51
53
 
52
54
  const t = babel.types as typeof types;
53
55
 
54
56
  /*
55
57
  - Wrap route.get(...) with (app: Application) => { }
56
58
  - Inject chunk ID into client route options
57
- - Transform aoi.fetch:
59
+ - Transform api.fetch:
58
60
 
59
61
  Input:
60
62
  const { stats } = api.fetch({
@@ -83,7 +85,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
83
85
  // Replace by: nothing
84
86
  ImportDeclaration(path) {
85
87
 
86
- const shouldTransformImports = this.file.process && this.file.side === 'front';
88
+ const shouldTransformImports = this.file.process;
87
89
  if (!shouldTransformImports)
88
90
  return;
89
91
 
@@ -154,7 +156,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
154
156
  &&
155
157
  callee.property.type === 'Identifier'
156
158
  &&
157
- ['page', 'error', 'get', 'post', 'put', 'delete'].includes(callee.property.name)
159
+ ['page', 'error', ...routerMethods].includes(callee.property.name)
158
160
  ) {
159
161
 
160
162
  // Should be at the root of the document
@@ -171,6 +173,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
171
173
  }
172
174
 
173
175
  // Adjust
176
+ // /client/pages/*
174
177
  if (this.file.side === 'front') {
175
178
  transformDataFetchers(path, this, routeDef);
176
179
  }
@@ -181,12 +184,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
181
184
  // Delete the route def since it will be replaced by a wrapper
182
185
  path.replaceWithMultiple([]);
183
186
 
184
- /* [client] Backend Service calls: Transform to api.post( <method path>, <params> )
185
-
186
- Events.Create( form.data ).then(res => toast.success(res.message))
187
- =>
188
- api.post( '/api/events/create', form.data ).then(res => toast.success(res.message))
189
- */
187
+
190
188
  } else if (this.file.side === 'front') {
191
189
 
192
190
  const isAService = (
@@ -197,14 +195,20 @@ function Plugin(babel, { app, side, debug }: TOptions) {
197
195
  if(!isAService)
198
196
  return;
199
197
 
200
- if (side === 'client') {
198
+ /* [client] Backend Service calls: Transform to api.post( <method path>, <params> )
199
+
200
+ Events.Create( form.data ).then(res => toast.success(res.message))
201
+ =>
202
+ api.post( '/api/events/create', form.data ).then(res => toast.success(res.message))
203
+ */
204
+ if (side === 'client' && !clientServices.includes(serviceName)) {
201
205
 
202
206
  // Get complete call path
203
207
  const apiPath = '/api/' + completePath.join('/');
204
208
 
205
209
  // Replace by api.post( <method path>, <params> )
206
210
  const apiPostArgs: types.CallExpression["arguments"] = [t.stringLiteral(apiPath)];
207
- if (path.node.arguments.length === 1)
211
+ if (path.node.arguments.length >= 1)
208
212
  apiPostArgs.push( path.node.arguments[0] );
209
213
 
210
214
  path.replaceWith(
@@ -214,6 +218,13 @@ function Plugin(babel, { app, side, debug }: TOptions) {
214
218
  ), apiPostArgs
215
219
  )
216
220
  )
221
+
222
+ /* [server] Backend Service calls
223
+
224
+ Events.Create( form.data ).then(res => toast.success(res.message))
225
+ =>
226
+ app.Events.Create( form.data, context ).then(res => toast.success(res.message))
227
+ */
217
228
  } else {
218
229
 
219
230
  // Rebuild member expression from completePath, adding a api prefix
@@ -231,7 +242,8 @@ function Plugin(babel, { app, side, debug }: TOptions) {
231
242
  // Replace by app.<service>.<method>(...)
232
243
  path.replaceWith(
233
244
  t.callExpression(
234
- newCallee, [...path.node.arguments]
245
+ newCallee,
246
+ [...path.node.arguments]
235
247
  )
236
248
  )
237
249
  }
@@ -268,8 +280,8 @@ function Plugin(babel, { app, side, debug }: TOptions) {
268
280
  relativeFileName = filename.substring( cli.paths.appRoot.length );
269
281
  if (filename.startsWith( cli.paths.coreRoot ))
270
282
  relativeFileName = filename.substring( cli.paths.coreRoot.length );
271
- if (filename.startsWith('/node_modules/5htp-core/'))
272
- relativeFileName = filename.substring( '/node_modules/5htp-core/'.length );
283
+ /*if (filename.startsWith('/node_modules/5htp-core/'))
284
+ relativeFileName = filename.substring( '/node_modules/5htp-core/'.length );*/
273
285
 
274
286
  // The file isn't a route definition
275
287
  if (relativeFileName === undefined) {
@@ -320,7 +332,39 @@ function Plugin(babel, { app, side, debug }: TOptions) {
320
332
  }
321
333
  */
322
334
  routeDef.dataFetchers.push(
323
- ...path.node.arguments[0].properties
335
+ ...path.node.arguments[0].properties.map(p => {
336
+
337
+ // Server side: Pass request context as 2nd argument
338
+ // companies: Companies.create( <params>, context )
339
+ if (
340
+ side === 'server'
341
+ &&
342
+ p.type === 'ObjectProperty'
343
+ &&
344
+ p.key.type === 'Identifier'
345
+ &&
346
+ p.value.type === 'CallExpression'
347
+ &&
348
+ // TODO: reliable way to know if it's a service
349
+ !(
350
+ p.value.callee.type === 'MemberExpression'
351
+ &&
352
+ p.value.callee.object.type === 'Identifier'
353
+ &&
354
+ p.value.callee.object.name === 'api'
355
+ )
356
+ ) {
357
+
358
+ // Pass request context as 2nd argument
359
+ p.value.arguments = p.value.arguments.length === 0
360
+ ? [ t.objectExpression([]), t.identifier('context') ]
361
+ : [ p.value.arguments[0], t.identifier('context') ];
362
+
363
+ }
364
+
365
+ return p;
366
+
367
+ })
324
368
  );
325
369
 
326
370
  /* Replace the:
@@ -357,7 +401,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
357
401
 
358
402
  // Generate page chunk id
359
403
  const { filepath, chunkId } = cli.paths.getPageChunk(app, filename);
360
- debug && console.log(`[routes]`, filename, '=>', chunkId);
404
+ debug && console.log(`[routes]`, filename.replace(cli.paths.appRoot + '/client/pages', ''));
361
405
 
362
406
  // Create new options to add in route.options
363
407
  const newProperties = [
@@ -374,9 +418,19 @@ function Plugin(babel, { app, side, debug }: TOptions) {
374
418
  // Add data fetchers
375
419
  if (routeDef.dataFetchers.length !== 0) {
376
420
 
421
+ const rendererContext = t.cloneNode( renderer.params[0] );
422
+ // If not already present, add context to the 1st argument (a object spread)
423
+ if (!rendererContext.properties.some( p => p.key.name === 'context' ))
424
+ rendererContext.properties.push(
425
+ t.objectProperty(
426
+ t.identifier('context'),
427
+ t.identifier('context')
428
+ )
429
+ )
430
+
377
431
  // (contollerParams) => { stats: api.get(...) }
378
432
  const dataFetchersFunc = t.arrowFunctionExpression(
379
- renderer.params.map( param => t.cloneNode( param )),
433
+ [rendererContext],
380
434
  t.objectExpression(
381
435
  routeDef.dataFetchers.map( df => t.cloneNode( df ))
382
436
  )
@@ -467,7 +521,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
467
521
  if (importedServicesList.length === 0)
468
522
  return;
469
523
 
470
- let exportValue: types.Expression | types.BlockStatement;
524
+ const definitions: types.BlockStatement["body"] = [];
471
525
  if (file.side === 'front') {
472
526
 
473
527
  // Limit to one route def per file
@@ -496,48 +550,89 @@ function Plugin(babel, { app, side, debug }: TOptions) {
496
550
 
497
551
  // Force babel to create new fresh nodes
498
552
  // If we directy use statementParent, it will not be included in the final compiler code
499
- exportValue = t.callExpression(
500
- t.memberExpression(
501
- t.identifier( callee.object.name ),
502
- callee.property,
503
- ),
504
- [routePath, ...routeArgs]
553
+ definitions.push(
554
+ t.returnStatement(
555
+ t.callExpression(
556
+ t.memberExpression(
557
+ t.identifier( callee.object.name ),
558
+ callee.property,
559
+ ),
560
+ [routePath, ...routeArgs]
561
+ )
562
+ )
505
563
  )
506
564
 
507
565
  } else {
508
566
 
509
- exportValue = t.blockStatement([
567
+ definitions.push(
510
568
  // Without spread = react jxx need additionnal loader
511
569
  ...file.routeDefinitions.map( def =>
512
570
  t.expressionStatement(def.definition)
513
571
  ),
514
- ])
572
+ )
515
573
  }
516
574
 
517
- const exportDeclaration = t.exportNamedDeclaration(
518
- t.variableDeclaration('const', [
519
- t.variableDeclarator(
520
- t.identifier('__register'),
521
- t.arrowFunctionExpression(
522
- [
523
- t.objectPattern(
524
- importedServicesList.map(([ local, imported ]) =>
525
- t.objectProperty(
526
- t.identifier( local ),
527
- t.identifier( imported ),
575
+ /*
576
+ ({ Router, app: { Events } }}) => {
577
+ ...
578
+ }
579
+ */
580
+ const appSpread: types.ObjectProperty[] = []
581
+ const servicesSpread: types.ObjectProperty[] = [
582
+ t.objectProperty(
583
+ t.identifier('app'),
584
+ t.identifier('app'),
585
+ ),
586
+ t.objectProperty(
587
+ t.identifier('context'),
588
+ t.identifier('context'),
589
+ )
590
+ ]
591
+ for (const [local, imported] of importedServicesList) {
592
+ if (imported === 'app')
593
+ appSpread.push(
594
+ t.objectProperty(
595
+ t.identifier(local),
596
+ t.identifier(local),
597
+ )
598
+ )
599
+ else
600
+ servicesSpread.push(
601
+ t.objectProperty(
602
+ t.identifier(local),
603
+ t.identifier(imported),
604
+ )
605
+ )
606
+ }
607
+
608
+ // export const __register = ({ Router, app }) => { ... }
609
+ const exportDeclaration = t.exportNamedDeclaration(
610
+ t.variableDeclaration('const', [
611
+ t.variableDeclarator(
612
+ t.identifier('__register'),
613
+ t.arrowFunctionExpression(
614
+ [
615
+ t.objectPattern(servicesSpread)
616
+ ],
617
+ t.blockStatement([
618
+ // const { Events } = app;
619
+ t.variableDeclaration('const', [
620
+ t.variableDeclarator(
621
+ t.objectPattern(appSpread),
622
+ t.identifier('app')
528
623
  )
529
- )
530
- )
531
- ],
532
- exportValue
624
+ ]),
625
+ // Router.post(....)
626
+ ...definitions,
627
+ ])
628
+ )
533
629
  )
534
- )
535
- ])
536
- );
630
+ ])
631
+ );
537
632
 
538
633
 
539
- (file.side === 'front' && file.path.includes('/server/routes/events.ts'))
540
- && console.log( file.path, generate(exportDeclaration).code );
634
+ // (file.path.includes('clients/prospect/search') && side === 'client')
635
+ // && console.log( file.path, generate(exportDeclaration).code );
541
636
 
542
637
  return exportDeclaration;
543
638
  }
@@ -1,11 +1,10 @@
1
1
  import { staticAssetName } from '../../../paths';
2
2
  import type webpack from 'webpack';
3
- import type App from '../../../app';
4
3
 
5
4
  module.exports = (app: App, dev: boolean, client: boolean): webpack.RuleSetRule[] => {
6
5
 
7
6
  return [{
8
- test: /\.(bmp|gif|jpg|jpeg|png|ico|svg|webp)$/,
7
+ test: /\.(bmp|gif|png|jpg|jpegico|svg|webp)$/,
9
8
  type: 'asset',
10
9
  parser: {
11
10
  dataUrlCondition: {
@@ -17,6 +16,19 @@ module.exports = (app: App, dev: boolean, client: boolean): webpack.RuleSetRule[
17
16
  }
18
17
 
19
18
  }, {
19
+ test: /\.(jpg|jpeg|png)$/i,
20
+ type: "javascript/auto",
21
+ use: [{
22
+ loader: "responsive-loader",
23
+ options: {
24
+ sizes: [320, 480, 640, 768, 1024, 1300],
25
+ placeholder: true,
26
+ placeholderSize: 20,
27
+ quality: 100,
28
+ publicPath: '/public'
29
+ }
30
+ }]
31
+ }, {
20
32
  test: /\.(webm|mp4|avi|mpk|mov|mkv)$/,
21
33
  type: 'asset/resource',
22
34
  },]
package/compiler/index.ts CHANGED
@@ -17,6 +17,7 @@ import cli from '..';
17
17
  import createServerConfig from './server';
18
18
  import createClientConfig from './client';
19
19
  import { TCompileMode } from './common';
20
+ import { routerServices } from './common/babel/plugins/services';
20
21
 
21
22
  type TCompilerCallback = (compiler: webpack.Compiler) => void
22
23
 
@@ -25,7 +26,16 @@ type TServiceMetas = {
25
26
  name: string,
26
27
  parent: string,
27
28
  dependences: string,
28
- importationPath: string
29
+ importationPath: string,
30
+ priority: number
31
+ }
32
+
33
+ type TRegisteredService = {
34
+ id?: string,
35
+ name: string,
36
+ className: string,
37
+ instanciation: (parentRef: string) => string,
38
+ priority: number,
29
39
  }
30
40
 
31
41
  /*----------------------------------
@@ -140,33 +150,40 @@ export default class Compiler {
140
150
 
141
151
 
142
152
  // Index services
143
- const searchDirs = {
153
+ const searchDirs = [
144
154
  // The less priority is the first
145
- // The last override the first if there are duplicates
146
- '@server/services/': path.join(cli.paths.core.root, 'server', 'services'),
147
- '@/server/services/': path.join(app.paths.root, 'server', 'services'),
155
+ {
156
+ path: '@server/services/',
157
+ priority: -1,
158
+ root: path.join(cli.paths.core.root, 'server', 'services')
159
+ },
160
+ {
161
+ path: '@/server/services/',
162
+ priority: 0,
163
+ root: path.join(app.paths.root, 'server', 'services')
164
+ },
148
165
  // Temp disabled because compile issue on vercel
149
166
  //'': path.join(app.paths.root, 'node_modules'),
150
- }
167
+ ]
151
168
 
152
169
  // Generate app class file
153
170
  const servicesAvailable: {[id: string]: TServiceMetas} = {};
154
- for (const importationPrefix in searchDirs) {
171
+ for (const searchDir of searchDirs) {
155
172
 
156
- const searchDir = searchDirs[ importationPrefix ];
157
- const services = this.findServices(searchDir);
173
+ const services = this.findServices(searchDir.root);
158
174
 
159
175
  for (const serviceDir of services) {
160
176
  const metasFile = path.join( serviceDir, 'service.json');
161
177
 
162
178
  // The +1 is to remove the slash
163
- const importationPath = importationPrefix + serviceDir.substring( searchDir.length + 1 );
179
+ const importationPath = searchDir.path + serviceDir.substring( searchDir.root.length + 1 );
164
180
 
165
181
  const serviceMetas = require(metasFile);
166
182
 
167
183
  servicesAvailable[ serviceMetas.id ] = {
184
+ importationPath,
185
+ priority: searchDir.priority,
168
186
  ...serviceMetas,
169
- importationPath
170
187
  };
171
188
  }
172
189
  }
@@ -175,13 +192,13 @@ export default class Compiler {
175
192
  const imported: string[] = []
176
193
  const referencedNames: {[serviceId: string]: string} = {} // ID to Name
177
194
 
178
- const refService = (serviceName: string, serviceConfig: any, level: number = 0) => {
195
+ const refService = (serviceName: string, serviceConfig: any, level: number = 0): TRegisteredService => {
179
196
 
180
197
  if (serviceConfig.refTo !== undefined) {
181
198
  const refTo = serviceConfig.refTo;
182
199
  return {
183
200
  name: serviceName,
184
- code: `${serviceName}: this.${refTo},`,
201
+ instanciation: (parentRef: string) => `this.${refTo}`,
185
202
  priority: 0
186
203
  }
187
204
  }
@@ -200,51 +217,51 @@ export default class Compiler {
200
217
  if (serviceConfig.name !== undefined)
201
218
  referencedNames[serviceConfig.id] = serviceConfig.name;
202
219
 
203
- // Subservices
204
- let subservices = '';
205
- if (serviceConfig.subservices) {
220
+ const processConfig = (config: any, level: number = 0) => {
206
221
 
207
- const subservicesList = serviceConfig.subservices;
208
- const subservicesCode = Object.entries(subservicesList).map(([name, service]) =>
209
- refService(name, service, level + 1)
210
- );
222
+ let propsStr = '';
223
+ for (const key in config) {
224
+ const value = config[key];
225
+
226
+ if (!value || typeof value !== 'object')
227
+ propsStr += `"${key}":${serialize(value, { space: 4 })},\n`;
228
+
229
+ // Reference to a service
230
+ else if (value.type === 'service.setup' || value.type === 'service.ref') // TODO: more reliable way to detect a service reference
231
+ propsStr += `${key}:`+ refService(key, value, level + 1).instanciation('instance') + ',\n'
232
+
233
+ // Recursion
234
+ else if (level <= 4 && !Array.isArray(value))
235
+ propsStr += `"${key}":` + processConfig(value, level + 1) + ',\n';
236
+
237
+ else
238
+ propsStr += `"${key}":${serialize(value, { space: 4 })},\n`;
211
239
 
212
- // Sort by priority
213
- const sortedSubservices = subservicesCode.sort((a, b) => a.priority - b.priority);
240
+ }
214
241
 
215
- // Generate code
216
- subservices = sortedSubservices.map(s => s.code).join('\n');
242
+ return `{ ${propsStr} }`;
217
243
  }
244
+ const config = processConfig(serviceConfig.config || {});
218
245
 
219
246
  // Generate the service instance
220
- const instanciation = `new ${serviceMetas.name}(
221
- this,
222
- ${serialize(serviceConfig.config || {}) || '{}'},
223
- () => ({
224
- ${subservices}
225
- }),
226
- this
227
- )`
228
-
229
- if (level === 0)
230
- return {
231
- name: serviceName,
232
- code: `public ${serviceName} = ${instanciation};`,
233
- priority: serviceConfig.config?.priority || 0
234
- };
235
- else
236
- return {
237
- name: serviceName,
238
- code: `${serviceName}: ${instanciation},`,
239
- priority: serviceConfig.config?.priority || 0
240
- };
247
+ const instanciation = (parentRef: string) =>
248
+ `new ${serviceMetas.name}(
249
+ ${parentRef},
250
+ (instance: ${serviceMetas.name}) => (${config}),
251
+ this
252
+ )`
253
+
254
+ return {
255
+ id: serviceConfig.id,
256
+ name: serviceName,
257
+ instanciation,
258
+ className: serviceMetas.name,
259
+ priority: serviceConfig.config?.priority || serviceMetas.priority || 0,
260
+ };
241
261
  }
242
262
 
243
263
  const servicesCode = Object.values(app.registered).map( s => refService(s.name, s, 0));
244
264
  const sortedServices = servicesCode.sort((a, b) => a.priority - b.priority);
245
-
246
- const services = sortedServices.map(s => s.code).join('\n');
247
- const servicesNames = sortedServices.map(s => s.name);
248
265
 
249
266
  // Define the app class identifier
250
267
  const appClassIdentifier = app.identity.identifier;
@@ -255,43 +272,67 @@ export default class Compiler {
255
272
  path.join( app.paths.client.generated, 'services.d.ts'),
256
273
  `declare module "@app" {
257
274
 
258
- import { RouenEvents as RouenEventsClient } from "@/client";
259
- import RouenEventsServer from "@/server/.generated/app";
275
+ import { ${appClassIdentifier} as ${appClassIdentifier}Client } from "@/client";
276
+ import ${appClassIdentifier}Server from "@/server/.generated/app";
260
277
 
261
278
  import { ApplicationProperties as ClientApplicationProperties } from "@client/app";
262
279
  import { ApplicationProperties as ServerApplicationProperties } from "@server/app";
263
280
 
264
- type ClientServices = Omit<RouenEventsClient, ClientApplicationProperties>;
265
- type ServerServices = Omit<RouenEventsServer, ServerApplicationProperties | keyof ClientServices>;
281
+ type ClientServices = Omit<${appClassIdentifier}Client, ClientApplicationProperties>;
282
+ type ServerServices = Omit<${appClassIdentifier}Server, ServerApplicationProperties | keyof ClientServices>;
266
283
 
267
284
  type CombinedServices = ClientServices & ServerServices;
268
285
 
269
286
  const appClass: CombinedServices;
270
287
  export = appClass;
271
288
  }
289
+
290
+ declare module '@models/types' {
291
+ export * from '@/var/prisma/index';
292
+ }
272
293
 
294
+ declare module '@request' {
295
+
296
+ }
273
297
 
274
- // Temporary
275
- /*declare module '@models' {
276
- export * from '@/var/prisma/index';
277
- }*/
278
-
279
- declare module '@models' {
280
- import { Prisma, PrismaClient } from '@/var/prisma/index';
281
-
282
- type ModelNames = Prisma.ModelName;
283
-
284
- type ModelDelegates = {
285
- [K in ModelNames]: PrismaClient[Uncapitalize<K>];
286
- };
287
-
288
- const models: ModelDelegates;
289
-
290
- export = models;
298
+ declare namespace preact.JSX {
299
+ interface HTMLAttributes {
300
+ src?: string;
301
+ }
291
302
  }
292
- `
303
+ `
293
304
  );
294
305
 
306
+ fs.outputFileSync(
307
+ path.join( app.paths.client.generated, 'context.ts'),
308
+ `// TODO: move it into core (but how to make sure usecontext returns ${appClassIdentifier}'s context ?)
309
+ import React from 'react';
310
+
311
+ import type ${appClassIdentifier}Server from '@/server/.generated/app';
312
+ import type { TRouterContext as TServerRouterRequestContext } from '@server/services/router/response';
313
+ import type { TRouterContext as TClientRouterRequestContext } from '@client/services/router/response';
314
+ import type ${appClassIdentifier}Client from '.';
315
+
316
+ // TO Fix: TClientRouterRequestContext is unable to get the right type of ${appClassIdentifier}Client["router"]
317
+ // (it gets ClientApplication instead of ${appClassIdentifier}Client)
318
+ type ClientRequestContext = TClientRouterRequestContext<${appClassIdentifier}Client["Router"], ${appClassIdentifier}Client>;
319
+ type ServerRequestContext = TServerRouterRequestContext<${appClassIdentifier}Server["Router"]>
320
+ type UniversalServices = ClientRequestContext | ServerRequestContext
321
+
322
+ // Non-universla services are flagged as potentially undefined
323
+ export type ClientContext = (
324
+ UniversalServices
325
+ &
326
+ Partial<Omit<ClientRequestContext, keyof UniversalServices>>
327
+ &
328
+ {
329
+ Router: ${appClassIdentifier}Client["Router"],
330
+ }
331
+ )
332
+
333
+ export const ReactClientContext = React.createContext<ClientContext>({} as ClientContext);
334
+ export default (): ClientContext => React.useContext<ClientContext>(ReactClientContext);`);
335
+
295
336
  fs.outputFileSync(
296
337
  path.join( app.paths.server.generated, 'app.ts'),
297
338
  `import { Application } from '@server/app/index';
@@ -300,15 +341,19 @@ ${imported.join('\n')}
300
341
 
301
342
  export default class ${appClassIdentifier} extends Application {
302
343
 
303
- protected serviceNames = [
304
- ${Object.values(servicesNames).map(name => `"${name}"`).join(',\n')}
305
- ] as const;
306
-
307
- protected servicesIdToName = {
308
- ${Object.entries(referencedNames).map(([id, name]) => `"${id}": "${name}"`).join(',\n')}
344
+ ${sortedServices.map(service =>
345
+ `public ${service.name}!: ${service.className};`
346
+ ).join('\n')}
347
+
348
+ protected registered = {
349
+ ${sortedServices.map(service =>
350
+ `"${service.id}": {
351
+ name: "${service.name}",
352
+ priority: ${service.priority},
353
+ start: () => ${service.instanciation('this')}
354
+ }`
355
+ ).join(',\n')}
309
356
  } as const;
310
-
311
- ${services}
312
357
  }
313
358
 
314
359
 
@@ -320,6 +365,29 @@ export default class ${appClassIdentifier} extends Application {
320
365
 
321
366
  declare type ${appClassIdentifier} = import("@/server/.generated/app").default;
322
367
 
368
+ declare module '@cli/app' {
369
+
370
+ type App = {
371
+
372
+ env: TEnvConfig;
373
+
374
+ use: (referenceName: string) => TServiceRef;
375
+
376
+ setup: <TServiceName extends keyof ${appClassIdentifier}>(...args: [
377
+ // { user: app.setup('Core/User') }
378
+ servicePath: string,
379
+ serviceConfig?: {}
380
+ ] | [
381
+ // app.setup('User', 'Core/User')
382
+ serviceName: TServiceName,
383
+ servicePath: string,
384
+ serviceConfig?: ${appClassIdentifier}[TServiceName]["config"]
385
+ ]) => TServiceSetup;
386
+ }
387
+ const app: App;
388
+ export = app;
389
+ }
390
+
323
391
  declare module "@app" {
324
392
 
325
393
  import { ApplicationContainer } from '@server/app/container';
@@ -355,6 +423,12 @@ declare module '@server/app' {
355
423
 
356
424
  export = foo;
357
425
  }
426
+
427
+ declare module '@request' {
428
+ import type { TRouterContext } from '@server/services/router/response';
429
+ const routerContext: TRouterContext<CrossPath["Router"]>;
430
+ export = routerContext;
431
+ }
358
432
 
359
433
  declare module '@models' {
360
434
  import { Prisma, PrismaClient } from '@/var/prisma/index';
@@ -368,6 +442,10 @@ declare module '@models' {
368
442
  const models: ModelDelegates;
369
443
 
370
444
  export = models;
445
+ }
446
+
447
+ declare module '@models/types' {
448
+ export * from '@/var/prisma/index';
371
449
  }`
372
450
  );
373
451
  }
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.5.9",
4
+ "version": "0.6.1",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp.git",
7
7
  "license": "MIT",
@@ -54,10 +54,7 @@
54
54
  "dayjs": "^1.11.5",
55
55
  "favicons": "^7.2.0",
56
56
  "filesize": "^8.0.3",
57
- "image-minimizer-webpack-plugin": "^2.2.0",
58
- "imagemin": "^8.0.1",
59
57
  "imagemin-svgo": "^10.0.0",
60
- "imagemin-webp": "^6.0.0",
61
58
  "json5": "^2.2.0",
62
59
  "less-loader": "^10.0.1",
63
60
  "less-vars-to-js": "^1.3.0",
@@ -91,6 +88,11 @@
91
88
  "@types/node": "^16.9.1",
92
89
  "@types/nodemailer": "^6.4.4",
93
90
  "@types/prompts": "^2.0.14",
94
- "@types/webpack-env": "^1.16.2"
91
+ "@types/webpack-env": "^1.16.2",
92
+ "image-minimizer-webpack-plugin": "^4.1.4",
93
+ "imagemin": "^9.0.1",
94
+ "imagemin-webp": "^6.0.0",
95
+ "responsive-loader": "^3.1.2",
96
+ "sharp": "^0.34.3"
95
97
  }
96
98
  }
package/paths.ts CHANGED
@@ -148,7 +148,7 @@ export default class Paths {
148
148
  rootDir: this.core.cli
149
149
  });
150
150
 
151
- console.log('Applying Aliases ...', aliases.forModuleAlias());
151
+ //console.log('Applying Aliases ...', aliases.forModuleAlias());
152
152
  moduleAlias.addAliases( aliases.forModuleAlias() );
153
153
 
154
154
  }