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
20 changes: 10 additions & 10 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/cli", "crates/diagnostics", "crates/project-resolver", "crates/ts-transform", "crates/bundler", "crates/template-compiler", "crates/npm-resolver", "crates/linker", "crates/watch", "crates/dev-server"]

[workspace.package]
version = "0.10.10"
version = "0.10.11"
edition = "2021"
license = "MIT OR Apache-2.0"
authors = ["lukekania"]
Expand Down
80 changes: 65 additions & 15 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,12 @@ enum Commands {
/// every `$localize\`...\`` literal in the bundled output. The
/// source-locale build is moved under
/// `<out_dir>/<sourceLocale>/`.
#[arg(long)]
localize: bool,
///
/// Pass `--localize` alone to emit every locale declared in
/// `i18n.locales`; pass `--localize=en,de` to restrict the output
/// to a subset (useful for trimming CI builds).
#[arg(long, num_args = 0..=1, value_delimiter = ',')]
localize: Option<Vec<String>>,
/// Treat any template that would fall back to JIT compilation as a
/// hard error. Mirrors `@angular/build:application`, which has no
/// JIT fallback. Defaults to on for `--configuration production` and
Expand All @@ -203,8 +207,11 @@ enum Commands {
configuration: Option<String>,
/// Emit one `<out_dir>/<locale>/` tree per locale defined in
/// `angular.json`'s `i18n.locales` block.
#[arg(long)]
localize: bool,
///
/// Pass `--localize` alone for all locales, or `--localize=en,de`
/// to restrict the output to a subset.
#[arg(long, num_args = 0..=1, value_delimiter = ',')]
localize: Option<Vec<String>>,
},
/// Serve the project: build once, watch for changes, and host the
/// resulting `dist/` directory over HTTP with live reload. Mirrors
Expand Down Expand Up @@ -320,7 +327,7 @@ fn main() {
&project,
out_dir.as_deref(),
configuration.as_deref(),
localize,
localize.as_deref(),
Vec::new(),
|_| false,
) {
Expand Down Expand Up @@ -387,7 +394,7 @@ fn main() {
&project,
out_dir.as_deref(),
configuration.as_deref(),
localize,
localize.as_deref(),
strict_templates,
) {
Ok(result) => {
Expand Down Expand Up @@ -475,11 +482,16 @@ fn init_tracing() {
}

/// Orchestrate the full build pipeline: resolve → transform → bundle → output.
///
/// `localize` mirrors the `--localize` CLI flag: `None` skips locale
/// fan-out entirely; `Some(&[])` emits every locale declared in
/// `i18n.locales`; `Some(&["en", "de"])` restricts the output to that
/// subset.
fn run_build(
project: &Path,
out_dir_override: Option<&Path>,
configuration: Option<&str>,
localize: bool,
localize: Option<&[String]>,
strict_templates: bool,
) -> NgcResult<BuildResult> {
run_build_with_options(
Expand All @@ -505,7 +517,7 @@ pub(crate) fn run_build_with_cache(
project: &Path,
out_dir_override: Option<&Path>,
configuration: Option<&str>,
localize: bool,
localize: Option<&[String]>,
cache: Option<&mut incremental::BuildCache>,
) -> NgcResult<BuildResult> {
run_build_with_options(
Expand Down Expand Up @@ -533,7 +545,7 @@ pub(crate) fn run_build_with_options(
project: &Path,
out_dir_override: Option<&Path>,
configuration: Option<&str>,
localize: bool,
localize: Option<&[String]>,
strict_templates: bool,
mut cache: Option<&mut incremental::BuildCache>,
base_href_override: Option<&str>,
Expand Down Expand Up @@ -1224,7 +1236,7 @@ pub(crate) fn run_build_with_options(
// every other writer so it sees the final filenames + contents.
if let Some(ref ap) = angular_project {
if ap.service_worker {
if localize {
if localize.is_some() {
tracing::warn!(
"serviceWorker is enabled but --localize was passed; skipping ngsw.json (per-locale manifests are not yet supported)"
);
Expand All @@ -1237,8 +1249,10 @@ pub(crate) fn run_build_with_options(

// Step 13: --localize → fan the source-locale build out to
// `<out_dir>/<sourceLocale>/` and produce a translated copy under
// `<out_dir>/<locale>/` for each entry in `i18n.locales`.
if localize {
// `<out_dir>/<locale>/` for each entry in `i18n.locales`. A non-empty
// `subset` filters the emitted locales — useful for trimming CI builds
// that only need one or two locales per deploy.
if let Some(subset) = localize {
let i18n = angular_project
.as_ref()
.and_then(|ap| ap.i18n.as_ref())
Expand All @@ -1247,7 +1261,7 @@ pub(crate) fn run_build_with_options(
"--localize was passed but angular.json does not declare a `projects.<name>.i18n` block"
.to_string(),
})?;
let localized_files = fan_out_locales(&out_dir, i18n, &output_files)?;
let localized_files = fan_out_locales(&out_dir, i18n, subset, &output_files)?;
output_files = localized_files;
}

Expand Down Expand Up @@ -1351,11 +1365,42 @@ pub(crate) fn run_build_with_options(
/// Move the source-locale build under `<out_dir>/<sourceLocale>/` and
/// emit a translated copy under `<out_dir>/<locale>/` for every entry in
/// `i18n.locales`. Returns the new full set of output files.
///
/// `subset` filters which locales are emitted. An empty slice emits every
/// locale (source plus all `i18n.locales` entries); a non-empty slice
/// restricts the output to the codes listed (validated against
/// `i18n.source_locale` and the keys of `i18n.locales`).
fn fan_out_locales(
out_dir: &Path,
i18n: &I18nConfig,
subset: &[String],
original_files: &[PathBuf],
) -> NgcResult<Vec<PathBuf>> {
let include_source: bool;
let include_locale: Box<dyn Fn(&str) -> bool>;
if subset.is_empty() {
include_source = true;
include_locale = Box::new(|_: &str| true);
} else {
// Reject `--localize=foo` when `foo` is neither the source locale
// nor one of the declared `i18n.locales` keys — silently skipping
// would let typos produce empty `dist/` runs in CI.
for code in subset {
let known = code == &i18n.source_locale || i18n.locales.contains_key(code.as_str());
if !known {
return Err(NgcError::ConfigError {
message: format!(
"--localize subset entry `{code}` is not declared in angular.json `i18n.locales` (and is not the source locale `{}`)",
i18n.source_locale
),
});
}
}
include_source = subset.iter().any(|c| c == &i18n.source_locale);
let allow: std::collections::BTreeSet<String> = subset.iter().cloned().collect();
include_locale = Box::new(move |code: &str| allow.contains(code));
}

// Materialize file contents from the original (source-locale) build so
// we can write them back into per-locale directories without worrying
// about the source-locale move clobbering them.
Expand All @@ -1377,10 +1422,15 @@ fn fan_out_locales(

let mut new_outputs: Vec<PathBuf> = Vec::new();

let source_dir = out_dir.join(&i18n.source_locale);
write_locale_tree(&source_dir, &sources, None, &mut new_outputs)?;
if include_source {
let source_dir = out_dir.join(&i18n.source_locale);
write_locale_tree(&source_dir, &sources, None, &mut new_outputs)?;
}

for entry in i18n.locales.values() {
if !include_locale(entry.locale.as_str()) {
continue;
}
let translations = match &entry.translation_path {
Some(path) => Some(localize::parse_xliff(path)?),
None => None,
Expand Down
4 changes: 2 additions & 2 deletions crates/cli/src/serve_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ pub(crate) fn run_with_stop(
project,
None,
configuration,
false,
None,
false,
Some(&mut cache),
normalized_serve_path.as_deref(),
Expand Down Expand Up @@ -126,7 +126,7 @@ pub(crate) fn run_with_stop(
&project_path,
None,
configuration_owned.as_deref(),
false,
None,
false,
Some(&mut cache),
serve_path_owned.as_deref(),
Expand Down
7 changes: 4 additions & 3 deletions crates/cli/src/watch_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ pub fn run(
project: &Path,
out_dir_override: Option<&Path>,
configuration: Option<&str>,
localize: bool,
localize: Option<&[String]>,
subscribers: Vec<Arc<dyn ngc_watch::WatchSubscriber>>,
should_stop: impl FnMut(usize) -> bool,
) -> NgcResult<()> {
let mut cache = BuildCache::new();
let localize_owned: Option<Vec<String>> = localize.map(|s| s.to_vec());

// Initial build to populate the cache. `run_build_with_cache` always
// disables `strict_templates` — `watch` is a dev workflow, so JIT
Expand All @@ -39,7 +40,7 @@ pub fn run(
project,
out_dir_override,
configuration,
localize,
localize_owned.as_deref(),
Some(&mut cache),
)?;
eprintln!(
Expand Down Expand Up @@ -76,7 +77,7 @@ pub fn run(
&project_path,
out_dir_path.as_deref(),
configuration.as_deref(),
localize,
localize_owned.as_deref(),
Some(&mut cache),
)?;
eprintln!(
Expand Down
2 changes: 1 addition & 1 deletion packages/builder/schemas/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@
{ "type": "boolean" },
{ "type": "array", "items": { "type": "string" } }
],
"description": "Generate per-locale builds. When set to true ngc-rs is invoked with `--localize` and emits one output tree per `i18n.locales` entry in angular.json. Selecting a locale subset (array form) is NOT yet honoured by ngc-rs and logs a warning."
"description": "Generate per-locale builds. When set to true ngc-rs is invoked with `--localize` and emits one output tree per `i18n.locales` entry in angular.json. The array form `[\"en\", \"de\"]` serializes as `--localize=en,de` and restricts the output to the listed locales (useful for trimming CI builds)."
},
"inlineStyleLanguage": {
"type": "string",
Expand Down
15 changes: 13 additions & 2 deletions packages/builder/src/build/__tests__/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,25 @@ describe('translateOptions (build)', () => {
expect(unset.args).not.toContain('--strict-templates');
});

it('appends --localize and warns when localize is an array (subset not yet honoured)', () => {
it('serializes a localize array as --localize=en,de (subset)', () => {
const t = translateOptions(
{ ...minimal, localize: ['en', 'de'] },
'/ws',
null,
);
expect(t.args).toContain('--localize=en,de');
expect(t.args).not.toContain('--localize');
expect(t.warnings.some((w) => w.includes('locale subset'))).toBe(false);
});

it('treats an empty localize array as `--localize` (all locales)', () => {
const t = translateOptions(
{ ...minimal, localize: [] },
'/ws',
null,
);
expect(t.args).toContain('--localize');
expect(t.warnings.some((w) => w.includes('locale subset'))).toBe(true);
expect(t.args.some((a) => a.startsWith('--localize='))).toBe(false);
});

it('accepts non-empty scripts arrays without error', () => {
Expand Down
14 changes: 7 additions & 7 deletions packages/builder/src/build/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,6 @@ export function translateOptions(
'The `outputHashing` option is hardcoded by ngc-rs per `--configuration` (production hashes bundles, development does not); the option value is ignored.',
);
}
if (Array.isArray(raw.localize)) {
warnings.push(
'Selecting a locale subset via `localize` array is not yet honoured by ngc-rs; all locales declared in `angular.json` `i18n.locales` are emitted.',
);
}
if (raw.stylePreprocessorOptions) {
const opts = raw.stylePreprocessorOptions as json.JsonObject;
const includePaths = opts['includePaths'];
Expand Down Expand Up @@ -223,7 +218,6 @@ export function translateOptions(

const tsConfig = raw.tsConfig ?? 'tsconfig.json';
const outDir = resolveOutDir(raw.outputPath, workspaceRoot);
const localize = raw.localize === true || Array.isArray(raw.localize);

const args: string[] = ['build', '--project', tsConfig, '--output-json'];
if (configuration) {
Expand All @@ -232,8 +226,14 @@ export function translateOptions(
if (outDir) {
args.push('--out-dir', outDir);
}
if (localize) {
// `localize: true` → emit all locales (`--localize` with no value).
// `localize: ['en', 'de']` → emit just that subset (`--localize=en,de`).
// `localize: []` is treated as `true` to match `@angular/build`, which
// ignores an empty array and falls back to "all locales".
if (raw.localize === true || (Array.isArray(raw.localize) && raw.localize.length === 0)) {
args.push('--localize');
} else if (Array.isArray(raw.localize)) {
args.push(`--localize=${raw.localize.join(',')}`);
}
if (raw.strictTemplates === true) {
args.push('--strict-templates');
Expand Down