grails-cli 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +250 -0
- package/README.md +425 -2
- package/dist/cli.js +31181 -0
- package/package.json +45 -4
- package/templates/grails-cli-sync.yml +56 -0
- package/templates/skill/grails-cli/SKILL.md +337 -0
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grails-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Grails Code Sync from the command line: reads a component library's code and uploads its design tokens and component specs to Grails.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"homepage": "https://docs.grails.design/cli/overview/",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"grails",
|
|
9
|
+
"design-system",
|
|
10
|
+
"design-tokens",
|
|
11
|
+
"dtcg",
|
|
12
|
+
"components",
|
|
13
|
+
"figma",
|
|
14
|
+
"react",
|
|
15
|
+
"tailwind",
|
|
16
|
+
"cli",
|
|
17
|
+
"code-sync"
|
|
18
|
+
],
|
|
19
|
+
"packageManager": "pnpm@10.32.1",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"grails-cli": "./dist/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"templates"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"registry": "https://registry.npmjs.org/"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "node scripts/build.mjs",
|
|
37
|
+
"smoke": "node scripts/smoke.mjs",
|
|
38
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"typescript": "5.8.3"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.0.0",
|
|
45
|
+
"esbuild": "^0.27.7"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Grails Code Sync: on every push to the branch your team syncs from (a merge into it), read this repository's
|
|
2
|
+
# design tokens and components and upload them to Grails, so the vault shows what the code actually holds.
|
|
3
|
+
#
|
|
4
|
+
# SETUP
|
|
5
|
+
# 1. In Grails, create an API key with the write:code scope: Team Settings > Integrations > API Keys.
|
|
6
|
+
# 2. Save it as a repository secret named GRAILS_API_KEY: Settings > Secrets and variables > Actions.
|
|
7
|
+
# 3. Say in grails.code.json which branch syncs ("branch": "main", or "preview", …): `npx grails-cli init`
|
|
8
|
+
# proposes it. Put the SAME branch under `branches:` below.
|
|
9
|
+
# 4. Commit this file as .github/workflows/grails-cli-sync.yml.
|
|
10
|
+
#
|
|
11
|
+
# THE KEY stays in the step's `env:`. It is never written into a `run:` line, never echoed, and never passed as
|
|
12
|
+
# an argument: the CLI reads it from the environment and refuses it as a flag. Do not change that, and do not run
|
|
13
|
+
# this on `pull_request`, where a fork's run has no secrets and no business syncing.
|
|
14
|
+
#
|
|
15
|
+
# WHAT LEAVES THE REPOSITORY: design tokens (names, types, values), component names, file paths, and each
|
|
16
|
+
# component's spec (props, anatomy names, resolved style values). Never source code. `npx grails-cli tokens
|
|
17
|
+
# --json` prints the exact token list. `sync --dry-run` stores nothing: with the key it asks Grails what would
|
|
18
|
+
# change, without it it counts locally.
|
|
19
|
+
#
|
|
20
|
+
# EXIT CODES: 0 done · 1 the sync ended incomplete · 2 misconfigured (grails.code.json declares no branch, too) · 3
|
|
21
|
+
# refused by the server (a CLI older than the minimum, too) · 4 the server could not be reached or failed. Only 4 is
|
|
22
|
+
# worth retrying.
|
|
23
|
+
#
|
|
24
|
+
# THE BRANCH: the CLI uploads only from the branch grails.code.json declares. Run anywhere else it uploads nothing,
|
|
25
|
+
# exits 0 so the job stays green, and says so: a notice on the run, the job summary, and the step output
|
|
26
|
+
# `steps.grails.outputs.result` = `skipped-branch` (otherwise `synced`, `preview`, `offline` or `failed`).
|
|
27
|
+
#
|
|
28
|
+
# THE VERSION: `npx --yes grails-cli` fetches the latest release from npm on every run. To choose when a new release
|
|
29
|
+
# reaches your pushes, pin a version (`grails-cli@<version>`) so a new release cannot change a push you did not look at.
|
|
30
|
+
name: Grails Code Sync
|
|
31
|
+
|
|
32
|
+
on:
|
|
33
|
+
push:
|
|
34
|
+
# The branch grails.code.json declares. Keep the two equal.
|
|
35
|
+
branches: [main]
|
|
36
|
+
|
|
37
|
+
permissions:
|
|
38
|
+
contents: read
|
|
39
|
+
|
|
40
|
+
jobs:
|
|
41
|
+
sync:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
- uses: actions/setup-node@v4
|
|
46
|
+
with:
|
|
47
|
+
node-version: 22
|
|
48
|
+
# Needed when the library uses Tailwind (its tokens are read with the project's OWN tailwindcss; without the
|
|
49
|
+
# dependencies installed the run exits 2), and better in any case: with them the scanner reads the types the
|
|
50
|
+
# way the project does (the "typed" run). Use your package manager's command.
|
|
51
|
+
# - run: npm ci
|
|
52
|
+
- name: Sync the design tokens and the components to Grails
|
|
53
|
+
id: grails
|
|
54
|
+
env:
|
|
55
|
+
GRAILS_API_KEY: ${{ secrets.GRAILS_API_KEY }}
|
|
56
|
+
run: npx --yes grails-cli sync --trigger ci
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grails-cli
|
|
3
|
+
description: Use when working with the Grails CLI (`grails-cli`) in a component library's repository. That covers setting up `grails.code.json`, syncing components and design tokens to Grails from a terminal or from CI, writing DTCG token files that Grails accepts, starting a first DTCG version from the scanner's tokens, listing the token changes people asked for in Grails, and fixing a sync that failed. Triggers on grails-cli, grails.code.json, Grails Code Sync, GRAILS_API_KEY, DTCG upload, design tokens to Grails, tokens pending, tokens baseline.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# The Grails CLI
|
|
7
|
+
|
|
8
|
+
Grails (grails.design) keeps a design system's Figma files and its code side by side. This CLI reads your
|
|
9
|
+
component library's repository (its components and its design tokens) and syncs them to a Grails project.
|
|
10
|
+
|
|
11
|
+
**It never changes your code.** It writes only the files you explicitly ask it to: `init --write` writes
|
|
12
|
+
`grails.code.json`, `scan --out <file>` writes the manifest, and `tokens baseline --out <dir>` writes the scanner's
|
|
13
|
+
tokens as DTCG files (both refuse a path inside the repository unless you pass `--in-repo`). When people in Grails ask
|
|
14
|
+
for a token change, the CLI lists the change, and you or your build make it in your own source.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
`npm i -D grails-cli` in the repository (Node 20 or newer), or run any command without installing it as
|
|
19
|
+
`npx grails-cli <command>`. The commands below are written `grails-cli`. The GitHub workflow template the package
|
|
20
|
+
ships (`templates/grails-cli-sync.yml`) runs `npx --yes grails-cli sync`: pin a version in it (`grails-cli@<version>`)
|
|
21
|
+
to choose when a new release reaches your pushes.
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
| command | what it does | sends anything? |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `grails-cli init [root] [--write]` | proposes a `grails.code.json` from the code, with the evidence for each decision | no |
|
|
28
|
+
| `grails-cli scan [root] [--out <file>]` | writes the component manifest Grails would receive | no |
|
|
29
|
+
| `grails-cli tokens [root] [--json]` | the design tokens a sync would upload (with `dtcg`: the files, or the exact upload with `--json`) | no |
|
|
30
|
+
| `grails-cli tokens baseline [root] --out <dir> [--in-repo]` | writes the scanner's tokens as DTCG files and a resolver, a first version to own | no |
|
|
31
|
+
| `grails-cli sync [root] [--dry-run] [--json]` | uploads the tokens, then the components | yes (with `--dry-run`: asks what would change, stores nothing) |
|
|
32
|
+
| `grails-cli tokens pending [root] [--collection <name>] [--request <id>] [--json]` | the token changes people asked for in Grails: type, before → after, and the id; the filters narrow it | reads only |
|
|
33
|
+
| `grails-cli tokens discard <change-id> \| --all [--collection <name>] \| --request <id> --reason "<why>" [--dry-run]` | closes one change, or a set in one step, that your team will not carry, with the reason | yes (Grails keeps the changes, closed); `--dry-run` changes nothing |
|
|
34
|
+
| `grails-cli rules [root] [--set code=figma]` | the naming rules that match code tokens to Figma ones | edits need `write:tokens` |
|
|
35
|
+
| `grails-cli connect [--prompt]` | keeps the API key in the operating system's keychain (checked against Grails first) so no command needs `GRAILS_API_KEY`; reads the clipboard and clears it, or with `--prompt` asks in a hidden prompt | reads only (one authenticated read to validate the key) |
|
|
36
|
+
| `grails-cli disconnect` | removes the key `connect` stored (the key itself is not revoked) | no |
|
|
37
|
+
| `grails-cli update [root]` | asks the npm registry now for the latest `grails-cli` and prints the command that updates it for the project's package manager; it does not run it | asks the npm registry only |
|
|
38
|
+
| `grails-cli status` | the project's code sources, their last syncs, and how many changes asked for in Grails wait for each | reads only |
|
|
39
|
+
| `grails-cli chi [root]` | the Component Health Index of every component | no |
|
|
40
|
+
| `grails-cli check-classes [root]` | Tailwind classes that are probably misspelt | no |
|
|
41
|
+
| `grails-cli skill` | prints this file | no |
|
|
42
|
+
|
|
43
|
+
**The API key** is read from `GRAILS_API_KEY` in the environment (CI, and anyone who prefers it) and, when that is not
|
|
44
|
+
set, from the operating system's keychain, where `grails-cli connect` put it; and from nowhere else. Never pass it as a
|
|
45
|
+
flag or an argument, and never print the environment. To create one, go to Team Settings › Integrations › API Keys in
|
|
46
|
+
Grails and give it the `write:code` scope. `GRAILS_API_URL` points the CLI at another API; the keychain keeps one key
|
|
47
|
+
per API, by its host.
|
|
48
|
+
|
|
49
|
+
**An agent does not run `connect` for the person.** `connect` takes the key from the person's clipboard or from a hidden
|
|
50
|
+
prompt on their terminal; a key an agent was handed in a chat is already in a transcript, so it does not go to
|
|
51
|
+
`connect --prompt` either. When there is no key, tell the person to run `grails-cli connect` in their own terminal (or to set
|
|
52
|
+
`GRAILS_API_KEY`), and carry on with the commands that need none (`init`, `scan`, `tokens`, `sync --dry-run`).
|
|
53
|
+
|
|
54
|
+
**Updates.** Once a day the CLI asks the npm registry for the latest version and, while it is behind, every run ends with
|
|
55
|
+
one line on stderr naming the two versions and the update command. That line is not an error and does not change the
|
|
56
|
+
exit code. `GRAILS_CLI_NO_UPDATE_CHECK=1` turns it off. `grails-cli update` prints the command and never runs it: update
|
|
57
|
+
the version only when the person asks.
|
|
58
|
+
|
|
59
|
+
## `grails.code.json`
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"branch": "main",
|
|
64
|
+
"components": ["src/components/**/*.tsx"],
|
|
65
|
+
"exclude": ["**/*.stories.tsx"],
|
|
66
|
+
"dtcg": { "files": ["tokens/**/*.json"], "resolver": "tokens/resolver.json" }
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- **`branch`** is required for a real sync. It names the branch that IS this source in Grails: the one merges land
|
|
71
|
+
on. Run anywhere else, the sync uploads nothing and exits 0.
|
|
72
|
+
- **`components`** and **`exclude`** are globs, relative to the root.
|
|
73
|
+
- **`dtcg`** names your DTCG files. **When it is set, it is the only token source**: the scanner reads no token from
|
|
74
|
+
Tailwind or CSS, and `css` and `tokens` are ignored with a warning.
|
|
75
|
+
- `files` are globs of `.json` files.
|
|
76
|
+
- `resolver` is optional, and is a DTCG resolver document. Its `$ref`s name the files relative to its own folder,
|
|
77
|
+
so every file must sit in or below that folder.
|
|
78
|
+
- `cssVariables` is optional: how your CSS names the tokens, so Grails can tell which token a `var(--x)` is. It holds
|
|
79
|
+
the `prefix` of your build, the `exceptions` to the naming, and `ignore`, patterns for custom properties that are
|
|
80
|
+
not your tokens. See "Telling Grails which token a `var(--x)` is".
|
|
81
|
+
- **`css`** (CSS files whose custom properties are tokens) and **`tokens`** (TypeScript token objects) are for the
|
|
82
|
+
scanner, when there is no `dtcg`.
|
|
83
|
+
|
|
84
|
+
## Tokens: two ways in
|
|
85
|
+
|
|
86
|
+
1. **DTCG files (the recommended way).** Generate them with your tool of choice: a Style Dictionary build,
|
|
87
|
+
Tokens Studio's DTCG export, Specify, your own script. Declare them in `dtcg`, and `sync` uploads them as they are.
|
|
88
|
+
Grails reads them and answers with precise findings when it cannot.
|
|
89
|
+
2. **The scanner (a convenience).** With no `dtcg`, the CLI reads a Tailwind 3 or 4 theme, the CSS custom properties
|
|
90
|
+
of the files in `css`, and the token objects in `tokens`. It is good for a first sync. Anything it does not
|
|
91
|
+
support should be exported to DTCG rather than worked around. To move to DTCG, run `tokens baseline --out <dir>`,
|
|
92
|
+
review the files, commit them and declare them in `dtcg`. From then on the files are yours and the scanner stops
|
|
93
|
+
being the source. The command lists the values the scanner left out (they are not in the files), refuses to run while
|
|
94
|
+
`dtcg` is set, and never writes into a folder that has files.
|
|
95
|
+
|
|
96
|
+
With DTCG, a sync records which components each component renders, and which tokens it uses **only through a `var(--x)`
|
|
97
|
+
that `cssVariables` ties to a token** (next section). Every `var(--x)` it cannot tie is listed, so you can fix it.
|
|
98
|
+
|
|
99
|
+
## Telling Grails which token a `var(--x)` is
|
|
100
|
+
|
|
101
|
+
*This section is written for a coding agent that is asked to set it up. Follow the steps in order, and never guess a
|
|
102
|
+
name.*
|
|
103
|
+
|
|
104
|
+
Your components write `var(--color-brand-500)`; your DTCG files say `color.brand.500`. With `dtcg` declared, the scanner
|
|
105
|
+
reads no token, so the only thing that ties the two is how your build names a token's custom property. Grails assumes
|
|
106
|
+
**Style Dictionary's default**, the `css` transform group (`name/kebab`):
|
|
107
|
+
|
|
108
|
+
- the token's path segments, joined with `-`, lower case, behind two dashes: `color.brand.500` is `--color-brand-500`;
|
|
109
|
+
- with the build's `prefix` first: `prefix: "ds"` makes it `--ds-color-brand-500`;
|
|
110
|
+
- camelCase is split (`fontSize` → `font-size`, `h1Bold` → `h1-bold`), but **a digit stays with its letters** (`2xl`
|
|
111
|
+
stays `2xl`, `body2` stays `body2`);
|
|
112
|
+
- anything that is not a letter or a digit is a separator (`Brand Primary`, `brand_primary` and `brand-primary` all give
|
|
113
|
+
`brand-primary`);
|
|
114
|
+
- the name is **case-sensitive**: `var(--Color-Brand-500)` is no token, because the build writes lower case.
|
|
115
|
+
|
|
116
|
+
Where a name does not follow that, you write an **exception**: the custom property and the token it is. A custom
|
|
117
|
+
property that is **not your token at all** (a library's, the framework's) goes in **`ignore`**, as a pattern.
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"dtcg": {
|
|
122
|
+
"files": ["tokens/**/*.json"],
|
|
123
|
+
"resolver": "tokens/resolver.json",
|
|
124
|
+
"cssVariables": {
|
|
125
|
+
"prefix": "ds",
|
|
126
|
+
"exceptions": { "--brand": "color.brand.500", "--surface": "color.semantic.surface" },
|
|
127
|
+
"ignore": ["--radix-*", "--tw-*"]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
All three keys are optional. An exception's value is the token's dotted path in the files (the first segment is its
|
|
134
|
+
collection). **An exception wins over the convention**, and one that names a path the files do not have is reported.
|
|
135
|
+
|
|
136
|
+
**`ignore` is a list of patterns**: a name as `var()` writes it, two dashes first, with `*` for the part that varies
|
|
137
|
+
(`*` is the only wildcard, and at most 200 patterns). A name an `ignore` pattern covers is not resolved, not counted,
|
|
138
|
+
not listed and never sent, **even when the convention would have found a token of that name**; an exception for the same
|
|
139
|
+
name wins over `ignore`. A pattern that covers nothing the components use is not an error: `--json` lists it in
|
|
140
|
+
`css_variables.ignore_unused`, and the terminal line says how many were ignored (`· 14 ignored`), never which.
|
|
141
|
+
|
|
142
|
+
**The steps:**
|
|
143
|
+
|
|
144
|
+
1. **Look at how the repository builds its CSS from the DTCG files.** Find the Style Dictionary configuration
|
|
145
|
+
(`config.json`, `style-dictionary.config.*`, `sd.config.*`, or a script that calls it) and read, for the CSS platform,
|
|
146
|
+
its `prefix`, its `transformGroup` and its `transforms`. Then open the CSS it generates and look at a few real names:
|
|
147
|
+
a name in the output beats a reading of the configuration.
|
|
148
|
+
- `transformGroup: "css"` (or `name/kebab` in `transforms`), and no custom `name` transform: the convention applies.
|
|
149
|
+
- `name/camel`, `name/snake`, `name/constant`, `name/pascal` or a custom `name` transform: the names do not follow
|
|
150
|
+
the convention, and each one a component uses needs an exception.
|
|
151
|
+
2. **Set the prefix**: `"cssVariables": { "prefix": "<the build's prefix>" }`, and leave it out when the build has none.
|
|
152
|
+
3. **Run `grails-cli sync --dry-run`.** It needs no key and sends nothing (with a key it also asks Grails what
|
|
153
|
+
would change). `--json` adds a `css_variables` object with the same list. The line `css variables` counts the custom
|
|
154
|
+
properties your components use and how many resolve to a token.
|
|
155
|
+
4. **Read the unresolved list** (the `!` lines under it), and decide each one:
|
|
156
|
+
- `var(--x) is no token of the DTCG files`: find where `--x` comes from, and choose by who owns it.
|
|
157
|
+
- **It is your token under another name**: generated from a DTCG token, or an alias of a generated one
|
|
158
|
+
(`--x: var(--ds-color-brand-500)`). Add it to **`exceptions`** with that token's path. An exception says what a
|
|
159
|
+
name IS.
|
|
160
|
+
- **It is not your token, and never will be**: a library's, the framework's, one the page sets at run time. Add a
|
|
161
|
+
pattern that covers the family to **`ignore`**, so the list holds only what a person still has to decide.
|
|
162
|
+
Examples that are common: Radix sets sizes for its popovers and menus
|
|
163
|
+
(`--radix-dropdown-menu-content-available-height`, `--radix-select-trigger-width`: pattern `--radix-*`);
|
|
164
|
+
Tailwind's own internals, which a component reads when it writes a ring or a shadow by hand
|
|
165
|
+
(`--tw-ring-color`, `--tw-ring-offset-color`: pattern `--tw-*`); a value the page sets from JavaScript (`--vh`,
|
|
166
|
+
`--scroll-y`). A custom property one component declares for itself (`--card-padding`) is also not a token: ignore
|
|
167
|
+
it, by name or by a prefix that component's team already uses.
|
|
168
|
+
- **You cannot tell which of the two it is**: do not ignore it and do not guess an exception. Leave it listed and
|
|
169
|
+
ask the person.
|
|
170
|
+
- `var(--x) is the name of N tokens`: two tokens spell the same CSS name (`brandPrimary` and `brand-primary`).
|
|
171
|
+
Read the generated CSS to see which one the build keeps (the last declaration wins), and add that one to
|
|
172
|
+
`exceptions`. Better, rename one of them in the DTCG files.
|
|
173
|
+
- `dtcg.cssVariables.exceptions: --x names color.y, which is not a token of the DTCG files`: the exception is
|
|
174
|
+
mistyped, or the token was renamed or removed. Fix its path (`grails-cli tokens --json` prints the files) or
|
|
175
|
+
delete the exception.
|
|
176
|
+
- `whose name has a "/"`: a level of that token's name contains a `/`, which a usage upload cannot spell. Rename the
|
|
177
|
+
level in the DTCG files.
|
|
178
|
+
5. **Run step 3 again** until every remaining `!` line is one you decided to leave for the person. Then tell them what
|
|
179
|
+
you added to `exceptions` and to `ignore` and what you left, with the reason for each.
|
|
180
|
+
6. **Prune** with `grails-cli sync --dry-run --json`: `css_variables.ignore_unused` lists the patterns that ignored
|
|
181
|
+
nothing (no component uses a name they cover, or an exception already names every such name). Delete those.
|
|
182
|
+
|
|
183
|
+
**Keep `ignore` narrow.** A pattern beats the naming convention, so `--*-bg` would hide a real token that happens to end
|
|
184
|
+
in `-bg`: the token would no longer be tied to the components that use it, and the line would not say so. Cover a family
|
|
185
|
+
by its owner's prefix (`--radix-*`), not by a shape (`--*-width`). If a name the pattern covers IS a token, add an
|
|
186
|
+
exception for it: that is the way to keep one inside a broad pattern.
|
|
187
|
+
|
|
188
|
+
**What this reads, and what it does not.** The scanner reads a `var(--x)` a component can use in three places:
|
|
189
|
+
|
|
190
|
+
- **the `style` of any element** in the component, every property (`margin`, `border`, `boxShadow`, …) and not only
|
|
191
|
+
colour, padding and size; also the props libraries use for styles (`contentContainerStyle`, `css`, `sx`), a constant or
|
|
192
|
+
style sheet the style refers to, and every branch of a condition or lookup;
|
|
193
|
+
- **the stylesheet the component's file imports** (`.css`, CSS modules, `.scss`, `.less`), for every component that file
|
|
194
|
+
holds. A custom property a rule *declares* in `:root`, a theme selector or `@theme` is a definition, not a use, so a
|
|
195
|
+
tokens sheet imported by a layout does not make the layout "use" every token. `@import` inside a stylesheet is not
|
|
196
|
+
followed;
|
|
197
|
+
- **Tailwind classes** in `className` and in `cn`, `clsx`, `classnames`, `cx`, `twMerge` and `cva` calls, variants
|
|
198
|
+
included: `bg-[var(--x)]` in either version, `bg-(--x)` in Tailwind 4, `bg-[--x]` in Tailwind 3. (Tailwind 4
|
|
199
|
+
compiles `bg-[--x]` to a literal `--x`, not a `var()`, so it is not read there.)
|
|
200
|
+
|
|
201
|
+
It does **not** read a name a template assembles at run time (`` `var(--space-${n})` ``), a class helper it does not know
|
|
202
|
+
(`tv()`), CSS-in-JS template literals (styled-components, Emotion's `css` tag), a component a file defines but does not
|
|
203
|
+
export, or a property set from JavaScript. So a custom property missing from the list may still be used. A reference
|
|
204
|
+
that is not a `var()` (a Tailwind theme class such as `bg-primary`, a TypeScript token object) is counted in a line ("N
|
|
205
|
+
token reference(s)… are not linked") but not tied to a token. A run never fails for any of this.
|
|
206
|
+
|
|
207
|
+
**Do not** edit the token files or the CSS to make a line disappear: an exception, or a pattern in `ignore`, is the
|
|
208
|
+
team's statement of what a name is, or that it is not theirs.
|
|
209
|
+
|
|
210
|
+
## Writing DTCG that Grails accepts
|
|
211
|
+
|
|
212
|
+
Grails reads the **DTCG format 2025.10** (designtokens.org) and validates every document against its schema.
|
|
213
|
+
|
|
214
|
+
- **A token's path is its dotted name, and the first segment is its collection.** `color.brand.500` belongs to the
|
|
215
|
+
collection `color`. A token directly at the root (one segment) is refused.
|
|
216
|
+
- **Types.** All 13 DTCG types are accepted: `color` (`{ "colorSpace", "components" }`), `dimension`
|
|
217
|
+
(`{ "value", "unit" }`), `number`, `fontFamily`, `fontWeight`, `duration`, `cubicBezier`, `strokeStyle`, `border`,
|
|
218
|
+
`transition`, `shadow` (one or a list, `inset` allowed), `gradient` and `typography`. A group's `$type` applies to
|
|
219
|
+
the tokens inside it.
|
|
220
|
+
- **Aliases** are `{path}` references, at most 64 deep, with no cycles. A JSON Pointer `$ref` inside a value is
|
|
221
|
+
refused: use `{path}`.
|
|
222
|
+
- **Modes.**
|
|
223
|
+
- With no resolver, each collection has one mode, `default`.
|
|
224
|
+
- With a resolver, its sets give every mode the same value, and a modifier's contexts become the collection's modes.
|
|
225
|
+
- Two or more modifiers give one mode per combination, named with `·` (`light·brand-a`), at most 50.
|
|
226
|
+
- A resolver's `$ref` names only a file of the same upload (or `#/sets/x`, `#/modifiers/x`). Nothing is fetched.
|
|
227
|
+
- **Limits.**
|
|
228
|
+
- At most 500 files and 4 MiB in one upload.
|
|
229
|
+
- At most 20,000 values, 500 collections, 20 modifiers, 50 contexts per modifier, and groups 32 levels deep.
|
|
230
|
+
- The CLI checks the file count and the size before sending anything.
|
|
231
|
+
- **Refused outright:**
|
|
232
|
+
- Tokens Studio's legacy format (`$themes`, `$metadata`, or `value` / `type` without `$`): export as DTCG (W3C).
|
|
233
|
+
- A `boolean` or object `$value` with no `$type`.
|
|
234
|
+
|
|
235
|
+
**What DTCG cannot express.** The token stays valid DTCG, with a `$value` any tool can read, and the extra goes in
|
|
236
|
+
`$extensions` under a `design.grails.*` key:
|
|
237
|
+
|
|
238
|
+
- **Typography.** Use `design.grails.typography.extraProps`: `textCase`, `textDecoration`, `fontStyle`,
|
|
239
|
+
`paragraphSpacing`, `paragraphIndent`, `listSpacing`, `leadingTrim`.
|
|
240
|
+
- **Booleans, copy strings, blurs and layout grids.** Use no `$type`, a readable string `$value` (`"true"`,
|
|
241
|
+
`"blur(4px)"`, `"12 columns, 24px gutter"`), and the real value in
|
|
242
|
+
`design.grails.value: { "type": "boolean" | "string" | "blur" | "grid", "value": … }`.
|
|
243
|
+
- **A colour built from a colour and an opacity token.** Keep the applied colour in `$value`, and name the parts in
|
|
244
|
+
`design.grails.composed: { "color": "{path}" or a DTCG colour, "opacity": "{path}" }`. The opacity token is read
|
|
245
|
+
as it is: a Figma opacity is 0–100, so divide by 100.
|
|
246
|
+
|
|
247
|
+
**What Grails does with them.**
|
|
248
|
+
- It reads a token with `design.grails.value` as that type and that value. The extension wins over `$value`, so a
|
|
249
|
+
`$value` like `"{brand}"` there is text, not a reference.
|
|
250
|
+
- It validates the forms above. A malformed one (a boolean whose value is `"yes"`, a `textCase` of `"shout"`) refuses
|
|
251
|
+
the sync with `extension_invalid` and the pointer. A `{path}` inside them that names no token is `alias_unknown`.
|
|
252
|
+
- What it does not know (a key a form does not have, a value type it has never seen) is kept as written, with the
|
|
253
|
+
warning `extension_not_interpreted`, so a document a newer tool wrote loses nothing.
|
|
254
|
+
|
|
255
|
+
When a sync is refused, each finding names the file, the JSON pointer inside it and a code (`schema_invalid`,
|
|
256
|
+
`token_at_root`, `legacy_format`, `alias_unknown`, `alias_cycle`, `extension_invalid`, …). Fix the source and sync
|
|
257
|
+
again.
|
|
258
|
+
|
|
259
|
+
## Changes asked for in Grails
|
|
260
|
+
|
|
261
|
+
Admins and editors can ask for token changes in Grails: a new value, a new token, collection or mode, or a removal.
|
|
262
|
+
**Grails never writes your repository, and neither does the CLI.** Closing the loop takes three steps, and the last one
|
|
263
|
+
is automatic:
|
|
264
|
+
|
|
265
|
+
1. `grails-cli tokens pending` lists the changes by collection: each with its **type**, `before → after` as DTCG, and
|
|
266
|
+
its **id**. The `before` is the value your repository holds now, resolved (an alias in your code shows as the colour
|
|
267
|
+
it resolves to). `--json` has every value exact.
|
|
268
|
+
2. Carry each change into your token source, or into the rules that generate it, and merge it as any other change.
|
|
269
|
+
3. The next `grails-cli sync` (in CI, on the declared branch) **marks the change applied** once your repository holds
|
|
270
|
+
its value, and prints how many. Grails compares the resolved value of the token in that mode, with the tolerance the
|
|
271
|
+
Diff uses for colours and sizes, so an alias that resolves to the asked value counts. A token you removed closes
|
|
272
|
+
its change; a created token, collection or mode closes when it exists.
|
|
273
|
+
|
|
274
|
+
To see what the next sync would do **before** it runs, use `grails-cli sync --dry-run` (with the key): it prints how many
|
|
275
|
+
changes it would mark applied, close because their token is gone, or leave open, with their ids, and stores nothing.
|
|
276
|
+
`grails-cli status` shows how many changes wait for each source.
|
|
277
|
+
|
|
278
|
+
**If the repository holds another value, the change is not closed.** Grails keeps proposing its value, checks again on
|
|
279
|
+
every sync, and records what your code holds: running `tokens pending` again after a sync shows the real "before".
|
|
280
|
+
|
|
281
|
+
**A change your team will not carry is closed with a reason, not ignored:**
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
grails-cli tokens discard <change-id> --reason "Handled in the design-tokens repo"
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
- The id is the one `tokens pending` prints beside the change. Never invent one.
|
|
288
|
+
- `--reason` is required: 1 to 1000 characters on one line. Say why, in words a designer reads: Grails keeps it with the
|
|
289
|
+
change, with who discarded it (the owner of the API key) and that it came from code.
|
|
290
|
+
- Discarding a collection takes its modes and tokens with it. One value of a token being created cannot be discarded
|
|
291
|
+
alone: discard the token.
|
|
292
|
+
- Exit 3 means Grails has no open change with that id for this source, or it was already applied or discarded. Do not
|
|
293
|
+
retry; list again with `tokens pending`.
|
|
294
|
+
- Do it only when the team decided not to carry the change. If it only has not been carried yet, leave it: the sync
|
|
295
|
+
closes it when it arrives.
|
|
296
|
+
|
|
297
|
+
**Many at once.** Exactly one of three picks the changes (the reason is required, as above):
|
|
298
|
+
|
|
299
|
+
```sh
|
|
300
|
+
grails-cli tokens discard --all --dry-run # look first: nothing is changed
|
|
301
|
+
grails-cli tokens discard --all --reason "Handled in the tokens repo" # everything waiting for this repository
|
|
302
|
+
grails-cli tokens discard --all --collection color --reason "…" # one collection's
|
|
303
|
+
grails-cli tokens discard --request <request-id> --reason "…" # one change request's
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
- `--dry-run` lists the changes that would be discarded (id, token, before → after) and changes nothing. Run it first
|
|
307
|
+
and read the list: there is no `--yes`, and a discard cannot be undone from the CLI.
|
|
308
|
+
- The set is closed all together or not at all, and at most 1000 at once. Past that the CLI says so and discards
|
|
309
|
+
nothing: narrow it with `--collection` or `--request`.
|
|
310
|
+
- `tokens pending --collection <name>` and `--request <id>` list exactly what the same words would discard. The request
|
|
311
|
+
id is printed under the heading of `tokens pending`.
|
|
312
|
+
- `--collection` goes with `--all` only. Naming a change id and `--all` together is refused.
|
|
313
|
+
- The answer says how many were discarded; `--json` has the ids. Exit 3 means a request or source Grails does not hold
|
|
314
|
+
(nothing was discarded) or a set over the cap.
|
|
315
|
+
|
|
316
|
+
## In CI
|
|
317
|
+
|
|
318
|
+
The package ships `templates/grails-cli-sync.yml`, a GitHub Actions workflow that runs `npx --yes grails-cli sync --trigger ci`
|
|
319
|
+
on a push to the declared branch.
|
|
320
|
+
|
|
321
|
+
- Store the key as the repository secret `GRAILS_API_KEY`. It reaches the step only through `env:`, never inside a
|
|
322
|
+
`run:` line.
|
|
323
|
+
- Do not run it on `pull_request`.
|
|
324
|
+
- The run writes a job summary, and the step output `result` is `synced`, `preview`, `offline`, `skipped-branch` or
|
|
325
|
+
`failed`.
|
|
326
|
+
|
|
327
|
+
**Exit codes:**
|
|
328
|
+
|
|
329
|
+
| code | meaning |
|
|
330
|
+
|---|---|
|
|
331
|
+
| 0 | done (synced, unchanged, or a dry run) |
|
|
332
|
+
| 1 | the sync ended incomplete |
|
|
333
|
+
| 2 | misconfigured: a flag, `grails.code.json` (no `branch`, a DTCG file that is not JSON or matches nothing), or the key |
|
|
334
|
+
| 3 | refused by the server: the key, the plan, or the content (DTCG findings included) |
|
|
335
|
+
| 4 | the server could not be reached or failed |
|
|
336
|
+
|
|
337
|
+
Only 4 is worth retrying.
|