@@ -21,9 +21,8 @@ import {
2121 unlinkSync ,
2222 writeFileSync ,
2323} from "node:fs" ;
24- import { basename , dirname , isAbsolute , join , relative } from "node:path" ;
24+ import { basename , dirname , isAbsolute , join , posix , relative } from "node:path" ;
2525import { fileURLToPath , pathToFileURL } from "node:url" ;
26- import { expandPackageDistImportClosure } from "./lib/package-dist-imports.mjs" ;
2726import { resolveNpmRunner } from "./npm-runner.mjs" ;
2827
2928export const BUNDLED_PLUGIN_INSTALL_TARGETS = [ ] ;
@@ -272,6 +271,137 @@ function pruneEmptyDistDirectories(params = {}) {
272271 prune ( distRoot . distDir ) ;
273272}
274273
274+ const JS_DIST_FILE_RE = / ^ d i s t \/ .* \. (?: c j s | j s | m j s ) $ / u;
275+
276+ function stripSpecifierSuffix ( value ) {
277+ return value . replace ( / [ ? # ] .* $ / u, "" ) ;
278+ }
279+
280+ function resolveDistImportPath ( importerPath , specifier ) {
281+ if ( ! specifier . startsWith ( "." ) ) {
282+ return null ;
283+ }
284+ const stripped = stripSpecifierSuffix ( specifier ) ;
285+ if ( ! stripped ) {
286+ return null ;
287+ }
288+ return posix . normalize ( posix . join ( posix . dirname ( importerPath ) , stripped ) ) ;
289+ }
290+
291+ function findStatementStart ( source , index ) {
292+ return (
293+ Math . max (
294+ source . lastIndexOf ( ";" , index ) ,
295+ source . lastIndexOf ( "{" , index ) ,
296+ source . lastIndexOf ( "}" , index ) ,
297+ source . lastIndexOf ( "\n" , index ) ,
298+ source . lastIndexOf ( "\r" , index ) ,
299+ ) + 1
300+ ) ;
301+ }
302+
303+ function isImportSpecifierContext ( source , index ) {
304+ const dynamicPrefix = source . slice ( Math . max ( 0 , index - 32 ) , index ) ;
305+ if ( / \b i m p o r t \s * \( \s * $ / u. test ( dynamicPrefix ) ) {
306+ return true ;
307+ }
308+ const statementPrefix = source . slice ( findStatementStart ( source , index ) , index ) . trimStart ( ) ;
309+ return (
310+ / ^ (?: i m p o r t | e x p o r t ) \b [ \s \S ] * \b f r o m \s * $ / u. test ( statementPrefix ) ||
311+ / ^ i m p o r t \s * $ / u. test ( statementPrefix )
312+ ) ;
313+ }
314+
315+ function collectImportSpecifiers ( source ) {
316+ const specifiers = [ ] ;
317+ let inBlockComment = false ;
318+ let inLineComment = false ;
319+ for ( let index = 0 ; index < source . length ; index += 1 ) {
320+ if ( inBlockComment ) {
321+ if ( source [ index ] === "*" && source [ index + 1 ] === "/" ) {
322+ inBlockComment = false ;
323+ index += 1 ;
324+ }
325+ continue ;
326+ }
327+ if ( inLineComment ) {
328+ if ( source [ index ] === "\n" || source [ index ] === "\r" ) {
329+ inLineComment = false ;
330+ }
331+ continue ;
332+ }
333+ if ( source [ index ] === "/" && source [ index + 1 ] === "*" ) {
334+ inBlockComment = true ;
335+ index += 1 ;
336+ continue ;
337+ }
338+ if ( source [ index ] === "/" && source [ index + 1 ] === "/" ) {
339+ inLineComment = true ;
340+ index += 1 ;
341+ continue ;
342+ }
343+
344+ const quote = source [ index ] ;
345+ if ( quote !== '"' && quote !== "'" ) {
346+ continue ;
347+ }
348+
349+ let cursor = index + 1 ;
350+ let value = "" ;
351+ while ( cursor < source . length ) {
352+ const char = source [ cursor ] ;
353+ if ( char === "\\" ) {
354+ value += source . slice ( cursor , cursor + 2 ) ;
355+ cursor += 2 ;
356+ continue ;
357+ }
358+ if ( char === quote ) {
359+ break ;
360+ }
361+ value += char ;
362+ cursor += 1 ;
363+ }
364+ if ( cursor >= source . length ) {
365+ break ;
366+ }
367+
368+ if ( value . startsWith ( "." ) && isImportSpecifierContext ( source , index ) ) {
369+ specifiers . push ( value ) ;
370+ }
371+ index = cursor ;
372+ }
373+ return specifiers ;
374+ }
375+
376+ function expandInstalledDistImportClosure ( params ) {
377+ const files = [ ...new Set ( params . files ) ] ;
378+ const fileSet = new Set ( files ) ;
379+ const expectedSet = new Set ( params . seedFiles ) ;
380+ let changed = true ;
381+
382+ while ( changed ) {
383+ changed = false ;
384+ for ( const importerPath of [ ...expectedSet ]
385+ . filter ( ( file ) => fileSet . has ( file ) )
386+ . toSorted ( ( left , right ) => left . localeCompare ( right ) ) ) {
387+ if ( ! JS_DIST_FILE_RE . test ( importerPath ) || importerPath . includes ( "/node_modules/" ) ) {
388+ continue ;
389+ }
390+ const source = params . readText ( importerPath ) ;
391+ for ( const specifier of collectImportSpecifiers ( source ) ) {
392+ const importedPath = resolveDistImportPath ( importerPath , specifier ) ;
393+ if ( ! importedPath || ! fileSet . has ( importedPath ) || expectedSet . has ( importedPath ) ) {
394+ continue ;
395+ }
396+ expectedSet . add ( importedPath ) ;
397+ changed = true ;
398+ }
399+ }
400+ }
401+
402+ return [ ...expectedSet ] . toSorted ( ( left , right ) => left . localeCompare ( right ) ) ;
403+ }
404+
275405export function pruneInstalledPackageDist ( params = { } ) {
276406 const packageRoot = params . packageRoot ?? DEFAULT_PACKAGE_ROOT ;
277407 const removeFile = params . unlinkSync ?? unlinkSync ;
@@ -295,7 +425,7 @@ export function pruneInstalledPackageDist(params = {}) {
295425 const installedFiles = listInstalledDistFiles ( params ) ;
296426 const readFile = params . readFileSync ?? readFileSync ;
297427 expectedFiles = new Set (
298- expandPackageDistImportClosure ( {
428+ expandInstalledDistImportClosure ( {
299429 files : installedFiles ,
300430 seedFiles : [ ...expectedFiles ] ,
301431 readText ( relativePath ) {
0 commit comments