5htp 0.6.3 → 0.6.4-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
@@ -56,7 +56,13 @@ export class App {
56
56
  public paths = {
57
57
 
58
58
  root: cli.paths.appRoot,
59
- bin: path.join( cli.paths.appRoot, 'bin'),
59
+ binBuild: path.join( cli.paths.appRoot, 'bin'),
60
+ binDev: path.join( cli.paths.appRoot, 'bin-dev'),
61
+ get bin() {
62
+ return cli.commandName === 'dev'
63
+ ? this.binDev
64
+ : this.binBuild;
65
+ },
60
66
  data: path.join( cli.paths.appRoot, 'var', 'data'),
61
67
  public: path.join( cli.paths.appRoot, 'public'),
62
68
  pages: path.join( cli.paths.appRoot, 'client', 'pages'),
@@ -192,4 +198,4 @@ export class App {
192
198
 
193
199
  export const app = new App
194
200
 
195
- export default app
201
+ export default app
package/cli.js ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ console.log('=== Running 5HTP via ts-node ===');
4
+
5
+ const path = require('path');
6
+ const tsNode = require('ts-node');
7
+
8
+ tsNode.register({
9
+ project: path.join(__dirname, 'tsconfig.json'),
10
+ transpileOnly: true,
11
+ });
12
+
13
+ require('./index.ts');
package/commands/build.ts CHANGED
@@ -2,22 +2,35 @@
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
4
 
5
- // Npm
6
- import prompts from 'prompts';
7
-
8
5
  // Configs
6
+ import cli from '..';
9
7
  import Compiler from '../compiler';
8
+ import type { TCompileMode } from '../compiler/common';
10
9
 
11
10
  /*----------------------------------
12
11
  - TYPES
13
12
  ----------------------------------*/
14
13
 
14
+ const buildModes: TCompileMode[] = ['dev', 'prod'];
15
+
16
+ function getBuildMode(): TCompileMode {
17
+
18
+ const selectedModes = buildModes.filter(mode => cli.args[mode] === true);
19
+
20
+ if (selectedModes.length > 1)
21
+ throw new Error(`Build mode is ambiguous. Use only one mode among: ${buildModes.join(', ')}`);
22
+
23
+ return selectedModes[0] || 'dev';
24
+ }
25
+
15
26
  /*----------------------------------
16
27
  - COMMAND
17
28
  ----------------------------------*/
18
- export const run = (): Promise<void> => new Promise(async (resolve) => {
29
+ export const run = (): Promise<void> => new Promise(async (resolve, reject) => {
19
30
 
20
- const compiler = new Compiler('prod');
31
+ const mode = getBuildMode();
32
+
33
+ const compiler = new Compiler(mode);
21
34
 
22
35
  const multiCompiler = await compiler.create();
23
36
 
@@ -25,10 +38,24 @@ export const run = (): Promise<void> => new Promise(async (resolve) => {
25
38
 
26
39
  if (error) {
27
40
  console.error("An error occurred during the compilation:", error);
28
- throw error;
41
+ reject(error);
42
+ return;
43
+ }
44
+
45
+ if (stats?.hasErrors()) {
46
+ reject(new Error(`Build failed in ${mode} mode.`));
47
+ return;
29
48
  }
30
49
 
31
- //resolve();
50
+ multiCompiler.close((closeError) => {
51
+
52
+ if (closeError) {
53
+ reject(closeError);
54
+ return;
55
+ }
56
+
57
+ resolve();
58
+ });
32
59
 
33
60
  });
34
- });
61
+ });
package/commands/dev.ts CHANGED
@@ -2,12 +2,8 @@
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
4
 
5
- // Npm
6
- import fs from 'fs-extra';
7
5
  import { spawn, ChildProcess } from 'child_process';
8
6
 
9
- // Cor elibs
10
- import cli from '..';
11
7
  import Keyboard from '../utils/keyboard';
12
8
 
13
9
  // Configs
@@ -16,6 +12,20 @@ import Compiler from '../compiler';
16
12
  // Core
17
13
  import { app, App } from '../app';
18
14
 
15
+ const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
+
17
+ const ignoredOutputs = [app.paths.binBuild, app.paths.binDev]
18
+ .map(outputPath => escapeRegExp(outputPath.replace(/\\/g, '/')) + '(/|$)')
19
+ .join('|');
20
+
21
+ const ignoredFiles = new RegExp(
22
+ [
23
+ '(node_modules\\/(?!5htp\\-core\\/))',
24
+ '(\\.generated\\/)',
25
+ `(${ignoredOutputs})`,
26
+ ].join('|')
27
+ );
28
+
19
29
  /*----------------------------------
20
30
  - COMMANDE
21
31
  ----------------------------------*/
@@ -51,7 +61,8 @@ export const run = () => new Promise<void>(async () => {
51
61
  // Ignore updated from:
52
62
  // - Node modules except 5HTP core (framework dev mode)
53
63
  // - Generated files during runtime (cause infinite loop. Ex: models.d.ts)
54
- ignored: /(node_modules\/(?!5htp\-core\/))|(\.generated\/)/
64
+ // - Build outputs
65
+ ignored: ignoredFiles
55
66
 
56
67
  //aggregateTimeout: 1000,
57
68
  }, async (error, stats) => {
@@ -106,4 +117,4 @@ function stopApp( reason: string ) {
106
117
  cp.kill();
107
118
  }
108
119
 
109
- }
120
+ }
@@ -0,0 +1,18 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ // Configs
6
+ import Compiler from '../compiler';
7
+
8
+ /*----------------------------------
9
+ - COMMAND
10
+ ----------------------------------*/
11
+ export const run = (): Promise<void> => new Promise(async (resolve) => {
12
+
13
+ const compiler = new Compiler('dev');
14
+
15
+ await compiler.refreshGeneratedTypings();
16
+
17
+ resolve();
18
+ });
@@ -11,9 +11,6 @@ const TerserPlugin = require('terser-webpack-plugin');
11
11
  // Optimisations
12
12
  const BrotliCompression = require("brotli-webpack-plugin");
13
13
  import CompressionPlugin from "compression-webpack-plugin";
14
- const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
15
- const imageminWebp = require('imagemin-webp');
16
- const { extendDefaultPlugins } = require("svgo");
17
14
  // Ressources
18
15
  const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
19
16
  import MiniCssExtractPlugin from "mini-css-extract-plugin";
@@ -38,6 +35,7 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
38
35
 
39
36
  console.info(`Creating compiler for client (${mode}).`);
40
37
  const dev = mode === 'dev';
38
+ const buildCheck = dev && cli.commandName === 'build';
41
39
 
42
40
  const commonConfig = createCommonConfig(app, 'client', mode);
43
41
 
@@ -81,7 +79,7 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
81
79
 
82
80
  output: {
83
81
 
84
- pathinfo: dev,
82
+ pathinfo: dev && !buildCheck,
85
83
  path: app.paths.bin + '/public',
86
84
  filename: '[name].js', // Output client.js
87
85
  assetModuleFilename: '[hash][ext]',
@@ -160,7 +158,7 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
160
158
  // Emit a file with assets cli.paths
161
159
  // https://github.com/webdeveric/webpack-assets-manifest#options
162
160
  new WebpackAssetsManifest({
163
- output: app.paths.root + `/bin/asset-manifest.json`,
161
+ output: app.paths.bin + `/asset-manifest.json`,
164
162
  publicPath: true,
165
163
  writeToDisk: true, // Force la copie du fichier sur e disque, au lieu d'en mémoire en mode dev
166
164
  customize: ({ key, value }) => {
@@ -170,7 +168,7 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
170
168
  },
171
169
  done: (manifest, stats) => {
172
170
  // Write chunk-manifest.json.json
173
- const chunkFileName = app.paths.root + `/bin/chunk-manifest.json`;
171
+ const chunkFileName = app.paths.bin + `/chunk-manifest.json`;
174
172
  try {
175
173
  const fileFilter = file => !file.endsWith('.map');
176
174
  const addPath = file => manifest.getPublicPath(file);
@@ -229,7 +227,7 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
229
227
  ],
230
228
 
231
229
  // https://webpack.js.org/configuration/devtool/#devtool
232
- devtool: 'source-map',
230
+ devtool: buildCheck ? false : 'source-map',
233
231
  /*devServer: {
234
232
  hot: true,
235
233
  },*/
@@ -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