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 13 commits into
base: main
Choose a base branch
from
Open
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
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
151 changes: 151 additions & 0 deletions packages/qwik/src/cli/check-client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import type { AppCommand } from '../utils/app-command';
import { intro, log, outro } from '@clack/prompts';
import { 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))) {
await goBuild(app);
} else {
const manifest = await getManifestTs(getManifestPath(dist));
if (manifest === null) {
await goBuild(app);
} else {
if (await hasNewer(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();
intro('Building client (manifest missing or outdated)...');
const { install } = await runInPkg(pkgManager, ['run', 'build.client'], app.rootDir);
if (!(await install)) {
throw new Error('Client build command reported failure.');
}
outro('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(path: string): Promise<boolean> {
try {
await fs.access(getDiskPath(path));
return true; // Directory exists
} catch (err: any) {
if (!(err.code === 'ENOENT')) {
panic(`Error accessing disk directory ${path}: ${err.message}`);
}
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 hasNewer(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') {
log.warn(`Cannot read directory ${dir}: ${err.message}`);
}
return;
}

for (const item of items) {
if (returnValue) {
return;
}
const fullPath = path.join(dir, item.name);
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) {
log.warn(`Cannot access ${fullPath}: ${err.message}`);
}
}
}

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