@cellajs/create-cella 0.0.5 → 0.0.7

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/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ import './src/index.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cellajs/create-cella",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Create your own app in seconds with Cella: a TypeScript template for local-first web apps.",
@@ -18,10 +18,14 @@
18
18
  "node": ">=20.14.0"
19
19
  },
20
20
  "type": "module",
21
- "main": "index.js",
21
+ "main": "index.ts",
22
22
  "bin": {
23
- "create-cella": "index.js"
23
+ "create-cella": "index.ts"
24
24
  },
25
+ "scripts": {
26
+ "create": "tsx index.ts"
27
+ },
28
+ "packageManager": "pnpm@9.11.0",
25
29
  "dependencies": {
26
30
  "@inquirer/prompts": "^6.0.1",
27
31
  "commander": "^12.1.0",
@@ -31,7 +35,7 @@
31
35
  "validate-npm-package-name": "^5.0.1",
32
36
  "yocto-spinner": "^0.1.0"
33
37
  },
34
- "scripts": {
35
- "create": "node index.js"
38
+ "devDependencies": {
39
+ "tsx": "^4.19.2"
36
40
  }
37
- }
41
+ }
@@ -0,0 +1,55 @@
1
+ import yoctoSpinner from 'yocto-spinner';
2
+ import { runGitCommand } from './utils/run-git-command.ts';
3
+ import { CELLA_REMOTE_URL } from './constants.ts';
4
+
5
+ interface AddRemoteOptions {
6
+ targetFolder: string;
7
+ remoteUrl?: string; // Optional, defaults to CELLA_REMOTE_URL
8
+ remoteName?: string; // Optional, defaults to 'upstream'
9
+ }
10
+
11
+ export async function addRemote({
12
+ targetFolder,
13
+ remoteUrl = CELLA_REMOTE_URL,
14
+ remoteName = 'upstream',
15
+ }: AddRemoteOptions): Promise<void> {
16
+
17
+ // Spinner for adding remote
18
+ const remoteSpinner = yoctoSpinner({
19
+ text: 'Adding remote',
20
+ }).start();
21
+
22
+ try {
23
+ // Check if the remote exists
24
+ let remote: string | null = null;
25
+
26
+ try {
27
+ remote = await runGitCommand({ targetFolder, command: `remote get-url ${remoteName}` });
28
+ } catch (error: any) {
29
+ // If the remote doesn't exist, it throws a fatal error
30
+ const errorMessage = typeof error === 'string' ? error : error?.message || '';
31
+ if (errorMessage.includes('fatal: No such remote')) {
32
+ remote = null;
33
+ } else {
34
+ throw error;
35
+ }
36
+ }
37
+
38
+ // Add or update the remote if it doesn't exist or differs from `remoteUrl`
39
+ if (!remote) {
40
+ await runGitCommand({ targetFolder, command: `remote add ${remoteName} ${remoteUrl}` });
41
+ remoteSpinner.success('Remote added successfully.');
42
+ } else if (remote !== remoteUrl) {
43
+ // Remove existing remote and set the new URL
44
+ await runGitCommand({ targetFolder, command: `remote remove ${remoteName}` });
45
+ await runGitCommand({ targetFolder, command: `remote add ${remoteName} ${remoteUrl}` });
46
+ remoteSpinner.success('Remote updated successfully.');
47
+ } else {
48
+ remoteSpinner.success('Remote is already configured correctly.');
49
+ }
50
+ } catch (error) {
51
+ console.error(error);
52
+ remoteSpinner.error('Failed to add remote.');
53
+ process.exit(1);
54
+ }
55
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { basename, resolve } from 'node:path';
2
+ import { Command, InvalidArgumentError } from 'commander';
3
+
4
+ import { NAME } from './constants.ts';
5
+ import { packageJson } from './utils/package-json.ts';
6
+ import { validateProjectName } from './utils/validate-project-name.ts';
7
+
8
+ // Define types for CLI options
9
+ interface CLIOptions {
10
+ skipNewBranch: boolean;
11
+ skipClean: boolean;
12
+ skipGit: boolean;
13
+ skipInstall: boolean;
14
+ skipGenerate: boolean;
15
+ newBranchName?: string;
16
+ }
17
+
18
+ // Define CLI configuration
19
+ interface CLIConfig {
20
+ options: CLIOptions;
21
+ args: string[];
22
+ directory: string | null;
23
+ newBranchName: string | null;
24
+ createNewBranch: boolean | null;
25
+ packageManager: string;
26
+ }
27
+
28
+ // Initialize CLI variables
29
+ let directory: string | null = null;
30
+ let newBranchName: string | null = null;
31
+ let createNewBranch: boolean | null = null;
32
+ const packageManager = 'pnpm';
33
+
34
+ // Set up the CLI command using Commander
35
+ export const command = new Command(NAME)
36
+ .version(
37
+ packageJson.version,
38
+ '-v, --version',
39
+ `Output the current version of ${NAME}.`
40
+ )
41
+ .argument('[directory]', 'The directory name for the new project.')
42
+ .usage('[directory] [options]')
43
+ .helpOption('-h, --help', 'Display this help message.')
44
+ .option('--skip-new-branch', 'Skip creating a new branch during initialization.', false)
45
+ .option('--skip-install', 'Skip the installation of packages.', false)
46
+ .option('--skip-generate', 'Skip generating SQL files.', false)
47
+ .option('--skip-clean', 'Skip cleaning the `cella` template.', false)
48
+ .option('--skip-git', 'Skip initializing a git repository.', false)
49
+ .option(
50
+ '--new-branch-name <name>',
51
+ 'Specify a new branch name to create and use.',
52
+ (name: string) => {
53
+ if (typeof name === 'string') {
54
+ name = name.trim();
55
+ }
56
+
57
+ if (name) {
58
+ const validation = validateProjectName(basename(resolve(name)));
59
+
60
+ if (!validation.valid) {
61
+ throw new InvalidArgumentError(
62
+ `Invalid branch name: ${validation.problems[0]}`
63
+ );
64
+ }
65
+
66
+ createNewBranch = true;
67
+ newBranchName = name;
68
+ }
69
+ }
70
+ )
71
+ .action((name: string) => {
72
+ if (typeof name === 'string') {
73
+ name = name.trim();
74
+ }
75
+
76
+ if (name) {
77
+ const validation = validateProjectName(basename(resolve(name)));
78
+
79
+ if (!validation.valid) {
80
+ throw new InvalidArgumentError(
81
+ `Invalid project name: ${validation.problems[0]}`
82
+ );
83
+ }
84
+
85
+ directory = name;
86
+ }
87
+ })
88
+ .parse();
89
+
90
+ // Gather the CLI options and arguments
91
+ const options: CLIOptions = command.opts<CLIOptions>({
92
+ skipNewBranch: false,
93
+ skipClean: false,
94
+ skipGit: false,
95
+ skipInstall: false,
96
+ skipGenerate: false,
97
+ });
98
+
99
+ // Export the CLI configuration for use in other modules
100
+ export const cli: CLIConfig = {
101
+ options,
102
+ args: command.args,
103
+ directory,
104
+ newBranchName,
105
+ createNewBranch,
106
+ packageManager,
107
+ };
@@ -0,0 +1,43 @@
1
+ export const NAME = 'create-cella';
2
+
3
+ // URL of the template repository
4
+ export const TEMPLATE_URL = 'github:cellajs/cella';
5
+
6
+ // URL to the Cella repository
7
+ export const CELLA_REMOTE_URL: string = 'git@github.com:cellajs/cella.git';
8
+
9
+ // Import package.json dynamically for version and website information
10
+ import packageJson from '../package.json' assert { type: 'json' };
11
+
12
+ // Export version, website, and author from package.json
13
+ export const VERSION: string = packageJson.version;
14
+ export const AUTHOR: string = packageJson.author;
15
+ export const WEBSITE: string = packageJson.homepage;
16
+
17
+ // Files or folders to be removed from the template after downloading
18
+ export const TO_REMOVE: string[] = [
19
+ 'info',
20
+ './cli/create-cella',
21
+ ];
22
+
23
+ // Specific folder contents to be cleaned out from the template
24
+ export const TO_CLEAN: string[] = [
25
+ './backend/drizzle',
26
+ ];
27
+
28
+ // Files to copy/paste after downloading
29
+ export const TO_COPY: Record<string, string> = {
30
+ './backend/.env.example': './backend/.env',
31
+ './frontend/.env.example': './frontend/.env',
32
+ './tus/.env.example': './tus/.env',
33
+ './info/QUICKSTART.md': 'README.md',
34
+ };
35
+
36
+ // ASCII title for the CLI output
37
+ export const CELLA_TITLE = `
38
+ _ _
39
+ ▒▓█████▓▒ ___ ___| | | __ _
40
+ ▒▓█ █▓▒ / __/ _ \\ | |/ _\` |
41
+ ▒▓█ █▓▒ | (_| __/ | | (_| |
42
+ ▒▓█████▓▒ \\___\\___|_|_|\\__,_|
43
+ `;
package/src/create.ts ADDED
@@ -0,0 +1,183 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { join, relative } from 'node:path';
4
+ import colors from 'picocolors';
5
+ import { downloadTemplate } from 'giget';
6
+ import yoctoSpinner from 'yocto-spinner';
7
+
8
+ import { TEMPLATE_URL } from './constants.ts';
9
+
10
+ import { install, generate } from './utils/run-package-manager-command.ts';
11
+ import { cleanTemplate } from './utils/clean-template.ts';
12
+ import { runGitCommand } from './utils/run-git-command.ts';
13
+ import { addRemote } from './add-remote.ts';
14
+
15
+ interface CreateOptions {
16
+ projectName: string;
17
+ targetFolder: string;
18
+ newBranchName?: string | null;
19
+ skipInstall: boolean;
20
+ skipGit: boolean;
21
+ skipClean: boolean;
22
+ skipGenerate: boolean;
23
+ packageManager: string;
24
+ }
25
+
26
+ export async function create({
27
+ projectName,
28
+ targetFolder,
29
+ newBranchName,
30
+ skipInstall,
31
+ skipGit,
32
+ skipClean,
33
+ skipGenerate,
34
+ packageManager,
35
+ }: CreateOptions): Promise<void> {
36
+ // Save the original working directory
37
+ const originalCwd = process.cwd();
38
+
39
+ console.info();
40
+
41
+ // Create the target folder if it doesn't exist
42
+ const createFolderSpinner = yoctoSpinner({
43
+ text: 'Creating project folder',
44
+ }).start();
45
+
46
+ await mkdir(targetFolder, { recursive: true });
47
+ process.chdir(targetFolder);
48
+
49
+ createFolderSpinner.success('Project folder created');
50
+
51
+ // Download the template from the specified URL
52
+ const downloadSpinner = yoctoSpinner({
53
+ text: 'Downloading `cella` template',
54
+ }).start();
55
+
56
+ await downloadTemplate(TEMPLATE_URL, {
57
+ cwd: process.cwd(),
58
+ dir: targetFolder,
59
+ force: true,
60
+ provider: 'github',
61
+ });
62
+
63
+ downloadSpinner.success('`cella` template downloaded');
64
+
65
+ // Clean the template if the skipClean flag is not set
66
+ if (!skipClean) {
67
+ const cleanSpinner = yoctoSpinner({
68
+ text: 'cleaning `cella` template',
69
+ }).start();
70
+
71
+ try {
72
+ await cleanTemplate({
73
+ targetFolder,
74
+ projectName,
75
+ });
76
+ cleanSpinner.success('`cella` template cleaned');
77
+ } catch (e) {
78
+ console.error(e);
79
+ cleanSpinner.error('Failed to clean `cella` template');
80
+ process.exit(1);
81
+ }
82
+ } else {
83
+ console.info(`${colors.yellow('⚠')} --skip-clean > Skip cleaning \`cella\` template`);
84
+ }
85
+
86
+ // Install dependencies if the skipInstall flag is not set
87
+ if (!skipInstall) {
88
+ const installSpinner = yoctoSpinner({
89
+ text: 'installing dependencies',
90
+ }).start();
91
+
92
+ try {
93
+ await install(packageManager);
94
+ installSpinner.success('Dependencies installed');
95
+ } catch (e) {
96
+ console.error(e);
97
+ installSpinner.error('Failed to install dependencies');
98
+ process.exit(1);
99
+ }
100
+ } else {
101
+ console.info(`${colors.yellow('⚠')} --skip-install > Skip installing dependencies`);
102
+ }
103
+
104
+ // Generate SQL files if the skipGenerate flag is not set
105
+ if (!skipGenerate) {
106
+ const generateSpinner = yoctoSpinner({
107
+ text: 'generating SQL files',
108
+ }).start();
109
+
110
+ try {
111
+ await generate(packageManager);
112
+ generateSpinner.success('SQL files generated');
113
+ } catch (e) {
114
+ console.error(e);
115
+ generateSpinner.error('Failed to generate SQL files');
116
+ process.exit(1);
117
+ }
118
+ } else {
119
+ console.info(`${colors.yellow('⚠')} --skip-generate > Skip generating SQL files`);
120
+ }
121
+
122
+ // Initialize Git repository if skipGit flag is not set
123
+ if (!skipGit) {
124
+ const gitSpinner = yoctoSpinner({
125
+ text: 'initializing git repository',
126
+ }).start();
127
+
128
+ const gitFolderPath = join(targetFolder, '.git');
129
+
130
+ if (!existsSync(gitFolderPath)) {
131
+ try {
132
+ // Run Git commands to initialize the repository and make the first commit
133
+ await runGitCommand({ targetFolder, command: 'init' });
134
+ await runGitCommand({ targetFolder, command: 'add .' });
135
+ await runGitCommand({ targetFolder, command: 'commit -m "Initial commit"' });
136
+
137
+ // If a new branch name is specified, create and checkout the branch
138
+ if (newBranchName) {
139
+ await runGitCommand({ targetFolder, command: `branch ${newBranchName}` });
140
+ await runGitCommand({ targetFolder, command: `checkout ${newBranchName}` });
141
+ gitSpinner.success(`Git repository initialized, initial commit created, and new branch ${newBranchName} created`);
142
+ } else {
143
+ gitSpinner.success('Git repository initialized and initial commit created');
144
+ }
145
+ } catch (e) {
146
+ console.error(e);
147
+ gitSpinner.error('Failed to initialize Git repository or create branch');
148
+ process.exit(1);
149
+ }
150
+ } else {
151
+ gitSpinner.warning('Git repository already initialized > Skip git init');
152
+ }
153
+ } else {
154
+ console.info(`${colors.yellow('⚠')} --skip-git > Skip git init`);
155
+ }
156
+
157
+ // Add Cella as upstream remote
158
+ await addRemote({ targetFolder });
159
+
160
+ // Final success message indicating project creation
161
+ console.info();
162
+ console.info(`${colors.green('Success')} Created ${projectName} at ${targetFolder}`);
163
+ console.info();
164
+
165
+ // Check if the working directory needs to be changed
166
+ const needsCd = originalCwd !== targetFolder;
167
+ if (needsCd) {
168
+ // Calculate the relative path between the original working directory and the target folder
169
+ const relativePath = relative(originalCwd, targetFolder);
170
+
171
+ console.info('now go to your project using:');
172
+ console.info(colors.cyan(` cd ${relativePath}`)); // Adding './' to make it clear it's a relative path
173
+ console.info();
174
+ }
175
+
176
+ console.info(`${needsCd ? 'then ' : ''}quick start with:`);
177
+ console.info(colors.cyan(` ${packageManager} quick`));
178
+ console.info();
179
+
180
+ console.info('Check out the readme to get started: /README.md');
181
+ console.info(`Enjoy building ${projectName} using cella! 🎉`);
182
+ console.info();
183
+ }
@@ -1,17 +1,28 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env tsx
2
2
 
3
- import { basename, resolve } from 'node:path'
4
- import { existsSync } from 'node:fs'
3
+ import { basename, resolve } from 'node:path';
4
+ import { existsSync } from 'node:fs';
5
5
 
6
6
  import { input, confirm, select } from '@inquirer/prompts';
7
7
 
8
- import { cli } from './cli.js'
9
- import { validateProjectName } from './utils/validate-project-name.js'
10
- import { isEmptyDirectory } from './utils/is-empty-directory.js'
11
- import { create } from './create.js'
12
- import { CELLA_TITLE, VERSION, WEBSITE, AUTHOR } from './constants.js'
8
+ import { cli } from './cli';
9
+ import { validateProjectName } from './utils/validate-project-name.ts';
10
+ import { isEmptyDirectory } from './utils/is-empty-directory.ts';
11
+ import { create } from './create.ts';
12
+ import { CELLA_TITLE, VERSION, WEBSITE, AUTHOR } from './constants.ts';
13
+
14
+ interface CreateOptions {
15
+ projectName: string;
16
+ targetFolder: string;
17
+ newBranchName?: string | null;
18
+ skipInstall: boolean;
19
+ skipGit: boolean;
20
+ skipClean: boolean;
21
+ skipGenerate: boolean;
22
+ packageManager: string;
23
+ }
13
24
 
14
- async function main() {
25
+ async function main(): Promise<void> {
15
26
  console.info(CELLA_TITLE);
16
27
 
17
28
  // Display CLI version and created by information
@@ -28,7 +39,7 @@ async function main() {
28
39
  }
29
40
 
30
41
  // Skip generating sql files if --skipGenerate flag is provided
31
- if (cli.options.skipGenerate === true) {
42
+ if (cli.options.skipGenerate === true) {
32
43
  cli.options.skipGenerate = true;
33
44
  }
34
45
 
@@ -73,14 +84,14 @@ async function main() {
73
84
  message: 'Enter the new branch name',
74
85
  default: 'development',
75
86
  validate: (name) => {
76
- const validation = validateProjectName(basename(resolve(name)))
87
+ const validation = validateProjectName(basename(resolve(name)));
77
88
  return validation.valid ? true : `Invalid branch name: ${validation.problems[0]}`;
78
89
  },
79
90
  });
80
91
  }
81
92
 
82
- const targetFolder = resolve(cli.directory)
83
- const projectName = basename(targetFolder)
93
+ const targetFolder = resolve(cli.directory);
94
+ const projectName = basename(targetFolder);
84
95
 
85
96
  // Check if the target folder exists and is not empty
86
97
  if (existsSync(targetFolder) && !(await isEmptyDirectory(targetFolder))) {
@@ -94,13 +105,14 @@ async function main() {
94
105
  { name: 'Ignore existing files and continue', value: 'ignore' },
95
106
  ],
96
107
  });
108
+
97
109
  if (action === 'cancel') {
98
110
  process.exit(1);
99
111
  }
100
112
  }
101
-
113
+
102
114
  // Proceed with the project creation
103
- await create({
115
+ const createOptions: CreateOptions = {
104
116
  projectName,
105
117
  targetFolder,
106
118
  newBranchName: cli.newBranchName,
@@ -109,7 +121,9 @@ async function main() {
109
121
  skipClean: cli.options.skipClean,
110
122
  skipGenerate: cli.options.skipGenerate,
111
123
  packageManager: cli.packageManager,
112
- });
124
+ };
125
+
126
+ await create(createOptions);
113
127
  }
114
128
 
115
- main().catch(console.error)
129
+ main().catch(console.error);
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import colors from 'picocolors';
4
+
5
+ import { TO_CLEAN, TO_REMOVE, TO_COPY } from '../constants.ts';
6
+
7
+ /**
8
+ * Cleans the specified template by removing designated folders and files.
9
+ * @param params - Parameters containing the target folder and project name.
10
+ */
11
+ export async function cleanTemplate({
12
+ targetFolder,
13
+ projectName,
14
+ }: {
15
+ targetFolder: string;
16
+ projectName: string;
17
+ }): Promise<void> {
18
+ // Change the current working directory to targetFolder if not already set
19
+ if (process.cwd() !== targetFolder) {
20
+ process.chdir(targetFolder);
21
+ }
22
+
23
+ return new Promise<void>(async (resolve, reject) => {
24
+ try {
25
+ // Copy specified files
26
+ for (const [src, dest] of Object.entries(TO_COPY)) {
27
+ const srcAbsolutePath = path.resolve(targetFolder, src);
28
+ const destAbsolutePath = path.resolve(targetFolder, dest);
29
+ await copyFile(srcAbsolutePath, destAbsolutePath);
30
+ }
31
+
32
+ // Clean specified folder contents
33
+ await Promise.all(
34
+ TO_CLEAN.map((folderPath) => {
35
+ const absolutePath = path.resolve(targetFolder, folderPath);
36
+ return removeFolderContents(absolutePath);
37
+ })
38
+ );
39
+
40
+ // Remove specified files and folders
41
+ await Promise.all(
42
+ TO_REMOVE.map((filePath) => {
43
+ const absolutePath = path.resolve(targetFolder, filePath);
44
+ return removeFileOrFolder(absolutePath);
45
+ })
46
+ );
47
+
48
+ resolve();
49
+ } catch (err) {
50
+ reject(`Error during the cleaning process: ${err}`);
51
+ }
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Removes all contents within a specified folder.
57
+ * @param folderPath - The path of the folder to clean.
58
+ */
59
+ export async function removeFolderContents(folderPath: string): Promise<void> {
60
+ // List all files in the folder
61
+ const files = await fs.readdir(folderPath);
62
+
63
+ await Promise.all(
64
+ files.map(async (file) => {
65
+ const filePath = path.join(folderPath, file);
66
+
67
+ // Get the file or folder statistics
68
+ const stat = await fs.lstat(filePath);
69
+
70
+ // If it's a directory, remove it and all its contents
71
+ if (stat.isDirectory()) {
72
+ await fs.rm(filePath, { recursive: true, force: true });
73
+ } else {
74
+ // If it's a file, remove it
75
+ await fs.rm(filePath);
76
+ }
77
+ })
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Removes a specified file or folder.
83
+ * @param pathToRemove - The path to the file or folder to remove.
84
+ */
85
+ export async function removeFileOrFolder(pathToRemove: string): Promise<void> {
86
+ await fs.rm(pathToRemove, { recursive: true, force: true });
87
+ }
88
+
89
+ /**
90
+ * Helper function to copy files if the source exists.
91
+ * @param src - The source file path.
92
+ * @param dest - The destination file path.
93
+ */
94
+ export async function copyFile(src: string, dest: string): Promise<void> {
95
+ try {
96
+ // Check if the source file exists
97
+ await fs.access(src);
98
+
99
+ // Ensure the destination directory exists
100
+ await fs.mkdir(path.dirname(dest), { recursive: true });
101
+
102
+ // Copy the file
103
+ await fs.copyFile(src, dest);
104
+ } catch (err: any) {
105
+ if (err.code === 'ENOENT') {
106
+ console.info(`\n${colors.yellow('⚠')} Source file "${src}" does not exist > Skip copy`);
107
+ } else {
108
+ throw err;
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,15 @@
1
+ import { readdir } from 'node:fs/promises';
2
+
3
+ /**
4
+ * Checks if a directory is empty or only contains a .git directory.
5
+ *
6
+ * @param path - The path of the directory to check.
7
+ * @returns Resolves to true if the directory is empty or contains only a .git folder, false otherwise.
8
+ * @throws Throws an error if the path is not a directory or if there's an issue reading the directory.
9
+ */
10
+ export async function isEmptyDirectory(path: string): Promise<boolean> {
11
+ const files = await readdir(path);
12
+
13
+ // Check if directory is empty or contains only the .git directory
14
+ return files.length === 0 || (files.length === 1 && files[0] === '.git');
15
+ }
@@ -0,0 +1,25 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ interface PackageJson {
6
+ name?: string;
7
+ version: string;
8
+ dependencies?: Record<string, string>;
9
+ devDependencies?: Record<string, string>;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ // Helper function to read and parse package.json
14
+ async function readPackageJson(): Promise<PackageJson> {
15
+ const PACKAGE_JSON_FILE = resolve(
16
+ fileURLToPath(import.meta.url),
17
+ '../../../package.json'
18
+ );
19
+
20
+ const packageJsonContent = await readFile(PACKAGE_JSON_FILE, 'utf-8');
21
+ return JSON.parse(packageJsonContent) as PackageJson;
22
+ }
23
+
24
+ // Export the parsed package.json content
25
+ export const packageJson: PackageJson = await readPackageJson();