Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions .scripts/command-utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import chalk from "chalk"
import chokidar from "chokidar"
import fs from "fs/promises"
import path from "path"
import { execa } from "execa"

export const cancelCurrentBuild = async ({
currentBuildProcesses,
commandTag,
}) => {
console.log(chalk.red(`${commandTag} Canceling current build processes...`))
for (const proc of currentBuildProcesses) {
try {
proc.kill()
} catch (err) {
console.error(
chalk.red(
`${commandTag} Error killing process ${proc.pid}: ${err.message}`
)
)
}
}
await execa("pnpm", ["clean:tmp"], { preferLocal: true })
currentBuildProcesses = []
}

export const getConsumerAppBasePath = () => {
const consumerPath = process.env.BASEAPP_FRONTEND_TEMPLATE_PATH
if (!consumerPath) {
console.error(
chalk.red(
" Error: Please set the environment variable BASEAPP_FRONTEND_TEMPLATE_PATH in your shell startup (e.g., in ~/.bashrc or ~/.zshrc) before running this command.\n",
"Example: export BASEAPP_FRONTEND_TEMPLATE_PATH=/path/to/the/baseapp-frontend-template\n",
`Note: Don't forget to restart your terminal after setting the new environment variable.`
)
)

process.exit(1)
}
return consumerPath
}

export const updateConsumerRsync = async ({
consumerAppPath,
sourceDist,
packageName,
commandTag,
}) => {
const targetDist =
path.join(
consumerAppPath,
"node_modules",
"@baseapp-frontend",
packageName,
"dist"
) + "/"

console.log(
chalk.cyan(`${commandTag} Syncing dist folder to consumer app...`)
)
try {
await execa(
"rsync",
["-av", "--delete", "--delay-updates", sourceDist, targetDist],
{
shell: true,
}
)
console.log(chalk.cyan(`${commandTag} Sync completed successfully.`))
} catch (error) {
console.error(chalk.red(`${commandTag} Sync failed:`))
console.error(chalk.red(error.stderr || error.message))
}
}

export const waitForReadyFile = async ({ readyFileParentPath, commandTag }) => {
console.log(
chalk.yellow(`${commandTag} Waiting for other packages to start...`)
)

return new Promise((resolve, reject) => {
const watcher = chokidar.watch(readyFileParentPath, {
ignoreInitial: false,
usePolling: true,
interval: 100,
awaitWriteFinish: {
stabilityThreshold: 500,
pollInterval: 100,
},
})
watcher.on('add', (filePath) => {
if (path.basename(filePath) === 'build.ready') {
console.log(chalk.green(`${commandTag} Ready file detected.`))
watcher.close()
resolve()
}
})
watcher.on("error", (err) => {
watcher.close()
reject(err)
})
})
}

export const cleanupReadyFile = async ({readyFilePath}) => {
try {
await fs.unlink(readyFilePath)
} catch (err) {
// pass
}
}
55 changes: 44 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,56 @@ To build all apps and packages, run the following command:
pnpm build

# build only the authentication package
pnpm build --filter=authentication
pnpm build --filter=@baseapp-frontend/authentication
```

## Develop

To develop all apps and packages, run the following command:
Our development mode is designed to provide immediate feedback as you work on our monorepo apps and packages. In dev mode, each package automatically watches for changes in its source files, rebuilds itself using a custom build process, and synchronizes its output (bundled code, type declarations, etc.) to the consumer app.

This ensures that any changes you make are quickly reflected in the running application without the need to manually rebuild or restart servers.

### What Happens in Dev Mode

Some of our packages—like `@baseapp-frontend/components` and `@baseapp-frontend/design-system`—have a multi-step build process. When you run:

```bash
pnpm dev
```

Each package in our monorepo enters a persistent watch mode.

For example, when running dev mode for `@baseapp-frontend/components`, you might see output similar to the following:
```bash
[@baseapp-frontend/components] Waiting for other packages to start... # wait for other dependencies to be build
[@baseapp-frontend/components] Starting build process... # start the build process
[@baseapp-frontend/components] Running Relay Compiler... # since this package uses relay, run the relay compiler
[@baseapp-frontend/components] Relay compilation completed.
[@baseapp-frontend/components] Running Babel transpiling... # run babel step to transpile the code
[@baseapp-frontend/components] Babel transpilation completed.
[@baseapp-frontend/components] Running tsup bundling... # run tsup step to bunle the code
[@baseapp-frontend/components] Running type declaration generation... # run tsc step to create type declarations
[@baseapp-frontend/components] tsup Bundling completed.
[@baseapp-frontend/components] Type declarations generated.
[@baseapp-frontend/components] Copying DTS files... # merge the declaration files with the bundled files
[@baseapp-frontend/components] DTS files copied.
[@baseapp-frontend/components] Cleaning temporary files... # remove temporary folders
[@baseapp-frontend/components] Temporary files cleaned.
[@baseapp-frontend/components] Build completed successfully. # build completed
[@baseapp-frontend/components] Syncing dist folder to consumer app... # sync the build output with the consumer app (baseapp-frontend-template)
[@baseapp-frontend/components] Sync completed successfully.
```
**Disclaimer**

The dev mode is a powerful tool that makes live testing of changes very convenient by automatically rebuilding packages as you edit files.

However, note that for packages like `@baseapp-frontend/design-system` and `@baseapp-frontend/components`, the watch process can trigger multiple build tasks upon every file change.

This continuous rebuild may lead to increased memory consumption and CPU usage if you’re making a lot of simultaneous changes.

It is recommended to use this live mode only at appropriate times rather than throughout your entire development phase.


## **PNPM Catalog Overview**

This monorepo manages some dependencies using pnpm catalogs. As a rule of thumb, we often add dependencies to the catalogs that are reused across multiple packages, rather than arbitrarily adding dependencies to these lists. This approach ensures that shared dependencies are centrally managed and consistently applied across the codebase.
Expand All @@ -104,13 +143,10 @@ Make sure to keep [`@baseapp-frontend's catalog`](https://github.com/silverlogic

### **Remove Catalog Entries**:

Before using a package from GitHub, remove its catalog entry. This is necessary because pnpm doesn't handle catalogs well when using non-published versions. To remove the catalogs for the desired package, run the following command:
Before using a package from GitHub, remove its catalog entry. This is necessary because pnpm doesn't handle catalogs well when using non-published versions. To remove the catalogs for all packages, run the following command:

```bash
# will replace catalogs for utils and authentication packages
pnpm replace-catalogs utils authentication

# will replace catalogs for all packages
pnpm i # make sure the dependencies are installed
pnpm replace-catalogs
```

Expand All @@ -119,10 +155,7 @@ Make sure to keep [`@baseapp-frontend's catalog`](https://github.com/silverlogic
To restore the catalog entries to their original state, run the following command:

```bash
# will restore catalogs for utils and authentication packages
pnpm restore-catalogs utils authentication

# will restore catalogs for all packages
pnpm i # make sure the dependencies are installed
pnpm restore-catalogs
```

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
"@parcel/packager-ts": "latest",
"@parcel/transformer-typescript-types": "latest",
"@types/node": "catalog:",
"chalk": "^5.4.1",
"chokidar": "^4.0.3",
"eslint": "catalog:lint",
"execa": "^9.5.2",
"husky": "catalog:lint",
"lint-staged": "catalog:lint",
"prettier": "catalog:lint",
Expand Down
3 changes: 3 additions & 0 deletions packages/components/.scripts/build-command.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { runBuild } from './build.mjs'

runBuild()
61 changes: 61 additions & 0 deletions packages/components/.scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import chalk from 'chalk'
import { execa } from 'execa'

const commandTag = '[@baseapp-frontend/components]'

export const runBuild = async (currentBuildProcesses = []) => {
console.log(`${chalk.magenta(`${commandTag} Starting build process...`)}`)

try {
console.log(chalk.cyanBright(`${commandTag} Running Relay Compiler...`))
const relayProc = execa('pnpm', ['relay'], { preferLocal: true })
currentBuildProcesses?.push(relayProc)
await relayProc
console.log(chalk.cyanBright(`${commandTag} Relay compilation completed.`))

console.log(chalk.yellowBright(`${commandTag} Running Babel transpiling...`))
const babelProc = execa('pnpm', ['babel:transpile'], { preferLocal: true })
currentBuildProcesses?.push(babelProc)
await babelProc
console.log(chalk.yellowBright(`${commandTag} Babel transpilation completed.`))

await Promise.all([
(async () => {
console.log(chalk.yellow(`${commandTag} Running tsup bundling...`))
const tsupProc = execa('pnpm', ['tsup:bundle', '--silent'], { preferLocal: true })
currentBuildProcesses?.push(tsupProc)
await tsupProc
console.log(chalk.yellow(`${commandTag} tsup Bundling completed.`))
})(),
(async () => {
console.log(chalk.blue(`${commandTag} Running type declaration generation...`))
const tscProc = execa('pnpm', ['tsc:declaration'], { preferLocal: true })
currentBuildProcesses?.push(tscProc)
await tscProc
console.log(chalk.blue(`${commandTag} Type declarations generated.`))

console.log(chalk.cyan(`${commandTag} Copying DTS files...`))
const copyProc = execa('pnpm', ['copy:dts'], { preferLocal: true })
currentBuildProcesses?.push(copyProc)
await copyProc
console.log(chalk.cyan(`${commandTag} DTS files copied.`))
})(),
])

console.log(chalk.hex('#c86c2c')(`${commandTag} Cleaning temporary files...`))
const cleanProc = execa('pnpm', ['clean:tmp'], { preferLocal: true })
currentBuildProcesses?.push(cleanProc)
await cleanProc
console.log(chalk.hex('#c86c2c')(`${commandTag} Temporary files cleaned.`))

console.log(chalk.green(`${commandTag} Build completed successfully.`))
} catch (error) {
if (error.signal !== 'SIGTERM') {
console.error(chalk.red(`${commandTag} Build failed:`))
console.error(chalk.red(error.stderr || error.message))
}
throw error
} finally {
currentBuildProcesses = []
}
}
100 changes: 100 additions & 0 deletions packages/components/.scripts/dev-command.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import chalk from 'chalk'
import chokidar from 'chokidar'
import path from 'path'
import { fileURLToPath } from 'url'

import {
cancelCurrentBuild,
getConsumerAppBasePath,
updateConsumerRsync,
waitForReadyFile,
} from '../../../.scripts/command-utils.mjs'
import { runBuild } from './build.mjs'

const currentDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.join(currentDir, '..')

const commandTag = '[@baseapp-frontend/components]'

let isBuilding = false
let needsRebuild = false
let buildTimeout = null
let currentBuildProcesses = []

const runWatchBuild = async () => {
if (isBuilding) {
needsRebuild = true
await cancelCurrentBuild({ currentBuildProcesses, commandTag })
return
}

isBuilding = true

try {
const consumerAppPath = getConsumerAppBasePath()

const designSystemPath = path.join(rootDir, '..', 'design-system')
const readyFileParentPath = path.join(designSystemPath, 'dist')
await waitForReadyFile({ readyFileParentPath, commandTag })

await runBuild(currentBuildProcesses)

await updateConsumerRsync({
consumerAppPath,
sourceDist: path.join(rootDir, 'dist/'),
packageName: 'components',
commandTag,
})

console.log(`${chalk.magenta(`${commandTag} Watching for file changes...`)}`)
} catch (error) {
if (error.signal !== 'SIGTERM') {
console.error(chalk.red(`${commandTag} Build failed:`))
console.error(chalk.red(error.stderr || error.message))
}
} finally {
isBuilding = false
currentBuildProcesses = []
if (needsRebuild) {
needsRebuild = false
runWatchBuild()
}
}
}

const watchRegex = /^modules\/(?:.*\/)?(common|web|native)\/.*$/

const watcher = chokidar.watch(rootDir, {
ignoreInitial: true,
usePolling: true,
interval: 100,
awaitWriteFinish: {
stabilityThreshold: 500,
pollInterval: 100,
},
ignored: (filePath, stats) => {
if (
filePath.includes('node_modules') ||
filePath.includes('dist') ||
filePath.includes('tmp')
) {
return true
}
if (stats && stats.isFile()) {
const relative = path.relative(rootDir, filePath).replace(/\\/g, '/')
if (!watchRegex.test(relative)) {
return true
}
}
return false
},
})

watcher.on('all', (event, changedPath) => {
const relativePath = path.relative(rootDir, changedPath).replace(/\\/g, '/')
console.log(`${commandTag} Detected event "${event}" on: ${relativePath}`)
if (buildTimeout) clearTimeout(buildTimeout)
buildTimeout = setTimeout(runWatchBuild, 2000)
})

runWatchBuild()
4 changes: 2 additions & 2 deletions packages/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ pnpm install @baseapp-frontend/components

## **What is in here?**

This package contains essential BaseApp modules such as `comments`, `notifications`, `messages` and `navigations`. It also includes Storybook, a tool for component documentation and visualization. To run the Storybook locally, navigate to the package folder and run the following command:
This package contains essential BaseApp modules such as `comments`, `notifications`, `messages` and `navigations`. It also includes Storybook, a tool for component documentation and visualization. To run the Storybook locally, run the following command:

```bash
# at root level

pnpm storybook --filter components
pnpm storybook --filter @baseapp-frontend/components
```

## **Build Process**
Expand Down
Loading
Loading