This commit is contained in:
OllieinCanada 2026-09-26 08:50:30 -04:00 • committed by GitHub
commit 4d3f2353d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 166 additions and 1 deletions

View file

@ -0,0 +1,137 @@
import fs from 'node:fs'
import path from 'node:path'
import { candidate, css, html, js, test, ts, yaml } from '../utils'
const DIST = path.resolve(import.meta.dirname, '../../dist')
const resolutions = Object.fromEntries(
fs
.readdirSync(DIST)
.filter((filename) => filename.endsWith('.tgz'))
.map((filename) => {
let name =
filename === 'tailwindcss.tgz'
? 'tailwindcss'
: `@tailwindcss/${filename.slice('tailwindcss-'.length, -'.tgz'.length)}`
let tarball = path.join(DIST, filename).replaceAll('\\', '/')
return [name, `file:${tarball}`]
}),
)
test(
'Yarn PnP scans a physical workspace package through its virtual peer-dependency path',
{
installDependencies: false,
timeout: 240_000,
fs: {
'package.json': JSON.stringify(
{
private: true,
packageManager: 'yarn@4.18.0',
workspaces: ['packages/*'],
resolutions,
},
null,
2,
),
'.yarnrc.yml': yaml`
#
nodeLinker: pnp
enableGlobalCache: false
`,
'packages/app/package.json': JSON.stringify(
{
name: 'app',
private: true,
type: 'module',
dependencies: {
dep: 'workspace:*',
react: '19.2.0',
},
devDependencies: {
'@tailwindcss/vite': 'workspace:^',
tailwindcss: 'workspace:^',
vite: '^7',
},
},
null,
2,
),
'packages/app/index.html': html`
<head>
<link rel="stylesheet" href="/src/index.css" />
</head>
<body>
<div class="twp:p-10"></div>
</body>
`,
'packages/app/src/index.css': css` @import 'dep/style.css'; `,
'packages/app/vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'packages/dep/package.json': JSON.stringify(
{
name: 'dep',
type: 'module',
exports: {
'./style.css': './style.css',
},
devDependencies: {
tailwindcss: 'workspace:^',
},
peerDependencies: {
react: '>=18',
},
},
null,
2,
),
'packages/dep/style.css': css`
@import 'tailwindcss' prefix(twp) source(none);
@plugin './plugin.js';
@source './src';
@source not './src/excluded.ts';
`,
'packages/dep/plugin.js': js`
export default function ({ addUtilities }) {
addUtilities({
'.from-virtual-plugin': {
display: 'block',
},
})
}
`,
'packages/dep/src/index.ts': ts` export let classes = 'twp:flex twp:from-virtual-plugin' `,
'packages/dep/src/excluded.ts': ts` export let classes = 'twp:hidden' `,
},
},
async ({ root, exec, fs, expect }) => {
await exec('corepack yarn install')
let resolution = await exec(
`corepack yarn workspace app node -e "const p=require('pnpapi');const v=p.resolveToUnqualified('dep',process.cwd()+'/');console.log('VIRTUAL='+JSON.stringify({virtual:v.includes('__virtual__'),resolved:p.resolveVirtual(v)}))"`,
)
let virtual = JSON.parse(resolution.match(/VIRTUAL=(.*)/)![1])
expect(virtual.virtual).toBe(true)
expect(virtual.resolved).toBe(path.join(root, 'packages/dep') + path.sep)
await exec('corepack yarn workspace app vite build')
let files = await fs.glob('packages/app/dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]
await fs.expectFileToContain(filename, [
candidate`twp:flex`,
candidate`twp:from-virtual-plugin`,
])
await fs.expectFileNotToContain(filename, [candidate`twp:hidden`, candidate`twp:p-10`])
},
)

View file

@ -10,6 +10,7 @@ import {
import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner } from '@tailwindcss/oxide'
import fs from 'node:fs/promises'
import Module from 'node:module'
import path from 'node:path'
import type { Environment, InternalResolveOptions, Plugin, ResolvedConfig } from 'vite'
import * as vite from 'vite'
@ -308,6 +309,28 @@ function idToPath(id: string) {
return path.resolve(id.replace(/\?.*$/, ''))
}
type PnpApi = {
resolveVirtual?: (path: string) => string | null
}
function devirtualizeScannerSourceBase(base: string) {
let findPnpApi = (
Module as typeof Module & {
findPnpApi?: (lookupSource: string) => PnpApi | null
}
).findPnpApi
let resolved = findPnpApi?.(base)?.resolveVirtual?.(base)
// Yarn's JavaScript filesystem layer can access virtual package paths, but
// the native scanner cannot. Keep the original path everywhere else and
// only devirtualize the base handed to the filesystem scanner. Paths inside
// ZIP archives are still virtual filesystems, so leave those untouched.
return resolved && !resolved.split(/[\\/]/).some((segment) => segment.endsWith('.zip'))
? resolved
: base
}
/**
* A Map that can generate default values for keys that don't exist.
* Generated default values are added to the map to avoid recomputation.
@ -431,7 +454,12 @@ class Root {
return [{ ...this.compiler.root, negated: false }]
})().concat(this.compiler.sources)
this.scanner = new Scanner({ sources })
let scannerSources = sources.map((source) => {
let base = devirtualizeScannerSourceBase(source.base)
return { ...source, base }
})
this.scanner = new Scanner({ sources: scannerSources })
DEBUG && I.end('Setup scanner')
} else {
for (let buildDependency of this.buildDependencies.keys()) {