From fda68d423c3380b14bb912265e0ec5740cadf309 Mon Sep 17 00:00:00 2001 From: lskramarov Date: Mon, 17 Aug 2026 15:33:00 +0300 Subject: [PATCH 1/4] chore(schematics): document the scrollbar-deprecated-path migration --- .../scrollbar-deprecated-path/README.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 packages/schematics/src/migrations/scrollbar-deprecated-path/README.md diff --git a/packages/schematics/src/migrations/scrollbar-deprecated-path/README.md b/packages/schematics/src/migrations/scrollbar-deprecated-path/README.md new file mode 100644 index 000000000..c28f95d87 --- /dev/null +++ b/packages/schematics/src/migrations/scrollbar-deprecated-path/README.md @@ -0,0 +1,128 @@ +# scrollbar-deprecated-path + +Migration schematic invoked automatically by `ng update @koobiq/components@20` +(registered for `20.3.0-0`). Rewrites `@koobiq/components/scrollbar` imports to +`@koobiq/components/scrollbar/deprecated`. + +## Background + +`@koobiq/components/scrollbar` now resolves to a new, dependency-free scrollbar built on +`CdkScrollable`. The previous implementation — the `overlayscrollbars`-based `KbqScrollbar` +component together with `KbqScrollbarDirective` — moved to +`@koobiq/components/scrollbar/deprecated` and will be removed in a future major version. + +Nothing about that implementation changed: `initializationTarget`, `options`, `events`, `defer`, +`scrollbarInstance`, the `onInitialize` / `onUpdate` / `onDestroy` / `onScroll` outputs and the +`kbq-scrollbar` element selector all behave exactly as before at the new path. Repointing the +import is the whole migration — adopting the new scrollbar is a separate, manual step. + +## Behaviour change + +The two entry points share only two exported names, so most unmigrated imports break loudly: +`KbqScrollbarModule`, `KbqScrollbarDirective`, `KBQ_SCROLLBAR_CONFIG`, +`KBQ_SCROLLBAR_OPTIONS_DEFAULT_CONFIG`, `KBQ_SCROLLBAR_OPTIONS_DEFAULT_CONFIG_PROVIDER`, +`KbqScrollbarEvents`, `KbqScrollbarEventListenerArgs` and `KbqScrollbarTarget` live at the +`/deprecated` path only, so the build stops with `TS2305: … has no exported member`. + +`KbqScrollbar` and `KbqScrollbarOptions` are the silent pair — both names exist at both paths: + +- `KbqScrollbar` keeps the `kbq-scrollbar` selector and `exportAs: 'kbqScrollbar'`, so a template + that only renders `` keeps compiling and keeps rendering — with the new + component, which has a different set of inputs. The `[kbq-scrollbar]` attribute form of the + selector is gone as well, so a host marked that way quietly matches nothing. +- `KbqScrollbarOptions` is now `{ mode: KbqScrollbarMode }` instead of the `overlayscrollbars` + `PartialOptions`. + +## What it does + +The schematic walks every `.ts` file under the project root — the whole workspace when `--project` +is omitted, which is how `ng update` invokes it — skipping `node_modules` and `dist`, and rewrites +the module specifier `@koobiq/components/scrollbar` to `@koobiq/components/scrollbar/deprecated`. + +The match is quote-anchored (`(['"])@koobiq/components/scrollbar\1`), so only the exact, bare +specifier is taken, in the quote style it was written in: + +| Specifier | Result | +| ------------------------------------------- | ------------------------------------------- | +| `'@koobiq/components/scrollbar'` | rewritten, still single-quoted | +| `"@koobiq/components/scrollbar"` | rewritten, still double-quoted | +| `'@koobiq/components/scrollbar/deprecated'` | untouched — the migration is idempotent | +| `'@koobiq/components/scrollbar-x'` | untouched — a sibling package, not a prefix | + +It is a textual replacement rather than an AST rewrite, so an `import`, an `export … from`, a +dynamic `import()` and a specifier handed to something like `jest.mock()` are all covered by the +same pass. + +## What it does _not_ do (manual) + +| Pattern | Manual migration | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A specifier outside a `.ts` file | Only `.ts` files are visited — the same string in a JSON config or a documentation snippet stays as written | +| `overlayscrollbars` in `package.json` | The `/deprecated` entry point still imports it. Adding it as a dependency is the `mandatory-peer-dependencies` migration's job | +| Moving off `/deprecated` | Nothing is migrated _to_ the new scrollbar: `options` / `events` / `defer` are replaced by `kbqScrollbarMode`, and the `overlayscrollbars` instance by the `scrollTo*` methods and `scrollChanges` | + +`fix` defaults to `true`. `ng update` invokes migrations with no options at all, and +`migrations.json` declares no schema, so the rule applies that default itself. With `--fix false` +every file that would change is logged instead of written, followed by the same count either way. + +[Params](schema.ts) + +Usage for Angular Cli: + +```shell +ng g @koobiq/components:scrollbar-deprecated-path --project +``` + +Usage for Nx: + +```shell +nx g @koobiq/components:scrollbar-deprecated-path --project +``` + +### Run locally + +Build package + +```shell +yarn run build:schematics +``` + +Run command (for example, for `koobiq-docs` project) + +```shell +ng g ./dist/components/schematics/collection.json:scrollbar-deprecated-path --project koobiq-docs +``` + +### Result + +#### Before + +```ts +import { Component } from '@angular/core'; +import { KbqScrollbarModule } from '@koobiq/components/scrollbar'; + +@Component({ + selector: 'my-page', + imports: [KbqScrollbarModule], + template: ` + ... + ` +}) +export class MyPage {} +``` + +#### After + +```ts +import { Component } from '@angular/core'; +import { KbqScrollbarModule } from '@koobiq/components/scrollbar/deprecated'; + +@Component({ + selector: 'my-page', + imports: [KbqScrollbarModule], + template: ` + ... + ` +}) +export class MyPage {} +``` From f829f58434943cdf180b4d19cdc2ad410c097561 Mon Sep 17 00:00:00 2001 From: lskramarov Date: Mon, 17 Aug 2026 15:48:00 +0300 Subject: [PATCH 2/4] fix(schematics): fail the build when copying package metadata fails --- .../schematics/scripts/copy-meta-to-dist.js | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/schematics/scripts/copy-meta-to-dist.js b/packages/schematics/scripts/copy-meta-to-dist.js index 2948353bb..9d9a695fd 100644 --- a/packages/schematics/scripts/copy-meta-to-dist.js +++ b/packages/schematics/scripts/copy-meta-to-dist.js @@ -3,6 +3,16 @@ const { resolve, join } = require('path'); const { getMigrations } = require('../src/utils/migrations'); const { statSync } = require('fs'); +/** Files every migration directory has to provide; they ship next to the compiled rule. */ +const MIGRATION_META_FILES = ['schema.json', 'README.md']; + +/** + * Everything that could not be copied. Collected rather than thrown so that one missing file + * neither hides the ones after it nor leaves the remaining copies undone — an aborted run used + * to publish a truncated `dist/components/schematics`. + */ +const failures = []; + const resolvePath = (...segments) => resolve(__dirname, ...segments); const ensureDirectoryExistence = async (filePath) => { @@ -17,8 +27,20 @@ const copyFileWrapper = async (src, dest) => { try { await copyFile(src, dest); } catch (error) { - console.error(`Failed to copy file from ${src} to ${dest}: ${error.message}`); - throw error; + failures.push(`Failed to copy file from ${src} to ${dest}: ${error.message}`); + } +}; + +/** Copies the meta files of one migration, naming the migration instead of reporting a bare ENOENT path. */ +const copyMigrationMeta = async (migration, migrationPath) => { + for (const file of MIGRATION_META_FILES) { + const src = resolvePath(`../src/migrations/${migration}/${file}`); + + if (statSync(src, { throwIfNoEntry: false })) { + await copyFileWrapper(src, join(migrationPath, file)); + } else { + failures.push(`No ${file} for the ${migration} migration`); + } } }; @@ -45,14 +67,8 @@ const init = async () => { await ensureDirectoryExistence(migrationPath); - await copyFileWrapper( - resolvePath(`../src/migrations/${migration}/schema.json`), - join(migrationPath, 'schema.json') - ); - await copyFileWrapper( - resolvePath(`../src/migrations/${migration}/README.md`), - join(migrationPath, 'README.md') - ); + await copyMigrationMeta(migration, migrationPath); + await copyFileWrapper(resolvePath(`../dist/migrations/${migration}/index.js`), join(migrationPath, 'index.js')); const optionalMigrationData = resolvePath(`../dist/migrations/${migration}/data.js`); const fileExists = statSync(optionalMigrationData, { throwIfNoEntry: false }); @@ -76,6 +92,15 @@ const init = async () => { await copyFileWrapper(resolvePath('../dist/utils/typescript.js'), join(utilsPath, 'typescript.js')); await copyFileWrapper(resolvePath('../dist/utils/ast.js'), join(utilsPath, 'ast.js')); await copyFileWrapper(resolvePath('../dist/utils/angular-parsing.js'), join(utilsPath, 'angular-parsing.js')); + + if (failures.length > 0) { + throw new Error([`${failures.length} file(s) missing from the package:`, ...failures].join('\n ')); + } }; -init().catch((error) => console.error(`Failed to initialize directories and copy files: ${error.message}`)); +init().catch((error) => { + console.error(`Failed to initialize directories and copy files: ${error.message}`); + // Without this the rejection is swallowed, the build stays green, and an incomplete + // dist/components/schematics gets published. + process.exitCode = 1; +}); From a8b1ec1b4152d6167d42c783e74996cbb80c8805 Mon Sep 17 00:00:00 2001 From: lskramarov Date: Mon, 17 Aug 2026 19:33:56 +0300 Subject: [PATCH 3/4] chore(schematics): fix cspell error in the scrollbar-deprecated-path README --- .../src/migrations/scrollbar-deprecated-path/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/schematics/src/migrations/scrollbar-deprecated-path/README.md b/packages/schematics/src/migrations/scrollbar-deprecated-path/README.md index c28f95d87..61ce13e38 100644 --- a/packages/schematics/src/migrations/scrollbar-deprecated-path/README.md +++ b/packages/schematics/src/migrations/scrollbar-deprecated-path/README.md @@ -13,7 +13,7 @@ component together with `KbqScrollbarDirective` — moved to Nothing about that implementation changed: `initializationTarget`, `options`, `events`, `defer`, `scrollbarInstance`, the `onInitialize` / `onUpdate` / `onDestroy` / `onScroll` outputs and the -`kbq-scrollbar` element selector all behave exactly as before at the new path. Repointing the +`kbq-scrollbar` element selector all behave exactly as before at the new path. Rewriting the import is the whole migration — adopting the new scrollbar is a separate, manual step. ## Behaviour change From c3fc9bd5b506568c18062261e3df4e7a8b34cc26 Mon Sep 17 00:00:00 2001 From: lskramarov Date: Tue, 18 Aug 2026 12:40:41 +0300 Subject: [PATCH 4/4] chore(schematics): mark the scrollbar-x specifier in the README as hypothetical --- .../migrations/scrollbar-deprecated-path/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/schematics/src/migrations/scrollbar-deprecated-path/README.md b/packages/schematics/src/migrations/scrollbar-deprecated-path/README.md index 61ce13e38..ec573e061 100644 --- a/packages/schematics/src/migrations/scrollbar-deprecated-path/README.md +++ b/packages/schematics/src/migrations/scrollbar-deprecated-path/README.md @@ -42,12 +42,12 @@ the module specifier `@koobiq/components/scrollbar` to `@koobiq/components/scrol The match is quote-anchored (`(['"])@koobiq/components/scrollbar\1`), so only the exact, bare specifier is taken, in the quote style it was written in: -| Specifier | Result | -| ------------------------------------------- | ------------------------------------------- | -| `'@koobiq/components/scrollbar'` | rewritten, still single-quoted | -| `"@koobiq/components/scrollbar"` | rewritten, still double-quoted | -| `'@koobiq/components/scrollbar/deprecated'` | untouched — the migration is idempotent | -| `'@koobiq/components/scrollbar-x'` | untouched — a sibling package, not a prefix | +| Specifier | Result | +| ------------------------------------------------- | -------------------------------------------------------------------------------------- | +| `'@koobiq/components/scrollbar'` | rewritten, still single-quoted | +| `"@koobiq/components/scrollbar"` | rewritten, still double-quoted | +| `'@koobiq/components/scrollbar/deprecated'` | untouched — the migration is idempotent | +| `'@koobiq/components/scrollbar-x'` (hypothetical) | untouched — the closing quote is required, so a longer specifier is not a prefix match | It is a textual replacement rather than an AST rewrite, so an `import`, an `export … from`, a dynamic `import()` and a specifier handed to something like `jest.mock()` are all covered by the