5htp 0.6.2 → 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.
package/app/index.ts CHANGED
@@ -69,7 +69,10 @@ export class App {
69
69
  generated: path.join( cli.paths.appRoot, 'server', '.generated'),
70
70
  configs: path.join( cli.paths.appRoot, 'server', 'app')
71
71
  },
72
-
72
+ common: {
73
+ generated: path.join( cli.paths.appRoot, 'common', '.generated')
74
+ },
75
+
73
76
  withAlias: (filename: string, side: TAppSide) =>
74
77
  this.aliases[side].apply(filename),
75
78
 
@@ -126,7 +126,7 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
126
126
  // On ne compile les ressources (css) qu'une seule fois
127
127
  {
128
128
  test: regex.style,
129
- rules: require('../common/files/style')(app, true, dev),
129
+ rules: require('../common/files/style')(app, dev, true),
130
130
 
131
131
  // Don't consider CSS imports dead code even if the
132
132
  // containing package claims to have no side effects.
@@ -331,4 +331,4 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
331
331
  };
332
332
 
333
333
  return config;
334
- };
334
+ };
@@ -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
 
@@ -53,6 +53,13 @@ function Plugin(babel, { app, side, debug }: TOptions) {
53
53
 
54
54
  const t = babel.types as typeof types;
55
55
 
56
+ type TPluginState = {
57
+ filename: string,
58
+ file: TFileInfos,
59
+ apiInjectedRootFunctions: WeakSet<types.Node>,
60
+ needsUseContextImport: boolean
61
+ }
62
+
56
63
  /*
57
64
  - Wrap route.get(...) with (app: Application) => { }
58
65
  - Inject chunk ID into client route options
@@ -70,14 +77,14 @@ function Plugin(babel, { app, side, debug }: TOptions) {
70
77
  const stats = page.data.stats;
71
78
  */
72
79
 
73
- const plugin: PluginObj<{
74
- filename: string,
75
- file: TFileInfos
76
- }> = {
80
+ const plugin: PluginObj<TPluginState> = {
77
81
  pre(state) {
78
82
  this.filename = state.opts.filename as string;
79
83
 
80
84
  this.file = getFileInfos(this.filename);
85
+
86
+ this.apiInjectedRootFunctions = new WeakSet();
87
+ this.needsUseContextImport = false;
81
88
  },
82
89
  visitor: {
83
90
  // Find @app imports
@@ -203,6 +210,8 @@ function Plugin(babel, { app, side, debug }: TOptions) {
203
210
  */
204
211
  if (side === 'client' && !clientServices.includes(serviceName)) {
205
212
 
213
+ ensureApiExposedInRootFunction(path, this);
214
+
206
215
  // Get complete call path
207
216
  const apiPath = '/api/' + completePath.join('/');
208
217
 
@@ -255,6 +264,8 @@ function Plugin(babel, { app, side, debug }: TOptions) {
255
264
 
256
265
  if (!this.file.process)
257
266
  return;
267
+
268
+ ensureUseContextImport(path, this);
258
269
 
259
270
  const wrappedrouteDefs = wrapRouteDefs( this.file );
260
271
  if (wrappedrouteDefs)
@@ -264,6 +275,187 @@ function Plugin(babel, { app, side, debug }: TOptions) {
264
275
  }
265
276
  }
266
277
 
278
+ function ensureApiExposedInRootFunction(
279
+ path: NodePath<types.CallExpression>,
280
+ pluginState: TPluginState
281
+ ) {
282
+ if (path.scope.hasBinding('api'))
283
+ return;
284
+
285
+ const rootFunctionPath = getRootFunctionPath(path);
286
+ if (!rootFunctionPath)
287
+ return;
288
+
289
+ // Root function should be at the program body level (not nested in another function / expression)
290
+ if (rootFunctionPath.getFunctionParent())
291
+ return;
292
+ if (!isProgramBodyLevelFunction(rootFunctionPath))
293
+ return;
294
+
295
+ if (pluginState.apiInjectedRootFunctions.has(rootFunctionPath.node))
296
+ return;
297
+
298
+ const exposeApiDeclaration = t.variableDeclaration('const', [
299
+ t.variableDeclarator(
300
+ t.objectPattern([
301
+ t.objectProperty(t.identifier('api'), t.identifier('api'), false, true),
302
+ ]),
303
+ t.callExpression(t.identifier('useContext'), [])
304
+ )
305
+ ]);
306
+
307
+ const body = rootFunctionPath.node.body;
308
+ if (body.type === 'BlockStatement') {
309
+ body.body.unshift(exposeApiDeclaration);
310
+ } else {
311
+ rootFunctionPath.node.body = t.blockStatement([
312
+ exposeApiDeclaration,
313
+ t.returnStatement(body)
314
+ ]);
315
+ }
316
+
317
+ pluginState.apiInjectedRootFunctions.add(rootFunctionPath.node);
318
+ pluginState.needsUseContextImport = true;
319
+ }
320
+
321
+ function getRootFunctionPath(path: NodePath): NodePath<types.Function | types.ArrowFunctionExpression> | undefined {
322
+
323
+ let functionPath = path.getFunctionParent();
324
+ if (!functionPath)
325
+ return;
326
+
327
+ // Only support plain functions / arrow functions (no class/object methods)
328
+ if (!(
329
+ functionPath.isFunctionDeclaration()
330
+ || functionPath.isFunctionExpression()
331
+ || functionPath.isArrowFunctionExpression()
332
+ ))
333
+ return;
334
+
335
+ let parentFunction = functionPath.getFunctionParent();
336
+ while (parentFunction) {
337
+
338
+ if (!(
339
+ parentFunction.isFunctionDeclaration()
340
+ || parentFunction.isFunctionExpression()
341
+ || parentFunction.isArrowFunctionExpression()
342
+ ))
343
+ break;
344
+
345
+ functionPath = parentFunction;
346
+ parentFunction = functionPath.getFunctionParent();
347
+ }
348
+
349
+ return functionPath;
350
+ }
351
+
352
+ function isProgramBodyLevelFunction(path: NodePath): boolean {
353
+
354
+ const parent = path.parentPath;
355
+ if (!parent)
356
+ return false;
357
+
358
+ // function Foo() {}
359
+ if (parent.isProgram())
360
+ return true;
361
+
362
+ // export default function Foo() {} / export default () => {}
363
+ if (
364
+ parent.isExportDefaultDeclaration()
365
+ &&
366
+ parent.parentPath?.isProgram()
367
+ )
368
+ return true;
369
+
370
+ // export const Foo = () => {}
371
+ if (
372
+ parent.isExportNamedDeclaration()
373
+ &&
374
+ parent.parentPath?.isProgram()
375
+ )
376
+ return true;
377
+
378
+ // const Foo = () => {} (top-level) / export const Foo = () => {}
379
+ if (parent.isVariableDeclarator()) {
380
+
381
+ const declaration = parent.parentPath;
382
+ if (!declaration?.isVariableDeclaration())
383
+ return false;
384
+
385
+ const declarationParent = declaration.parentPath;
386
+ if (!declarationParent)
387
+ return false;
388
+
389
+ if (declarationParent.isProgram())
390
+ return true;
391
+
392
+ if (
393
+ declarationParent.isExportNamedDeclaration()
394
+ &&
395
+ declarationParent.parentPath?.isProgram()
396
+ )
397
+ return true;
398
+ }
399
+
400
+ return false;
401
+ }
402
+
403
+ function ensureUseContextImport(path: NodePath<types.Program>, pluginState: TPluginState) {
404
+
405
+ if (!pluginState.needsUseContextImport)
406
+ return;
407
+
408
+ const body = path.node.body;
409
+
410
+ // Already imported as a value import
411
+ for (const stmt of body) {
412
+ if (
413
+ stmt.type === 'ImportDeclaration'
414
+ &&
415
+ stmt.source.value === '@/client/context'
416
+ &&
417
+ stmt.importKind !== 'type'
418
+ &&
419
+ stmt.specifiers.some(s =>
420
+ s.type === 'ImportDefaultSpecifier' && s.local.name === 'useContext'
421
+ )
422
+ )
423
+ return;
424
+ }
425
+
426
+ // Try to reuse an existing value import from the same module
427
+ for (const stmt of body) {
428
+ if (
429
+ stmt.type !== 'ImportDeclaration'
430
+ ||
431
+ stmt.source.value !== '@/client/context'
432
+ ||
433
+ stmt.importKind === 'type'
434
+ )
435
+ continue;
436
+
437
+ const hasDefaultImport = stmt.specifiers.some(s => s.type === 'ImportDefaultSpecifier');
438
+ if (!hasDefaultImport) {
439
+ stmt.specifiers.unshift(
440
+ t.importDefaultSpecifier(t.identifier('useContext'))
441
+ );
442
+ return;
443
+ }
444
+ }
445
+
446
+ // Otherwise, add a new import (placed after existing imports)
447
+ const importDeclaration = t.importDeclaration(
448
+ [t.importDefaultSpecifier(t.identifier('useContext'))],
449
+ t.stringLiteral('@/client/context')
450
+ );
451
+
452
+ let insertIndex = 0;
453
+ while (insertIndex < body.length && body[insertIndex].type === 'ImportDeclaration')
454
+ insertIndex++;
455
+
456
+ body.splice(insertIndex, 0, importDeclaration);
457
+ }
458
+
267
459
  function getFileInfos( filename: string ): TFileInfos {
268
460
 
269
461
  const file: TFileInfos = {
@@ -290,7 +482,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
290
482
  }
291
483
 
292
484
  // Differenciate back / front
293
- if (relativeFileName.startsWith('/client/pages')) {
485
+ if (relativeFileName.startsWith('/client/pages') || relativeFileName.startsWith('/client/components') || relativeFileName.startsWith('/client/hooks')) {
294
486
  file.side = 'front';
295
487
  } else if (relativeFileName.startsWith('/server/routes')) {
296
488
  file.side = 'back';
@@ -302,7 +494,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
302
494
 
303
495
  function transformDataFetchers(
304
496
  path: NodePath<types.CallExpression>,
305
- routerDefContext: PluginObj,
497
+ routerDefContext: TPluginState,
306
498
  routeDef: TRouteDefinition
307
499
  ) {
308
500
  path.traverse({
@@ -638,4 +830,4 @@ function Plugin(babel, { app, side, debug }: TOptions) {
638
830
  }
639
831
 
640
832
  return plugin;
641
- }
833
+ }
@@ -24,7 +24,7 @@ module.exports = (app: App, dev: boolean, client: boolean) => ([
24
24
  // Texte brut
25
25
  {
26
26
  type: 'asset/source',
27
- test: /\.(md|hbs|sql|txt|csv)$/,
27
+ test: /\.(md|hbs|sql|txt|csv|html)$/,
28
28
  },
29
29
 
30
30
  // Polices dans un fichier distinc dans le dossier dédié