Skip to content

Commit d5c95a2

Browse files
docs: Add turboExtendsKeyword future flag and $TURBO_EXTENDS$ microsyntax (#11246)
### Description Documenting `turboExtendsKeyword` future flag and `$TURBO_EXTENDS$` microsyntax. ### Testing Instructions 👀 <sub>CLOSES TURBO-4940</sub> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent d29bb06 commit d5c95a2

File tree

3 files changed

+183
-52
lines changed

3 files changed

+183
-52
lines changed

docs/link-checker/src/markdown.ts

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ const slugger = new GitHubSlugger();
5151
/** Collect the paths of all .mdx files we care about */
5252
const getAllMdxFilePaths = async (): Promise<string[]> => {
5353
const allFiles = await fs.readdir(DOCS_PATH, { recursive: true });
54-
return allFiles.filter((file) => file.endsWith(".mdx"));
54+
return allFiles.filter(
55+
(file) => file.endsWith(".mdx") && !file.startsWith("openapi/")
56+
);
5557
};
5658

5759
// Returns the slugs of all headings in a tree
@@ -146,6 +148,33 @@ const prepareDocumentMapEntry = async (
146148
}
147149
};
148150

151+
/** Checks if the hash exists in a document's headings, accounting for ExperimentalBadge suffixes */
152+
const hashExistsInHeadings = (headings: string[], hash: string): boolean => {
153+
if (headings.includes(hash)) {
154+
return true;
155+
}
156+
157+
// Handle experimental badge suffix: -experimental -> -experimentalbadgeexperimentalexperimentalbadge
158+
const experimentalHeading = hash.replace(
159+
"-experimental",
160+
"-experimentalbadgeexperimentalexperimentalbadge"
161+
);
162+
if (headings.includes(experimentalHeading)) {
163+
return true;
164+
}
165+
166+
// Handle pre-release badge suffix: -pre-release -> -experimentalbadgepre-releaseexperimentalbadge
167+
const preReleaseHeading = hash.replace(
168+
"-pre-release",
169+
"-experimentalbadgepre-releaseexperimentalbadge"
170+
);
171+
if (headings.includes(preReleaseHeading)) {
172+
return true;
173+
}
174+
175+
return false;
176+
};
177+
149178
/** Checks if the links point to existing documents */
150179
const validateInternalLink =
151180
(documentMap: Map<string, Document>) => (doc: Document, href: string) => {
@@ -172,7 +201,7 @@ const validateInternalLink =
172201
});
173202
} else if (hash && !EXCLUDED_HASHES.includes(hash)) {
174203
// Check if the hash link points to an existing section within the document
175-
const hashFound = foundPage.headings.includes(hash);
204+
const hashFound = hashExistsInHeadings(foundPage.headings, hash);
176205

177206
if (!hashFound) {
178207
errors.push({
@@ -197,18 +226,26 @@ const validateHashLink = (doc: Document, href: string) => {
197226
return [];
198227
}
199228

200-
if (
201-
doc.headings.includes(
202-
// Handles when the link has the experimental badge in it.
203-
// Because we're parsing the raw document (not the rendered output), the JSX declaration is still present.
204-
hashLink.replace(
205-
"-experimental",
206-
"-experimentalbadgeexperimentalexperimentalbadge"
207-
)
208-
)
209-
) {
229+
// Handles when the link has the experimental badge in it.
230+
// Because we're parsing the raw document (not the rendered output), the JSX declaration is still present.
231+
const experimentalHeading = hashLink.replace(
232+
"-experimental",
233+
"-experimentalbadgeexperimentalexperimentalbadge"
234+
);
235+
const preReleaseHeading =
236+
hashLink + "-experimentalbadgepre-releaseexperimentalbadge";
237+
238+
if (doc.headings.includes(experimentalHeading)) {
239+
console.warn(
240+
`The hash link "${hashLink}" passed when including the <ExperimentalBadge>Experimental</ExperimentalBadge> JSX declaration.`
241+
);
242+
console.log();
243+
return [];
244+
}
245+
246+
if (doc.headings.includes(preReleaseHeading)) {
210247
console.warn(
211-
`The hash link "${hashLink}" passed when including the <ExperimentalBadge /> JSX declaration.`
248+
`The hash link "${hashLink}" passed when including the <ExperimentalBadge>Pre-release</ExperimentalBadge> JSX declaration.`
212249
);
213250
console.log();
214251
return [];

docs/site/content/docs/reference/configuration.mdx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,30 @@ Turborepo's Environment Modes allow you to control which environment variables a
187187

188188
Read more about [Environment Modes](/docs/crafting-your-repository/using-environment-variables#environment-modes).
189189

190+
### `futureFlags`
191+
192+
```jsonc title="./turbo.json"
193+
{
194+
"futureFlags": {
195+
"turboExtendsKeyword": true
196+
}
197+
}
198+
```
199+
200+
Enable experimental features that will become the default behavior in future versions of Turborepo.
201+
202+
<Callout type="info">
203+
`futureFlags` can only be set in the root `turbo.json`. An error will be
204+
thrown if set in a [Package
205+
Configuration](/docs/reference/package-configurations).
206+
</Callout>
207+
208+
#### `turboExtendsKeyword`
209+
210+
Default: `false`
211+
212+
When enabled, allows using the [`$TURBO_EXTENDS$`](#turbo_extends) microsyntax in array fields. This microsyntax changes the behavior of [Package Configurations](/docs/reference/package-configurations) from replacing array fields to appending to them.
213+
190214
### `tags` <ExperimentalBadge>Experimental</ExperimentalBadge>
191215

192216
```jsonc title="./apps/web/turbo.json"
@@ -476,6 +500,56 @@ Starting a file glob with `$TURBO_ROOT$` will change the glob to be relative to
476500
}
477501
```
478502

503+
#### `$TURBO_EXTENDS$` <ExperimentalBadge>Pre-release</ExperimentalBadge>
504+
505+
<Callout type="info">
506+
This feature requires enabling
507+
[`futureFlags.turboExtendsKeyword`](#turboextendskeyword) in your root
508+
`turbo.json`.
509+
</Callout>
510+
511+
When using [Package Configurations](/docs/reference/package-configurations), array fields completely replace the values from the root `turbo.json` by default. The `$TURBO_EXTENDS$` microsyntax changes this behavior to **append** instead of **replace**.
512+
513+
This microsyntax can be used in the following array fields:
514+
515+
- `dependsOn`
516+
- `env`
517+
- `inputs`
518+
- `outputs`
519+
- `passThroughEnv`
520+
- `with`
521+
522+
For example, if your root `turbo.json` defines:
523+
524+
```jsonc title="./turbo.json"
525+
{
526+
"futureFlags": {
527+
"turboExtendsKeyword": true
528+
},
529+
"tasks": {
530+
"build": {
531+
"outputs": ["dist/**"]
532+
}
533+
}
534+
}
535+
```
536+
537+
A Package Configuration can add additional outputs while keeping the root outputs:
538+
539+
```jsonc title="./apps/web/turbo.json"
540+
{
541+
"extends": ["//"],
542+
"tasks": {
543+
"build": {
544+
// Inherits "dist/**" from root, and adds ".next/**"
545+
"outputs": ["$TURBO_EXTENDS$", ".next/**", "!.next/cache/**"]
546+
}
547+
}
548+
}
549+
```
550+
551+
Without `$TURBO_EXTENDS$`, the `outputs` array would be completely replaced with `[".next/**", "!.next/cache/**"]`, dropping the `"dist/**"` from the root configuration.
552+
479553
### `outputLogs`
480554

481555
Default: `full`

docs/site/content/docs/reference/package-configurations.mdx

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,60 @@ key:
3434
special name used to identify the root directory of the monorepo.
3535
</Callout>
3636

37-
Configuration in a package can override any of [the configurations for a
38-
task](/docs/reference/configuration#defining-tasks). Any keys that are not included are inherited
39-
from the extended `turbo.json`.
37+
## Inheritance behavior
38+
39+
When a Package Configuration extends the root `turbo.json`, task properties are inherited differently depending on their type.
40+
41+
### Scalar fields are inherited
42+
43+
Scalar fields like `outputLogs`, `cache`, `persistent`, and `interactive` are **inherited** from the root configuration. You only need to specify them in a Package Configuration if you want to override them.
44+
45+
For example, if your root `turbo.json` sets `"outputLogs": "hash-only"` for a task, all packages inherit that setting automatically.
46+
47+
### Array fields replace by default
48+
49+
Array fields like `outputs`, `env`, `inputs`, `dependsOn`, and `passThroughEnv` **completely replace** the root configuration's values by default.
50+
51+
```jsonc title="./turbo.json"
52+
{
53+
"tasks": {
54+
"build": {
55+
"outputs": ["dist/**"],
56+
"env": ["NODE_ENV"]
57+
}
58+
}
59+
}
60+
```
61+
62+
```jsonc title="./apps/my-app/turbo.json"
63+
{
64+
"extends": ["//"],
65+
"tasks": {
66+
"build": {
67+
// This REPLACES the root outputs - "dist/**" is NOT included
68+
"outputs": [".next/**"]
69+
}
70+
}
71+
}
72+
```
73+
74+
### Extending arrays with `$TURBO_EXTENDS$` <ExperimentalBadge>Pre-release</ExperimentalBadge>
75+
76+
To **add** to inherited array values instead of replacing them, use the [`$TURBO_EXTENDS$` microsyntax](/docs/reference/configuration#turbo_extends-pre-release):
77+
78+
```jsonc title="./apps/my-app/turbo.json"
79+
{
80+
"extends": ["//"],
81+
"tasks": {
82+
"build": {
83+
// Inherits "dist/**" from root AND adds ".next/**"
84+
"outputs": ["$TURBO_EXTENDS$", ".next/**"]
85+
}
86+
}
87+
}
88+
```
89+
90+
The `$TURBO_EXTENDS$` marker must be the first element in the array. It works with `outputs`, `env`, `inputs`, `dependsOn`, `passThroughEnv`, and `with`.
4091

4192
## Examples
4293

@@ -143,40 +194,9 @@ but continue to inherit any other tasks defined at the root.
143194

144195
## Comparison to package-specific tasks
145196

146-
At first glance, Package Configurations may sound a lot like the
147-
[`package#task` syntax](/docs/crafting-your-repository/configuring-tasks#depending-on-a-specific-task-in-a-specific-package) in the root `turbo.json`. The features are
148-
similar, but have one significant difference: when you declare a package-specific task
149-
in the root `turbo.json`, it _completely_ overwrites the baseline task
150-
configuration. With a Package Configuration, the task configuration is merged
151-
instead.
152-
153-
Consider the example of the monorepo with multiple Next.js apps and a Sveltekit
154-
app again. With a package-specific task, you might configure your root
155-
`turbo.json` like this:
156-
157-
```jsonc title="./turbo.json"
158-
{
159-
"tasks": {
160-
"build": {
161-
"outputLogs": "hash-only",
162-
"inputs": ["src/**"],
163-
"outputs": [".next/**", "!.next/cache/**"]
164-
},
165-
"my-sveltekit-app#build": {
166-
"outputLogs": "hash-only", // must duplicate this
167-
"inputs": ["src/**"], // must duplicate this
168-
"outputs": [".svelte-kit/**"]
169-
}
170-
}
171-
}
172-
```
173-
174-
In this example, `my-sveltekit-app#build` completely overwrites `build` for the
175-
Sveltekit app, so `outputLogs` and `inputs` also need to be duplicated.
197+
The [`package#task` syntax](/docs/crafting-your-repository/configuring-tasks#depending-on-a-specific-task-in-a-specific-package) in the root `turbo.json` **completely overwrites** all task configuration—nothing is inherited.
176198

177-
With Package Configurations, `outputLogs` and `inputs` are inherited, so
178-
you don't need to duplicate them. You only need to override `outputs` in
179-
`my-sveltekit-app` config.
199+
With Package Configurations, scalar fields are inherited and only the fields you specify are overridden. This means less duplication when you only need to change one or two properties.
180200

181201
<Callout type="info">
182202
Although there are no plans to remove package-specific task configurations, we
@@ -218,18 +238,18 @@ Package Configuration for `my-nextjs-app`:
218238
{
219239
"tasks": {
220240
"my-nextjs-app#build": {
221-
// This is not allowed. Even though it's
241+
// This is not allowed. Even though it's
222242
// referencing the correct package, "my-nextjs-app"
223243
// is inferred, and we don't need to specify it again.
224244
// This syntax also has different behavior, so we do not want to allow it.
225245
// (see "Comparison to package-specific tasks" section)
226246
},
227247
"my-sveltekit-app#build": {
228-
// Changing configuration for the "my-sveltekit-app" package
248+
// Changing configuration for the "my-sveltekit-app" package
229249
// from Package Configuration in "my-nextjs-app" is not allowed.
230250
},
231251
"build": {
232-
// ✅ just use the task name!
252+
// Just use the task name!
233253
}
234254
}
235255
}

0 commit comments

Comments
 (0)