alepha 0.11.10 → 0.11.11

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,6 +1,7 @@
1
1
  {
2
2
  "name": "alepha",
3
- "version": "0.11.10",
3
+ "description": "Easy-to-use modern TypeScript framework for building many kind of applications.",
4
+ "version": "0.11.11",
4
5
  "type": "module",
5
6
  "engines": {
6
7
  "node": ">=22.0.0"
@@ -15,7 +16,7 @@
15
16
  ],
16
17
  "bin": "./dist/index.js",
17
18
  "dependencies": {
18
- "@alepha/postgres": "^0.11.10",
19
+ "@alepha/orm": "^0.11.11",
19
20
  "@biomejs/biome": "^2.3.5",
20
21
  "drizzle-kit": "^0.31.7",
21
22
  "tar": "^7.5.2",
@@ -25,9 +26,9 @@
25
26
  "vitest": "^4.0.9"
26
27
  },
27
28
  "devDependencies": {
28
- "@alepha/command": "^0.11.10",
29
- "@alepha/core": "^0.11.10",
30
- "@alepha/logger": "^0.11.10"
29
+ "@alepha/command": "^0.11.11",
30
+ "@alepha/core": "^0.11.11",
31
+ "@alepha/logger": "^0.11.11"
31
32
  },
32
33
  "scripts": {
33
34
  "lint": "biome check --fix",
@@ -40,7 +41,12 @@
40
41
  "url": "git+https://github.com/feunard/alepha.git"
41
42
  },
42
43
  "keywords": [
43
- "alepha"
44
+ "alepha",
45
+ "aleph",
46
+ "framework",
47
+ "serverless",
48
+ "react",
49
+ "api"
44
50
  ],
45
51
  "module": "./dist/index.js",
46
52
  "exports": {
@@ -10,6 +10,7 @@ export const tsconfigJson = `
10
10
  "moduleDetection": "force",
11
11
  "skipLibCheck": true,
12
12
  "resolveJsonModule": true,
13
+ "noEmit": true,
13
14
  "allowImportingTsExtensions": true
14
15
  },
15
16
  "exclude": ["node_modules", "dist"]
@@ -1,14 +1,14 @@
1
- import { access } from "node:fs/promises";
2
- import { join } from "node:path";
3
1
  import { $command } from "@alepha/command";
4
2
  import { $inject, t } from "@alepha/core";
5
3
  import { $logger } from "@alepha/logger";
6
- import { biomeJson } from "../assets/biomeJson.ts";
7
4
  import { ProcessRunner } from "../services/ProcessRunner.ts";
5
+ import { ProjectUtils } from "../services/ProjectUtils.ts";
8
6
 
9
7
  export class BiomeCommands {
10
8
  protected readonly log = $logger();
11
9
  protected readonly runner = $inject(ProcessRunner);
10
+ protected readonly utils = $inject(ProjectUtils);
11
+
12
12
  protected readonly biomeFlags = t.object({
13
13
  config: t.optional(
14
14
  t.text({
@@ -22,7 +22,7 @@ export class BiomeCommands {
22
22
  description: "Format the codebase using Biome",
23
23
  flags: this.biomeFlags,
24
24
  handler: async ({ flags }) => {
25
- const configPath = await this.configPath(flags.config);
25
+ const configPath = await this.utils.getBiomeConfigPath(flags.config);
26
26
  await this.runner.exec(`biome format --fix --config-path=${configPath}`);
27
27
  },
28
28
  });
@@ -32,29 +32,10 @@ export class BiomeCommands {
32
32
  description: "Run linter across the codebase using Biome",
33
33
  flags: this.biomeFlags,
34
34
  handler: async ({ flags }) => {
35
- const configPath = await this.configPath(flags.config);
35
+ const configPath = await this.utils.getBiomeConfigPath(flags.config);
36
36
  await this.runner.exec(
37
37
  `biome check --formatter-enabled=false --fix --config-path=${configPath}`,
38
38
  );
39
39
  },
40
40
  });
41
-
42
- protected async configPath(maybePath?: string): Promise<string> {
43
- const root = process.cwd();
44
- if (maybePath) {
45
- try {
46
- const path = join(root, maybePath);
47
- await access(path);
48
- return path;
49
- } catch {}
50
- }
51
-
52
- try {
53
- const path = join(root, "biome.json");
54
- await access(path);
55
- return path;
56
- } catch {
57
- return await this.runner.writeConfigFile("biome.json", biomeJson);
58
- }
59
- }
60
41
  }
@@ -1,18 +1,19 @@
1
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { access, mkdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { Readable } from "node:stream";
4
3
  import { $command, CliProvider } from "@alepha/command";
5
- import { $inject, AlephaError, t } from "@alepha/core";
4
+ import { $inject, t } from "@alepha/core";
6
5
  import { $logger } from "@alepha/logger";
7
- import { pipeline } from "stream/promises";
8
- import * as tar from "tar";
9
- import { tsconfigJson } from "../assets/tsconfigJson.ts";
6
+ import { ProjectUtils } from "../services/ProjectUtils.ts";
10
7
  import { version } from "../version.ts";
11
8
 
12
9
  export class CoreCommands {
13
10
  protected readonly log = $logger();
14
11
  protected readonly cli = $inject(CliProvider);
12
+ protected readonly utils = $inject(ProjectUtils);
15
13
 
14
+ /**
15
+ * Called when no command is provided
16
+ */
16
17
  public readonly root = $command({
17
18
  root: true,
18
19
  flags: t.object({
@@ -33,6 +34,9 @@ export class CoreCommands {
33
34
  },
34
35
  });
35
36
 
37
+ /**
38
+ * Create a new Alepha project based on one of the sample projects (for now, only one sample project is available)
39
+ */
36
40
  public readonly create = $command({
37
41
  name: "create",
38
42
  description: "Create a new Alepha project",
@@ -45,9 +49,8 @@ export class CoreCommands {
45
49
  pnpm: t.optional(t.boolean({ description: "Use pnpm package manager" })),
46
50
  }),
47
51
  summary: false,
48
- handler: async ({ run, args, flags }) => {
52
+ handler: async ({ run, args, flags, root }) => {
49
53
  const name = args;
50
- const root = process.cwd();
51
54
  const dest = join(root, name);
52
55
 
53
56
  try {
@@ -73,11 +76,11 @@ export class CoreCommands {
73
76
  await mkdir(dest, { recursive: true }).catch(() => null);
74
77
 
75
78
  await run("Downloading sample project", () =>
76
- this.downloadSampleProject(dest),
79
+ this.utils.downloadSampleProject(dest),
77
80
  );
78
81
 
79
82
  if (flags.yarn) {
80
- await this.ensureYarn(dest);
83
+ await this.utils.ensureYarn(dest);
81
84
  await run(`cd ${name} && yarn set version stable`, {
82
85
  alias: "Setting Yarn to stable version",
83
86
  });
@@ -109,6 +112,9 @@ export class CoreCommands {
109
112
  },
110
113
  });
111
114
 
115
+ /**
116
+ * Clean the project, removing the "dist" directory
117
+ */
112
118
  public readonly clean = $command({
113
119
  name: "clean",
114
120
  description: "Clean the project",
@@ -117,6 +123,10 @@ export class CoreCommands {
117
123
  },
118
124
  });
119
125
 
126
+ /**
127
+ * Ensure the project has the necessary Alepha configuration files.
128
+ * Add the correct dependencies to package.json and install them.
129
+ */
120
130
  public readonly init = $command({
121
131
  name: "init",
122
132
  description: "Add missing Alepha configuration files to the project",
@@ -125,18 +135,30 @@ export class CoreCommands {
125
135
  // force: t.boolean({
126
136
  // description: "If true, all config files will be overwritten",
127
137
  // }),
128
- yarn: t.boolean({ description: "Use Yarn package manager" }),
129
- api: t.boolean({ description: "Include Alepha Server dependencies" }),
130
- react: t.boolean({ description: "Include Alepha React dependencies" }),
138
+ // choose package manager
139
+ yarn: t.optional(t.boolean({ description: "Use Yarn package manager" })),
140
+ // choose which dependencies to add
141
+ api: t.optional(
142
+ t.boolean({ description: "Include Alepha Server dependencies" }),
143
+ ),
144
+ react: t.optional(
145
+ t.boolean({ description: "Include Alepha React dependencies" }),
146
+ ),
147
+ orm: t.optional(
148
+ t.boolean({ description: "Include Alepha ORM dependencies" }),
149
+ ),
131
150
  }),
132
- handler: async ({ run, flags }) => {
133
- const root = process.cwd();
134
-
135
- await this.ensureTsConfig(root);
136
- await this.ensurePackageJson(root, flags);
151
+ handler: async ({ run, flags, root }) => {
152
+ await run("Ensuring Alepha configuration files", async () => {
153
+ await this.utils.ensureTsConfig(root);
154
+ await this.utils.ensurePackageJson(root, flags);
155
+ });
137
156
 
138
157
  if (flags.yarn) {
139
- await this.ensureYarn(root);
158
+ await this.utils.ensureYarn(root);
159
+ await run("yarn install", {
160
+ alias: "Installing dependencies with Yarn",
161
+ });
140
162
  } else {
141
163
  await run("npm install", {
142
164
  alias: "Installing dependencies with npm",
@@ -144,123 +166,4 @@ export class CoreCommands {
144
166
  }
145
167
  },
146
168
  });
147
-
148
- public async ensureYarn(root: string) {
149
- const tsconfigPath = join(root, ".yarnrc.yml");
150
- try {
151
- await access(tsconfigPath);
152
- } catch {
153
- await writeFile(tsconfigPath, "nodeLinker: node-modules");
154
- }
155
- }
156
-
157
- public generatePackageJsonContent(modes: { api?: boolean; react?: boolean }) {
158
- const dependencies: Record<string, string> = {
159
- "@alepha/core": `^${version}`,
160
- "@alepha/logger": `^${version}`,
161
- "@alepha/datetime": `^${version}`,
162
- };
163
-
164
- const devDependencies: Record<string, string> = {
165
- alepha: `^${version}`,
166
- "@alepha/vite": `^${version}`,
167
- };
168
-
169
- if (modes.api) {
170
- dependencies["@alepha/server"] = `^${version}`;
171
- dependencies["@alepha/server-swagger"] = `^${version}`;
172
- dependencies["@alepha/server-multipart"] = `^${version}`;
173
- }
174
-
175
- if (modes.react) {
176
- dependencies["@alepha/server"] = `^${version}`;
177
- dependencies["@alepha/server-links"] = `^${version}`;
178
- dependencies["@alepha/react"] = `^${version}`;
179
- dependencies.react = "^19.2.0";
180
- devDependencies["@types/react"] = "^19.0.0";
181
- }
182
-
183
- return {
184
- dependencies,
185
- devDependencies,
186
- scripts: {
187
- dev: "alepha dev",
188
- build: "alepha build",
189
- },
190
- };
191
- }
192
-
193
- public async ensurePackageJson(
194
- root: string,
195
- modes: { api?: boolean; react?: boolean },
196
- ) {
197
- const packageJsonPath = join(root, "package.json");
198
- try {
199
- await access(packageJsonPath);
200
- } catch (error) {
201
- this.log.info("No package.json found. Creating one...");
202
- await writeFile(
203
- packageJsonPath,
204
- JSON.stringify(this.generatePackageJsonContent(modes), null, 2),
205
- );
206
- return;
207
- }
208
-
209
- const content = await readFile(packageJsonPath, "utf8");
210
- const packageJson = JSON.parse(content);
211
- if (!packageJson.type || packageJson.type !== "module") {
212
- packageJson.type = "module";
213
- }
214
- const newPackageJson = this.generatePackageJsonContent(modes);
215
-
216
- packageJson.type = "module";
217
- packageJson.dependencies ??= {};
218
- packageJson.devDependencies ??= {};
219
- packageJson.scripts ??= {};
220
-
221
- Object.assign(packageJson.dependencies, newPackageJson.dependencies);
222
- Object.assign(packageJson.devDependencies, newPackageJson.devDependencies);
223
- Object.assign(packageJson.scripts, newPackageJson.scripts);
224
-
225
- await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
226
- }
227
-
228
- public async ensureTsConfig(root = process.cwd()) {
229
- const tsconfigPath = join(root, "tsconfig.json");
230
- try {
231
- await access(tsconfigPath);
232
- } catch {
233
- this.log.info("Missing tsconfig.json detected. Creating one...");
234
- await writeFile(tsconfigPath, tsconfigJson);
235
- }
236
- }
237
-
238
- public async downloadSampleProject(targetDir: string) {
239
- const url = "https://api.github.com/repos/feunard/alepha/tarball/main";
240
- const response = await fetch(url, {
241
- headers: {
242
- "User-Agent": "Alepha-CLI", // GitHub API requires User-Agent
243
- },
244
- });
245
-
246
- if (!response.ok) {
247
- throw new AlephaError(`Failed to download: ${response.statusText}`);
248
- }
249
-
250
- const tarStream = Readable.fromWeb(response.body as any);
251
- await pipeline(
252
- tarStream,
253
- tar.extract({
254
- cwd: targetDir, // Extract to target directory
255
- strip: 3, // Remove feunard-alepha-<hash>/apps/starter prefix
256
- filter: (path) => {
257
- // Only extract files from apps/starter/
258
- const parts = path.split("/");
259
- return (
260
- parts.length >= 3 && parts[1] === "apps" && parts[2] === "starter"
261
- );
262
- },
263
- }),
264
- );
265
- }
266
169
  }
@@ -2,19 +2,15 @@ import { readFile } from "node:fs/promises";
2
2
  import { createRequire } from "node:module";
3
3
  import { join } from "node:path";
4
4
  import { $command } from "@alepha/command";
5
- import { $inject, Alepha, AlephaError, boot, t } from "@alepha/core";
5
+ import { $inject, AlephaError, t } from "@alepha/core";
6
6
  import { $logger } from "@alepha/logger";
7
- import type { RepositoryProvider } from "@alepha/postgres";
8
- import { tsImport } from "tsx/esm/api";
9
7
  import { ProcessRunner } from "../services/ProcessRunner.ts";
8
+ import { ProjectUtils } from "../services/ProjectUtils.ts";
10
9
 
11
10
  export class DrizzleCommands {
12
11
  log = $logger();
13
12
  runner = $inject(ProcessRunner);
14
-
15
- flags = t.object({
16
- root: t.text({ description: "Project root", default: "." }),
17
- });
13
+ utils = $inject(ProjectUtils);
18
14
 
19
15
  /**
20
16
  * Check if database migrations are up to date
@@ -29,17 +25,16 @@ export class DrizzleCommands {
29
25
  check = $command({
30
26
  name: "db:check-migrations",
31
27
  description: "Verify database migration files are up to date",
32
- flags: this.flags,
33
28
  args: t.optional(
34
29
  t.text({
35
30
  title: "path",
36
31
  description: "Path to the Alepha server entry file",
37
32
  }),
38
33
  ),
39
- handler: async ({ flags, args }) => {
40
- const rootDir = join(process.cwd(), flags.root);
34
+ handler: async ({ args, root }) => {
35
+ const rootDir = root;
41
36
  this.log.debug(`Using project root: ${rootDir}`);
42
- const { alepha } = await this.loadAlephaFromServerEntryFile(
37
+ const { alepha } = await this.utils.loadAlephaFromServerEntryFile(
43
38
  rootDir,
44
39
  args,
45
40
  );
@@ -127,16 +122,15 @@ export class DrizzleCommands {
127
122
  name: "db:generate",
128
123
  description: "Generate migration files based on current database schema",
129
124
  summary: false,
130
- flags: this.flags,
131
125
  args: t.optional(
132
126
  t.text({
133
127
  title: "path",
134
128
  description: "Path to the Alepha server entry file",
135
129
  }),
136
130
  ),
137
- handler: async ({ flags, args }) => {
138
- await this.runDrizzleKitCommand({
139
- flags,
131
+ handler: async ({ args, root }) => {
132
+ await this.utils.runDrizzleKitCommand({
133
+ root,
140
134
  args,
141
135
  command: "generate",
142
136
  logMessage: (providerName, dialect) =>
@@ -157,16 +151,15 @@ export class DrizzleCommands {
157
151
  name: "db:push",
158
152
  description: "Push database schema changes directly to the database",
159
153
  summary: false,
160
- flags: this.flags,
161
154
  args: t.optional(
162
155
  t.text({
163
156
  title: "path",
164
157
  description: "Path to the Alepha server entry file",
165
158
  }),
166
159
  ),
167
- handler: async ({ flags, args }) => {
168
- await this.runDrizzleKitCommand({
169
- flags,
160
+ handler: async ({ root, args }) => {
161
+ await this.utils.runDrizzleKitCommand({
162
+ root,
170
163
  args,
171
164
  command: "push",
172
165
  logMessage: (providerName, dialect) =>
@@ -187,16 +180,15 @@ export class DrizzleCommands {
187
180
  name: "db:migrate",
188
181
  description: "Apply pending database migrations",
189
182
  summary: false,
190
- flags: this.flags,
191
183
  args: t.optional(
192
184
  t.text({
193
185
  title: "path",
194
186
  description: "Path to the Alepha server entry file",
195
187
  }),
196
188
  ),
197
- handler: async ({ flags, args }) => {
198
- await this.runDrizzleKitCommand({
199
- flags,
189
+ handler: async ({ root, args }) => {
190
+ await this.utils.runDrizzleKitCommand({
191
+ root,
200
192
  args,
201
193
  command: "migrate",
202
194
  logMessage: (providerName, dialect) =>
@@ -217,16 +209,15 @@ export class DrizzleCommands {
217
209
  name: "db:studio",
218
210
  description: "Launch Drizzle Studio database browser",
219
211
  summary: false,
220
- flags: this.flags,
221
212
  args: t.optional(
222
213
  t.text({
223
214
  title: "path",
224
215
  description: "Path to the Alepha server entry file",
225
216
  }),
226
217
  ),
227
- handler: async ({ flags, args }) => {
228
- await this.runDrizzleKitCommand({
229
- flags,
218
+ handler: async ({ root, args }) => {
219
+ await this.utils.runDrizzleKitCommand({
220
+ root,
230
221
  args,
231
222
  command: "studio",
232
223
  logMessage: (providerName, dialect) =>
@@ -234,170 +225,4 @@ export class DrizzleCommands {
234
225
  });
235
226
  },
236
227
  });
237
-
238
- /**
239
- * Run a drizzle-kit command for all database providers
240
- */
241
- protected async runDrizzleKitCommand(options: {
242
- flags: { root: string };
243
- args?: string;
244
- command: string;
245
- logMessage: (providerName: string, dialect: string) => string;
246
- }): Promise<void> {
247
- const rootDir = join(process.cwd(), options.flags.root);
248
- this.log.debug(`Using project root: ${rootDir}`);
249
-
250
- const { alepha, entry } = await this.loadAlephaFromServerEntryFile(
251
- rootDir,
252
- options.args,
253
- );
254
-
255
- const kit = this.getKitFromAlepha(alepha);
256
- const repositoryProvider =
257
- alepha.inject<RepositoryProvider>("RepositoryProvider");
258
- const accepted = new Set<string>([]);
259
-
260
- for (const descriptor of repositoryProvider.getRepositories()) {
261
- const provider = descriptor.provider;
262
- const providerName = provider.name;
263
- const dialect = provider.dialect;
264
-
265
- if (accepted.has(providerName)) {
266
- continue;
267
- }
268
- accepted.add(providerName);
269
-
270
- this.log.info("");
271
- this.log.info(options.logMessage(providerName, dialect));
272
-
273
- const drizzleConfigJsPath = await this.prepareDrizzleConfig({
274
- kit,
275
- provider,
276
- providerName,
277
- providerUrl: provider.url,
278
- dialect,
279
- entry,
280
- rootDir,
281
- });
282
-
283
- await this.runner.exec(
284
- `drizzle-kit ${options.command} --config=${drizzleConfigJsPath}`,
285
- );
286
- }
287
- }
288
-
289
- /**
290
- * Prepare Drizzle configuration files for a provider
291
- */
292
- protected async prepareDrizzleConfig(options: {
293
- kit: any;
294
- provider: any;
295
- providerName: string;
296
- providerUrl: string;
297
- dialect: string;
298
- entry: string;
299
- rootDir: string;
300
- }): Promise<string> {
301
- const models = Object.keys(options.kit.getModels(options.provider));
302
- const entitiesJs = this.generateEntitiesJs(
303
- options.entry,
304
- options.providerName,
305
- models,
306
- );
307
-
308
- const entitiesJsPath = await this.runner.writeConfigFile(
309
- "entities.js",
310
- entitiesJs,
311
- options.rootDir,
312
- );
313
-
314
- const config: Record<string, any> = {
315
- schema: entitiesJsPath,
316
- out: `./migrations/${options.providerName}`,
317
- dialect: options.dialect,
318
- dbCredentials: {
319
- url: options.providerUrl,
320
- },
321
- };
322
-
323
- if (options.providerName === "pglite") {
324
- config.driver = "pglite";
325
- }
326
-
327
- const drizzleConfigJs = "export default " + JSON.stringify(config, null, 2);
328
-
329
- return await this.runner.writeConfigFile(
330
- "drizzle.config.js",
331
- drizzleConfigJs,
332
- options.rootDir,
333
- );
334
- }
335
-
336
- /**
337
- * Get DrizzleKitProvider from Alepha instance
338
- */
339
- protected getKitFromAlepha(alepha: Alepha): any {
340
- // biome-ignore lint/complexity/useLiteralKeys: private key
341
- return alepha["registry"]
342
- .values()
343
- .find((it: any) => it.instance.constructor.name === "DrizzleKitProvider")
344
- ?.instance;
345
- }
346
-
347
- public async loadAlephaFromServerEntryFile(
348
- rootDir?: string,
349
- explicitEntry?: string,
350
- ): Promise<{
351
- alepha: Alepha;
352
- entry: string;
353
- }> {
354
- process.env.ALEPHA_SKIP_START = "true";
355
-
356
- const entry = await boot.getServerEntry(rootDir, explicitEntry);
357
- const mod = await tsImport(entry, {
358
- parentURL: import.meta.url,
359
- });
360
-
361
- this.log.debug(`Load entry: ${entry}`);
362
-
363
- // check if alepha is correctly exported
364
- if (mod.default instanceof Alepha) {
365
- return {
366
- alepha: mod.default,
367
- entry,
368
- };
369
- }
370
-
371
- // else, try with global variable
372
- const g: any = global;
373
- if (g.__alepha) {
374
- return {
375
- alepha: g.__alepha,
376
- entry,
377
- };
378
- }
379
-
380
- throw new AlephaError(
381
- `Could not find Alepha instance in entry file: ${entry}`,
382
- );
383
- }
384
-
385
- protected generateEntitiesJs(
386
- entry: string,
387
- provider: string,
388
- models: string[] = [],
389
- ) {
390
- return `
391
- import "${entry}";
392
- import { DrizzleKitProvider, Repository } from "@alepha/postgres";
393
-
394
- const alepha = globalThis.__alepha;
395
- const kit = alepha.inject(DrizzleKitProvider);
396
- const provider = alepha.services(Repository).find((it) => it.provider.name === "${provider}").provider;
397
- const models = kit.getModels(provider);
398
-
399
- ${models.map((it: string) => `export const ${it} = models["${it}"];`).join("\n")}
400
-
401
- `.trim();
402
- }
403
228
  }
@@ -3,7 +3,7 @@ import { $inject } from "@alepha/core";
3
3
  import { ProcessRunner } from "../services/ProcessRunner.ts";
4
4
 
5
5
  export class VerifyCommands {
6
- runner = $inject(ProcessRunner);
6
+ protected readonly processRunner = $inject(ProcessRunner);
7
7
 
8
8
  /**
9
9
  * Run a series of verification commands to ensure code quality and correctness.
@@ -26,10 +26,6 @@ export class VerifyCommands {
26
26
  await run("alepha lint");
27
27
  await run("alepha test");
28
28
  await run("alepha typecheck");
29
-
30
- // run only if migrations dir is present ?
31
- //await run("alepha db:check-migrations");
32
-
33
29
  await run("alepha build");
34
30
  await run("alepha clean");
35
31
  },
@@ -42,7 +38,7 @@ export class VerifyCommands {
42
38
  name: "typecheck",
43
39
  description: "Check TypeScript types across the codebase",
44
40
  handler: async () => {
45
- await this.runner.exec("tsc --noEmit");
41
+ await this.processRunner.exec("tsc --noEmit");
46
42
  },
47
43
  });
48
44
  }