From d9e9ffd7309a96539da6c97c8c7a4460cc921f85 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Mon, 28 Apr 2025 17:27:38 +0100 Subject: [PATCH] Add Angular example --- ci/prepare_playwright.sh | 1 + ci/single_example.sh | 9 +- ci/typescript/create_angular_ng.sh | 138 +++++++++++++++++ .../recipes/typescript/angular_ng_recipe.ts | 120 +++++++++++++++ recipes/src/recipes/typescript/index.ts | 1 + .../recipes/typescript/react_vite_recipe.ts | 1 - recipes/src/step.ts | 2 +- typescript/angular_ng/README.md | 142 ++++++++++++++++++ 8 files changed, 411 insertions(+), 3 deletions(-) create mode 100755 ci/typescript/create_angular_ng.sh create mode 100644 recipes/src/recipes/typescript/angular_ng_recipe.ts create mode 100644 typescript/angular_ng/README.md diff --git a/ci/prepare_playwright.sh b/ci/prepare_playwright.sh index 3520645..57ab078 100755 --- a/ci/prepare_playwright.sh +++ b/ci/prepare_playwright.sh @@ -9,3 +9,4 @@ set -eux ./single_example.sh typescript vanilla_vite ./single_example.sh typescript react_vite ./single_example.sh typescript vue_vite +./single_example.sh typescript angular_ng diff --git a/ci/single_example.sh b/ci/single_example.sh index ce34ad4..5bf70a7 100755 --- a/ci/single_example.sh +++ b/ci/single_example.sh @@ -12,9 +12,16 @@ fi export TYPE=$1 export EXAMPLE=$2 +export SANITISED_EXAMPLE=$EXAMPLE + if [[ $EXAMPLE =~ _vite$ ]]; then export PORT=5173 export SERVE_CMD="npm run dev" +elif [[ $EXAMPLE =~ ^angular_ng$ ]]; then + export PORT=4200 + export SERVE_CMD="npm run start" + # Angular annoyingly converts underscores to dashes + export SANITISED_EXAMPLE=angular-ng else export PORT=4500 export SERVE_CMD="npm run serve" @@ -74,7 +81,7 @@ EOF cat > temp.json << EOF { "scripts": { - "serve": "npm explore $EXAMPLE -- $SERVE_CMD", + "serve": "npm explore $SANITISED_EXAMPLE -- $SERVE_CMD", "test": "playwright test", "test:ui": "playwright test --ui" } diff --git a/ci/typescript/create_angular_ng.sh b/ci/typescript/create_angular_ng.sh new file mode 100755 index 0000000..3309bdf --- /dev/null +++ b/ci/typescript/create_angular_ng.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash + +set -eux + +export OUTPUT_DIRECTORY=../temp/typescript/angular_ng + +mkdir -p $OUTPUT_DIRECTORY +cd $OUTPUT_DIRECTORY +rm -rf * + +function merge-json() { + # merge the second json file into the first. + TEMP_FILE=$(mktemp) + jq '. * input' $1 $2 > TEMP_FILE && mv TEMP_FILE $1 +} + +# 1. Install @angular/cli +npm install -g @angular/cli + +# 2. Create Angular application +ng new angular_ng --directory ./ --minimal --routing=false --skip-git --ssr=false --style=css + +# 3. Build and serve the initial project +# npm run build +# # npm run start +# In a web browser navigate to http://localhost:4200/ + +# 4. Add BokehJS dependency to this project. This assumes the package has been built and copied to the root directory of this repository as outlined in the top-level README.md. +npm install ../../../../bokeh-bokehjs-3.8.0-dev.1.tgz + +# 5. Create a new file src/app/bokeh-js/bokeh-js.component.ts containing a BokehJS plot component +mkdir -p src/app/bokeh-js +cat > src/app/bokeh-js/bokeh-js.component.ts << EOF +import { Component, OnInit } from '@angular/core' +import * as Bokeh from "@bokeh/bokehjs"; + +function create_bokehjs_plot(): Bokeh.Column { + const source = new Bokeh.ColumnDataSource({data: { x: [0.1, 0.9], y: [0.1, 0.9], size: [40, 10] }}); + + const plot = Bokeh.Plotting.figure({ + title: "Example BokehJS plot", height: 500, width: 500, + x_range: [0, 1], y_range: [0, 1], sizing_mode: "stretch_width", + }); + + plot.scatter({ field: "x" }, { field: "y" }, {source, size: { field: "size" }}); + + const button = new Bokeh.Widgets.Button({label: "Click me to add a point", button_type: "primary"}); + function button_callback() { + const data = source.data as any; + data.x.push(Math.random()); + data.y.push(Math.random()); + data.size.push(10 + Math.random()*30); + source.change.emit(); + } + button.on_click(button_callback); + + return new Bokeh.Column({children: [plot, button], sizing_mode: "stretch_width"}); +} + +@Component({ + selector: 'app-bokeh-js', + imports: [], + template: \`
\`, + styles: \`\` +}) + +export class BokehJSComponent implements OnInit { + ngOnInit() { + console.info("BokehJS version:", Bokeh.version); + Bokeh.Plotting.show(create_bokehjs_plot(), "#target"); + } +} +EOF + +# 6. Replace src/app/app.component.ts so that it uses the BokehJSComponent +cat > src/app/app.component.ts << EOF +import { Component } from '@angular/core' +import { BokehJSComponent } from './bokeh-js/bokeh-js.component'; + +@Component({ + selector: 'app-root', + imports: [BokehJSComponent], + template: \`\`, + styles: [], +}) + +export class AppComponent {} +EOF + +# 7. Remove some build warnings by allowing non ESM imports by adding to angular.json +cat > temp.json << EOF +{ + "projects": { + "angular_ng": { + "architect": { + "build": { + "options": { + "allowedCommonJsDependencies": [ + "@bokeh/bokehjs", + "mathjax-full", + "regl" + ] + } + } + } + } + } +} +EOF +merge-json angular.json temp.json +rm temp.json + +# 8. Remove bundle size limits in angular.json by setting the "budgets" to an empty array +cat > temp.json << EOF +{ + "projects": { + "angular_ng": { + "architect": { + "build": { + "configurations": { + "production": { + "budgets": [ + ] + } + } + } + } + } + } +} +EOF +merge-json angular.json temp.json +rm temp.json + +# 9. Rebuild and Serve the project +npm run build +# npm run start +# In a web browser navigate to http://localhost:4200/ diff --git a/recipes/src/recipes/typescript/angular_ng_recipe.ts b/recipes/src/recipes/typescript/angular_ng_recipe.ts new file mode 100644 index 0000000..36ca7ac --- /dev/null +++ b/recipes/src/recipes/typescript/angular_ng_recipe.ts @@ -0,0 +1,120 @@ +import { Recipe } from '../../recipe'; +import { CommandStep, CreateFileStep, MergeJsonStep, ReplaceFileStep } from '../../step'; +import { baseTypeScriptExample } from './common'; + +export class AngularNgRecipe extends Recipe { + constructor() { + super( + 'typescript', + 'angular', + 'ng', + 'The Angular web framework includes its own builder `ng` in the `@angular/cli` package' + ); + + this.add(new CommandStep( + 'Install `@angular/cli`', + ['npm install -g @angular/cli'] + )); + + this.add(new CommandStep( + 'Create Angular application', + ['ng new angular_ng --directory ./ --minimal --routing=false --skip-git --ssr=false --style=css'] + )); + + this.add(new CommandStep( + 'Build and serve the initial project', + ['npm run build', 'npm run start'], + 'In a web browser navigate to http://localhost:4200/', + true + )); + + this.add(new CommandStep( + 'Add BokehJS dependency to this project. This assumes the package has been built and ' + + 'copied to the root directory of this repository as outlined in the top-level `README.md`.', + ['npm install ../../../../bokeh-bokehjs-3.8.0-dev.1.tgz'] + )); + + this.add(new CreateFileStep( + 'Create a new file `src/app/bokeh-js/bokeh-js.component.ts` containing a BokehJS plot component', + 'src/app/bokeh-js/bokeh-js.component.ts', + "import { Component, OnInit } from '@angular/core'\n" + + baseTypeScriptExample.import + "\n" + + baseTypeScriptExample.function + "\n" + + "@Component({\n" + + " selector: 'app-bokeh-js',\n" + + " imports: [],\n" + + ' template: \\`
\\`,\n' + + " styles: \\`\\`\n" + + "})\n\n" + + "export class BokehJSComponent implements OnInit {\n" + + " ngOnInit() {\n" + + " " + baseTypeScriptExample.version + + " " + baseTypeScriptExample.show() + + " }\n" + + "}") + ); + + this.add(new ReplaceFileStep( + 'Replace `src/app/app.component.ts` so that it uses the `BokehJSComponent`', + 'src/app/app.component.ts', + "import { Component } from '@angular/core'\n" + + "import { BokehJSComponent } from './bokeh-js/bokeh-js.component';\n\n" + + "@Component({\n" + + " selector: 'app-root',\n" + + " imports: [BokehJSComponent],\n" + + " template: \\`\\`,\n" + + " styles: [],\n" + + "})\n\n" + + "export class AppComponent {}") + ); + + this.add(new MergeJsonStep( + 'Remove some build warnings by allowing non ESM imports by adding to `angular.json`', + 'angular.json', +`{ + "projects": { + "angular_ng": { + "architect": { + "build": { + "options": { + "allowedCommonJsDependencies": [ + "@bokeh/bokehjs", + "mathjax-full", + "regl" + ] + } + } + } + } + } +}`) + ); + + this.add(new MergeJsonStep( + 'Remove bundle size limits in `angular.json` by setting the `"budgets"` to an empty array', + 'angular.json', +`{ + "projects": { + "angular_ng": { + "architect": { + "build": { + "configurations": { + "production": { + "budgets": [ + ] + } + } + } + } + } + } +}`) + ); + + this.add(new CommandStep( + 'Rebuild and Serve the project', + ['npm run build', 'npm run start'], + 'In a web browser navigate to http://localhost:4200/' + )); + } +} diff --git a/recipes/src/recipes/typescript/index.ts b/recipes/src/recipes/typescript/index.ts index 61e3298..e0ab356 100644 --- a/recipes/src/recipes/typescript/index.ts +++ b/recipes/src/recipes/typescript/index.ts @@ -1,3 +1,4 @@ +export * from './angular_ng_recipe'; export * from './react_vite_recipe'; export * from './vanilla_rspack_recipe'; export * from './vanilla_vite_recipe'; diff --git a/recipes/src/recipes/typescript/react_vite_recipe.ts b/recipes/src/recipes/typescript/react_vite_recipe.ts index 33f0ffd..ad0d7a5 100644 --- a/recipes/src/recipes/typescript/react_vite_recipe.ts +++ b/recipes/src/recipes/typescript/react_vite_recipe.ts @@ -42,7 +42,6 @@ export class ReactViteRecipe extends Recipe { export default App`) ); - this.add(new ReplaceFileStep( 'Remove CSS lines from `src/main.tsx` by replacing it', 'src/main.tsx', diff --git a/recipes/src/step.ts b/recipes/src/step.ts index 3d2ba26..92a14c9 100644 --- a/recipes/src/step.ts +++ b/recipes/src/step.ts @@ -43,7 +43,7 @@ export class CommandStep extends Step { const allPrefix = this.ignoreIfBash ? '# ' : ''; for (const command of this.commands) { let prefix = allPrefix; - if (command === 'npm run serve' || command === 'npm run dev') { + if (command === 'npm run serve' || command === 'npm run dev' || command === 'npm run start') { prefix = '# ' + prefix; } fs.writeSync(fd, prefix + command + '\n'); diff --git a/typescript/angular_ng/README.md b/typescript/angular_ng/README.md new file mode 100644 index 0000000..11fc104 --- /dev/null +++ b/typescript/angular_ng/README.md @@ -0,0 +1,142 @@ +# Angular ng typescript example + +The Angular web framework includes its own builder `ng` in the `@angular/cli` package + +1. Install `@angular/cli` + + ```bash + npm install -g @angular/cli + ``` + +2. Create Angular application + + ```bash + ng new angular_ng --directory ./ --minimal --routing=false --skip-git --ssr=false --style=css + ``` + +3. Build and serve the initial project + + ```bash + npm run build + npm run start + ``` + + In a web browser navigate to http://localhost:4200/ + +4. Add BokehJS dependency to this project. This assumes the package has been built and copied to the root directory of this repository as outlined in the top-level `README.md`. + + ```bash + npm install ../../../../bokeh-bokehjs-3.8.0-dev.1.tgz + ``` + +5. Create a new file `src/app/bokeh-js/bokeh-js.component.ts` containing a BokehJS plot component containing + + ```ts + import { Component, OnInit } from '@angular/core' + import * as Bokeh from "@bokeh/bokehjs"; + + function create_bokehjs_plot(): Bokeh.Column { + const source = new Bokeh.ColumnDataSource({data: { x: [0.1, 0.9], y: [0.1, 0.9], size: [40, 10] }}); + + const plot = Bokeh.Plotting.figure({ + title: "Example BokehJS plot", height: 500, width: 500, + x_range: [0, 1], y_range: [0, 1], sizing_mode: "stretch_width", + }); + + plot.scatter({ field: "x" }, { field: "y" }, {source, size: { field: "size" }}); + + const button = new Bokeh.Widgets.Button({label: "Click me to add a point", button_type: "primary"}); + function button_callback() { + const data = source.data as any; + data.x.push(Math.random()); + data.y.push(Math.random()); + data.size.push(10 + Math.random()*30); + source.change.emit(); + } + button.on_click(button_callback); + + return new Bokeh.Column({children: [plot, button], sizing_mode: "stretch_width"}); + } + + @Component({ + selector: 'app-bokeh-js', + imports: [], + template: `
`, + styles: `` + }) + + export class BokehJSComponent implements OnInit { + ngOnInit() { + console.info("BokehJS version:", Bokeh.version); + Bokeh.Plotting.show(create_bokehjs_plot(), "#target"); + } + } + ``` + +6. Replace `src/app/app.component.ts` so that it uses the `BokehJSComponent` containing + + ```ts + import { Component } from '@angular/core' + import { BokehJSComponent } from './bokeh-js/bokeh-js.component'; + + @Component({ + selector: 'app-root', + imports: [BokehJSComponent], + template: ``, + styles: [], + }) + + export class AppComponent {} + ``` + +7. Remove some build warnings by allowing non ESM imports by adding to `angular.json` + + ```.json + { + "projects": { + "angular_ng": { + "architect": { + "build": { + "options": { + "allowedCommonJsDependencies": [ + "@bokeh/bokehjs", + "mathjax-full", + "regl" + ] + } + } + } + } + } + } + ``` + +8. Remove bundle size limits in `angular.json` by setting the `"budgets"` to an empty array + + ```.json + { + "projects": { + "angular_ng": { + "architect": { + "build": { + "configurations": { + "production": { + "budgets": [ + ] + } + } + } + } + } + } + } + ``` + +9. Rebuild and Serve the project + + ```bash + npm run build + npm run start + ``` + + In a web browser navigate to http://localhost:4200/