( raw: string | undefined, projectDir: string, stat: (path: string) => Stats, )
| 140 | * without touching the filesystem. |
| 141 | */ |
| 142 | export function parseCompositionEntryArg( |
| 143 | raw: string | undefined, |
| 144 | projectDir: string, |
| 145 | stat: (path: string) => Stats, |
| 146 | ): CompositionEntryParseResult { |
| 147 | const trimmed = raw?.trim().replace(/^\.\//, "") || undefined; |
| 148 | // Normalize the project-root shorthands to "no entry override" so the |
| 149 | // producer falls back to index.html instead of statSync-ing the dir |
| 150 | // and later blowing up with EISDIR inside readFileSync(). |
| 151 | if (!trimmed || trimmed === ".") return { ok: true, value: undefined }; |
| 152 | |
| 153 | const absProjectDir = resolve(projectDir); |
| 154 | const entryPath = resolve(absProjectDir, trimmed); |
| 155 | // Trailing-separator guard: `startsWith` alone treats `/proj` as a |
| 156 | // prefix of `/proj-evil`, letting a sibling-directory escape through. |
| 157 | // Allow the resolved path to BE the project dir (already covered by |
| 158 | // the trimmed === "." branch above) or to live beneath it with a |
| 159 | // path separator. |
| 160 | if (entryPath !== absProjectDir && !entryPath.startsWith(absProjectDir + sep)) { |
| 161 | return { ok: false, error: { kind: "outside-project", entryFile: trimmed } }; |
| 162 | } |
| 163 | |
| 164 | let entryStat: Stats; |
| 165 | try { |
| 166 | entryStat = stat(entryPath); |
| 167 | } catch { |
| 168 | return { ok: false, error: { kind: "not-found", entryFile: trimmed } }; |
| 169 | } |
| 170 | if (!entryStat.isFile()) { |
| 171 | // Directory paths slip past existsSync downstream and explode with |
| 172 | // `EISDIR: illegal operation on a directory, read` inside the |
| 173 | // producer's readFileSync. Reject here with an actionable message. |
| 174 | return { ok: false, error: { kind: "not-a-file", entryFile: trimmed } }; |
| 175 | } |
| 176 | return { ok: true, value: trimmed }; |
| 177 | } |
| 178 | |
| 179 | function compositionEntryErrorMessage(error: CompositionEntryParseError): { |
| 180 | title: string; |
no test coverage detected