@@ -28,6 +28,7 @@ const NATIVE_IS_SUPPORTED = isNativeSupported();
2828const moduleFileCache = { } ;
2929
3030const JINJA_SYNTAX = / { % | % } | { { | } } / ig;
31+ const JINJA_MACRO_DEFINITION = / { % [ - + ] ? \s * m a c r o \s / ;
3132
3233const getThreadsCount = ( ) => {
3334 const envThreads = getEnv ( 'transpilationWorkerThreadsCount' ) ;
@@ -100,6 +101,7 @@ export type TranspileOptions = {
100101 compilerId ?: string ;
101102 stage ?: 0 | 1 | 2 | 3 ;
102103 jinjaUsed ?: boolean ;
104+ jinjaMacrosFingerprint ?: string ;
103105} ;
104106
105107export type CompileStage = 0 | 1 | 2 | 3 ;
@@ -280,6 +282,8 @@ export class DataSchemaCompiler {
280282 this . loadJinjaTemplates ( jinjaTemplatedFiles ) ;
281283 }
282284
285+ const jinjaMacrosFingerprint = DataSchemaCompiler . computeJinjaMacrosFingerprint ( jinjaTemplatedFiles ) ;
286+
283287 const errorsReport = new ErrorReporter ( null , [ ] , this . errorReportOptions ) ;
284288 this . errorsReporter = errorsReport ;
285289
@@ -328,11 +332,11 @@ export class DataSchemaCompiler {
328332 }
329333
330334 const jinjaFilesTasks = jinjaTemplatedFiles
331- . map ( f => this . transpileJinjaFile ( f , errorsReport , { cubeNames, cubeSymbols, transpilerNames } ) ) ;
335+ . map ( f => this . transpileJinjaFile ( f , errorsReport , { cubeNames, cubeSymbols, transpilerNames, jinjaMacrosFingerprint } ) ) ;
332336
333337 results = ( await Promise . all ( [ ...jsFilesTasks , ...yamlFilesTasks , ...jinjaFilesTasks ] ) ) . flat ( ) ;
334338 } else {
335- results = await Promise . all ( toCompile . map ( f => this . transpileFile ( f , errorsReport , { cubeNames, cubeSymbols, transpilerNames } ) ) ) ;
339+ results = await Promise . all ( toCompile . map ( f => this . transpileFile ( f , errorsReport , { cubeNames, cubeSymbols, transpilerNames, jinjaMacrosFingerprint } ) ) ) ;
336340 }
337341
338342 return results . filter ( f => ! ! f ) as FileContent [ ] ;
@@ -576,6 +580,33 @@ export class DataSchemaCompiler {
576580 } ) ;
577581 }
578582
583+ /**
584+ * Macro files are hidden dependencies of any cube file that imports them —
585+ * minijinja resolves `{% import %}` lazily against its template store, so
586+ * the per-file Jinja render cache must be invalidated when *any* macro file
587+ * changes. Hashing all macro files together rather than tracking per-cube
588+ * imports keeps the implementation simple at the cost of over-invalidating
589+ * when macro edits happen (which is rare). CUB-2357.
590+ */
591+ private static computeJinjaMacrosFingerprint ( files : FileContent [ ] ) : string {
592+ const macroFiles = files
593+ . filter ( ( f ) => JINJA_MACRO_DEFINITION . test ( f . content ) )
594+ . sort ( ( a , b ) => a . fileName . localeCompare ( b . fileName ) ) ;
595+
596+ if ( macroFiles . length === 0 ) {
597+ return '' ;
598+ }
599+
600+ const hash = crypto . createHash ( 'md5' ) ;
601+ for ( const f of macroFiles ) {
602+ hash . update ( f . fileName ) ;
603+ hash . update ( '\0' ) ;
604+ hash . update ( f . content ) ;
605+ hash . update ( '\0' ) ;
606+ }
607+ return hash . digest ( 'hex' ) ;
608+ }
609+
579610 private prepareTranspileSymbols ( ) {
580611 const cubeNames : string [ ] = this . cubeDictionary . cubeNames ( ) ;
581612 // We need only cubes and all its member names for transpiling.
@@ -802,7 +833,11 @@ export class DataSchemaCompiler {
802833 errorsReport : ErrorReporter ,
803834 options : TranspileOptions
804835 ) : Promise < ( FileContent | undefined ) > {
805- const cacheKey = crypto . createHash ( 'md5' ) . update ( file . content ) . digest ( 'hex' ) ;
836+ const cacheKey = crypto . createHash ( 'md5' )
837+ . update ( file . content )
838+ . update ( '\0' )
839+ . update ( options . jinjaMacrosFingerprint || '' )
840+ . digest ( 'hex' ) ;
806841
807842 let renderedFileContent : string ;
808843
0 commit comments