@@ -22,7 +22,7 @@ import { Mime } from 'mime';
2222import otherMimes from 'mime/types/other.js' ;
2323import standardMimes from 'mime/types/standard.js' ;
2424import { gte , minVersion , satisfies } from 'semver' ;
25- import { escapePath , glob } from 'tinyglobby' ;
25+ import { glob } from 'tinyglobby' ;
2626
2727import {
2828 ACTOR_ENV_VARS ,
@@ -50,6 +50,10 @@ import { deleteFile, ensureFolderExistsSync, rimrafPromised } from './files.js';
5050import type { AuthJSON } from './types.js' ;
5151import { cliDebugPrint } from './utils/cliDebugPrint.js' ;
5252
53+ // `ignore` is a CJS package; TypeScript sees its default import as the module
54+ // object rather than the callable factory, so we cast through unknown.
55+ const makeIg = ignoreModule as unknown as ( ) => Ignore ;
56+
5357// Export AJV properly: https://github.com/ajv-validator/ajv/issues/2132
5458// Welcome to the state of JavaScript/TypeScript and CJS/ESM interop.
5559export const Ajv2019 = _Ajv2019 as unknown as typeof import ( 'ajv/dist/2019.js' ) . default ;
@@ -306,10 +310,6 @@ const getGitignoreFallbackFilter = async (cwd: string): Promise<(paths: string[]
306310 expandDirectories : false ,
307311 } ) ;
308312
309- // `ignore` is a CJS package; TypeScript sees its default import as the module
310- // object rather than the callable factory, so we cast through unknown.
311- const makeIg = ignoreModule as unknown as ( ) => Ignore ;
312-
313313 const filters : { dir : string ; ig : Ignore ; ancestorPrefix ?: string } [ ] = [ ] ;
314314
315315 for ( const gitignoreFile of gitignoreFiles ) {
@@ -368,16 +368,59 @@ const getGitignoreFallbackFilter = async (cwd: string): Promise<(paths: string[]
368368 } ) ;
369369} ;
370370
371+ interface ActorIgnoreResult {
372+ /** Filter that removes paths matching non-negated .actorignore patterns */
373+ excludeFilter : ( ( paths : string [ ] ) => string [ ] ) | null ;
374+ /** Patterns from negation lines (with `!` stripped) — files matching these should be force-included even if git-ignored */
375+ forceIncludePatterns : string [ ] ;
376+ }
377+
378+ const parseActorIgnore = async ( cwd : string ) : Promise < ActorIgnoreResult > => {
379+ const actorignorePath = join ( cwd , '.actorignore' ) ;
380+ if ( ! existsSync ( actorignorePath ) ) {
381+ return { excludeFilter : null , forceIncludePatterns : [ ] } ;
382+ }
383+
384+ const content = await readFile ( actorignorePath , 'utf-8' ) ;
385+ const lines = content . split ( '\n' ) ;
386+
387+ const excludeLines : string [ ] = [ ] ;
388+ const forceIncludePatterns : string [ ] = [ ] ;
389+
390+ for ( const line of lines ) {
391+ const trimmed = line . trim ( ) ;
392+ if ( ! trimmed || trimmed . startsWith ( '#' ) ) continue ;
393+ if ( trimmed . startsWith ( '!' ) ) {
394+ forceIncludePatterns . push ( trimmed . slice ( 1 ) ) ;
395+ } else {
396+ excludeLines . push ( trimmed ) ;
397+ }
398+ }
399+
400+ const excludeFilter =
401+ excludeLines . length > 0
402+ ? ( paths : string [ ] ) => {
403+ const ig = makeIg ( ) . add ( excludeLines ) ;
404+ return paths . filter ( ( filePath ) => ! ig . ignores ( filePath ) ) ;
405+ }
406+ : null ;
407+
408+ return { excludeFilter, forceIncludePatterns } ;
409+ } ;
410+
371411/**
372- * Get Actor local files, omit files defined in .gitignore and .git folder
412+ * Get Actor local files, omit files defined in .gitignore, .actorignore and .git folder
373413 * All dot files(.file) and folders(.folder/) are included.
374414 */
375415export const getActorLocalFilePaths = async ( cwd ?: string ) => {
376416 const resolvedCwd = cwd ?? process . cwd ( ) ;
377417
378- const ignore = [ '.git/**' , 'apify_storage' , 'node_modules' , 'storage' , 'crawlee_storage' ] ;
418+ const hardcodedIgnore = [ '.git/**' , 'apify_storage' , 'node_modules' , 'storage' , 'crawlee_storage' ] ;
379419
380- let fallbackFilter : ( ( paths : string [ ] ) => string [ ] ) | null = null ;
420+ // Parse .actorignore early to get both exclude filter and force-include patterns
421+ const { excludeFilter : actorignoreFilter , forceIncludePatterns } = await parseActorIgnore ( resolvedCwd ) ;
422+
423+ let gitIgnoreFilter : ( ( paths : string [ ] ) => string [ ] ) | null = null ;
381424
382425 // Use git ls-files to get gitignored paths — this correctly handles ancestor .gitignore files,
383426 // nested .gitignore files, .git/info/exclude, and global gitignore config
@@ -388,23 +431,53 @@ export const getActorLocalFilePaths = async (cwd?: string) => {
388431 stdio : [ 'ignore' , 'pipe' , 'ignore' ] ,
389432 } )
390433 . split ( '\n' )
391- . filter ( Boolean )
392- . map ( ( p ) => escapePath ( p ) ) ;
434+ . filter ( Boolean ) ;
393435
394- ignore . push ( ...gitIgnored ) ;
436+ if ( gitIgnored . length > 0 ) {
437+ const ig = makeIg ( ) . add ( gitIgnored ) ;
438+ gitIgnoreFilter = ( paths ) => paths . filter ( ( p ) => ! ig . ignores ( p ) ) ;
439+ }
395440 } catch {
396441 // git is unavailable or directory is not a git repo — fall back to parsing .gitignore files
397- fallbackFilter = await getGitignoreFallbackFilter ( resolvedCwd ) ;
442+ gitIgnoreFilter = await getGitignoreFallbackFilter ( resolvedCwd ) ;
398443 }
399444
400- const paths = await glob ( [ '*' , '**/**' ] , {
401- ignore,
445+ const allFiles = await glob ( [ '*' , '**/**' ] , {
446+ ignore : hardcodedIgnore ,
402447 dot : true ,
403448 expandDirectories : false ,
404449 cwd : resolvedCwd ,
405450 } ) ;
406451
407- return fallbackFilter ? fallbackFilter ( paths ) : paths ;
452+ let paths = gitIgnoreFilter ? gitIgnoreFilter ( allFiles ) : allFiles ;
453+
454+ if ( actorignoreFilter ) {
455+ paths = actorignoreFilter ( paths ) ;
456+ }
457+
458+ // Force-include: negation patterns in .actorignore (e.g. !dist/) override gitignore,
459+ // allowing git-ignored files to be included in the push
460+ if ( forceIncludePatterns . length > 0 ) {
461+ const forceIncludeIg = makeIg ( ) . add ( forceIncludePatterns ) ;
462+ const forceIncluded = allFiles . filter ( ( filePath ) => forceIncludeIg . ignores ( filePath ) ) ;
463+ const pathSet = new Set ( paths ) ;
464+ for ( const file of forceIncluded ) {
465+ pathSet . add ( file ) ;
466+ }
467+ paths = [ ...pathSet ] ;
468+ }
469+
470+ // .actor/ is the Actor specification folder — always include it regardless of gitignore/actorignore
471+ const actorSpecFiles = allFiles . filter ( ( p ) => p === '.actor' || p . startsWith ( '.actor/' ) ) ;
472+ if ( actorSpecFiles . length > 0 ) {
473+ const pathSet = new Set ( paths ) ;
474+ for ( const file of actorSpecFiles ) {
475+ pathSet . add ( file ) ;
476+ }
477+ paths = [ ...pathSet ] ;
478+ }
479+
480+ return paths ;
408481} ;
409482
410483/**
0 commit comments