Skip to content

Commit cfa4d5c

Browse files
{feature} add option to configure whether to enable Angular Zoneless (#33)
Co-authored-by: Razvan Tudorache <[email protected]>
1 parent 6b17e2f commit cfa4d5c

File tree

11 files changed

+3282
-2122
lines changed

11 files changed

+3282
-2122
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,11 @@ inline-style-language = "css"
213213
# Whether to create an optimized angular build or not
214214
optimize = false
215215

216-
# Which polyfills to load, if zone.js is not in the list then it will be loaded
217-
# as first polyfill
216+
# Whether to enable Angular Zoneless (requires Angular 20 or later)
217+
zoneless = false
218+
219+
# Which polyfills to load, if zoneless is disabled and zone.js is not in the
220+
# list then it will be included as first polyfill.
218221
# This is a list of strings, all of which must be bare identifiers. Relative
219222
# imports won't work.
220223
polyfills = []

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"build-scripts": "esbuild --minify --format=esm --loader=js <src/js/playground-io.js >src/js/playground-io.min.js"
99
},
1010
"devDependencies": {
11-
"@angular/cli": "^18.0.0",
12-
"esbuild": "^0.18.12",
11+
"@angular/cli": "^20.0.0",
12+
"esbuild": "^0.25.9",
1313
"express-check-in": "^0.1.2",
1414
"husky": "8.0.3",
1515
"is-ci": "3.0.1",

src/angular/builder/background.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use super::{default::write_angular_workspace, Writer};
1111

1212
pub(super) fn build(config: &Config, chapters: Vec<ChapterWithCodeBlocks>) -> Result<()> {
1313
let root = &config.angular_root_folder;
14-
let mut writer = Writer::new(true);
1514

1615
let mut root_exists = root.exists();
1716
if root_exists && !background::is_running(config)? {
@@ -26,11 +25,7 @@ pub(super) fn build(config: &Config, chapters: Vec<ChapterWithCodeBlocks>) -> Re
2625
fs::create_dir_all(root)?;
2726
}
2827

29-
if !is_running {
30-
write_angular_workspace(config, root, false)?;
31-
32-
writer.write_tsconfig(config)?;
33-
}
28+
let mut writer = Writer::new(config, true);
3429

3530
for (
3631
index,
@@ -43,7 +38,13 @@ pub(super) fn build(config: &Config, chapters: Vec<ChapterWithCodeBlocks>) -> Re
4338
writer.write_chapter(root, index, &source_path, code_blocks)?;
4439
}
4540

46-
writer.write_main(config, root)?;
41+
writer.write_main(root)?;
42+
43+
if !is_running {
44+
writer.write_tsconfig()?;
45+
46+
write_angular_workspace(config, root, false)?;
47+
}
4748

4849
if !is_running {
4950
background::start(config)?;

src/angular/builder/default.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ pub(super) fn write_angular_workspace(
5757

5858
pub(super) fn build(config: &Config, chapters: Vec<ChapterWithCodeBlocks>) -> Result<()> {
5959
let root = &config.angular_root_folder;
60-
let mut writer = Writer::new(false);
60+
let mut writer = Writer::new(config, false);
6161

6262
if root.exists() {
6363
fs::remove_dir_all(root)?;
6464
}
6565

6666
fs::create_dir_all(root)?;
6767

68-
writer.write_tsconfig(config)?;
68+
writer.write_tsconfig()?;
6969

7070
write_angular_workspace(config, root, config.optimize)?;
7171

@@ -83,7 +83,7 @@ pub(super) fn build(config: &Config, chapters: Vec<ChapterWithCodeBlocks>) -> Re
8383
chapter_paths.push(source_path);
8484
}
8585

86-
writer.write_main(config, root)?;
86+
writer.write_main(root)?;
8787

8888
ng_build(root)?;
8989

src/angular/builder/writer.rs

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@ use serde_json::json;
44

55
use crate::{codeblock::CodeBlock, Config, Context, Result};
66

7-
pub(super) struct Writer {
7+
pub(super) struct Writer<'a> {
88
changed_only: bool,
9+
config: &'a Config,
910
chapter_to_angular_file: Vec<(String, String)>,
1011
}
1112

12-
impl Writer {
13-
pub(super) fn new(changed_only: bool) -> Self {
13+
impl<'a> Writer<'a> {
14+
pub(super) fn new(config: &'a Config, changed_only: bool) -> Self {
1415
Self {
1516
changed_only,
17+
config,
1618
chapter_to_angular_file: Vec::new(),
1719
}
1820
}
21+
}
1922

23+
impl Writer<'_> {
2024
pub(super) fn write<P: AsRef<Path>>(&self, path: P, contents: &str) -> Result<()> {
2125
if self.changed_only
2226
&& matches!(fs::read_to_string(&path), Ok(existing) if existing.eq(contents))
@@ -52,19 +56,34 @@ impl Writer {
5256

5357
let mut main_script = Vec::with_capacity(1 + code_blocks.len());
5458

55-
main_script.push(
56-
"\n\
57-
import {NgZone, type ApplicationRef, type Provider, type EnvironmentProviders, type Type} from '@angular/core';\n\
58-
import {bootstrapApplication} from '@angular/platform-browser';\n\
59-
const zone = new NgZone({});\n\
60-
function makeProviders(component: Type<unknown> & {rootProviders?: readonly (Provider | EnvironmentProviders)[] | null | undefined}) {\n\
61-
return [{provide: NgZone, useValue: zone}, ...(component.rootProviders ?? [])];\n\
62-
}\n\
63-
const applications: Promise<ApplicationRef>[] = [];\n\
64-
(globalThis as any).mdBookAngular = {zone, applications};\n\
65-
"
66-
.to_owned(),
67-
);
59+
if self.config.zoneless {
60+
main_script.push(
61+
"\n\
62+
import {provideZonelessChangeDetection, type ApplicationRef, type Provider, type EnvironmentProviders, type Type} from '@angular/core';\n\
63+
import {bootstrapApplication} from '@angular/platform-browser';\n\
64+
function makeProviders(component: Type<unknown> & {rootProviders?: readonly (Provider | EnvironmentProviders)[] | null | undefined}) {\n\
65+
return [provideZonelessChangeDetection(), ...(component.rootProviders ?? [])];\n\
66+
}\n\
67+
const applications: Promise<ApplicationRef>[] = [];\n\
68+
(globalThis as any).mdBookAngular = {zone: null, applications};\n\
69+
"
70+
.to_owned(),
71+
);
72+
} else {
73+
main_script.push(
74+
"\n\
75+
import {NgZone, type ApplicationRef, type Provider, type EnvironmentProviders, type Type} from '@angular/core';\n\
76+
import {bootstrapApplication} from '@angular/platform-browser';\n\
77+
const zone = new NgZone({});\n\
78+
function makeProviders(component: Type<unknown> & {rootProviders?: readonly (Provider | EnvironmentProviders)[] | null | undefined}) {\n\
79+
return [{provide: NgZone, useValue: zone}, ...(component.rootProviders ?? [])];\n\
80+
}\n\
81+
const applications: Promise<ApplicationRef>[] = [];\n\
82+
(globalThis as any).mdBookAngular = {zone, applications};\n\
83+
"
84+
.to_owned(),
85+
);
86+
}
6887

6988
for (code_block_index, code_block) in code_blocks.into_iter().enumerate() {
7089
self.write(
@@ -99,15 +118,12 @@ impl Writer {
99118
Ok(())
100119
}
101120

102-
pub(super) fn write_main<P: AsRef<Path>>(&self, config: &Config, root: P) -> Result<()> {
103-
let mut main_script =
104-
Vec::with_capacity(3 + config.polyfills.len() + self.chapter_to_angular_file.len());
105-
106-
if !config.polyfills.contains(&"zone.js".to_owned()) {
107-
main_script.push("import 'zone.js';".to_owned());
108-
}
121+
pub(super) fn write_main<P: AsRef<Path>>(&self, root: P) -> Result<()> {
122+
let mut main_script = Vec::with_capacity(
123+
3 + self.config.polyfills.len() + self.chapter_to_angular_file.len(),
124+
);
109125

110-
for polyfill in &config.polyfills {
126+
for polyfill in &self.config.polyfills {
111127
main_script.push(format!("import '{polyfill}';"));
112128
}
113129

@@ -129,8 +145,8 @@ impl Writer {
129145
Ok(())
130146
}
131147

132-
pub(super) fn write_tsconfig(&self, config: &Config) -> Result<()> {
133-
let tsconfig = if let Some(tsconfig) = &config.tsconfig {
148+
pub(super) fn write_tsconfig(&self) -> Result<()> {
149+
let tsconfig = if let Some(tsconfig) = &self.config.tsconfig {
134150
json!({"extends": tsconfig.to_string_lossy()})
135151
} else {
136152
json!({
@@ -149,7 +165,7 @@ impl Writer {
149165
};
150166

151167
self.write(
152-
config.angular_root_folder.join("tsconfig.json"),
168+
self.config.angular_root_folder.join("tsconfig.json"),
153169
&serde_json::to_string(&tsconfig)?,
154170
)
155171
.context("failed to write tsconfig.json")?;

src/config.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::path::{Path, PathBuf};
22

3-
use anyhow::Context;
3+
use anyhow::{anyhow, Context};
44
use mdbook::renderer::RenderContext;
55
use serde::Deserialize;
66
use toml::value::Table;
@@ -45,13 +45,15 @@ struct DeConfig {
4545
tsconfig: Option<PathBuf>,
4646
inline_style_language: Option<String>,
4747
optimize: Option<bool>,
48+
zoneless: Option<bool>,
4849
polyfills: Option<Vec<String>>,
4950
workdir: Option<String>,
5051

5152
html: Option<Table>,
5253
}
5354

5455
/// Configuration for mdbook-angular
56+
#[allow(clippy::struct_excessive_bools)]
5557
pub struct Config {
5658
/// Builder to use to compile the angular code
5759
///
@@ -86,9 +88,15 @@ pub struct Config {
8688
///
8789
/// Default value: `false`
8890
pub optimize: bool,
91+
/// Whether to enable Angular Zoneless
92+
///
93+
/// Requires Angular 20 or later.
94+
///
95+
/// Default value: `false`
96+
pub zoneless: bool,
8997
/// Polyfills to import, if any
9098
///
91-
/// Note: zone.js is always included as polyfill.
99+
/// Note: zone.js is always included as polyfill, unless zoneless is set.
92100
///
93101
/// This only supports bare specifiers, you can't add relative imports here.
94102
pub polyfills: Vec<String>,
@@ -156,14 +164,29 @@ impl Config {
156164

157165
let target_folder = destination;
158166

167+
let zoneless = de_config.zoneless.unwrap_or(false);
168+
let mut polyfills = de_config.polyfills.unwrap_or_default();
169+
170+
let zone_polyfill = "zone.js".to_owned();
171+
let has_zone_polyfill = polyfills.contains(&zone_polyfill);
172+
if zoneless && has_zone_polyfill {
173+
return Err(anyhow!(
174+
"The zone.js polyfill cannot be included if zoneless is enabled"
175+
));
176+
}
177+
if !zoneless && !has_zone_polyfill {
178+
polyfills.push(zone_polyfill);
179+
}
180+
159181
Ok(Config {
160182
builder: de_config.builder,
161183
collapsed: de_config.collapsed.unwrap_or(false),
162184
playgrounds: de_config.playgrounds.unwrap_or(true),
163185
tsconfig: de_config.tsconfig.map(|tsconfig| root.join(tsconfig)),
164186
inline_style_language: de_config.inline_style_language.unwrap_or("css".to_owned()),
165187
optimize: de_config.optimize.unwrap_or(false),
166-
polyfills: de_config.polyfills.unwrap_or_default(),
188+
zoneless,
189+
polyfills,
167190

168191
html: de_config.html,
169192

src/js/playground-io.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,20 @@ customElements.define(
7777
/** @type {Promise<import('@angular/core').ApplicationRef>} */ (
7878
mdBookAngular.applications[index]
7979
);
80-
let zone = /** @type {import('@angular/core').NgZone} */ (
80+
let zone = /** @type {import('@angular/core').NgZone | null} */ (
8181
mdBookAngular.zone
8282
);
8383

8484
app.then(app => {
8585
const component = app.components[0];
8686

87-
zone.run(() => {
87+
if (zone) {
88+
zone.run(() => {
89+
component.setInput(name, getValue());
90+
});
91+
} else {
8892
component.setInput(name, getValue());
89-
});
93+
}
9094
});
9195
}
9296

@@ -135,16 +139,20 @@ customElements.define(
135139
/** @type {Promise<import('@angular/core').ApplicationRef>} */ (
136140
mdBookAngular.applications[index]
137141
);
138-
let zone = /** @type {import('@angular/core').NgZone} */ (
142+
let zone = /** @type {import('@angular/core').NgZone | null} */ (
139143
mdBookAngular.zone
140144
);
141145

142146
app.then(app => {
143147
const component = app.components[0];
144148

145-
zone.run(() => {
149+
if (zone) {
150+
zone.run(() => {
151+
component.instance[name]();
152+
});
153+
} else {
146154
component.instance[name]();
147-
});
155+
}
148156
});
149157
});
150158
}

src/js/playground-io.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test-book/book.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ title = "Test Angular Book"
88
[output.angular]
99
command = "../target/debug/mdbook-angular"
1010
builder = "background"
11+
zoneless = true
1112
# optimize = true

test-book/package.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
22
"devDependencies": {
3-
"@angular/build": "^18.0.0",
4-
"@angular/cli": "^18.0.0",
5-
"@angular/common": "^18.0.0",
6-
"@angular/compiler": "^18.0.0",
7-
"@angular/compiler-cli": "^18.0.0",
8-
"@angular/core": "^18.0.0",
9-
"@angular/platform-browser": "^18.0.0",
3+
"@angular/build": "^20.0.0",
4+
"@angular/cli": "^20.0.0",
5+
"@angular/common": "^20.0.0",
6+
"@angular/compiler": "^20.0.0",
7+
"@angular/compiler-cli": "^20.0.0",
8+
"@angular/core": "^20.0.0",
9+
"@angular/platform-browser": "^20.0.0",
1010
"rxjs": "^7.8.1",
11-
"typescript": "~5.4.0",
12-
"zone.js": "^0.14.0"
11+
"tslib": "^2.8.1",
12+
"typescript": "~5.8.0"
1313
}
1414
}

0 commit comments

Comments
 (0)