devcontainers / devcontainers/cli

Feature request: First-class array input for Feature options (array option type + multiple invocations) [blocked on spec#766]

Đang mở
#1,298 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
TypeScript
Star
3k
Fork
457
Merge trung bình
13 giờ 17 phút
Pull request đã merge (30 ngày)
6

Mô tả

# Feature request: First-class array input for Feature options (array option type + multiple invocations)

> **Complex feature request — depends on the spec change in
> [devcontainers/spec#766](https://github.com/devcontainers/spec/issues/766) (RFC).**
> This issue tracks the **reference CLI implementation** work once the spec RFC is accepted.
> Related long-standing issues: [spec#57](https://github.com/devcontainers/spec/issues/57)
> (array option type, open since 2022) and [spec#44](https://github.com/devcontainers/spec/issues/44)
> (install a feature more than once).

---

## Problem

The dev container spec restricts Feature option values to `boolean` and `string`. There is **no
array type**. As a result, every Feature that needs a list (packages, extensions, tools, versions)
is forced to accept a **comma-separated string** and split it inside `install.sh`.

This CLI encodes that limitation directly in its TypeScript types, which is the root cause that
blocks any progress:

```ts
// src/spec-configuration/containerFeaturesConfiguration.ts
export type FeatureOption = {
type: 'boolean';
default?: boolean;
description?: string;
} | {
type: 'string';
enum?: string[];
default?: string;
description?: string;
} | {
type: 'string';
proposals?: string[];
default?: string;
description?: string;
};
```

```ts
// src/spec-configuration/configuration.ts
export interface DevContainerFeature {
userFeatureId: string;
options: boolean | string | Record;
}
// ...
features?: Record>;
```

`FeatureOption` has no `'array'` variant. `DevContainerFeature.options` and the `features` map
only permit `boolean | string` for option values — **arrays are not representable**, so they can
never reach `install.sh` no matter what a user writes in `devcontainer.json`.

### Real-world impact (shipped Features, today)

| Feature | Option | Today | Symptom |
|---|---|---|---|
| `ghcr.io/rocker-org/devcontainer-features/r-packages:1` | `packages` | `"cli,rlang"` | Comma-joined; values containing commas are unrepresentable. |
| `ghcr.io/devcontainers/features/github-cli` | extensions | `"github/gh-copilot"` | Comma-joined; extension refs/args with commas break. |
| `mwmahlberg/devcontainer-features` `npm-packages` | `packages` | `"typescript,eslint"` | Accepts comma **or** whitespace **or** newline — three delimiters, because the spec gives no canonical list form. |

The `npm-packages` case is the clearest failure signal: with no array type, **every Feature author
invents a different delimiter**. Consumers cannot reason about a list option without reading each
Feature's `install.sh`.

This was always meant to be temporary. From [spec#57](https://github.com/devcontainers/spec/issues/57),
maintainer @Chuxel (2022):

> Right now things in devcontainers/features are using a comma separated string as a **near term
> workaround**. Converting this into an array is pretty easy…

That workaround has now been the de facto standard for **3+ years**.

---

## Requirements (hard — no alternatives)

This is a **required** capability, not a nice-to-have. Comma-separated strings are not an acceptable
long-term substitute (they are ambiguous, lossy, and force per-Feature delimiter conventions). The
CLI must implement:

1. **`array` option type** — `devcontainer-feature.json` may declare `"type": "array"` for an
option, with `default` as an array and `proposals`/`enum` constraining **elements**.
2. **Array option values in `devcontainer.json`** — `"packages": ["curl","git","jq"]` must be
accepted, validated, and propagated.
3. **Multiple invocations via array of option objects** — a Feature value may be an array of option
objects (`"dotnet": [{"version":"3.1"},{"version":"6.0"}]`), invoking `install.sh` once per
element in order (resolves spec#44).
4. **Option Resolution for arrays** — array values are serialized to `devcontainer-features.env` as
a **JSON array string** (`PACKAGES='["curl","git","jq"]'`), the only delimiter-free, unambiguous
encoding.
5. **Backward-compatible string→array coercion** — if a string is supplied for an `array`-typed
option: parse as JSON if it looks like a JSON array, else split on commas. Existing
comma-separated Features keep working when they migrate to `type: "array"`.
6. **CLI flag support** — accept JSON array values in `--override-features`, and support repeated
flags (`--feature-option . `) that append to an array option.

---

## Proposed implementation

### 1. Type changes

**`src/spec-configuration/containerFeaturesConfiguration.ts`** — extend `FeatureOption`:

```ts
export type FeatureOption = {
type: 'boolean';
default?: boolean;
description?: string;
} | {
type: 'string';
enum?: string[];
default?: string;
description?: string;
} | {
type: 'string';
proposals?: string[];
default?: string;
description?: string;
} | {
type: 'array'; // NEW
enum?: string[]; // constrains elements
proposals?: string[]; // suggests elements
default?: (string | boolean | number)[];// array default
description?: string;
};
```

**`src/spec-configuration/configuration.ts`** — widen option value and Feature value types:

```ts
export interface DevContainerFeature {
userFeatureId: string;
// allow arrays of primitives as option values, and array-of-option-objects as the Feature value
options: boolean | string | (string | boolean | number)[] | Record | Record[];
}
// ...
features?: Record | Record[]>;
```

### 2. Option parsing & normalization

Where feature option values are read and normalized (the `getFeatureValueDefaults` /
option-resolution path in `containerFeaturesConfiguration.ts`):

- When `option.type === 'array'`:
- Accept a JSON array value as-is.
- If the supplied value is a string, attempt `JSON.parse`; if it yields an array, use it;
otherwise split on `,` and trim each element. Emit a warning recommending the array form.
- Validate each element against `enum`/`proposals` when present.
- Default to `[]` when omitted and no `default`.

### 3. Option Resolution (env serialization)

In the code that writes `devcontainer-features.env` (`=`):

- For an `array` option, serialize the value with `JSON.stringify(value)` so the env var holds the
canonical JSON array string. Example output:
```env
PACKAGES='["curl","git","jq"]'
```
- This keeps a single, unambiguous source of truth. `install.sh` parses with `jq` (already
ubiquitous in dev container images):
```bash
for pkg in $(printf '%s' "$PACKAGES" | jq -r '.[]'); do apt-get install -y "$pkg"; done
```

### 4. Multiple invocations (array of option objects)

In the feature install layer (`getFeatureLayers` / `getFeatureInstallWrapperScript` and the
surrounding orchestration):

- When a Feature's value is an array of option objects, emit one install layer/wrapper per element,
in array order, each with its own `devcontainer-features.env`. All invocations of a given Feature
run consecutively at that Feature's position in the install order (do not interleave with other
Features).

### 5. CLI flags

- `--override-features`: already accepts a JSON blob; ensure array option values and array-of-objects
Feature values parse and flow through the new types.
- Add/confirm a repeated-flag form: `--feature-option . ` appends to an
array-typed option (and sets/replaces for scalar options).

### 6. Validation & errors

- Reject non-array values for `array`-typed options (after string coercion) with a clear error
naming the Feature and option.
- Validate `enum` elements; report the offending element.

---

## Test plan

- [ ] `devcontainer-feature.json` with `type: "array"` parses and surfaces `default`/`enum`/`proposals`.
- [ ] `devcontainer.json` with `"packages": ["curl","git"]` reaches `install.sh` as
`PACKAGES='["curl","git"]'`.
- [ ] String `"curl,git"` for an `array` option coerces to `["curl","git"]` with a warning.
- [ ] String `'["curl","git"]'` (JSON) coerces to the array without warning.
- [ ] `enum`-constrained array option rejects an out-of-set element.
- [ ] Array-of-option-objects Feature value invokes `install.sh` twice with distinct env vars, in
order.
- [ ] Repeated `--feature-option` flags append to an array option.
- [ ] Existing comma-separated Features (e.g. `r-packages`) continue to work unchanged after
migrating their option to `type: "array"`.
- [ ] JSON Schema (`devContainerFeature.schema.json`) validates the new shapes.

---

## Use cases

1. **Package lists** — `r-packages`, `npm-packages`, `github-cli` extensions, Homebrew formulae:
explicit arrays, values may contain commas.
2. **Multiple runtime versions** — install `.NET 3.1` and `6.0` (or Node `18` + `20`) in one image
via array-of-option-objects, without bespoke per-Feature "multi-version" options.
3. **Multi-select tool bundles** — a Feature offering a curated subset of tools; with `enum`
elements the UX can render a multi-select picker.
4. **Tool-readable config** — schema validation, IntelliSense, and diffs work natively on JSON
arrays; comma-strings are opaque to every tool except the splitting `install.sh`.

---

## Prior art (alternative stacks)

The dev container spec is the **only** major dev-environment format lacking a native list type for
user-supplied option values:

| Stack | List input | Notes |
|---|---|---|
| **Coder** | `coder_parameter` `type = "list(string)"`; UI `multi-select`/`tag-select`; defaults via `jsonencode([...])` | First-class list type. Coder's docs warn that overriding `list(string)` on the CLI is *"tricky"* (CSV+JSON quoting) and offer a YAML workaround — exactly the ambiguity this CLI should avoid by defining array semantics up front. |
| **Nix** (`mkShell`) | Native lists `packages = [ curl git jq ];` | First-class; no string parsing. |
| **Gitpod** (`.gitpod.yml`) | Native YAML arrays (`tasks`, `ports`, `vscode.extensions`) | First-class. |
| **Docker Compose** | Native YAML arrays (`volumes`, `ports`, `environment`) | First-class. |
| **Helm** | Native YAML arrays in `values.yaml`, iterated with `range` | First-class. |
| **Terraform** | `list(string)`, `list(any)` native variable types | First-class. |
| **Dev Containers (this CLI)** | ❌ No array option type — comma-separated `string` only | **Outlier.** |

---

## Dependencies & unblocking

- **Blocked on spec acceptance**: [devcontainers/spec#766](https://github.com/devcontainers/spec/issues/766)
(the RFC defining `array` option type, Option Resolution for arrays, and array-of-option-objects).
- **Closes the long-standing workaround**: [spec#57](https://github.com/devcontainers/spec/issues/57)
(2022) and [spec#44](https://github.com/devcontainers/spec/issues/44).

Once the spec RFC lands, this issue is the implementation tracker for the reference CLI. The type
changes in §1 are the minimal unblocking step; everything else follows from the spec's normative
requirements.

---

## References

- Spec RFC: https://github.com/devcontainers/spec/issues/766
- Original array-option request: https://github.com/devcontainers/spec/issues/57
- Multiple invocations: https://github.com/devcontainers/spec/issues/44
- Spec option resolution: https://containers.dev/implementors/features/#option-resolution
- Coder `list(string)` parameters: https://coder.com/docs/admin/templates/extending-templates/parameters

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Hướng nghiên cứu

Start with the spec#766 RFC, since this implementation is explicitly blocked on its acceptance. Then read src/spec-configuration/containerFeaturesConfiguration.ts and configuration.ts, followed by getFeatureLayers and getFeatureInstallWrapperScript; inspect devContainerFeature.schema.json for schema changes. Done means array options, coercion, serialization, repeated CLI flags, multiple invocations, validation, and the listed test-plan cases all work.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
typescript
Lĩnh vực
cli, devtools
Loại issue
Tính năng
Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức độ hoạt động
Sôi nổi
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
35/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.