mirror of
https://github.com/pestphp/pest.git
synced 2026-06-05 02:52:12 +02:00
wip
This commit is contained in:
@ -1,24 +1,50 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { readdir } from 'node:fs/promises'
|
import { readdir, readFile } from 'node:fs/promises'
|
||||||
import { existsSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
import { resolve, relative, extname, posix, sep, join } from 'node:path'
|
import { resolve, relative, extname, sep, join } from 'node:path'
|
||||||
import { pathToFileURL } from 'node:url'
|
import { pathToFileURL } from 'node:url'
|
||||||
|
|
||||||
const PAGE_EXTENSIONS = new Set(['.vue', '.tsx', '.jsx', '.svelte'])
|
const PAGE_EXTENSIONS = new Set(['.vue', '.tsx', '.jsx', '.svelte'])
|
||||||
|
const ASSET_EXT_RE = /\.(css|scss|sass|less|styl|stylus|svg|png|jpe?g|gif|webp|avif|ico|bmp|woff2?|ttf|eot|otf|md|mdx|txt|html|mp4|webm|mp3|wav|ogg|m4a|pdf|wasm|glsl|frag|vert)$/i
|
||||||
const PROJECT_ROOT = resolve(process.argv[2] ?? process.cwd())
|
const PROJECT_ROOT = resolve(process.argv[2] ?? process.cwd())
|
||||||
const PAGES_REL = (process.env.TIA_VITE_PAGES_DIR ?? 'resources/js/Pages').replace(/\\/g, '/')
|
const PAGES_REL = (process.env.TIA_VITE_PAGES_DIR ?? 'resources/js/Pages').replace(/\\/g, '/')
|
||||||
const TIMEOUT_MS = Number.parseInt(process.env.TIA_VITE_TIMEOUT_MS ?? '20000', 10)
|
|
||||||
const CONCURRENCY = Math.max(1, Number.parseInt(process.env.TIA_VITE_CONCURRENCY ?? '16', 10))
|
|
||||||
|
|
||||||
async function loadVite() {
|
async function loadRolldown() {
|
||||||
const projectRequire = createRequire(join(PROJECT_ROOT, 'package.json'))
|
const projectRequire = createRequire(join(PROJECT_ROOT, 'package.json'))
|
||||||
const vitePath = projectRequire.resolve('vite')
|
const path = projectRequire.resolve('rolldown')
|
||||||
return await import(pathToFileURL(vitePath).href)
|
return await import(pathToFileURL(path).href)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { createServer } = await loadVite()
|
async function readJsonWithComments(path) {
|
||||||
|
const raw = await readFile(path, 'utf8')
|
||||||
|
const stripped = raw
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||||
|
.replace(/(^|[^:])\/\/[^\n]*/g, '$1')
|
||||||
|
return JSON.parse(stripped)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAliasFromTsconfig() {
|
||||||
|
const alias = {}
|
||||||
|
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
||||||
|
const p = join(PROJECT_ROOT, name)
|
||||||
|
if (!existsSync(p)) continue
|
||||||
|
let cfg
|
||||||
|
try { cfg = await readJsonWithComments(p) } catch { continue }
|
||||||
|
const baseUrl = resolve(PROJECT_ROOT, cfg?.compilerOptions?.baseUrl ?? '.')
|
||||||
|
const paths = cfg?.compilerOptions?.paths ?? {}
|
||||||
|
for (const [key, targets] of Object.entries(paths)) {
|
||||||
|
if (!key.endsWith('/*')) continue
|
||||||
|
const t0 = Array.isArray(targets) ? targets[0] : null
|
||||||
|
if (typeof t0 !== 'string' || !t0.endsWith('/*')) continue
|
||||||
|
const aliasKey = key.slice(0, -2)
|
||||||
|
if (alias[aliasKey] !== undefined) continue
|
||||||
|
alias[aliasKey] = resolve(baseUrl, t0.slice(0, -2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return alias
|
||||||
|
}
|
||||||
|
|
||||||
async function listPageFiles(pagesDir) {
|
async function listPageFiles(pagesDir) {
|
||||||
if (!existsSync(pagesDir)) return []
|
if (!existsSync(pagesDir)) return []
|
||||||
@ -44,6 +70,14 @@ function componentNameFor(pageAbs, pagesDir) {
|
|||||||
return rel.slice(0, rel.length - ext.length)
|
return rel.slice(0, rel.length - ext.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLocalSpecifier(source, aliasKeys) {
|
||||||
|
if (source.startsWith('.') || source.startsWith('/')) return true
|
||||||
|
for (const key of aliasKeys) {
|
||||||
|
if (source === key || source.startsWith(key + '/')) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const pagesDir = resolve(PROJECT_ROOT, PAGES_REL)
|
const pagesDir = resolve(PROJECT_ROOT, PAGES_REL)
|
||||||
const pages = await listPageFiles(pagesDir)
|
const pages = await listPageFiles(pagesDir)
|
||||||
@ -53,100 +87,104 @@ async function main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = await createServer({
|
const { rolldown } = await loadRolldown()
|
||||||
configFile: undefined, // auto-detect vite.config.*
|
const alias = await loadAliasFromTsconfig()
|
||||||
root: PROJECT_ROOT,
|
const aliasKeys = Object.keys(alias)
|
||||||
logLevel: 'silent',
|
|
||||||
clearScreen: false,
|
const graph = new Map()
|
||||||
server: {
|
|
||||||
middlewareMode: true,
|
const collector = {
|
||||||
hmr: false,
|
name: 'pest-tia-collector',
|
||||||
watch: null,
|
moduleParsed(info) {
|
||||||
|
const id = info.id
|
||||||
|
if (!id || id.startsWith('\0')) return
|
||||||
|
const deps = new Set()
|
||||||
|
for (const i of info.importedIds) if (i && !i.startsWith('\0')) deps.add(i)
|
||||||
|
for (const i of info.dynamicallyImportedIds) if (i && !i.startsWith('\0')) deps.add(i)
|
||||||
|
graph.set(id, deps)
|
||||||
},
|
},
|
||||||
appType: 'custom',
|
|
||||||
optimizeDeps: { disabled: true },
|
|
||||||
})
|
|
||||||
|
|
||||||
const killer = setTimeout(() => {
|
|
||||||
server.close().catch(() => {}).finally(() => process.exit(2))
|
|
||||||
}, TIMEOUT_MS)
|
|
||||||
|
|
||||||
const reverse = new Map()
|
|
||||||
|
|
||||||
const pageComponentCache = new Map()
|
|
||||||
for (const page of pages) {
|
|
||||||
pageComponentCache.set(page, componentNameFor(page, pagesDir))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectRootPosix = PROJECT_ROOT.split(sep).join('/')
|
const externalBare = {
|
||||||
const pageEntries = pages.map((pagePath) => ({
|
name: 'pest-tia-external-bare',
|
||||||
pagePath,
|
resolveId(source) {
|
||||||
pageComponent: pageComponentCache.get(pagePath),
|
if (!source) return null
|
||||||
pageUrl: '/' + posix.relative(projectRootPosix, pagePath.split(sep).join('/')),
|
if (isLocalSpecifier(source, aliasKeys)) return null
|
||||||
}))
|
return { id: source, external: true }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetStub = {
|
||||||
|
name: 'pest-tia-asset-stub',
|
||||||
|
load(id) {
|
||||||
|
if (!id) return null
|
||||||
|
if (ASSET_EXT_RE.test(id)) {
|
||||||
|
return { code: 'export default null', moduleSideEffects: false }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = Object.create(null)
|
||||||
|
for (let i = 0; i < pages.length; i++) input[`p${i}`] = pages[i]
|
||||||
|
|
||||||
|
const bundle = await rolldown({
|
||||||
|
input,
|
||||||
|
cwd: PROJECT_ROOT,
|
||||||
|
resolve: {
|
||||||
|
alias,
|
||||||
|
extensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.json'],
|
||||||
|
},
|
||||||
|
transform: { jsx: 'preserve' },
|
||||||
|
treeshake: false,
|
||||||
|
plugins: [externalBare, assetStub, collector],
|
||||||
|
logLevel: 'silent',
|
||||||
|
onLog: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let cursor = 0
|
await bundle.generate({ format: 'esm' })
|
||||||
const workers = Array.from({ length: Math.min(CONCURRENCY, pageEntries.length) }, async () => {
|
} finally {
|
||||||
while (true) {
|
await bundle.close()
|
||||||
const i = cursor++
|
}
|
||||||
if (i >= pageEntries.length) return
|
|
||||||
const { pageUrl } = pageEntries[i]
|
|
||||||
try {
|
|
||||||
await server.transformRequest(pageUrl, { ssr: false })
|
|
||||||
} catch {
|
|
||||||
// ignore, handled below when we look up the module
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await Promise.all(workers)
|
|
||||||
|
|
||||||
const transitiveCache = new Map()
|
const reverse = new Map()
|
||||||
const computeTransitive = (mod, stack) => {
|
const transitiveCache = new Map()
|
||||||
const key = mod.file ?? mod.id
|
|
||||||
if (!key) return null
|
|
||||||
const cached = transitiveCache.get(key)
|
|
||||||
if (cached) return cached
|
|
||||||
if (stack.has(key)) return null // cycle: let the originating frame fold us in
|
|
||||||
|
|
||||||
stack.add(key)
|
const computeTransitive = (id, stack) => {
|
||||||
const acc = new Set()
|
const cached = transitiveCache.get(id)
|
||||||
for (const imported of mod.importedModules) {
|
if (cached) return cached
|
||||||
const id = imported.file ?? imported.id
|
if (stack.has(id)) return null
|
||||||
if (!id) continue
|
|
||||||
if (id.startsWith('\0')) continue
|
|
||||||
|
|
||||||
if (id.startsWith(PROJECT_ROOT)) {
|
stack.add(id)
|
||||||
const rel = relative(PROJECT_ROOT, id).split(sep).join('/')
|
const acc = new Set()
|
||||||
|
const deps = graph.get(id)
|
||||||
|
if (deps) {
|
||||||
|
for (const dep of deps) {
|
||||||
|
if (!dep || dep.startsWith('\0')) continue
|
||||||
|
if (dep.startsWith(PROJECT_ROOT)) {
|
||||||
|
const rel = relative(PROJECT_ROOT, dep).split(sep).join('/')
|
||||||
acc.add(rel)
|
acc.add(rel)
|
||||||
}
|
}
|
||||||
|
if (stack.has(dep)) continue
|
||||||
const childKey = id
|
const child = computeTransitive(dep, stack)
|
||||||
if (stack.has(childKey)) continue
|
|
||||||
const child = computeTransitive(imported, stack)
|
|
||||||
if (child) for (const r of child) acc.add(r)
|
if (child) for (const r of child) acc.add(r)
|
||||||
}
|
}
|
||||||
stack.delete(key)
|
|
||||||
transitiveCache.set(key, acc)
|
|
||||||
return acc
|
|
||||||
}
|
}
|
||||||
|
stack.delete(id)
|
||||||
|
transitiveCache.set(id, acc)
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
for (const { pageComponent, pageUrl } of pageEntries) {
|
for (const page of pages) {
|
||||||
const pageModule = await server.moduleGraph.getModuleByUrl(pageUrl, false)
|
const pageComponent = componentNameFor(page, pagesDir)
|
||||||
if (!pageModule) continue
|
const reachable = computeTransitive(page, new Set())
|
||||||
|
if (!reachable) continue
|
||||||
const reachable = computeTransitive(pageModule, new Set())
|
for (const rel of reachable) {
|
||||||
if (!reachable) continue
|
const bucket = reverse.get(rel) ?? new Set()
|
||||||
|
bucket.add(pageComponent)
|
||||||
for (const rel of reachable) {
|
reverse.set(rel, bucket)
|
||||||
const bucket = reverse.get(rel) ?? new Set()
|
|
||||||
bucket.add(pageComponent)
|
|
||||||
reverse.set(rel, bucket)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
clearTimeout(killer)
|
|
||||||
await server.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = Object.create(null)
|
const payload = Object.create(null)
|
||||||
|
|||||||
@ -249,7 +249,8 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
$alwaysEnabled = $watchPatterns->isEnabled()
|
$alwaysEnabled = $watchPatterns->isEnabled()
|
||||||
&& (! $watchPatterns->isLocally() || Environment::name() === Environment::LOCAL);
|
&& (! $watchPatterns->isLocally() || Environment::name() === Environment::LOCAL);
|
||||||
$enabled = $cliEnabled || $alwaysEnabled;
|
$enabled = $cliEnabled || $alwaysEnabled;
|
||||||
$this->filteredMode = $this->hasArgument(self::FILTERED_OPTION, $arguments) || $watchPatterns->isFiltered();
|
$this->filteredMode = ($this->hasArgument(self::FILTERED_OPTION, $arguments) || $watchPatterns->isFiltered())
|
||||||
|
&& ! $this->hasExplicitPathArgument($arguments);
|
||||||
$freshRequested = $this->hasArgument(self::FRESH_OPTION, $arguments);
|
$freshRequested = $this->hasArgument(self::FRESH_OPTION, $arguments);
|
||||||
$this->forceRefetch = $this->hasArgument(self::REFETCH_OPTION, $arguments);
|
$this->forceRefetch = $this->hasArgument(self::REFETCH_OPTION, $arguments);
|
||||||
|
|
||||||
@ -1377,6 +1378,32 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
|||||||
return $coverage->coverage === true;
|
return $coverage->coverage === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
*/
|
||||||
|
private function hasExplicitPathArgument(array $arguments): bool
|
||||||
|
{
|
||||||
|
$projectRoot = TestSuite::getInstance()->rootPath;
|
||||||
|
|
||||||
|
foreach ($arguments as $arg) {
|
||||||
|
if ($arg === '' || str_starts_with($arg, '-')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_file($arg) || is_dir($arg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$absolute = rtrim($projectRoot, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.ltrim($arg, DIRECTORY_SEPARATOR);
|
||||||
|
|
||||||
|
if (is_file($absolute) || is_dir($absolute)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, string> $changedFiles
|
* @param array<int, string> $changedFiles
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user