( params: CommonParams, processFeature: (userFeature: DevContainerFeature) => Promise<FeatureSet | undefined>, worklist: FNode[], acc: FNode[], lockfile: Lockfile | undefined)
| 343 | } |
| 344 | |
| 345 | async function _buildDependencyGraph( |
| 346 | params: CommonParams, |
| 347 | processFeature: (userFeature: DevContainerFeature) => Promise<FeatureSet | undefined>, |
| 348 | worklist: FNode[], |
| 349 | acc: FNode[], |
| 350 | lockfile: Lockfile | undefined): Promise<DependencyGraph> { |
| 351 | const { output } = params; |
| 352 | |
| 353 | while (worklist.length > 0) { |
| 354 | const current = worklist.shift()!; |
| 355 | |
| 356 | output.write(`Resolving Feature dependencies for '${current.userFeatureId}'...`, LogLevel.Info); |
| 357 | |
| 358 | const processedFeature = await processFeature(current); |
| 359 | if (!processedFeature) { |
| 360 | throw new Error(`ERR: Feature '${current.userFeatureId}' could not be processed. You may not have permission to access this Feature, or may not be logged in. If the issue persists, report this to the Feature author.`); |
| 361 | } |
| 362 | |
| 363 | // Set the processed FeatureSet object onto Node. |
| 364 | current.featureSet = processedFeature; |
| 365 | |
| 366 | // If the current Feature is already in the accumulator, skip it. |
| 367 | // This stops cycles but doesn't report them. |
| 368 | // Cycles/inconsistencies are thrown as errors in the next stage (rounds). |
| 369 | if (acc.some(f => equals(params, f, current))) { |
| 370 | continue; |
| 371 | } |
| 372 | |
| 373 | const type = processedFeature.sourceInformation.type; |
| 374 | let metadata: Feature | undefined; |
| 375 | // Switch on the source type of the provided Feature. |
| 376 | // Retrieving the metadata for the Feature (the contents of 'devcontainer-feature.json') |
| 377 | switch (type) { |
| 378 | case 'oci': |
| 379 | metadata = await getOCIFeatureMetadata(params, current); |
| 380 | break; |
| 381 | |
| 382 | case 'file-path': |
| 383 | const filePath = (current.featureSet.sourceInformation as FilePathSourceInformation).resolvedFilePath; |
| 384 | const metadataFilePath = path.join(filePath, DEVCONTAINER_FEATURE_FILE_NAME); |
| 385 | if (!isLocalFile(filePath)) { |
| 386 | throw new Error(`Metadata file '${metadataFilePath}' cannot be read for Feature '${current.userFeatureId}'.`); |
| 387 | } |
| 388 | const serialized = (await readLocalFile(metadataFilePath)).toString(); |
| 389 | if (serialized) { |
| 390 | metadata = jsonc.parse(serialized) as Feature; |
| 391 | } |
| 392 | break; |
| 393 | |
| 394 | case 'direct-tarball': |
| 395 | const tarballUri = (processedFeature.sourceInformation as DirectTarballSourceInformation).tarballUri; |
| 396 | const expectedDigest = lockfile?.features[tarballUri]?.integrity; |
| 397 | metadata = await getTgzFeatureMetadata(params, current, expectedDigest); |
| 398 | break; |
| 399 | |
| 400 | default: |
| 401 | // Legacy |
| 402 | // No dependency metadata to retrieve. |
no test coverage detected