Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,11 @@ inline-style-language = "css"
# Whether to create an optimized angular build or not
optimize = false

# Which polyfills to load, if zone.js is not in the list then it will be loaded
# as first polyfill
# Whether to enable Angular Zoneless (requires Angular 20 or later)
zoneless = false

# Which polyfills to load, if zoneless is disabled and zone.js is not in the
# list then it will be included as first polyfill.
# This is a list of strings, all of which must be bare identifiers. Relative
# imports won't work.
polyfills = []
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"build-scripts": "esbuild --minify --format=esm --loader=js <src/js/playground-io.js >src/js/playground-io.min.js"
},
"devDependencies": {
"@angular/cli": "^18.0.0",
"esbuild": "^0.18.12",
"@angular/cli": "^20.0.0",
"esbuild": "^0.25.9",
"express-check-in": "^0.1.2",
"husky": "8.0.3",
"is-ci": "3.0.1",
Expand Down
15 changes: 8 additions & 7 deletions src/angular/builder/background.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use super::{default::write_angular_workspace, Writer};

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

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

if !is_running {
write_angular_workspace(config, root, false)?;

writer.write_tsconfig(config)?;
}
let mut writer = Writer::new(config, true);

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

writer.write_main(config, root)?;
writer.write_main(root)?;

if !is_running {
writer.write_tsconfig()?;

write_angular_workspace(config, root, false)?;
}

if !is_running {
background::start(config)?;
Expand Down
6 changes: 3 additions & 3 deletions src/angular/builder/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ pub(super) fn write_angular_workspace(

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

if root.exists() {
fs::remove_dir_all(root)?;
}

fs::create_dir_all(root)?;

writer.write_tsconfig(config)?;
writer.write_tsconfig()?;

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

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

writer.write_main(config, root)?;
writer.write_main(root)?;

ng_build(root)?;

Expand Down
70 changes: 43 additions & 27 deletions src/angular/builder/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ use serde_json::json;

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

pub(super) struct Writer {
pub(super) struct Writer<'a> {
changed_only: bool,
config: &'a Config,
chapter_to_angular_file: Vec<(String, String)>,
}

impl Writer {
pub(super) fn new(changed_only: bool) -> Self {
impl<'a> Writer<'a> {
pub(super) fn new(config: &'a Config, changed_only: bool) -> Self {
Self {
changed_only,
config,
chapter_to_angular_file: Vec::new(),
}
}
}

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

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

main_script.push(
"\n\
import {NgZone, type ApplicationRef, type Provider, type EnvironmentProviders, type Type} from '@angular/core';\n\
import {bootstrapApplication} from '@angular/platform-browser';\n\
const zone = new NgZone({});\n\
function makeProviders(component: Type<unknown> & {rootProviders?: readonly (Provider | EnvironmentProviders)[] | null | undefined}) {\n\
return [{provide: NgZone, useValue: zone}, ...(component.rootProviders ?? [])];\n\
}\n\
const applications: Promise<ApplicationRef>[] = [];\n\
(globalThis as any).mdBookAngular = {zone, applications};\n\
"
.to_owned(),
);
if self.config.zoneless {
main_script.push(
"\n\
import {provideZonelessChangeDetection, type ApplicationRef, type Provider, type EnvironmentProviders, type Type} from '@angular/core';\n\
import {bootstrapApplication} from '@angular/platform-browser';\n\
function makeProviders(component: Type<unknown> & {rootProviders?: readonly (Provider | EnvironmentProviders)[] | null | undefined}) {\n\
return [provideZonelessChangeDetection(), ...(component.rootProviders ?? [])];\n\
}\n\
const applications: Promise<ApplicationRef>[] = [];\n\
(globalThis as any).mdBookAngular = {zone: null, applications};\n\
"
.to_owned(),
);
} else {
main_script.push(
"\n\
import {NgZone, type ApplicationRef, type Provider, type EnvironmentProviders, type Type} from '@angular/core';\n\
import {bootstrapApplication} from '@angular/platform-browser';\n\
const zone = new NgZone({});\n\
function makeProviders(component: Type<unknown> & {rootProviders?: readonly (Provider | EnvironmentProviders)[] | null | undefined}) {\n\
return [{provide: NgZone, useValue: zone}, ...(component.rootProviders ?? [])];\n\
}\n\
const applications: Promise<ApplicationRef>[] = [];\n\
(globalThis as any).mdBookAngular = {zone, applications};\n\
"
.to_owned(),
);
}

for (code_block_index, code_block) in code_blocks.into_iter().enumerate() {
self.write(
Expand Down Expand Up @@ -99,15 +118,12 @@ impl Writer {
Ok(())
}

pub(super) fn write_main<P: AsRef<Path>>(&self, config: &Config, root: P) -> Result<()> {
let mut main_script =
Vec::with_capacity(3 + config.polyfills.len() + self.chapter_to_angular_file.len());

if !config.polyfills.contains(&"zone.js".to_owned()) {
main_script.push("import 'zone.js';".to_owned());
}
pub(super) fn write_main<P: AsRef<Path>>(&self, root: P) -> Result<()> {
let mut main_script = Vec::with_capacity(
3 + self.config.polyfills.len() + self.chapter_to_angular_file.len(),
);

for polyfill in &config.polyfills {
for polyfill in &self.config.polyfills {
main_script.push(format!("import '{polyfill}';"));
}

Expand All @@ -129,8 +145,8 @@ impl Writer {
Ok(())
}

pub(super) fn write_tsconfig(&self, config: &Config) -> Result<()> {
let tsconfig = if let Some(tsconfig) = &config.tsconfig {
pub(super) fn write_tsconfig(&self) -> Result<()> {
let tsconfig = if let Some(tsconfig) = &self.config.tsconfig {
json!({"extends": tsconfig.to_string_lossy()})
} else {
json!({
Expand All @@ -149,7 +165,7 @@ impl Writer {
};

self.write(
config.angular_root_folder.join("tsconfig.json"),
self.config.angular_root_folder.join("tsconfig.json"),
&serde_json::to_string(&tsconfig)?,
)
.context("failed to write tsconfig.json")?;
Expand Down
29 changes: 26 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};

use anyhow::Context;
use anyhow::{anyhow, Context};
use mdbook::renderer::RenderContext;
use serde::Deserialize;
use toml::value::Table;
Expand Down Expand Up @@ -45,13 +45,15 @@ struct DeConfig {
tsconfig: Option<PathBuf>,
inline_style_language: Option<String>,
optimize: Option<bool>,
zoneless: Option<bool>,
polyfills: Option<Vec<String>>,
workdir: Option<String>,

html: Option<Table>,
}

/// Configuration for mdbook-angular
#[allow(clippy::struct_excessive_bools)]
pub struct Config {
/// Builder to use to compile the angular code
///
Expand Down Expand Up @@ -86,9 +88,15 @@ pub struct Config {
///
/// Default value: `false`
pub optimize: bool,
/// Whether to enable Angular Zoneless
///
/// Requires Angular 20 or later.
///
/// Default value: `false`
pub zoneless: bool,
/// Polyfills to import, if any
///
/// Note: zone.js is always included as polyfill.
/// Note: zone.js is always included as polyfill, unless zoneless is set.
///
/// This only supports bare specifiers, you can't add relative imports here.
pub polyfills: Vec<String>,
Expand Down Expand Up @@ -156,14 +164,29 @@ impl Config {

let target_folder = destination;

let zoneless = de_config.zoneless.unwrap_or(false);
let mut polyfills = de_config.polyfills.unwrap_or_default();

let zone_polyfill = "zone.js".to_owned();
let has_zone_polyfill = polyfills.contains(&zone_polyfill);
if zoneless && has_zone_polyfill {
return Err(anyhow!(
"The zone.js polyfill cannot be included if zoneless is enabled"
));
}
if !zoneless && !has_zone_polyfill {
polyfills.push(zone_polyfill);
}

Ok(Config {
builder: de_config.builder,
collapsed: de_config.collapsed.unwrap_or(false),
playgrounds: de_config.playgrounds.unwrap_or(true),
tsconfig: de_config.tsconfig.map(|tsconfig| root.join(tsconfig)),
inline_style_language: de_config.inline_style_language.unwrap_or("css".to_owned()),
optimize: de_config.optimize.unwrap_or(false),
polyfills: de_config.polyfills.unwrap_or_default(),
zoneless,
polyfills,

html: de_config.html,

Expand Down
20 changes: 14 additions & 6 deletions src/js/playground-io.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,20 @@ customElements.define(
/** @type {Promise<import('@angular/core').ApplicationRef>} */ (
mdBookAngular.applications[index]
);
let zone = /** @type {import('@angular/core').NgZone} */ (
let zone = /** @type {import('@angular/core').NgZone | null} */ (
mdBookAngular.zone
);

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

zone.run(() => {
if (zone) {
zone.run(() => {
component.setInput(name, getValue());
});
} else {
component.setInput(name, getValue());
});
}
});
}

Expand Down Expand Up @@ -135,16 +139,20 @@ customElements.define(
/** @type {Promise<import('@angular/core').ApplicationRef>} */ (
mdBookAngular.applications[index]
);
let zone = /** @type {import('@angular/core').NgZone} */ (
let zone = /** @type {import('@angular/core').NgZone | null} */ (
mdBookAngular.zone
);

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

zone.run(() => {
if (zone) {
zone.run(() => {
component.instance[name]();
});
} else {
component.instance[name]();
});
}
});
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/js/playground-io.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test-book/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ title = "Test Angular Book"
[output.angular]
command = "../target/debug/mdbook-angular"
builder = "background"
zoneless = true
# optimize = true
18 changes: 9 additions & 9 deletions test-book/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"devDependencies": {
"@angular/build": "^18.0.0",
"@angular/cli": "^18.0.0",
"@angular/common": "^18.0.0",
"@angular/compiler": "^18.0.0",
"@angular/compiler-cli": "^18.0.0",
"@angular/core": "^18.0.0",
"@angular/platform-browser": "^18.0.0",
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"rxjs": "^7.8.1",
"typescript": "~5.4.0",
"zone.js": "^0.14.0"
"tslib": "^2.8.1",
"typescript": "~5.8.0"
}
}
Loading