@simplysm/sd-cli 14.0.45 → 14.0.46

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.
@@ -8,7 +8,7 @@ import { fileURLToPath } from "url";
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
  const fixturesDir = path.join(__dirname, "fixtures", "worker-plugin");
10
10
 
11
- const { transformWorkerPatterns, createWorkerBundlePlugin } = await import(
11
+ const { transformWorkerPatterns, createWorkerBundlePlugin, findWorkerPatterns } = await import(
12
12
  "../../src/esbuild/esbuild-worker-plugin.js"
13
13
  );
14
14
 
@@ -59,6 +59,27 @@ describe("transformWorkerPatterns — 패턴 감지", () => {
59
59
  expect(result).toBeUndefined();
60
60
  });
61
61
 
62
+ it("주석 내 Worker 패턴은 무시한다", () => {
63
+ const result = transformWorkerPatterns(
64
+ `// new Worker(new URL("./worker.js", import.meta.url))
65
+ const x = 1;`,
66
+ "/test/entry.js",
67
+ createMockBuild(),
68
+ );
69
+
70
+ expect(result).toBeUndefined();
71
+ });
72
+
73
+ it("문자열 리터럴 내 Worker 패턴은 무시한다", () => {
74
+ const result = transformWorkerPatterns(
75
+ `const s = "new Worker(new URL(\\"./worker.js\\", import.meta.url))";`,
76
+ "/test/entry.js",
77
+ createMockBuild(),
78
+ );
79
+
80
+ expect(result).toBeUndefined();
81
+ });
82
+
62
83
  it("Worker + new URL + import.meta.url 패턴을 감지하여 치환한다", () => {
63
84
  const entryPath = path.join(fixturesDir, "entry.js");
64
85
 
@@ -295,3 +316,291 @@ describe("createWorkerBundlePlugin — 플러그인 구조", () => {
295
316
  expect(hasOnEnd).toBe(true);
296
317
  });
297
318
  });
319
+
320
+ describe("findWorkerPatterns — AST 기반 패턴 탐지", () => {
321
+ it("Worker + new URL + import.meta.url 패턴을 탐지한다", () => {
322
+ const matches = findWorkerPatterns(
323
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
324
+ );
325
+
326
+ expect(matches).toHaveLength(1);
327
+ expect(matches[0].type).toBe("browser");
328
+ expect(matches[0].urlPath).toBe("./worker.js");
329
+ expect(matches[0].workerType).toBe("Worker");
330
+ expect(matches[0].existingOpts).toBeUndefined();
331
+ });
332
+
333
+ it("SharedWorker 패턴을 탐지한다", () => {
334
+ const matches = findWorkerPatterns(
335
+ `const sw = new SharedWorker(new URL("./sw.js", import.meta.url));`,
336
+ );
337
+
338
+ expect(matches).toHaveLength(1);
339
+ expect(matches[0].workerType).toBe("SharedWorker");
340
+ expect(matches[0].urlPath).toBe("./sw.js");
341
+ });
342
+
343
+ it("옵션 객체가 있는 Worker 패턴을 탐지하고 원본 텍스트를 보존한다", () => {
344
+ const content = `const w = new Worker(new URL("./w.js", import.meta.url), { type: "module" });`;
345
+ const matches = findWorkerPatterns(content);
346
+
347
+ expect(matches).toHaveLength(1);
348
+ expect(matches[0].existingOpts).toBe('{ type: "module" }');
349
+ });
350
+
351
+ it("import.meta.resolve 상대 경로 패턴을 탐지한다", () => {
352
+ const matches = findWorkerPatterns(
353
+ `const p = import.meta.resolve("./node-worker.js");`,
354
+ );
355
+
356
+ expect(matches).toHaveLength(1);
357
+ expect(matches[0].type).toBe("node");
358
+ expect(matches[0].urlPath).toBe("./node-worker.js");
359
+ });
360
+
361
+ it("import.meta.resolve 절대 모듈 경로는 무시한다", () => {
362
+ const matches = findWorkerPatterns(
363
+ `const p = import.meta.resolve("some-package");`,
364
+ );
365
+
366
+ expect(matches).toHaveLength(0);
367
+ });
368
+
369
+ it("new URL 없는 Worker는 무시한다", () => {
370
+ const matches = findWorkerPatterns(
371
+ `const w = new Worker("./worker.js");`,
372
+ );
373
+
374
+ expect(matches).toHaveLength(0);
375
+ });
376
+
377
+ it("import.meta.url이 아닌 URL 생성은 무시한다", () => {
378
+ const matches = findWorkerPatterns(
379
+ `const w = new Worker(new URL("./w.js", location.href));`,
380
+ );
381
+
382
+ expect(matches).toHaveLength(0);
383
+ });
384
+
385
+ it("복수 패턴을 모두 탐지한다", () => {
386
+ const matches = findWorkerPatterns(
387
+ [
388
+ `const w1 = new Worker(new URL("./w1.js", import.meta.url));`,
389
+ `const w2 = new Worker(new URL("./w2.js", import.meta.url));`,
390
+ ].join("\n"),
391
+ );
392
+
393
+ expect(matches).toHaveLength(2);
394
+ expect(matches[0].urlPath).toBe("./w1.js");
395
+ expect(matches[1].urlPath).toBe("./w2.js");
396
+ });
397
+
398
+ it("start/end 위치가 정확하다", () => {
399
+ const content = `const w = new Worker(new URL("./w.js", import.meta.url));`;
400
+ const matches = findWorkerPatterns(content);
401
+
402
+ expect(matches).toHaveLength(1);
403
+ const matched = content.slice(matches[0].start, matches[0].end);
404
+ expect(matched).toBe(`new Worker(new URL("./w.js", import.meta.url))`);
405
+ });
406
+
407
+ it("주석 내 Worker 패턴은 무시한다", () => {
408
+ const matches = findWorkerPatterns(
409
+ `// new Worker(new URL("./w.js", import.meta.url))
410
+ const x = 1;`,
411
+ );
412
+
413
+ expect(matches).toHaveLength(0);
414
+ });
415
+
416
+ it("문자열 리터럴 내 Worker 패턴은 무시한다", () => {
417
+ const matches = findWorkerPatterns(
418
+ `const s = "new Worker(new URL(\\"./w.js\\", import.meta.url))";`,
419
+ );
420
+
421
+ expect(matches).toHaveLength(0);
422
+ });
423
+
424
+ it("블록 주석 내 Worker 패턴은 무시한다", () => {
425
+ const matches = findWorkerPatterns(
426
+ `/* new Worker(new URL("./w.js", import.meta.url)) */
427
+ const x = 1;`,
428
+ );
429
+
430
+ expect(matches).toHaveLength(0);
431
+ });
432
+
433
+ it("주석 내 import.meta.resolve는 무시한다", () => {
434
+ const matches = findWorkerPatterns(
435
+ `// import.meta.resolve("./w.js")
436
+ const x = 1;`,
437
+ );
438
+
439
+ expect(matches).toHaveLength(0);
440
+ });
441
+
442
+ it("파싱 불가능한 코드는 빈 배열을 반환한다", () => {
443
+ const matches = findWorkerPatterns(
444
+ `this is not valid javascript }{][`,
445
+ );
446
+
447
+ expect(matches).toHaveLength(0);
448
+ });
449
+ });
450
+
451
+ describe("transformWorkerPatterns — TypeScript 파일 처리", () => {
452
+ it("import type이 포함된 .ts 파일에서 Worker 패턴을 탐지하여 치환한다", () => {
453
+ const entryPath = path.join(fixturesDir, "entry.ts");
454
+ const result = transformWorkerPatterns(
455
+ `import type { T } from "pkg";
456
+ const w = new Worker(new URL("./worker.js", import.meta.url));`,
457
+ entryPath,
458
+ createMockBuild(),
459
+ );
460
+
461
+ expect(result).toBeDefined();
462
+ expect(result!.contents).not.toContain("./worker.js");
463
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
464
+ expect(result!.errors).toHaveLength(0);
465
+ });
466
+
467
+ it("import type이 포함된 .ts 파일에서 import.meta.resolve 패턴을 탐지하여 치환한다", () => {
468
+ const entryPath = path.join(fixturesDir, "entry.ts");
469
+ const result = transformWorkerPatterns(
470
+ `import type { T } from "pkg";
471
+ const p = import.meta.resolve("./node-worker.js");`,
472
+ entryPath,
473
+ createMockBuild({ platform: "node" }),
474
+ );
475
+
476
+ expect(result).toBeDefined();
477
+ expect(result!.contents).not.toContain("./node-worker.js");
478
+ expect(result!.contents).toMatch(
479
+ /new URL\("worker-[a-z0-9]+\.js", import\.meta\.url\)\.href/i,
480
+ );
481
+ expect(result!.errors).toHaveLength(0);
482
+ });
483
+
484
+ it("타입 어노테이션이 있는 변수 선언(const w: Worker = ...)에서 Worker를 탐지한다", () => {
485
+ const entryPath = path.join(fixturesDir, "entry.ts");
486
+ const result = transformWorkerPatterns(
487
+ `const w: Worker = new Worker(new URL("./worker.js", import.meta.url));`,
488
+ entryPath,
489
+ createMockBuild(),
490
+ );
491
+
492
+ expect(result).toBeDefined();
493
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
494
+ });
495
+
496
+ it(".mts 확장자의 TS 파일도 변환 후 처리한다", () => {
497
+ const entryPath = path.join(fixturesDir, "entry.mts");
498
+ const result = transformWorkerPatterns(
499
+ `import type { T } from "pkg";
500
+ const w = new Worker(new URL("./worker.js", import.meta.url));`,
501
+ entryPath,
502
+ createMockBuild(),
503
+ );
504
+
505
+ expect(result).toBeDefined();
506
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
507
+ });
508
+
509
+ it(".cts 확장자의 TS 파일도 변환 후 처리한다", () => {
510
+ const entryPath = path.join(fixturesDir, "entry.cts");
511
+ const result = transformWorkerPatterns(
512
+ `import type { T } from "pkg";
513
+ const w = new Worker(new URL("./worker.js", import.meta.url));`,
514
+ entryPath,
515
+ createMockBuild(),
516
+ );
517
+
518
+ expect(result).toBeDefined();
519
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
520
+ });
521
+
522
+ it("TS 파일의 주석 내 Worker 패턴은 무시한다", () => {
523
+ const entryPath = path.join(fixturesDir, "entry.ts");
524
+ const result = transformWorkerPatterns(
525
+ `import type { T } from "pkg";
526
+ // new Worker(new URL("./worker.js", import.meta.url))
527
+ const x: number = 1;`,
528
+ entryPath,
529
+ createMockBuild(),
530
+ );
531
+
532
+ expect(result).toBeUndefined();
533
+ });
534
+
535
+ it("TS 파일의 문자열 리터럴 내 Worker 패턴은 무시한다", () => {
536
+ const entryPath = path.join(fixturesDir, "entry.ts");
537
+ const result = transformWorkerPatterns(
538
+ `import type { T } from "pkg";
539
+ const s: string = "new Worker(new URL(\\"./worker.js\\", import.meta.url))";`,
540
+ entryPath,
541
+ createMockBuild(),
542
+ );
543
+
544
+ expect(result).toBeUndefined();
545
+ });
546
+
547
+ it("사전 필터를 통과하지 못하는 TS 파일(Worker 키워드 없음)은 undefined를 반환한다", () => {
548
+ const entryPath = path.join(fixturesDir, "entry.ts");
549
+ const result = transformWorkerPatterns(
550
+ `import type { T } from "pkg";
551
+ const x: number = 1;`,
552
+ entryPath,
553
+ createMockBuild(),
554
+ );
555
+
556
+ expect(result).toBeUndefined();
557
+ });
558
+
559
+ it("TS 변환 실패(문법 오류) 시 errors에 에러를 포함하여 반환한다", () => {
560
+ const entryPath = path.join(fixturesDir, "entry.ts");
561
+ const result = transformWorkerPatterns(
562
+ `const w = new Worker(new URL("./worker.js", import.meta.url)); const x: =`,
563
+ entryPath,
564
+ createMockBuild(),
565
+ );
566
+
567
+ expect(result).toBeDefined();
568
+ expect(result!.errors.length).toBeGreaterThan(0);
569
+ });
570
+ });
571
+
572
+ describe("createWorkerBundlePlugin — TS 파일 onLoad 반환", () => {
573
+ it("TS 파일에서 Worker 감지 시 loader로 'js'를 반환한다", async () => {
574
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-onload-"));
575
+ const tsFile = path.join(tmpDir, "entry.ts");
576
+ fs.copyFileSync(
577
+ path.join(fixturesDir, "worker.js"),
578
+ path.join(tmpDir, "worker.js"),
579
+ );
580
+ fs.writeFileSync(
581
+ tsFile,
582
+ `import type { T } from "pkg";
583
+ const w = new Worker(new URL("./worker.js", import.meta.url));`,
584
+ );
585
+
586
+ const plugin = createWorkerBundlePlugin();
587
+
588
+ let onLoadCallback: ((args: { path: string }) => Promise<any> | any) | null = null;
589
+ const mockBuild = {
590
+ esbuild,
591
+ initialOptions: { outdir: tmpDir, write: false },
592
+ onLoad: (_filter: unknown, cb: (args: { path: string }) => Promise<any> | any) => {
593
+ onLoadCallback = cb;
594
+ },
595
+ onEnd: () => { /* noop */ },
596
+ } as unknown as esbuild.PluginBuild;
597
+
598
+ await plugin.setup(mockBuild);
599
+ expect(onLoadCallback).not.toBeNull();
600
+
601
+ const result = await onLoadCallback!({ path: tsFile });
602
+ expect(result).toBeDefined();
603
+ expect(result.loader).toBe("js");
604
+ expect(result.contents).toMatch(/worker-[a-z0-9]+\.js/i);
605
+ });
606
+ });