@@ -2454,3 +2454,241 @@ func getPackageNames(ws *Workspace) []string {
24542454 }
24552455 return names
24562456}
2457+
2458+ // TestYarnPackage_ScopedLinkDependencies_Integration verifies that yarn packages with link:
2459+ // dependencies to scoped packages (e.g., @scope/pkg) are correctly built.
2460+ //
2461+ // Scoped packages have an extra directory level in node_modules:
2462+ // ./node_modules/@scope/pkg-name/package.json
2463+ //
2464+ // This requires different --strip-components handling than non-scoped packages.
2465+ func TestYarnPackage_ScopedLinkDependencies_Integration (t * testing.T ) {
2466+ if testing .Short () {
2467+ t .Skip ("Skipping integration test in short mode" )
2468+ }
2469+
2470+ // Ensure yarn is available
2471+ if err := exec .Command ("yarn" , "--version" ).Run (); err != nil {
2472+ t .Skip ("yarn not available, skipping integration test" )
2473+ }
2474+
2475+ // Ensure node is available
2476+ if err := exec .Command ("node" , "--version" ).Run (); err != nil {
2477+ t .Skip ("node not available, skipping integration test" )
2478+ }
2479+
2480+ tmpDir := t .TempDir ()
2481+
2482+ // Create WORKSPACE.yaml
2483+ workspaceYAML := `defaultTarget: "app:lib"`
2484+ workspacePath := filepath .Join (tmpDir , "WORKSPACE.yaml" )
2485+ if err := os .WriteFile (workspacePath , []byte (workspaceYAML ), 0644 ); err != nil {
2486+ t .Fatal (err )
2487+ }
2488+
2489+ // Create scoped package directory structure: packages/@test/utils
2490+ scopedPkgDir := filepath .Join (tmpDir , "packages" , "@test" , "utils" )
2491+ if err := os .MkdirAll (scopedPkgDir , 0755 ); err != nil {
2492+ t .Fatal (err )
2493+ }
2494+
2495+ // Create scoped package package.json
2496+ scopedPkgPackageJSON := `{
2497+ "name": "@test/utils",
2498+ "version": "1.0.0",
2499+ "main": "index.js"
2500+ }`
2501+ if err := os .WriteFile (filepath .Join (scopedPkgDir , "package.json" ), []byte (scopedPkgPackageJSON ), 0644 ); err != nil {
2502+ t .Fatal (err )
2503+ }
2504+
2505+ // Create scoped package index.js
2506+ scopedPkgIndexJS := `module.exports = {
2507+ formatName: function(name) {
2508+ return "Formatted: " + name;
2509+ }
2510+ };`
2511+ if err := os .WriteFile (filepath .Join (scopedPkgDir , "index.js" ), []byte (scopedPkgIndexJS ), 0644 ); err != nil {
2512+ t .Fatal (err )
2513+ }
2514+
2515+ // Create scoped package yarn.lock (empty but required for YarnApp)
2516+ scopedPkgYarnLock := `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2517+ # yarn lockfile v1
2518+
2519+ `
2520+ if err := os .WriteFile (filepath .Join (scopedPkgDir , "yarn.lock" ), []byte (scopedPkgYarnLock ), 0644 ); err != nil {
2521+ t .Fatal (err )
2522+ }
2523+
2524+ // Create scoped package BUILD.yaml
2525+ // Using packaging: app (YarnApp) to test the ./node_modules/@scope/pkg/ extraction path
2526+ // YarnApp creates a tarball with ./node_modules/<pkg-name>/ structure
2527+ scopedPkgBuildYAML := `packages:
2528+ - name: lib
2529+ type: yarn
2530+ srcs:
2531+ - "package.json"
2532+ - "yarn.lock"
2533+ - "index.js"
2534+ config:
2535+ packaging: app
2536+ dontTest: true
2537+ commands:
2538+ build: ["echo", "build complete"]`
2539+ if err := os .WriteFile (filepath .Join (scopedPkgDir , "BUILD.yaml" ), []byte (scopedPkgBuildYAML ), 0644 ); err != nil {
2540+ t .Fatal (err )
2541+ }
2542+
2543+ // Create app directory (depends on @test/utils)
2544+ appDir := filepath .Join (tmpDir , "app" )
2545+ if err := os .MkdirAll (appDir , 0755 ); err != nil {
2546+ t .Fatal (err )
2547+ }
2548+
2549+ // Create app package.json with link: dependency to scoped package
2550+ appPackageJSON := `{
2551+ "name": "test-app",
2552+ "version": "1.0.0",
2553+ "dependencies": {
2554+ "@test/utils": "link:./../packages/@test/utils"
2555+ },
2556+ "scripts": {
2557+ "test": "node test.js"
2558+ }
2559+ }`
2560+ if err := os .WriteFile (filepath .Join (appDir , "package.json" ), []byte (appPackageJSON ), 0644 ); err != nil {
2561+ t .Fatal (err )
2562+ }
2563+
2564+ // Create app yarn.lock with link: reference to scoped package
2565+ // Note: yarn.lock normalizes the path
2566+ appYarnLock := `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2567+ # yarn lockfile v1
2568+
2569+
2570+ "@test/utils@link:../packages/@test/utils":
2571+ version "1.0.0"
2572+ `
2573+ if err := os .WriteFile (filepath .Join (appDir , "yarn.lock" ), []byte (appYarnLock ), 0644 ); err != nil {
2574+ t .Fatal (err )
2575+ }
2576+
2577+ // Create app test.js that uses the scoped package
2578+ appTestJS := `const utils = require('@test/utils');
2579+ const result = utils.formatName('World');
2580+ if (result !== 'Formatted: World') {
2581+ console.error('Expected "Formatted: World" but got:', result);
2582+ process.exit(1);
2583+ }
2584+ console.log('Test passed:', result);`
2585+ if err := os .WriteFile (filepath .Join (appDir , "test.js" ), []byte (appTestJS ), 0644 ); err != nil {
2586+ t .Fatal (err )
2587+ }
2588+
2589+ // Create app BUILD.yaml
2590+ appBuildYAML := `packages:
2591+ - name: lib
2592+ type: yarn
2593+ srcs:
2594+ - "package.json"
2595+ - "yarn.lock"
2596+ - "test.js"
2597+ deps:
2598+ - packages/@test/utils:lib
2599+ config:
2600+ packaging: library
2601+ dontTest: true
2602+ commands:
2603+ build: ["echo", "build complete"]`
2604+ if err := os .WriteFile (filepath .Join (appDir , "BUILD.yaml" ), []byte (appBuildYAML ), 0644 ); err != nil {
2605+ t .Fatal (err )
2606+ }
2607+
2608+ // Initialize git repository (required for leeway)
2609+ gitInit := exec .Command ("git" , "init" )
2610+ gitInit .Dir = tmpDir
2611+ gitInit .Env = append (os .Environ (), "GIT_CONFIG_GLOBAL=/dev/null" , "GIT_CONFIG_SYSTEM=/dev/null" )
2612+ if err := gitInit .Run (); err != nil {
2613+ t .Fatalf ("Failed to initialize git repository: %v" , err )
2614+ }
2615+
2616+ gitConfigName := exec .Command ("git" , "config" , "user.name" , "Test User" )
2617+ gitConfigName .Dir = tmpDir
2618+ gitConfigName .Env = append (os .Environ (), "GIT_CONFIG_GLOBAL=/dev/null" , "GIT_CONFIG_SYSTEM=/dev/null" )
2619+ if err := gitConfigName .Run (); err != nil {
2620+ t .Fatalf ("Failed to configure git user.name: %v" , err )
2621+ }
2622+
2623+ gitConfigEmail := exec .Command ("git" , "config" , "user.email" , "test@example.com" )
2624+ gitConfigEmail .Dir = tmpDir
2625+ gitConfigEmail .Env = append (os .Environ (), "GIT_CONFIG_GLOBAL=/dev/null" , "GIT_CONFIG_SYSTEM=/dev/null" )
2626+ if err := gitConfigEmail .Run (); err != nil {
2627+ t .Fatalf ("Failed to configure git user.email: %v" , err )
2628+ }
2629+
2630+ gitAdd := exec .Command ("git" , "add" , "." )
2631+ gitAdd .Dir = tmpDir
2632+ gitAdd .Env = append (os .Environ (), "GIT_CONFIG_GLOBAL=/dev/null" , "GIT_CONFIG_SYSTEM=/dev/null" )
2633+ if err := gitAdd .Run (); err != nil {
2634+ t .Fatalf ("Failed to git add: %v" , err )
2635+ }
2636+
2637+ gitCommit := exec .Command ("git" , "commit" , "-m" , "initial" )
2638+ gitCommit .Dir = tmpDir
2639+ gitCommit .Env = append (os .Environ (),
2640+ "GIT_CONFIG_GLOBAL=/dev/null" ,
2641+ "GIT_CONFIG_SYSTEM=/dev/null" ,
2642+ "GIT_AUTHOR_DATE=2021-01-01T00:00:00Z" ,
2643+ "GIT_COMMITTER_DATE=2021-01-01T00:00:00Z" ,
2644+ )
2645+ if err := gitCommit .Run (); err != nil {
2646+ t .Fatalf ("Failed to git commit: %v" , err )
2647+ }
2648+
2649+ // Load workspace
2650+ workspace , err := FindWorkspace (tmpDir , Arguments {}, "" , "" )
2651+ if err != nil {
2652+ t .Fatalf ("Failed to load workspace: %v" , err )
2653+ }
2654+
2655+ // Get app package
2656+ pkg , ok := workspace .Packages ["app:lib" ]
2657+ if ! ok {
2658+ t .Fatalf ("Package app:lib not found in workspace. Available packages: %v" , getPackageNames (& workspace ))
2659+ }
2660+
2661+ // Create local cache
2662+ cacheDir := filepath .Join (tmpDir , ".cache" )
2663+ if err := os .MkdirAll (cacheDir , 0755 ); err != nil {
2664+ t .Fatal (err )
2665+ }
2666+
2667+ localCache , err := local .NewFilesystemCache (cacheDir )
2668+ if err != nil {
2669+ t .Fatalf ("Failed to create local cache: %v" , err )
2670+ }
2671+
2672+ // Build the app package (which depends on @test/utils via link:)
2673+ t .Log ("Building app:lib which depends on @test/utils:lib via link: dependency (scoped package)" )
2674+
2675+ err = Build (pkg ,
2676+ WithLocalCache (localCache ),
2677+ WithDontTest (true ),
2678+ )
2679+
2680+ if err != nil {
2681+ t .Fatalf ("Build failed: %v\n \n This likely means scoped package link: dependency handling is broken." , err )
2682+ }
2683+
2684+ t .Log ("✅ Build succeeded - scoped package link: dependency was correctly resolved" )
2685+
2686+ // Verify cache artifact exists
2687+ cachePath , exists := localCache .Location (pkg )
2688+ if ! exists {
2689+ t .Fatal ("Package not found in cache after build" )
2690+ }
2691+
2692+ t .Logf ("Cache artifact created at: %s" , cachePath )
2693+ t .Log ("✅ Yarn scoped package link: dependency integration test passed" )
2694+ }
0 commit comments