diff --git a/.claude/settings.json b/.claude/settings.json index 297d8e0c03..8fdee8bb0c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -4,29 +4,21 @@ ], "permissions": { "deny": [ - "Read(./lib/**)", - "Read(./es/**)", - "Read(./commonjs/**)", - "Read(./dist/**)", - "Read(./languages/**)", - "Read(./typings/**)", - "Read(./docs/api/**)", - "Read(./docs/functions/**)", - "Read(./docs/guide/built-in-functions.md)", - "Read(./docs/.vuepress/dist/**)", - "Read(./docs/.vuepress/api-sidebar.json)", - "Read(./docs/.vuepress/api-sidebar-relative.json)", - "Read(./typedoc/**)", - "Read(./doc/**)", - "Read(./coverage/**)", - "Read(./test-jest/**)", - "Read(./test-jasmine/**)", - "Read(./storage/**)" - ] - }, - "worktree": { - "symlinkDirectories": [ - "node_modules" + "Read(/hyperformula/lib/**)", + "Read(/hyperformula/es/**)", + "Read(/hyperformula/commonjs/**)", + "Read(/hyperformula/dist/**)", + "Read(/hyperformula/languages/**)", + "Read(/hyperformula/typings/**)", + "Read(/hyperformula/coverage/**)", + "Read(/hyperformula/test-jest/**)", + "Read(/hyperformula/test-jasmine/**)", + "Read(/hyperformula/typedoc/**)", + "Read(/hyperformula/storage/**)", + "Read(/docs/functions/**)", + "Read(/docs/.vuepress/dist/**)", + "Read(/docs/.vuepress/api-sidebar.json)", + "Read(/docs/.vuepress/api-sidebar-relative.json)" ] } } diff --git a/.claude/skills/changelog-creation/SKILL.md b/.claude/skills/changelog-creation/SKILL.md index b15d335e86..4ccbc4ce26 100644 --- a/.claude/skills/changelog-creation/SKILL.md +++ b/.claude/skills/changelog-creation/SKILL.md @@ -20,7 +20,7 @@ Documentation-only, test-only, and CI or tooling changes take none, and neither ## 4. Write the bullet -Under `## [Unreleased]` in `CHANGELOG.md`, in the section that matches the change, creating the `### ` heading if it is absent. End it with the link from step 3. +Under `## [Unreleased]` in `CHANGELOG.md` at the repository root — one changelog for every package, because they release together on one version. Put it in the section that matches the change, creating the `### ` heading if it is absent. Name the package the entry concerns when the text does not make it obvious. End it with the link from step 3. ## 5. Re-read it as a user would diff --git a/.claude/skills/hyperformula-code-review/SKILL.md b/.claude/skills/hyperformula-code-review/SKILL.md index a63ab845cb..cb5e2248da 100644 --- a/.claude/skills/hyperformula-code-review/SKILL.md +++ b/.claude/skills/hyperformula-code-review/SKILL.md @@ -11,9 +11,9 @@ Always: |---|---| | [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md) | What the change was required to contain | | [`CODE-STYLE.md`](../../../dev-docs/CODE-STYLE.md) | Style, and which paths are hot enough that complexity matters | -| [`TESTING.md`](../../../dev-docs/TESTING.md#a-test-must-prove-behaviour) | Whether the tests prove anything, or only execute code | +| [`TESTING.md`](../../../hyperformula/dev-docs/TESTING.md#a-test-must-prove-behaviour) | Whether the tests prove anything, or only execute code | -Then the page covering what the diff touches: [`ARCHITECTURE.md`](../../../dev-docs/ARCHITECTURE.md), [`PARSER.md`](../../../dev-docs/PARSER.md), [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md), [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md), [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md), [`I18N.md`](../../../dev-docs/I18N.md). +Then the page covering what the diff touches: [`ARCHITECTURE.md`](../../../hyperformula/dev-docs/ARCHITECTURE.md), [`PARSER.md`](../../../hyperformula/dev-docs/PARSER.md), [`INTERPRETER.md`](../../../hyperformula/dev-docs/INTERPRETER.md), [`DEPENDENCY-GRAPH.md`](../../../hyperformula/dev-docs/DEPENDENCY-GRAPH.md), [`FUNCTION-CATALOGUE.md`](../../../hyperformula/dev-docs/FUNCTION-CATALOGUE.md), [`I18N.md`](../../../hyperformula/dev-docs/I18N.md). Review in the order below, and stop to report the first serious finding rather than burying it under style notes. @@ -29,7 +29,7 @@ Review in the order below, and stop to report the first serious finding rather t ## 3. Completeness of a function change -Check every one of [the five places a function change must touch](../../../dev-docs/INTERPRETER.md#the-five-places-a-function-change-must-touch); most of them fail silently when missed. Plus `sizeOfResultArrayMethod` for anything array-returning, and an explicit `optionalArg` where arity does not express the valid call. Skill `hyperformula-function-dev`. +Check every one of [the five places a function change must touch](../../../hyperformula/dev-docs/INTERPRETER.md#the-five-places-a-function-change-must-touch); most of them fail silently when missed. Plus `sizeOfResultArrayMethod` for anything array-returning, and an explicit `optionalArg` where arity does not express the valid call. Skill `hyperformula-function-dev`. ## 4. Performance @@ -37,7 +37,7 @@ Allocation in a per-cell or per-vertex loop; work that could be hoisted out of t ## 5. Public API -`src/HyperFormula.ts` and its exported types are the contract. A signature, return-type, or behaviour change is breaking and needs a migration-guide section and an explicit note. JSDoc here is published output — review it as documentation. +`hyperformula/src/HyperFormula.ts` and its exported types are the contract. A signature, return-type, or behaviour change is breaking and needs a migration-guide section and an explicit note. JSDoc here is published output — review it as documentation. ## 6. Process diff --git a/.claude/skills/hyperformula-dev/SKILL.md b/.claude/skills/hyperformula-dev/SKILL.md index 61df2081c0..587af24066 100644 --- a/.claude/skills/hyperformula-dev/SKILL.md +++ b/.claude/skills/hyperformula-dev/SKILL.md @@ -1,12 +1,12 @@ --- name: hyperformula-dev -paths: src/** +paths: hyperformula/src/** description: > - Use for ANY work touching the HyperFormula engine in `src/`: fixing bugs, adding features, + Use for ANY work touching the HyperFormula engine in `hyperformula/src/`: fixing bugs, adding features, changing the public API, working on the parser, the interpreter, the dependency graph, CRUD operations, configuration options, named expressions, or number and date formats. Also use for how-to questions about engine internals (how recalculation works, why a formula - returns an error, where a value is coerced). Triggers on file paths under `src/`, or when the + returns an error, where a value is coerced). Triggers on file paths under `hyperformula/src/`, or when the user describes a symptom in a calculation without naming a file. This is the primary entry point for engine development — when in doubt, load it. --- @@ -17,7 +17,7 @@ Always: | File | Why | |---|---| -| [`ARCHITECTURE.md`](../../../dev-docs/ARCHITECTURE.md) | The pipeline, the core modules, and the invariants that hold everywhere in `src/` | +| [`ARCHITECTURE.md`](../../../hyperformula/dev-docs/ARCHITECTURE.md) | The pipeline, the core modules, and the invariants that hold everywhere in `hyperformula/src/` | | [`CODE-STYLE.md`](../../../dev-docs/CODE-STYLE.md) | Style, and which paths are hot enough that complexity matters | | [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md) | What the change must contain before review | @@ -25,12 +25,12 @@ Then the page for the stage you are changing: | File | For | |---|---| -| [`PARSER.md`](../../../dev-docs/PARSER.md) | `src/parser/` — formula text to AST, and back | -| [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md) | `src/interpreter/` — AST to value, and built-in functions | -| [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md) | `src/DependencyGraph/` — dependency tracking and recalculation order | -| [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | `src/interpreter/functionMetadata/` — function descriptions | -| [`I18N.md`](../../../dev-docs/I18N.md) | `src/i18n/` — function-name translations | -| [`TESTING.md`](../../../dev-docs/TESTING.md) | Writing the test the change needs | +| [`PARSER.md`](../../../hyperformula/dev-docs/PARSER.md) | `hyperformula/src/parser/` — formula text to AST, and back | +| [`INTERPRETER.md`](../../../hyperformula/dev-docs/INTERPRETER.md) | `hyperformula/src/interpreter/` — AST to value, and built-in functions | +| [`DEPENDENCY-GRAPH.md`](../../../hyperformula/dev-docs/DEPENDENCY-GRAPH.md) | `hyperformula/src/DependencyGraph/` — dependency tracking and recalculation order | +| [`FUNCTION-CATALOGUE.md`](../../../hyperformula/dev-docs/FUNCTION-CATALOGUE.md) | `hyperformula/src/interpreter/functionMetadata/` — function descriptions | +| [`I18N.md`](../../../hyperformula/dev-docs/I18N.md) | `hyperformula/src/i18n/` — function-name translations | +| [`TESTING.md`](../../../hyperformula/dev-docs/TESTING.md) | Writing the test the change needs | ## 2. Locate the stage before changing anything @@ -38,14 +38,14 @@ The engine is a pipeline: `CellContentParser` → `parser/` → `GraphBuilder` | Symptom | Stage | |---|---| -| Does not parse, or parses wrongly | `src/parser/` | -| `getCellFormula` returns something the user never typed | `src/parser/Unparser.ts` | -| A function returns the wrong value or error | `src/interpreter/plugin/` — skill `hyperformula-function-dev` | -| Value right, but stale after an edit | `src/DependencyGraph/`, `src/Evaluator.ts` | -| Wrong after adding or removing rows or columns | `src/dependencyTransformers/`, `LazilyTransformingAstService.ts` | -| Wrong in one language only | `src/i18n/languages/` — skill `i18n-translations` | -| Coercion or comparison is wrong | `src/interpreter/ArithmeticHelper.ts` | -| The public API disagrees with its docs | `src/HyperFormula.ts` | +| Does not parse, or parses wrongly | `hyperformula/src/parser/` | +| `getCellFormula` returns something the user never typed | `hyperformula/src/parser/Unparser.ts` | +| A function returns the wrong value or error | `hyperformula/src/interpreter/plugin/` — skill `hyperformula-function-dev` | +| Value right, but stale after an edit | `hyperformula/src/DependencyGraph/`, `hyperformula/src/Evaluator.ts` | +| Wrong after adding or removing rows or columns | `hyperformula/src/dependencyTransformers/`, `LazilyTransformingAstService.ts` | +| Wrong in one language only | `hyperformula/src/i18n/languages/` — skill `i18n-translations` | +| Coercion or comparison is wrong | `hyperformula/src/interpreter/ArithmeticHelper.ts` | +| The public API disagrees with its docs | `hyperformula/src/HyperFormula.ts` | A bug that looks like an interpreter problem is often a parser or graph problem. Confirm which before editing. Use the `typescript-lsp` plugin to find a definition or its callers; grep is for text, not symbols. @@ -53,7 +53,7 @@ A bug that looks like an interpreter problem is often a parser or graph problem. Write the failing test before the fix and watch it fail — skill `test-writing-discipline`. For a calculation bug the smallest reproduction is a two-line `buildFromArray` plus one `getCellValue`. -If `test/hyperformula-tests/` is absent, `npm run test:jest` runs only the smoke tests and reports a clean pass over almost nothing. Run `npm run test:setup-private` first, and after every branch switch. +If `hyperformula/test/hyperformula-tests/` is absent, `npm run test:jest` runs only the smoke tests and reports a clean pass over almost nothing. Run `npm run test:setup-private` first, and after every branch switch. ## 4. Change, then run the fast loop diff --git a/.claude/skills/hyperformula-function-dev/SKILL.md b/.claude/skills/hyperformula-function-dev/SKILL.md index d0cb5c39bd..e99a778432 100644 --- a/.claude/skills/hyperformula-function-dev/SKILL.md +++ b/.claude/skills/hyperformula-function-dev/SKILL.md @@ -1,6 +1,6 @@ --- name: hyperformula-function-dev -paths: src/interpreter/** +paths: hyperformula/src/interpreter/** description: Use when adding a new built-in spreadsheet function to HyperFormula, changing an existing one's signature, arguments, return type, or error behaviour, or when a function returns the wrong value or the wrong error. Covers the FunctionPlugin contract, runFunction and argument metadata, the function metadata catalogue, translations, and the full end-to-end checklist. --- @@ -8,9 +8,9 @@ description: Use when adding a new built-in spreadsheet function to HyperFormula | File | Why | |---|---| -| [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#built-in-functions) | The plugin contract, `runFunction`, and every argument and function metadata field. Read this before writing any code. | -| [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | What the catalogue entry must contain, and the two ways to get it wrong | -| [`I18N.md`](../../../dev-docs/I18N.md) | Where to source a translation, and why an invented one cannot be taken back | +| [`INTERPRETER.md`](../../../hyperformula/dev-docs/INTERPRETER.md#built-in-functions) | The plugin contract, `runFunction`, and every argument and function metadata field. Read this before writing any code. | +| [`FUNCTION-CATALOGUE.md`](../../../hyperformula/dev-docs/FUNCTION-CATALOGUE.md) | What the catalogue entry must contain, and the two ways to get it wrong | +| [`I18N.md`](../../../hyperformula/dev-docs/I18N.md) | Where to source a translation, and why an invented one cannot be taken back | | [`TESTING.md`](../../../dev-docs/TESTING.md#what-each-kind-of-change-needs) | The list of cases a function change must cover | | [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md) | What the change must contain before review | @@ -18,7 +18,7 @@ description: Use when adding a new built-in spreadsheet function to HyperFormula A function is not done until all five agree, and they do not fail the same way — a missing catalogue entry fails the docs build, a parameter-count mismatch only warns on the console, and the rest fail silently. -The list is in [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#the-five-places-a-function-change-must-touch). Work through it there rather than from a copy; two of the five have their own page, linked from it. +The list is in [`INTERPRETER.md`](../../../hyperformula/dev-docs/INTERPRETER.md#the-five-places-a-function-change-must-touch). Work through it there rather than from a copy; two of the five have their own page, linked from it. ## 3. Declare the two things nothing cross-checks diff --git a/.claude/skills/hyperformula-unit-testing/SKILL.md b/.claude/skills/hyperformula-unit-testing/SKILL.md index d3d6605efc..3bcb498325 100644 --- a/.claude/skills/hyperformula-unit-testing/SKILL.md +++ b/.claude/skills/hyperformula-unit-testing/SKILL.md @@ -1,17 +1,17 @@ --- name: hyperformula-unit-testing -paths: test/** -description: Use when writing or modifying tests for HyperFormula, or when a change to `src/` needs test coverage. Covers the two suites, fetching the private suite, how to build an engine in a test, and what a function or CRUD change must cover. +paths: hyperformula/test/** +description: Use when writing or modifying tests for HyperFormula, or when a change to `hyperformula/src/` needs test coverage. Covers the two suites, fetching the private suite, how to build an engine in a test, and what a function or CRUD change must cover. --- ## 1. Read the relevant files from `dev-docs/` | File | Why | |---|---| -| [`TESTING.md`](../../../dev-docs/TESTING.md) | The two suites, how to write a case, and what each kind of change must cover | +| [`TESTING.md`](../../../hyperformula/dev-docs/TESTING.md) | The two suites, how to write a case, and what each kind of change must cover | | [`WORKTREES.md`](../../../dev-docs/WORKTREES.md) | Only when working in a linked worktree, where the private suite is absent entirely | -And [`test/README.md`](../../../test/README.md) for how the private suite is fetched and the environment variables it honours. +And [`hyperformula/test/README.md`](../../../dev-docs/README.md) for how the private suite is fetched and the environment variables it honours. ## 2. Attach the private suite before trusting anything @@ -19,7 +19,7 @@ And [`test/README.md`](../../../test/README.md) for how the private suite is fet npm run test:setup-private ``` -Run it after every branch switch. Without `test/hyperformula-tests/` the Jest run covers only the smoke tests and reports a clean pass over almost nothing — the most common false signal in this repository. `test:performance`, `test:compatibility` and the benchmarks all resolve into that directory, so without it they fail on a missing path rather than on an assertion; read the error before concluding the code is broken. +Run it after every branch switch. Without `hyperformula/test/hyperformula-tests/` the Jest run covers only the smoke tests and reports a clean pass over almost nothing — the most common false signal in this repository. `test:performance` and `test:compatibility` fail on a missing path rather than an assertion; read the error before concluding the code is broken. ## 3. Write the case from the requirement diff --git a/.claude/skills/i18n-translations/SKILL.md b/.claude/skills/i18n-translations/SKILL.md index 08bd5b49e4..84b729a067 100644 --- a/.claude/skills/i18n-translations/SKILL.md +++ b/.claude/skills/i18n-translations/SKILL.md @@ -1,6 +1,6 @@ --- name: i18n-translations -paths: src/i18n/** +paths: hyperformula/src/i18n/** description: Use when adding a built-in function that needs translated names, adding a language pack, or fixing a function name that is wrong in one language. Covers the translation sources, the rules, and what breaks when a key is missing. --- @@ -8,12 +8,12 @@ description: Use when adding a built-in function that needs translated names, ad | File | Why | |---|---| -| [`I18N.md`](../../../dev-docs/I18N.md) | Why translations are engine input rather than decoration, the rules, and the table of sources to translate from | -| [`PARSER.md`](../../../dev-docs/PARSER.md) | Only when changing separators or error literals — the lexer builds its token set from the language package | +| [`I18N.md`](../../../hyperformula/dev-docs/I18N.md) | Why translations are engine input rather than decoration, the rules, and the table of sources to translate from | +| [`PARSER.md`](../../../hyperformula/dev-docs/PARSER.md) | Only when changing separators or error literals — the lexer builds its token set from the language package | ## 2. Look the name up in a real source -Use the sources in [`I18N.md`](../../../dev-docs/I18N.md#where-to-find-a-translation), in the order listed. **Never invent or machine-translate a function name.** A wrong one ships to every user of that language pack and cannot be changed without breaking their formulas. +Use the sources in [`I18N.md`](../../../hyperformula/dev-docs/I18N.md#where-to-find-a-translation), in the order listed. **Never invent or machine-translate a function name.** A wrong one ships to every user of that language pack and cannot be changed without breaking their formulas. ## 3. Add the key to every language file @@ -32,4 +32,4 @@ npm run lint ## Adding a whole language pack -The file, its export in `src/i18n/languages/index.ts`, a key set identical to the other packs, a changelog entry, and `npm run bundle:languages` for the standalone UMD build. +The file, its export in `hyperformula/src/i18n/languages/index.ts`, a key set identical to the other packs, a changelog entry, and `npm run bundle:languages --workspace=hyperformula` for the standalone UMD build. diff --git a/.claude/skills/pr-creation/SKILL.md b/.claude/skills/pr-creation/SKILL.md index 2837d631fe..017a3a86d9 100644 --- a/.claude/skills/pr-creation/SKILL.md +++ b/.claude/skills/pr-creation/SKILL.md @@ -23,7 +23,7 @@ npm run lint npm run test:jest ``` -A green Jest run without `test/hyperformula-tests/` covers only the smoke tests. Confirm the suite is attached before calling it green. Do not open a pull request on a red run and describe it as ready. +A green Jest run without `hyperformula/test/hyperformula-tests/` covers only the smoke tests. Confirm the suite is attached before calling it green. Do not open a pull request on a red run and describe it as ready. ## 4. Push and open the pull request diff --git a/.claude/skills/test-writing-discipline/SKILL.md b/.claude/skills/test-writing-discipline/SKILL.md index 35bf9d1126..0a1506c7f6 100644 --- a/.claude/skills/test-writing-discipline/SKILL.md +++ b/.claude/skills/test-writing-discipline/SKILL.md @@ -23,7 +23,7 @@ When a test is red the default assumption is that the code is wrong. Changing th ## 5. Run it again and read the output -Never claim a test passes because the reasoning is sound. And before treating green as coverage, confirm `test/hyperformula-tests/` is present — `npm run test:setup-private`. +Never claim a test passes because the reasoning is sound. And before treating green as coverage, confirm `hyperformula/test/hyperformula-tests/` is present — `npm run test:setup-private`. ## If you cannot make it pass honestly diff --git a/.claude/skills/writing-docs-pages/SKILL.md b/.claude/skills/writing-docs-pages/SKILL.md index 97654218d6..774a956e97 100644 --- a/.claude/skills/writing-docs-pages/SKILL.md +++ b/.claude/skills/writing-docs-pages/SKILL.md @@ -10,17 +10,17 @@ description: Use when creating or editing a page in the HyperFormula documentati |---|---| | [`DOCS-CONTENT-GUIDE.md`](../../../dev-docs/DOCS-CONTENT-GUIDE.md) | How to write the page: structure, chunking, language, code examples, VuePress conventions, and the self-review checklist to run before finishing | | [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md) | When documentation is required, and describing HyperFormula rather than Excel | -| [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | Only when the change concerns the built-in functions page, which is generated from the catalogue | +| [`FUNCTION-CATALOGUE.md`](../../../hyperformula/dev-docs/FUNCTION-CATALOGUE.md) | Only when the change concerns the built-in functions page, which is generated from the catalogue | | [`BUILD.md`](../../../dev-docs/BUILD.md) | Which documentation files are generated, by which command | -And [`docs/README.md`](../../../docs/README.md), for what the portal contains and how to run it. +And [`docs/README.md`](../../../dev-docs/README.md), for what the portal contains and how to run it. ## 2. Change the source, not the output | To change | Edit | Then run | |---|---|---| -| What the functions page says about a function | its catalogue entry in `src/interpreter/functionMetadata/categories/` | `npm run docs:generate-function-docs` | -| The API reference | the JSDoc in `src/` | `npm run typedoc:build-api` | +| What the functions page says about a function | its catalogue entry in `hyperformula/src/interpreter/functionMetadata/categories/` | `npm run docs:generate-function-docs` | +| The API reference | the JSDoc in `hyperformula/src/` | `npm run typedoc:build-api` | | A guide | the file in `docs/guide/` | `npm run docs:dev` | `docs/guide/built-in-functions.md` and `docs/api/` are git-ignored build output. Editing them is always wrong, and the edit disappears on the next build. diff --git a/.eslintignore b/.eslintignore index 03546876e2..0ed40f6680 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,24 +5,41 @@ node_modules docs/examples/ # 3rd party -src/interpreter/plugin/3rdparty +hyperformula/src/interpreter/plugin/3rdparty # Configurations *.config.js -karma.* +hyperformula/.config/ +hyperformula/karma.* doc -test/_setupFiles/*.js +hyperformula/test/_setupFiles/*.js + +# Scripts, not linted +script + +# The source language packs, deliberately, and only for now. +# +# The old ignore list carried a bare `languages` entry, meant for the build +# output. An unanchored pattern matches a directory of that name at ANY depth, +# so it also excluded hyperformula/src/i18n/languages/ - and the `sort-keys` +# override that targets those files has therefore never run. Anchoring the +# build-output entry below exposed 881 pre-existing violations in them. +# +# Sorting 19 translation files is a change of its own, not a side effect of +# moving directories around. Delete this entry in that change. +hyperformula/src/i18n/languages # Auto-generated directories -commonjs -coverage -dist +hyperformula/commonjs +hyperformula/coverage +hyperformula/dist +hyperformula/es +hyperformula/languages +hyperformula/lib +hyperformula/test-jasmine +hyperformula/test-jest +hyperformula/typings +docs/.vuepress/dist doc -es -languages -lib -script -test-jasmine -test-jest typedoc -typings + diff --git a/.eslintrc.js b/.eslintrc.js index a5e9976e13..5eaf854bc6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { root: true, ignorePatterns: ['.eslintrc.js'], @@ -15,7 +17,7 @@ module.exports = { }, parserOptions: { tsconfigRootDir: __dirname, - project: './tsconfig.json', + project: './hyperformula/tsconfig.json', createDefaultProgram: true, }, extends: [ @@ -134,7 +136,7 @@ module.exports = { { files: ['**/src/**/*.ts'], rules: { - 'license-header/header': [ 'error', './.config/source-license-header.js' ], + 'license-header/header': [ 'error', path.join(__dirname, 'hyperformula/.config/source-license-header.js') ], } }, { diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 60c43dfc32..5ef21e7919 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -34,4 +34,4 @@ jobs: run: npm ci - name: Build docs - run: npm run docs:build + run: npm run docs:install && npm run docs:build diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 936ef53fa2..b745b1a0b4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,10 +36,10 @@ jobs: with: ssh-key: ${{ secrets.DEPLOY_TOKEN }} repository: handsontable/hyperformula-tests - path: test/hyperformula-tests + path: hyperformula/test/hyperformula-tests - name: Fetch hyperformula-tests and sync branches - run: cd test && ./fetch-tests.sh + run: cd hyperformula/test && ./fetch-tests.sh - name: Install dependencies run: npm ci diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 9ef3c8190c..80dd56bb05 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -34,16 +34,16 @@ jobs: with: ssh-key: ${{ secrets.DEPLOY_TOKEN }} repository: handsontable/hyperformula-tests - path: test/hyperformula-tests + path: hyperformula/test/hyperformula-tests - name: Fetch hyperformula-tests and sync branches - run: cd test && ./fetch-tests.sh + run: cd hyperformula/test && ./fetch-tests.sh - name: (base) Install dependencies run: npm ci - name: (base) Run performance tests - run: npm run benchmark:write-to-file base.json + run: npm run benchmark:write-to-file --workspace=hyperformula base.json - name: (head) Checkout main repository uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # https://github.com/actions/checkout/releases/tag/v2.3.4 @@ -55,11 +55,11 @@ jobs: - name: (head) Run performance tests run: | - npm run benchmark:write-to-file head.json + npm run benchmark:write-to-file --workspace=hyperformula head.json - name: Compare the results run: | - npm run benchmark:compare-benchmarks base.json head.json performance-report.md + npm run benchmark:compare-benchmarks --workspace=hyperformula base.json head.json performance-report.md - name: Publish a comment - header uses: marocchino/sticky-pull-request-comment@6804b5ad49d19c10c9ae7cf5057352f7ff333f31 # https://github.com/marocchino/sticky-pull-request-comment/tree/v1.6.0 @@ -73,4 +73,4 @@ jobs: with: append: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - path: performance-report.md + path: hyperformula/performance-report.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c87e1f36d1..8eebea9a1a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,7 +28,7 @@ jobs: run: npm ci - name: Build docs - run: npm run docs:build + run: npm run docs:install && npm run docs:build - name: Deploy to GH pages uses: peaceiris/actions-gh-pages@ba0b7df03e25ff29c924be8149041119e9421ea6 # https://github.com/peaceiris/actions-gh-pages/releases/tag/v3.5.6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9486ae9250..264d67847b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,10 +36,10 @@ jobs: with: ssh-key: ${{ secrets.DEPLOY_TOKEN }} repository: handsontable/hyperformula-tests - path: test/hyperformula-tests + path: hyperformula/test/hyperformula-tests - name: Fetch hyperformula-tests and sync branches - run: cd test && ./fetch-tests.sh + run: cd hyperformula/test && ./fetch-tests.sh - name: Install dependencies run: npm ci @@ -74,10 +74,10 @@ jobs: with: ssh-key: ${{ secrets.DEPLOY_TOKEN }} repository: handsontable/hyperformula-tests - path: test/hyperformula-tests + path: hyperformula/test/hyperformula-tests - name: Fetch hyperformula-tests and sync branches - run: cd test && ./fetch-tests.sh + run: cd hyperformula/test && ./fetch-tests.sh - name: Install dependencies run: npm ci diff --git a/.gitignore b/.gitignore index 886d7289b9..4409ab3db0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,32 @@ .idea/ .vscode -/commonjs/ -/coverage/ -/dist/ -/doc/ +/hyperformula/commonjs/ +/hyperformula/coverage/ +/hyperformula/dist/ +/hyperformula/doc/ /docs/api/ /docs/functions/ /docs/.vuepress/dist/ /docs/.vuepress/api-sidebar-relative.json /docs/.vuepress/api-sidebar.json -/typedoc/ -/es/ -/languages/ -/lib/ -/test-jasmine/ -/test-jest/ +/hyperformula/typedoc/ +/hyperformula/es/ +/hyperformula/languages/ +/hyperformula/lib/ +/hyperformula/test-jasmine/ +/hyperformula/test-jest/ node_modules/ -/typings/ -/storage/ +/hyperformula/typings/ +/hyperformula/storage/ +# Copied in by the package's prepack so the tarball carries one; the source is the +# CHANGELOG.md at the repository root. +/hyperformula/CHANGELOG.md *.iml dev*.html .DS_Store -/test/hyperformula-tests/ +/hyperformula/test/hyperformula-tests/ # Generated at docs:build from built-in-functions.tmpl.md (HF-249 single-source); do not commit. docs/guide/built-in-functions.md diff --git a/.worktreeinclude b/.worktreeinclude index c2c6c2f70d..74111d6626 100644 --- a/.worktreeinclude +++ b/.worktreeinclude @@ -7,19 +7,26 @@ # This covers only the file half of setting up a worktree. It cannot install # dependencies and it cannot fetch the private test suite. See dev-docs/WORKTREES.md. -# Cloudflare Worker local secrets, used by the docs deploy/preview scripts. -# Absent from a fresh worktree, so `wrangler` commands fail there with no -# explanation that points at the worktree. -.dev.vars -.dev.vars.* +# Nothing is copied into a worktree. The list below records what was considered +# and rejected, so it does not get added back. # Deliberately NOT copied — listing these here would cause real bugs: # +# .dev.vars .dev.vars.* +# Cloudflare Worker deploy credentials. Copying them +# puts a deploy token on disk in every worktree, +# including the isolated ones subagents get, for the +# convenience of the one maintainer who actually runs +# `wrangler`. They can copy the file themselves. +# # node_modules A copy is not an install: `.bin` shims and native -# builds do not survive it. Run `npm ci`, or symlink it -# with `worktree.symlinkDirectories`. +# builds do not survive it. Run `npm ci` in the worktree. +# Do NOT symlink it back to the main checkout: in a +# workspace `npm ci` then installs THROUGH the symlink +# and rewrites the main checkout's dependency tree. See +# dev-docs/WORKTREES.md. # -# test/hyperformula-tests/ +# hyperformula/test/hyperformula-tests/ # The private suite is BRANCH-MATCHED to this repository # (test/fetch-tests.sh checks out the branch of the same # name). A copy carries the source branch's tests and @@ -27,7 +34,7 @@ # suite at all. Run `npm run test:setup-private` in the # worktree instead. # -# lib/ dist/ es/ commonjs/ typings/ languages/ +# hyperformula/{lib,dist,es,commonjs,typings,languages}/ # Build outputs. A stale copy makes `npm run test:browser` # and the bundle checks pass or fail against the other # branch's build. Run `npm run bundle-all`. diff --git a/README.md b/README.md index 8d3d14707b..5e46f88181 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,41 @@ -
-

- - HyperFormula - A headless spreadsheet, a parser and evaluator of Excel formulas - -

+# HyperFormula monorepo -

- An open-source headless spreadsheet for business web apps -

+[HyperFormula](https://hyperformula.handsontable.com/) is a headless spreadsheet calculation engine in TypeScript. It parses formulas, tracks cell dependencies, and recalculates incrementally, in the browser and in Node. -

- npm total downloads - npm monthly downloads - GitHub contributors - Known Vulnerabilities -
- FOSSA Status - GitHub Workflow Status - codecov -

+This repository holds the engine and everything built around it. ---- +| Directory | What it is | Published | +|---|---|---| +| [`hyperformula/`](hyperformula/) | The calculation engine. **Start here** — its [README](dev-docs/README.md) is the product documentation. | yes | +| [`hyperformula-ui/`](hyperformula-ui/) | UI components for working with HyperFormula. Not imported yet. | yes | -HyperFormula is a headless spreadsheet built in TypeScript, serving as both a parser and evaluator of spreadsheet formulas. It can be integrated into your browser or utilized as a service with Node.js as your back-end technology. +The published packages release together, on one version, and share the single [`CHANGELOG.md`](CHANGELOG.md) at the root. +| [`docs/`](docs/) | The documentation portal. Installed separately; not a workspace member. | no | -## What HyperFormula can be used for? - -HyperFormula doesn't assume any existing user interface, making it a general-purpose library that can be used in various business applications. Here are some examples: - -- Deterministic compute layer for AI & LLMs -- Calculated fields in CRM and ERP software -- Custom spreadsheet-like app -- Business logic builder -- Forms and form builder -- Educational app -- Online calculator - -## Features - -- [Function syntax compatible with Microsoft Excel](https://hyperformula.handsontable.com/docs/guide/compatibility-with-microsoft-excel.html) and [Google Sheets](https://hyperformula.handsontable.com/docs/guide/compatibility-with-google-sheets.html) -- High-speed parsing and evaluation of spreadsheet formulas -- [A library of ~400 built-in functions](https://hyperformula.handsontable.com/docs/guide/built-in-functions.html) -- [Support for custom functions](https://hyperformula.handsontable.com/docs/guide/custom-functions.html) -- [Support for Node.js](https://hyperformula.handsontable.com/docs/guide/server-side-installation.html#install-with-npm-or-yarn) -- [Support for undo/redo](https://hyperformula.handsontable.com/docs/guide/undo-redo.html) -- [Support for CRUD operations](https://hyperformula.handsontable.com/docs/guide/basic-operations.html) -- [Support for clipboard](https://hyperformula.handsontable.com/docs/guide/clipboard-operations.html) -- [Support for named expressions](https://hyperformula.handsontable.com/docs/guide/named-expressions.html) -- [Support for data sorting](https://hyperformula.handsontable.com/docs/guide/sorting-data.html) -- [Support for formula localization with 17 built-in languages](https://hyperformula.handsontable.com/docs/guide/i18n-features.html) -- Easy integration with any front-end or back-end application -- GPLv3 or a [commercial license](https://handsontable.com/get-a-quote) -- Maintained by the team that stands behind the [Handsontable](https://handsontable.com/) data grid - -## Documentation - -- [Client-side installation](https://hyperformula.handsontable.com/docs/guide/client-side-installation.html) -- [Server-side installation](https://hyperformula.handsontable.com/docs/guide/server-side-installation.html) -- [Basic usage](https://hyperformula.handsontable.com/docs/guide/basic-usage.html) -- [Configuration options](https://hyperformula.handsontable.com/docs/guide/configuration-options.html) -- [List of built-in functions](https://hyperformula.handsontable.com/docs/guide/built-in-functions.html) -- [API Reference](https://hyperformula.handsontable.com/docs/api/) - -## Integrations - -- [Integration with React](https://hyperformula.handsontable.com/docs/guide/integration-with-react.html#demo) -- [Integration with Angular](https://hyperformula.handsontable.com/docs/guide/integration-with-angular.html#demo) -- [Integration with Vue](https://hyperformula.handsontable.com/docs/guide/integration-with-vue.html#demo) -- [Integration with Svelte](https://hyperformula.handsontable.com/docs/guide/integration-with-svelte.html#demo) - -## Installation and usage - -Install the library from [npm](https://www.npmjs.com/package/hyperformula) like so: +## Getting started ```bash -npm install hyperformula +npm ci # installs the workspace +npm run test:setup-private # attaches the private test suite, if you have access +npm run test:jest # the fast test loop +npm run bundle-all # every bundle for the engine ``` -Once installed, you can use it to develop applications tailored to your specific business needs. Here, we've used it to craft a form that calculates mortgage payments using the `PMT` formula. - -```js -import { HyperFormula } from 'hyperformula'; - -// Create a HyperFormula instance -const hf = HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' }); - -// Add an empty sheet -const sheetName = hf.addSheet('Mortgage Calculator'); -const sheetId = hf.getSheetId(sheetName); - -// Enter the mortgage parameters -hf.addNamedExpression('AnnualInterestRate', '8%'); -hf.addNamedExpression('NumberOfMonths', 360); -hf.addNamedExpression('LoanAmount', 800000); +Root scripts fan out to the packages; run a package's own scripts from its directory, or with `--workspace=hyperformula`. -// Use the PMT function to calculate the monthly payment -hf.setCellContents({ sheet: sheetId, row: 0, col: 0 }, [['Monthly Payment', '=PMT(AnnualInterestRate/12, NumberOfMonths, -LoanAmount)']]); +The documentation portal installs on its own: -// Display the result -console.log(`${hf.getCellValue({ sheet: sheetId, row: 0, col: 0 })}: ${hf.getCellValue({ sheet: sheetId, row: 0, col: 1 })}`); +```bash +npm run docs:install +npm run docs:dev ``` -[Run this code in StackBlitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.4.x/mortgage-calculator) - -HyperFormula ships an official Claude skill and machine-readable docs, so your AI coding agent can scaffold, configure, and debug HyperFormula correctly. To install the skill in Claude Code, or to point Cursor, GitHub Copilot, or another agent at the docs, see [Set up your coding agent](https://hyperformula.handsontable.com/docs/guide/setup-coding-agent.html). - -## Contributing - -Contributions are welcome, but before you make them, please read the [Contributing Guide](https://hyperformula.handsontable.com/docs/guide/contributing.html) and accept the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). +## Contributing and development -## License +- External contributors: [`CONTRIBUTING.md`](CONTRIBUTING.md) +- Everyone working on the source, including AI agents: [`dev-docs/README.md`](dev-docs/README.md) — architecture, build, testing, standards, and the definition of done +- AI coding agents: [`AGENTS.md`](AGENTS.md) -HyperFormula is available under two different licenses: GPLv3 and proprietary. The proprietary license can be purchased by [contacting our team](https://handsontable.com/get-a-quote) at Handsontable. +## Licence -Copyright (c) Handsoncode +GPL-3.0-only, plus a commercial licence. See [`LICENSE.txt`](LICENSE.txt). diff --git a/dev-docs/AGENT-TOOLING.md b/dev-docs/AGENT-TOOLING.md index ecdb32c016..e6654010c8 100644 --- a/dev-docs/AGENT-TOOLING.md +++ b/dev-docs/AGENT-TOOLING.md @@ -21,10 +21,11 @@ Committed, so every developer gets the same setup. | Key | Why | |---|---| | `enabledPlugins` | `typescript-lsp` — language-server go-to-definition and find-references. Use it instead of grepping for a symbol's definition or callers; grep stays right for text searches. | -| `permissions.deny` | Blocks agent reads of every generated and built path. The build outputs are git-ignored, so content searches already skip them, but nothing otherwise stops an agent opening `dist/hyperformula.js` or answering a behaviour question from `typings/` instead of `src/`. | -| `worktree.symlinkDirectories` | Symlinks `node_modules` into each worktree rather than duplicating it. See [`WORKTREES.md`](WORKTREES.md). | +| `permissions.deny` | Blocks agent reads of **build artifacts**. They are git-ignored, so content searches already skip them, but nothing otherwise stops an agent opening `dist/hyperformula.js` or answering a behaviour question from `typings/` instead of `hyperformula/src/`. | -`node_modules/` and `package-lock.json` are deliberately readable: reading a dependency's source is sometimes the right move when debugging, and a deny rule would also block a targeted grep for a dependency version. +**Generated documentation is not a build artifact, and is deliberately readable.** `docs/api/` and `docs/guide/built-in-functions.md` are produced by a build step, but they are the API reference and the function reference — reading them to answer a question is the right move, and `FUNCTION-CATALOGUE.md` links straight into them. The rule is about *artifacts*: bundles, declarations, coverage, and the compiled site. Editing either of those files is still always wrong; that is what the build regenerates. + +`node_modules/` and `package-lock.json` are deliberately readable too: reading a dependency's source is sometimes the right move when debugging, and a deny rule would also block a targeted grep for a dependency version. Relative deny patterns anchor at the session's working directory, and project settings are not inherited from parent directories — these rules apply to sessions started at the repository root. @@ -42,7 +43,7 @@ A skill holds the **steps**: what to do, in what order, and what to check. It do | Skill | For | |---|---| -| `hyperformula-dev` | Any work in `src/` — the entry point | +| `hyperformula-dev` | Any work in `hyperformula/src/` — the entry point | | `hyperformula-function-dev` | Adding or changing a built-in function | | `hyperformula-unit-testing` | Writing or modifying tests | | `test-writing-discipline` | Any red test, and any test that might be going green for the wrong reason | diff --git a/dev-docs/BUILD.md b/dev-docs/BUILD.md index 785fad27ca..268d86ab83 100644 --- a/dev-docs/BUILD.md +++ b/dev-docs/BUILD.md @@ -1,31 +1,30 @@ -# Building — the maintainer's view +# Building and releasing -The command reference lives in the public guide, [`docs/guide/building.md`](../docs/guide/building.md): output formats, every `bundle:*` and `verify:*` script, the test commands, and the linter. Read that first. +How the workspace installs, and the repository-level steps: generating the documentation, deploying the portal, and cutting a release. -This page holds only what the guide does not cover. +The engine's own build — the intermediate `lib/`, the bundles, and packaging — is in [`hyperformula/dev-docs/BUILD.md`](BUILD.md). ## Install -Node version is pinned in [`.nvmrc`](../.nvmrc) — 22, the same everywhere in the repository. The repository uses npm with a committed `package-lock.json`, so install with `npm ci`, not `npm install`. +Node version is pinned in [`.nvmrc`](../.nvmrc) — 22, the same in every package. The repository uses npm workspaces with a committed `package-lock.json`, so install with `npm ci`, not `npm install`. -## The intermediate build +```bash +npm ci # the workspace: hyperformula, hyperformula-ui +npm run docs:install # the portal, which is not a workspace member +``` -`npm run compile` runs `tsc` into `lib/`. Every bundle reads `lib/`, not `src/`, so a bundle built without recompiling ships the previous source. - -`npm run bundle-all` chains `clean`, `compile`, every `bundle:*`, then `verify-bundles`. The individual `bundle:*` scripts recompile first unless `HF_COMPILE=1` is set — `script/if-ne-env.js` is what skips the redundant recompile inside `bundle-all`. - -`npm run clean` removes `coverage/`, `commonjs/`, `dist/`, `es/`, `languages/`, `lib/`, `typings/`, and `test-jasmine/`. - -`languages/` — the standalone UMD language packs — is a build output like the rest, and is missing from the guide's list of output formats. +Root scripts fan out to the packages. `npm run lint` is the exception: it runs once, from the root, over the whole repository. To run a package's own scripts, work from its directory or pass `--workspace=hyperformula`. ## Generated documentation | File | Generated by | Rule | |---|---|---| -| `docs/api/` | `npm run typedoc:build-api` | Never edit. Change the JSDoc in `src/`. | -| `docs/guide/built-in-functions.md` | `npm run docs:generate-function-docs`, from `built-in-functions.tmpl.md` and the metadata catalogue | Never edit, never commit. | +| `docs/api/` | `npm run typedoc:build-api` — TypeDoc runs inside `hyperformula/`, where the `tsconfig.json` is, and writes across into the portal | Never edit. Change the JSDoc in `hyperformula/src/`. | +| `docs/guide/built-in-functions.md` | `npm run docs:generate-function-docs`, which runs `docs/script/generate-builtin-functions-doc.ts` against the metadata catalogue | Never edit, never commit. Needs the portal installed. | -Both are git-ignored, and both are regenerated as the first step of `docs:dev` and `docs:build`. `docs:generate-function-docs` is a gate, not a formatter: it fails the build on a missing catalogue entry or a `'Custom'` category. See [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md). +The generator lives in `docs/script/` rather than beside the engine build scripts because it uses `@vuepress/shared-utils` for slugs, and that package only resolves inside a full VuePress dependency tree. + +Both are git-ignored, and both are regenerated as the first step of `docs:dev` and `docs:build`. `docs:generate-function-docs` is a gate, not a formatter: it fails the build on a missing catalogue entry or a `'Custom'` category. See [`FUNCTION-CATALOGUE.md`](../hyperformula/dev-docs/FUNCTION-CATALOGUE.md). ## Deploying the portal @@ -37,14 +36,10 @@ The portal is served by a Cloudflare Worker, and **deployment is driven by Worke | `npm run docs:deploy:cf` | `wrangler deploy` — production | | `npm run docs:preview:cf` | `wrangler versions upload` — a per-branch preview URL | -Config is [`wrangler.jsonc`](../wrangler.jsonc); the Worker entry point is [`worker/index.js`](../worker/index.js). +Config is [`docs/wrangler.jsonc`](../docs/wrangler.jsonc); the Worker entry point is [`docs/worker/index.js`](../docs/worker/index.js). ## Release and licences `npm run release` runs `script/release/release.sh`. Releasing is maintainer-owned — do not invent steps around it, and do not run it as part of another task. `npm run check:licenses` asserts that every production dependency carries a permissive licence. - -## Packaging - -`npm run verify:publish-package` runs `npm pack` through `script/check-publish-package.js` and checks what would actually ship. Run it whenever a change touches `package.json`, `.npmignore`, or the `exports`/`typings` surface. diff --git a/dev-docs/CODE-STYLE.md b/dev-docs/CODE-STYLE.md index 0edd7f670a..9fe1eb71cc 100644 --- a/dev-docs/CODE-STYLE.md +++ b/dev-docs/CODE-STYLE.md @@ -14,25 +14,11 @@ ESLint is the source of truth for formatting and code rules — run `npm run lin - Match the style of the surrounding code and of the project as a whole. New code should not stand out from its neighbours. - Optimize for long-term maintainability: someone else should be able to read, extend, and safely change the code months from now. -## Performance - -HyperFormula is a calculation engine, so the performance of production code is a feature, not an afterthought. - -- Consider the computational complexity of every change, especially in code that runs **per cell, per formula, or per dependency-graph node**. Nested loops over ranges, and repeated work that could be computed once or cached, are the usual suspects. -- Pick the best complexity that still keeps the code readable. When a faster algorithm is harder to follow, explain the trade-off in a JSDoc comment. -- Run `npm run test:performance` for changes that may affect the evaluation or CRUD hot paths. - -Hot paths worth knowing about before touching them: - -| Path | Why it is hot | -|---|---| -| `src/interpreter/` function implementations | Runs once per formula, and once per cell for array-broadcast arguments | -| `src/DependencyGraph/` vertex and range mapping | Every read and every structural change goes through it | -| `src/Evaluator.ts` | Walks the whole recalculation order | -| `src/parser/ParserWithCaching.ts` | Cache misses reparse; a change that defeats the cache is a regression | -| `src/LazilyTransformingAstService.ts` | Deferred AST rewrites after row/column/sheet operations | - ## TypeScript -- The public API surface is `src/HyperFormula.ts` and the types it exports; `npm run bundle:typings` emits them into `typings/`. +- The public API surface is `hyperformula/src/HyperFormula.ts` and the types it exports; `npm run bundle:typings --workspace=hyperformula` emits them into `hyperformula/typings/`. - `npm run verify:typings` (`tsc --noEmit`) must pass. A change that only compiles because of an `as` cast usually has a modelling problem behind it. + +## Performance + +Where a package's code is on a hot path, its own reference says so and names the paths. For the engine — where performance is a feature rather than an afterthought — that is [`hyperformula/dev-docs/PERFORMANCE.md`](../hyperformula/dev-docs/PERFORMANCE.md). diff --git a/dev-docs/DEFINITION-OF-DONE.md b/dev-docs/DEFINITION-OF-DONE.md index 0be743a922..8611a4b6c1 100644 --- a/dev-docs/DEFINITION-OF-DONE.md +++ b/dev-docs/DEFINITION-OF-DONE.md @@ -2,15 +2,15 @@ Every change to production code — bug fix, feature, or improvement — must include all of the following **before** a code review is requested. -1. **The production change**, including every supported language pack in `src/i18n/languages/` when function names are involved. -2. **Automatic tests** in `test/`: +1. **The production change**, including every supported language pack in `hyperformula/src/i18n/languages/` when function names are involved. +2. **Automatic tests** in `hyperformula/test/`: - bug fix — at least one test that reproduces the bug; - new feature — a set of tests that precisely describe the feature; - - pull requests from external contributors put tests in `test/`; the internal team adds them to the private repository through a separate pull request. + - pull requests from external contributors put tests in `hyperformula/test/`; the internal team adds them to the private repository through a separate pull request. See [`TESTING.md`](TESTING.md). 3. **Documentation updates** matching the change. A breaking change also needs a section in the migration guide. See [`DOC-STANDARDS.md`](DOC-STANDARDS.md). 4. **JSDoc** on classes and functions, plus a high-level description of the concepts used in any complex fragment. -5. **A changelog entry**, unless the change is documentation-only (guides, JSDoc, README). +5. **A changelog entry**. Not needed for documentation-only, test-only, or CI and tooling changes, nor for a bug that was introduced and never released. The full rule, and how to write the entry, is in [`DOC-STANDARDS.md`](DOC-STANDARDS.md#the-changelog). 6. **A pull request description** — kept current as the branch evolves, not written once and left to rot. Every element must be not only present but correct: the changelog entry must describe the change accurately, and the documentation must match the new behaviour. @@ -27,7 +27,7 @@ Every change in the pull request must be relevant to the issue it solves. Unrela ## Breaking changes -The public API is `src/HyperFormula.ts` and the types it exports. Avoid breaking it. When a change genuinely requires a break: +The public API is `hyperformula/src/HyperFormula.ts` and the types it exports. Avoid breaking it. When a change genuinely requires a break: - state it explicitly in the pull request description and the changelog entry; - add a migration-guide section describing what breaks and what to do instead; diff --git a/dev-docs/DOC-STANDARDS.md b/dev-docs/DOC-STANDARDS.md index c5df378a29..4edba41885 100644 --- a/dev-docs/DOC-STANDARDS.md +++ b/dev-docs/DOC-STANDARDS.md @@ -1,6 +1,6 @@ # Documentation standards -These apply across the whole repository: guides, the API reference, JSDoc inside `src/`, the changelog, migration guides, and README files. +These apply across the whole repository: guides, the API reference, JSDoc inside `hyperformula/src/`, the changelog, migration guides, and README files. *How* to write a page — structure, chunking, language, code examples, and the self-review checklist — is [`DOCS-CONTENT-GUIDE.md`](DOCS-CONTENT-GUIDE.md). This page is *when* documentation is required and what it must be true about. @@ -21,8 +21,8 @@ This is not a licence to make a guide unreadable on its own. A guide page repeat | File | Generated by | Rule | |---|---|---| -| `docs/api/` | `npm run typedoc:build-api` | Never edit. Change the JSDoc in `src/`. | -| `docs/guide/built-in-functions.md` | `npm run docs:generate-function-docs` | Never edit, never commit. Change the metadata catalogue — see [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md). | +| `docs/api/` | `npm run typedoc:build-api` | Never edit. Change the JSDoc in `hyperformula/src/`. | +| `docs/guide/built-in-functions.md` | `npm run docs:generate-function-docs` | Never edit, never commit. Change the metadata catalogue — see [`FUNCTION-CATALOGUE.md`](../hyperformula/dev-docs/FUNCTION-CATALOGUE.md). | Both are git-ignored. A missing `docs/api/` folder means it has not been built yet; run `npm run docs:build`. @@ -63,6 +63,6 @@ A breaking change says what breaks and what to do instead, and still needs a mig ## The documentation portal -`docs/` is a VuePress site. How to run it, what is generated, and how to add a page are in [`docs/README.md`](../docs/README.md). +`docs/` is a VuePress site. How to run it, what is generated, and how to add a page are in [`docs/README.md`](README.md). Two rules from above bear repeating there, because they are the ones most often broken while writing a guide: do not restate the API reference, and describe HyperFormula rather than Excel. diff --git a/dev-docs/README.md b/dev-docs/README.md index 6ac11238b1..9cf49d6aa3 100644 --- a/dev-docs/README.md +++ b/dev-docs/README.md @@ -14,29 +14,33 @@ Everything a developer needs to know lives here or is linked from here. External In every directory, `CLAUDE.md` is a symlink to its sibling `AGENTS.md`. Edit `AGENTS.md` — the symlink keeps Claude Code and Cursor reading the same single source. +`dev-docs/` exists at two levels. This one holds what applies to the whole repository; each package holds its own internals. A fact has exactly one home in exactly one of them — the split is by ownership, never a copy. + ## Where to look +Repository-wide standards and process live here. Each package documents its own internals: the engine's are in [`hyperformula/dev-docs/`](../hyperformula/dev-docs/README.md). + | You are working on | Read | |---|---| -| Anything in `src/` | [`ARCHITECTURE.md`](ARCHITECTURE.md) — the pipeline, the core modules, and the invariants that hold everywhere in `src/` | -| Formula parsing | [`PARSER.md`](PARSER.md) | -| Formula evaluation, or a built-in function | [`INTERPRETER.md`](INTERPRETER.md) | -| Function descriptions in the API and the docs | [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md) | -| Dependency tracking and recalculation order | [`DEPENDENCY-GRAPH.md`](DEPENDENCY-GRAPH.md) | -| Function-name translations | [`I18N.md`](I18N.md) | -| Tests | [`TESTING.md`](TESTING.md) | | What a change must include before review | [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md) | -| Style and performance | [`CODE-STYLE.md`](CODE-STYLE.md) | +| Code style | [`CODE-STYLE.md`](CODE-STYLE.md) | +| What a test must prove, and how a case is written | [`TESTING.md`](TESTING.md) | | Documentation rules, and the changelog | [`DOC-STANDARDS.md`](DOC-STANDARDS.md) | | Writing a documentation page | [`DOCS-CONTENT-GUIDE.md`](DOCS-CONTENT-GUIDE.md) | -| Building, bundling, releasing | [`BUILD.md`](BUILD.md) | +| Installing the workspace, deploying the portal, cutting a release | [`BUILD.md`](BUILD.md) | | Opening a pull request | [`PULL-REQUESTS.md`](PULL-REQUESTS.md) | -| Where things live, and where they are going | [`STRUCTURE.md`](STRUCTURE.md) | +| Which package holds what, and what the monorepo move still owes | [`STRUCTURE.md`](STRUCTURE.md) | | A linked git worktree | [`WORKTREES.md`](WORKTREES.md) | | How this repository is set up for agents | [`AGENT-TOOLING.md`](AGENT-TOOLING.md) | | Step-by-step task workflows | [`.claude/skills/`](../.claude/skills/) | -Outside this directory: [`docs/README.md`](../docs/README.md) for running the documentation portal, [`test/README.md`](../test/README.md) for attaching the private test suite, [`script/README.md`](../script/README.md) for what each build and release script does, and [`CONTRIBUTING.md`](../CONTRIBUTING.md) for external contributors. +Inside the engine — architecture, the parser, the interpreter, the dependency graph, the function catalogue, translations, performance, its test suites and its build: + +| You are working on | Read | +|---|---| +| Anything in `hyperformula/src/` | [`hyperformula/dev-docs/README.md`](../hyperformula/dev-docs/README.md) | + +Outside both: [`docs/README.md`](../docs/README.md) for running the documentation portal, [`hyperformula/test/README.md`](../hyperformula/test/README.md) for attaching the private test suite, and [`CONTRIBUTING.md`](../CONTRIBUTING.md) for external contributors. ## Conventions diff --git a/dev-docs/STRUCTURE.md b/dev-docs/STRUCTURE.md index 072f718fd0..7aedde4b91 100644 --- a/dev-docs/STRUCTURE.md +++ b/dev-docs/STRUCTURE.md @@ -1,146 +1,126 @@ # Repository structure -Where everything lives today, and where it is going. The repository is becoming a monorepo (HF-359); the second half of this page is the target and the steps that get there. +A monorepo. Three top-level directories hold code; the rest is repository-wide. What the move still owes is at the bottom of this page. ``` . -├── src/ # Source code -│ ├── HyperFormula.ts # Main engine class, public API entry point -│ ├── BuildEngineFactory.ts # Engine construction from sheets, data, and config -│ ├── Config.ts, ConfigParams.ts # Engine configuration and its defaults -│ ├── CrudOperations.ts # Create/read/update/delete on sheets and cells -│ ├── Operations.ts, UndoRedo.ts # Operation primitives and the undo/redo stack -│ ├── Evaluator.ts # Recalculation driver -│ ├── Serialization.ts, Exporter.ts # Reading values and formulas back out -│ ├── NamedExpressions.ts # Named expression store -│ ├── parser/ # Formula parsing (Chevrotain parser generator) -│ ├── interpreter/ # Formula evaluation -│ │ ├── plugin/ # Built-in spreadsheet function plugins -│ │ └── functionMetadata/ # Human-readable metadata for every built-in function -│ ├── DependencyGraph/ # Cell dependency tracking and recalculation order -│ ├── dependencyTransformers/ # AST rewrites when rows/columns/sheets move -│ ├── i18n/languages/ # Function-name translations, one file per language -│ ├── format/ # Number and date format parsing -│ ├── helpers/ # Shared utilities -│ ├── Lookup/ # Lookup/search strategies used by lookup functions -│ └── statistics/ # Instrumentation counters -├── test/ # Smoke tests; the full suite is fetched here -│ ├── README.md # How to attach the private suite -│ ├── smoke.spec.ts # Public smoke tests -│ ├── fetch-tests.sh # Clones/updates the private test repository -│ └── hyperformula-tests/ # Private suite (git-ignored, branch-matched) -├── docs/ # Public documentation portal (VuePress) +├── hyperformula/ # ── package: the calculation engine (published) +│ ├── src/ # Source code +│ │ ├── HyperFormula.ts # Main engine class, public API entry point +│ │ ├── parser/ # Formula parsing (Chevrotain parser generator) +│ │ ├── interpreter/ # Formula evaluation +│ │ │ ├── plugin/ # Built-in spreadsheet function plugins +│ │ │ └── functionMetadata/ # Human-readable metadata for every built-in +│ │ ├── DependencyGraph/ # Cell dependency tracking and recalculation order +│ │ ├── dependencyTransformers/ # AST rewrites when rows/columns/sheets move +│ │ ├── i18n/languages/ # Function-name translations, one file per language +│ │ ├── format/ helpers/ Lookup/ statistics/ +│ ├── test/ # Smoke tests; the private suite mounts here +│ │ ├── smoke.spec.ts # Public smoke tests +│ │ ├── fetch-tests.sh # Clones/updates the private test repository +│ │ └── hyperformula-tests/ # Private suite (git-ignored, branch-matched) +│ ├── dev-docs/ # the engine's own internals reference +│ ├── script/ # its build checks: check-file, check-publish-package, if-ne-env +│ ├── .config/ # webpack, karma, and babel config factories +│ ├── tsconfig.json jest.config.js karma.conf.js webpack.config.js +│ ├── babel.config.js ht.config.js jasmine.json .npmignore +│ ├── .typedoc.ts .typedoc.md.ts # API reference generation, output into docs/api +│ ├── package.json .nvmrc README.md LICENSE.txt +│ └── AGENTS.md CLAUDE.md +│ +├── hyperformula-ui/ # ── package: UI components (not imported yet) +│ +├── docs/ # ── the documentation portal (NOT a workspace member) │ ├── guide/ # Markdown guides -│ ├── api/ # API reference (generated from JSDoc; git-ignored) -│ └── .vuepress/ # VuePress configuration, theme, components -├── script/ # Maintenance, docs-generation, and release scripts -├── worker/ # Cloudflare Worker that serves the built docs -├── examples/ # Images and CSV fixtures used by the docs -├── dev-docs/ # Developer reference (this directory) -├── .claude/ # Claude Code settings and skills +│ ├── api/ # API reference (generated; git-ignored) +│ ├── examples/ # Code examples embedded in guides +│ ├── .vuepress/ # VuePress configuration, theme, components, plugins +│ ├── script/ # Generates guide/built-in-functions.md; composes the Worker assets +│ ├── worker/index.js # Cloudflare Worker serving the built portal +│ ├── wrangler.jsonc # Its deploy configuration +│ ├── package.json .nvmrc +│ └── AGENTS.md CLAUDE.md README.md +│ +├── script/ # Repository-wide only: the release procedure and the licence gate +├── dev-docs/ # Repository-wide reference (this directory; start at README.md) +├── .ai/ # One sentence pointing at dev-docs/, for agents that look here +├── .claude/ # Claude Code settings, skills, and hooks ├── .github/ # CI workflows, issue and PR templates -├── AGENTS.md # Always-loaded agent rules and routing map -├── CLAUDE.md # Symlink to AGENTS.md -├── CONTRIBUTING.md # Guide for external contributors -├── README.md # Project overview -└── CHANGELOG.md +├── .eslintrc.js .eslintignore # Linting, run once from the root over everything +├── package.json # Private workspace root: fan-out scripts only +├── package-lock.json .nvmrc .worktreeinclude +├── AGENTS.md CLAUDE.md README.md CONTRIBUTING.md LICENSE.txt +├── CHANGELOG.md # one history for every package +└── CODE_OF_CONDUCT.md ``` -## Build outputs +## Workspaces + +`workspaces` in the root `package.json` lists `hyperformula`. `npm ci` at the root installs it into a shared `node_modules`. `hyperformula-ui/` is a placeholder and is deliberately not listed yet — see [What the move still owes](#what-the-move-still-owes). + +**`docs/` is deliberately outside the workspace.** The portal drags in a large, old dependency tree (VuePress 1.x, `--openssl-legacy-provider`) that must not reach an engine install. It has its own `package.json` and installs separately with `npm run docs:install`. -All git-ignored, all produced by `npm run bundle-all` (see [`BUILD.md`](BUILD.md)): +## Where a command runs -| Directory | Contents | +| Command | Runs in | |---|---| -| `lib/` | `tsc` output, the input to every bundle | -| `es/` | ES modules (`.mjs`) | -| `commonjs/` | CommonJS modules | -| `dist/` | UMD bundles, minified and not, base and `.full` | -| `languages/` | Standalone UMD language packs | -| `typings/` | Public `.d.ts` declarations | +| `npm run lint` | The root, over the whole repository | +| `npm run test:jest`, `bundle-all`, `compile` | Fanned out to `hyperformula` | +| `npm run docs:*` | Orchestrated from the root across both `hyperformula` and `docs` | -Never edit these, and never read them to answer a question about behaviour — read `src/` instead. +Run a package's own scripts from its directory, or with `--workspace=hyperformula`. See [`BUILD.md`](BUILD.md). -## Directories with their own `AGENTS.md` +## Build outputs -Each of these carries rules that load only when an agent works inside it: +All git-ignored, all under `hyperformula/`: `lib/` (`tsc` output, the input to every bundle), `es/`, `commonjs/`, `dist/`, `languages/`, `typings/`. The portal's output is `docs/.vuepress/dist/`, and `docs/api/` plus `docs/guide/built-in-functions.md` are generated. -`src/`, `src/parser/`, `src/interpreter/`, `src/interpreter/plugin/`, `src/interpreter/functionMetadata/`, `src/DependencyGraph/`, `src/i18n/`, `docs/`, `test/`, `script/`. +Never edit any of them. Never read the **build artifacts** — `lib/`, `es/`, `commonjs/`, `dist/`, `languages/`, `typings/`, `docs/.vuepress/dist/` — to answer a question about behaviour; read `hyperformula/src/` instead, and the agent deny list in `.claude/settings.json` enforces that. -## Where it is going +`docs/api/` and `docs/guide/built-in-functions.md` are the exception. They are generated, so editing them is pointless, but they *are* the API reference and the function reference and reading them is often exactly right. -The repository is becoming a monorepo. The tree above is what a checkout looks like now; everything below is the target. +## `dev-docs/` at two levels -### Packages +This directory holds what applies to the whole repository: the definition of done, code style, testing standards, documentation rules, the build and release process, pull requests, worktrees, and the agent setup. -| Package | Directory | Purpose | Published | -|---|---|---|---| -| `hyperformula` | `hyperformula/` | The calculation engine. Everything in `src/` and `test/` today. | yes | -| `hyperformula-ui` | `hyperformula-ui/` | UI components for working with HyperFormula: reference highlighting, inline formula editor, function help. | yes | -| `hyperformula-docs` | `docs/` | The VuePress documentation portal. | no | +[`hyperformula/dev-docs/`](../hyperformula/dev-docs/README.md) holds the engine's internals: architecture, the parser, the interpreter, the dependency graph, the function catalogue, translations, performance, its test suites, and its own build steps. -`docs/` is not a workspace member: the portal drags in a large, old dependency tree (VuePress, `--openssl-legacy-provider`) that must not reach an engine install. It keeps its own `package.json` and is installed separately. +The split is by ownership. A fact lives in exactly one of them, and `hyperformula-ui` gets its own when it lands. -**The packages release together, on one version, from one changelog.** A release cuts every published package at the same version, whether or not each one changed, and `CHANGELOG.md` at the repository root is the single history for all of them. That keeps one number to reason about — the version a user reports a bug against identifies the state of the whole repository — at the cost of publishing a package whose code did not move. Entries name the package they concern where it is not obvious. +## Scripts at three levels -### Target tree +Each script lives with whatever invokes it, and every one has exactly one caller: -``` -hyperformula/ # repository root — private, workspace root -├── AGENTS.md # monorepo-wide rules + routing map -├── CLAUDE.md -> AGENTS.md -├── README.md CONTRIBUTING.md CHANGELOG.md LICENSE.txt -├── package.json # private: true, npm workspaces, fan-out scripts -├── package-lock.json -├── .nvmrc # 22, like every other one here -├── .worktreeinclude -├── .claude/ -│ ├── settings.json # hooks, enabledPlugins, worktree settings -│ └── skills/ # ALL skills, scoped by the `paths` frontmatter field -├── dev-docs/ # monorepo-scope reference -│ -├── hyperformula/ # ── package: the engine -│ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ ├── package.json .nvmrc CHANGELOG.md -│ ├── dev-docs/ # engine-scope reference -│ ├── src/ -│ │ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ │ ├── parser/ AGENTS.md -│ │ ├── interpreter/ AGENTS.md -│ │ │ ├── plugin/ AGENTS.md -│ │ │ └── functionMetadata/ AGENTS.md -│ │ ├── DependencyGraph/ AGENTS.md -│ │ ├── i18n/languages/ AGENTS.md -│ │ └── dependencyTransformers/ format/ helpers/ Lookup/ statistics/ -│ └── test/ AGENTS.md README.md -│ └── hyperformula-tests/ # private suite, git-ignored, branch-matched -│ -├── hyperformula-ui/ # ── package: the UI components -│ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ ├── package.json .nvmrc CHANGELOG.md -│ ├── dev-docs/ -│ └── src/ test/ -│ -├── docs/ # ── documentation portal (NOT a workspace member) -│ ├── AGENTS.md CLAUDE.md -> AGENTS.md README.md -│ ├── package.json .nvmrc -│ ├── wrangler.jsonc # deploy config for the portal -│ ├── worker/index.js # Cloudflare Worker serving the built portal -│ └── guide/ api/ .vuepress/ -│ -├── examples/ # images and CSV fixtures used by the docs -├── script/ AGENTS.md README.md -└── .github/workflows/ # path-filtered per-package jobs -``` +| Directory | Holds | +|---|---| +| `script/` | `release/` and `check-licenses.mjs` — both span the whole repository | +| `hyperformula/script/` | `check-file.js`, `check-publish-package.js`, `if-ne-env.js` — called by the engine's build | +| `docs/script/` | the built-in-functions generator and `prepare-cf-assets.js` — called by the portal | + +## Directories with their own `AGENTS.md` + +`hyperformula/`, `hyperformula/src/`, `hyperformula/src/parser/`, `hyperformula/src/interpreter/`, `hyperformula/src/interpreter/plugin/`, `hyperformula/src/interpreter/functionMetadata/`, `hyperformula/src/DependencyGraph/`, `hyperformula/src/i18n/`, `hyperformula/test/`, `docs/`, and `script/`. + +Each is a pointer of a few lines — what the directory is, and which `dev-docs/` page or local `README.md` holds the detail. They load automatically when an agent reads a file in that subtree, so they stay small on purpose. + +## What the move still owes + +1. **Import `hyperformula-ui`.** The directory is a placeholder; the package is imported from the formula-builder repository in a separate change, preserving its history, and it keeps the scope it publishes under today. Add `hyperformula-ui` to the root `workspaces` array in that same change, not before: npm silently ignores an entry with no `package.json`, so listing it early buys nothing and the lockfile has to be regenerated when the package lands either way. When it arrives it also needs an `.nvmrc` saying `22` and an `AGENTS.md` with a `CLAUDE.md` symlink — but no changelog of its own, and its version moves in step with the engine's. The release script bumps one manifest today; give it the second one in the same change. +2. **Path-filter CI.** Each package's jobs should run only when its own paths change, with full runs on `develop`, `master`, and release branches. Not done here on purpose: a naive `paths:` filter on a workflow that branch protection lists as a required check leaves the check permanently pending, and pull requests become unmergeable. Doing it safely needs the required-checks list, which lives in repository settings rather than in the tree, and the `dorny/paths-filter`-plus-single-gate shape that the Handsontable monorepo uses. + +## What the move decided + +- **npm workspaces, not pnpm.** A package-manager migration is a risk the move did not need to carry at the same time. +- **`docs/` is not a workspace member.** VuePress 1.x and its `--openssl-legacy-provider` dependency tree must never reach an engine install. It installs on its own with `npm run docs:install`, and CI installs it before building the portal. +- **One `dev-docs/`, at the root.** The original plan put an engine-scope copy inside `hyperformula/`. That was dropped: two directories fragment the single source of truth, and every page would have to know which scope it was written from. The engine's subsystem pages live here alongside the repository-wide ones. +- **The packages release together, on one version, from one changelog.** A release cuts every published package at the same version, whether or not each one changed, and `CHANGELOG.md` at the repository root is the single history for all of them. That keeps one number to reason about — the version a user reports a bug against identifies the state of the whole repository — at the cost of publishing a package whose code did not move. Entries name the package they concern where it is not obvious. +- **The published tarball still carries a changelog.** `hyperformula`'s `prepack` copies the root `CHANGELOG.md` into the package and `postpack` removes it again, so there is one file under version control and npm consumers still get one. +- **Every `.nvmrc` says `22`.** +- **Linting stays at the root**, run once over the whole repository, so nothing between packages falls through the gap. +- **The private test suite stays branch-matched.** Only its checkout path moved, to `hyperformula/test/hyperformula-tests/`. Its specs needed no change: they import the engine relatively, and the depth from a spec to the package root is unchanged. + +## Two things the move uncovered + +Both were pre-existing, and both are recorded here because the next person will otherwise rediscover them the hard way. -### Migration steps - -1. **Move `src/` and `test/` into `hyperformula/`.** Mechanical, but it invalidates every path in CI, in `tsconfig.json`, `jest.config.js`, `karma.conf.js`, `.eslintignore`, and the docs generator scripts. The private suite's specs need no change: they import the engine relatively, and the depth from a spec to the package root is unchanged. -2. **Add `workspaces` to the root `package.json`** and make it `private: true`. Move the build scripts down into `hyperformula/package.json`, leaving fan-out scripts at the root. -3. **Give `docs/` its own `package.json`** and take it out of the root dependency tree. -4. **Move `wrangler.jsonc` and `worker/` under `docs/`.** Update `wrangler.jsonc`'s `main`, the `docs:*:cf` scripts, and `script/prepare-cf-assets.js` in the same change. Verify with `npm run docs:preview:cf` — a broken `main` path fails only at deploy time. -5. **Bring in `hyperformula-ui`**, preserving its history. -6. **Keep `CHANGELOG.md` at the repository root**, in the current Keep a Changelog form, as the single history for every package. The published package still needs one in its tarball, so the release copies it in at pack time rather than keeping a second file under version control. -7. **Give every package an `.nvmrc` saying `22`**, and keep their versions in step — a release bumps them together. -8. **Update the private test suite's checkout path**, from `test/hyperformula-tests/` to `hyperformula/test/hyperformula-tests/`, in `fetch-tests.sh`, `.gitignore`, the three workflows that check it out, and `.worktreeinclude`. It stays branch-matched. -9. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. +- **The source language packs were never linted.** The old ignore list carried a bare `languages` entry meant for the build output. An unanchored pattern matches a directory of that name at any depth, so it also excluded `src/i18n/languages/`, and the `sort-keys` override targeting those files never ran. Anchoring the build-output entry exposed 881 violations. They are excluded again, deliberately and with a comment, in [`.eslintignore`](../.eslintignore); sorting 19 translation files is a change of its own. +- **`@vuepress/shared-utils` only works inside a full VuePress dependency tree.** It requires `markdown-it-emoji` and a `lru-cache` major it does not declare, and relied on `vuepress` hoisting them. That is why the built-in-functions generator moved into `docs/script/`, where that tree exists, rather than staying beside the engine build scripts. diff --git a/dev-docs/TESTING.md b/dev-docs/TESTING.md index a136dc5af4..1e2004cc19 100644 --- a/dev-docs/TESTING.md +++ b/dev-docs/TESTING.md @@ -1,43 +1,16 @@ -# Testing +# Testing standards -## The two suites +What a change must prove, and how a test case is written. These apply to every package in the repository. -| Suite | Where | Who has it | -|---|---|---| -| Smoke tests | [`test/smoke.spec.ts`](../test/smoke.spec.ts) | Everyone, in this repository | -| Full suite | `test/hyperformula-tests/` | Internal team and anyone granted access | - -The full suite is kept in a separate private repository and is **git-ignored** here. It carries the unit tests, the browser and compatibility runs, and the performance benchmarks. External contributors put their tests in `test/`; the internal team moves them into the private repository through a separate pull request. - -## Fetching the private suite - -```bash -npm run test:setup-private -``` - -**Run it after every branch switch.** The suite is branch-matched, so skipping it runs the previous branch's tests against the current source: the results are meaningless, and they look like ordinary passes and failures. How the fetch works, and the environment variables it honours, are in [`test/README.md`](../test/README.md). In a fresh git worktree the directory is absent entirely — see [`WORKTREES.md`](WORKTREES.md). - -## Running tests - -| Command | Runs | -|---|---| -| `npm run test` | Lint, Jest, and the Karma browser run — the full local gate | -| `npm run test:jest` | Jest only; the fast loop | -| `npm run test:watch` | Jest in watch mode | -| `npm run test:coverage` | Jest with coverage | -| `npm run test:browser` | Karma, against the `dist` build | -| `npm run test:compatibility` | The compatibility suite, which ships with the private repository | -| `npm run test:performance` | The basic and CRUD benchmarks | - -`test:performance`, `test:compatibility`, and the benchmark scripts all resolve into `test/hyperformula-tests/`, so they need the private suite attached. Without it they fail on a missing path rather than on an assertion — read the error before concluding the code is broken. +Each package documents its own suites and commands: the engine's are in [`hyperformula/dev-docs/TESTING.md`](../hyperformula/dev-docs/TESTING.md). ## What a change must cover -- Every change to `src/` needs tests in `test/`. This is part of the [definition of done](DEFINITION-OF-DONE.md), not a suggestion. +- Every change to `hyperformula/src/` needs tests in `hyperformula/test/`. This is part of the [definition of done](DEFINITION-OF-DONE.md), not a suggestion. - **Bug fix**: at least one test that reproduces the bug — it must fail against the unfixed code. Write it first and watch it fail. - **New feature**: a set of tests that describe the feature precisely enough to serve as its specification. - Cover more than the happy path: boundary values, empty and invalid input, error results, and interaction with related features. -- `docs/`, `examples/`, and `script/` are not tested. +- `docs/` and the `script/` directories are not tested. ## How to write a test case @@ -58,22 +31,6 @@ it('returns the divisor sign for arguments with opposite signs', () => { Before requesting a review, ask which further tests would be valuable and add the ones that protect against realistic regressions. -## What each kind of change needs - -| Change | Cover | -|---|---| -| A built-in function | Ordinary arguments; each declared boundary (`minValue`, `maxValue`, `lessThan`, `greaterThan`); too few and too many arguments; wrong argument type, asserting the specific `CellError`; an argument that is itself an error; an empty cell and an empty range; the spilled shape if it returns an array; the call with an omitted optional argument | -| CRUD or a structural change | Add and remove rows and columns around a formula, move a range across a formula that references it, then assert **both** the recalculated value and the formula text afterwards — structural bugs show up in the formula text first | -| A parser change | The parse, the round trip through `Unparser`, at least one non-English language, and malformed input that must produce a parsing error rather than a throw | -| A config option | The default, a valid non-default value, and an invalid value that must be rejected | -| A translation | A formula parsed using the translated name, asserted in that language | - -Skills: `hyperformula-unit-testing`, `test-writing-discipline`. - -## Performance - -HyperFormula is a calculation engine, so production-code performance is a feature. Run `npm run test:performance` for any change that can touch the evaluation or CRUD hot paths. See [`CODE-STYLE.md`](CODE-STYLE.md#performance). - ## A test must prove behaviour A test that passes without proving anything is worse than no test: it occupies the space where the real test would have gone, and it makes the next reader believe the behaviour is covered. diff --git a/dev-docs/WORKTREES.md b/dev-docs/WORKTREES.md index 13151d3d19..ff5e8b9c05 100644 --- a/dev-docs/WORKTREES.md +++ b/dev-docs/WORKTREES.md @@ -6,33 +6,27 @@ Claude Code can run a session — or an isolated subagent — in a `git worktree | Missing | Why it matters | Fix | |---|---|---| -| `node_modules/` | Nothing runs. | `npm ci` in the worktree, or symlink it — see below | -| `test/hyperformula-tests/` | The private suite is git-ignored, so every `npm run test:jest` run covers only the smoke tests | `npm run test:setup-private` | -| `lib/`, `dist/`, `es/`, `commonjs/`, `typings/`, `languages/` | `npm run test:browser` and the bundle checks have nothing to run against | `npm run bundle-all` | +| `node_modules/` | Nothing runs. | `npm ci` in the worktree | +| `hyperformula/test/hyperformula-tests/` | The private suite is git-ignored, so every `npm run test:jest` run covers only the smoke tests | `npm run test:setup-private` | +| `hyperformula/{lib,dist,es,commonjs,typings,languages}/` | `npm run test:browser` and the bundle checks have nothing to run against | `npm run bundle-all` | | `docs/api/`, `docs/guide/built-in-functions.md` | The docs build fails, or serves nothing | `npm run docs:build` | -| `.dev.vars*` | `wrangler` commands fail | Copied automatically — see [`.worktreeinclude`](../.worktreeinclude) | +| `.dev.vars*` | `wrangler` deploy and preview fail | Copy it yourself if you need it. Deploy credentials are deliberately **not** copied into worktrees — see [`.worktreeinclude`](../.worktreeinclude) | ## The branch-matched test suite is the trap -`test/fetch-tests.sh` checks out the branch of the **same name** in the private test repository. Two consequences in a worktree: +`hyperformula/test/fetch-tests.sh` checks out the branch of the **same name** in the private test repository. Two consequences in a worktree: -1. Copying `test/hyperformula-tests/` from the main checkout brings the *other* branch's tests. They will run, and they will report results that have nothing to do with the code in front of you. `.worktreeinclude` deliberately does not copy it. +1. Copying `hyperformula/test/hyperformula-tests/` from the main checkout brings the *other* branch's tests. They will run, and they will report results that have nothing to do with the code in front of you. `.worktreeinclude` copies nothing at all, for this reason among others. 2. Run `npm run test:setup-private` once per worktree, and again after any branch switch inside it. -## Symlinking `node_modules` +## Do not symlink `node_modules` -`worktree.symlinkDirectories` in `.claude/settings.json` points each worktree's `node_modules/` at the main checkout's copy instead of duplicating it: +`worktree.symlinkDirectories` used to point each worktree's `node_modules/` at the main checkout's copy. It was removed when the repository became a workspace, and should not come back. -```json -{ - "worktree": { - "symlinkDirectories": ["node_modules"] - } -} -``` +A root-level symlink was safe while there was exactly one `node_modules/.bin`. In a workspace it is not: npm may place a package-local `node_modules/.bin` under a package, which the symlink does not cover, and scripts then die mid-build with a bare `command not found`. Worse, `npm ci` inside a worktree whose `node_modules` is a symlink installs *through* it, so the main checkout and every other worktree silently get that branch's dependency tree. -This is safe while HyperFormula is a single package: there is exactly one `node_modules/.bin`, and the symlink resolves it. **It stops being safe once the repository becomes a workspace** — package-local `node_modules/.bin` directories are not covered by a root-level symlink, and scripts then die mid-build with a bare `command not found`. Revisit this setting as part of the monorepo migration ([`STRUCTURE.md`](STRUCTURE.md#where-it-is-going)). +Run `npm ci` in the worktree. The portal is a separate install either way — `npm run docs:install`. ## Sparse checkouts -`worktree.sparsePaths` limits what git writes to disk. It buys little today — this repository is small and `src/` is needed by everything. It becomes worth setting once the packages in [`STRUCTURE.md`](STRUCTURE.md#where-it-is-going) exist and a task can be scoped to one of them. +`worktree.sparsePaths` limits what git writes to disk. Worth setting once a task can be scoped to one package — `[".claude", "hyperformula"]` for engine work. It is not set by default, because a task that turns out to span packages then fails in a confusing way. diff --git a/docs/.nvmrc b/docs/.nvmrc new file mode 100644 index 0000000000..2bd5a0a98a --- /dev/null +++ b/docs/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/docs/.vuepress/build.config.js b/docs/.vuepress/build.config.js index 633ea41f32..85a777e5d8 100644 --- a/docs/.vuepress/build.config.js +++ b/docs/.vuepress/build.config.js @@ -2,11 +2,11 @@ * Docs build configuration. * Override any of these via environment variables: * DOCS_BASE — public base path (must start and end with `/`) - * DOCS_DEST — output directory (relative to repo root) + * DOCS_DEST — output directory (relative to the docs/ package root) * DOCS_HOSTNAME — absolute origin used for the sitemap */ module.exports = { base: '/docs/', - dest: 'docs/.vuepress/dist/docs', + dest: '.vuepress/dist/docs', hostname: 'https://hyperformula.handsontable.com', }; diff --git a/docs/.vuepress/components/graph.vue b/docs/.vuepress/components/graph.vue index 9c518f1968..edfccddffd 100644 --- a/docs/.vuepress/components/graph.vue +++ b/docs/.vuepress/components/graph.vue @@ -5,7 +5,7 @@