Skip to content

Commit 01275e8

Browse files
JerryWu1234gioboa
andauthored
feat(cli): Add check-client command to verify bundle freshness (#7517)
* feat(cli): Add check-client command to verify bundle freshness * take some improvements according to a comment * take some improvements according to a comment * fix(cli): update check-client command and improve hints; adjust package versioning * refactor(cli): consolidate check-client functionality into a single file Move all check-client related functions and constants into a single index.ts file to improve maintainability and reduce file fragmentation * refactor(cli): update check-client command to accept src and dist paths Modify the check-client command to accept `src` and `dist` as arguments instead of hardcoding them. This change enhances flexibility and allows for more dynamic usage of the command across different adapters. The corresponding build scripts in various adapters' `package.json` files have been updated to pass these arguments. * refactor(cli): simplify client check and build logging Remove redundant log steps and improve error handling in the client check process. This change focuses on reducing noise in the logs and ensuring clearer error messages. * fix test * rename argument * refactor(cli): rename isNewerThan to hasNewer for clarity The function name `isNewerThan` was renamed to `hasNewer` to better reflect its purpose of checking if any files in the directory are newer than the given timestamp. This improves code readability and understanding. * clean up all intro outro warnings --------- Co-authored-by: Giorgio Boa <[email protected]>
1 parent 15cb441 commit 01275e8

File tree

16 files changed

+175
-13
lines changed

16 files changed

+175
-13
lines changed

.changeset/lucky-adults-tan.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@builder.io/qwik-city': patch
3+
'@builder.io/qwik': patch
4+
---
5+
6+
feat(cli): Add check-client command to verify bundle freshness
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import type { AppCommand } from '../utils/app-command';
2+
// Removed non-critical logging to keep command output silent unless there are serious issues
3+
import { red } from 'kleur/colors';
4+
import { runInPkg } from '../utils/install-deps';
5+
import { getPackageManager, panic } from '../utils/utils';
6+
import fs from 'fs/promises';
7+
import type { Stats } from 'fs';
8+
import path from 'path';
9+
10+
const getDiskPath = (dist: string) => path.resolve(dist);
11+
const getSrcPath = (src: string) => path.resolve(src);
12+
const getManifestPath = (dist: string) => path.resolve(dist, 'q-manifest.json');
13+
14+
export async function runQwikClientCommand(app: AppCommand) {
15+
try {
16+
const src = app.args[1];
17+
const dist = app.args[2];
18+
await checkClientCommand(app, src, dist);
19+
} catch (e) {
20+
console.error(`❌ ${red(String(e))}\n`);
21+
process.exit(1);
22+
}
23+
}
24+
25+
/**
26+
* Handles the core logic for the 'check-client' command. Exports this function so other modules can
27+
* import and call it.
28+
*
29+
* @param {AppCommand} app - Application command context (assuming structure).
30+
*/
31+
async function checkClientCommand(app: AppCommand, src: string, dist: string): Promise<void> {
32+
if (!(await clientDirExists(dist))) {
33+
await goBuild(app);
34+
} else {
35+
const manifest = await getManifestTs(getManifestPath(dist));
36+
if (manifest === null) {
37+
await goBuild(app);
38+
} else {
39+
if (await hasNewer(getSrcPath(src), manifest)) {
40+
await goBuild(app);
41+
}
42+
}
43+
}
44+
}
45+
46+
/**
47+
* Builds the application using the appropriate package manager.
48+
*
49+
* @param {AppCommand} app - The application command object containing app details.e path to the
50+
* manifest file (though it's not used in the current function).
51+
* @throws {Error} Throws an error if the build process encounters any issues.
52+
*/
53+
54+
async function goBuild(app: AppCommand) {
55+
const pkgManager = getPackageManager();
56+
const { install } = await runInPkg(pkgManager, ['run', 'build.client'], app.rootDir);
57+
if (!(await install)) {
58+
throw new Error('Client build command reported failure.');
59+
}
60+
}
61+
62+
/**
63+
* Retrieves the last modified timestamp of the manifest file.
64+
*
65+
* @param {string} manifestPath - The path to the manifest file.
66+
* @returns {Promise<number | null>} Returns the last modified timestamp (in milliseconds) of the
67+
* manifest file, or null if an error occurs.
68+
*/
69+
async function getManifestTs(manifestPath: string) {
70+
try {
71+
// Get stats for the manifest file
72+
const stats: Stats = await fs.stat(manifestPath);
73+
return stats.mtimeMs;
74+
} catch (err: any) {
75+
// Handle errors accessing the manifest file
76+
if (err.code !== 'ENOENT') {
77+
panic(`Error accessing manifest file ${manifestPath}: ${err.message}`);
78+
}
79+
return null;
80+
}
81+
}
82+
83+
/**
84+
* Checks if the specified disk directory exists and is accessible.
85+
*
86+
* @returns {Promise<boolean>} Returns true if the directory exists and can be accessed, returns
87+
* false if it doesn't exist or an error occurs.
88+
*/
89+
export async function clientDirExists(path: string): Promise<boolean> {
90+
try {
91+
await fs.access(getDiskPath(path));
92+
return true; // Directory exists
93+
} catch (err: any) {
94+
if (!(err.code === 'ENOENT')) {
95+
panic(`Error accessing disk directory ${path}: ${err.message}`);
96+
}
97+
return false; // Directory doesn't exist or there was an error
98+
}
99+
}
100+
101+
/**
102+
* Recursively finds the latest modification time (mtime) of any file in the given directory.
103+
*
104+
* @param {string} srcPath - The directory path to search.
105+
* @returns {Promise<number>} Returns the latest mtime (Unix timestamp in milliseconds), or 0 if the
106+
* directory doesn't exist or is empty.
107+
*/
108+
export async function hasNewer(srcPath: string, timestamp: number): Promise<boolean> {
109+
let returnValue = false;
110+
async function traverse(dir: string): Promise<void> {
111+
if (returnValue) {
112+
return;
113+
}
114+
let items: Array<import('fs').Dirent>;
115+
try {
116+
items = await fs.readdir(dir, { withFileTypes: true });
117+
} catch (err: any) {
118+
return;
119+
}
120+
121+
for (const item of items) {
122+
if (returnValue) {
123+
return;
124+
}
125+
const fullPath = path.join(dir, item.name);
126+
try {
127+
if (item.isDirectory()) {
128+
await traverse(fullPath);
129+
} else if (item.isFile()) {
130+
const stats = await fs.stat(fullPath);
131+
if (stats.mtimeMs > timestamp) {
132+
returnValue = true;
133+
return;
134+
}
135+
}
136+
} catch (err: any) {
137+
// Intentionally silent for non-critical access issues
138+
}
139+
}
140+
}
141+
142+
await traverse(srcPath);
143+
return returnValue;
144+
}

packages/qwik/src/cli/run.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { note, panic, pmRunCmd, printHeader, bye } from './utils/utils';
88
import { runBuildCommand } from './utils/run-build-command';
99
import { intro, isCancel, select, confirm } from '@clack/prompts';
1010
import { runV2Migration } from './migrate-v2/run-migration';
11+
import { runQwikClientCommand } from './check-client';
1112

1213
const SPACE_TO_HINT = 18;
1314
const COMMANDS = [
@@ -53,6 +54,13 @@ const COMMANDS = [
5354
run: (app: AppCommand) => runV2Migration(app),
5455
showInHelp: false,
5556
},
57+
{
58+
value: 'check-client',
59+
label: 'check-client',
60+
hint: 'Make sure the client bundle is up-to-date with the source code',
61+
run: (app: AppCommand) => runQwikClientCommand(app),
62+
showInHelp: true,
63+
},
5664
{
5765
value: 'help',
5866
label: 'help',
@@ -110,6 +118,10 @@ async function runCommand(app: AppCommand) {
110118
await runV2Migration(app);
111119
return;
112120
}
121+
case 'check-client': {
122+
await runQwikClientCommand(app);
123+
return;
124+
}
113125
case 'version': {
114126
printVersion();
115127
return;

starters/adapters/aws-lambda/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"description": "AWS Lambda",
33
"scripts": {
4-
"build.server": "vite build -c adapters/aws-lambda/vite.config.ts",
4+
"build.server": "qwik check-client src dist && vite build -c adapters/aws-lambda/vite.config.ts",
55
"serve": "qwik build && serverless offline",
66
"deploy": "serverless deploy"
77
},

starters/adapters/azure-swa/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"description": "Azure Static Web Apps",
33
"scripts": {
4-
"build.server": "vite build -c adapters/azure-swa/vite.config.ts",
4+
"build.server": "qwik check-client src dist && vite build -c adapters/azure-swa/vite.config.ts",
55
"serve": "swa start"
66
},
77
"devDependencies": {

starters/adapters/bun/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"description": "Bun server",
33
"scripts": {
4-
"build.server": "vite build -c adapters/bun/vite.config.ts",
4+
"build.server": "qwik check-client src dist && vite build -c adapters/bun/vite.config.ts",
55
"serve": "bun server/entry.bun.js"
66
},
77
"__qwik__": {

starters/adapters/cloud-run/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"description": "Google Cloud Run server",
33
"scripts": {
4-
"build.server": "vite build -c adapters/cloud-run/vite.config.ts",
4+
"build.server": "qwik check-client src dist && vite build -c adapters/cloud-run/vite.config.ts",
55
"deploy": "gcloud run deploy my-cloud-run-app --source ."
66
},
77
"__qwik__": {

starters/adapters/cloudflare-pages/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"description": "Cloudflare Pages",
33
"scripts": {
4-
"build.server": "vite build -c adapters/cloudflare-pages/vite.config.ts",
4+
"build.server": "qwik check-client src dist && vite build -c adapters/cloudflare-pages/vite.config.ts",
55
"deploy": "wrangler pages deploy ./dist",
66
"serve": "wrangler pages dev ./dist --compatibility-flags=nodejs_als"
77
},

starters/adapters/deno/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"description": "Deno server",
33
"scripts": {
4-
"build.server": "vite build -c adapters/deno/vite.config.ts",
4+
"build.server": "qwik check-client src dist && vite build -c adapters/deno/vite.config.ts",
55
"serve": "deno run --allow-net --allow-read --allow-env server/entry.deno.js"
66
},
77
"__qwik__": {

starters/adapters/express/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"description": "Express.js server",
33
"scripts": {
4-
"build.server": "vite build -c adapters/express/vite.config.ts",
4+
"build.server": "qwik check-client src dist && vite build -c adapters/express/vite.config.ts",
55
"serve": "node server/entry.express"
66
},
77
"dependencies": {

0 commit comments

Comments
 (0)