Skip to content

Commit

Permalink
Support Vite projects in debug:cpu command (#2124)
Browse files Browse the repository at this point in the history
* Improve deferPromise utility for debugging

* Implement --watch in build command

* Support --codegen with --watch in build command

* Add --build to preview command

* Fix codegen flag relationship

* Use new flag in skeleton

* Pass --entry from preview to build

* Extract resource cleanup logic

* Setup resource cleanup for build watch

* Add --watch to preview command

* Replace command in examples

* Consider classic compiler in preview

* Support --diff in preview to test in examples

* Changesets

* Silence non-actionable build logs during preview

* Typo

* Support --diff in debug:cpu to test in examples

* Fix output filepath resolution

* Extract classic project logic

* Setup resource cleanup

* Rename hook

* Extract profiler logic

* Support Vite in debug:cpu

* Changesets

* Enable sourcemaps in debug:cpu

* Add more info to top-level script

* Try to fix mini-oxygen tests
  • Loading branch information
frandiox committed May 24, 2024
1 parent 608389d commit 5a554b2
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 101 deletions.
5 changes: 5 additions & 0 deletions .changeset/slow-colts-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli-hydrogen': minor
---

Support Vite projects in `h2 debug cpu` command.
16 changes: 16 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,22 @@
"multiple": false,
"type": "option"
},
"diff": {
"description": "Applies the current files on top of Hydrogen's starter template in a temporary directory.",
"hidden": true,
"name": "diff",
"required": false,
"allowNo": false,
"type": "boolean"
},
"entry": {
"description": "Entry file for the worker. Defaults to `./server`.",
"env": "SHOPIFY_HYDROGEN_FLAG_ENTRY",
"name": "entry",
"hasDynamicHelp": false,
"multiple": false,
"type": "option"
},
"output": {
"description": "Specify a path to generate the profile file. Defaults to \"startup.cpuprofile\".",
"name": "output",
Expand Down
11 changes: 7 additions & 4 deletions packages/cli/src/commands/hydrogen/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,22 @@ type RunBuildOptions = {
bundleStats?: boolean;
lockfileCheck?: boolean;
watch?: boolean;
onRebuild?: () => void | Promise<void>;
onServerBuildStart?: () => void | Promise<void>;
onServerBuildFinish?: () => void | Promise<void>;
};

export async function runBuild({
entry: ssrEntry,
directory,
useCodegen = false,
codegenConfigPath,
sourcemap = false,
sourcemap = true,
disableRouteWarning = false,
lockfileCheck = true,
assetPath = '/',
watch = false,
onRebuild,
onServerBuildStart,
onServerBuildFinish,
}: RunBuildOptions) {
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = 'production';
Expand Down Expand Up @@ -214,10 +216,11 @@ export async function runBuild({
// it might complain about missing files and loop infinitely.
serverBuildStatus?.resolve();
serverBuildStatus = deferPromise();
await onServerBuildStart?.();
},
async writeBundle() {
if (serverBuildStatus?.state !== 'rejected') {
await onRebuild?.();
await onServerBuildFinish?.();
}

serverBuildStatus.resolve();
Expand Down
175 changes: 90 additions & 85 deletions packages/cli/src/commands/hydrogen/debug/cpu.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import {Flags} from '@oclif/core';
import {joinPath, resolvePath} from '@shopify/cli-kit/node/path';
import Command from '@shopify/cli-kit/node/base-command';
import {outputInfo, outputWarn} from '@shopify/cli-kit/node/output';
import colors from '@shopify/cli-kit/node/colors';
import {outputInfo} from '@shopify/cli-kit/node/output';
import {writeFile} from '@shopify/cli-kit/node/fs';
import {AbortError} from '@shopify/cli-kit/node/error';
import colors from '@shopify/cli-kit/node/colors';
import ansiEscapes from 'ansi-escapes';
import {
type RemixConfig,
getProjectPaths,
getRemixConfig,
handleRemixImportFail,
type ServerMode,
hasRemixConfigFile,
} from '../../../lib/remix-config.js';
import {createRemixLogger, muteDevLogs} from '../../../lib/log.js';
import {muteDevLogs} from '../../../lib/log.js';
import {commonFlags, flagsToCamelObject} from '../../../lib/flags.js';
import {prepareDiffDirectory} from '../../../lib/template-diff.js';
import {runClassicCompilerDebugCpu} from '../../../lib/classic-compiler/debug-cpu.js';
import {setupResourceCleanup} from '../../../lib/resource-cleanup.js';
import {createCpuStartupProfiler} from '../../../lib/cpu-profiler.js';
import {importLocal} from '../../../lib/import-utils.js';
import {runBuild} from '../build.js';
import {getViteConfig} from '../../../lib/vite-config.js';

const DEFAULT_OUTPUT_PATH = 'startup.cpuprofile';

Expand All @@ -29,6 +28,8 @@ export default class DebugCpu extends Command {
static description = 'Builds and profiles the server startup time the app.';
static flags = {
...commonFlags.path,
...commonFlags.diff,
...commonFlags.entry,
output: Flags.string({
description: `Specify a path to generate the profile file. Defaults to "${DEFAULT_OUTPUT_PATH}".`,
default: DEFAULT_OUTPUT_PATH,
Expand All @@ -38,103 +39,107 @@ export default class DebugCpu extends Command {

async run(): Promise<void> {
const {flags} = await this.parse(DebugCpu);
const directory = flags.path ? resolvePath(flags.path) : process.cwd();
const output = flags.output
? resolvePath(flags.output)
: joinPath(process.cwd(), flags.output);
let directory = flags.path ? resolvePath(flags.path) : process.cwd();
const output = resolvePath(directory, flags.output);

if (flags.diff) {
directory = await prepareDiffDirectory(directory, true);
}

await runDebugCpu({
const {close} = await runDebugCpu({
...flagsToCamelObject(flags),
path: directory,
directory,
output,
});

setupResourceCleanup(close);
}
}

async function runDebugCpu({
path: appPath,
output = DEFAULT_OUTPUT_PATH,
}: {
path?: string;
output?: string;
}) {
type RunDebugCpuOptions = {
directory: string;
output: string;
entry?: string;
};

async function runDebugCpu({directory, entry, output}: RunDebugCpuOptions) {
if (!process.env.NODE_ENV) process.env.NODE_ENV = 'production';

muteDevLogs({workerReload: false});

const {root, buildPathWorkerFile} = getProjectPaths(appPath);
let {buildPath, buildPathWorkerFile} = getProjectPaths(directory);

if (!(await hasRemixConfigFile(root))) {
throw new AbortError(
'No remix.config.js file found. This command is not supported in Vite projects.',
);
}
const isClassicProject = await hasRemixConfigFile(directory);

outputInfo(
'⏳️ Starting profiler for CPU startup... Profile will be written to:\n' +
colors.dim(output),
);

const runProfiler = await createCpuStartupProfiler(root);
let times = 0;
let sourceEntrypoint: string;
const profiler = await createCpuStartupProfiler(directory);

type RemixWatch = typeof import('@remix-run/dev/dist/compiler/watch.js');
type RemixFileWatchCache =
typeof import('@remix-run/dev/dist/compiler/fileWatchCache.js');
const hooks = {
onServerBuildStart() {
if (times > 0) {
process.stdout.write(ansiEscapes.eraseLines(4));
}

const [{watch}, {createFileWatchCache}] = await Promise.all([
importLocal<RemixWatch>('@remix-run/dev/dist/compiler/watch.js', root),
importLocal<RemixFileWatchCache>(
'@remix-run/dev/dist/compiler/fileWatchCache.js',
root,
),
]).catch(handleRemixImportFail);
outputInfo(`\n#${++times} Building and profiling...`);
},
async onServerBuildFinish() {
const {profile, totalScriptTimeMs} = await profiler.run(
buildPathWorkerFile,
sourceEntrypoint,
);

let times = 0;
const fileWatchCache = createFileWatchCache();

await watch(
{
config: (await getRemixConfig(root)) as RemixConfig,
options: {
mode: process.env.NODE_ENV as ServerMode,
sourcemap: true,
},
fileWatchCache,
logger: createRemixLogger(),
process.stdout.write(ansiEscapes.eraseLines(2));
outputInfo(
`#${times} Total time: ${totalScriptTimeMs.toLocaleString()} ms` +
`\n${colors.dim(output)}`,
);

await writeFile(output, JSON.stringify(profile, null, 2));

outputInfo(`\nWaiting for changes...`);
},
{
onBuildStart() {
if (times > 0) {
process.stdout.write(ansiEscapes.eraseLines(4));
}

outputInfo(`\n#${++times} Building and profiling...`);
},
async onBuildFinish(context, duration, succeeded) {
if (succeeded) {
const {profile, totalScriptTimeMs} = await runProfiler(
buildPathWorkerFile,
);

process.stdout.write(ansiEscapes.eraseLines(2));
outputInfo(
`#${times} Total time: ${totalScriptTimeMs.toLocaleString()} ms` +
`\n${colors.dim(output)}`,
);

await writeFile(output, JSON.stringify(profile, null, 2));

outputInfo(`\nWaiting for changes...`);
} else {
outputWarn('\nBuild failed, waiting for changes to restart...');
}
},
async onFileChanged(file) {
fileWatchCache.invalidateFile(file);
},
async onFileDeleted(file) {
fileWatchCache.invalidateFile(file);
},
};

if (isClassicProject) {
return runClassicCompilerDebugCpu({
directory,
output,
buildPathWorkerFile,
hooks,
});
}

const maybeViteConfig = await getViteConfig(directory).catch(() => null);
buildPathWorkerFile =
maybeViteConfig?.serverOutFile ?? joinPath(buildPath, 'server', 'index.js');

sourceEntrypoint = maybeViteConfig?.remixConfig.serverEntryPoint ?? '';

const buildProcess = await runBuild({
entry,
directory,
watch: true,
sourcemap: true,
disableRouteWarning: true,
lockfileCheck: false,
...hooks,
onServerBuildStart() {
if (times === 0) {
process.stdout.write(ansiEscapes.eraseLines(1));
}
return hooks.onServerBuildStart();
},
);
});

return {
async close() {
await Promise.allSettled([buildProcess.close(), profiler.close()]);
},
};
}
2 changes: 1 addition & 1 deletion packages/cli/src/commands/hydrogen/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export async function runPreview({
: await runBuild({
...buildOptions,
watch,
async onRebuild() {
async onServerBuildFinish() {
if (projectBuild.state === 'pending') {
projectBuild.resolve();
} else {
Expand Down
72 changes: 72 additions & 0 deletions packages/cli/src/lib/classic-compiler/debug-cpu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {outputWarn} from '@shopify/cli-kit/node/output';
import {createRemixLogger} from '../log.js';
import {importLocal} from '../import-utils.js';
import {
getRemixConfig,
handleRemixImportFail,
type RemixConfig,
type ServerMode,
} from '../remix-config.js';

type DebugOptions = {
directory: string;
output: string;
buildPathWorkerFile: string;
hooks: {
onServerBuildStart: () => void | Promise<void>;
onServerBuildFinish: () => void | Promise<void>;
};
};

export async function runClassicCompilerDebugCpu({
directory,
hooks,
}: DebugOptions) {
type RemixWatch = typeof import('@remix-run/dev/dist/compiler/watch.js');
type RemixFileWatchCache =
typeof import('@remix-run/dev/dist/compiler/fileWatchCache.js');

const [{watch}, {createFileWatchCache}] = await Promise.all([
importLocal<RemixWatch>('@remix-run/dev/dist/compiler/watch.js', directory),
importLocal<RemixFileWatchCache>(
'@remix-run/dev/dist/compiler/fileWatchCache.js',
directory,
),
]).catch(handleRemixImportFail);

const fileWatchCache = createFileWatchCache();

const closeWatcher = await watch(
{
config: (await getRemixConfig(directory)) as RemixConfig,
options: {
mode: process.env.NODE_ENV as ServerMode,
sourcemap: true,
},
fileWatchCache,
logger: createRemixLogger(),
},
{
onBuildStart: hooks.onServerBuildStart,
async onBuildFinish(context, duration, succeeded) {
if (succeeded) {
await hooks.onServerBuildFinish();
} else {
outputWarn('\nBuild failed, waiting for changes to restart...');
}
},
async onFileChanged(file) {
fileWatchCache.invalidateFile(file);
},
async onFileDeleted(file) {
fileWatchCache.invalidateFile(file);
},
},
);

return {
async close() {
await closeWatcher();
},
};
}
Loading

0 comments on commit 5a554b2

Please sign in to comment.