5htp 0.6.3-1 → 0.6.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.
@@ -5,8 +5,6 @@
5
5
  // Npm
6
6
  import type webpack from 'webpack';
7
7
  import PresetBabel, { Options } from '@babel/preset-env';
8
- // Core
9
- import PluginIndexage from '../plugins/indexage';
10
8
 
11
9
  import cli from '@cli';
12
10
  import type { TAppSide, App } from '@cli/app';
@@ -70,10 +68,6 @@ module.exports = (app: App, side: TAppSide, dev: boolean): webpack.RuleSetRule[]
70
68
  // Désactive car ralenti compilation
71
69
  cacheCompression: false,
72
70
 
73
- metadataSubscribers: [
74
- PluginIndexage.metadataContextFunctionName
75
- ],
76
-
77
71
  compact: !dev,
78
72
 
79
73
  // https://babeljs.io/docs/usage/options/
@@ -165,14 +159,10 @@ module.exports = (app: App, side: TAppSide, dev: boolean): webpack.RuleSetRule[]
165
159
  ],
166
160
 
167
161
  overrides: [
168
-
169
- require('./plugins/icones-svg')(app),
170
162
 
171
163
  ...(side === 'client' ? [
172
164
 
173
165
  ] : [
174
- //require('./plugins/queries'),
175
- //require('./plugins/injection-dependances'),
176
166
  ]),
177
167
  ]
178
168
  }
@@ -6,6 +6,10 @@
6
6
  import * as types from '@babel/types'
7
7
  import type { NodePath, PluginObj } from '@babel/core';
8
8
  import generate from '@babel/generator';
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { parse } from '@babel/parser';
12
+ import traverse from '@babel/traverse';
9
13
 
10
14
  // Core
11
15
  import cli from '@cli';
@@ -38,6 +42,170 @@ type TImportedIndex = {
38
42
  source: TImportSource // container | application | models
39
43
  }
40
44
 
45
+ type TRoutesIndex = {
46
+ byFile: Map<string, Set<string>>,
47
+ all: Set<string>,
48
+ initialized: boolean
49
+ }
50
+
51
+ const routesIndexByAppRoot = new Map<string, TRoutesIndex>();
52
+
53
+ function isRouteDecoratorExpression(expression: types.Expression): boolean {
54
+ return (
55
+ // Handles the case of @Route without parameters
56
+ (types.isIdentifier(expression) && expression.name === 'Route')
57
+ ||
58
+ // Handles the case of @Route() with parameters
59
+ (
60
+ types.isCallExpression(expression)
61
+ &&
62
+ types.isIdentifier(expression.callee)
63
+ &&
64
+ expression.callee.name === 'Route'
65
+ )
66
+ );
67
+ }
68
+
69
+ function getRoutePathFromDecoratorExpression(expression: types.Expression): string | undefined {
70
+ if (
71
+ !types.isCallExpression(expression)
72
+ ||
73
+ !types.isIdentifier(expression.callee)
74
+ ||
75
+ expression.callee.name !== 'Route'
76
+ )
77
+ return;
78
+
79
+ const firstArg = expression.arguments[0];
80
+ if (!types.isStringLiteral(firstArg))
81
+ return;
82
+
83
+ return firstArg.value;
84
+ }
85
+
86
+ function extractRoutePathsFromCode(code: string, filename: string): Set<string> {
87
+ const routePaths = new Set<string>();
88
+
89
+ let ast: ReturnType<typeof parse> | undefined;
90
+ try {
91
+ ast = parse(code, {
92
+ sourceType: 'module',
93
+ sourceFilename: filename,
94
+ plugins: [
95
+ 'typescript',
96
+ 'decorators-legacy',
97
+ 'jsx',
98
+ 'classProperties',
99
+ 'classPrivateProperties',
100
+ 'classPrivateMethods',
101
+ 'dynamicImport',
102
+ 'importMeta',
103
+ 'optionalChaining',
104
+ 'nullishCoalescingOperator',
105
+ 'topLevelAwait',
106
+ ],
107
+ });
108
+ } catch {
109
+ return routePaths;
110
+ }
111
+
112
+ traverse(ast, {
113
+ ClassMethod(path) {
114
+ const { node } = path;
115
+ if (!node.decorators || node.key.type !== 'Identifier')
116
+ return;
117
+
118
+ for (const decorator of node.decorators) {
119
+ if (!isRouteDecoratorExpression(decorator.expression))
120
+ continue;
121
+
122
+ const routePath = getRoutePathFromDecoratorExpression(decorator.expression);
123
+ if (routePath)
124
+ routePaths.add(routePath);
125
+ }
126
+ }
127
+ });
128
+
129
+ return routePaths;
130
+ }
131
+
132
+ function listTsFiles(dirPath: string): string[] {
133
+ let entries: fs.Dirent[];
134
+ try {
135
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
136
+ } catch {
137
+ return [];
138
+ }
139
+
140
+ const files: string[] = [];
141
+ for (const entry of entries) {
142
+ const fullPath = path.join(dirPath, entry.name);
143
+ if (entry.isDirectory()) {
144
+ files.push(...listTsFiles(fullPath));
145
+ continue;
146
+ }
147
+
148
+ const isTs = fullPath.endsWith('.ts') || fullPath.endsWith('.tsx');
149
+ if (!isTs || fullPath.endsWith('.d.ts'))
150
+ continue;
151
+
152
+ files.push(fullPath);
153
+ }
154
+ return files;
155
+ }
156
+
157
+ function initializeRoutesIndex(appRoot: string, index: TRoutesIndex) {
158
+ const servicesDir = path.join(appRoot, 'server', 'services');
159
+ const files = listTsFiles(servicesDir);
160
+
161
+ for (const file of files) {
162
+ let code: string;
163
+ try {
164
+ code = fs.readFileSync(file, 'utf8');
165
+ } catch {
166
+ continue;
167
+ }
168
+
169
+ const routes = extractRoutePathsFromCode(code, file);
170
+ index.byFile.set(file, routes);
171
+ }
172
+
173
+ index.all.clear();
174
+ for (const routes of index.byFile.values()) {
175
+ for (const route of routes)
176
+ index.all.add(route);
177
+ }
178
+ index.initialized = true;
179
+ }
180
+
181
+ function getRoutesIndex(appRoot: string): TRoutesIndex {
182
+ let index = routesIndexByAppRoot.get(appRoot);
183
+ if (!index) {
184
+ index = {
185
+ byFile: new Map(),
186
+ all: new Set(),
187
+ initialized: false
188
+ };
189
+ routesIndexByAppRoot.set(appRoot, index);
190
+ }
191
+
192
+ if (!index.initialized) {
193
+ initializeRoutesIndex(appRoot, index);
194
+ }
195
+
196
+ return index;
197
+ }
198
+
199
+ function updateRoutesIndexForFile(index: TRoutesIndex, filename: string, routePaths: Set<string>) {
200
+ index.byFile.set(filename, routePaths);
201
+
202
+ index.all.clear();
203
+ for (const routes of index.byFile.values()) {
204
+ for (const route of routes)
205
+ index.all.add(route);
206
+ }
207
+ }
208
+
41
209
  /*----------------------------------
42
210
  - PLUGIN
43
211
  ----------------------------------*/
@@ -75,6 +243,9 @@ function Plugin(babel, { app, side, debug }: TOptions) {
75
243
  // Count how many total imports we transform
76
244
  importedCount: number,
77
245
  routeMethods: string[],
246
+ routePaths: Set<string>,
247
+ routesIndex: TRoutesIndex,
248
+ contextGuardedClassMethods: WeakSet<types.ClassMethod>,
78
249
 
79
250
  // For every local identifier, store info about how it should be rewritten
80
251
  imported: {
@@ -92,6 +263,10 @@ function Plugin(babel, { app, side, debug }: TOptions) {
92
263
  this.debug = debug || false;
93
264
 
94
265
  this.routeMethods = [];
266
+ this.routePaths = new Set();
267
+
268
+ this.routesIndex = getRoutesIndex(app.paths.root);
269
+ this.contextGuardedClassMethods = new WeakSet();
95
270
  },
96
271
 
97
272
  visitor: {
@@ -109,28 +284,21 @@ function Plugin(babel, { app, side, debug }: TOptions) {
109
284
 
110
285
  for (const decorator of node.decorators) {
111
286
 
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
- );
287
+ const isRoute = isRouteDecoratorExpression(decorator.expression);
125
288
 
126
289
  if (!isRoute) continue;
127
290
 
128
291
  const methodName = node.key.name;
129
292
  this.routeMethods.push( methodName );
130
293
 
294
+ const routePath = getRoutePathFromDecoratorExpression(decorator.expression);
295
+ if (routePath)
296
+ this.routePaths.add(routePath);
131
297
  }
132
298
  }
133
299
  });
300
+
301
+ updateRoutesIndexForFile(this.routesIndex, this.filename, this.routePaths);
134
302
  },
135
303
 
136
304
  /**
@@ -203,6 +371,88 @@ function Plugin(babel, { app, side, debug }: TOptions) {
203
371
  path.replaceWithMultiple(replaceWith);
204
372
  },
205
373
 
374
+ CallExpression(path) {
375
+ if (!this.processFile)
376
+ return;
377
+
378
+ const classMethodPath = path.findParent(p => p.isClassMethod()) as NodePath<types.ClassMethod> | null;
379
+ if (!classMethodPath)
380
+ return;
381
+
382
+ // Ignore constructors
383
+ if (classMethodPath.node.kind === 'constructor')
384
+ return;
385
+
386
+ const callee = path.node.callee;
387
+ if (!t.isMemberExpression(callee) && !(t as any).isOptionalMemberExpression?.(callee))
388
+ return;
389
+
390
+ // Build member chain segments: this.app.<Service>.<...>
391
+ const segments: string[] = [];
392
+ let current: any = callee;
393
+ while (t.isMemberExpression(current) || (t as any).isOptionalMemberExpression?.(current)) {
394
+ const prop = current.property;
395
+ if (t.isIdentifier(prop)) {
396
+ segments.unshift(prop.name);
397
+ } else if (t.isStringLiteral(prop)) {
398
+ segments.unshift(prop.value);
399
+ } else {
400
+ return;
401
+ }
402
+ current = current.object;
403
+ }
404
+
405
+ if (!t.isThisExpression(current))
406
+ return;
407
+
408
+ // Expect: this.app.<Service>.<method>
409
+ if (segments.length < 3 || segments[0] !== 'app')
410
+ return;
411
+
412
+ const serviceLocalName = segments[1];
413
+ const importedRef = this.imported[serviceLocalName];
414
+ if (!importedRef || importedRef.source !== 'services')
415
+ return;
416
+
417
+ const routePath = [
418
+ importedRef.imported || serviceLocalName,
419
+ ...segments.slice(2)
420
+ ].join('/');
421
+
422
+ if (!this.routesIndex.all.has(routePath))
423
+ return;
424
+
425
+ // Ensure the parent function checks that `context` exists
426
+ if (!this.contextGuardedClassMethods.has(classMethodPath.node)) {
427
+ const guard = t.ifStatement(
428
+ t.binaryExpression(
429
+ '===',
430
+ t.unaryExpression('typeof', t.identifier('context')),
431
+ t.stringLiteral('undefined')
432
+ ),
433
+ t.blockStatement([
434
+ t.throwStatement(
435
+ t.newExpression(t.identifier('Error'), [
436
+ t.stringLiteral('context variable should be passed in this function')
437
+ ])
438
+ )
439
+ ])
440
+ );
441
+ classMethodPath.get('body').unshiftContainer('body', guard);
442
+ this.contextGuardedClassMethods.add(classMethodPath.node);
443
+ }
444
+
445
+ // Ensure call arguments: second argument is `context`
446
+ const args = path.node.arguments;
447
+ if (args.length === 0) {
448
+ args.push(t.identifier('undefined'), t.identifier('context'));
449
+ } else if (args.length === 1) {
450
+ args.push(t.identifier('context'));
451
+ } else {
452
+ args[1] = t.identifier('context') as any;
453
+ }
454
+ },
455
+
206
456
  // This visitor fires for every class method.
207
457
  ClassMethod(path) {
208
458
 
@@ -8,9 +8,6 @@ import dayjs from 'dayjs';
8
8
 
9
9
  // Plugins
10
10
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
11
- import PluginIndexage from './plugins/indexage';
12
- import IconesSvg from './plugins/indexage/icones-svg';
13
- import InjectDeps from './plugins/indexage/injection-dependances';
14
11
 
15
12
  // Core
16
13
  import cli from '../..';
@@ -88,12 +85,6 @@ export default function createCommonConfig(
88
85
 
89
86
  }),
90
87
 
91
- new PluginIndexage(side === 'client' ? {
92
- 'icones-svg': new IconesSvg(app),
93
- } : {
94
- //'injection-dependances': new InjectDeps,
95
- }),
96
-
97
88
  ...(side === 'client' && cli.args.analyze ? [
98
89
 
99
90
  new BundleAnalyzerPlugin({
package/compiler/index.ts CHANGED
@@ -344,13 +344,15 @@ export default (): ClientContext => React.useContext<ClientContext>(ReactClientC
344
344
  // @/server/.generated/app.ts
345
345
  fs.outputFileSync(
346
346
  path.join( app.paths.server.generated, 'app.ts'),
347
- `import { Application } from '@server/app/index';
347
+ `
348
+ import { Application } from '@server/app/index';
349
+ import { ServicesContainer } from '@server/app/service/container';
348
350
 
349
351
  ${imported.join('\n')}
350
352
 
351
- export default class ${appClassIdentifier} extends Application {
353
+ export default class ${appClassIdentifier} extends Application<ServicesContainer, CurrentUser> {
352
354
 
353
- // Makke sure the services typigs are reflecting the config and referring to the app
355
+ // Make sure the services typigs are reflecting the config and referring to the app
354
356
  ${sortedServices.map(service =>
355
357
  `public ${service.name}!: ReturnType<${appClassIdentifier}["registered"]["${service.id}"]["start"]>;`
356
358
  ).join('\n')}
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.6.3-1",
4
+ "version": "0.6.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",
@@ -1,191 +0,0 @@
1
- /*----------------------------------
2
- - DEPENDANCES
3
- ----------------------------------*/
4
-
5
- import { PluginObj } from '@babel/core';
6
-
7
- import * as types from '@babel/types'
8
- import generate from '@babel/generator';
9
-
10
- /*----------------------------------
11
- - WEBPACK RULE
12
- ----------------------------------*/
13
- module.exports = {
14
- test: "**/client/**/*.tsx",
15
- plugins: [
16
- [Plugin]
17
- ]
18
- }
19
-
20
- const debug = false
21
-
22
- /*----------------------------------
23
- - PLUGIN
24
- ----------------------------------*/
25
- function Plugin (babel) {
26
-
27
- const t = babel.types as typeof types;
28
-
29
- const plugin: PluginObj<{
30
- fichier: string,
31
- cancel: boolean
32
- }> = {
33
- pre(state) {
34
-
35
- this.fichier = state.opts.filename as string;
36
-
37
- if (!('useForm' in state.scope.bindings))
38
- this.cancel = true;
39
-
40
- },
41
- visitor: {
42
- JSXElement(instruction) {
43
-
44
- if (this.cancel === true)
45
- return;
46
-
47
- const balise = instruction.node.openingElement;
48
-
49
- /*
50
- <Champs.metas.titre className="full" attrsChamp={{ className: "h1" }} />
51
- */
52
-
53
- if (!(
54
- balise.selfClosing === true
55
- &&
56
- balise.name.type === 'JSXMemberExpression'
57
- &&
58
- balise.name.property.type === 'JSXIdentifier'
59
- ))
60
- return;
61
-
62
- debug && console.log(`[compilation][babel][form] Original: `, generate(instruction.node).code);
63
-
64
- // Si le premier element du memberexpression est Champ
65
- let nomA: types.JSXMemberExpression | types.JSXIdentifier = balise.name;
66
- while (nomA.type === 'JSXMemberExpression')
67
- nomA = nomA.object;
68
- if (!nomA.name.startsWith('Champs'))
69
- return;
70
-
71
- // Ne pas parcourir les élements enfant
72
- // Avec .stop, babel arrête d'itérer les élements voisins à partir du 6ème - 7ème
73
- //instruction.stop();
74
- instruction.skip();
75
-
76
- // Transformation de la lste des attributs en un objet
77
- /*
78
- className="full" attrsChamp={{ className: "h1" }}
79
-
80
- =>
81
-
82
- { className: "full", attrsChamp: { className: "h1" } }
83
- */
84
- let objAttributs: types.ObjectProperty[] = [];
85
- for (const attribut of balise.attributes)
86
- if (
87
- attribut.type === 'JSXAttribute' &&
88
- attribut.value !== undefined &&
89
- typeof attribut.name.name === "string"
90
- ) {
91
-
92
- let propValue: types.ObjectProperty["value"];
93
- if (attribut.value === null) // <Champ.titre autoFocus />
94
- propValue = t.booleanLiteral(true);
95
- else if (attribut.value.type !== 'JSXExpressionContainer')
96
- propValue = attribut.value;
97
- else if (attribut.value.expression.type !== 'JSXEmptyExpression')
98
- propValue = attribut.value.expression;
99
- else
100
- propValue = t.nullLiteral();
101
-
102
- objAttributs.push(
103
- t.objectProperty(
104
- t.identifier( attribut.name.name ),
105
- propValue
106
- )
107
- )
108
- }
109
-
110
- // Traverse chaque branche du chemin du champ, dans l'ordre inverse
111
- // NOTE: on aurai pu reconstituer le chemin et créer les memberexpressions en une seule itération
112
- // Mais le fait d ele faire en deux itérations rend le code plus claire et maintenable
113
- let cheminComposant: string[] = []
114
- let brancheA: types.JSXMemberExpression | types.JSXIdentifier = balise.name;
115
- while (brancheA.type === 'JSXMemberExpression') {
116
-
117
- const { property } = brancheA;
118
-
119
- cheminComposant.unshift(property.name)
120
- brancheA = brancheA.object;
121
- }
122
-
123
- let cheminSchema: types.MemberExpression | types.OptionalMemberExpression = t.memberExpression(
124
- t.identifier('Champs'),
125
- t.identifier('schema')
126
- );
127
- let cheminDonnees: types.MemberExpression | types.OptionalMemberExpression = t.memberExpression(
128
- t.identifier('Champs'),
129
- t.identifier('data')
130
- );
131
- const iDerniereBranche = cheminComposant.length - 1
132
- for (let iBranche = iDerniereBranche; iBranche >= 0; iBranche--) {
133
-
134
- const branche = cheminComposant[ iBranche ];
135
-
136
- cheminSchema = t.optionalMemberExpression(
137
- cheminSchema,
138
- t.identifier( branche ),
139
- undefined,
140
- true
141
- )
142
-
143
- if (iBranche !== iDerniereBranche)
144
- cheminDonnees = t.optionalMemberExpression(
145
- cheminDonnees,
146
- t.identifier(branche),
147
- undefined,
148
- true
149
- )
150
-
151
- }
152
-
153
- // Remplacement
154
- /*
155
- {Champs._render( Champs.metas?.titre, Champs._data.metas?.titre, 'metas.titre', {
156
- className: "full",
157
- attrsChamp: { className: "h1" }
158
- })}
159
- */
160
- const remplacement = t.callExpression(
161
-
162
- // Champs.render
163
- t.memberExpression(
164
- t.identifier('Champs'),
165
- t.identifier('render')
166
- ),
167
- [
168
- // Champs.<chemin>
169
- cheminSchema,
170
-
171
- // Champs._data.<chemin>
172
- cheminDonnees,
173
-
174
- // Chemin
175
- t.stringLiteral( cheminComposant.join('.') ),
176
-
177
- // { <attrs> }
178
- t.objectExpression(objAttributs)
179
- ]
180
- )
181
-
182
- debug && console.log(`[compilation][babel][form] Remplacement: `, generate(remplacement).code );
183
-
184
- instruction.replaceWith(remplacement);
185
-
186
- }
187
- },
188
- };
189
-
190
- return plugin;
191
- }