Skip to content

Commit

Permalink
feat(@schematics/angular): update ng new to use the esbuild applica…
Browse files Browse the repository at this point in the history
…tion builder based builder

This commit updates the `ng generate application` to use the esbuild `application` builder. This also updates the schematics to support both `browser` and `application` builders.

BREAKING CHANGE: `rootModuleClassName`, `rootModuleFileName` and `main` options have been removed from the public `pwa` and `app-shell` schematics.
  • Loading branch information
alan-agius4 committed Sep 13, 2023
1 parent 3d3afc7 commit 3f8aa9d
Show file tree
Hide file tree
Showing 37 changed files with 953 additions and 700 deletions.
5 changes: 4 additions & 1 deletion packages/angular/pwa/pwa/index.ts
Expand Up @@ -114,7 +114,10 @@ export default function (options: PwaOptions): Rule {
const buildTargets = [];
const testTargets = [];
for (const target of project.targets.values()) {
if (target.builder === '@angular-devkit/build-angular:browser') {
if (
target.builder === '@angular-devkit/build-angular:browser' ||
target.builder === '@angular-devkit/build-angular:application'
) {
buildTargets.push(target);
} else if (target.builder === '@angular-devkit/build-angular:karma') {
testTargets.push(target);
Expand Down
183 changes: 92 additions & 91 deletions packages/angular/pwa/pwa/index_spec.ts
Expand Up @@ -52,122 +52,123 @@ describe('PWA Schematic', () => {
);
});

it('should run the service worker schematic', (done) => {
schematicRunner
.runSchematic('ng-add', defaultOptions, appTree)

.then((tree) => {
const configText = tree.readContent('/angular.json');
const config = JSON.parse(configText);
const swFlag = config.projects.bar.architect.build.options.serviceWorker;
expect(swFlag).toEqual(true);
done();
}, done.fail);
});

it('should create icon files', (done) => {
it('should create icon files', async () => {
const dimensions = [72, 96, 128, 144, 152, 192, 384, 512];
const iconPath = '/projects/bar/src/assets/icons/icon-';
schematicRunner
.runSchematic('ng-add', defaultOptions, appTree)

.then((tree) => {
dimensions.forEach((d) => {
const path = `${iconPath}${d}x${d}.png`;
expect(tree.exists(path)).toEqual(true);
});
done();
}, done.fail);
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

dimensions.forEach((d) => {
const path = `${iconPath}${d}x${d}.png`;
expect(tree.exists(path)).toBeTrue();
});
});

it('should create a manifest file', (done) => {
schematicRunner
.runSchematic('ng-add', defaultOptions, appTree)
it('should run the service worker schematic', async () => {
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);
const configText = tree.readContent('/angular.json');
const config = JSON.parse(configText);
const swFlag = config.projects.bar.architect.build.configurations.production.serviceWorker;

.then((tree) => {
expect(tree.exists('/projects/bar/src/manifest.webmanifest')).toEqual(true);
done();
}, done.fail);
expect(swFlag).toBe('projects/bar/ngsw-config.json');
});

it('should set the name & short_name in the manifest file', (done) => {
schematicRunner
.runSchematic('ng-add', defaultOptions, appTree)
it('should create a manifest file', async () => {
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);
expect(tree.exists('/projects/bar/src/manifest.webmanifest')).toBeTrue();
});

it('should set the name & short_name in the manifest file', async () => {
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

.then((tree) => {
const manifestText = tree.readContent('/projects/bar/src/manifest.webmanifest');
const manifest = JSON.parse(manifestText);
const manifestText = tree.readContent('/projects/bar/src/manifest.webmanifest');
const manifest = JSON.parse(manifestText);

expect(manifest.name).toEqual(defaultOptions.title);
expect(manifest.short_name).toEqual(defaultOptions.title);
done();
}, done.fail);
expect(manifest.name).toEqual(defaultOptions.title);
expect(manifest.short_name).toEqual(defaultOptions.title);
});

it('should set the name & short_name in the manifest file when no title provided', (done) => {
it('should set the name & short_name in the manifest file when no title provided', async () => {
const options = { ...defaultOptions, title: undefined };
schematicRunner
.runSchematic('ng-add', options, appTree)
const tree = await schematicRunner.runSchematic('ng-add', options, appTree);

.then((tree) => {
const manifestText = tree.readContent('/projects/bar/src/manifest.webmanifest');
const manifest = JSON.parse(manifestText);
const manifestText = tree.readContent('/projects/bar/src/manifest.webmanifest');
const manifest = JSON.parse(manifestText);

expect(manifest.name).toEqual(defaultOptions.project);
expect(manifest.short_name).toEqual(defaultOptions.project);
done();
}, done.fail);
expect(manifest.name).toEqual(defaultOptions.project);
expect(manifest.short_name).toEqual(defaultOptions.project);
});

it('should update the index file', (done) => {
schematicRunner
.runSchematic('ng-add', defaultOptions, appTree)

.then((tree) => {
const content = tree.readContent('projects/bar/src/index.html');
it('should update the index file', async () => {
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);
const content = tree.readContent('projects/bar/src/index.html');

expect(content).toMatch(/<link rel="manifest" href="manifest.webmanifest">/);
expect(content).toMatch(/<meta name="theme-color" content="#1976d2">/);
expect(content).toMatch(
/<noscript>Please enable JavaScript to continue using this application.<\/noscript>/,
);
done();
}, done.fail);
expect(content).toMatch(/<link rel="manifest" href="manifest.webmanifest">/);
expect(content).toMatch(/<meta name="theme-color" content="#1976d2">/);
expect(content).toMatch(
/<noscript>Please enable JavaScript to continue using this application.<\/noscript>/,
);
});

it('should not add noscript element to the index file if already present', (done) => {
it('should not add noscript element to the index file if already present', async () => {
let index = appTree.readContent('projects/bar/src/index.html');
index = index.replace('</body>', '<noscript>NO JAVASCRIPT</noscript></body>');
appTree.overwrite('projects/bar/src/index.html', index);
schematicRunner
.runSchematic('ng-add', defaultOptions, appTree)

.then((tree) => {
const content = tree.readContent('projects/bar/src/index.html');

expect(content).toMatch(/<link rel="manifest" href="manifest.webmanifest">/);
expect(content).toMatch(/<meta name="theme-color" content="#1976d2">/);
expect(content).not.toMatch(
/<noscript>Please enable JavaScript to continue using this application.<\/noscript>/,
);
expect(content).toMatch(/<noscript>NO JAVASCRIPT<\/noscript>/);
done();
}, done.fail);

const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);
const content = tree.readContent('projects/bar/src/index.html');

expect(content).toMatch(/<link rel="manifest" href="manifest.webmanifest">/);
expect(content).toMatch(/<meta name="theme-color" content="#1976d2">/);
expect(content).not.toMatch(
/<noscript>Please enable JavaScript to continue using this application.<\/noscript>/,
);
expect(content).toMatch(/<noscript>NO JAVASCRIPT<\/noscript>/);
});

it('should update the build and test assets configuration', (done) => {
schematicRunner
.runSchematic('ng-add', defaultOptions, appTree)
it('should update the build and test assets configuration', async () => {
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);
const configText = tree.readContent('/angular.json');
const config = JSON.parse(configText);
const targets = config.projects.bar.architect;

.then((tree) => {
const configText = tree.readContent('/angular.json');
const config = JSON.parse(configText);
const targets = config.projects.bar.architect;
['build', 'test'].forEach((target) => {
expect(targets[target].options.assets).toContain('projects/bar/src/manifest.webmanifest');
});
});

['build', 'test'].forEach((target) => {
expect(targets[target].options.assets).toContain('projects/bar/src/manifest.webmanifest');
});
done();
}, done.fail);
describe('Legacy browser builder', () => {
function convertBuilderToLegacyBrowser(): void {
const config = JSON.parse(appTree.readContent('/angular.json'));
const build = config.projects.bar.architect.build;

build.builder = '@angular-devkit/build-angular:browser';
build.options = {
...build.options,
main: build.options.browser,
browser: undefined,
};

build.configurations.development = {
...build.configurations.development,
vendorChunk: true,
namedChunks: true,
buildOptimizer: false,
};

appTree.overwrite('/angular.json', JSON.stringify(config, undefined, 2));
}

beforeEach(() => {
convertBuilderToLegacyBrowser();
});

it('should run the service worker schematic', async () => {
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);
const configText = tree.readContent('/angular.json');
const config = JSON.parse(configText);
const swFlag = config.projects.bar.architect.build.options.serviceWorker;

expect(swFlag).toBeTrue();
});
});
});
3 changes: 3 additions & 0 deletions packages/angular/ssr/package.json
Expand Up @@ -9,6 +9,9 @@
"ssr",
"universal"
],
"ng-add": {
"save": "dependencies"
},
"dependencies": {
"critters": "0.0.20",
"tslib": "^2.3.0"
Expand Down
@@ -0,0 +1,53 @@
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import <% if (isStandalone) { %>bootstrap<% } else { %>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 browserDistFolder = dirname(fileURLToPath(import.meta.url));
const indexHtml = join(browserDistFolder, 'index.server.html');

const commonEngine = new CommonEngine();

server.set('view engine', 'html');
server.set('views', browserDistFolder);

// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(browserDistFolder, {
maxAge: '1y'
}));

// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
commonEngine
.render({
<% if (isStandalone) { %>bootstrap<% } else { %>bootstrap: AppServerModule<% } %>,
documentFilePath: indexHtml,
url: req.originalUrl,
publicPath: browserDistFolder,
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}`);
});
}

run();
Expand Up @@ -5,7 +5,7 @@ import { CommonEngine } from '@angular/ssr';
import * as express from 'express';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import <% if (isStandalone) { %>bootstrap<% } else { %>{ AppServerModule }<% } %> from './src/<%= stripTsExtension(main) %>';
import <% if (isStandalone) { %>bootstrap<% } else { %>AppServerModule<% } %> from './src/main.server';

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
Expand Down Expand Up @@ -64,4 +64,4 @@ if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}

<% if (isStandalone) { %>export default bootstrap;<% } else { %>export * from './src/<%= stripTsExtension(main) %>';<% } %>
<% if (isStandalone) { %>export default bootstrap;<% } else { %>export * from './src/main.server';<% } %>

0 comments on commit 3f8aa9d

Please sign in to comment.