AAE-45391 Lazy loading and improvements for pdfJS (#11869)

* [AAE-45391] - Lazy loading some of the big external libs

* [AAE-45391] - Added some extra units to pdfjs viewer

* [AAE-45391] - Some improement on viewer component

* [AAE-45391] - Fixed the way it was redered to a newer angular system

* [AAE-45391] - Fixed the way it was redered to a newer angular system

* [AAE-45391] - fixed schematic

* [AAE-45391] - Added some docs for the schematics as well

* [AAE-37328] - Fixed comment on missing provider
This commit is contained in:
Vito Albano
2026-05-11 14:17:39 +01:00
committed by GitHub
parent 62fb51e910
commit 9fe4006a0d
42 changed files with 939 additions and 65 deletions
@@ -0,0 +1,91 @@
# Migration Schematic: migrate-pdf-viewer-imports (v9.0.0)
Automatically migrates consumer projects from the monolithic `@alfresco/adf-core` PDF viewer to the new `@alfresco/adf-core/viewer/pdf` secondary entrypoint.
## What it does
### 1. Rewrites imports
Moves PDF-related symbols from `@alfresco/adf-core` to `@alfresco/adf-core/viewer/pdf`:
**Before:**
```typescript
import { AppConfigService, PdfViewerComponent, RenderingQueueServices } from '@alfresco/adf-core';
```
**After:**
```typescript
import { AppConfigService } from '@alfresco/adf-core';
import { PdfViewerComponent, RenderingQueueServices } from '@alfresco/adf-core/viewer/pdf';
```
Affected symbols:
- `PdfViewerComponent`
- `PdfPasswordDialogComponent`
- `PdfThumbListComponent`
- `PdfThumbComponent`
- `PDFJS_MODULE`
- `PDFJS_VIEWER_MODULE`
- `RenderingQueueServices`
### 2. Adds `providePdfViewer()` to app providers
The PDF viewer now requires explicit registration via the `providePdfViewer()` provider function. The schematic automatically adds it to the consumer's application configuration.
**Standalone app (`app.config.ts`):**
```typescript
import { providePdfViewer } from '@alfresco/adf-core/viewer/pdf';
export const appConfig: ApplicationConfig = {
providers: [providePdfViewer(), /* ...existing providers */]
};
```
**NgModule app (`app.module.ts`):**
```typescript
import { providePdfViewer } from '@alfresco/adf-core/viewer/pdf';
@NgModule({
providers: [providePdfViewer()]
})
export class AppModule {}
```
## How it runs
Triggered automatically via `ng update @alfresco/adf-core` when upgrading to v9.0.0+.
Can also be run manually:
```bash
ng generate @alfresco/adf-core:migrate-pdf-viewer-imports
```
## File discovery
The schematic scans all `.ts` files in the project (excluding `node_modules`, `.git`, `.angular`, `.nxcache`) and processes any file containing one of the PDF symbols listed above.
For provider injection, it searches for the app config file in this order:
1. `src/app/app.config.ts`
2. `src/app/app.module.ts`
3. `src/main.ts`
4. Falls back to scanning for any file containing `bootstrapApplication` or `ApplicationConfig`
## Edge cases handled
| Scenario | Behavior |
|----------|----------|
| Existing `@alfresco/adf-core/viewer/pdf` import | Merges moved symbols into it (deduplicates) |
| All symbols in the core import are PDF symbols | Removes the entire `@alfresco/adf-core` import |
| `providePdfViewer` already present in the project | Skips provider injection |
| No app config file found | Skips provider injection (consumer must add manually) |
| File has no PDF symbols | Skipped entirely (no modifications) |
| Empty providers array | Inserts `providePdfViewer()` as the first element |
## Why this migration exists
In v9.0.0, the PDF viewer was extracted into a secondary entrypoint (`@alfresco/adf-core/viewer/pdf`) so that:
- `pdfjs-dist` becomes an optional peer dependency
- Applications that don't use PDF viewing avoid bundling ~500KB of PDF processing code
- The primary `@alfresco/adf-core` bundle stays lean
Without this schematic, consumers upgrading to v9 would see PDF files rendered as "unknown format" because the `PDF_VIEWER_COMPONENT` injection token defaults to `null` when `providePdfViewer()` is not called.
@@ -0,0 +1,274 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as ts from 'typescript';
import { Rule, Tree } from '@angular-devkit/schematics';
const PDF_SYMBOLS = [
'PdfViewerComponent',
'PdfPasswordDialogComponent',
'PdfThumbListComponent',
'PdfThumbComponent',
'PDFJS_MODULE',
'PDFJS_VIEWER_MODULE',
'RenderingQueueServices'
];
const OLD_SOURCE = '@alfresco/adf-core';
const NEW_SOURCE = '@alfresco/adf-core/viewer/pdf';
/**
* @returns Schematic rule for migrating PDF viewer imports to the secondary entrypoint
*/
export function migratePdfViewerImports(): Rule {
return (tree: Tree) => {
let providerAdded = false;
tree.visit((filePath: string) => {
if (
!filePath.includes('/.git/') &&
!filePath.includes('/node_modules/') &&
!filePath.includes('/.angular/') &&
!filePath.includes('/.nxcache/') &&
/\.ts$/.test(filePath)
) {
const bufferContent = tree.read(filePath);
if (!bufferContent) {
return;
}
const fileContent = bufferContent.toString();
if (PDF_SYMBOLS.some((sym) => fileContent.includes(sym))) {
const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true);
const updatedContent = movePdfImports(sourceFile, fileContent);
if (updatedContent !== fileContent) {
tree.overwrite(filePath, updatedContent);
}
}
if (!providerAdded && fileContent.includes('providePdfViewer')) {
providerAdded = true;
}
}
});
if (!providerAdded) {
addProvidePdfViewerToAppConfig(tree);
}
return tree;
};
}
/**
* @param sourceFile - the parsed TypeScript source file
* @param fileContent - the raw file content string
* @returns updated file content with PDF imports moved to the secondary entrypoint
*/
function movePdfImports(sourceFile: ts.SourceFile, fileContent: string): string {
const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration);
const coreImport = importDeclarations.find((decl) => {
const moduleSpecifier = decl.moduleSpecifier.getText().replace(/['"]/g, '');
return moduleSpecifier === OLD_SOURCE;
});
if (!coreImport?.importClause?.namedBindings || !ts.isNamedImports(coreImport.importClause.namedBindings)) {
return fileContent;
}
const namedImports = coreImport.importClause.namedBindings.elements;
const pdfImports = namedImports.filter((el) => PDF_SYMBOLS.includes(el.name.text));
const remainingImports = namedImports.filter((el) => !PDF_SYMBOLS.includes(el.name.text));
if (pdfImports.length === 0) {
return fileContent;
}
const pdfImportNames = pdfImports.map((el) => el.name.text);
const existingNewImport = importDeclarations.find((decl) => decl.moduleSpecifier.getText().replace(/['"]/g, '') === NEW_SOURCE);
const existingNewImportNames: string[] = [];
if (existingNewImport?.importClause?.namedBindings && ts.isNamedImports(existingNewImport.importClause.namedBindings)) {
existingNewImportNames.push(...existingNewImport.importClause.namedBindings.elements.map((el) => el.name.text));
}
const mergedNames = [...new Set([...existingNewImportNames, ...pdfImportNames])];
const mergedImportStatement = `import { ${mergedNames.join(', ')} } from '${NEW_SOURCE}';`;
const edits: { start: number; end: number; replacement: string }[] = [];
if (remainingImports.length === 0) {
edits.push({ start: coreImport.getFullStart(), end: coreImport.getEnd(), replacement: '' });
} else {
const remainingNames = remainingImports.map((el) => el.getText()).join(', ');
const updatedImport = `import { ${remainingNames} } from '${OLD_SOURCE}';`;
edits.push({ start: coreImport.getStart(), end: coreImport.getEnd(), replacement: updatedImport });
}
if (existingNewImport) {
edits.push({ start: existingNewImport.getStart(), end: existingNewImport.getEnd(), replacement: mergedImportStatement });
}
edits.sort((a, b) => b.start - a.start);
let updatedContent = fileContent;
for (const edit of edits) {
updatedContent = updatedContent.slice(0, edit.start) + edit.replacement + updatedContent.slice(edit.end);
}
if (!existingNewImport) {
const updatedSource = ts.createSourceFile('temp.ts', updatedContent, ts.ScriptTarget.Latest, true);
const firstNonImport = updatedSource.statements.find((stmt) => !ts.isImportDeclaration(stmt));
const insertPos = firstNonImport ? firstNonImport.getFullStart() : updatedContent.length;
updatedContent = updatedContent.slice(0, insertPos).trimEnd() + '\n' + mergedImportStatement + '\n' + updatedContent.slice(insertPos);
}
return updatedContent.replace(/\n{3,}/g, '\n\n');
}
/**
* @param tree - the schematic file tree
*/
function addProvidePdfViewerToAppConfig(tree: Tree): void {
const candidates = ['src/app/app.config.ts', 'src/app/app.module.ts', 'src/main.ts'];
let targetPath: string | null = null;
for (const candidate of candidates) {
if (tree.exists(`/${candidate}`)) {
targetPath = `/${candidate}`;
break;
}
}
if (!targetPath) {
tree.visit((filePath: string) => {
if (targetPath) {
return;
}
if (!filePath.includes('/node_modules/') && !filePath.includes('/.git/') && /\.ts$/.test(filePath)) {
const content = tree.read(filePath)?.toString() ?? '';
if (content.includes('bootstrapApplication') || content.includes('ApplicationConfig')) {
targetPath = filePath;
}
}
});
}
if (!targetPath) {
return;
}
const buffer = tree.read(targetPath);
if (!buffer) {
return;
}
const content = buffer.toString();
if (content.includes('providePdfViewer')) {
return;
}
const sourceFile = ts.createSourceFile(targetPath, content, ts.ScriptTarget.Latest, true);
const result = insertProvidePdfViewer(sourceFile, content);
if (result !== content) {
tree.overwrite(targetPath, result);
}
}
/**
* @param sourceFile - the parsed TypeScript source file
* @param content - the raw file content
* @returns updated content with providePdfViewer() added to providers
*/
function insertProvidePdfViewer(sourceFile: ts.SourceFile, content: string): string {
const providersArray = findProvidersArray(sourceFile);
if (!providersArray) {
return content;
}
const lastElement = providersArray.elements[providersArray.elements.length - 1];
let insertPos: number;
let prefix: string;
if (lastElement) {
insertPos = lastElement.getEnd();
prefix = ', providePdfViewer()';
} else {
insertPos = providersArray.getStart() + 1;
prefix = 'providePdfViewer()';
}
let updatedContent = content.slice(0, insertPos) + prefix + content.slice(insertPos);
const importStatement = `import { providePdfViewer } from '${NEW_SOURCE}';\n`;
const existingPdfImport = sourceFile.statements
.filter(ts.isImportDeclaration)
.find((decl) => decl.moduleSpecifier.getText().replace(/['"]/g, '') === NEW_SOURCE);
if (existingPdfImport) {
if (!content.includes('providePdfViewer')) {
const updatedSource = ts.createSourceFile('temp.ts', updatedContent, ts.ScriptTarget.Latest, true);
const existingDecl = updatedSource.statements
.filter(ts.isImportDeclaration)
.find((decl) => decl.moduleSpecifier.getText().replace(/['"]/g, '') === NEW_SOURCE);
if (existingDecl?.importClause?.namedBindings && ts.isNamedImports(existingDecl.importClause.namedBindings)) {
const names = existingDecl.importClause.namedBindings.elements.map((el) => el.name.text);
if (!names.includes('providePdfViewer')) {
names.push('providePdfViewer');
const newImport = `import { ${names.join(', ')} } from '${NEW_SOURCE}';`;
updatedContent = updatedContent.slice(0, existingDecl.getStart()) + newImport + updatedContent.slice(existingDecl.getEnd());
}
}
}
} else {
const firstImport = sourceFile.statements.find(ts.isImportDeclaration);
const importInsertPos = firstImport ? firstImport.getFullStart() : 0;
updatedContent = updatedContent.slice(0, importInsertPos) + importStatement + updatedContent.slice(importInsertPos);
}
return updatedContent;
}
/**
* @param sourceFile - the TypeScript source file to search
* @returns the providers ArrayLiteralExpression if found
*/
function findProvidersArray(sourceFile: ts.SourceFile): ts.ArrayLiteralExpression | null {
let result: ts.ArrayLiteralExpression | null = null;
const visit = (node: ts.Node): void => {
if (result) {
return;
}
if (ts.isPropertyAssignment(node) && node.name.getText() === 'providers' && ts.isArrayLiteralExpression(node.initializer)) {
result = node.initializer;
return;
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return result;
}
export default migratePdfViewerImports;
@@ -5,6 +5,11 @@
"description": "Update alfresco-api imports",
"version": "7.0.0",
"factory": "./7_0_0/index#updateAlfrescoApiImports"
},
"migrate-pdf-viewer-imports": {
"description": "Move PDF viewer imports to @alfresco/adf-core/viewer/pdf secondary entrypoint",
"version": "9.0.0",
"factory": "./9_0_0/index#migratePdfViewerImports"
}
},
"packageJsonUpdates": {