Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions .github/workflows/server-examples-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
name: server-examples — build

# Builds every frontend under server-examples/. Nothing else in CI touches this
# tree: ci.yml and the e2e workflows are scoped to runner/ and examples/, and
# the starter matrix boots a dev server rather than `build`. That gap is how
# DEV-2727 and DEV-2731 both shipped — a Handsontable release tightened a public
# type, every Angular frontend stopped compiling, and no job noticed.
#
# These 21 projects are NOT part of the root pnpm workspace (the root
# package.json declares no `workspaces` and there is no pnpm-workspace.yaml
# covering them). Each carries its own npm package-lock.json, so this is a
# per-project matrix rather than one install at the root. The matrix is
# discovered at run time from the checkout, so adding a backend or a frontend
# needs no edit here.
#
# Two jobs, because they answer different questions:
#
# build deterministic — `npm ci` against the committed lock. Guards
# source edits. By construction it can NEVER see a new upstream
# release, because the lock pins one.
# build-latest canary — installs handsontable@latest (and the wrapper) before
# building. THIS is the job that would have caught 18.1.0 on the
# day it shipped. It is expected to go red on a breaking release;
# that is the signal, not a flake.
#
# `npm run build` is deliberately what runs, rather than a hand-rolled tsc
# invocation: it is what a contributor runs, and for the Angular projects it is
# `ng build --configuration development`, whose AOT pass is what actually
# type-checks the `[settings]` binding.

on:
pull_request:
paths:
- 'server-examples/**'
- '.github/workflows/server-examples-build.yml'
schedule:
# Weekly, Monday 04:00 UTC. Off-peak for the CET team, and clear of the
# nightly e2e workflows (01:00 / 03:00) — this job needs no container pool,
# but there is no reason to pile onto the same window.
- cron: '0 4 * * 1'
workflow_dispatch: {}

concurrency:
group: server-examples-build-${{ github.ref }}
cancel-in-progress: true

jobs:
# Emits the project list as a matrix. A project qualifies if it has a
# package-lock.json AND a `build` script — that excludes express/server and
# nestjs/server, which are backends with neither a build step nor a
# Handsontable dependency.
discover:
runs-on: ubuntu-latest
outputs:
projects: ${{ steps.find.outputs.projects }}
steps:
- uses: actions/checkout@v4

- id: find
run: |
projects=$(find server-examples -maxdepth 3 -name package-lock.json \
-not -path '*/node_modules/*' -exec dirname {} \; \
| sort \
| while read -r d; do
# `if`, not `&&`: Actions runs `bash -e -o pipefail`, so the
# loop's exit status is the last iteration's. With `&&`, a
# project that sorts LAST and is legitimately skipped would
# fail this whole job. Today the last entry happens to have a
# build script; that is luck, not a guarantee.
if node -e "process.exit(require('./$d/package.json').scripts?.build ? 0 : 1)"; then
echo "$d"
fi
done \
| jq -R -s -c 'split("\n") | map(select(length > 0))')
Comment thread
cursor[bot] marked this conversation as resolved.
echo "projects=$projects" >> "$GITHUB_OUTPUT"
echo "Discovered: $projects"

build:
needs: discover
runs-on: ubuntu-latest
strategy:
# One broken project must not mask the state of the other 20 — the whole
# point of this workflow is to see the full blast radius of a type change.
fail-fast: false
matrix:
project: ${{ fromJSON(needs.discover.outputs.projects) }}
defaults:
run:
working-directory: ${{ matrix.project }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: ${{ matrix.project }}/package-lock.json

- run: npm ci

# Asserts, rather than reports: `npm ls` exits non-zero when the installed
# tree does not satisfy package.json. That is the one failure this job
# exists to catch and cannot see otherwise — a hand-edited or stale lock
# would still build fine against whatever it happened to resolve.
- name: Verify the lock resolves a satisfying Handsontable
run: npm ls handsontable --depth=0

- run: npm run build

build-latest:
# PRs get the deterministic job only. This one exists to detect upstream
# drift, which a PR cannot introduce.
if: github.event_name != 'pull_request'
needs: discover
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project: ${{ fromJSON(needs.discover.outputs.projects) }}
defaults:
run:
working-directory: ${{ matrix.project }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: ${{ matrix.project }}/package-lock.json

- run: npm ci

# The wrapper differs per project family (angular / react / none), so read
# it out of package.json rather than hard-coding three variants.
- name: Install handsontable@latest
run: |
wrapper=$(node -e "
const d = require('./package.json').dependencies || {};
const w = Object.keys(d).find(k => /^@handsontable\//.test(k));
process.stdout.write(w ? w + '@latest' : '');
")
echo "Installing handsontable@latest $wrapper"
npm install handsontable@latest $wrapper

- name: Report resolved Handsontable version
run: npm ls handsontable --depth=0 || true

- run: npm run build
12 changes: 6 additions & 6 deletions server-examples/django/frontend-angular/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 11 additions & 5 deletions server-examples/django/frontend-angular/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import {
ViewEncapsulation,
CUSTOM_ELEMENTS_SCHEMA,
} from '@angular/core';
import { HotTableModule, HotTableComponent } from '@handsontable/angular-wrapper';
import {
HotTableModule,
HotTableComponent,
type GridSettings,
} from '@handsontable/angular-wrapper';
import { registerAllModules } from 'handsontable/registry';
import type {
DataProviderQueryParameters,
Expand Down Expand Up @@ -62,7 +66,7 @@ export class AppComponent {

private removeConfirmed = false;

settings = {
settings: GridSettings = {
dataProvider: {
rowId: 'id',

Expand Down Expand Up @@ -129,7 +133,10 @@ export class AppComponent {
},
},

beforeRowsMutation: (operation: 'create' | 'update' | 'remove', payload: RowMutationPayload): false | void => {
// `operation` is typed `string` to match Handsontable's hook signature —
// narrowing it to the union it actually carries ('create' | 'update' |
// 'remove') is rejected contravariantly under strictFunctionTypes.
beforeRowsMutation: (operation: string, payload: RowMutationPayload): false | void => {
if (operation === 'remove' && !this.removeConfirmed) {
const { rowsRemove } = payload as RowMutationRemovePayload;
const hot = this.hotRef.hotInstance!;
Expand Down Expand Up @@ -170,8 +177,7 @@ export class AppComponent {
pagination: { pageSize: 10 },
columnSorting: true,
filters: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
dropdownMenu: ['filter_by_condition', 'filter_action_bar'] as any,
dropdownMenu: ['filter_by_condition', 'filter_action_bar'],
contextMenu: true,
emptyDataState: true,
notification: true,
Expand Down
12 changes: 6 additions & 6 deletions server-examples/django/frontend-react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 12 additions & 7 deletions server-examples/django/frontend-react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useRef, useMemo } from 'react';
import { HotTable, HotTableRef } from '@handsontable/react-wrapper';
import {
HotTable,
type HotTableProps,
type HotTableRef,
} from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import type {
DataProviderQueryParameters,
Expand Down Expand Up @@ -49,7 +53,7 @@ export default function App() {
const hotRef = useRef<HotTableRef>(null);
const removeConfirmedRef = useRef(false);

const settings = useMemo(() => ({
const settings = useMemo<HotTableProps>(() => ({
dataProvider: {
rowId: 'id',

Expand Down Expand Up @@ -116,7 +120,10 @@ export default function App() {
},
},

beforeRowsMutation: (operation: 'create' | 'update' | 'remove', payload: RowMutationPayload): false | void => {
// `operation` is typed `string` to match Handsontable's hook signature —
// narrowing it to the union it actually carries ('create' | 'update' |
// 'remove') is rejected contravariantly under strictFunctionTypes.
beforeRowsMutation: (operation: string, payload: RowMutationPayload): false | void => {
if (operation === 'remove' && !removeConfirmedRef.current) {
const { rowsRemove } = payload as RowMutationRemovePayload;
const hot = hotRef.current!.hotInstance!;
Expand Down Expand Up @@ -157,8 +164,7 @@ export default function App() {
pagination: { pageSize: 10 },
columnSorting: true,
filters: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
dropdownMenu: ['filter_by_condition', 'filter_action_bar'] as any,
dropdownMenu: ['filter_by_condition', 'filter_action_bar'],
contextMenu: true,
emptyDataState: true,
notification: true,
Expand Down Expand Up @@ -198,8 +204,7 @@ export default function App() {

<div id="example1">
{/* React wrapper spreads settings as individual props, not a 'settings' object */}
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<HotTable ref={hotRef} {...(settings as any)} />
<HotTable ref={hotRef} {...settings} />
</div>
</>
);
Expand Down
6 changes: 3 additions & 3 deletions server-examples/django/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions server-examples/express/client-angular/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions server-examples/express/client-angular/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import {
ViewEncapsulation,
CUSTOM_ELEMENTS_SCHEMA,
} from '@angular/core';
import { HotTableModule, HotTableComponent } from '@handsontable/angular-wrapper';
import {
HotTableModule,
HotTableComponent,
type GridSettings,
} from '@handsontable/angular-wrapper';
import { registerAllModules } from 'handsontable/registry';
import type {
DataProviderQueryParameters,
Expand Down Expand Up @@ -70,7 +74,7 @@ export class AppComponent {

constructor(private zone: NgZone) {}

settings = {
settings: GridSettings = {
dataProvider: {
rowId: 'id',

Expand Down Expand Up @@ -148,7 +152,10 @@ export class AppComponent {

// beforeRowsMutation is sync — show confirmation dialog, cancel original,
// re-issue after user confirms.
beforeRowsMutation: (operation: 'create' | 'update' | 'remove', payload: RowMutationPayload): false | void => {
// `operation` is typed `string` to match Handsontable's hook signature —
// narrowing it to the union it actually carries ('create' | 'update' |
// 'remove') is rejected contravariantly under strictFunctionTypes.
beforeRowsMutation: (operation: string, payload: RowMutationPayload): false | void => {
if (operation === 'remove' && !this.removeConfirmed) {
const { rowsRemove } = payload as RowMutationRemovePayload;
const hot = this.hotRef.hotInstance!;
Expand Down
12 changes: 6 additions & 6 deletions server-examples/express/client-react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading