Skip to content

Commit a4e829e

Browse files
committed
fun: Fun mode.
1 parent ef77182 commit a4e829e

12 files changed

Lines changed: 439 additions & 23 deletions

File tree

packages/cli/assets/error.png

1.77 MB
Loading

packages/cli/binding/index.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,4 +771,5 @@ module.exports.rewritePrettier = nativeBinding.rewritePrettier;
771771
module.exports.rewriteScripts = nativeBinding.rewriteScripts;
772772
module.exports.run = nativeBinding.run;
773773
module.exports.runCommand = nativeBinding.runCommand;
774+
module.exports.takeCheckFailureKind = nativeBinding.takeCheckFailureKind;
774775
module.exports.vitePlusHeader = nativeBinding.vitePlusHeader;

packages/cli/binding/index.d.cts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,5 +369,7 @@ export interface RunCommandResult {
369369
pathAccesses: Record<string, PathAccess>;
370370
}
371371

372+
export declare function takeCheckFailureKind(): string | null;
373+
372374
/** Render the Vite+ header using the Rust implementation. */
373375
export declare function vitePlusHeader(): string;

packages/cli/binding/src/cli.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,13 @@ impl LintMessageKind {
832832
Self::LintAndTypeCheck => "Lint or type issues found",
833833
}
834834
}
835+
836+
fn failure_kind(self) -> &'static str {
837+
match self {
838+
Self::LintOnly => "lint",
839+
Self::LintAndTypeCheck => "type-aware",
840+
}
841+
}
835842
}
836843

837844
fn parse_check_summary(line: &str) -> Option<CheckSummary> {
@@ -1098,6 +1105,7 @@ async fn execute_direct_subcommand(
10981105
"Formatting failed during fix",
10991106
);
11001107
}
1108+
crate::set_last_check_failure_kind(Some("formatting"));
11011109
return Ok(status);
11021110
}
11031111
}
@@ -1175,6 +1183,7 @@ async fn execute_direct_subcommand(
11751183
}
11761184
}
11771185
if status != ExitStatus::SUCCESS {
1186+
crate::set_last_check_failure_kind(Some(lint_message_kind.failure_kind()));
11781187
return Ok(status);
11791188
}
11801189
}
@@ -1211,6 +1220,7 @@ async fn execute_direct_subcommand(
12111220
&combined_output,
12121221
"Formatting failed after lint fixes were applied",
12131222
);
1223+
crate::set_last_check_failure_kind(Some("formatting"));
12141224
return Ok(status);
12151225
}
12161226
if let Some(started) = fmt_fix_started {
@@ -1663,6 +1673,12 @@ mod tests {
16631673
assert_eq!(kind.success_label(), "Found no warnings, lint errors, or type errors");
16641674
assert_eq!(kind.warning_heading(), "Lint or type warnings found");
16651675
assert_eq!(kind.issue_heading(), "Lint or type issues found");
1676+
assert_eq!(kind.failure_kind(), "type-aware");
1677+
}
1678+
1679+
#[test]
1680+
fn lint_message_kind_reports_plain_lint_failures() {
1681+
assert_eq!(LintMessageKind::LintOnly.failure_kind(), "lint");
16661682
}
16671683

16681684
#[test]

packages/cli/binding/src/lib.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ mod package_manager;
1818
#[allow(dead_code)]
1919
mod utils;
2020

21-
use std::{collections::HashMap, error::Error as StdError, ffi::OsStr, fmt::Write as _, sync::Arc};
21+
use std::{
22+
collections::HashMap,
23+
error::Error as StdError,
24+
ffi::OsStr,
25+
fmt::Write as _,
26+
sync::{Arc, LazyLock, Mutex},
27+
};
2228

2329
use napi::{anyhow, bindgen_prelude::*, threadsafe_function::ThreadsafeFunction};
2430
use napi_derive::napi;
@@ -28,6 +34,21 @@ use crate::cli::{
2834
BoxedResolverFn, CliOptions as ViteTaskCliOptions, ResolveCommandResult, ViteConfigResolverFn,
2935
};
3036

37+
static LAST_CHECK_FAILURE_KIND: LazyLock<Mutex<Option<String>>> =
38+
LazyLock::new(|| Mutex::new(None));
39+
40+
pub(crate) fn set_last_check_failure_kind(kind: Option<&str>) {
41+
let mut last_failure_kind =
42+
LAST_CHECK_FAILURE_KIND.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
43+
*last_failure_kind = kind.map(str::to_owned);
44+
}
45+
46+
fn take_last_check_failure_kind() -> Option<String> {
47+
let mut last_failure_kind =
48+
LAST_CHECK_FAILURE_KIND.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
49+
last_failure_kind.take()
50+
}
51+
3152
/// Module initialization - sets up tracing for debugging
3253
#[napi_derive::module_init]
3354
pub fn init() {
@@ -130,6 +151,8 @@ fn format_error_message(error: &(dyn StdError + 'static)) -> String {
130151
/// and process JavaScript callbacks (via ThreadsafeFunction).
131152
#[napi]
132153
pub async fn run(options: CliOptions) -> Result<i32> {
154+
set_last_check_failure_kind(None);
155+
133156
// Use provided cwd or current directory
134157
let mut cwd = current_dir()?;
135158
if let Some(options_cwd) = options.cwd {
@@ -198,6 +221,11 @@ pub async fn run(options: CliOptions) -> Result<i32> {
198221
}
199222
}
200223

224+
#[napi]
225+
pub fn take_check_failure_kind() -> Option<String> {
226+
take_last_check_failure_kind()
227+
}
228+
201229
/// Render the Vite+ header using the Rust implementation.
202230
#[napi]
203231
pub fn vite_plus_header() -> String {

packages/cli/build.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
import { execSync } from 'node:child_process';
2222
import { existsSync, globSync, readFileSync, readdirSync, statSync } from 'node:fs';
23-
import { copyFile, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
23+
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2424
import { dirname, join } from 'node:path';
2525
import { fileURLToPath } from 'node:url';
2626
import { parseArgs } from 'node:util';
@@ -60,6 +60,7 @@ const napiArgs = process.argv
6060

6161
if (!skipTs) {
6262
await buildCli();
63+
await copyCliAssets();
6364
buildGlobalModules();
6465
generateLicenseFile({
6566
title: 'Vite-Plus CLI license',
@@ -165,10 +166,11 @@ async function buildCli() {
165166
console.error(formatDiagnostics(cjsDiagnostics, cjsHost));
166167
process.exit(1);
167168
}
168-
await rename(
169-
join(projectDir, 'dist/define-config.js'),
170-
join(projectDir, 'dist/define-config.cjs'),
171-
);
169+
const defineConfigJsPath = join(projectDir, 'dist/define-config.js');
170+
const defineConfigCjsPath = join(projectDir, 'dist/define-config.cjs');
171+
if (existsSync(defineConfigJsPath)) {
172+
await copyFile(defineConfigJsPath, defineConfigCjsPath);
173+
}
172174

173175
const host = createCompilerHost(options);
174176

@@ -196,6 +198,30 @@ async function buildCli() {
196198
console.error(formatDiagnostics(diagnostics, host));
197199
process.exit(1);
198200
}
201+
202+
if (existsSync(defineConfigJsPath)) {
203+
await copyFile(defineConfigJsPath, defineConfigCjsPath);
204+
} else if (existsSync(defineConfigCjsPath)) {
205+
await copyFile(defineConfigCjsPath, defineConfigJsPath);
206+
}
207+
}
208+
209+
async function copyCliAssets() {
210+
const assetsDir = join(projectDir, 'assets');
211+
if (!existsSync(assetsDir)) {
212+
return;
213+
}
214+
215+
const distAssetsDir = join(projectDir, 'dist', 'assets');
216+
await mkdir(distAssetsDir, { recursive: true });
217+
218+
for (const entry of readdirSync(assetsDir)) {
219+
const sourcePath = join(assetsDir, entry);
220+
if (!statSync(sourcePath).isFile()) {
221+
continue;
222+
}
223+
await copyFile(sourcePath, join(distAssetsDir, entry));
224+
}
199225
}
200226

201227
function buildGlobalModules() {

packages/cli/package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@
102102
"types": "./dist/test/internal/browser.d.ts",
103103
"default": "./dist/test/internal/browser.js"
104104
},
105+
"./test/internal/module-runner": {
106+
"types": "./dist/test/internal/module-runner.d.ts",
107+
"default": "./dist/test/internal/module-runner.js"
108+
},
105109
"./test/runners": {
106110
"types": "./dist/test/runners.d.ts",
107111
"default": "./dist/test/runners.js"
@@ -131,9 +135,9 @@
131135
"types": "./dist/test/snapshot.d.ts",
132136
"default": "./dist/test/snapshot.js"
133137
},
134-
"./test/runtime": {
135-
"types": "./dist/test/runtime.d.ts",
136-
"default": "./dist/test/runtime.js"
138+
"./test/mocker": {
139+
"types": "./dist/test/mocker.d.ts",
140+
"default": "./dist/test/mocker.js"
137141
},
138142
"./test/worker": {
139143
"types": "./dist/test/worker.d.ts",

packages/cli/src/bin.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import path from 'node:path';
1414

15-
import { run } from '../binding/index.js';
15+
import { run, takeCheckFailureKind } from '../binding/index.js';
1616
import { applyToolInitConfigToViteConfig, inspectInitCommand } from './init-config.js';
1717
import { doc } from './resolve-doc.js';
1818
import { fmt } from './resolve-fmt.js';
@@ -21,6 +21,7 @@ import { pack } from './resolve-pack.js';
2121
import { test } from './resolve-test.js';
2222
import { resolveUniversalViteConfig } from './resolve-vite-config.js';
2323
import { vite } from './resolve-vite.js';
24+
import { normalizeCheckFailureKind, printCheckFailureImage } from './utils/check-failure-image.js';
2425
import { accent, errorMsg, log } from './utils/terminal.js';
2526

2627
function getErrorMessage(err: unknown): string {
@@ -94,6 +95,13 @@ if (command === 'create') {
9495
});
9596

9697
let finalExitCode = exitCode;
98+
if (command === 'check' && finalExitCode !== 0) {
99+
await printCheckFailureImage(
100+
process.stderr,
101+
process.env,
102+
normalizeCheckFailureKind(takeCheckFailureKind()),
103+
);
104+
}
97105
if (exitCode === 0) {
98106
try {
99107
const result = await applyToolInitConfigToViteConfig(command, args.slice(1));
@@ -126,6 +134,13 @@ if (command === 'create') {
126134
process.exit(finalExitCode);
127135
} catch (err) {
128136
errorMsg(getErrorMessage(err));
137+
if (command === 'check') {
138+
await printCheckFailureImage(
139+
process.stderr,
140+
process.env,
141+
normalizeCheckFailureKind(takeCheckFailureKind()),
142+
);
143+
}
129144
process.exit(1);
130145
}
131146
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import {
4+
buildKittyImageSequence,
5+
buildOsc1337FileSequence,
6+
detectInlineImageProtocol,
7+
getCheckFailureCaption,
8+
getImagePlacement,
9+
normalizeCheckFailureKind,
10+
readPngDimensions,
11+
} from '../check-failure-image.js';
12+
13+
describe('detectInlineImageProtocol', () => {
14+
it('prefers the OSC 1337 file protocol for iTerm2 and WezTerm', () => {
15+
expect(detectInlineImageProtocol({ TERM_PROGRAM: 'iTerm.app' })).toBe('file');
16+
expect(detectInlineImageProtocol({ TERM_PROGRAM: 'WezTerm' })).toBe('file');
17+
});
18+
19+
it('uses kitty graphics for kitty-compatible terminals', () => {
20+
expect(detectInlineImageProtocol({ KITTY_WINDOW_ID: '12' })).toBe('kitty');
21+
expect(detectInlineImageProtocol({ TERM_PROGRAM: 'ghostty' })).toBe('kitty');
22+
expect(detectInlineImageProtocol({ TERM: 'xterm-kitty' })).toBe('kitty');
23+
});
24+
25+
it('falls back to sixel when advertised and an encoder is available', () => {
26+
expect(detectInlineImageProtocol({ TERM_FEATURES: 'RGBSx' }, { canRenderSixel: true })).toBe(
27+
'sixel',
28+
);
29+
});
30+
31+
it('returns null when no supported image protocol is available', () => {
32+
expect(detectInlineImageProtocol({ TERM: 'xterm-256color' })).toBeNull();
33+
});
34+
});
35+
36+
describe('inline image sequences', () => {
37+
it('builds a kitty graphics sequence with the encoded file path', () => {
38+
const sequence = buildKittyImageSequence('/tmp/error.png', {
39+
columns: 26,
40+
rows: 17,
41+
});
42+
43+
expect(sequence).toContain('a=T,t=f,f=100,c=26,r=17');
44+
expect(sequence).toContain(Buffer.from('/tmp/error.png').toString('base64'));
45+
expect(sequence).toContain('\u001b_G');
46+
});
47+
48+
it('builds an OSC 1337 file sequence with the encoded file contents', () => {
49+
const sequence = buildOsc1337FileSequence('/tmp/error.png', Buffer.from('png-bytes'), {
50+
columns: 26,
51+
rows: 17,
52+
});
53+
54+
expect(sequence).toContain(']1337;File=');
55+
expect(sequence).toContain('width=26;height=17;preserveAspectRatio=1;inline=1');
56+
expect(sequence).toContain(Buffer.from('error.png').toString('base64'));
57+
expect(sequence).toContain(Buffer.from('png-bytes').toString('base64'));
58+
});
59+
});
60+
61+
describe('PNG helpers', () => {
62+
it('reads dimensions from the PNG header and derives a bounded placement', () => {
63+
const png = Buffer.from(
64+
'89504e470d0a1a0a0000000d494844520000038a000004c0080600000000000000',
65+
'hex',
66+
);
67+
68+
expect(readPngDimensions(png)).toEqual({ width: 906, height: 1216 });
69+
expect(getImagePlacement({ width: 906, height: 1216 })).toEqual({
70+
columns: 26,
71+
rows: 17,
72+
});
73+
});
74+
});
75+
76+
describe('fun mode captions', () => {
77+
it('uses the requested default caption for generic failures', () => {
78+
expect(getCheckFailureCaption('error')).toBe(
79+
'The static analysis enthusiast says: outstanding, another error',
80+
);
81+
});
82+
83+
it('varies the caption by failure kind', () => {
84+
expect(getCheckFailureCaption('formatting')).toContain('another formatting error');
85+
expect(getCheckFailureCaption('lint')).toContain('another lint error');
86+
expect(getCheckFailureCaption('type-aware')).toContain('another type-aware error');
87+
});
88+
89+
it('normalizes unknown failure kinds to the generic error caption', () => {
90+
expect(normalizeCheckFailureKind('formatting')).toBe('formatting');
91+
expect(normalizeCheckFailureKind('lint')).toBe('lint');
92+
expect(normalizeCheckFailureKind('type-aware')).toBe('type-aware');
93+
expect(normalizeCheckFailureKind('something-else')).toBe('error');
94+
expect(normalizeCheckFailureKind(null)).toBe('error');
95+
});
96+
});

0 commit comments

Comments
 (0)