UNPKG

ngx-extended-pdf-viewer

Version:

Embedding PDF files in your Angular application. Highly configurable viewer including the toolbar, sidebar, and all the features you're used to.

654 lines 31.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.findModule = exports.updateAngularJsonRule = exports.ngAdd = exports.addToModule = exports.getSourceFile = void 0; const core_1 = require("@angular-devkit/core"); const schematics_1 = require("@angular-devkit/schematics"); const tasks_1 = require("@angular-devkit/schematics/tasks"); const change_1 = require("@schematics/angular/utility/change"); const find_module_1 = require("@schematics/angular/utility/find-module"); const ts = require("typescript"); const ast_utils_1 = require("./ast-utils"); /** * Detects the Angular version from package.json */ function detectAngularVersion(tree) { const packageJsonBuffer = tree.read('/package.json'); if (!packageJsonBuffer) { return null; } const packageJson = JSON.parse(packageJsonBuffer.toString()); const angularCore = packageJson.dependencies?.['@angular/core'] || packageJson.devDependencies?.['@angular/core'] || packageJson.peerDependencies?.['@angular/core']; if (angularCore) { // Extract version number from version string (e.g., "^17.0.0", "~16.2.5", "18.0.0-rc.0") const versionMatch = angularCore.match(/(\d+)\.(\d+)\.(\d+)/); if (versionMatch) { return { major: parseInt(versionMatch[1], 10), minor: parseInt(versionMatch[2], 10), patch: parseInt(versionMatch[3], 10), }; } } return null; } /** * Checks if the Angular version is supported */ function isSupportedAngularVersion(version) { if (!version) { console.log('Warning: Could not detect Angular version. Proceeding with default configuration.'); return true; } const isSupported = version.major >= 17 && version.major <= 20; if (!isSupported) { console.log(`Warning: Angular ${version.major}.${version.minor}.${version.patch} may not be fully supported.`); console.log('Supported Angular versions: 17.x - 20.x'); } return true; // Still proceed, but with warning } /** * Reads file given path and returns TypeScript source file. */ function getSourceFile(host, path) { const buffer = host.read(path); if (!buffer) { throw new schematics_1.SchematicsException(`Could not find file for path: ${path}`); } const content = buffer.toString(); const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); return source; } exports.getSourceFile = getSourceFile; /** * Import and add module to specific module path. */ function addToModule(host, modulePath, moduleName, src) { const moduleSource = getSourceFile(host, modulePath); const changes = (0, ast_utils_1.addImportToModule)(moduleSource, modulePath, moduleName, src); const recorder = host.beginUpdate(modulePath); changes.forEach((change) => { if (change instanceof change_1.InsertChange) { recorder.insertLeft(change.pos, change.toAdd); } }); host.commitUpdate(recorder); } exports.addToModule = addToModule; function ngAdd(options) { return (tree, context) => { // Detect and validate Angular version const angularVersion = detectAngularVersion(tree); isSupportedAngularVersion(angularVersion); // Load workspace config to get default project const workspaceConfig = loadWorkspaceConfig(tree); let projectName = options.project; if (!projectName || projectName === '') { projectName = options.defaultProject || workspaceConfig.defaultProject; } if (!projectName) { const availableProjects = Object.keys(workspaceConfig.projects); throw new schematics_1.SchematicsException(`No project specified and no default project found. Available projects: ${availableProjects.join(', ')}`); } // Set the resolved project name back to options for downstream code options.project = projectName; // Validate project configuration const workspaceConfigForValidation = validateProjectConfiguration(tree, projectName); const project = getProjectConfig(workspaceConfigForValidation, projectName); // Check if this is a library vs application (prefer projectType over path heuristics) const isLibrary = project.projectType === 'library' || (project.projectType !== 'application' && project.root && (project.root.startsWith('libs/') || project.root.includes('/libs/'))); // For applications, verify it has a browser/application build target let hasBrowserBuild = false; if (!isLibrary) { const buildConfig = project.architect || project.targets; if (buildConfig) { const browserExecutors = [ '@angular-devkit/build-angular:browser', '@angular-devkit/build-angular:application', '@nrwl/angular:webpack-browser', '@nx/angular:webpack-browser', '@nx/angular:application', '@nx/angular:browser', '@nx/vite:build', '@nrwl/vite:build', ]; hasBrowserBuild = Object.values(buildConfig).some((target) => target.executor && browserExecutors.some((executor) => target.executor.includes(executor))); } } // Set defaults only if not already specified by user if (!options.path) { options.path = isLibrary ? 'lib/example-pdf-viewer' : 'app/example-pdf-viewer'; } if (!options.name) { options.name = 'example-pdf-viewer'; } if (options.skipImport === undefined) { options.skipImport = false; } const stable = options.stable; const exampleComponent = options.exampleComponent; // Schedule package installation - this is the main purpose of ng-add context.addTask(new tasks_1.NodePackageInstallTask()); if (exampleComponent && !isLibrary) { // Only generate example components for applications const folder = detectProjectStructure(tree, projectName, options.defaultProject); return (0, schematics_1.chain)([addDeclarationToNgModule(options), generateExampleComponent(folder, stable, options), updateAngularJsonRule(projectName, stable)]); } else if (exampleComponent && isLibrary) { console.log(`Skipping example component generation for library project "${projectName}".`); console.log("Libraries typically don't need example PDF viewer components."); return tree; // No asset configuration for libraries } // For applications with browser build or libraries, update configuration if (!isLibrary && hasBrowserBuild) { return (0, schematics_1.chain)([updateAngularJsonRule(projectName, stable)]); } else if (isLibrary) { console.log(`Skipping asset configuration for library project "${projectName}".`); console.log("Libraries don't typically need browser assets configured."); return tree; // No configuration changes for libraries } else { console.log(`Warning: Project "${projectName}" doesn't appear to have a browser build target.`); console.log('Assets may not be configured correctly. You may need to manually add assets to your build configuration.'); return tree; } }; } exports.ngAdd = ngAdd; /** * Detects the project structure to determine the correct source folder * Handles Angular CLI, Nx, and hybrid project structures */ function detectProjectStructure(tree, projectName, defaultProject) { const workspaceConfig = loadWorkspaceConfig(tree); const project = getProjectConfig(workspaceConfig, projectName); // Use sourceRoot if available (common in Nx) if (project.sourceRoot) { return `/${project.sourceRoot}`; } // Use root + src if available if (project.root) { const srcPath = `/${project.root}/src`; if (tree.exists(srcPath)) { return srcPath; } return `/${project.root}`; } // Fallback to Angular CLI-style detection const isDefaultProject = projectName === defaultProject; if (isDefaultProject) { // For root project, assume standard src structure return '/src'; } else { // Try different common project paths const possiblePaths = [ `/apps/${projectName}/src`, `/libs/${projectName}/src`, `/projects/${projectName}/src`, `/apps/${projectName}`, `/libs/${projectName}`, `/projects/${projectName}`, // Angular CLI without src ]; for (const path of possiblePaths) { if (tree.exists(path)) { return path; } } // Last resort - assume standard structure return `/projects/${projectName}/src`; } } /** * Detects workspace type and loads configuration (Angular CLI, Nx, or hybrid) */ function loadWorkspaceConfig(tree) { // Try angular.json first (Angular CLI or migrated Nx) let configBuffer = tree.read('/angular.json'); if (configBuffer) { const config = JSON.parse(configBuffer.toString()); const projects = config.projects || {}; // If angular.json exists but has no projects, continue to Nx scan // (common in modern Nx hybrids where projects live in project.json files) if (Object.keys(projects).length > 0) { return { projects, type: 'angular', configPath: '/angular.json', defaultProject: config.defaultProject, }; } } // Try workspace.json (older Nx) configBuffer = tree.read('/workspace.json'); if (configBuffer) { const config = JSON.parse(configBuffer.toString()); return { projects: config.projects || {}, type: 'nx', configPath: '/workspace.json', defaultProject: config.defaultProject, }; } // Try nx.json with project.json files (modern Nx) const nxJsonBuffer = tree.read('/nx.json'); if (nxJsonBuffer) { const nxConfig = JSON.parse(nxJsonBuffer.toString()); const projects = {}; // Scan for project.json files const projectDirs = ['/apps', '/libs', '/projects']; for (const dir of projectDirs) { if (tree.exists(dir)) { const dirEntry = tree.getDir(dir); dirEntry.subdirs.forEach((subdir) => { const projectJsonPath = `${dir}/${subdir}/project.json`; if (tree.exists(projectJsonPath)) { const projectBuffer = tree.read(projectJsonPath); if (projectBuffer) { const projectConfig = JSON.parse(projectBuffer.toString()); // Use project name from config, fall back to folder name const projectName = projectConfig.name || subdir; projects[projectName] = { ...projectConfig, root: `${dir}/${subdir}`.substring(1), sourceRoot: `${dir}/${subdir}/src`.substring(1), }; } } }); } } return { projects, type: 'nx', configPath: '/nx.json', defaultProject: nxConfig.defaultProject, }; } throw new schematics_1.SchematicsException('Could not find workspace configuration (angular.json, workspace.json, or nx.json). This might not be an Angular or Nx project.'); } /** * Gets project configuration from workspace, handling both Angular CLI and Nx */ function getProjectConfig(workspaceConfig, projectName) { const project = workspaceConfig.projects[projectName]; if (!project) { const availableProjects = Object.keys(workspaceConfig.projects); throw new schematics_1.SchematicsException(`Project "${projectName}" not found in ${workspaceConfig.configPath}. Available projects: ${availableProjects.join(', ')}`); } return project; } /** * Validates the project configuration before proceeding */ function validateProjectConfiguration(tree, projectName) { const workspaceConfig = loadWorkspaceConfig(tree); const project = getProjectConfig(workspaceConfig, projectName); // Check for build configuration const buildConfig = project.architect || project.targets; if (!buildConfig) { throw new schematics_1.SchematicsException(`No build configuration found for project "${projectName}". The project might be misconfigured.`); } return workspaceConfig; } function updateAngularJsonRule(projectName, stable) { return (tree, _context) => { return updateAngularJson(tree, projectName, stable); }; } exports.updateAngularJsonRule = updateAngularJsonRule; function updateAngularJson(tree, projectName, stable) { const workspaceConfig = loadWorkspaceConfig(tree); const project = getProjectConfig(workspaceConfig, projectName); let updated = false; // Handle both "architect" (Angular 6+) and "targets" (older versions) configurations const buildConfig = project.architect || project.targets; if (!buildConfig) { throw new schematics_1.SchematicsException(`No build configuration found for project "${projectName}".`); } // Helper function to normalize paths by removing trailing slashes const normalizePath = (path) => path.replace(/\/+$/, ''); // Helper function to add assets to a target's options const addAssetsToTarget = (target, targetName) => { if (!target?.options) { console.log(`No options found for ${targetName} target in project ${projectName}`); return false; } if (!target.options.assets) { target.options.assets = []; } // Detect Angular version to determine correct assets source folder const angularVersion = detectAngularVersion(tree); console.log('Angular version', angularVersion); const usePublicFolder = angularVersion && angularVersion.major >= 17; console.log('Use public folder:', usePublicFolder); // Prepare normalized asset paths const inputPath = stable ? 'node_modules/ngx-extended-pdf-viewer/assets/' : 'node_modules/ngx-extended-pdf-viewer/bleeding-edge/'; let assetsSourcePath; if (stable) { assetsSourcePath = 'assets'; } else { assetsSourcePath = 'bleeding-edge'; } const normalizedInputPath = normalizePath(inputPath); // Check if assets already exist to avoid duplicates (with path normalization) const existingAsset = target.options.assets.find((asset) => { // Handle object-form assets if (typeof asset === 'object' && asset !== null && asset.input !== null && asset.input !== undefined) { return normalizePath(asset.input) === normalizedInputPath; } // Handle string-form assets (though less common for this use case) if (typeof asset === 'string') { return normalizePath(asset) === normalizedInputPath; } return false; }); if (!existingAsset) { target.options.assets.push({ glob: '**/*', input: inputPath, output: assetsSourcePath, }); console.log(`Added assets to ${targetName} target in project ${projectName}`); return true; } else { console.log(`Assets already exist in ${targetName} target for project ${projectName}`); return false; } }; // Prioritize executor detection over name heuristics const buildExecutors = [ '@angular-devkit/build-angular:browser', '@angular-devkit/build-angular:application', '@nrwl/angular:webpack-browser', '@nx/angular:webpack-browser', '@nx/angular:application', '@nx/angular:browser', '@nx/vite:build', '@nrwl/vite:build', ]; // First, try to find a build target by executor let foundBuildTarget = false; for (const [targetName, target] of Object.entries(buildConfig)) { const targetConfig = target; if (targetConfig.executor && buildExecutors.some((executor) => targetConfig.executor.includes(executor))) { if (addAssetsToTarget(targetConfig, targetName)) { updated = true; foundBuildTarget = true; } break; // Only update one build target } } // If no executor-based target found, fall back to name heuristics (only build targets) if (!foundBuildTarget) { const buildTargetNames = ['build', 'application', 'esbuild', 'webpack', 'build-angular']; for (const targetName of buildTargetNames) { if (buildConfig[targetName]) { if (addAssetsToTarget(buildConfig[targetName], targetName)) { updated = true; } break; // Only update one build target } } } // If no suitable targets found, provide helpful error message if (!updated) { const availableTargets = buildConfig ? Object.keys(buildConfig) : []; console.log("Couldn't find suitable build target in workspace config"); console.log(`Available targets for project ${projectName}: ${availableTargets.join(', ')}`); console.log('Please manually add the following to your workspace config assets array:'); console.log('{'); console.log(" glob: '**/*',"); if (stable) { const angularVersion = detectAngularVersion(tree); const usePublicFolder = angularVersion && angularVersion.major >= 17; const outputPath = usePublicFolder ? 'public' : 'assets'; console.log(" input: 'node_modules/ngx-extended-pdf-viewer/assets/',"); console.log(` output: '${outputPath}',`); } else { console.log(" input: 'node_modules/ngx-extended-pdf-viewer/bleeding-edge/',"); console.log(" output: 'bleeding-edge',"); } console.log('}'); return tree; } // Write back the updated configuration if (workspaceConfig.type === 'nx' && workspaceConfig.configPath === '/nx.json') { // For modern Nx with project.json files, update the individual project.json const projectJsonPath = `/${project.root}/project.json`; if (tree.exists(projectJsonPath)) { const updatedProjectJson = JSON.stringify(project, null, 2); tree.overwrite(projectJsonPath, updatedProjectJson); } } else { // For angular.json or workspace.json const configBuffer = tree.read(workspaceConfig.configPath); if (configBuffer) { const config = JSON.parse(configBuffer.toString()); config.projects[projectName] = project; const updatedConfig = JSON.stringify(config, null, 2); tree.overwrite(workspaceConfig.configPath, updatedConfig); } } return tree; } function generateExampleComponent(folder, stable, options) { return (tree, context) => { context.logger.info('=== generateExampleComponent called ==='); // Detect Angular version and check folder existence const angularVersion = detectAngularVersion(tree); const usePublicFolder = angularVersion && angularVersion.major >= 17; context.logger.info(`Angular version: ${JSON.stringify(angularVersion)}`); context.logger.info(`Use public folder: ${usePublicFolder}`); // Get project configuration const workspaceConfig = loadWorkspaceConfig(tree); const project = getProjectConfig(workspaceConfig, options.project); const projectRoot = project.root || ''; context.logger.info(`Project root: ${projectRoot}`); // Check if assets or public folder exists const assetsPath = `${projectRoot ? projectRoot + '/' : ''}src/assets`; const publicPath = `${projectRoot ? projectRoot + '/' : ''}public`; context.logger.info('Checking paths:'); context.logger.info(` assetsPath: ${assetsPath} -> exists: ${tree.exists(assetsPath)}`); context.logger.info(` publicPath: ${publicPath} -> exists: ${tree.exists(publicPath)}`); let targetAssetsFolder; if (tree.exists(assetsPath)) { targetAssetsFolder = assetsPath; context.logger.info(`✓ Using assets folder (${assetsPath})`); } else if (usePublicFolder && tree.exists(publicPath)) { targetAssetsFolder = publicPath; context.logger.info(`✓ Using public folder (${publicPath})`); } else { // Fallback: create the appropriate folder targetAssetsFolder = usePublicFolder ? publicPath : assetsPath; context.logger.info(`✓ Fallback: creating ${targetAssetsFolder} folder`); } context.logger.info(`Final target folder: ${targetAssetsFolder}`); // Determine the correct PDF path for the component template const pdfPath = targetAssetsFolder === publicPath ? '' : '/assets'; context.logger.info(`PDF path for component template: "${pdfPath}"`); // Create two template sources - one for components, one for assets context.logger.info('Creating component template source...'); const componentTemplateSource = (0, schematics_1.apply)((0, schematics_1.url)('./files'), [ (0, schematics_1.applyTemplates)({ classify: core_1.strings.classify, dasherize: core_1.strings.dasherize, stable, standalone: options.standalone, pdfPath: pdfPath, }), // Filter out the assets folder from component templates (templateTree) => { context.logger.info('Component template - filtering assets...'); const filesBefore = []; templateTree.visit(path => filesBefore.push(path)); context.logger.info(`Template files before filter: ${filesBefore.join(', ')}`); // Delete all assets files/folders from component template const assetsToDelete = []; templateTree.visit((path) => { if (path.startsWith('/assets')) { assetsToDelete.push(path); } }); assetsToDelete.forEach(path => { context.logger.info(`Deleting asset from component template: ${path}`); templateTree.delete(path); }); const filesAfter = []; templateTree.visit(path => filesAfter.push(path)); context.logger.info(`Template files after filter: ${filesAfter.join(', ')}`); return templateTree; }, (0, schematics_1.move)((0, core_1.normalize)(folder)), ]); context.logger.info('Creating assets template source...'); // Separate template source for assets that copies to the correct target folder const assetsTemplateSource = (0, schematics_1.apply)((0, schematics_1.url)('./files'), [ // Filter to only include assets (templateTree) => { context.logger.info('Assets template - processing...'); const filesBefore = []; templateTree.visit(path => filesBefore.push(path)); context.logger.info(`Template files before asset filter: ${filesBefore.join(', ')}`); const assetsDir = templateTree.getDir('/assets'); if (assetsDir) { context.logger.info('Found assets directory, keeping only assets...'); // Keep only assets, remove everything else templateTree.visit((path) => { if (!path.startsWith('/assets')) { context.logger.info(`Deleting non-asset: ${path}`); templateTree.delete(path); } else { context.logger.info(`Keeping asset: ${path}`); } }); // If targeting public folder, rename /assets/* to /* to remove the assets prefix if (targetAssetsFolder === publicPath) { context.logger.info('Renaming assets to remove /assets prefix for public folder'); const assetsToRename = []; // Collect all assets that need to be renamed templateTree.visit((path) => { if (path.startsWith('/assets/')) { const entry = templateTree.get(path); if (entry && entry.content) { const newPath = path.substring('/assets'.length); // Remove /assets prefix assetsToRename.push({ oldPath: path, newPath, content: entry.content }); } } }); // Delete old paths and create new ones assetsToRename.forEach(({ oldPath, newPath, content }) => { context.logger.info(`Renaming ${oldPath} to ${newPath}`); templateTree.delete(oldPath); templateTree.create(newPath, content); }); } } else { context.logger.info('No assets directory found in template'); } const filesAfter = []; templateTree.visit(path => filesAfter.push(path)); context.logger.info(`Template files after asset filter: ${filesAfter.join(', ')}`); return templateTree; }, // Move assets to target location (0, schematics_1.move)((0, core_1.normalize)(`/${targetAssetsFolder}`)), ]); context.logger.info('Applying both template sources...'); return (0, schematics_1.chain)([ (0, schematics_1.mergeWith)(componentTemplateSource), (0, schematics_1.mergeWith)(assetsTemplateSource), ])(tree, context); }; } function addDeclarationToNgModule(options) { return (host) => { if (options.standalone) { return host; } const workspaceConfig = loadWorkspaceConfig(host); const project = getProjectConfig(workspaceConfig, options.project); if (options.skipImport || !options.module) { // Use project sourceRoot to find the correct module search paths const sourceRoot = project.sourceRoot || (0, core_1.join)(project.root || '', 'src').replace(/\\/g, '/'); const searchPaths = [ `${sourceRoot}/app`, sourceRoot, (0, core_1.normalize)((0, core_1.join)(sourceRoot, '..')), // Bubble up one level if needed ]; let foundModule; for (const searchPath of searchPaths) { foundModule = findModule(host, searchPath); if (foundModule) break; } options.module = foundModule; if (!options.module) { options.standalone = true; return host; } } const modulePath = options.module; const text = host.read(modulePath); if (text === null) { throw new schematics_1.SchematicsException(`File ${modulePath} does not exist.`); } const sourceText = text.toString('utf-8'); const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); // Use project sourceRoot for component base path const sourceRoot = project.sourceRoot || (0, core_1.join)(project.root || '', 'src').replace(/\\/g, '/'); const componentBasePath = `/${sourceRoot}`; const componentPath = `${componentBasePath}/${options.path}/${core_1.strings.dasherize(options.name)}.component`; const relativePath = (0, find_module_1.buildRelativePath)(modulePath, componentPath); const componentChanges = (0, ast_utils_1.addDeclarationToModule)(source, modulePath, core_1.strings.classify(`${options.name}Component`), relativePath); const moduleChanges = (0, ast_utils_1.addImportToModule)(source, modulePath, core_1.strings.classify(`NgxExtendedPdfViewerModule`), 'ngx-extended-pdf-viewer'); const recorder = host.beginUpdate(modulePath); for (const change of componentChanges) { if (change instanceof change_1.InsertChange) { recorder.insertLeft(change.pos, change.toAdd); } } for (const change of moduleChanges) { if (change instanceof change_1.InsertChange) { recorder.insertLeft(change.pos, change.toAdd); } } host.commitUpdate(recorder); return host; }; } function findModule(host, generateDir, moduleExt = find_module_1.MODULE_EXT, routingModuleExt = find_module_1.ROUTING_MODULE_EXT) { let dir = host.getDir(`/${generateDir}`); let foundRoutingModule = false; while (dir) { const allMatches = dir.subfiles.filter((p) => p.endsWith(moduleExt)); const filteredMatches = allMatches.filter((p) => !p.endsWith(routingModuleExt)); foundRoutingModule = foundRoutingModule || allMatches.length !== filteredMatches.length; if (filteredMatches.length == 1) { return (0, core_1.join)(dir.path, filteredMatches[0]); } else if (filteredMatches.length > 1) { throw new Error('More than one module matches. Use the skip-import option to skip importing ' + 'the component into the closest module or use the module option to specify a module.'); } dir = dir.parent; } console.error(''); console.error("Error: Couldn't find a module. Assuming this is a stand-alone project. You need to add these lines to the decorator of the ExamplePdfViewerComponent:"); console.error(''); console.error('standalone: true,'); console.error('imports: [NgxExtendedPdfViewerModule],'); console.error(''); return undefined; } exports.findModule = findModule; //# sourceMappingURL=index.js.map