-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(@schematics/angular): add migration to migrate from `@nguniversa…
…l` to `@angular/ssr` This commit adds a migration to migrate usages of `@nguniversal` to `@angular/ssr`.
- Loading branch information
1 parent
1635fdc
commit 3938863
Showing
3 changed files
with
360 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
packages/schematics/angular/migrations/update-17/replace-nguniversal-engines.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import { DirEntry, Rule, chain } from '@angular-devkit/schematics'; | ||
import { addDependency } from '../../utility'; | ||
import { removePackageJsonDependency } from '../../utility/dependencies'; | ||
import { latestVersions } from '../../utility/latest-versions'; | ||
import { allTargetOptions, getWorkspace } from '../../utility/workspace'; | ||
import { Builders, ProjectType } from '../../utility/workspace-models'; | ||
|
||
function* visit(directory: DirEntry): IterableIterator<[fileName: string, contents: string]> { | ||
for (const path of directory.subfiles) { | ||
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) { | ||
const entry = directory.file(path); | ||
if (entry) { | ||
const content = entry.content; | ||
if (content.includes('@nguniversal/')) { | ||
// Only need to rename the import so we can just string replacements. | ||
yield [entry.path, content.toString()]; | ||
} | ||
} | ||
} | ||
} | ||
|
||
for (const path of directory.subdirs) { | ||
if (path === 'node_modules' || path.startsWith('.')) { | ||
continue; | ||
} | ||
|
||
yield* visit(directory.dir(path)); | ||
} | ||
} | ||
|
||
/** | ||
* Regexp to match Universal packages. | ||
* @nguniversal/common/engine | ||
* @nguniversal/common | ||
* @nguniversal/express-engine | ||
**/ | ||
const NGUNIVERSAL_PACKAGE_REGEXP = /@nguniversal\/(common(\/engine)?|express-engine)/g; | ||
|
||
export default function (): Rule { | ||
return chain([ | ||
async (tree) => { | ||
// Replace server file. | ||
const workspace = await getWorkspace(tree); | ||
for (const [, project] of workspace.projects) { | ||
if (project.extensions.projectType !== ProjectType.Application) { | ||
continue; | ||
} | ||
|
||
const serverMainFiles = new Map<string /** Main Path */, string /** Output Path */>(); | ||
for (const [, target] of project.targets) { | ||
if (target.builder !== Builders.Server) { | ||
continue; | ||
} | ||
|
||
const outputPath = project.targets.get('build')?.options?.outputPath; | ||
|
||
for (const [, { main }] of allTargetOptions(target, false)) { | ||
if ( | ||
typeof main === 'string' && | ||
typeof outputPath === 'string' && | ||
tree.readText(main).includes('ngExpressEngine') | ||
) { | ||
serverMainFiles.set(main, outputPath); | ||
} | ||
} | ||
} | ||
|
||
// Replace server file | ||
for (const [path, outputPath] of serverMainFiles.entries()) { | ||
tree.rename(path, path + '.bak'); | ||
tree.create(path, getServerFileContents(outputPath)); | ||
} | ||
} | ||
|
||
// Replace all import specifiers in all files. | ||
for (const file of visit(tree.root)) { | ||
const [path, content] = file; | ||
tree.overwrite(path, content.replaceAll(NGUNIVERSAL_PACKAGE_REGEXP, '@angular/ssr')); | ||
} | ||
|
||
// Remove universal packages from deps. | ||
removePackageJsonDependency(tree, '@nguniversal/express-engine'); | ||
removePackageJsonDependency(tree, '@nguniversal/common'); | ||
}, | ||
addDependency('@angular/ssr', latestVersions.Angular), | ||
]); | ||
} | ||
|
||
function getServerFileContents(outputPath: string): string { | ||
return ` | ||
import 'zone.js/node'; | ||
import { APP_BASE_HREF } from '@angular/common'; | ||
import { CommonEngine } from '@angular/ssr'; | ||
import * as express from 'express'; | ||
import { existsSync } from 'node:fs'; | ||
import { join } from 'node:path'; | ||
import bootstrap from './src/main.server'; | ||
// The Express app is exported so that it can be used by serverless Functions. | ||
export function app(): express.Express { | ||
const server = express(); | ||
const distFolder = join(process.cwd(), '${outputPath}'); | ||
const indexHtml = existsSync(join(distFolder, 'index.original.html')) | ||
? join(distFolder, 'index.original.html') | ||
: join(distFolder, 'index.html'); | ||
const commonEngine = new CommonEngine(); | ||
server.set('view engine', 'html'); | ||
server.set('views', distFolder); | ||
// Example Express Rest API endpoints | ||
// server.get('/api/**', (req, res) => { }); | ||
// Serve static files from /browser | ||
server.get('*.*', express.static(distFolder, { | ||
maxAge: '1y' | ||
})); | ||
// All regular routes use the Angular engine | ||
server.get('*', (req, res, next) => { | ||
commonEngine | ||
.render({ | ||
bootstrap, | ||
documentFilePath: indexHtml, | ||
url: req.originalUrl, | ||
publicPath: distFolder, | ||
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }], | ||
}) | ||
.then((html) => res.send(html)) | ||
.catch((err) => next(err)); | ||
}); | ||
return server; | ||
} | ||
function run(): void { | ||
const port = process.env['PORT'] || 4000; | ||
// Start up the Node server | ||
const server = app(); | ||
server.listen(port, () => { | ||
console.log(\`Node Express server listening on http://localhost:\${port}\`); | ||
}); | ||
} | ||
// Webpack will replace 'require' with '__webpack_require__' | ||
// '__non_webpack_require__' is a proxy to Node 'require' | ||
// The below code is to ensure that the server is run only when not requiring the bundle. | ||
declare const __non_webpack_require__: NodeRequire; | ||
const mainModule = __non_webpack_require__.main; | ||
const moduleFilename = mainModule && mainModule.filename || ''; | ||
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { | ||
run(); | ||
} | ||
export default bootstrap; | ||
`; | ||
} |
188 changes: 188 additions & 0 deletions
188
packages/schematics/angular/migrations/update-17/replace-nguniversal-engines_spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import { EmptyTree } from '@angular-devkit/schematics'; | ||
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; | ||
import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models'; | ||
|
||
function createWorkSpaceConfig(tree: UnitTestTree) { | ||
const angularConfig: WorkspaceSchema = { | ||
version: 1, | ||
projects: { | ||
app: { | ||
root: '', | ||
sourceRoot: '/src', | ||
projectType: ProjectType.Application, | ||
prefix: 'app', | ||
architect: { | ||
build: { | ||
builder: Builders.Browser, | ||
options: { | ||
tsConfig: 'tsconfig.json', | ||
main: 'main.ts', | ||
polyfills: '', | ||
outputPath: 'dist/browser', | ||
}, | ||
}, | ||
server: { | ||
builder: Builders.Server, | ||
options: { | ||
tsConfig: 'tsconfig.json', | ||
main: 'server.ts', | ||
outputPath: 'dist/server', | ||
}, | ||
configurations: { | ||
production: { | ||
main: 'server.ts', | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); | ||
} | ||
|
||
describe(`Migration to replace usages of '@nguniversal/' packages with '@angular/ssr'.`, () => { | ||
const schematicName = 'replace-nguniversal-engines'; | ||
const schematicRunner = new SchematicTestRunner( | ||
'migrations', | ||
require.resolve('../migration-collection.json'), | ||
); | ||
|
||
let tree: UnitTestTree; | ||
beforeEach(() => { | ||
tree = new UnitTestTree(new EmptyTree()); | ||
|
||
createWorkSpaceConfig(tree); | ||
tree.create( | ||
'/package.json', | ||
JSON.stringify( | ||
{ | ||
dependencies: { | ||
'@nguniversal/common': '0.0.0', | ||
'@nguniversal/express-engine': '0.0.0', | ||
}, | ||
}, | ||
undefined, | ||
2, | ||
), | ||
); | ||
|
||
tree.create( | ||
'server.ts', | ||
` | ||
import 'zone.js/node'; | ||
import { APP_BASE_HREF } from '@angular/common'; | ||
import { ngExpressEngine } from '@nguniversal/express-engine'; | ||
import * as express from 'express'; | ||
import { existsSync } from 'fs'; | ||
import { join } from 'path'; | ||
import { AppServerModule } from './src/main.server'; | ||
// The Express app is exported so that it can be used by serverless Functions. | ||
export function app(): express.Express { | ||
const server = express(); | ||
const distFolder = join(process.cwd(), 'dist/browser'); | ||
const indexHtml = existsSync(join(distFolder, 'index.original.html')) | ||
? 'index.original.html' | ||
: 'index'; | ||
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine) | ||
server.engine( | ||
'html', | ||
ngExpressEngine({ | ||
bootstrap: AppServerModule, | ||
inlineCriticalCss: true, | ||
}), | ||
); | ||
server.set('view engine', 'html'); | ||
server.set('views', distFolder); | ||
// Example Express Rest API endpoints | ||
// server.get('/api/**', (req, res) => { }); | ||
// Serve static files from /browser | ||
server.get( | ||
'*.*', | ||
express.static(distFolder, { | ||
maxAge: '1y', | ||
}), | ||
); | ||
// All regular routes use the Universal engine | ||
server.get('*', (req, res) => { | ||
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] }); | ||
}); | ||
return server; | ||
} | ||
function run() { | ||
const port = process.env.PORT || 4000; | ||
// Start up the Node server | ||
const server = app(); | ||
server.listen(port); | ||
} | ||
// Webpack will replace 'require' with '__webpack_require__' | ||
// '__non_webpack_require__' is a proxy to Node 'require' | ||
// The below code is to ensure that the server is run only when not requiring the bundle. | ||
declare const __non_webpack_require__: NodeRequire; | ||
const mainModule = __non_webpack_require__.main; | ||
const moduleFilename = (mainModule && mainModule.filename) || ''; | ||
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { | ||
run(); | ||
} `, | ||
); | ||
}); | ||
|
||
it(`should remove all '@nguniversal/' from dependencies`, async () => { | ||
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); | ||
const { dependencies } = JSON.parse(newTree.readContent('/package.json')); | ||
expect(dependencies['@nguniversal/common']).toBeUndefined(); | ||
expect(dependencies['@nguniversal/express-engine']).toBeUndefined(); | ||
}); | ||
|
||
it(`should add '@angular/ssr' as a dependencies`, async () => { | ||
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); | ||
const { dependencies } = JSON.parse(newTree.readContent('/package.json')); | ||
expect(dependencies['@angular/ssr']).toBeDefined(); | ||
}); | ||
|
||
it(`should replace imports from '@nguniversal/common' to '@angular/ssr'`, async () => { | ||
tree.create( | ||
'file.ts', | ||
` | ||
import { CommonEngine } from '@nguniversal/common'; | ||
import { Component } from '@angular/core'; | ||
`, | ||
); | ||
|
||
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); | ||
expect(newTree.readContent('/file.ts')).toContain( | ||
`import { CommonEngine } from '@angular/ssr';`, | ||
); | ||
}); | ||
|
||
it(`should replace anf backup 'server.ts' file`, async () => { | ||
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); | ||
expect(newTree.readContent('server.ts.bak')).toContain( | ||
`import { ngExpressEngine } from '@nguniversal/express-engine';`, | ||
); | ||
|
||
const newServerFile = newTree.readContent('server.ts'); | ||
expect(newServerFile).toContain(`import { CommonEngine } from '@angular/ssr';`); | ||
expect(newServerFile).toContain(`const distFolder = join(process.cwd(), 'dist/browser');`); | ||
}); | ||
}); |