Skip to content

feat(cli): Add check-client command to verify bundle freshness #7517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions .changeset/lucky-adults-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@builder.io/qwik-city': patch
'@builder.io/qwik': patch
---

feat(cli): Add check-client command to verify bundle freshness
155 changes: 155 additions & 0 deletions packages/qwik/src/cli/check-client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import type { AppCommand } from '../utils/app-command';
import { intro, log, outro } from '@clack/prompts';
import { bgBlue, bgMagenta, bold, cyan, red } from 'kleur/colors';
import { runInPkg } from '../utils/install-deps';
import { getPackageManager, panic } from '../utils/utils';
import fs from 'fs/promises';
import type { Stats } from 'fs';
import path from 'path';

const getDiskPath = (dist: string) => path.resolve(dist);
const getSrcPath = (src: string) => path.resolve(src);
const getManifestPath = (dist: string) => path.resolve(dist, 'q-manifest.json');

export async function runQwikClientCommand(app: AppCommand) {
try {
const src = app.args[1];
const dist = app.args[2];
await checkClientCommand(app, src, dist);
} catch (e) {
console.error(`❌ ${red(String(e))}\n`);
process.exit(1);
}
}

/**
* Handles the core logic for the 'check-client' command. Exports this function so other modules can
* import and call it.
*
* @param {AppCommand} app - Application command context (assuming structure).
*/
async function checkClientCommand(app: AppCommand, src: string, dist: string): Promise<void> {
if (!(await clientDirExists(dist))) {
intro(`🚀 ${bgBlue(bold(' Qwik Client Check '))}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this

log.step(`Checking Disk directory: ${cyan(dist)}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this

await goBuild(app);
} else {
log.step(`Checking q-manifest.json file: ${cyan(dist)}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this

const manifest = await getManifestTs(getManifestPath(dist));
if (manifest === null) {
await goBuild(app);
} else {
log.step(`Compare source modification time with q-manifest.json modification time`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this

if (await isNewerThan(getSrcPath(src), manifest)) {
await goBuild(app);
}
}
}
}

/**
* Builds the application using the appropriate package manager.
*
* @param {AppCommand} app - The application command object containing app details.e path to the
* manifest file (though it's not used in the current function).
* @throws {Error} Throws an error if the build process encounters any issues.
*/

async function goBuild(app: AppCommand) {
const pkgManager = getPackageManager();
log.step('Building client (manifest missing or outdated)...');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can make this intro

try {
const { install } = await runInPkg(pkgManager, ['run', 'build.client'], app.rootDir);
if (!(await install)) {
throw new Error('Client build command reported failure.');
}
} catch (buildError: any) {
log.error(`Build error: ${buildError.message}`);
throw new Error('Client build process encountered an error.');
}
Comment on lines +66 to +69
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for the catch, runInPkg already does that

outro(`✅ ${bgMagenta(bold(' Check complete '))}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change this to say 'Client build complete'

}

/**
* Retrieves the last modified timestamp of the manifest file.
*
* @param {string} manifestPath - The path to the manifest file.
* @returns {Promise<number | null>} Returns the last modified timestamp (in milliseconds) of the
* manifest file, or null if an error occurs.
*/
async function getManifestTs(manifestPath: string) {
try {
// Get stats for the manifest file
const stats: Stats = await fs.stat(manifestPath);
return stats.mtimeMs;
} catch (err: any) {
// Handle errors accessing the manifest file
if (err.code === 'ENOENT') {
log.warn(`q-manifest.json file not found`);
} else {
panic(`Error accessing manifest file ${manifestPath}: ${err.message}`);
}
return null;
}
}

/**
* Checks if the specified disk directory exists and is accessible.
*
* @returns {Promise<boolean>} Returns true if the directory exists and can be accessed, returns
* false if it doesn't exist or an error occurs.
*/
export async function clientDirExists(dist: string): Promise<boolean> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since you're using it for both src and dist, and it doesn't have anything to do with the client, you should rename this to just dirExists. Also, a better name for the parameter would be path

try {
await fs.access(getDiskPath(dist));
return true; // Directory exists
} catch (err: any) {
panic(`Error accessing disk directory ${dist}: ${err.message}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the err.code is ENOENT then there's no need for panic

return false; // Directory doesn't exist or there was an error
}
}

/**
* Recursively finds the latest modification time (mtime) of any file in the given directory.
*
* @param {string} srcPath - The directory path to search.
* @returns {Promise<number>} Returns the latest mtime (Unix timestamp in milliseconds), or 0 if the
* directory doesn't exist or is empty.
*/
export async function isNewerThan(srcPath: string, timestamp: number): Promise<boolean> {
let returnValue = false;
async function traverse(dir: string): Promise<void> {
if (returnValue) {
return;
}
let items: Array<import('fs').Dirent>;
try {
items = await fs.readdir(dir, { withFileTypes: true });
} catch (err: any) {
if (err.code !== 'ENOENT') {
console.warn(`Cannot read directory ${dir}: ${err.message}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be log.warn

}
return;
}

for (const item of items) {
const fullPath = path.join(dir, item.name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put the returnValue check here so it also stops stat()ing files in the same directory

try {
if (item.isDirectory()) {
await traverse(fullPath);
} else if (item.isFile()) {
const stats = await fs.stat(fullPath);
if (stats.mtimeMs > timestamp) {
returnValue = true;
return;
}
}
} catch (err: any) {
console.warn(`Cannot access ${fullPath}: ${err.message}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

}
}
}

await traverse(srcPath);
return returnValue;
}
12 changes: 12 additions & 0 deletions packages/qwik/src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { note, panic, pmRunCmd, printHeader, bye } from './utils/utils';
import { runBuildCommand } from './utils/run-build-command';
import { intro, isCancel, select, confirm } from '@clack/prompts';
import { runV2Migration } from './migrate-v2/run-migration';
import { runQwikClientCommand } from './check-client';

const SPACE_TO_HINT = 18;
const COMMANDS = [
Expand Down Expand Up @@ -53,6 +54,13 @@ const COMMANDS = [
run: (app: AppCommand) => runV2Migration(app),
showInHelp: false,
},
{
value: 'check-client',
label: 'check-client',
hint: 'Make sure the client bundle is up-to-date with the source code',
run: (app: AppCommand) => runQwikClientCommand(app),
showInHelp: true,
},
{
value: 'help',
label: 'help',
Expand Down Expand Up @@ -110,6 +118,10 @@ async function runCommand(app: AppCommand) {
await runV2Migration(app);
return;
}
case 'check-client': {
await runQwikClientCommand(app);
return;
}
case 'version': {
printVersion();
return;
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/aws-lambda/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "AWS Lambda",
"scripts": {
"build.server": "vite build -c adapters/aws-lambda/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/aws-lambda/vite.config.ts",
"serve": "qwik build && serverless offline",
"deploy": "serverless deploy"
},
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/azure-swa/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Azure Static Web Apps",
"scripts": {
"build.server": "vite build -c adapters/azure-swa/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/azure-swa/vite.config.ts",
"serve": "swa start"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/bun/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Bun server",
"scripts": {
"build.server": "vite build -c adapters/bun/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/bun/vite.config.ts",
"serve": "bun server/entry.bun.js"
},
"__qwik__": {
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/cloud-run/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Google Cloud Run server",
"scripts": {
"build.server": "vite build -c adapters/cloud-run/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/cloud-run/vite.config.ts",
"deploy": "gcloud run deploy my-cloud-run-app --source ."
},
"__qwik__": {
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/cloudflare-pages/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Cloudflare Pages",
"scripts": {
"build.server": "vite build -c adapters/cloudflare-pages/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/cloudflare-pages/vite.config.ts",
"deploy": "wrangler pages deploy ./dist",
"serve": "wrangler pages dev ./dist --compatibility-flags=nodejs_als"
},
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/deno/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Deno server",
"scripts": {
"build.server": "vite build -c adapters/deno/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/deno/vite.config.ts",
"serve": "deno run --allow-net --allow-read --allow-env server/entry.deno.js"
},
"__qwik__": {
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/express/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Express.js server",
"scripts": {
"build.server": "vite build -c adapters/express/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/express/vite.config.ts",
"serve": "node server/entry.express"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/fastify/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Fastify server",
"scripts": {
"build.server": "vite build -c adapters/fastify/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/fastify/vite.config.ts",
"serve": "node server/entry.fastify"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/firebase/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Firebase",
"scripts": {
"build.server": "vite build -c adapters/firebase/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/firebase/vite.config.ts",
"serve": "qwik build && firebase emulators:start",
"deploy": "firebase deploy"
},
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/netlify-edge/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Netlify Edge Functions",
"scripts": {
"build.server": "vite build -c adapters/netlify-edge/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/netlify-edge/vite.config.ts",
"deploy": "netlify deploy --build"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/node-server/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Vanilla Node server",
"scripts": {
"build.server": "vite build -c adapters/node-server/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/node-server/vite.config.ts",
"serve": "node server/entry.node-server"
},
"__qwik__": {
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/static/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Static Site Generator",
"scripts": {
"build.server": "vite build -c adapters/static/vite.config.ts"
"build.server": "qwik check-client src dist && vite build -c adapters/static/vite.config.ts"
},
"__qwik__": {
"priority": 10,
Expand Down
2 changes: 1 addition & 1 deletion starters/adapters/vercel-edge/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"description": "Vercel Edge Functions",
"scripts": {
"build.server": "vite build -c adapters/vercel-edge/vite.config.ts",
"build.server": "qwik check-client src dist && vite build -c adapters/vercel-edge/vite.config.ts",
"deploy": "vercel deploy"
},
"devDependencies": {
Expand Down