5htp 0.0.9 → 0.2.0

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.
@@ -0,0 +1,279 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ // Npm
6
+ import * as types from '@babel/types'
7
+ import type { PluginObj, NodePath } from '@babel/core';
8
+ import generate from '@babel/generator';
9
+
10
+ // Core
11
+ import cli from '@cli';
12
+ import App, { TAppSide } from '../../../../app';
13
+
14
+ /*----------------------------------
15
+ - WEBPACK RULE
16
+ ----------------------------------*/
17
+
18
+ type TOptions = {
19
+ side: TAppSide,
20
+ app: App
21
+ }
22
+
23
+ module.exports = (options: TOptions) => (
24
+ [Plugin, options]
25
+ )
26
+
27
+ const debug = true;
28
+
29
+ /*----------------------------------
30
+ - PLUGIN
31
+ ----------------------------------*/
32
+ function Plugin(babel, { app, side }: TOptions) {
33
+
34
+ const t = babel.types as typeof types;
35
+
36
+ /*
37
+ - Wrap route.get(...) with (app: Application) => { }
38
+ - Inject chunk ID into client route options
39
+ */
40
+
41
+ const plugin: PluginObj<{
42
+
43
+ filename: string,
44
+ fileType: 'front' | 'back',
45
+ processFile: boolean,
46
+
47
+ // Identifier => Name
48
+ importedServices: {[local: string]: string},
49
+ routeDefinitions: types.Expression[]
50
+ }> = {
51
+ pre(state) {
52
+
53
+ this.filename = state.opts.filename as string;
54
+ this.processFile = true;
55
+
56
+ if (
57
+ this.filename.startsWith( cli.paths.appRoot + '/src/client/pages' )
58
+ ||
59
+ this.filename.startsWith( cli.paths.coreRoot + '/src/client/pages' )
60
+ ) {
61
+
62
+ this.fileType = 'front';
63
+
64
+ } else if (this.filename.startsWith( cli.paths.appRoot + '/src/server/routes' )) {
65
+
66
+ this.fileType = 'back';
67
+
68
+ } else
69
+ this.processFile = false;
70
+
71
+ this.importedServices = {}
72
+ this.routeDefinitions = []
73
+
74
+ },
75
+ visitor: {
76
+
77
+ // Find @app imports
78
+ // Test: import { router } from '@app';
79
+ // Replace by: nothing
80
+ ImportDeclaration(path) {
81
+
82
+ if (!this.processFile)
83
+ return;
84
+
85
+ if (path.node.source.value !== '@app')
86
+ return;
87
+
88
+ for (const specifier of path.node.specifiers) {
89
+
90
+ if (specifier.type !== 'ImportSpecifier')
91
+ continue;
92
+
93
+ if (specifier.imported.type !== 'Identifier')
94
+ continue;
95
+
96
+ this.importedServices[ specifier.local.name ] = specifier.imported.name;
97
+ }
98
+
99
+ // Remove this import
100
+ path.replaceWithMultiple([]);
101
+ },
102
+
103
+ // Find router definitions
104
+ // Test: router.xxx()
105
+ // Replace by: nothing
106
+ CallExpression(path) {
107
+
108
+ if (!this.processFile)
109
+ return;
110
+
111
+ // Should be at the root of the document
112
+ if (!(
113
+ path.parent.type === 'ExpressionStatement'
114
+ &&
115
+ path.parentPath.parent.type === 'Program'
116
+ ))
117
+ return;
118
+
119
+ // service.method()
120
+ const callee = path.node.callee
121
+ if (!(
122
+ callee.type === 'MemberExpression'
123
+ &&
124
+ callee.object.type === 'Identifier'
125
+ &&
126
+ callee.property.type === 'Identifier'
127
+ &&
128
+ (callee.object.name in this.importedServices)
129
+ ))
130
+ return;
131
+
132
+ // Client route definition: Add chunk id
133
+ let [routePath, ...routeArgs] = path.node.arguments;
134
+ if (this.fileType === 'front' && callee.object.name === 'router') {
135
+
136
+ // Inject chunk id in options (2nd arg)
137
+ const newRouteArgs = injectChunkId(routeArgs, this.filename);
138
+ if (newRouteArgs === 'ALREADY_PROCESSED')
139
+ return;
140
+
141
+ routeArgs = newRouteArgs;
142
+ }
143
+
144
+ // Force babel to create new fresh nodes
145
+ // If we directy use statementParent, it will not be included in the final compiler code
146
+ const statementParent =
147
+ t.callExpression(
148
+ t.memberExpression(
149
+ t.identifier( callee.object.name ),
150
+ callee.property,
151
+ ),
152
+ [routePath, ...routeArgs]
153
+ )
154
+
155
+ this.routeDefinitions.push( statementParent );
156
+
157
+ // Delete this node
158
+ path.replaceWithMultiple([]);
159
+ },
160
+
161
+ // Wrap declarations into a exported const app function
162
+ /*
163
+ export const __register = ({ router }} => {
164
+
165
+ router.page(..)
166
+
167
+ }
168
+ */
169
+ Program: {
170
+ exit: function(path, parent) {
171
+
172
+ const importedServices = Object.entries(this.importedServices);
173
+ if (importedServices.length === 0)
174
+ return;
175
+
176
+ let exportValue: types.Expression | types.BlockStatement;
177
+ if (this.fileType === 'front') {
178
+
179
+ const routesDefCount = this.routeDefinitions.length;
180
+ if (routesDefCount !== 1)
181
+ throw new Error(`Frontend route definition files (/client/pages/**/**.ts) can contain only one route definition.
182
+ ${routesDefCount} were given in ${this.filename}.`);
183
+
184
+ exportValue = this.routeDefinitions[0];
185
+
186
+ } else {
187
+
188
+ exportValue = t.blockStatement([
189
+ // Without spread = react jxx need additionnal loader
190
+ ...this.routeDefinitions.map( def =>
191
+ t.expressionStatement(def)
192
+ ),
193
+ ])
194
+ }
195
+
196
+ const exportDeclaration = t.exportNamedDeclaration(
197
+ t.variableDeclaration('const', [
198
+ t.variableDeclarator(
199
+ t.identifier('__register'),
200
+ t.arrowFunctionExpression(
201
+ [
202
+ t.objectPattern(
203
+ importedServices.map(([ local, imported ]) =>
204
+ t.objectProperty(
205
+ t.identifier( local ),
206
+ t.identifier( imported ),
207
+ )
208
+ )
209
+ )
210
+ ],
211
+ exportValue
212
+ )
213
+ )
214
+ ])
215
+ )
216
+
217
+ // Sans
218
+ // console.log('import app via', this.filename, this.importedServices);
219
+ //debug && console.log( generate(exportDeclaration).code )
220
+ path.pushContainer('body', [exportDeclaration])
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ function injectChunkId(
227
+ routeArgs: types.CallExpression["arguments"],
228
+ filename: string
229
+ ): types.CallExpression["arguments"] | 'ALREADY_PROCESSED' {
230
+
231
+ let [routeOptions, ...otherArgs] = routeArgs;
232
+
233
+ const { filepath, chunkId } = cli.paths.getPageChunk(app, filename);
234
+ debug && console.log(`[routes]`, filename, '=>', chunkId);
235
+
236
+ const newProperties = [
237
+ t.objectProperty(
238
+ t.identifier('id'),
239
+ t.stringLiteral(chunkId)
240
+ ),
241
+ t.objectProperty(
242
+ t.identifier('filepath'),
243
+ t.stringLiteral(filepath)
244
+ )
245
+ ]
246
+
247
+ // No options object
248
+ if (routeOptions.type !== 'ObjectExpression') {
249
+ return [
250
+ t.objectExpression(newProperties),
251
+ ...routeArgs
252
+ ]
253
+ }
254
+
255
+ const wasAlreadyProcessed = routeOptions.properties.some( o =>
256
+ o.type === 'ObjectProperty'
257
+ &&
258
+ o.key.type === 'Identifier'
259
+ &&
260
+ o.key.name === 'id'
261
+ )
262
+
263
+ if (wasAlreadyProcessed) {
264
+ // Cancel processing
265
+ debug && console.log(`[routes]`, filename, 'Already Processed');
266
+ return 'ALREADY_PROCESSED';
267
+ }
268
+
269
+ return [
270
+ t.objectExpression([
271
+ ...routeOptions.properties,
272
+ ...newProperties
273
+ ]),
274
+ ...otherArgs
275
+ ]
276
+ }
277
+
278
+ return plugin;
279
+ }
@@ -80,57 +80,22 @@ function Plugin(babel, { side }: TOptions) {
80
80
  const replacement = t.callExpression( path.node.callee, [ routePath, ...routeArgs ]);
81
81
  debug && console.log( generate(replacement).code );
82
82
 
83
+ path.replaceWith( replacement );
84
+
83
85
  // Force export default
84
- if (path.parent.type === 'ExportDefaultDeclaration')
86
+ // NOTE: now done by app-import.ts
87
+ /*if (path.parent.type === 'ExportDefaultDeclaration')
85
88
  path.replaceWith( replacement );
86
89
  else
87
90
  path.parentPath.replaceWith(
88
91
  t.exportDefaultDeclaration( replacement )
89
- )
92
+ )*/
90
93
 
91
94
  }
92
95
 
93
96
  }
94
97
  };
95
98
 
96
- function addChunkId(
97
- routeArgs: types.CallExpression["arguments"],
98
- filename: string
99
- ): void | 'ALREADY_PROCESSED' {
100
-
101
- if (routeArgs[0].type === 'ObjectExpression') {
102
-
103
- if (routeArgs[0].properties.some(o =>
104
- o.type === 'ObjectProperty'
105
- &&
106
- o.key.type === 'Identifier'
107
- &&
108
- o.key.name === 'id'
109
- )) {
110
- debug && console.log(`[routes]`, filename, 'Already Processed');
111
- return 'ALREADY_PROCESSED';
112
- }
113
-
114
- } else
115
- routeArgs.unshift(t.objectExpression([]));
116
-
117
- const { filepath, chunkId } = cli.paths.getPageChunk(app, filename);
118
- debug && console.log(`[routes]`, filename, '=>', chunkId);
119
-
120
- // Add object property
121
- (routeArgs[0] as types.ObjectExpression).properties.push(
122
- t.objectProperty(
123
- t.identifier('id'),
124
- t.stringLiteral(chunkId)
125
- ),
126
- t.objectProperty(
127
- t.identifier('filepath'),
128
- t.stringLiteral(filepath)
129
- )
130
- );
131
-
132
- }
133
-
134
99
  function addRendererContext(
135
100
  routeArgs: types.CallExpression["arguments"],
136
101
  filename: string
@@ -175,7 +140,7 @@ function Plugin(babel, { side }: TOptions) {
175
140
  'body',
176
141
  t.importDeclaration(
177
142
  [t.importDefaultSpecifier(t.identifier('useContext'))],
178
- t.stringLiteral('@client/context')
143
+ t.stringLiteral('@/client/context')
179
144
  )
180
145
  );
181
146
  }
@@ -20,7 +20,7 @@ export const fixNpmLinkIssues = ( app: App ) => {
20
20
 
21
21
  const corePath = path.join(app.paths.root, '/node_modules/5htp-core');
22
22
  if (!fs.lstatSync( corePath ).isSymbolicLink())
23
- return;
23
+ return console.info("Not fixing npm issue because 5htp-core wasn't installed with npm link.");
24
24
 
25
25
  console.info(`Fix NPM link issues ...`);
26
26
 
@@ -42,5 +42,5 @@ export const fixNpmLinkIssues = ( app: App ) => {
42
42
  if (!fs.existsSync( preactAppModule ))
43
43
  fs.symlinkSync( preactCoreModule, preactAppModule );
44
44
  if (!fs.existsSync( reactAppModule ))
45
- fs.symlinkSync( preactCoreModule, reactAppModule );
45
+ fs.symlinkSync( path.join(preactCoreModule, 'compat'), reactAppModule );
46
46
  }
@@ -101,6 +101,12 @@ export default async function createCompilers(
101
101
  const time = timeEnd.getTime() - timeStart.getTime();
102
102
  if (stats.hasErrors()) {
103
103
  console.error(`############## Failed to compile '${name}' after ${time} ms`);
104
+
105
+ // Exit process with code 0, so the CI container can understand building failed
106
+ // Only in prod, because in dev, we want the compiler watcher continue running
107
+ if (mode === 'prod')
108
+ process.exit(0);
109
+
104
110
  } else {
105
111
  console.info(`############## [${name}] Finished compilation after ${time} ms`);
106
112
  }
@@ -43,11 +43,16 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
43
43
  const dev = mode === 'dev';
44
44
 
45
45
  const commonConfig = createCommonConfig(app, 'server', mode);
46
- const { aliases } = app.aliases.server.forWebpack(app.paths.root + '/node_modules');
46
+ const { aliases } = app.aliases.server.forWebpack({
47
+ modulesPath: app.paths.root + '/node_modules'
48
+ });
49
+
50
+ // We're not supposed in any case to import client services from server
51
+ delete aliases["@client/services"];
52
+ delete aliases["@/client/services"];
47
53
 
48
54
  console.log(`[${mode}] node_modules dirs:`, commonConfig.resolveLoader?.modules,
49
- '\nModule aliases:', aliases);
50
-
55
+ '\nModule aliases for webpack:', aliases);
51
56
  const config: webpack.Configuration = {
52
57
 
53
58
  ...commonConfig,
@@ -56,7 +61,7 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
56
61
  target: 'node',
57
62
  entry: {
58
63
  server: [
59
- app.paths.root + '/src/server/index.ts',
64
+ cli.paths.coreRoot + '/src/server/index.ts'
60
65
  ],
61
66
  },
62
67
 
@@ -115,10 +120,7 @@ export default function createCompiler( app: App, mode: TCompileMode ): webpack.
115
120
 
116
121
  ...commonConfig.resolve,
117
122
 
118
- alias: {
119
- ...aliases,
120
- "@root": app.paths.root,
121
- },
123
+ alias: aliases,
122
124
 
123
125
  extensions: ['.ts', '.tsx', ".json", ".sql"],
124
126
  },
package/src/index.ts CHANGED
@@ -19,6 +19,10 @@ type TCliCommand = () => Promise<{
19
19
  run: () => Promise<void>
20
20
  }>
21
21
 
22
+ type TArgsObject = {
23
+ [key: string]: string | boolean | string[]
24
+ }
25
+
22
26
  /*----------------------------------
23
27
  - CLASSE
24
28
  ----------------------------------*/
@@ -28,7 +32,7 @@ type TCliCommand = () => Promise<{
28
32
  export class CLI {
29
33
 
30
34
  // Context
31
- public args: TObjetDonnees = {};
35
+ public args: TArgsObject = {};
32
36
 
33
37
  public constructor(
34
38
  public paths = new Paths( process.cwd() )
@@ -98,7 +102,7 @@ export class CLI {
98
102
  this.runCommand(commandName, options);
99
103
  }
100
104
 
101
- public async runCommand(command: string, args: TObjetDonnees) {
105
+ public async runCommand(command: string, args: TArgsObject) {
102
106
 
103
107
  this.args = args;
104
108
 
package/src/paths.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import path from 'path';
7
7
  import TsAlias from 'ts-alias';
8
8
  import moduleAlias from 'module-alias';
9
+ import { filenameToImportName } from 'babel-plugin-glob-import';
9
10
 
10
11
  // Core
11
12
 
@@ -16,6 +17,13 @@ import moduleAlias from 'module-alias';
16
17
  import type App from './app';
17
18
  import type { TAppSide } from './app';
18
19
 
20
+ export type TPathInfosOptions = {
21
+ basePath?: string,
22
+ shortenExtensions: string[],
23
+ // Indexed will be trimed only when the extension can be shorten
24
+ trimIndex: boolean,
25
+ }
26
+
19
27
  export type TPathInfos = {
20
28
 
21
29
  original: string,
@@ -28,8 +36,17 @@ export type TPathInfos = {
28
36
  isIndex: boolean
29
37
  }
30
38
 
39
+ /*----------------------------------
40
+ - CONFIG
41
+ ----------------------------------*/
42
+
31
43
  export const staticAssetName = /*isDebug ? '[name].[ext].[hash:8]' :*/ '[hash:8][ext]';
32
44
 
45
+ const pathInfosDefaultOpts = {
46
+ shortenExtensions: ['ts', 'js', 'tsx', 'jsx'],
47
+ trimIndex: true,
48
+ }
49
+
33
50
  /*----------------------------------
34
51
  - LIB
35
52
  ----------------------------------*/
@@ -57,18 +74,20 @@ export default class Paths {
57
74
  - EXTRACTION
58
75
  ----------------------------------*/
59
76
 
60
- public infos(filename: string, basePath?: string, side: TAppSide = 'server'): TPathInfos {
77
+ public infos(filename: string, givenOpts: Partial<TPathInfosOptions> = {}): TPathInfos {
78
+
79
+ const opts: TPathInfosOptions = { ...pathInfosDefaultOpts, ...givenOpts }
61
80
 
62
81
  // Extraction élements du chemin
63
82
  const decomp = filename.split('/')
64
83
  let [nomFichier, extension] = (decomp.pop() as string).split('.');
65
- const raccourcir = ['ts', 'js', 'tsx', 'jsx'].includes(extension);
84
+ const shortenExtension = opts.shortenExtensions && opts.shortenExtensions.includes(extension);
66
85
 
67
86
  // Vire l'index
68
87
  const isIndex = nomFichier === 'index'
69
88
  let cheminAbsolu: string;
70
89
  let nomReel: string;
71
- if (isIndex && raccourcir) {
90
+ if (isIndex && shortenExtension && opts.trimIndex) {
72
91
  cheminAbsolu = decomp.join('/');
73
92
  nomReel = decomp.pop() as string;
74
93
  } else {
@@ -77,12 +96,12 @@ export default class Paths {
77
96
  }
78
97
 
79
98
  // Conserve l'extension si nécessaire
80
- if (!raccourcir)
99
+ if (!shortenExtension)
81
100
  cheminAbsolu += '.' + extension;
82
101
 
83
- const relative = basePath === undefined
102
+ const relative = opts.basePath === undefined
84
103
  ? ''
85
- : cheminAbsolu.substring( basePath.length + 1 )
104
+ : cheminAbsolu.substring( opts.basePath.length + 1 )
86
105
 
87
106
  // Retour
88
107
  const retour = {
@@ -104,16 +123,17 @@ export default class Paths {
104
123
 
105
124
  public getPageChunk( app: App, file: string ) {
106
125
 
107
- const infos = this.infos( file, file.startsWith( app.paths.pages )
108
- ? app.paths.pages
109
- : this.core.pages,
110
- );
126
+ const infos = this.infos( file, {
127
+ basePath: file.startsWith( app.paths.pages ) ? app.paths.pages : this.core.pages,
128
+ // Avoid potential conflicts between /landing.tsx and /landing/index.tsx
129
+ trimIndex: false,
130
+ });
111
131
 
112
132
  const filepath = infos.relative;
113
133
 
114
134
  // Before: /home/.../src/client/pages/landing/index.tsx
115
135
  // After: landing_index
116
- let chunkId = filepath.replace(/\//g, '_');
136
+ let chunkId = filenameToImportName(filepath);
117
137
 
118
138
  // nsure it's non-empty
119
139
  if (chunkId.length === 0) // = /index.tsx