Build-time tooling for the SurveyJS repos, behind one bin: survey-utils.
Four commands, each solving a problem that used to be a per-repo script:
| Command | What it does |
|---|---|
translate <product> |
Sends every not-yet-translated string in a product's locale files to the Azure Translator API and writes the result back, keeping the files' comments and structure. |
check-strings [product] |
Reports localization strings that no product source reaches any more, and exits 1 so CI fails when a newly added string is never used. |
generate-doc [product] |
Generates the API docs, the survey JSON Schema and the LLM authoring guide from a product's TypeScript sources and built bundle. |
generate-doc <preset> |
The same, as a named bundle per product and per role: library-site publishes everything surveyjs.io serves for the Form Library — the docs and the design-token tables — while library-build produces what ships in the npm package. |
survey-utils help # the full option listAll three take a product and find its folders themselves. Nothing about a repo's layout is
typed at the command line: translate knows where a product keeps its locale files,
check-strings knows its source roots, and generate-doc knows the entry files its docs are
built from — survey-core is documented from entries/chunks/model.ts, survey-pdf from
pdf.ts and forms.ts together. Name the product; --path says where the repo is, and only if
it is not where the command would look.
Products: library (survey-core), creator (survey-creator-core), analytics
(survey-analytics, also accepted as dashboard), pdf (survey-pdf, docs only), and
creator-presets (the creator's UI presets, translate only). Each is described in full
under Using it from a product's package.json.
The -site presets write into the site's content repo rather than into a product, so they take a
second root: --out. --path still means what it means everywhere else — the product repo the
sources are read from.
Where the paths inside those roots live. Which file declares the theme, which topic holds
the token tables, which subfolders the generated docs land in — none of that is passed at the
command line. It is declared in paths.json, one file at the root of this repo, so a
pipeline reduced to --path X --out Y keeps working when a layout changes upstream: the JSON
follows the change, this repo is released, and no CI/CD definition is edited. See
The path defaults.
survey-utils/
├── src/
│ ├── cli.ts # The bin: generate-doc (+ presets), translate, check-strings
│ ├── index.ts # Package exports (see "The package exports" below)
│ ├── paths.ts # What --path means: the repo root, for every command
│ ├── site-paths.ts # Reads paths.json: the relative paths inside --path and --out
│ ├── doc-products.ts # generate-doc: product -> repo, entry files, front-matter name
│ ├── translate.ts # translate: product -> localization folder, --key, --path
│ ├── translateLibrary.ts # Per-product entry points, kept for the run_translate_*.cmd
│ ├── translateCreator.ts # files; the CLI is the supported route
│ ├── translateCreatorPresets.ts
│ ├── translateAnalytics.ts
│ ├── localization-utils.ts # Locale-file parsing: JSON + comment extraction, Azure calls
│ ├── checkUnusedStrings.ts # check-strings: argument parsing, reporting, exit code
│ ├── token-tables/ # The design-token tables, filled by the library-site preset
│ │ ├── theme.ts # reads base-theme.ts as data: every --sjs2-* and its value
│ │ ├── tables.ts # the <div id="..."> placeholders and what each one matches
│ │ ├── value.ts # how a value is written in the Value column
│ │ └── run.ts # fillTokenTables: the topics, the per-table report
│ ├── loc-lint/ # The unused-string check itself
│ │ ├── inventory.ts # every key in a product's locale file
│ │ ├── literals.ts # string literals in the product's TS sources (parsed, not grepped)
│ │ ├── analyze.ts # judges each key: literal | resolver | allowlist
│ │ ├── products/ # one module per product: locale file, source roots, resolvers
│ │ └── index.ts # the product registry + aliases (dashboard -> analytics)
│ └── doc-gen/ # The API-doc generator (absorbed from surveyjs-doc-generator)
│ ├── generator.ts # builds the doc model from the TS sources
│ ├── md-generator.ts # --md
│ ├── json-definition.ts # --json-definition (runtime and ast)
│ ├── llm-guide.ts # --llm-guide
│ ├── operators.ts # expression operators, read off expressions.ts' AST
│ └── ts-compat.ts # the two ts.* APIs that changed shape in TypeScript 5
├── tests/
│ ├── translation_utils.test.ts
│ ├── loc-lint.test.ts
│ ├── paths.test.ts # --path and entry resolution
│ ├── site-paths.test.ts # paths.json's defaults, and how --out is resolved
│ ├── token-tables.test.ts # placeholder matching, value formatting, idempotency
│ ├── doc-products.test.ts # product -> repo + entries, and the roots it refuses
│ └── doc-gen/ # Doc-generator specs + fixtures
├── paths.json # The relative paths inside --path and --out, in one file
├── allowlists/ # Known-dynamic and known-dead strings, one file per product
├── run_*.cmd # Windows shortcuts for the checkout-local runs
├── dist/ # Built output (generated; `bin` points at dist/cli.js)
├── tsconfig.json
├── jest.config.js
└── package.json
--path means one thing in every command: the root of a product's repo — the folder
that holds its package.json, not a folder inside it. Each command joins its own subfolders onto
that root, so nothing else about the layout has to be typed:
survey-utils check-strings library --path ../../LibV3/survey-library
survey-utils translate library --path ../../LibV3/survey-library
survey-utils generate-doc library --md --path ../../LibV3/survey-library| Command | What it joins onto the root |
|---|---|
check-strings |
The locale file, the source roots and the built bundle (packages/survey-core/…). Only allowlists/<product>.json stays in survey-utils. |
translate |
The product's localization folder (library → packages/survey-core/src/localization). |
generate-doc |
The product's entry files (library → packages/survey-core/entries/chunks/model.ts), and every relative path the caller passed: --serializer, --out, --md-out. |
generate-doc library-site |
The above, plus survey-core's default theme — product.theme in paths.json — for the design-token tables. |
Without --path, translate and check-strings expect the SurveyJS repos to sit side by
side — the folder that holds survey-utils also holds survey-library, survey-creator,
survey-analytics and survey-pdf. That is how they find a product without being told where it
is. generate-doc looks at the working directory first and falls back to the same sibling
lookup: a product's own package.json script runs inside the package it documents, and a run from
this checkout does not.
--path may name either the repo root (../survey-library) or the package inside it that
holds the entry (../survey-library/packages/survey-core) — both are things a caller has in hand,
and the entry files are known for each. Which one it is, is not guessed from the folder name: the
name in the package.json at that root has to be the product's. A root that holds the wrong
package is a mistake, and saying so is the whole point — a bare src/index.ts is survey-analytics'
entry and survey-utils' own file, so a check that matched on the entry path alone would cheerfully
document the wrong repo.
A --path is one repo, so it cannot answer for a run over several products: check-strings --path <dir> with no product named is rejected, and translate requires the product too.
A path the caller typed and got wrong is reported as a usage mistake — exit code 2, no stack trace, and the report says what it found against what it wanted:
library: C:\survey.js\Lib\survey-pdf is not where it is documented from.
package.json there: survey-pdf
expected:
survey-library -- packages/survey-core/entries/chunks/model.ts
survey-core -- entries/chunks/model.ts
--path must name the root of survey-library, or the package inside it that holds the entry.
The entries are resolved and checked before the --serializer bundle is loaded, so a run that
gets both wrong blames the entry rather than the bundle, and for every emitter — including
--json-definition (runtime), which never builds the doc model — so a root that is not there is
never quietly ignored.
--out means one of two things, and the preset's role decides which — because the two roles
publish to different places.
-site: --out is the content repo's root. It is resolved exactly the way --path is —
against the working directory when relative, falling back to the surveyjs-site-data sibling
checkout when omitted — and the product's folder inside it is derived from the preset:
| Preset | Writes into |
|---|---|
library-site |
<out>/DocsLibrary |
creator-site |
<out>/DocsEditor |
analytics-site |
<out>/DocsAnalytics |
pdf-site |
<out>/DocsPdf |
So every -site preset takes the same --out, and none of them names a subfolder:
survey-utils generate-doc library-site --path .../survey-library --out .../surveyjs-site-data
survey-utils generate-doc creator-site --path .../survey-creator --out .../surveyjs-site-dataThose folder names belong to the site, not to the products — survey-creator is published as
DocsEditor — which is exactly why a pipeline should not have to know them. They live in
paths.json under site.docs.
-build and a plain generate-doc run: --out writes inside the product. It keeps the
older rule — resolved against --path, defaulting to the documented package's own docs folder
— because that is what it names: a folder in the product repo.
# survey-core's own build step: --out is a path inside the repo --path names
survey-utils generate-doc library-build --path ../.. --out ./packages/survey-core/buildThe design-token topics are not under a docs folder at all — they are siblings of them,
Docs/complete-design-token-list.md in the content repo root — so library-site keeps both roots
and fills them there.
Everything inside those two roots is declared in paths.json at the root of this repo:
Why a file rather than options. None of these is a decision a caller makes — they are facts of
two repos' layouts, and a caller who has said --path and --out has already said everything it
knows. Spelling them out at the command line only spreads one layout across every pipeline that
runs the tool, so the day the topic is renamed, the theme moves or a docs folder is renamed, every
one of those pipelines has to be found and edited. Kept here, the layout change is a commit to
this repo and a release; the CI/CD definitions stay --path X --out Y and follow along.
That is also why --theme is gone. It named the file that product.theme now names, and
there was never a second theme to point it at: base-theme.ts is generated from the design system
upstream, and every other theme in survey-core only overrides parts of it, so the complete list of
tokens and their defaults is there and nowhere else. Passing it in was a way to get it wrong.
And it is why the site folders moved out of --out. The pipeline used to say
--out .../surveyjs-site-data/DocsLibrary, which put the site's own naming into every script that
publishes to it and made --out mean the repo in one command and a folder inside it in the next.
It is derived from the preset now, and site.docs is the one place the mapping lives.
A missing or malformed paths.json is reported as a broken install, naming the key that is wrong
— it ships in the package's files, so an install that lacks it is incomplete rather than
misconfigured.
npm i
npm run build # tsc -> dist/ (also runs on `prepare` and `prepublishOnly`)
npm test # jest + ts-jest, over tests/survey-utils is not on the path inside this repo, so run the built bin through node:
node ./dist/cli.js translate library
node ./dist/cli.js check-strings creator --list-dead
node ./dist/cli.js generate-doc library --md
node ./dist/cli.js generate-doc library-site # both roots default to the sibling checkoutsThe npm run translate, npm run check:unused-strings and npm run generate-doc scripts are
thin aliases for those, and take the same arguments after --:
npm run translate -- creator --key <key>
npm run check:unused-strings -- creator --list-deadThe run_*.cmd files at the repo root are the same runs with no arguments to remember
(run_translate_library.cmd, run_check_unused_strings_creator.cmd, …).
translate calls the Azure Cognitive Services Translator API, which needs a subscription
key. Get it from the Azure portal: your Translator resource → Keys and Endpoint (either of
the two keys works). No other command needs one — check-strings and generate-doc never leave
the machine.
The key is read from one of three places, in this order — the first one that has it wins:
| # | Source | Use it for |
|---|---|---|
| 1 | --key <key> on the command line |
A key that lives outside this machine: a CI secret, a colleague's key, a one-off run. |
| 2 | TRANSLATION_API_KEY in the environment |
CI, or a shell you've exported it in. |
| 3 | TRANSLATION_API_KEY in a .env file |
Your own machine: set it once and forget it. |
With none of them, the command fails with an explicit error before it translates anything, so a missing key never leaves the locale files half-written.
.env is the one to use locally. Copy the template and paste the key in:
cp .env.example .env# .env
TRANSLATION_API_KEY=<your Azure Translator subscription key>.env is git-ignored and must never be committed — a leaked key is a billable resource
someone else can spend. .env.example is the committed one, and holds no value.
The file is loaded from the working directory the command runs in, not from wherever
survey-utils is installed. In this repo that means survey-utils/.env. When a product calls
the bin from its own package.json, the .env has to sit in that product's package —
survey-library/packages/survey-core/.env, next to the package.json whose script runs.
Same variable, no file — this is what CI uses:
# bash
export TRANSLATION_API_KEY=<key>
# PowerShell
$env:TRANSLATION_API_KEY = "<key>"# a CI job: the key comes from the secret store, never from the repo
env:
TRANSLATION_API_KEY: ${{ secrets.TRANSLATION_API_KEY }}Overrides both of the above:
node ./dist/cli.js translate library --key <key> # in this repo
npm run translate -- --key <key> # through an npm scriptNever hard-code the key in a scripts entry. It would be committed, and the $VAR trick
doesn't save you — a "translate": "survey-utils translate --key $TRANSLATION_API_KEY" script
does not expand on Windows and passes the literal text $TRANSLATION_API_KEY as the key. Leave
--key out of package.json entirely; let the command read the environment, and pass --key
after -- on the rare run that needs a different one.
Add the dependency and call the bin from scripts — npm puts node_modules/.bin on the path,
so the bare survey-utils name resolves. Every path a command takes is resolved against the
working directory, i.e. the package the script runs in.
{
"devDependencies": { "survey-utils": "^1.0.0" }
}An installed dependency has no sibling checkouts, so translate and check-strings cannot find
the product by looking next to themselves: both need --path, and from a package inside a
monorepo it is the repo root above it — --path ../.. from packages/survey-core. generate-doc
needs none: the working directory is the package it documents, its package.json names the
product, and the entry files follow from that.
Nothing has been cut over yet — no product depends on survey-utils, and
surveyjs-doc-generator still ships to every
consumer. The entries below are what each product will run; migration is manual, repo by repo.
The one product that uses all three commands. survey-core must be built first for the
doc commands and for check-strings: the question types, serializer properties, defaults and
themes are read from the built bundle, not from source, so a stale bundle documents the previous
release and a missing one aborts the check with an explanatory message.
// survey-library/packages/survey-core/package.json
{
"scripts": {
// Markdown API docs + the survey JSON Schema. Replaces doc_generator/lib_docgenerator.js.
// No entry file, no --path, no --serializer and no --out: 'library' plus this package.json is
// the whole address, and ./build/survey.core and ./docs follow from it. Build first: the
// schema is generated from the bundle.
"doc_gen": "survey-utils generate-doc library --md --json-definition",
// The LLM authoring guide, alongside the schema. Both come from the built bundle, and both
// are survey-core's, so neither needs the product named. survey-json-authoring.md defaults
// to ./llms while the schema stays in ./docs; --llm-guide-out overrides that location.
"llm_guide": "survey-utils generate-doc --llm-guide --json-definition",
// CI: regenerate in memory, diff against disk, exit 1 when someone changed survey-core
// and did not regenerate. Output is deterministic, so two runs are byte-identical.
"doc_gen:check": "npm run doc_gen -- --check",
// Localization. --path is the repo root: the product cannot be found from node_modules.
// No key here: it comes from TRANSLATION_API_KEY (environment, or a .env in THIS package).
// See "Setting up the translation key" above.
"translate": "survey-utils translate library --path ../..",
// The unused-string check, over the same repo root.
"check:unused-strings": "survey-utils check-strings library --path ../.."
}
}From this checkout the same check needs no --path: node ./dist/cli.js check-strings library,
or run_check_unused_strings_library.cmd.
Docs only, and without a bundle: survey-creator has no Serializer of its own to generate a
schema from, so it selects --md alone and every serializer-derived section is skipped. No build
is needed for the docs — but check-strings creator does need survey-creator-core built,
because it reads the property grid, question types and logic types from the bundle.
// survey-creator/packages/survey-creator-core/package.json
{
"scripts": {
// No --serializer -> no --json-definition, no --llm-guide. Replaces
// doc_generator/editor_docgenerator.js.
"doc_gen": "survey-utils generate-doc creator --md",
"doc_gen:check": "npm run doc_gen -- --check",
"translate": "survey-utils translate creator --path ../..",
"check:unused-strings": "survey-utils check-strings creator --path ../.."
}
}A second localization folder in the same package (src/ui-preset-editor/localization), translated
on its own. It has no docs and no string check — only translate, and it is a product of its own
precisely because the repo root cannot tell the two folders apart.
// survey-creator/packages/survey-creator-core/package.json
{
"scripts": {
"translate:presets": "survey-utils translate creator-presets --path ../.."
}
}From the checkout: node ./dist/cli.js translate creator-presets.
Localization, and docs from src/index.ts — no bundle, so no schema and no guide. Its string check
is the one that needs no build: the chart types live behind Plotly, which will not import under
Node, so its resolver is static (see products/analytics.ts).
analytics also answers to dashboard, the name it was registered under first — both spellings
select the same product, and an alias never makes it run twice in a whole-repo check.
Its package is its repo root, so --path . is what the scripts pass.
// survey-analytics/package.json
{
"scripts": {
// Replaces doc_generator/lib_docgenerator.js src/index.ts.
"doc:gen": "survey-utils generate-doc analytics --md",
"translate": "survey-utils translate analytics --path .",
"check:unused-strings": "survey-utils check-strings analytics --path ."
}
}Docs only, and the one product documented from two entries at once — src/entries/pdf.ts
and src/entries/forms.ts, exactly as its own doc_gen script always passed them. Naming the
product keeps that pair together; it is the sort of detail a caller should not have to remember,
and the reason the entries live in a table rather than in each repo's scripts. Its package is its
repo root, and it has no localization, so it is not a translate or check-strings product.
// survey-pdf/package.json
{
"scripts": {
// Replaces doc_generator/lib_docgenerator.js src/entries/pdf.ts src/entries/forms.ts.
"doc_gen": "survey-utils generate-doc pdf --md",
"doc_gen:check": "npm run doc_gen -- --check"
}
}# from this repo -- no build of survey-analytics required
node ./dist/cli.js check-strings analytics
node ./dist/cli.js check-strings dashboard # the alias, kept for old scriptsgenerateDocumentation, generateMDFiles and setJsonObj are still exported from the package
root with their original surveyjs-doc-generator signatures (generateDocumentation now also
returns the model), so a consumer can swap the dependency without rewriting its
doc_generator/*.js wrapper in the same step. The bin is the supported route; the exports exist
for that one migration step.
paths(), themePath(), tokenTopicPaths(), siteDocsDir(), apiReferenceDir(),
llmGuideDir() and siteRoot() are exported for the same reason: a consumer that drives the generators directly
resolves the layout the way the bin does, rather than restating it.
survey-utils check-strings analytics # verdict + summary
survey-utils check-strings creator --list-dead # print the cleanup backlog
survey-utils check-strings # every known product
# a checkout that is not a sibling of survey-utils
survey-utils check-strings library --path ../../LibV3/survey-library--path is the repo root, as everywhere (above): everything the
check reads — the locale file, the source roots, the built bundle — is found under it, and only
allowlists/<product>.json stays in survey-utils. The run_*.cmd files forward their arguments,
so run_check_unused_strings_library.cmd --path <dir> works too.
library: 0 new unused string(s), 0 known dead, 0 dynamic exemption(s).
creator: 0 new unused string(s), 0 known dead, 5 dynamic exemption(s).
analytics: 0 new unused string(s), 0 known dead, 0 dynamic exemption(s).
The five exemptions are dynamic: keys built at runtime (e.g. getLocString("ed." + state)).
N known dead means strings recorded with a baseline: reason that are already unused and
waiting to be deleted; --list-dead prints them grouped by namespace.
Exit code 1 — the build should fail when a new unused string appears, when an allowlist
entry rots, or when a dynamic namespace loses its resolver. Strings already recorded as
baseline: are reported on every run but do not fail the build: they are a cleanup backlog,
not a regression.
Most SurveyJS strings are never written down as a whole key. They are assembled at runtime from a registry:
editorLocalization.getString("qt." + this.currentType); // toolbox.ts
editorLocalization.getString("op." + operator); // expressionToDisplayText.ts
editorLocalization.getStringByPath(["pe", propName]); // editorLocalization.tsSearching source text for "pe.enterNewValue" finds nothing for ~1400 of the creator's 1673
keys, live ones included. So the linter asks each namespace's real registry instead: pe.* is
checked against Serializer property names, qt.* against the registered question types,
ed.lg.* against the logic types, and so on. Because those registries come from the loaded
bundle, a property added in survey-core immediately counts as a use of its pe.* / pehelp.* /
p.* strings.
A key is used when any one of three providers vouches for it:
| Provider | Vouches when |
|---|---|
literal |
the dotted path appears verbatim as a string literal in any source file |
resolver |
the namespace's resolver recognises the key against a live registry |
allowlist |
allowlists/<product>.json names it, with a reason |
Only a key that nobody vouches for is reported. TypeScript sources are parsed, not regex-scanned, so a key mentioned only in a commented-out line does not count.
Two kinds of entry, distinguished by the reason's prefix:
dynamic:— reachable, but through a lookup no static check can follow. Cite the call site. The creator has five, allgetString("ed." + variable).baseline:— dead strings inherited when the check was introduced. A cleanup backlog, not a permanent exemption. Delete the key from every locale file in the product, then drop the entry here.
The linter fails if an allowlist entry stops being needed — the key was deleted, or it became reachable again — so the file cannot rot.
src/loc-lint/ knows nothing about SurveyJS apart from products/. Add a module to
src/loc-lint/products/ that exports a LocLintProduct (locale file and export name, source
roots, one resolver per dynamically built namespace, an allowlist), then register it in
src/loc-lint/index.ts:
export const products: Record<string, (root?: string) => LocLintProduct> = {
library: createLibraryProduct, // survey-core
creator: createCreatorProduct, // survey-creator-core
analytics: createAnalyticsProduct, // survey-analytics
};The factory's root is what --path passed. Build every path in the product module under it —
productRoot(repo, root) in paths.ts resolves it, falling back to the
sibling checkout and failing with an actionable message when the folder is not there.
The key is the name callers type and the name of the product's allowlists/<name>.json. When a
product is renamed, keep the old name working by adding it to productAliases in the same file
(dashboard → analytics) instead of leaving a second registry entry.
The three existing products cover three shapes:
products/creator.ts— namespaced keys (qt.*,pe.*, …), one resolver per namespace, each backed by a live registry (Serializer,ElementFactory, grid definitions). The bulk of the work.products/library.ts— survey-core'senglishStringsis a flat table, so the whole key is one segment. It uses a single catch-all"*"resolver that recognises a key as used when it is thelocalizationNameof some localizable string (the@property({ localizable: { defaultStr: true } })pattern, whose key is a bare property name the literal scan can't see). Nearly everything else isliteralevidence, and the allowlist is empty.products/analytics.ts— flat too, but its dynamic keys come from Plotly-backed chart/visualizer registries that won't load under Node. So its"*"resolver is static:visualizer_<type>/chartType_<type>/<type>DownloadCaptionare matched by checking the suffix is a source literal (the type is always assigned as a literal), whileintervalMode_<mode>andtopNValueText<n>are matched against closed enum lists copied from source. No build, no runtime.
A catch-all "*" resolver runs for any key whose first segment has no dedicated resolver — the
right tool for flat-key products. If a product adds a new dynamic lookup —
getString("newns." + x) — the check fails until newns gets a resolver. That is deliberate:
without one, every key in that namespace would be silently unprovable.
src/doc-gen/ is the code of
surveyjs-doc-generator, ported to
TypeScript 5.7 and exposed through the CLI, so a consumer needs one build-time tool dependency
instead of two.
survey-utils generate-doc [product] [options]| Product | Documented from | Front matter | Default out |
|---|---|---|---|
library |
packages/survey-core/entries/chunks/model.ts |
Form Library | packages/survey-core/docs |
creator |
packages/survey-creator-core/src/entries/index.ts |
Survey Creator | packages/survey-creator-core/docs |
analytics |
src/index.ts |
Dashboard | docs |
pdf |
src/entries/pdf.ts and src/entries/forms.ts |
PDF Generator | docs |
The Default out column is relative to the repo root. A run from inside the package writes that
same folder as ./docs, because it is already there.
The entry files are a fact of each repo's layout, not a decision the caller makes — so the product is all that is named, and the table above (in doc-products.ts) supplies the rest: which repo to look in, which entries to compile, and the product name written into the Markdown front matter and the documentation URLs. Each product is also listed with the package the entries are relative to, so the same name works from the repo root and from the package inside it.
| Emitter | Produces |
|---|---|
--md |
<ClassName>.md per class/interface + index.md, in <out>/api |
--json |
The raw doc model: classes.json + pmes.json |
--json-definition |
surveyjs_definition.json from Serializer.generateSchema() — needs survey-core built |
--json-definition=ast |
surveyjs_definition.json derived from the AST — a different, larger document (see below), from the sources alone |
--llm-guide |
survey-json-authoring.md, the authoring guide an LLM is given as context — needs survey-core built. Also emits llms.txt |
They are independently selectable: the model is built once and fanned out to whichever emitters were asked for. At least one is required — unlike the old generator, there is no implicit default, so a run that names none writes nothing and is a mistake, not a no-op: it exits 2 and prints the four flags above to choose from, rather than pointing at the usage text.
The bundle. --serializer <path> names the built product bundle whose Serializer supplies
the metadata, and it defaults to the product's own: survey-core's ./build/survey.core, found
under the root like the entries and the docs folder are. So a built product needs no --serializer
— it is there to name a bundle somewhere else. Without one the docs are AST/JSDoc only and every
serializer-derived section is skipped, which is a legitimate run (survey-creator has no Serializer
of its own and has always generated docs that way) — so a missing bundle only warns, naming the
path it looked at. --json-definition (runtime) and --llm-guide cannot work without it, and those
report the bundle they wanted and exit 2 rather than generate half a document.
--out <dir> defaults to the docs folder of the package the product is documented from — the
Default out column above — so a run from the repo root and a run from the package itself write
the same folder; --check diffs against disk instead of writing.
--llm-guide-out <dir> sets where survey-json-authoring.md is written, resolved against --path
the same way as --out. It defaults to llms — the guide belongs under survey-core/llms while
the API docs and the schema stay in docs. Only the guide file moves; the per-type --split files
and llms.txt stay in --out. It only applies alongside --llm-guide.
Which emitters need the product. --md and --json document one product's API, so the run has
to say which — omit it and the command exits 2 listing the four. --json-definition and
--llm-guide need no product: the schema is Serializer.generateSchema() and the guide is
built from the question types, so both are survey-core's whatever else is generated, and the
product is always library. Asking for either of them for another product is rejected rather
than quietly ignored:
survey-utils generate-doc creator --md --llm-guide --serializer ...
--llm-guide cannot be generated for 'creator': the schema and the guide both come from
survey-core, so they only apply to 'library'.
--entry <path> is the escape hatch, repeatable: it documents an entry the table does not
cover — a fork, another chunk, another package — instead of the product's own. Nothing in the
SurveyJS repos needs it; it exists so that an unusual layout is not a reason to go back to the old
generator.
A preset is a named bundle of emitters, so a release step names one instead of spelling out the flags — and the exact set of artifacts a build or a site publish produces lives in one place (cli.ts) rather than in a script that could drift from it. Each product has two:
survey-utils generate-doc library-site --path ../survey-library --out ../surveyjs-site-data
survey-utils generate-doc creator-site --path ../survey-creator --out ../surveyjs-site-dataPresets take only --path and --out; every other option is ignored — the preset decides
the emitters, so there is nothing else to select. What --out means depends on the role:
-site takes the content repo's root and derives the product's folder inside it, -build takes a
path inside the product. See --out; the folder names
and subfolders both come from paths.json.
library-site also fills the site's design-token tables
— they come from the same survey-core build as the rest, so they are part of the same publish.
| Preset | Emits |
|---|---|
library-build |
llms.txt (copied from survey-utils' static/llms.txt) and surveyjs_definition.json in <out>, and survey-json-authoring.md in <out>/llms |
library-site |
In <out>/DocsLibrary: classes.json, pmes.json, surveyjs_definition.json, llms/survey-json-authoring.md and the Markdown API reference in api-reference/. Plus the design-token tables of <out>/Docs/… |
creator-site, pdf-site, analytics-site |
In <out>/DocsEditor, <out>/DocsPdf, <out>/DocsAnalytics: classes.json, pmes.json and the Markdown API reference in api-reference/ |
creator-build, pdf-build, analytics-build |
Nothing — a no-op (see below) |
The two roles: -build is the artifacts shipped inside the product's npm package;
-site is the artifacts the website serves.
Only library has a built bundle, so only its presets emit the schema (surveyjs_definition.json)
and the LLM guide (survey-json-authoring.md) — both are generated from survey-core's Serializer.
Creator, the PDF Generator and the Dashboard are documented AST/JSDoc only, so their -site presets
emit just the doc model (classes.json + pmes.json) and the Markdown reference, and their
-build presets have nothing to ship and do nothing — kept for symmetry so a release script can
name <product>-build uniformly.
Both derive from survey-core alone. The schema constrains and verifies; the guide teaches. Generate with the guide, validate with the schema.
survey-core must be built first. The generator reads it twice: the built bundle for the
metadata — question types, properties, defaults — and the TypeScript sources under src/ for the
JSDoc.
The expression operators come from the sources too, and deliberately so. They are declared as
static object literals inside expressions.ts — values in a file, not API members — so
operators.ts reads their names off its AST rather than asking
survey-core to export the internal class that holds them. A documentation generator should not
cost the library a permanent public-API commitment. The source only supplies the candidate names;
which of them an author may actually write, and how it is spelled, is settled by
ConditionsParser, which survey-core already exports: a spelling it rejects never reaches the
guide, so the internal helpers drop out on their own, and greater is reported as > because
that is what the library renders it as.
Every fact in both files is extracted at generation time. There are no hand-maintained tables
of types, properties, descriptions, operators or examples: those rot the moment survey-core
changes, and a stale guide teaches a model to write JSON that no longer loads. The only fixed
prose is a ~10-line block of output rules, in one constant in
llm-guide.ts; the guide also points the model at the schema for its
own copy of survey-core (unpkg.com/survey-core@<version>, pinned to the generated version) so it
can self-check what it writes. Every JSON snippet is built through the library's own API,
serialized with toJSON(), then validated, loaded with new SurveyModel(json) and re-serialized
— a snippet that fails is never emitted.
generateSchema() cannot catch an unknown question type. It never emits a schema for
elements, so the per-type definitions it generates are unreachable when a survey is validated:
{"type": "radio"} — which is not a SurveyJS type — validates clean. The schema still checks the
survey's own properties and their types. Because of this the generator does not rely on it alone;
it also checks every snippet's type strings against ElementFactory.getAllTypes() and
round-trips the JSON through SurveyModel. Fixing this properly belongs in survey-core, not here.
Property ordering is weaker than it could be. JsonObjectProperty.category is the field that
would sort "data/logic" above "appearance", but survey-core never sets it — the categories live in
survey-creator's property grid, and the survey-core-only rule puts them out of reach. The guide
sorts on the proxies that are in the library: required first, then structural properties, then
expression-valued ones, then enums, then whatever is documented.
The size budget. The guide is spent from a context window, so every run logs its bytes and
approximate token count, and the run fails above --max-bytes (default 96 KB). The question
types, the shared bases and the survey/page shell come to ~57 KB; the triggers, validators and
nested objects an author also has to get right add ~16 KB more. --split emits one file per
question type instead, for a reader that retrieves rather than reads it all, and
--with-member-links adds member-level API links there.
The name is shared by two different documents, and they are not interchangeable:
- runtime (
--json-definition, ~107 KB) —Serializer.generateSchema(). What the library actually serializes. This is the one you want, and it reproduces survey-core'sdocs/generate_definition.jsbyte for byte. - ast (
--json-definition=ast, ~128 KB) — walks the TypeScript sources instead. It is what survey-core'sdoc_genscript emits today. Nobody has yet established who consumes it, so it is ported as-is rather than dropped.
Two ts.* APIs changed shape between 4.2 and 5.x in ways that fail silently — no crash, just a
quietly wrong doc model. Both are isolated in ts-compat.ts:
node.decoratorswas removed in 5.0. Reading it now yieldsundefined, which would drop every@property()-declared member — most of survey-core's serializable properties — from the model. Usets.canHaveDecorators/ts.getDecorators.JSDocTagInfo.textbecameSymbolDisplayPart[]in 4.3. Assigning it straight into aDocEntrywould put an array of parts where the JSON expects a string, corrupting@title,@description,@deprecated,@see,@returns,@sinceand@hidefor.
tests/doc-gen/members.test.ts and tags.test.ts guard both.
The port was validated by diffing its output against the old generator's, run at TypeScript 4.2.4,
for survey-core and survey-creator-core. surveyjs_definition.json (both producers) is
byte-identical. The doc model and Markdown differ only where TypeScript 5 is better, in three
explained ways, with no member gained or lost:
- DOM globals now resolve, so
anybecameHTMLElement,HTMLInputElement,File[],PointerEvent… @seetag text no longer carries a leading space (a display-parts consequence).SurveyCreatorModel.expandCollapseManager— an inferred type 4.2 gave up on and typedany.
Regenerate the baseline before changing anything in src/doc-gen/: a doc model with fewer
properties than the previous one means a ts.* API changed under you again.
The published token reference — the Complete Design Token List topic on surveyjs.io — lists
roughly a thousand --sjs2-* component tokens and the value each one defaults to. Both facts
live in survey-core's default theme, which is generated from the design system upstream and
changes with it, so the topic is filled from the theme rather than retyped and going stale
quietly.
It is not a command of its own. generate-doc library-site fills the tables as the last step
of the same run that writes the API reference, the schema and the authoring guide:
survey-utils generate-doc library-site --path ../survey-library --out ../surveyjs-site-dataDocsLibrary/ <- classes.json, pmes.json, the schema, api-reference/, llms/
Docs/complete-design-token-list.md <- the tables, filled in place
The two used to be separate commands, and that was the problem: the API reference, the schema and the guide come from a survey-core build, and the token list comes from the theme in that same build. Run apart, they could be pointed at two different checkouts, or one could be forgotten — and the site would describe two versions of survey-core with nothing saying so. Run together, the roots are settled once and neither can drift.
Which topics get filled is site.tokenTopics in paths.json, resolved against
--out — beside the docs folders, not inside one. A topic the checkout has not got yet is
reported and skipped: a branch that has not picked it up is a normal state of an upstream repo,
not a reason to fail a documentation build that otherwise succeeded.
A table is marked in the topic by a <div> whose id is the query that fills it:
<div id="-component-action-">
| Variable | Value |
| -------- | ----- |
| `--sjs2-layout-component-action-xx-small-label-padding-vertical` | spacing-x000 |
| ...every other token with -component-action- in its name |
</div>Several queries are separated by |, which is how one section covers a family of related
components:
<div id="-component-checkbox- | -component-radio- | -component-toggle- | -component-boolean- | -component-buttongroup-item-">The ids belong to the topic, not to this repo. Splitting a section, renaming a component or
adding one is an edit to the Markdown file; nothing here has to be changed to follow it. The
trailing dash is load-bearing: it is what keeps -component-label- from also claiming every
-component-labeled-item- token. An id also matches a name that ends where it does, because a
few tokens are named for a component and nothing else — --sjs2-radius-component-action is the
corner radius of every action, and the whole --sjs2-radius-component-* family would otherwise
be documented nowhere.
The tables span two repos — the ids are in surveyjs-site-data, the values are in survey-library — and both are already in hand, because they are the same two roots the docs need. So the publish step is the doc step:
- script: npx survey-utils generate-doc library-site
--path $(Build.SourcesDirectory)/survey-library
--out $(Build.SourcesDirectory)/surveyjs-site-datasurvey-core must be built first, as for any library-site run: the schema is
Serializer.generateSchema() and the guide's question types come from the same bundle. The theme
is read from the sources, so a missing build is reported before anything is written.
The placeholders survive the rewrite, so the fill is idempotent and re-running it is the whole of "sync the tables": a token added upstream appears in the table its own name puts it in, a removed one leaves, and a changed value changes in one cell. Rows keep the theme's declaration order rather than being sorted, so a diff shows the token change and nothing else — sorting would reshuffle neighbouring rows on every insert and bury it.
There is no staleness check to run any more, and nothing to check: the tables are refilled by
every site publish, from the same build the rest of the artifacts come from. The old
token-tables --check existed because filling them was a separate step that could be skipped.
Values are written the way the hand-written tables higher up the same topic write them — the bare
token name, with no var(), no --sjs2- prefix and no backticks, because a column of
var(--sjs2-…) is eleven characters of noise on a page that is entirely about --sjs2- tokens.
| Declared in base-theme.ts | In the table |
|---|---|
var(--sjs2-spacing-x025) |
spacing-x025 |
rgba(from var(--sjs2-color-bg-alert-tertiary) r g b / var(--sjs2-opacity-hidden)) |
color-bg-alert-tertiary, opacity-hidden |
calc(var(--sjs2-spacing-large-vertical) * 2) |
calc(spacing-large-vertical * 2) |
inset var(--sjs2-border-offset-x-form-default) … |
inset border-offset-x-form-default … |
transparent |
transparent |
| (empty) | — |
A lone reference and a color-at-an-opacity get a form of their own because the semantic tables
already gave them one. Everything else keeps its CSS structure and only loses the var()
wrappers: a composite border effect stays recognizable as the five primitives it is made of.
Rewriting those into prose would mean guessing which part matters, and a generated table is the
wrong place to guess.
Every run prints the row count per table, and warns about the two ways a topic and a theme can drift apart:
- an id that matches nothing — a typo, or a component renamed upstream. The table still generates, with a header and no rows, which reads on the page as "this component has no tokens"; the warning says the run has no way to know that is true.
- component tokens no table claims — declared by the theme and listed nowhere. Ten of them
today: the three
-action-group-gaptokens of the file upload, image and image picker questions,--sjs2-size-component-icon-{small,medium,large}, and the corner radii of the rating, ranking, tooltip and stepper item. Each needs a section in the topic, or an existing id widened to cover it.
MIT — see LICENSE.
{ // Relative to --path: the root of the product's checkout. "product": { "theme": "packages/survey-core/src/default-theme/base-theme.ts" }, "site": { // The checkout --out defaults to, looked up next to survey-utils. "repo": "surveyjs-site-data", // Where each product's -site preset writes, relative to --out. "docs": { "library": "DocsLibrary", "creator": "DocsEditor", "analytics": "DocsAnalytics", "pdf": "DocsPdf" }, // Topics filled from the product sources. Siblings of the docs folders, not inside them. "tokenTopics": ["Docs/complete-design-token-list.md"], // Inside a product's docs folder. "apiReference": "api-reference", "llmGuide": "llms" } }