Skip to content

Commit ac5df92

Browse files
BA-2259: improve dev mode (#218)
1 parent 6ed8766 commit ac5df92

18 files changed

+758
-131
lines changed

.scripts/command-utils.mjs

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import chalk from "chalk"
2+
import chokidar from "chokidar"
3+
import fs from "fs/promises"
4+
import path from "path"
5+
import { execa } from "execa"
6+
7+
export const cancelCurrentBuild = async ({
8+
currentBuildProcesses,
9+
commandTag,
10+
}) => {
11+
console.log(chalk.red(`${commandTag} Canceling current build processes...`))
12+
for (const proc of currentBuildProcesses) {
13+
try {
14+
proc.kill()
15+
} catch (err) {
16+
console.error(
17+
chalk.red(
18+
`${commandTag} Error killing process ${proc.pid}: ${err.message}`
19+
)
20+
)
21+
}
22+
}
23+
await execa("pnpm", ["clean:tmp"], { preferLocal: true })
24+
currentBuildProcesses = []
25+
}
26+
27+
export const getConsumerAppBasePath = () => {
28+
const consumerPath = process.env.BASEAPP_FRONTEND_TEMPLATE_PATH
29+
if (!consumerPath) {
30+
console.error(
31+
chalk.red(
32+
" 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",
33+
"Example: export BASEAPP_FRONTEND_TEMPLATE_PATH=/path/to/the/baseapp-frontend-template\n",
34+
`Note: Don't forget to restart your terminal after setting the new environment variable.`
35+
)
36+
)
37+
38+
process.exit(1)
39+
}
40+
return consumerPath
41+
}
42+
43+
export const updateConsumerRsync = async ({
44+
consumerAppPath,
45+
sourceDist,
46+
packageName,
47+
commandTag,
48+
}) => {
49+
const targetDist =
50+
path.join(
51+
consumerAppPath,
52+
"node_modules",
53+
"@baseapp-frontend",
54+
packageName,
55+
"dist"
56+
) + "/"
57+
58+
console.log(
59+
chalk.cyan(`${commandTag} Syncing dist folder to consumer app...`)
60+
)
61+
try {
62+
await execa(
63+
"rsync",
64+
["-av", "--delete", "--delay-updates", sourceDist, targetDist],
65+
{
66+
shell: true,
67+
}
68+
)
69+
console.log(chalk.cyan(`${commandTag} Sync completed successfully.`))
70+
} catch (error) {
71+
console.error(chalk.red(`${commandTag} Sync failed:`))
72+
console.error(chalk.red(error.stderr || error.message))
73+
}
74+
}
75+
76+
export const waitForReadyFile = async ({ readyFileParentPath, commandTag }) => {
77+
console.log(
78+
chalk.yellow(`${commandTag} Waiting for other packages to start...`)
79+
)
80+
81+
return new Promise((resolve, reject) => {
82+
const watcher = chokidar.watch(readyFileParentPath, {
83+
ignoreInitial: false,
84+
usePolling: true,
85+
interval: 100,
86+
awaitWriteFinish: {
87+
stabilityThreshold: 500,
88+
pollInterval: 100,
89+
},
90+
})
91+
watcher.on('add', (filePath) => {
92+
if (path.basename(filePath) === 'build.ready') {
93+
console.log(chalk.green(`${commandTag} Ready file detected.`))
94+
watcher.close()
95+
resolve()
96+
}
97+
})
98+
watcher.on("error", (err) => {
99+
watcher.close()
100+
reject(err)
101+
})
102+
})
103+
}
104+
105+
export const cleanupReadyFile = async ({readyFilePath}) => {
106+
try {
107+
await fs.unlink(readyFilePath)
108+
} catch (err) {
109+
// pass
110+
}
111+
}

README.md

+44-11
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,56 @@ To build all apps and packages, run the following command:
8383
pnpm build
8484

8585
# build only the authentication package
86-
pnpm build --filter=authentication
86+
pnpm build --filter=@baseapp-frontend/authentication
8787
```
8888

8989
## Develop
9090

91-
To develop all apps and packages, run the following command:
91+
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.
92+
93+
This ensures that any changes you make are quickly reflected in the running application without the need to manually rebuild or restart servers.
94+
95+
### What Happens in Dev Mode
96+
97+
Some of our packages—like `@baseapp-frontend/components` and `@baseapp-frontend/design-system`—have a multi-step build process. When you run:
9298

9399
```bash
94100
pnpm dev
95101
```
96102

103+
Each package in our monorepo enters a persistent watch mode.
104+
105+
For example, when running dev mode for `@baseapp-frontend/components`, you might see output similar to the following:
106+
```bash
107+
[@baseapp-frontend/components] Waiting for other packages to start... # wait for other dependencies to be build
108+
[@baseapp-frontend/components] Starting build process... # start the build process
109+
[@baseapp-frontend/components] Running Relay Compiler... # since this package uses relay, run the relay compiler
110+
[@baseapp-frontend/components] Relay compilation completed.
111+
[@baseapp-frontend/components] Running Babel transpiling... # run babel step to transpile the code
112+
[@baseapp-frontend/components] Babel transpilation completed.
113+
[@baseapp-frontend/components] Running tsup bundling... # run tsup step to bunle the code
114+
[@baseapp-frontend/components] Running type declaration generation... # run tsc step to create type declarations
115+
[@baseapp-frontend/components] tsup Bundling completed.
116+
[@baseapp-frontend/components] Type declarations generated.
117+
[@baseapp-frontend/components] Copying DTS files... # merge the declaration files with the bundled files
118+
[@baseapp-frontend/components] DTS files copied.
119+
[@baseapp-frontend/components] Cleaning temporary files... # remove temporary folders
120+
[@baseapp-frontend/components] Temporary files cleaned.
121+
[@baseapp-frontend/components] Build completed successfully. # build completed
122+
[@baseapp-frontend/components] Syncing dist folder to consumer app... # sync the build output with the consumer app (baseapp-frontend-template)
123+
[@baseapp-frontend/components] Sync completed successfully.
124+
```
125+
**Disclaimer**
126+
127+
The dev mode is a powerful tool that makes live testing of changes very convenient by automatically rebuilding packages as you edit files.
128+
129+
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.
130+
131+
This continuous rebuild may lead to increased memory consumption and CPU usage if you’re making a lot of simultaneous changes.
132+
133+
It is recommended to use this live mode only at appropriate times rather than throughout your entire development phase.
134+
135+
97136
## **PNPM Catalog Overview**
98137

99138
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.
@@ -104,13 +143,10 @@ Make sure to keep [`@baseapp-frontend's catalog`](https://github.com/silverlogic
104143

105144
### **Remove Catalog Entries**:
106145

107-
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:
146+
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:
108147

109148
```bash
110-
# will replace catalogs for utils and authentication packages
111-
pnpm replace-catalogs utils authentication
112-
113-
# will replace catalogs for all packages
149+
pnpm i # make sure the dependencies are installed
114150
pnpm replace-catalogs
115151
```
116152

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

121157
```bash
122-
# will restore catalogs for utils and authentication packages
123-
pnpm restore-catalogs utils authentication
124-
125-
# will restore catalogs for all packages
158+
pnpm i # make sure the dependencies are installed
126159
pnpm restore-catalogs
127160
```
128161

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
"@parcel/packager-ts": "latest",
2424
"@parcel/transformer-typescript-types": "latest",
2525
"@types/node": "catalog:",
26+
"chalk": "^5.4.1",
27+
"chokidar": "^4.0.3",
2628
"eslint": "catalog:lint",
29+
"execa": "^9.5.2",
2730
"husky": "catalog:lint",
2831
"lint-staged": "catalog:lint",
2932
"prettier": "catalog:lint",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { runBuild } from './build.mjs'
2+
3+
runBuild()
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import chalk from 'chalk'
2+
import { execa } from 'execa'
3+
4+
const commandTag = '[@baseapp-frontend/components]'
5+
6+
export const runBuild = async (currentBuildProcesses = []) => {
7+
console.log(`${chalk.magenta(`${commandTag} Starting build process...`)}`)
8+
9+
try {
10+
console.log(chalk.cyanBright(`${commandTag} Running Relay Compiler...`))
11+
const relayProc = execa('pnpm', ['relay'], { preferLocal: true })
12+
currentBuildProcesses?.push(relayProc)
13+
await relayProc
14+
console.log(chalk.cyanBright(`${commandTag} Relay compilation completed.`))
15+
16+
console.log(chalk.yellowBright(`${commandTag} Running Babel transpiling...`))
17+
const babelProc = execa('pnpm', ['babel:transpile'], { preferLocal: true })
18+
currentBuildProcesses?.push(babelProc)
19+
await babelProc
20+
console.log(chalk.yellowBright(`${commandTag} Babel transpilation completed.`))
21+
22+
await Promise.all([
23+
(async () => {
24+
console.log(chalk.yellow(`${commandTag} Running tsup bundling...`))
25+
const tsupProc = execa('pnpm', ['tsup:bundle', '--silent'], { preferLocal: true })
26+
currentBuildProcesses?.push(tsupProc)
27+
await tsupProc
28+
console.log(chalk.yellow(`${commandTag} tsup Bundling completed.`))
29+
})(),
30+
(async () => {
31+
console.log(chalk.blue(`${commandTag} Running type declaration generation...`))
32+
const tscProc = execa('pnpm', ['tsc:declaration'], { preferLocal: true })
33+
currentBuildProcesses?.push(tscProc)
34+
await tscProc
35+
console.log(chalk.blue(`${commandTag} Type declarations generated.`))
36+
37+
console.log(chalk.cyan(`${commandTag} Copying DTS files...`))
38+
const copyProc = execa('pnpm', ['copy:dts'], { preferLocal: true })
39+
currentBuildProcesses?.push(copyProc)
40+
await copyProc
41+
console.log(chalk.cyan(`${commandTag} DTS files copied.`))
42+
})(),
43+
])
44+
45+
console.log(chalk.hex('#c86c2c')(`${commandTag} Cleaning temporary files...`))
46+
const cleanProc = execa('pnpm', ['clean:tmp'], { preferLocal: true })
47+
currentBuildProcesses?.push(cleanProc)
48+
await cleanProc
49+
console.log(chalk.hex('#c86c2c')(`${commandTag} Temporary files cleaned.`))
50+
51+
console.log(chalk.green(`${commandTag} Build completed successfully.`))
52+
} catch (error) {
53+
if (error.signal !== 'SIGTERM') {
54+
console.error(chalk.red(`${commandTag} Build failed:`))
55+
console.error(chalk.red(error.stderr || error.message))
56+
}
57+
throw error
58+
} finally {
59+
currentBuildProcesses = []
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import chalk from 'chalk'
2+
import chokidar from 'chokidar'
3+
import path from 'path'
4+
import { fileURLToPath } from 'url'
5+
6+
import {
7+
cancelCurrentBuild,
8+
getConsumerAppBasePath,
9+
updateConsumerRsync,
10+
waitForReadyFile,
11+
} from '../../../.scripts/command-utils.mjs'
12+
import { runBuild } from './build.mjs'
13+
14+
const currentDir = path.dirname(fileURLToPath(import.meta.url))
15+
const rootDir = path.join(currentDir, '..')
16+
17+
const commandTag = '[@baseapp-frontend/components]'
18+
19+
let isBuilding = false
20+
let needsRebuild = false
21+
let buildTimeout = null
22+
let currentBuildProcesses = []
23+
24+
const runWatchBuild = async () => {
25+
if (isBuilding) {
26+
needsRebuild = true
27+
await cancelCurrentBuild({ currentBuildProcesses, commandTag })
28+
return
29+
}
30+
31+
isBuilding = true
32+
33+
try {
34+
const consumerAppPath = getConsumerAppBasePath()
35+
36+
const designSystemPath = path.join(rootDir, '..', 'design-system')
37+
const readyFileParentPath = path.join(designSystemPath, 'dist')
38+
await waitForReadyFile({ readyFileParentPath, commandTag })
39+
40+
await runBuild(currentBuildProcesses)
41+
42+
await updateConsumerRsync({
43+
consumerAppPath,
44+
sourceDist: path.join(rootDir, 'dist/'),
45+
packageName: 'components',
46+
commandTag,
47+
})
48+
49+
console.log(`${chalk.magenta(`${commandTag} Watching for file changes...`)}`)
50+
} catch (error) {
51+
if (error.signal !== 'SIGTERM') {
52+
console.error(chalk.red(`${commandTag} Build failed:`))
53+
console.error(chalk.red(error.stderr || error.message))
54+
}
55+
} finally {
56+
isBuilding = false
57+
currentBuildProcesses = []
58+
if (needsRebuild) {
59+
needsRebuild = false
60+
runWatchBuild()
61+
}
62+
}
63+
}
64+
65+
const watchRegex = /^modules\/(?:.*\/)?(common|web|native)\/.*$/
66+
67+
const watcher = chokidar.watch(rootDir, {
68+
ignoreInitial: true,
69+
usePolling: true,
70+
interval: 100,
71+
awaitWriteFinish: {
72+
stabilityThreshold: 500,
73+
pollInterval: 100,
74+
},
75+
ignored: (filePath, stats) => {
76+
if (
77+
filePath.includes('node_modules') ||
78+
filePath.includes('dist') ||
79+
filePath.includes('tmp')
80+
) {
81+
return true
82+
}
83+
if (stats && stats.isFile()) {
84+
const relative = path.relative(rootDir, filePath).replace(/\\/g, '/')
85+
if (!watchRegex.test(relative)) {
86+
return true
87+
}
88+
}
89+
return false
90+
},
91+
})
92+
93+
watcher.on('all', (event, changedPath) => {
94+
const relativePath = path.relative(rootDir, changedPath).replace(/\\/g, '/')
95+
console.log(`${commandTag} Detected event "${event}" on: ${relativePath}`)
96+
if (buildTimeout) clearTimeout(buildTimeout)
97+
buildTimeout = setTimeout(runWatchBuild, 2000)
98+
})
99+
100+
runWatchBuild()

packages/components/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ pnpm install @baseapp-frontend/components
1818

1919
## **What is in here?**
2020

21-
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:
21+
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:
2222

2323
```bash
2424
# at root level
2525

26-
pnpm storybook --filter components
26+
pnpm storybook --filter @baseapp-frontend/components
2727
```
2828

2929
## **Build Process**

0 commit comments

Comments
 (0)