1010 * one verdict those surfaces share; this audit makes reaching a raw catalog
1111 * without it a build failure.
1212 *
13- * The rule: a scanned module that imports a raw catalog must also import
13+ * The rule: a scanned module that reads a raw catalog must also call
1414 * `useIntegrationAccess`, or appear in {@link EXEMPT} with the reason it may see
1515 * every integration (a lookup by id, a public page, a builder whose caller
1616 * gates). An exemption that no longer covers a raw import is reported too, so
2525import { readdirSync , readFileSync } from 'node:fs'
2626import { dirname , join , relative , resolve } from 'node:path'
2727import { fileURLToPath } from 'node:url'
28+ import { escapeRegExp } from '@sim/utils/string'
2829
2930const APP_ROOT = resolve ( dirname ( fileURLToPath ( import . meta. url ) ) , '../apps/sim' )
3031const SCAN_ROOTS = [ 'app' , 'components' , 'hooks' , 'stores' , 'ee' ] as const
@@ -63,36 +64,54 @@ interface SourceFile {
6364
6465interface IntegrationAccessViolation {
6566 path : string
66- /** The raw catalog exports, or the fail-open policy, imported without leave; empty for a stale exemption. */
67+ /** The raw catalog exports, or the fail-open policy, read without leave; empty for a stale exemption. */
6768 names : string [ ]
6869 stale : boolean
6970}
7071
7172const NAMED_IMPORT = / i m p o r t \s + ( t y p e \s + ) ? \{ ( [ ^ } ] * ) \} \s * f r o m \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g
72- const NAMESPACE_IMPORT = / i m p o r t \s + \* \s + a s \s + \w + \s + f r o m \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g
73+ const NAMESPACE_IMPORT = / i m p o r t \s + \* \s + a s \s + ( [ \w $ ] + ) \s + f r o m \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g
7374
74- /** The names a module imports as values from `from`, by their exported name. */
75- function valueImports ( source : string , from : string ) : string [ ] {
76- const names : string [ ] = [ ]
75+ /** One export of a module, and the expression the importing module reaches it through. */
76+ interface ExportReference {
77+ exported : string
78+ /** The local binding, or `namespace.member` for a read through a namespace import. */
79+ local : string
80+ }
81+
82+ /** The exports of `from` a module reaches as values, through named or namespace imports. */
83+ function exportReferences ( source : string , from : string ) : ExportReference [ ] {
84+ const references : ExportReference [ ] = [ ]
7785 for ( const [ , typeOnly , specifiers , module ] of source . matchAll ( NAMED_IMPORT ) ) {
7886 if ( module !== from || typeOnly ) continue
7987 for ( const specifier of specifiers . split ( ',' ) ) {
80- const [ imported ] = specifier . trim ( ) . split ( / \s + a s \s + / )
81- if ( imported && ! imported . startsWith ( 'type ' ) ) names . push ( imported )
88+ const [ exported , local = exported ] = specifier . trim ( ) . split ( / \s + a s \s + / )
89+ if ( exported && ! exported . startsWith ( 'type ' ) ) references . push ( { exported, local } )
90+ }
91+ }
92+ for ( const [ , namespace , module ] of source . matchAll ( NAMESPACE_IMPORT ) ) {
93+ if ( module !== from ) continue
94+ const memberRead = new RegExp ( `(?<![\\w$.])${ escapeRegExp ( namespace ) } \\.([\\w$]+)` , 'g' )
95+ for ( const [ local , exported ] of source . matchAll ( memberRead ) ) {
96+ references . push ( { exported, local } )
8297 }
8398 }
84- return names
99+ return references
85100}
86101
87- /** The raw catalog exports a module imports as values. */
88- function rawCatalogImports ( source : string ) : string [ ] {
89- const names = Object . entries ( RAW_CATALOGS ) . flatMap ( ( [ module , catalog ] ) =>
90- valueImports ( source , module ) . filter ( ( name ) => catalog . includes ( name ) )
91- )
92- for ( const [ , from ] of source . matchAll ( NAMESPACE_IMPORT ) ) {
93- if ( RAW_CATALOGS [ from ] ) names . push ( ...RAW_CATALOGS [ from ] )
102+ function isCalled ( source : string , local : string ) : boolean {
103+ return new RegExp ( `(?<![\\w$.])${ escapeRegExp ( local ) } \\s*\\(` ) . test ( source )
104+ }
105+
106+ /** The raw catalog exports a module reads. */
107+ function rawCatalogReads ( source : string ) : string [ ] {
108+ const names = new Set < string > ( )
109+ for ( const [ module , catalog ] of Object . entries ( RAW_CATALOGS ) ) {
110+ for ( const { exported } of exportReferences ( source , module ) ) {
111+ if ( catalog . includes ( exported ) ) names . add ( exported )
112+ }
94113 }
95- return names
114+ return [ ... names ]
96115}
97116
98117function exemptionFor ( path : string , exempt : Readonly < Record < string , string > > ) : string | undefined {
@@ -108,18 +127,22 @@ export function findIntegrationAccessViolations(
108127 const violations : IntegrationAccessViolation [ ] = [ ]
109128 const usedExemptions = new Set < string > ( )
110129 for ( const { path, source } of files ) {
111- const gateImports = valueImports ( source , GATE_MODULE )
112- if ( gateImports . includes ( FAIL_OPEN_NAME ) && path !== FAIL_OPEN_READER ) {
130+ const gateReferences = exportReferences ( source , GATE_MODULE )
131+ const readsFailOpen = gateReferences . some ( ( { exported } ) => exported === FAIL_OPEN_NAME )
132+ if ( readsFailOpen && path !== FAIL_OPEN_READER ) {
113133 violations . push ( { path, names : [ FAIL_OPEN_NAME ] , stale : false } )
114134 }
115- const names = rawCatalogImports ( source )
135+ const names = rawCatalogReads ( source )
116136 if ( names . length === 0 ) continue
117137 const exemption = exemptionFor ( path , exempt )
118138 if ( exemption ) {
119139 usedExemptions . add ( exemption )
120140 continue
121141 }
122- if ( ! gateImports . includes ( GATE_NAME ) ) violations . push ( { path, names, stale : false } )
142+ const callsGate = gateReferences . some (
143+ ( { exported, local } ) => exported === GATE_NAME && isCalled ( source , local )
144+ )
145+ if ( ! callsGate ) violations . push ( { path, names, stale : false } )
123146 }
124147 for ( const entry of Object . keys ( exempt ) ) {
125148 if ( ! usedExemptions . has ( entry ) ) violations . push ( { path : entry , names : [ ] , stale : true } )
@@ -152,21 +175,21 @@ function collectSources(): SourceFile[] {
152175function main ( ) {
153176 const violations = findIntegrationAccessViolations ( collectSources ( ) , EXEMPT )
154177 if ( violations . length === 0 ) {
155- console . log ( '✓ every integration listing reads useIntegrationAccess' )
178+ console . log ( '✓ every integration listing calls useIntegrationAccess' )
156179 return
157180 }
158181 for ( const violation of violations ) {
159182 if ( violation . stale ) {
160183 console . error (
161- `✗ ${ violation . path } : exempt in check-integration-access.ts but imports no raw catalog — remove the entry`
184+ `✗ ${ violation . path } : exempt in check-integration-access.ts but reads no raw catalog — remove the entry`
162185 )
163186 } else if ( violation . names . includes ( FAIL_OPEN_NAME ) ) {
164187 console . error (
165- `✗ apps/sim/${ violation . path } : imports ${ FAIL_OPEN_NAME } , which fails open while loading — only ${ FAIL_OPEN_READER } may`
188+ `✗ apps/sim/${ violation . path } : reads ${ FAIL_OPEN_NAME } , which fails open while loading — only ${ FAIL_OPEN_READER } may`
166189 )
167190 } else {
168191 console . error (
169- `✗ apps/sim/${ violation . path } : imports ${ violation . names . join ( ', ' ) } without ${ GATE_NAME } `
192+ `✗ apps/sim/${ violation . path } : reads ${ violation . names . join ( ', ' ) } without calling ${ GATE_NAME } `
170193 )
171194 }
172195 }
0 commit comments