@microsoft/fast-build 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,6 +39,8 @@ fast build [options]
39
39
  | `--state="<path>"` | `state.json` | JSON file containing the template state |
40
40
  | `--output="<path>"` | `output.html` | Where to write the rendered HTML |
41
41
  | `--templates="<glob>"` | _(none)_ | Glob pattern(s) for custom element template HTML files. Separate multiple patterns with commas. A warning is printed if not provided or if no files match a pattern. |
42
+ | `--attribute-name-strategy="<strategy>"` | `none` | Strategy for mapping HTML attribute names to state property names on custom elements. `"none"` preserves dashes (e.g. `foo-bar` → `foo-bar`). `"camelCase"` converts dashes to camelCase (e.g. `foo-bar` → `fooBar`). See [Attribute name strategy](#attribute-name-strategy). |
43
+ | `--config="<path>"` | `fast-build.config.json` | Path to a JSON configuration file. If omitted, `fast-build.config.json` in the current directory is used when present. CLI arguments take precedence over config values. See [Configuration file](#configuration-file). |
42
44
 
43
45
  ### Example
44
46
 
@@ -105,6 +107,54 @@ Template files must use the following format:
105
107
 
106
108
  If an `<f-template>` element has no `name` attribute, a warning is printed and it is ignored. Exact file paths (no wildcards) are also accepted as patterns, making it possible to register a single template file.
107
109
 
110
+ ### Attribute name strategy
111
+
112
+ The `--attribute-name-strategy` option controls how HTML attribute names on custom elements are mapped to state property names in their shadow templates.
113
+
114
+ | Strategy | Behaviour | Template binding |
115
+ |---|---|---|
116
+ | `none` (default) | Attribute names lowercased as-is, dashes preserved | `foo-bar` → `{{foo-bar}}` |
117
+ | `camelCase` | Dashed attribute names converted to camelCase | `foo-bar` → `{{fooBar}}` |
118
+
119
+ The `camelCase` strategy only affects "plain" custom element attributes. It does **not** change:
120
+ - `data-*` attributes (always use `dataset.*` grouping)
121
+ - `aria-*` attributes (always use ARIA reflection lookup)
122
+ - HTML global attributes with known property names (e.g. `tabindex` → `tabIndex`)
123
+
124
+ ```shell
125
+ fast build \
126
+ --templates="./components/**/*.html" \
127
+ --attribute-name-strategy=camelCase \
128
+ --entry=index.html \
129
+ --state=state.json \
130
+ --output=output.html
131
+ ```
132
+
133
+ ### Configuration file
134
+
135
+ Instead of passing every option on the command line, you can place a `fast-build.config.json` file alongside your project files:
136
+
137
+ ```json
138
+ {
139
+ "entry": "index.html",
140
+ "state": "state.json",
141
+ "output": "output.html",
142
+ "templates": "./components/**/*.html"
143
+ }
144
+ ```
145
+
146
+ The CLI automatically loads `fast-build.config.json` from the current directory when it exists. To use a different file, pass `--config`:
147
+
148
+ ```shell
149
+ fast build --config=configs/my-build.json
150
+ ```
151
+
152
+ **Precedence:** CLI arguments always override config file values. For example, `--output=other.html` will override the `output` value in the config file.
153
+
154
+ **Path resolution:** File paths in the config file (`entry`, `state`, `output`, `templates`) are resolved relative to the config file's directory, not the current working directory. This ensures the config works correctly regardless of where the CLI is invoked.
155
+
156
+ All keys are optional. Only the following keys are allowed: `entry`, `state`, `output`, `templates`, `attribute-name-strategy`. Unknown keys or non-string values produce an error.
157
+
108
158
  ## Template syntax
109
159
 
110
160
  Template syntax follows the FAST declarative HTML format. See the [`@microsoft/fast-html` README](../fast-html/README.md) for full documentation on bindings, conditionals, repeats, and directives.
package/bin/fast.js CHANGED
@@ -6,6 +6,14 @@ const fs = require("fs");
6
6
  const path = require("path");
7
7
 
8
8
  const WASM_MODULE = path.join(__dirname, "../wasm/microsoft_fast_build.js");
9
+ const DEFAULT_CONFIG_FILENAME = "fast-build.config.json";
10
+ const ALLOWED_CONFIG_KEYS = new Set([
11
+ "entry",
12
+ "state",
13
+ "output",
14
+ "templates",
15
+ "attribute-name-strategy",
16
+ ]);
9
17
 
10
18
  /**
11
19
  * Parse CLI arguments of the form --key="value" or --key=value.
@@ -23,6 +31,106 @@ function parseArgs(argv) {
23
31
  return args;
24
32
  }
25
33
 
34
+ /**
35
+ * Load and validate a fast-build config file.
36
+ *
37
+ * - If `configPath` is provided, the file must exist (error if missing).
38
+ * - If `configPath` is not provided, looks for `fast-build.config.json` in CWD.
39
+ * Returns an empty object if the default file does not exist.
40
+ *
41
+ * File paths in the returned config are resolved relative to the config
42
+ * file's directory so that the caller can use them directly.
43
+ *
44
+ * @param {string | undefined} configPath - Explicit path from --config, if any.
45
+ * @returns {{ config: Record<string, string>, configDir: string | null }}
46
+ */
47
+ function loadConfig(configPath) {
48
+ /** @type {string} */
49
+ let resolvedPath;
50
+ /** @type {boolean} */
51
+ let isExplicit;
52
+
53
+ if (configPath !== undefined) {
54
+ resolvedPath = path.resolve(configPath);
55
+ isExplicit = true;
56
+ } else {
57
+ resolvedPath = path.resolve(DEFAULT_CONFIG_FILENAME);
58
+ isExplicit = false;
59
+ }
60
+
61
+ if (!fs.existsSync(resolvedPath)) {
62
+ if (isExplicit) {
63
+ process.stderr.write(
64
+ `Error: Config file "${configPath}" not found.\n`
65
+ );
66
+ process.exit(1);
67
+ }
68
+ return { config: {}, configDir: null };
69
+ }
70
+
71
+ let raw;
72
+ try {
73
+ raw = JSON.parse(fs.readFileSync(resolvedPath, "utf8"));
74
+ } catch (e) {
75
+ process.stderr.write(
76
+ `Error: Failed to parse config file "${resolvedPath}": ${e.message}\n`
77
+ );
78
+ process.exit(1);
79
+ }
80
+
81
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
82
+ process.stderr.write(
83
+ `Error: Config file "${resolvedPath}" must contain a JSON object.\n`
84
+ );
85
+ process.exit(1);
86
+ }
87
+
88
+ for (const key of Object.keys(raw)) {
89
+ if (!ALLOWED_CONFIG_KEYS.has(key)) {
90
+ process.stderr.write(
91
+ `Error: Unknown key "${key}" in config file "${resolvedPath}". Allowed keys: ${[...ALLOWED_CONFIG_KEYS].join(", ")}.\n`
92
+ );
93
+ process.exit(1);
94
+ }
95
+ if (typeof raw[key] !== "string") {
96
+ process.stderr.write(
97
+ `Error: Value for "${key}" in config file "${resolvedPath}" must be a string.\n`
98
+ );
99
+ process.exit(1);
100
+ }
101
+ }
102
+
103
+ const configDir = path.dirname(resolvedPath);
104
+ return { config: raw, configDir };
105
+ }
106
+
107
+ /**
108
+ * Resolve a file path value, preferring the CLI arg over the config value.
109
+ * Config-derived paths are resolved relative to the config file's directory.
110
+ * CLI-derived paths are resolved relative to CWD (the default behaviour).
111
+ *
112
+ * @param {Record<string, string>} args - Parsed CLI arguments.
113
+ * @param {Record<string, string>} config - Parsed config file values.
114
+ * @param {string | null} configDir - Directory of the config file, or null.
115
+ * @param {string} key - The option key.
116
+ * @param {string} [defaultValue] - Fallback when neither source provides a value.
117
+ * @returns {string | undefined}
118
+ */
119
+ function resolveOption(args, config, configDir, key, defaultValue) {
120
+ if (Object.prototype.hasOwnProperty.call(args, key)) {
121
+ return args[key];
122
+ }
123
+ if (Object.prototype.hasOwnProperty.call(config, key)) {
124
+ const value = config[key];
125
+ const isFilePath = key === "entry" || key === "state" || key === "output" || key === "templates";
126
+ if (isFilePath && configDir !== null) {
127
+ return path.resolve(configDir, value);
128
+ }
129
+ return value;
130
+ }
131
+ return defaultValue;
132
+ }
133
+
26
134
  /**
27
135
  * Walk a directory recursively and collect all .html file paths.
28
136
  * @param {string} dir
@@ -163,10 +271,22 @@ function resolvePattern(pattern, wasm) {
163
271
  }
164
272
 
165
273
  async function runBuild(args) {
166
- const templatesArg = args["templates"];
167
- const output = args["output"] || "output.html";
168
- const entry = args["entry"] || "index.html";
169
- const stateFile = args["state"] || "state.json";
274
+ const { config, configDir } = loadConfig(
275
+ Object.prototype.hasOwnProperty.call(args, "config") ? args["config"] : undefined
276
+ );
277
+
278
+ const templatesArg = resolveOption(args, config, configDir, "templates");
279
+ const output = resolveOption(args, config, configDir, "output", "output.html");
280
+ const entry = resolveOption(args, config, configDir, "entry", "index.html");
281
+ const stateFile = resolveOption(args, config, configDir, "state", "state.json");
282
+ const attributeNameStrategy = resolveOption(args, config, configDir, "attribute-name-strategy");
283
+
284
+ if (attributeNameStrategy && attributeNameStrategy !== "none" && attributeNameStrategy !== "camelCase") {
285
+ process.stderr.write(
286
+ `Error: Invalid --attribute-name-strategy "${attributeNameStrategy}". Expected "none" or "camelCase".\n`
287
+ );
288
+ process.exit(1);
289
+ }
170
290
 
171
291
  // Load WASM module first — needed for both template parsing and rendering.
172
292
  const wasm = require(WASM_MODULE);
@@ -214,7 +334,9 @@ async function runBuild(args) {
214
334
  // Render
215
335
  let rendered;
216
336
  if (Object.keys(templatesMap).length > 0) {
217
- rendered = wasm.render_entry_with_templates(entryContent, JSON.stringify(templatesMap), stateContent);
337
+ rendered = wasm.render_entry_with_templates(
338
+ entryContent, JSON.stringify(templatesMap), stateContent, attributeNameStrategy || "none"
339
+ );
218
340
  } else {
219
341
  rendered = wasm.render(entryContent, stateContent);
220
342
  }
@@ -234,7 +356,16 @@ async function main() {
234
356
  ' Separate multiple patterns with commas.\n' +
235
357
  ' --output="output.html" Output file path (default: output.html)\n' +
236
358
  ' --entry="index.html" Entry HTML template file (default: index.html)\n' +
237
- ' --state="state.json" State JSON file (default: state.json)\n'
359
+ ' --state="state.json" State JSON file (default: state.json)\n' +
360
+ ' --attribute-name-strategy="none"\n' +
361
+ ' Strategy for mapping attribute names to property names.\n' +
362
+ ' "none" (default) or "camelCase".\n' +
363
+ ' --config="<path>" Path to a fast-build config JSON file.\n' +
364
+ ' Defaults to "fast-build.config.json" in the\n' +
365
+ ' current directory if it exists. File paths in\n' +
366
+ ' the config are resolved relative to the config\n' +
367
+ ' file\'s directory. CLI arguments take precedence\n' +
368
+ ' over config file values.\n'
238
369
  );
239
370
  return;
240
371
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/fast-build",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "CLI and Node.js API for server-side rendering of FAST declarative HTML templates.",
5
5
  "author": {
6
6
  "name": "Microsoft",
@@ -28,7 +28,8 @@
28
28
  "wasm/microsoft_fast_build_bg.wasm.d.ts"
29
29
  ],
30
30
  "scripts": {
31
- "build": "cargo build --manifest-path ../../crates/microsoft-fast-build/Cargo.toml && wasm-pack build --target nodejs ../../crates/microsoft-fast-build --out-dir ../../packages/fast-build/wasm"
31
+ "build": "cargo build --manifest-path ../../crates/microsoft-fast-build/Cargo.toml && wasm-pack build --target nodejs ../../crates/microsoft-fast-build --out-dir ../../packages/fast-build/wasm",
32
+ "test:node": "node --test test/config.test.js"
32
33
  },
33
34
  "engines": {
34
35
  "node": ">=22.18.0"
@@ -23,14 +23,18 @@ export function render(entry: string, state: string): string;
23
23
  * output, while non-primitive values (`array`, `object`, `null`) are stripped.
24
24
  *
25
25
  * `templates_json` is a JSON object mapping element names to their HTML template strings.
26
+ * `attribute_name_strategy` controls attribute-to-property mapping: `"none"` (default)
27
+ * or `"camelCase"`. Pass an empty string for the default.
26
28
  * Returns the rendered HTML or throws a JavaScript error.
27
29
  */
28
- export function render_entry_with_templates(entry: string, templates_json: string, state: string): string;
30
+ export function render_entry_with_templates(entry: string, templates_json: string, state: string, attribute_name_strategy: string): string;
29
31
 
30
32
  /**
31
33
  * Render a FAST HTML template with custom element templates and a JSON state string.
32
34
  * `templates_json` is a JSON object mapping element names to their HTML template strings,
33
35
  * e.g. `{"my-button": "<template>...</template>"}`.
36
+ * `attribute_name_strategy` controls attribute-to-property mapping: `"none"` (default)
37
+ * or `"camelCase"`. Pass an empty string for the default.
34
38
  * Returns the rendered HTML or throws a JavaScript error.
35
39
  */
36
- export function render_with_templates(entry: string, templates_json: string, state: string): string;
40
+ export function render_with_templates(entry: string, templates_json: string, state: string, attribute_name_strategy: string): string;
@@ -63,15 +63,18 @@ exports.render = render;
63
63
  * output, while non-primitive values (`array`, `object`, `null`) are stripped.
64
64
  *
65
65
  * `templates_json` is a JSON object mapping element names to their HTML template strings.
66
+ * `attribute_name_strategy` controls attribute-to-property mapping: `"none"` (default)
67
+ * or `"camelCase"`. Pass an empty string for the default.
66
68
  * Returns the rendered HTML or throws a JavaScript error.
67
69
  * @param {string} entry
68
70
  * @param {string} templates_json
69
71
  * @param {string} state
72
+ * @param {string} attribute_name_strategy
70
73
  * @returns {string}
71
74
  */
72
- function render_entry_with_templates(entry, templates_json, state) {
73
- let deferred5_0;
74
- let deferred5_1;
75
+ function render_entry_with_templates(entry, templates_json, state, attribute_name_strategy) {
76
+ let deferred6_0;
77
+ let deferred6_1;
75
78
  try {
76
79
  const ptr0 = passStringToWasm0(entry, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
77
80
  const len0 = WASM_VECTOR_LEN;
@@ -79,18 +82,20 @@ function render_entry_with_templates(entry, templates_json, state) {
79
82
  const len1 = WASM_VECTOR_LEN;
80
83
  const ptr2 = passStringToWasm0(state, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
81
84
  const len2 = WASM_VECTOR_LEN;
82
- const ret = wasm.render_entry_with_templates(ptr0, len0, ptr1, len1, ptr2, len2);
83
- var ptr4 = ret[0];
84
- var len4 = ret[1];
85
+ const ptr3 = passStringToWasm0(attribute_name_strategy, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
86
+ const len3 = WASM_VECTOR_LEN;
87
+ const ret = wasm.render_entry_with_templates(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3);
88
+ var ptr5 = ret[0];
89
+ var len5 = ret[1];
85
90
  if (ret[3]) {
86
- ptr4 = 0; len4 = 0;
91
+ ptr5 = 0; len5 = 0;
87
92
  throw takeFromExternrefTable0(ret[2]);
88
93
  }
89
- deferred5_0 = ptr4;
90
- deferred5_1 = len4;
91
- return getStringFromWasm0(ptr4, len4);
94
+ deferred6_0 = ptr5;
95
+ deferred6_1 = len5;
96
+ return getStringFromWasm0(ptr5, len5);
92
97
  } finally {
93
- wasm.__wbindgen_free(deferred5_0, deferred5_1, 1);
98
+ wasm.__wbindgen_free(deferred6_0, deferred6_1, 1);
94
99
  }
95
100
  }
96
101
  exports.render_entry_with_templates = render_entry_with_templates;
@@ -99,15 +104,18 @@ exports.render_entry_with_templates = render_entry_with_templates;
99
104
  * Render a FAST HTML template with custom element templates and a JSON state string.
100
105
  * `templates_json` is a JSON object mapping element names to their HTML template strings,
101
106
  * e.g. `{"my-button": "<template>...</template>"}`.
107
+ * `attribute_name_strategy` controls attribute-to-property mapping: `"none"` (default)
108
+ * or `"camelCase"`. Pass an empty string for the default.
102
109
  * Returns the rendered HTML or throws a JavaScript error.
103
110
  * @param {string} entry
104
111
  * @param {string} templates_json
105
112
  * @param {string} state
113
+ * @param {string} attribute_name_strategy
106
114
  * @returns {string}
107
115
  */
108
- function render_with_templates(entry, templates_json, state) {
109
- let deferred5_0;
110
- let deferred5_1;
116
+ function render_with_templates(entry, templates_json, state, attribute_name_strategy) {
117
+ let deferred6_0;
118
+ let deferred6_1;
111
119
  try {
112
120
  const ptr0 = passStringToWasm0(entry, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
113
121
  const len0 = WASM_VECTOR_LEN;
@@ -115,18 +123,20 @@ function render_with_templates(entry, templates_json, state) {
115
123
  const len1 = WASM_VECTOR_LEN;
116
124
  const ptr2 = passStringToWasm0(state, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
117
125
  const len2 = WASM_VECTOR_LEN;
118
- const ret = wasm.render_with_templates(ptr0, len0, ptr1, len1, ptr2, len2);
119
- var ptr4 = ret[0];
120
- var len4 = ret[1];
126
+ const ptr3 = passStringToWasm0(attribute_name_strategy, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
127
+ const len3 = WASM_VECTOR_LEN;
128
+ const ret = wasm.render_with_templates(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3);
129
+ var ptr5 = ret[0];
130
+ var len5 = ret[1];
121
131
  if (ret[3]) {
122
- ptr4 = 0; len4 = 0;
132
+ ptr5 = 0; len5 = 0;
123
133
  throw takeFromExternrefTable0(ret[2]);
124
134
  }
125
- deferred5_0 = ptr4;
126
- deferred5_1 = len4;
127
- return getStringFromWasm0(ptr4, len4);
135
+ deferred6_0 = ptr5;
136
+ deferred6_1 = len5;
137
+ return getStringFromWasm0(ptr5, len5);
128
138
  } finally {
129
- wasm.__wbindgen_free(deferred5_0, deferred5_1, 1);
139
+ wasm.__wbindgen_free(deferred6_0, deferred6_1, 1);
130
140
  }
131
141
  }
132
142
  exports.render_with_templates = render_with_templates;
Binary file
@@ -3,8 +3,8 @@
3
3
  export const memory: WebAssembly.Memory;
4
4
  export const parse_f_templates: (a: number, b: number) => [number, number];
5
5
  export const render: (a: number, b: number, c: number, d: number) => [number, number, number, number];
6
- export const render_entry_with_templates: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
7
- export const render_with_templates: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
6
+ export const render_entry_with_templates: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number];
7
+ export const render_with_templates: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number];
8
8
  export const __wbindgen_externrefs: WebAssembly.Table;
9
9
  export const __wbindgen_malloc: (a: number, b: number) => number;
10
10
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;