Skip to content

Commit 8b9566a

Browse files
leodidoona-agent
andcommitted
test(yarn): add tests for scoped package extraction
Add unit test TestYarnAppExtraction_ScopedPackage demonstrating that: - Non-scoped packages work with --strip-components=3 - Scoped packages fail with --strip-components=3 (the bug) - Scoped packages work with --strip-components=4 (the fix) Add integration test TestYarnPackage_ScopedLinkDependencies_Integration for end-to-end testing of scoped package link: dependencies. Co-authored-by: Ona <no-reply@ona.com>
1 parent f552c5c commit 8b9566a

File tree

2 files changed

+328
-0
lines changed

2 files changed

+328
-0
lines changed

pkg/leeway/build_integration_test.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2717,3 +2717,241 @@ func getPackageNames(ws *Workspace) []string {
27172717
}
27182718
return names
27192719
}
2720+
2721+
// TestYarnPackage_ScopedLinkDependencies_Integration verifies that yarn packages with link:
2722+
// dependencies to scoped packages (e.g., @scope/pkg) are correctly built.
2723+
//
2724+
// Scoped packages have an extra directory level in node_modules:
2725+
// ./node_modules/@scope/pkg-name/package.json
2726+
//
2727+
// This requires different --strip-components handling than non-scoped packages.
2728+
func TestYarnPackage_ScopedLinkDependencies_Integration(t *testing.T) {
2729+
if testing.Short() {
2730+
t.Skip("Skipping integration test in short mode")
2731+
}
2732+
2733+
// Ensure yarn is available
2734+
if err := exec.Command("yarn", "--version").Run(); err != nil {
2735+
t.Skip("yarn not available, skipping integration test")
2736+
}
2737+
2738+
// Ensure node is available
2739+
if err := exec.Command("node", "--version").Run(); err != nil {
2740+
t.Skip("node not available, skipping integration test")
2741+
}
2742+
2743+
tmpDir := t.TempDir()
2744+
2745+
// Create WORKSPACE.yaml
2746+
workspaceYAML := `defaultTarget: "app:lib"`
2747+
workspacePath := filepath.Join(tmpDir, "WORKSPACE.yaml")
2748+
if err := os.WriteFile(workspacePath, []byte(workspaceYAML), 0644); err != nil {
2749+
t.Fatal(err)
2750+
}
2751+
2752+
// Create scoped package directory structure: packages/@test/utils
2753+
scopedPkgDir := filepath.Join(tmpDir, "packages", "@test", "utils")
2754+
if err := os.MkdirAll(scopedPkgDir, 0755); err != nil {
2755+
t.Fatal(err)
2756+
}
2757+
2758+
// Create scoped package package.json
2759+
scopedPkgPackageJSON := `{
2760+
"name": "@test/utils",
2761+
"version": "1.0.0",
2762+
"main": "index.js"
2763+
}`
2764+
if err := os.WriteFile(filepath.Join(scopedPkgDir, "package.json"), []byte(scopedPkgPackageJSON), 0644); err != nil {
2765+
t.Fatal(err)
2766+
}
2767+
2768+
// Create scoped package index.js
2769+
scopedPkgIndexJS := `module.exports = {
2770+
formatName: function(name) {
2771+
return "Formatted: " + name;
2772+
}
2773+
};`
2774+
if err := os.WriteFile(filepath.Join(scopedPkgDir, "index.js"), []byte(scopedPkgIndexJS), 0644); err != nil {
2775+
t.Fatal(err)
2776+
}
2777+
2778+
// Create scoped package yarn.lock (empty but required for YarnApp)
2779+
scopedPkgYarnLock := `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2780+
# yarn lockfile v1
2781+
2782+
`
2783+
if err := os.WriteFile(filepath.Join(scopedPkgDir, "yarn.lock"), []byte(scopedPkgYarnLock), 0644); err != nil {
2784+
t.Fatal(err)
2785+
}
2786+
2787+
// Create scoped package BUILD.yaml
2788+
// Using packaging: app (YarnApp) to test the ./node_modules/@scope/pkg/ extraction path
2789+
// YarnApp creates a tarball with ./node_modules/<pkg-name>/ structure
2790+
scopedPkgBuildYAML := `packages:
2791+
- name: lib
2792+
type: yarn
2793+
srcs:
2794+
- "package.json"
2795+
- "yarn.lock"
2796+
- "index.js"
2797+
config:
2798+
packaging: app
2799+
dontTest: true
2800+
commands:
2801+
build: ["echo", "build complete"]`
2802+
if err := os.WriteFile(filepath.Join(scopedPkgDir, "BUILD.yaml"), []byte(scopedPkgBuildYAML), 0644); err != nil {
2803+
t.Fatal(err)
2804+
}
2805+
2806+
// Create app directory (depends on @test/utils)
2807+
appDir := filepath.Join(tmpDir, "app")
2808+
if err := os.MkdirAll(appDir, 0755); err != nil {
2809+
t.Fatal(err)
2810+
}
2811+
2812+
// Create app package.json with link: dependency to scoped package
2813+
appPackageJSON := `{
2814+
"name": "test-app",
2815+
"version": "1.0.0",
2816+
"dependencies": {
2817+
"@test/utils": "link:./../packages/@test/utils"
2818+
},
2819+
"scripts": {
2820+
"test": "node test.js"
2821+
}
2822+
}`
2823+
if err := os.WriteFile(filepath.Join(appDir, "package.json"), []byte(appPackageJSON), 0644); err != nil {
2824+
t.Fatal(err)
2825+
}
2826+
2827+
// Create app yarn.lock with link: reference to scoped package
2828+
// Note: yarn.lock normalizes the path
2829+
appYarnLock := `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2830+
# yarn lockfile v1
2831+
2832+
2833+
"@test/utils@link:../packages/@test/utils":
2834+
version "1.0.0"
2835+
`
2836+
if err := os.WriteFile(filepath.Join(appDir, "yarn.lock"), []byte(appYarnLock), 0644); err != nil {
2837+
t.Fatal(err)
2838+
}
2839+
2840+
// Create app test.js that uses the scoped package
2841+
appTestJS := `const utils = require('@test/utils');
2842+
const result = utils.formatName('World');
2843+
if (result !== 'Formatted: World') {
2844+
console.error('Expected "Formatted: World" but got:', result);
2845+
process.exit(1);
2846+
}
2847+
console.log('Test passed:', result);`
2848+
if err := os.WriteFile(filepath.Join(appDir, "test.js"), []byte(appTestJS), 0644); err != nil {
2849+
t.Fatal(err)
2850+
}
2851+
2852+
// Create app BUILD.yaml
2853+
appBuildYAML := `packages:
2854+
- name: lib
2855+
type: yarn
2856+
srcs:
2857+
- "package.json"
2858+
- "yarn.lock"
2859+
- "test.js"
2860+
deps:
2861+
- packages/@test/utils:lib
2862+
config:
2863+
packaging: library
2864+
dontTest: true
2865+
commands:
2866+
build: ["echo", "build complete"]`
2867+
if err := os.WriteFile(filepath.Join(appDir, "BUILD.yaml"), []byte(appBuildYAML), 0644); err != nil {
2868+
t.Fatal(err)
2869+
}
2870+
2871+
// Initialize git repository (required for leeway)
2872+
gitInit := exec.Command("git", "init")
2873+
gitInit.Dir = tmpDir
2874+
gitInit.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
2875+
if err := gitInit.Run(); err != nil {
2876+
t.Fatalf("Failed to initialize git repository: %v", err)
2877+
}
2878+
2879+
gitConfigName := exec.Command("git", "config", "user.name", "Test User")
2880+
gitConfigName.Dir = tmpDir
2881+
gitConfigName.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
2882+
if err := gitConfigName.Run(); err != nil {
2883+
t.Fatalf("Failed to configure git user.name: %v", err)
2884+
}
2885+
2886+
gitConfigEmail := exec.Command("git", "config", "user.email", "test@example.com")
2887+
gitConfigEmail.Dir = tmpDir
2888+
gitConfigEmail.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
2889+
if err := gitConfigEmail.Run(); err != nil {
2890+
t.Fatalf("Failed to configure git user.email: %v", err)
2891+
}
2892+
2893+
gitAdd := exec.Command("git", "add", ".")
2894+
gitAdd.Dir = tmpDir
2895+
gitAdd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
2896+
if err := gitAdd.Run(); err != nil {
2897+
t.Fatalf("Failed to git add: %v", err)
2898+
}
2899+
2900+
gitCommit := exec.Command("git", "commit", "-m", "initial")
2901+
gitCommit.Dir = tmpDir
2902+
gitCommit.Env = append(os.Environ(),
2903+
"GIT_CONFIG_GLOBAL=/dev/null",
2904+
"GIT_CONFIG_SYSTEM=/dev/null",
2905+
"GIT_AUTHOR_DATE=2021-01-01T00:00:00Z",
2906+
"GIT_COMMITTER_DATE=2021-01-01T00:00:00Z",
2907+
)
2908+
if err := gitCommit.Run(); err != nil {
2909+
t.Fatalf("Failed to git commit: %v", err)
2910+
}
2911+
2912+
// Load workspace
2913+
workspace, err := FindWorkspace(tmpDir, Arguments{}, "", "")
2914+
if err != nil {
2915+
t.Fatalf("Failed to load workspace: %v", err)
2916+
}
2917+
2918+
// Get app package
2919+
pkg, ok := workspace.Packages["app:lib"]
2920+
if !ok {
2921+
t.Fatalf("Package app:lib not found in workspace. Available packages: %v", getPackageNames(&workspace))
2922+
}
2923+
2924+
// Create local cache
2925+
cacheDir := filepath.Join(tmpDir, ".cache")
2926+
if err := os.MkdirAll(cacheDir, 0755); err != nil {
2927+
t.Fatal(err)
2928+
}
2929+
2930+
localCache, err := local.NewFilesystemCache(cacheDir)
2931+
if err != nil {
2932+
t.Fatalf("Failed to create local cache: %v", err)
2933+
}
2934+
2935+
// Build the app package (which depends on @test/utils via link:)
2936+
t.Log("Building app:lib which depends on @test/utils:lib via link: dependency (scoped package)")
2937+
2938+
err = Build(pkg,
2939+
WithLocalCache(localCache),
2940+
WithDontTest(true),
2941+
)
2942+
2943+
if err != nil {
2944+
t.Fatalf("Build failed: %v\n\nThis likely means scoped package link: dependency handling is broken.", err)
2945+
}
2946+
2947+
t.Log("✅ Build succeeded - scoped package link: dependency was correctly resolved")
2948+
2949+
// Verify cache artifact exists
2950+
cachePath, exists := localCache.Location(pkg)
2951+
if !exists {
2952+
t.Fatal("Package not found in cache after build")
2953+
}
2954+
2955+
t.Logf("Cache artifact created at: %s", cachePath)
2956+
t.Log("✅ Yarn scoped package link: dependency integration test passed")
2957+
}

pkg/leeway/build_internal_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ package leeway
33
import (
44
"archive/tar"
55
"compress/gzip"
6+
"fmt"
67
"os"
8+
"os/exec"
79
"path/filepath"
10+
"strings"
811
"testing"
912

1013
"github.com/google/go-cmp/cmp"
@@ -209,3 +212,90 @@ func TestExtractNpmPackageNames_InvalidGzip(t *testing.T) {
209212
t.Error("extractNpmPackageNames() expected error for non-gzipped file, got nil")
210213
}
211214
}
215+
216+
func TestYarnAppExtraction_ScopedPackage(t *testing.T) {
217+
// This test verifies that scoped packages are correctly extracted from YarnApp tarballs.
218+
// YarnApp tarballs have structure: ./node_modules/@scope/pkg-name/...
219+
// For scoped packages, we need --strip-components=4 (not 3) to correctly extract.
220+
//
221+
// Path components for scoped package:
222+
// ./node_modules/@scope/pkg-name/package.json
223+
// ^ ^ ^ ^ ^
224+
// 1 2 3 4 file
225+
//
226+
// Path components for non-scoped package:
227+
// ./node_modules/pkg-name/package.json
228+
// ^ ^ ^ ^
229+
// 1 2 3 file
230+
231+
tests := []struct {
232+
name string
233+
npmName string
234+
}{
235+
{
236+
name: "non-scoped package",
237+
npmName: "my-pkg",
238+
},
239+
{
240+
name: "scoped package",
241+
npmName: "@test/utils",
242+
},
243+
}
244+
245+
for _, tt := range tests {
246+
t.Run(tt.name, func(t *testing.T) {
247+
tmpDir := t.TempDir()
248+
249+
// Create tarball with the package structure (simulating YarnApp output)
250+
tarballDir := filepath.Join(tmpDir, "tarball-content")
251+
var pkgDir string
252+
if strings.HasPrefix(tt.npmName, "@") {
253+
parts := strings.SplitN(tt.npmName, "/", 2)
254+
pkgDir = filepath.Join(tarballDir, "node_modules", parts[0], parts[1])
255+
} else {
256+
pkgDir = filepath.Join(tarballDir, "node_modules", tt.npmName)
257+
}
258+
if err := os.MkdirAll(pkgDir, 0755); err != nil {
259+
t.Fatal(err)
260+
}
261+
262+
// Create package.json
263+
pkgJSON := fmt.Sprintf(`{"name":"%s","version":"1.0.0"}`, tt.npmName)
264+
if err := os.WriteFile(filepath.Join(pkgDir, "package.json"), []byte(pkgJSON), 0644); err != nil {
265+
t.Fatal(err)
266+
}
267+
268+
// Create tarball
269+
tarballPath := filepath.Join(tmpDir, "test.tar.gz")
270+
tarCmd := exec.Command("tar", "-czf", tarballPath, "-C", tarballDir, ".")
271+
if err := tarCmd.Run(); err != nil {
272+
t.Fatalf("failed to create tarball: %v", err)
273+
}
274+
275+
// This is what the CURRENT production code does - always uses strip=3
276+
// For scoped packages, this is WRONG and should be strip=4
277+
const currentProductionStripComponents = 3
278+
279+
// Extract to _link_deps/<npmName>/ using the current production logic
280+
extractDir := filepath.Join(tmpDir, "_link_deps", tt.npmName)
281+
if err := os.MkdirAll(extractDir, 0755); err != nil {
282+
t.Fatal(err)
283+
}
284+
285+
tarballFilter := fmt.Sprintf("./node_modules/%s/", tt.npmName)
286+
extractCmd := exec.Command("tar", "-xzf", tarballPath, "-C", extractDir,
287+
fmt.Sprintf("--strip-components=%d", currentProductionStripComponents), tarballFilter)
288+
if err := extractCmd.Run(); err != nil {
289+
t.Fatalf("extraction failed: %v", err)
290+
}
291+
292+
// Check if package.json is at the correct location
293+
correctPath := filepath.Join(extractDir, "package.json")
294+
if _, err := os.Stat(correctPath); err != nil {
295+
// List what was actually extracted for debugging
296+
files, _ := filepath.Glob(filepath.Join(extractDir, "*"))
297+
t.Errorf("package.json should be at %s but wasn't found. Extracted files: %v", correctPath, files)
298+
}
299+
})
300+
}
301+
}

0 commit comments

Comments
 (0)