(loaded: LoadedRunnableFile)
| 115 | |
| 116 | /** Resolves the init notebook for a `DeepnoteFile`, composing `[init, ...notebooks]` from a sibling `.deepnote` when not self-contained, else failing closed. */ |
| 117 | export async function resolveAndComposeInit(loaded: LoadedRunnableFile): Promise<ResolveAndComposeInitResult> { |
| 118 | const filePath = loaded.originalPath |
| 119 | const initNotebookId = loaded.file.project.initNotebookId |
| 120 | const warnings: string[] = [] |
| 121 | |
| 122 | if (initNotebookId === undefined) { |
| 123 | return { ...loaded, warnings } |
| 124 | } |
| 125 | |
| 126 | const localInit = loaded.file.project.notebooks.find(nb => nb.id === initNotebookId) |
| 127 | if (localInit !== undefined) { |
| 128 | return { ...loaded, warnings } |
| 129 | } |
| 130 | |
| 131 | const directory = dirname(filePath) |
| 132 | const ownBasename = basename(filePath) |
| 133 | |
| 134 | let entries: string[] |
| 135 | try { |
| 136 | entries = await fs.readdir(directory) |
| 137 | } catch (error) { |
| 138 | const message = error instanceof Error ? error.message : String(error) |
| 139 | throw new Error( |
| 140 | `Cannot resolve init notebook for ${filePath}: failed to read directory ${directory}.\n` + |
| 141 | `initNotebookId: ${initNotebookId}\n` + |
| 142 | `Error: ${message}` |
| 143 | ) |
| 144 | } |
| 145 | |
| 146 | const matches: Array<{ path: string; file: DeepnoteFile }> = [] |
| 147 | const rejected: RejectedCandidate[] = [] |
| 148 | |
| 149 | for (const entry of entries) { |
| 150 | if (entry === ownBasename) continue |
| 151 | const lowerEntry = entry.toLowerCase() |
| 152 | // Snapshots are single-notebook `.deepnote` files that share the source's project/notebook |
| 153 | // ids, so they would validate as init candidates — exclude them to avoid spurious ambiguity. |
| 154 | if (lowerEntry.endsWith('.snapshot.deepnote')) continue |
| 155 | if (!lowerEntry.endsWith('.deepnote')) continue |
| 156 | |
| 157 | const candidatePath = resolve(directory, entry) |
| 158 | |
| 159 | let candidate: DeepnoteFile |
| 160 | try { |
| 161 | const rawBytes = await fs.readFile(candidatePath) |
| 162 | const content = decodeUtf8NoBom(rawBytes) |
| 163 | candidate = deserializeDeepnoteFile(content) |
| 164 | } catch (error) { |
| 165 | const message = error instanceof Error ? error.message : String(error) |
| 166 | rejected.push({ path: candidatePath, reason: `failed to parse: ${message}` }) |
| 167 | continue |
| 168 | } |
| 169 | |
| 170 | const validation = isValidSiblingInitCandidate(candidate, loaded.file.project.id, initNotebookId) |
| 171 | if (!validation.valid) { |
| 172 | rejected.push({ path: candidatePath, reason: validation.reason }) |
| 173 | continue |
| 174 | } |
no test coverage detected