visual-remote 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,16 +18,20 @@ Node.js와 pnpm 버전은 각각 `.nvmrc`와 `package.json`에 고정되어 있
18
18
 
19
19
  ## 가장 빠른 사용
20
20
 
21
- Vite 앱의 `package.json`과 `vite.config`가 있는 폴더에서 한 번만 초기화합니다.
21
+ Vite 또는 Next.js 앱의 `package.json`이 있는 폴더에서 한 번만 초기화합니다.
22
+ Next.js 자동 통합은 `instrumentation-client`를 지원하는 Next.js 15.3 이상이
23
+ 필요합니다.
22
24
 
23
25
  ```bash
24
26
  npx --yes visual-remote@latest init
25
27
  ```
26
28
 
27
- `init`은 다음 작업을 수행합니다.
29
+ `init`은 프로젝트 종류를 감지하고 다음 작업을 수행합니다.
28
30
 
29
31
  - 현재 프로젝트에 `visual-remote`를 개발 의존성으로 설치합니다.
30
- - `vite.config`의 플러그인 배열에 `visualRemote()`를 추가합니다.
32
+ - Vite에서는 `vite.config`에 `visualRemote()` 플러그인을 추가합니다.
33
+ - Next.js에서는 `next.config`에 `withVisualRemote()`를 적용하고
34
+ `instrumentation-client`에 개발 전용 클라이언트 로더를 추가합니다.
31
35
  - 현재 폴더에 `.visualdev/config.yaml`을 생성합니다.
32
36
 
33
37
  이후에는 앱을 평소처럼 실행합니다.
@@ -42,14 +46,16 @@ npm run dev
42
46
 
43
47
  ```text
44
48
  http://localhost:9011/api/* → 기존 앱이 그대로 처리
45
- http://localhost:9011/@vite/* → 기존 Vite/HMR이 그대로 처리
49
+ http://localhost:9011/@vite/* → 기존 Vite HMR이 그대로 처리
50
+ http://localhost:9011/_next/* → 기존 Next.js 자산/HMR이 그대로 처리
46
51
  http://localhost:9011/_visual/* → 내부 Visual Remote Bridge로만 전달
47
52
  ```
48
53
 
49
- 이미 실행 중이던 개발 서버가 있다면 `init` 후 한 번 재시작해야 변경된 Vite 설정이
54
+ 이미 실행 중이던 개발 서버가 있다면 `init` 후 한 번 재시작해야 변경된 설정이
50
55
  적용됩니다. Portr를 사용할 때도 내부 Bridge 포트가 아니라 기존 앱 포트만 노출합니다.
51
56
 
52
- 현재 자동 통합은 Vite 프로젝트를 지원합니다.
57
+ 자동 통합은 Vite Next.js 프로젝트를 지원합니다. 다른 프레임워크나 설정 파일을
58
+ 자동으로 수정하고 싶지 않은 프로젝트에서는 아래의 `attach` 방식을 사용할 수 있습니다.
53
59
 
54
60
  ## 저장소에서 개발
55
61
 
@@ -98,7 +104,7 @@ corepack pnpm test
98
104
  ## 설정
99
105
 
100
106
  `init`은 명령을 실행한 현재 프로젝트 폴더에 설정을 생성합니다. 모노레포의 하위
101
- Vite 앱에서 실행하면 Git 루트가 아니라 해당 앱 폴더에 생성됩니다. 기존 설정은
107
+ Vite 또는 Next.js 앱에서 실행하면 Git 루트가 아니라 해당 앱 폴더에 생성됩니다. 기존 설정은
102
108
  덮어쓰지 않습니다.
103
109
 
104
110
  ```yaml
@@ -156,8 +162,8 @@ paths:
156
162
  - dist/**
157
163
  ```
158
164
 
159
- Vite 플러그인이 개발 서버와 함께 내부 Bridge를 시작하고 종료합니다. `agent`,
160
- `verification`, `paths` 같은 상세 설정만 YAML에서 조정하면 됩니다.
165
+ Vite 플러그인 또는 Next.js 설정 래퍼가 개발 서버와 함께 내부 Bridge를 시작합니다.
166
+ `agent`, `verification`, `paths` 같은 상세 설정만 YAML에서 조정하면 됩니다.
161
167
 
162
168
  ## 기존 attach 방식
163
169
 
@@ -167,9 +173,9 @@ Vite 플러그인이 개발 서버와 함께 내부 Bridge를 시작하고 종
167
173
  npx --yes visual-remote@latest http://localhost:9011
168
174
  ```
169
175
 
170
- 이 방식은 별도 Gateway 주소를 열어 앱 전체를 프록시하므로 Origin에 민감한 API나
171
- 쿠키가 있는 앱에는 권장하지 않습니다. Vite 프로젝트에서는 `init` 통합을 사용하고
172
- 원래 주소로 접속합니다.
176
+ 이 방식은 별도 Gateway 주소를 열어 앱 전체를 프록시합니다. 자동 통합을 사용할 수
177
+ 있는 Vite와 Next.js 프로젝트에서는 `init`을 사용하고 원래 앱 주소로 접속합니다.
178
+ 설정 파일을 변경하지 않거나 다른 프레임워크에 붙일 때는 `attach`를 사용합니다.
173
179
 
174
180
  ## 브라우저에서 변경 요청
175
181
 
@@ -210,7 +216,7 @@ visual status
210
216
  visual doctor
211
217
  ```
212
218
 
213
- - `init`: 현재 Vite 앱에 개발 전용 통합을 설치합니다.
219
+ - `init`: 현재 Vite 또는 Next.js 앱에 개발 전용 통합을 설치합니다.
214
220
  - `attach`: 기본 명령의 명시적 이름이며 기존 사용법과 호환됩니다.
215
221
  - `dev`: `init` 설정을 사용해 앱과 브리지를 함께 실행하는 선택 명령입니다.
216
222
  - `status`: 현재 Git 작업 트리의 브리지 실행 상태를 확인합니다.
@@ -239,6 +245,8 @@ packages/overlay/dist/client.js
239
245
  packages/overlay/dist/viewer.js
240
246
  apps/cli/dist/index.js
241
247
  apps/cli/dist/vite.js
248
+ apps/cli/dist/next.js
249
+ apps/cli/dist/next-client.js
242
250
  ```
243
251
 
244
252
  ## 저장소 구조
@@ -278,6 +286,7 @@ node --version
278
286
 
279
287
  ### 설정 파일 경고가 표시되는 경우
280
288
 
281
- Vite 앱의 `package.json`과 `vite.config`가 있는 폴더에서 `visual init`을 실행한 뒤
282
- `doctor`를 다시 실행합니다. 모노레포에서는 Git 루트가 아니라 실제 앱 폴더에서
283
- 실행합니다.
289
+ Vite 또는 Next.js 앱의 `package.json`이 있는 폴더에서 `visual init`을 실행한 뒤
290
+ `doctor`를 다시 실행합니다. Vite는 `vite.config`, Next.js는 `next.config`와
291
+ `instrumentation-client`가 구성되어야 합니다. 모노레포에서는 Git 루트가 아니라
292
+ 실제 앱 폴더에서 실행합니다.
@@ -5234,6 +5234,20 @@ var VITE_CONFIG_FILES = [
5234
5234
  "vite.config.mjs"
5235
5235
  ];
5236
5236
  var VITE_IMPORT = 'import { visualRemote } from "visual-remote/vite";';
5237
+ var NEXT_CONFIG_FILES = [
5238
+ "next.config.ts",
5239
+ "next.config.mjs",
5240
+ "next.config.js"
5241
+ ];
5242
+ var NEXT_ESM_IMPORT = 'import { withVisualRemote } from "visual-remote/next";';
5243
+ var NEXT_CJS_IMPORT = 'const { withVisualRemote } = require("visual-remote/next");';
5244
+ var NEXT_CLIENT_MODULE = "visual-remote/next/client";
5245
+ var NEXT_CLIENT_BOOTSTRAP = [
5246
+ 'if (process.env.NODE_ENV === "development") {',
5247
+ ` void import("${NEXT_CLIENT_MODULE}");`,
5248
+ "}",
5249
+ ""
5250
+ ].join("\n");
5237
5251
  function isMissingFile(error) {
5238
5252
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
5239
5253
  }
@@ -5284,6 +5298,13 @@ function isViteProject(manifest, devScript) {
5284
5298
  };
5285
5299
  return "vite" in packages || /(^|[\s;&|])vite(?:\s|$)/.test(devScript);
5286
5300
  }
5301
+ function isNextProject(manifest, devScript) {
5302
+ const packages = {
5303
+ ...packageRecord(manifest.dependencies),
5304
+ ...packageRecord(manifest.devDependencies)
5305
+ };
5306
+ return "next" in packages || /(^|[\s;&|])next(?:\s|$)/.test(devScript);
5307
+ }
5287
5308
  function hasVisualRemote(manifest) {
5288
5309
  return "visual-remote" in packageRecord(manifest.dependencies) || "visual-remote" in packageRecord(manifest.devDependencies);
5289
5310
  }
@@ -5330,6 +5351,13 @@ async function findViteConfig(projectRoot) {
5330
5351
  "visual init found Vite but could not find vite.config.ts, .mts, .js, or .mjs"
5331
5352
  );
5332
5353
  }
5354
+ async function findNextConfig(projectRoot) {
5355
+ for (const filename of NEXT_CONFIG_FILES) {
5356
+ const candidate = join5(projectRoot, filename);
5357
+ if (await fileExists2(candidate)) return candidate;
5358
+ }
5359
+ return join5(projectRoot, "next.config.mjs");
5360
+ }
5333
5361
  function insertViteImport(source) {
5334
5362
  const lines = source.split("\n");
5335
5363
  let index = 0;
@@ -5367,6 +5395,130 @@ async function configureVite(configPath) {
5367
5395
  await writeFile3(configPath, transformed, "utf8");
5368
5396
  return true;
5369
5397
  }
5398
+ function expressionEnd(source, expressionStart) {
5399
+ let roundDepth = 0;
5400
+ let squareDepth = 0;
5401
+ let curlyDepth = 0;
5402
+ let quote;
5403
+ let lineComment = false;
5404
+ let blockComment = false;
5405
+ for (let index = expressionStart; index < source.length; index += 1) {
5406
+ const character = source[index];
5407
+ const nextCharacter = source[index + 1];
5408
+ if (lineComment) {
5409
+ if (character === "\n") lineComment = false;
5410
+ continue;
5411
+ }
5412
+ if (blockComment) {
5413
+ if (character === "*" && nextCharacter === "/") {
5414
+ blockComment = false;
5415
+ index += 1;
5416
+ }
5417
+ continue;
5418
+ }
5419
+ if (quote !== void 0) {
5420
+ if (character === "\\") {
5421
+ index += 1;
5422
+ } else if (character === quote) {
5423
+ quote = void 0;
5424
+ }
5425
+ continue;
5426
+ }
5427
+ if (character === "/" && nextCharacter === "/") {
5428
+ lineComment = true;
5429
+ index += 1;
5430
+ continue;
5431
+ }
5432
+ if (character === "/" && nextCharacter === "*") {
5433
+ blockComment = true;
5434
+ index += 1;
5435
+ continue;
5436
+ }
5437
+ if (character === "'" || character === '"' || character === "`") {
5438
+ quote = character;
5439
+ continue;
5440
+ }
5441
+ if (character === "(") roundDepth += 1;
5442
+ if (character === ")") roundDepth -= 1;
5443
+ if (character === "[") squareDepth += 1;
5444
+ if (character === "]") squareDepth -= 1;
5445
+ if (character === "{") curlyDepth += 1;
5446
+ if (character === "}") curlyDepth -= 1;
5447
+ if (character === ";" && roundDepth === 0 && squareDepth === 0 && curlyDepth === 0) {
5448
+ return index;
5449
+ }
5450
+ }
5451
+ return source.length;
5452
+ }
5453
+ function wrapConfigExpression(source, assignmentPattern) {
5454
+ const assignment = assignmentPattern.exec(source);
5455
+ if (assignment?.index === void 0) {
5456
+ throw new Error("visual init could not find the default Next.js config export");
5457
+ }
5458
+ let start = assignment.index + assignment[0].length;
5459
+ while (/\s/.test(source[start] ?? "")) start += 1;
5460
+ const end = expressionEnd(source, start);
5461
+ const rawExpression = source.slice(start, end);
5462
+ const trailingWhitespace = rawExpression.match(/\s*$/)?.[0] ?? "";
5463
+ const expression = rawExpression.slice(0, rawExpression.length - trailingWhitespace.length);
5464
+ if (expression.length === 0) {
5465
+ throw new Error("visual init found an empty Next.js config export");
5466
+ }
5467
+ return `${source.slice(0, start)}withVisualRemote(${expression})${trailingWhitespace}${source.slice(end)}`;
5468
+ }
5469
+ function transformNextConfig(source) {
5470
+ if (source.includes("visual-remote/next") && /\bwithVisualRemote\s*\(/.test(source)) {
5471
+ return source;
5472
+ }
5473
+ if (/\bmodule\.exports\s*=/.test(source)) {
5474
+ const wrapped2 = wrapConfigExpression(source, /\bmodule\.exports\s*=/);
5475
+ return wrapped2.includes(NEXT_CJS_IMPORT) ? wrapped2 : `${NEXT_CJS_IMPORT}
5476
+ ${wrapped2}`;
5477
+ }
5478
+ const wrapped = wrapConfigExpression(source, /\bexport\s+default\b/);
5479
+ return wrapped.includes(NEXT_ESM_IMPORT) ? wrapped : `${NEXT_ESM_IMPORT}
5480
+ ${wrapped}`;
5481
+ }
5482
+ async function configureNext(configPath) {
5483
+ let source;
5484
+ try {
5485
+ source = await readFile5(configPath, "utf8");
5486
+ } catch (error) {
5487
+ if (!isMissingFile(error)) throw error;
5488
+ source = "export default {};\n";
5489
+ }
5490
+ const transformed = transformNextConfig(source);
5491
+ if (transformed === source) return false;
5492
+ await writeFile3(configPath, transformed, "utf8");
5493
+ return true;
5494
+ }
5495
+ async function findNextClientPath(projectRoot) {
5496
+ const candidates = [
5497
+ join5(projectRoot, "instrumentation-client.ts"),
5498
+ join5(projectRoot, "instrumentation-client.js"),
5499
+ join5(projectRoot, "src", "instrumentation-client.ts"),
5500
+ join5(projectRoot, "src", "instrumentation-client.js")
5501
+ ];
5502
+ for (const candidate of candidates) {
5503
+ if (await fileExists2(candidate)) return candidate;
5504
+ }
5505
+ const sourceRoot = await fileExists2(join5(projectRoot, "src")) ? join5(projectRoot, "src") : projectRoot;
5506
+ const extension = await fileExists2(join5(projectRoot, "tsconfig.json")) ? "ts" : "js";
5507
+ return join5(sourceRoot, `instrumentation-client.${extension}`);
5508
+ }
5509
+ async function configureNextClient(clientPath) {
5510
+ let source = "";
5511
+ try {
5512
+ source = await readFile5(clientPath, "utf8");
5513
+ } catch (error) {
5514
+ if (!isMissingFile(error)) throw error;
5515
+ }
5516
+ if (source.includes(NEXT_CLIENT_MODULE)) return false;
5517
+ const separator = source.length === 0 || source.endsWith("\n") ? "" : "\n";
5518
+ await mkdir3(dirname4(clientPath), { recursive: true });
5519
+ await writeFile3(clientPath, `${source}${separator}${NEXT_CLIENT_BOOTSTRAP}`, "utf8");
5520
+ return true;
5521
+ }
5370
5522
  function installCommand(request) {
5371
5523
  if (request.packageManager === "pnpm") {
5372
5524
  return {
@@ -5416,11 +5568,12 @@ async function initializeVisualDev(dependencies = {}) {
5416
5568
  const repoRoot = await discoverGitWorktreeRoot(projectRoot);
5417
5569
  const manifest = await readManifest(projectRoot);
5418
5570
  const devScript = readDevScript(manifest);
5419
- if (!isViteProject(manifest, devScript)) {
5420
- throw new Error("visual init currently supports Vite projects");
5571
+ const framework = isViteProject(manifest, devScript) ? "vite" : isNextProject(manifest, devScript) ? "next" : void 0;
5572
+ if (framework === void 0) {
5573
+ throw new Error("visual init currently supports Vite and Next.js projects");
5421
5574
  }
5422
5575
  const packageManager = await detectPackageManager(projectRoot, repoRoot, manifest);
5423
- const integrationPath = await findViteConfig(projectRoot);
5576
+ const integrationPath = framework === "vite" ? await findViteConfig(projectRoot) : await findNextConfig(projectRoot);
5424
5577
  const packageInstalled = !hasVisualRemote(manifest);
5425
5578
  if (packageInstalled) {
5426
5579
  await (dependencies.installPackage ?? installPackage)({
@@ -5429,7 +5582,9 @@ async function initializeVisualDev(dependencies = {}) {
5429
5582
  packageSpec: "visual-remote@latest"
5430
5583
  });
5431
5584
  }
5432
- const integrationChanged = await configureVite(integrationPath);
5585
+ const integrationChanged = framework === "vite" ? await configureVite(integrationPath) : await configureNext(integrationPath);
5586
+ const clientPath = framework === "next" ? await findNextClientPath(projectRoot) : void 0;
5587
+ const clientChanged = clientPath === void 0 ? void 0 : await configureNextClient(clientPath);
5433
5588
  const configPath = join5(projectRoot, CONFIG_PATH);
5434
5589
  const created = !await fileExists2(configPath);
5435
5590
  if (created) {
@@ -5461,24 +5616,35 @@ async function initializeVisualDev(dependencies = {}) {
5461
5616
  }
5462
5617
  }
5463
5618
  return {
5619
+ framework,
5464
5620
  created,
5465
5621
  configPath,
5466
5622
  devScript,
5467
5623
  packageManager,
5468
5624
  integrationPath,
5469
5625
  integrationChanged,
5626
+ ...clientPath === void 0 || clientChanged === void 0 ? {} : { clientPath, clientChanged },
5470
5627
  packageInstalled
5471
5628
  };
5472
5629
  }
5473
5630
  function formatInitResult(result) {
5474
5631
  const configLabel = result.created ? "Created" : "Existing";
5475
5632
  const integrationLabel = result.integrationChanged ? "Configured" : "Existing";
5476
- return [
5633
+ const rows = [
5634
+ `Framework: ${result.framework === "next" ? "Next.js" : "Vite"}`,
5477
5635
  `${configLabel}: ${relative7(process.cwd(), result.configPath) || CONFIG_PATH}`,
5478
- `${integrationLabel}: ${relative7(process.cwd(), result.integrationPath)}`,
5636
+ `${integrationLabel}: ${relative7(process.cwd(), result.integrationPath)}`
5637
+ ];
5638
+ if (result.clientPath !== void 0) {
5639
+ rows.push(
5640
+ `${result.clientChanged ? "Configured" : "Existing"}: ${relative7(process.cwd(), result.clientPath)}`
5641
+ );
5642
+ }
5643
+ rows.push(
5479
5644
  result.packageInstalled ? "Installed: visual-remote@latest" : "Installed: visual-remote",
5480
5645
  "Next: Start the app normally and open its original URL."
5481
- ].join("\n");
5646
+ );
5647
+ return rows.join("\n");
5482
5648
  }
5483
5649
 
5484
5650
  // src/status.ts
@@ -5531,8 +5697,8 @@ function setExitCode(dependencies, code) {
5531
5697
  }
5532
5698
  }
5533
5699
  function createCli(dependencies = {}) {
5534
- const program = new Command().name("visual").description("Visual Remote Dev Bridge").version("0.2.0");
5535
- program.command("init").description("Create .visualdev/config.yaml for the current project").action(async () => {
5700
+ const program = new Command().name("visual").description("Visual Remote Dev Bridge").version("0.3.0");
5701
+ program.command("init").description("Configure Visual Remote for the current Vite or Next.js project").action(async () => {
5536
5702
  const result = await initializeVisualDev(dependencies);
5537
5703
  output(dependencies, formatInitResult(result));
5538
5704
  });
@@ -5611,5 +5777,6 @@ export {
5611
5777
  runDoctor,
5612
5778
  startAttachBridge,
5613
5779
  startManagedBridge,
5780
+ transformNextConfig,
5614
5781
  transformViteConfig
5615
5782
  };
@@ -0,0 +1,11 @@
1
+ // src/next-client.ts
2
+ var clientPath = "/_visual/client.js";
3
+ if (document.querySelector(`script[src="${clientPath}"]`) === null && document.querySelector("script[data-visual-remote-client]") === null) {
4
+ const script = document.createElement("script");
5
+ script.type = "module";
6
+ script.src = clientPath;
7
+ script.dataset.visualRemoteClient = "";
8
+ const nonceSource = document.querySelector("script[nonce]");
9
+ if (nonceSource?.nonce) script.nonce = nonceSource.nonce;
10
+ document.head.append(script);
11
+ }