@anydigital/11ty-bricks 1.0.0-alpha.2 → 1.0.0-alpha.4

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
@@ -83,7 +83,7 @@ eleventyConfig.addPlugin(eleventyBricks, {
83
83
  });
84
84
  ```
85
85
 
86
- ## Available Helpers
86
+ ## Available 11ty Helpers
87
87
 
88
88
  ### autoRaw
89
89
 
@@ -102,6 +102,100 @@ Use {{ variable }} to output variables.
102
102
 
103
103
  Would try to process `{{ variable }}` as a template variable. With `autoRaw`, it displays exactly as written.
104
104
 
105
+ ## Available Bricks (Components)
106
+
107
+ ### Navigation Macro (`_nav.njk`)
108
+
109
+ A reusable Nunjucks macro for rendering navigation menus with proper accessibility attributes. This macro works seamlessly with the [11ty Navigation Plugin](https://www.11ty.dev/docs/plugins/navigation/).
110
+
111
+ **Usage:**
112
+
113
+ 1. Import the macro in your template:
114
+
115
+ ```njk
116
+ {% from "bricks/_nav.njk" import render as renderNav %}
117
+ ```
118
+
119
+ 2. Call the macro with your navigation data:
120
+
121
+ ```njk
122
+ {{ renderNav(collections.all | eleventyNavigation, page) }}
123
+ ```
124
+
125
+ **Parameters:**
126
+
127
+ - `navPages`: Array of navigation entries (typically from `eleventyNavigation` filter)
128
+ - `curPage`: Current page object (use Eleventy's `page` variable)
129
+
130
+ **Features:**
131
+
132
+ - Renders a semantic `<nav>` element
133
+ - Automatically adds `aria-current="page"` to the current page link for accessibility
134
+ - Clean, minimal markup ready for styling
135
+ - Works with nested navigation structures from the 11ty Navigation Plugin
136
+
137
+ **Example Output:**
138
+
139
+ ```html
140
+ <nav>
141
+ <a href="/">Home</a>
142
+ <a href="/about/">About</a>
143
+ <a href="/contact/" aria-current="page">Contact</a>
144
+ </nav>
145
+ ```
146
+
147
+ ## CLI Helper Commands
148
+
149
+ After installing this package, the `download-files` command becomes available:
150
+
151
+ ### download-files
152
+
153
+ A CLI command that downloads external files to your project based on URLs specified in your `package.json`.
154
+
155
+ **Usage:**
156
+
157
+ 1. Add a `_downloadFiles` field to your project's `package.json` with URL-to-path mappings:
158
+
159
+ ```json
160
+ {
161
+ "_downloadFiles": {
162
+ "https://example.com/library.js": "src/vendor/library.js",
163
+ "https://cdn.example.com/styles.css": "public/css/external.css"
164
+ }
165
+ }
166
+ ```
167
+
168
+ 2. Run the download command:
169
+
170
+ ```bash
171
+ npx download-files
172
+ ```
173
+
174
+ **Options:**
175
+
176
+ - `-o, --output <dir>`: Specify an output directory where all files will be downloaded (relative paths in `_downloadFiles` will be resolved relative to this directory)
177
+
178
+ ```bash
179
+ # Download all files to a specific directory
180
+ npx download-files --output public
181
+ ```
182
+
183
+ **Features:**
184
+
185
+ - Downloads multiple files from external URLs
186
+ - Automatically creates directories if they don't exist
187
+ - Overwrites existing files
188
+ - Continues downloading remaining files even if some fail
189
+ - Provides clear progress and error messages
190
+ - Returns appropriate exit codes for CI/CD integration
191
+
192
+ **Use Cases:**
193
+
194
+ - Download third-party libraries and assets
195
+ - Fetch external resources during build processes
196
+ - Keep vendored files up to date
197
+ - Automate dependency downloads that aren't available via npm
198
+
105
199
  ## Requirements
106
200
 
107
201
  - Node.js >= 18.0.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anydigital/11ty-bricks",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.4",
4
4
  "description": "A collection of helpful utilities and filters for Eleventy (11ty)",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -10,11 +10,15 @@
10
10
  "require": "./src/index.cjs"
11
11
  }
12
12
  },
13
+ "bin": {
14
+ "download-files": "src/cli/download-files.js"
15
+ },
13
16
  "files": [
14
17
  "src"
15
18
  ],
16
19
  "scripts": {
17
- "test": "node --test src/**/*.test.js"
20
+ "test": "node --test src/**/*.test.js",
21
+ "test2": "npx download-files && npx download-files --output ~subtest"
18
22
  },
19
23
  "keywords": [
20
24
  "11ty",
@@ -40,5 +44,9 @@
40
44
  },
41
45
  "engines": {
42
46
  "node": ">=18.0.0"
47
+ },
48
+ "_downloadFiles": {
49
+ "https://raw.githubusercontent.com/anydigital/11ty-bricks/refs/heads/main/README.md": "~README.md",
50
+ "https://raw.githubusercontent.com/anydigital/11ty-bricks/refs/heads/main/LICENSE": "~test/LICENSE"
43
51
  }
44
52
  }
@@ -0,0 +1,10 @@
1
+ {% macro render(navPages, curPage) %}
2
+ {# https://www.11ty.dev/docs/plugins/navigation/#bring-your-own-html-render-the-menu-items-manually #}
3
+ <nav>
4
+ {%- for entry in navPages %}
5
+ <a href="{{ entry.url }}" {{ 'aria-current="page"' | safe if entry.url == curPage.url }}>
6
+ {{- entry.title -}}
7
+ </a>
8
+ {%- endfor %}
9
+ </nav>
10
+ {% endmacro %}
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, writeFile, mkdir } from 'fs/promises';
4
+ import { dirname, resolve, join } from 'path';
5
+
6
+ /**
7
+ * Parse command line arguments
8
+ *
9
+ * @returns {Object} Parsed arguments
10
+ */
11
+ function parseArgs() {
12
+ const args = process.argv.slice(2);
13
+ const parsed = {
14
+ outputDir: null
15
+ };
16
+
17
+ for (let i = 0; i < args.length; i++) {
18
+ const arg = args[i];
19
+
20
+ if (arg === '--output' || arg === '-o') {
21
+ if (i + 1 < args.length) {
22
+ parsed.outputDir = args[i + 1];
23
+ i++; // Skip next argument
24
+ } else {
25
+ throw new Error(`${arg} requires a directory path`);
26
+ }
27
+ }
28
+ }
29
+
30
+ return parsed;
31
+ }
32
+
33
+ /**
34
+ * Downloads files specified in package.json's _downloadFiles field
35
+ *
36
+ * @param {string|null} outputDir - Optional output directory to prepend to all paths
37
+ * @returns {Promise<boolean>} True if all downloads succeeded, false if any failed
38
+ */
39
+ async function download(outputDir = null) {
40
+ try {
41
+ // Find and read package.json from the current working directory
42
+ const packageJsonPath = resolve(process.cwd(), 'package.json');
43
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
44
+
45
+ const downloadFiles = packageJson._downloadFiles;
46
+
47
+ if (!downloadFiles || typeof downloadFiles !== 'object') {
48
+ console.log('No _downloadFiles field found in package.json');
49
+ return true;
50
+ }
51
+
52
+ const entries = Object.entries(downloadFiles);
53
+
54
+ if (entries.length === 0) {
55
+ console.log('No files to download (_downloadFiles is empty)');
56
+ return true;
57
+ }
58
+
59
+ console.log(`Starting download of ${entries.length} file(s)...\n`);
60
+
61
+ let hasErrors = false;
62
+
63
+ // Process all downloads
64
+ for (const entry of entries) {
65
+ const url = entry[0];
66
+ let localPath = entry[1];
67
+
68
+ try {
69
+ console.log(`Downloading: ${url}`);
70
+ console.log(` To: ${localPath}`);
71
+
72
+ // Download the file
73
+ const response = await fetch(url);
74
+
75
+ if (!response.ok) {
76
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
77
+ }
78
+
79
+ // Get the file content
80
+ const content = await response.arrayBuffer();
81
+ const buffer = Buffer.from(content);
82
+
83
+ // Prepend output directory to local path if specified
84
+ if (outputDir) {
85
+ localPath = join(outputDir, localPath);
86
+ }
87
+
88
+ // Resolve the full path
89
+ const fullPath = resolve(process.cwd(), localPath);
90
+
91
+ // Create directory if it doesn't exist
92
+ const dir = dirname(fullPath);
93
+ await mkdir(dir, { recursive: true });
94
+
95
+ // Write the file
96
+ await writeFile(fullPath, buffer);
97
+
98
+ console.log(` Success: ${localPath}\n`);
99
+ } catch (error) {
100
+ hasErrors = true;
101
+ console.error(` Error: ${error.message}`);
102
+ console.error(` URL: ${url}\n`);
103
+ }
104
+ }
105
+
106
+ // Summary
107
+ if (hasErrors) {
108
+ console.log('Download completed with errors');
109
+ return false;
110
+ } else {
111
+ console.log('All downloads completed successfully');
112
+ return true;
113
+ }
114
+
115
+ } catch (error) {
116
+ console.error(`Fatal error: ${error.message}`);
117
+ return false;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * CLI entry point
123
+ */
124
+ async function main() {
125
+ try {
126
+ const args = parseArgs();
127
+ const success = await download(args.outputDir);
128
+ process.exit(success ? 0 : 1);
129
+ } catch (error) {
130
+ console.error(`Error: ${error.message}`);
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ main();
136
+