Skip to content

Commit 80ba146

Browse files
committed
fix(docs): blank comments whole, and fail loudly when subBlock parsing breaks
blankStringsAndComments kept the first and last character of every match. That is correct for a quoted string, where both are delimiters, but for a '//' comment the last character is arbitrary source text -- so a commented-out '// options: [' left an unbalanced bracket inside the subBlocks span, findMatchingClose returned -1, and the extractor reported that the block exposes nothing. google_drive lost three user-settable mimeType rows that way. The block renders mimeType as an Export Format dropdown. A parse failure and 'this block exposes nothing' were indistinguishable, and the fallback was the destructive branch. Parsing now throws a SubBlockParseError when the bracket scan fails or when an array holding literal objects yields no ids; the call site reports it and exits non-zero in both generate and check mode rather than dropping the page.
1 parent fc5d754 commit 80ba146

3 files changed

Lines changed: 112 additions & 7 deletions

File tree

apps/docs/content/docs/en/integrations/google_drive.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ Get content from a file in Google Drive with complete metadata (exports Google W
153153
| Parameter | Type | Required | Description |
154154
| --------- | ---- | -------- | ----------- |
155155
| `fileId` | string | Yes | The ID of the file to get content from |
156+
| `mimeType` | string | No | The MIME type to export Google Workspace files to \(optional\) |
156157
| `includeRevisions` | boolean | No | Whether to include revision history in the metadata \(default: true, returns first 100 revisions\) |
157158

158159
#### Output
@@ -280,6 +281,7 @@ Upload a file to Google Drive with complete metadata returned
280281
| `fileName` | string | Yes | The name of the file to upload |
281282
| `file` | file | No | Binary file to upload \(UserFile object\) |
282283
| `content` | string | No | Text content to upload \(use this OR file, not both\) |
284+
| `mimeType` | string | No | The MIME type of the file to upload \(auto-detected from file if not provided\) |
283285
| `folderSelector` | string | No | Google Drive folder ID to upload the file to \(e.g., 1ABCxyz...\) |
284286

285287
#### Output
@@ -349,6 +351,7 @@ Download a file from Google Drive with complete metadata (exports Google Workspa
349351
| Parameter | Type | Required | Description |
350352
| --------- | ---- | -------- | ----------- |
351353
| `fileId` | string | Yes | The ID of the file to download |
354+
| `mimeType` | string | No | The MIME type to export Google Workspace files to \(optional\) |
352355
| `fileName` | string | No | Optional filename override |
353356
| `includeRevisions` | boolean | No | Whether to include revision history in the metadata \(default: true, returns first 100 revisions\) |
354357

scripts/generate-docs.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,50 @@ describe('hidden tool params in the Input table', () => {
118118
)
119119
})
120120
})
121+
122+
describe('subBlock param extraction', () => {
123+
const blockSource = (blockFile: string) =>
124+
fs.readFileSync(path.join(import.meta.dirname, '../apps/sim/blocks/blocks', blockFile), 'utf-8')
125+
126+
it('extracts ids from a block whose subBlocks array contains commented-out code', () => {
127+
const ids = extractUserSettableParamIds(blockSource('google_drive.ts'))
128+
129+
expect(ids).toContain('operation')
130+
expect(ids).toContain('mimeType')
131+
expect(ids).toContain('fileName')
132+
expect(ids).toContain('uploadFolderSelector')
133+
})
134+
135+
it('extracts ids past a commented-out subBlock that ends a line on an open bracket', () => {
136+
const ids = extractUserSettableParamIds(blockSource('human_in_the_loop.ts'))
137+
138+
expect(ids).toContain('notification')
139+
expect(ids).toContain('inputFormat')
140+
})
141+
142+
it('returns no ids for blocks whose subBlocks array holds only spreads', () => {
143+
for (const blockFile of [
144+
'imap.ts',
145+
'chat_trigger.ts',
146+
'generic_webhook.ts',
147+
'manual_trigger.ts',
148+
'circleback.ts',
149+
'rss.ts',
150+
'sim_workspace_event.ts',
151+
]) {
152+
expect(extractUserSettableParamIds(blockSource(blockFile))).toEqual([])
153+
}
154+
})
155+
156+
it('throws when the subBlocks array holds literal objects but yields no ids', () => {
157+
expect(() =>
158+
extractUserSettableParamIds(`subBlocks: [\n { title: 'No id here' },\n],`)
159+
).toThrow(/subBlocks/)
160+
})
161+
162+
it('throws when the subBlocks array bracket scan fails', () => {
163+
expect(() => extractUserSettableParamIds(`subBlocks: [\n { id: 'operation' },\n`)).toThrow(
164+
/subBlocks/
165+
)
166+
})
167+
})

scripts/generate-docs.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,17 @@ ${mappingEntries}
648648
}
649649
}
650650

651+
/**
652+
* Raised when a block's `subBlocks` array is present but cannot be read. Distinguishes
653+
* a parse failure from a block that genuinely exposes no fields — both used to surface
654+
* as an empty array, and the empty array silently strips documented rows.
655+
*/
656+
class SubBlockParseError extends Error {
657+
override name = 'SubBlockParseError'
658+
}
659+
660+
const subBlockParseFailures: string[] = []
661+
651662
/**
652663
* Collects the param names a block exposes to the user through its own `subBlocks`.
653664
*
@@ -662,14 +673,18 @@ ${mappingEntries}
662673
* cannot skew it; only depth-1 properties of each subBlock are read, so `id` fields on
663674
* nested `options`/`condition` objects are never mistaken for the subBlock's own id.
664675
*/
665-
export function extractUserSettableParamIds(blockContent: string): string[] {
676+
export function extractUserSettableParamIds(blockContent: string, blockName = 'block'): string[] {
666677
const scannable = blankStringsAndComments(blockContent)
667678
const subBlocksMatch = /subBlocks\s*:\s*\[/.exec(scannable)
668679
if (!subBlocksMatch) return []
669680

670681
const arrayStart = subBlocksMatch.index + subBlocksMatch[0].length - 1
671682
const arrayEnd = findMatchingClose(scannable, arrayStart, '[', ']')
672-
if (arrayEnd === -1) return []
683+
if (arrayEnd === -1) {
684+
throw new SubBlockParseError(
685+
`${blockName}: found a subBlocks array but could not locate its closing bracket`
686+
)
687+
}
673688

674689
const ids = new Set<string>()
675690
let i = arrayStart + 1
@@ -705,6 +720,18 @@ export function extractUserSettableParamIds(blockContent: string): string[] {
705720
i = objectEnd
706721
}
707722

723+
/**
724+
* Zero ids is a legitimate answer only for a block whose array is nothing but
725+
* `...getTrigger(...).subBlocks` spreads. If the array holds an object literal and
726+
* still yields nothing, the scan failed — and the caller's fallback (strip every
727+
* hidden param from the page) is the destructive one, so fail instead of guessing.
728+
*/
729+
if (ids.size === 0 && scannable.slice(arrayStart, arrayEnd).includes('{')) {
730+
throw new SubBlockParseError(
731+
`${blockName}: subBlocks array holds object literals but no id was extracted`
732+
)
733+
}
734+
708735
return [...ids]
709736
}
710737

@@ -882,7 +909,17 @@ function extractAuthType(blockContent: string): 'oauth' | 'api-key' | 'none' {
882909
function blankStringsAndComments(content: string): string {
883910
return content.replace(
884911
/(['"`])(?:\\[\s\S]|(?!\1)[^\\])*\1|\/\/[^\n]*|\/\*[\s\S]*?\*\//g,
885-
(match) => match[0] + match.slice(1, -1).replace(/[^\n]/g, ' ') + match[match.length - 1]
912+
(match: string, quote: string | undefined) => {
913+
const blanked = match.replace(/[^\n]/g, ' ')
914+
/**
915+
* A comment has no delimiters worth preserving, so it is blanked whole. Keeping
916+
* its final character would leak arbitrary source text — commented-out code ending
917+
* in `[` or `{` leaves an unbalanced bracket that derails every scan downstream.
918+
* A quoted string keeps its own quotes so callers can still see where it began.
919+
*/
920+
if (quote === undefined) return blanked
921+
return quote + blanked.slice(1, -1) + quote
922+
}
886923
)
887924
}
888925

@@ -1396,11 +1433,20 @@ function extractBlockConfigFromContent(
13961433

13971434
const operations = extractOperationsFromContent(blockContent)
13981435
const triggerIds = extractTriggersAvailable(blockContent, fileContent)
1436+
let ownSettableParamIds: string[] = []
1437+
try {
1438+
ownSettableParamIds = extractUserSettableParamIds(blockContent, blockName)
1439+
} catch (error) {
1440+
/**
1441+
* Recorded rather than rethrown so one unreadable block cannot drop itself from the
1442+
* docs entirely. `generateAllBlockDocs` fails the run on any recorded failure.
1443+
*/
1444+
if (!(error instanceof SubBlockParseError)) throw error
1445+
subBlockParseFailures.push(error.message)
1446+
console.error(`✗ ${error.message}`)
1447+
}
13991448
const userSettableParamIds = [
1400-
...new Set([
1401-
...extractUserSettableParamIds(blockContent),
1402-
...((baseConfig as any)?.userSettableParamIds ?? []),
1403-
]),
1449+
...new Set([...ownSettableParamIds, ...((baseConfig as any)?.userSettableParamIds ?? [])]),
14041450
]
14051451
const docsLink =
14061452
extractStringPropertyFromContent(blockContent, 'docsLink', true) ||
@@ -4132,6 +4178,15 @@ async function generateAllBlockDocs() {
41324178
// Write the integrations meta after both passes so trigger-only pages are included
41334179
updateMetaJson()
41344180

4181+
if (subBlockParseFailures.length > 0) {
4182+
console.error(
4183+
`Could not read the subBlocks array of ${subBlockParseFailures.length} block(s):\n- ${[
4184+
...new Set(subBlockParseFailures),
4185+
].join('\n- ')}`
4186+
)
4187+
return false
4188+
}
4189+
41354190
return true
41364191
} catch (error) {
41374192
console.error('Error generating documentation:', error)

0 commit comments

Comments
 (0)