5htp 0.3.1 → 0.3.2

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",
4
+ "version": "0.3.2",
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",
@@ -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
  }
@@ -154,9 +154,9 @@ dependences: ${JSON.stringify(dependences)},
154
154
  path.join( app.paths.client.generated, 'services.d.ts'),
155
155
  `declare module "@app" {
156
156
 
157
- import { CrossPathClient } from '@/client/index';
157
+ import ${appClassIdentifier} from '@/client/index';
158
158
 
159
- const appClass: CrossPathClient;
159
+ const appClass: ${appClassIdentifier};
160
160
 
161
161
  export = appClass
162
162
  }`
@@ -200,8 +200,12 @@ declare module '@server/app' {
200
200
  import { Application } from "@server/app/index";
201
201
  import { ServicesContainer } from "@server/app/service/container";
202
202
 
203
+ abstract class ApplicationWithServices extends Application<
204
+ ServicesContainer<InstalledServices>
205
+ > {}
206
+
203
207
  export interface Exported {
204
- Application: typeof Application,
208
+ Application: typeof ApplicationWithServices,
205
209
  Services: ServicesContainer<InstalledServices>,
206
210
  }
207
211