From 77bc1e8778e37a3d3a6b2107d807c74531fcc0b4 Mon Sep 17 00:00:00 2001 From: Isaac Mann Date: Fri, 18 Aug 2023 10:59:18 -0400 Subject: [PATCH] feat(nx-dev): highlight lines in code samples (#18690) --- docs/README.md | 22 ++++ docs/generated/manifests/menus.json | 16 +++ docs/generated/manifests/nx.json | 20 +++ docs/generated/manifests/tags.json | 9 ++ docs/map.json | 6 + .../shared/concepts/types-of-configuration.md | 48 +++++++ docs/shared/reference/nx-json.md | 38 +++--- .../shared/reference/project-configuration.md | 18 +-- docs/shared/reference/sitemap.md | 1 + nx-dev/nx-dev/styles/syntax-highlight.css | 8 ++ nx-dev/ui-common/src/lib/selector.tsx | 40 +++--- .../src/lib/nodes/fence.component.tsx | 124 +++++++++++++++--- .../ui-markdoc/src/lib/nodes/fence.schema.tsx | 1 + 13 files changed, 280 insertions(+), 71 deletions(-) create mode 100644 docs/shared/concepts/types-of-configuration.md diff --git a/docs/README.md b/docs/README.md index 2597cf13f0d81..7f7785dff4713 100644 --- a/docs/README.md +++ b/docs/README.md @@ -67,6 +67,28 @@ You can add specific languages and a filename on the code snippet displayed. ‎``` ```` +#### Line Highlighting + +You can define groups of lines that can be interactively highlighted to illustrate a point. + +```` +‎```javascript {% lineGroups={ first:[2,3],second:[4,5] } %} +‎ const code = "goes here"; +‎ This is in the first group +‎ This is also in the first group +‎ This is in the second group +‎ This is also in the second group +‎``` +```` + +The line groups can be highlighted using a button on the code fence itself, or by clicking on a link that you provide that changes the url fragment. + +For example: + +``` +[This will highlight the first group.](#first) +``` + #### Terminal command To display a terminal command, use: diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 92001d0a19108..5a41ef6a0020e 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -988,6 +988,14 @@ "children": [], "disableCollapsible": false }, + { + "name": "Types of Configuration", + "path": "/concepts/types-of-configuration", + "id": "types-of-configuration", + "isExternal": false, + "children": [], + "disableCollapsible": false + }, { "name": "How Caching Works", "path": "/concepts/how-caching-works", @@ -1224,6 +1232,14 @@ "children": [], "disableCollapsible": false }, + { + "name": "Types of Configuration", + "path": "/concepts/types-of-configuration", + "id": "types-of-configuration", + "isExternal": false, + "children": [], + "disableCollapsible": false + }, { "name": "How Caching Works", "path": "/concepts/how-caching-works", diff --git a/docs/generated/manifests/nx.json b/docs/generated/manifests/nx.json index 216b558e22905..da13a79712772 100644 --- a/docs/generated/manifests/nx.json +++ b/docs/generated/manifests/nx.json @@ -1227,6 +1227,16 @@ "path": "/concepts/mental-model", "tags": ["intro"] }, + { + "id": "types-of-configuration", + "name": "Types of Configuration", + "description": "", + "file": "shared/concepts/types-of-configuration", + "itemList": [], + "isExternal": false, + "path": "/concepts/types-of-configuration", + "tags": [""] + }, { "id": "how-caching-works", "name": "How Caching Works", @@ -1528,6 +1538,16 @@ "path": "/concepts/mental-model", "tags": ["intro"] }, + "/concepts/types-of-configuration": { + "id": "types-of-configuration", + "name": "Types of Configuration", + "description": "", + "file": "shared/concepts/types-of-configuration", + "itemList": [], + "isExternal": false, + "path": "/concepts/types-of-configuration", + "tags": [""] + }, "/concepts/how-caching-works": { "id": "how-caching-works", "name": "How Caching Works", diff --git a/docs/generated/manifests/tags.json b/docs/generated/manifests/tags.json index f40665f51bb18..55de7dc348268 100644 --- a/docs/generated/manifests/tags.json +++ b/docs/generated/manifests/tags.json @@ -632,6 +632,15 @@ "path": "/recipes/tips-n-tricks/standalone-to-integrated" } ], + "": [ + { + "description": "", + "file": "shared/concepts/types-of-configuration", + "id": "types-of-configuration", + "name": "Types of Configuration", + "path": "/concepts/types-of-configuration" + } + ], "create-your-own-plugin": [ { "description": "", diff --git a/docs/map.json b/docs/map.json index de94302ea9107..eb9bf91925140 100644 --- a/docs/map.json +++ b/docs/map.json @@ -363,6 +363,12 @@ "id": "mental-model", "file": "shared/mental-model" }, + { + "name": "Types of Configuration", + "tags": [""], + "id": "types-of-configuration", + "file": "shared/concepts/types-of-configuration" + }, { "name": "How Caching Works", "tags": ["cache-task-results"], diff --git a/docs/shared/concepts/types-of-configuration.md b/docs/shared/concepts/types-of-configuration.md new file mode 100644 index 0000000000000..2c2081b858655 --- /dev/null +++ b/docs/shared/concepts/types-of-configuration.md @@ -0,0 +1,48 @@ +# Managing Configuration Files + +Besides providing caching and task orchestration, Nx also helps incorporate numerous tools and frameworks into your repo. With all these pieces of software commingling, you can end up with a lot of configuration files. Nx plugins help to abstract away some of the difficulties of managing this configuration, but the configuration is all still accessible, in case there is a particular setting that you need to change. + +## Different Kinds of Configuration + +When discussing configuration, it helps to categorize the configuration settings in two dimensions: + +- **Type** - The two different types we'll discuss are Nx configuration and tooling configuration. Tooling can be any framework or tool that you use in your repo (i.e. React, Jest, Playwright, Typescript, etc) +- **Specificity** - There are two different levels of specificity: global and project-specific. Project-specific configuration is merged into and overwrites global configuration. + +For example, Jest has a global `/jest.config.ts` file and a project-specific `/apps/my-app/jest.config.ts` file that extends it. + +| | Nx | Tooling | +| ---------------- | --------------------------- | ----------------------------- | +| Global | `/nx.json` | `/jest.config.ts` | +| Project-specific | `/apps/my-app/project.json` | `/apps/my-app/jest.config.ts` | + +## How Does Nx Help Manage Tooling Configuration? + +In a repository with many different projects and many different tools, there will be a lot of tooling configuration. Nx helps reduce the complexity of managing that configuration in two ways: + +1. Abstracting away common tooling configuration settings so that if your project is using the tool in the most common way, you won't need to worry about configuration at all. The default settings for any Nx plugin executor are intended to work without modification for most projects in the community. +2. Allowing you to provide `targetDefaults` so that the most common settings for projects in your repo can all be defined in one place. Then, only projects that are exceptions need to overwrite those settings. With the judicious application of this method, larger repositories can actually have less lines of configuration after adding Nx than before. + +## Determining the Value of a Configuration Property + +If you need to track down the value of a specific configuration property (say `runInBand` for `jest` on the `/apps/my-app` project) you need to look in the following locations. The configuration settings are merged with priority being given to the file higher up in the list. + +1. In `/apps/my-app/project.json`, the `options` listed under the `test` target that uses the `@nx/jest:jest` executor. +2. In `/nx.json`, the `targetDefaults` listed for the `test` target. +3. One of the `test` target options references `/apps/my-app/jest.config.ts` +4. Which extends `/jest.config.ts` + +```text +repo/ +├── apps/ +│ └── my-app/ +│ ├── jest.config.ts +│ └── project.json +├── jest.config.ts +└── nx.json +``` + +## More Information + +- [Nx Configuration](/reference/nx-json) +- [Project Configuration](/reference/project-configuration) diff --git a/docs/shared/reference/nx-json.md b/docs/shared/reference/nx-json.md index b3970ab57248a..06946094e8ca6 100644 --- a/docs/shared/reference/nx-json.md +++ b/docs/shared/reference/nx-json.md @@ -1,10 +1,10 @@ # nx.json -The `nx.json` file configures the Nx CLI and project defaults. +The `nx.json` file configures the Nx CLI and project defaults. The full [machine readable schema](https://github.com/nrwl/nx/blob/master/packages/nx/schemas/nx-schema.json) is available on Github. -The following is an expanded version showing all options. Your `nx.json` will likely be much shorter. +The following is an expanded example showing all options. Your `nx.json` will likely be much shorter. For a more intuitive understanding of the roles of each option, you can highlight the options in the excerpt below that relate to different categories. -```json {% fileName="nx.json" %} +```json {% fileName="nx.json" lineGroups={ Caching:[15,16,17,18,19,20,21,22,23,24,25,26,29], Orchestration:[3,4,5,28,30], Execution:[28,31,32,33,34] } %} { "extends": "nx/presets/npm.json", "affected": { @@ -14,24 +14,6 @@ The following is an expanded version showing all options. Your `nx.json` will li "appsDir": "demos", "libsDir": "packages" }, - "implicitDependencies": { - "package.json": { - "dependencies": "*", - "devDependencies": "*" - }, - "tsconfig.base.json": "*", - "nx.json": "*" - }, - "namedInputs": { - "default": ["{projectRoot}/**/*"], - "production": ["!{projectRoot}/**/*.spec.tsx"] - }, - "targetDefaults": { - "build": { - "inputs": ["production", "^production"], - "dependsOn": ["^build"] - } - }, "generators": { "@nx/js:library": { "buildable": true @@ -44,6 +26,20 @@ The following is an expanded version showing all options. Your `nx.json` will li "cacheableOperations": ["build", "lint", "test", "e2e"] } } + }, + "namedInputs": { + "default": ["{projectRoot}/**/*"], + "production": ["!{projectRoot}/**/*.spec.tsx"] + }, + "targetDefaults": { + "build": { + "inputs": ["production", "^production"], + "dependsOn": ["^build"], + "executor": "@nrwl/js:tsc", + "options": { + "main": "{projectRoot}/src/index.ts" + } + } } } ``` diff --git a/docs/shared/reference/project-configuration.md b/docs/shared/reference/project-configuration.md index fcf796b68d2cf..e36c378e22902 100644 --- a/docs/shared/reference/project-configuration.md +++ b/docs/shared/reference/project-configuration.md @@ -2,7 +2,7 @@ Projects can be configured in `package.json` (if you use npm scripts and not Nx executors) and `project.json` (if you [use task executors](/core-features/plugin-features/use-task-executors)). Both `package.json` and `project.json` files are located in each project's folder. Nx merges the two -files to get each project's configuration. +files to get each project's configuration. The full [machine readable schema](https://github.com/nrwl/nx/blob/master/packages/nx/schemas/project-schema.json) is available on Github. The following configuration creates `build` and `test` targets for Nx. @@ -47,12 +47,12 @@ The following configuration creates `build` and `test` targets for Nx. You can invoke `nx build mylib` or `nx test mylib` without any extra configuration. -You can add Nx-specific configuration as follows: +Below are some more complete examples of project configuration files. For a more intuitive understanding of the roles of each option, you can highlight the options in the excerpt below that relate to different categories. {% tabs %} {% tab label="package.json" %} -```jsonc {% fileName="package.json" %} +```jsonc {% fileName="package.json" lineGroups={ Orchestration:[14,17,19,22,25],Execution:[4,5,6],Caching:[9,10,11,12,15,16,20,21] } %} { "name": "mylib", "scripts": { @@ -85,33 +85,33 @@ You can add Nx-specific configuration as follows: {% /tab %} {% tab label="project.json" %} -```json {% fileName="project.json" %} +```json {% fileName="project.json" lineGroups={ "Orchestration": [5,6,12,15,19,22], "Execution": [12,16,17,19,22,23], "Caching": [7,8,9,10,13,14,20,21] } %} { "root": "libs/mylib/", "sourceRoot": "libs/mylib/src", "projectType": "library", + "tags": ["scope:myteam"], + "implicitDependencies": ["anotherlib"], "namedInputs": { "default": ["{projectRoot}/**/*"], "production": ["!{projectRoot}/**/*.spec.tsx"] }, "targets": { "test": { - "executor": "@nx/jest:jest", "inputs": ["default", "^production"], "outputs": [], "dependsOn": ["build"], + "executor": "@nx/jest:jest", "options": {} }, "build": { - "executor": "@nx/js:tsc", "inputs": ["production", "^production"], "outputs": ["{workspaceRoot}/dist/libs/mylib"], "dependsOn": ["^build"], + "executor": "@nx/js:tsc", "options": {} } - }, - "tags": ["scope:myteam"], - "implicitDependencies": ["anotherlib"] + } } ``` diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 9d489f288866c..315723ad2e542 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -63,6 +63,7 @@ - [Concepts](/concepts) - [Integrated Repos vs. Package-Based Repos vs. Standalone Apps](/concepts/integrated-vs-package-based) - [Mental Model](/concepts/mental-model) + - [Types of Configuration](/concepts/types-of-configuration) - [How Caching Works](/concepts/how-caching-works) - [Improve Worst Case CI Times](/concepts/dte) - [Task Pipeline Configuration](/concepts/task-pipeline-configuration) diff --git a/nx-dev/nx-dev/styles/syntax-highlight.css b/nx-dev/nx-dev/styles/syntax-highlight.css index ccbc0eeacd44d..a69e9c62b2c65 100644 --- a/nx-dev/nx-dev/styles/syntax-highlight.css +++ b/nx-dev/nx-dev/styles/syntax-highlight.css @@ -69,3 +69,11 @@ html { .hljs .hljs-strong { font-weight: 700; } + +.linenumber { + border-color: rgb(59 130 246); + background: #e5e7eb; +} +html.dark .linenumber { + background: #e5e7eb33; +} diff --git a/nx-dev/ui-common/src/lib/selector.tsx b/nx-dev/ui-common/src/lib/selector.tsx index 2868d98bf9503..4f9a4be3b964e 100644 --- a/nx-dev/ui-common/src/lib/selector.tsx +++ b/nx-dev/ui-common/src/lib/selector.tsx @@ -3,6 +3,8 @@ import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/solid'; import { Fragment } from 'react'; export interface SelectorProps { + children: JSX.Element; + className?: string; items: { label: string; value: string; data?: T }[]; selected: { label: string; value: string }; onChange: (item: { label: string; value: string; data?: T }) => void; @@ -17,9 +19,19 @@ export function Selector(props: SelectorProps): JSX.Element { > {({ open }) => ( <> -
- - {props.selected.label} +
+ + + + {props.children} + + {props.selected.label} + (props: SelectorProps): JSX.Element { > {props.items.map((item, personIdx) => ( - `${ - active - ? 'bg-blue-nx-base text-white' - : 'text-slate-500' - } - relative cursor-pointer select-none py-2 pl-10 pr-4` + `relative list-none cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 select-none py-2 px-3` } value={item} > @@ -59,19 +66,6 @@ export function Selector(props: SelectorProps): JSX.Element { {item.label} - {selected || item.value === props.selected.value ? ( - - - ) : null} )} diff --git a/nx-dev/ui-markdoc/src/lib/nodes/fence.component.tsx b/nx-dev/ui-markdoc/src/lib/nodes/fence.component.tsx index c2c65e560afa6..6b23c95eb2767 100644 --- a/nx-dev/ui-markdoc/src/lib/nodes/fence.component.tsx +++ b/nx-dev/ui-markdoc/src/lib/nodes/fence.component.tsx @@ -2,6 +2,7 @@ import { ClipboardDocumentCheckIcon, ClipboardDocumentIcon, InformationCircleIcon, + SparklesIcon, } from '@heroicons/react/24/outline'; import React, { ReactNode, useEffect, useState } from 'react'; // @ts-ignore @@ -10,6 +11,8 @@ import { CopyToClipboard } from 'react-copy-to-clipboard'; import SyntaxHighlighter from 'react-syntax-highlighter'; import { CodeOutput } from './fences/code-output.component'; import { TerminalOutput } from './fences/terminal-output.component'; +import { useRouter } from 'next/router'; +import { Selector } from '@nx/nx-dev/ui-common'; function resolveLanguage(lang: string) { switch (lang) { @@ -54,11 +57,38 @@ function CodeWrapper(options: { ); } +const useUrlHash = (initialValue: string) => { + const router = useRouter(); + const [hash, setHash] = useState(initialValue); + + const updateHash = (str: string) => { + if (!str) return; + setHash(str.split('#')[1]); + }; + + useEffect(() => { + const onWindowHashChange = () => updateHash(window.location.hash); + const onNextJSHashChange = (url: string) => updateHash(url); + + router.events.on('hashChangeStart', onNextJSHashChange); + window.addEventListener('hashchange', onWindowHashChange); + window.addEventListener('load', onWindowHashChange); + return () => { + router.events.off('hashChangeStart', onNextJSHashChange); + window.removeEventListener('load', onWindowHashChange); + window.removeEventListener('hashchange', onWindowHashChange); + }; + }, [router.asPath, router.events]); + + return hash; +}; + export function Fence({ children, command, path, fileName, + lineGroups, language, enableCopy, }: { @@ -66,9 +96,45 @@ export function Fence({ command: string; path: string; fileName: string; + lineGroups: Record; language: string; enableCopy: boolean; }) { + const { push, asPath } = useRouter(); + const hash = decodeURIComponent(useUrlHash('')); + + function lineNumberStyle(lineNumber: number) { + if (lineGroups[hash] && lineGroups[hash].includes(lineNumber)) { + return { + fontSize: 0, + display: 'inline-block', + position: 'absolute', + left: 0, + right: 0, + zIndex: -1, + borderLeftStyle: 'solid', + borderLeftWidth: 10, + lineHeight: '21px', + }; + } + return { + fontSize: 0, + position: 'absolute', + }; + } + const highlightOptions = Object.keys(lineGroups).map((lineNumberKey) => ({ + label: lineNumberKey, + value: lineNumberKey, + })); + if (highlightOptions.length > 0) { + highlightOptions.unshift({ + label: 'No highlighting', + value: '', + }); + } + let selectedOption = + highlightOptions.find((option) => option.value === hash) || + highlightOptions[0]; const [copied, setCopied] = useState(false); useEffect(() => { let t: NodeJS.Timeout; @@ -83,31 +149,53 @@ export function Fence({ }, [copied]); const showRescopeMessage = children.includes('@nx/') || command.includes('@nx/'); + function highlightChange(item: { label: string; value: string }) { + push(asPath.split('#')[0] + '#' + item.value); + } return (
- {enableCopy && enableCopy === true && ( - { - setCopied(true); - }} - > - - - )} + + + )} + {highlightOptions && highlightOptions[0] && ( + + + + )} +