basebrick-bricklayer 1.0.0 → 1.1.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
@@ -8,10 +8,22 @@ Bricklayer is the core static site generator (JamBrick) for BaseBrick. It is des
8
8
  - **Dynamic Content Fetching**: Automatically fetches content from a headless CMS based on configuration (`components/generic.json`) and generates individual pages for each content item with clean, SEO-friendly slugs.
9
9
  - **Production Optimization**: In production mode (`--prod`), it minifies HTML and CSS, and compresses image assets using `sharp`.
10
10
  - **Tailwind Integration**: Seamlessly builds Tailwind CSS using `@tailwindcss/cli`, supporting development and production (minified) builds.
11
+ - **Environment Variables**: Natively loads `.env` and `.dev.vars` (Cloudflare) files, makes variables accessible globally in Nunjucks templates, and interpolates variables in `generic.json`.
12
+ - **Automated Deployments**: Scaffolding optionally generates tailored GitHub Actions (`deploy.yml`) workflows for seamless CI/CD.
13
+ - **Cloudflare Native**: Integrates native Cloudflare Workers support, automatically configuring `deploy:prod` and `deploy:preview` environments mapped to `wrangler.toml`.
14
+ - **Centralized Management**: Includes a `bricklayer manage` command to securely register and sync your local `.basebrick.config` settings with a central Manager API.
11
15
 
12
16
  ## Installation
13
17
 
14
- Since Bricklayer is a local module within the BaseBrick ecosystem, you can link it directly:
18
+ To create a new Bricklayer project with automatic scaffolding, run the `init` command:
19
+
20
+ ```bash
21
+ npx bricklayer init
22
+ ```
23
+
24
+ This interactive setup will create the default folder structure and ask if you'd like to scaffold a demo site with starter templates, as well as configure Sonic JS CMS integration automatically.
25
+
26
+ Alternatively, if you are working within the BaseBrick ecosystem locally, you can link it:
15
27
 
16
28
  ```bash
17
29
  cd bricklayer
@@ -37,6 +49,12 @@ To run a production build (minifies HTML/CSS, compresses images):
37
49
  bricklayer --prod
38
50
  ```
39
51
 
52
+ To sync your project settings with a central Bricklayer Manager instance:
53
+
54
+ ```bash
55
+ bricklayer manage
56
+ ```
57
+
40
58
  ### Module
41
59
 
42
60
  ```javascript
@@ -74,11 +92,22 @@ To enable remote CMS fetching, place a `generic.json` configuration file in `src
74
92
  - `indexPage`: The template (e.g., `blog.njk`) where an array of posts will be injected under the `posts` variable.
75
93
  - `postPage`: The template (e.g., `post.njk`) that will be used to generate individual pages for each item fetched from the API. The generated HTML will be placed in a directory matching the `postPage` name (e.g., `public/post/my-slug.html`).
76
94
 
95
+ ## Environment Variables
96
+
97
+ Bricklayer natively supports reading `.env` and `.dev.vars` (Cloudflare Pages) files from the project's root directory.
98
+
99
+ - **Nunjucks Templates:** Variables are automatically passed to Nunjucks templates under the global `env` object. E.g., `{{ env.MY_API_KEY }}`.
100
+ - **CMS Configuration:** Environment variables can be directly interpolated in `generic.json` strings using the `${VARIABLE_NAME}` syntax.
101
+
77
102
  ## Directory Structure
78
103
 
79
104
  Bricklayer expects the following default structure (overridable via options):
80
105
 
81
- ```
106
+ ```text
107
+ ├── .github/
108
+ │ └── workflows/
109
+ │ └── deploy.yml # Auto-generated GitHub Actions
110
+ ├── cms/ # (Optional) Cloned Sonic JS CMS
82
111
  ├── src/
83
112
  │ ├── _includes/ # Nunjucks layouts
84
113
  │ ├── assets/ # Static assets (images, fonts, tailwind)
@@ -86,5 +115,8 @@ Bricklayer expects the following default structure (overridable via options):
86
115
  │ ├── index.njk # Pages
87
116
  │ └── post.njk
88
117
  ├── public/ # Build output
118
+ ├── .basebrick.config # Bricklayer project settings
119
+ ├── .gitignore # Version control exclusions
120
+ ├── wrangler.toml # (Optional) Cloudflare configuration
89
121
  └── package.json
90
122
  ```
package/bin/bricklayer.js CHANGED
@@ -1,6 +1,49 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { buildSite } from '../index.js';
4
+ import { initProject } from './init.js';
4
5
 
5
- const isProd = process.argv.includes('--prod');
6
- buildSite({ isProd, cwd: process.cwd() }).catch(console.error);
6
+ const args = process.argv.slice(2);
7
+
8
+ if (args.includes('help') || args.includes('--help') || args.includes('-h')) {
9
+ console.log(`
10
+ 🧱 Bricklayer CLI 🧱
11
+
12
+ Usage: bricklayer [command] [options]
13
+
14
+ Commands:
15
+ (empty) Run a standard development build
16
+ init Scaffold a new project (Demo, CMS, Cloudflare API)
17
+ manage Register this project with a Bricklayer Manager
18
+ help Show this help message
19
+
20
+ Options:
21
+ --prod Run a production build (minifies HTML/CSS, compresses images)
22
+
23
+ Examples:
24
+ bricklayer init Initialize a project with a demo site, CMS, & Cloudflare Workers API
25
+ bricklayer manage Send project configuration to your central Bricklayer Manager
26
+ bricklayer Builds the site in development mode
27
+ bricklayer --prod Builds the site with production optimizations
28
+
29
+ Generated NPM Scripts (after init):
30
+ npm run start Test your Cloudflare Worker locally
31
+ npm run deploy:preview Deploy to a Cloudflare preview environment
32
+ npm run deploy:prod Deploy to Cloudflare production
33
+ `);
34
+ process.exit(0);
35
+ }
36
+
37
+ if (args.includes('init')) {
38
+ initProject(process.cwd()).catch(console.error);
39
+ } else if (args.includes('manage')) {
40
+ import('./manage.js').then(m => m.manageProject(process.cwd())).catch(console.error);
41
+ } else {
42
+ const isProd = args.includes('--prod');
43
+ buildSite({ isProd, cwd: process.cwd() })
44
+ .then(() => process.exit(0))
45
+ .catch(err => {
46
+ console.error(err);
47
+ process.exit(1);
48
+ });
49
+ }
package/bin/init.js ADDED
@@ -0,0 +1,393 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+ import { execSync } from 'child_process';
5
+
6
+ const rl = readline.createInterface({
7
+ input: process.stdin,
8
+ output: process.stdout
9
+ });
10
+
11
+ const question = (query) => new Promise((resolve) => rl.question(query, resolve));
12
+
13
+ export async function initProject(cwd) {
14
+ console.log('\n🧱 Welcome to Bricklayer Initialization 🧱\n');
15
+
16
+ const confirm = await question('Initialize a new Bricklayer project in the current directory? (Y/n) ');
17
+ if (confirm.toLowerCase() === 'n') {
18
+ console.log('Initialization cancelled.');
19
+ rl.close();
20
+ return;
21
+ }
22
+
23
+ const existingFiles = fs.readdirSync(cwd).filter(f => f !== '.git' && f !== '.DS_Store');
24
+ if (existingFiles.length > 0) {
25
+ const wipe = await question('Warning: This directory is not empty. Would you like to wipe existing files before scaffolding? (y/N) ');
26
+ if (wipe.toLowerCase() === 'y') {
27
+ console.log('Wiping directory...');
28
+ for (const file of existingFiles) {
29
+ fs.rmSync(path.join(cwd, file), { recursive: true, force: true });
30
+ }
31
+ console.log('Directory wiped.\n');
32
+ }
33
+ }
34
+
35
+ const projectNameRaw = await question('What is the name of your project? (bricklayer-site) ');
36
+ const projectName = projectNameRaw.trim() || 'bricklayer-site';
37
+
38
+ const githubRepoRaw = await question('What is the GitHub repository URL? (leave blank for none) ');
39
+ const githubRepo = githubRepoRaw.trim();
40
+
41
+ const demoSite = await question('Would you like to scaffold a demo site with starter templates? (Y/n) ');
42
+ const includeDemo = demoSite.toLowerCase() !== 'n';
43
+
44
+ const sonicJs = await question('Would you like to configure Sonic JS CMS integration? (Y/n) ');
45
+ const includeSonic = sonicJs.toLowerCase() !== 'n';
46
+
47
+ let pullSonic = false;
48
+ if (includeSonic) {
49
+ const pull = await question('Do you want to pull Sonic.js into the cms/ folder? (y/N) ');
50
+ pullSonic = pull.toLowerCase() === 'y';
51
+ }
52
+
53
+ const cloudflare = await question('Would you like to start with Cloudflare? (Y/n) ');
54
+ const includeCloudflare = cloudflare.toLowerCase() !== 'n';
55
+ let cfFramework = '';
56
+ if (includeCloudflare) {
57
+ const fw = await question('Which API framework? (1)hono or (2)vanilla? (1/2) ');
58
+ cfFramework = fw.trim() === '1' ? 'hono' : 'vanilla';
59
+ }
60
+
61
+ const deployYml = await question('Would you like to generate a GitHub Actions deploy.yml? (Y/n) ');
62
+ const createDeploy = deployYml.toLowerCase() !== 'n';
63
+
64
+ console.log('\nScaffolding project...');
65
+
66
+ // 1. Create directory structure
67
+ const dirs = [
68
+ 'src',
69
+ 'src/_includes',
70
+ 'src/assets',
71
+ 'src/assets/images',
72
+ 'src/assets/tailwind',
73
+ 'src/components',
74
+ 'public'
75
+ ];
76
+
77
+ dirs.forEach(dir => {
78
+ const dirPath = path.join(cwd, dir);
79
+ if (!fs.existsSync(dirPath)) {
80
+ fs.mkdirSync(dirPath, { recursive: true });
81
+ console.log(` Created ${dir}/`);
82
+ }
83
+ });
84
+
85
+ // Download dancing ninja
86
+ const ninjaPath = path.join(cwd, 'src/assets/images/ninja-dance.gif');
87
+ if (!fs.existsSync(ninjaPath)) {
88
+ try {
89
+ console.log(' Downloading dancing ninja asset...');
90
+ const response = await fetch('https://www.rfgeneration.com/images/collections/gamepopper101/bitdance.gif', {
91
+ headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' }
92
+ });
93
+ const buffer = await response.arrayBuffer();
94
+ fs.writeFileSync(ninjaPath, Buffer.from(buffer));
95
+ } catch (e) {
96
+ console.log(' Failed to download ninja gif.');
97
+ }
98
+ }
99
+
100
+ // 2. Create base CSS
101
+ const cssPath = path.join(cwd, 'src/assets/tailwind/input.css');
102
+ if (!fs.existsSync(cssPath)) {
103
+ fs.writeFileSync(cssPath, `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`);
104
+ console.log(' Created src/assets/tailwind/input.css');
105
+ }
106
+
107
+ if (includeDemo) {
108
+ // Base layout
109
+ const layoutPath = path.join(cwd, 'src/_includes/base.njk');
110
+ if (!fs.existsSync(layoutPath)) {
111
+ fs.writeFileSync(layoutPath, `<!DOCTYPE html>
112
+ <html lang="en">
113
+ <head>
114
+ <meta charset="UTF-8">
115
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
116
+ <title>{{ title | default("Bricklayer Site") }}</title>
117
+ <link rel="stylesheet" href="/assets/css/style.css">
118
+ </head>
119
+ <body class="bg-gray-50 text-gray-900 font-sans p-8">
120
+ <main class="max-w-4xl mx-auto">
121
+ {{ content | safe }}
122
+ </main>
123
+ </body>
124
+ </html>`);
125
+ console.log(' Created src/_includes/base.njk');
126
+ }
127
+
128
+ const indexPath = path.join(cwd, 'src/index.njk');
129
+ if (!fs.existsSync(indexPath)) {
130
+ fs.writeFileSync(indexPath, `---
131
+ title: Welcome to Bricklayer
132
+ layout: base
133
+ ---
134
+ <h1 class="text-4xl font-bold text-blue-600 mb-4">Welcome to Bricklayer</h1>
135
+ <p class="text-lg">Your statically generated JamBrick site is ready.</p>
136
+ ${includeSonic ? '<p class="mt-4 mb-4"><a href="/blog.html" class="text-blue-500 underline">View the CMS Blog</a></p>' : ''}
137
+ <div class="mt-8">
138
+ <img src="/assets/images/ninja-dance.gif" alt="Dancing Ninja" class="rounded-lg shadow-md max-w-xs" />
139
+ </div>
140
+ `);
141
+ console.log(' Created src/index.njk');
142
+ }
143
+ }
144
+
145
+ if (includeSonic) {
146
+ // generic.json
147
+ const genericPath = path.join(cwd, 'src/components/generic.json');
148
+ if (!fs.existsSync(genericPath)) {
149
+ fs.writeFileSync(genericPath, JSON.stringify({
150
+ name: "sonic.js",
151
+ apiUrl: "/v1/posts",
152
+ productionUrl: "${PROD_CMS_URL}",
153
+ locaLUrl: "http://localhost:3018",
154
+ indexPage: "blog",
155
+ postPage: "post"
156
+ }, null, 2));
157
+ console.log(' Created src/components/generic.json');
158
+ }
159
+
160
+ // Environment files
161
+ const envPath = path.join(cwd, '.env');
162
+ if (!fs.existsSync(envPath)) {
163
+ fs.writeFileSync(envPath, `PROD_CMS_URL=https://cms.basebrick.xyz\n`);
164
+ console.log(' Created .env file');
165
+ }
166
+
167
+ if (includeDemo) {
168
+ // Blog Listing
169
+ const blogPath = path.join(cwd, 'src/blog.njk');
170
+ if (!fs.existsSync(blogPath)) {
171
+ fs.writeFileSync(blogPath, `---
172
+ title: Blog
173
+ layout: base
174
+ ---
175
+ <h1 class="text-3xl font-bold mb-6">Blog Posts</h1>
176
+ <div class="grid gap-4">
177
+ {% for post in posts %}
178
+ <div class="p-4 bg-white shadow rounded">
179
+ <h2 class="text-xl font-semibold"><a href="/post/{{ post.slug }}.html" class="text-blue-600 hover:underline">{{ post.title }}</a></h2>
180
+ </div>
181
+ {% else %}
182
+ <p>No posts found. Ensure your CMS is running and the API URL is correct.</p>
183
+ {% endfor %}
184
+ </div>
185
+ <p class="mt-6"><a href="/index.html" class="text-blue-500 underline">&larr; Back home</a></p>`);
186
+ console.log(' Created src/blog.njk');
187
+ }
188
+
189
+ // Post detail
190
+ const postPath = path.join(cwd, 'src/post.njk');
191
+ if (!fs.existsSync(postPath)) {
192
+ fs.writeFileSync(postPath, `---
193
+ layout: base
194
+ url: post.title
195
+ ---
196
+ <article>
197
+ <h1 class="text-4xl font-bold mb-4">{{ post.title }}</h1>
198
+ <div class="prose">
199
+ {{ post.body | safe }}
200
+ </div>
201
+ <p class="mt-8"><a href="/blog.html" class="text-blue-500 underline">&larr; Back to blog</a></p>
202
+ </article>`);
203
+ console.log(' Created src/post.njk');
204
+ }
205
+ }
206
+ }
207
+
208
+ console.log('\nConfiguring project dependencies...');
209
+ const pkgPath = path.join(cwd, 'package.json');
210
+ let pkg = {
211
+ name: projectName,
212
+ version: "1.0.0",
213
+ type: "module",
214
+ scripts: {
215
+ build: "bricklayer",
216
+ "build:prod": "bricklayer --prod"
217
+ },
218
+ dependencies: {},
219
+ devDependencies: {
220
+ "basebrick-bricklayer": "^1.0.0",
221
+ "@tailwindcss/cli": "^4.0.0",
222
+ "tailwindcss": "^4.0.0"
223
+ }
224
+ };
225
+ if (githubRepo) {
226
+ pkg.repository = { type: "git", url: githubRepo };
227
+ }
228
+ if (fs.existsSync(pkgPath)) {
229
+ try {
230
+ const existing = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
231
+ pkg = { ...pkg, ...existing };
232
+ pkg.dependencies = { ...pkg.dependencies, ...existing.dependencies };
233
+ pkg.devDependencies = { ...pkg.devDependencies, ...existing.devDependencies };
234
+ } catch(e) {}
235
+ }
236
+
237
+ if (includeCloudflare) {
238
+ console.log('\nConfiguring Cloudflare...');
239
+ pkg.scripts.start = "wrangler dev";
240
+ pkg.scripts["deploy:preview"] = "npm run build:prod && wrangler deploy --env preview";
241
+ pkg.scripts["deploy:prod"] = "npm run build:prod && wrangler deploy";
242
+ pkg.devDependencies.wrangler = "^3.0.0";
243
+
244
+ const wranglerPath = path.join(cwd, 'wrangler.toml');
245
+ if (!fs.existsSync(wranglerPath)) {
246
+ fs.writeFileSync(wranglerPath, `name = "${projectName}"\ncompatibility_date = "2024-05-12"\nmain = "api/worker.js"\n\n[assets]\ndirectory = "./public"\n\n[env.preview]\nname = "${projectName}-preview"\n`);
247
+ console.log(' Created wrangler.toml');
248
+ }
249
+
250
+ const apiDir = path.join(cwd, 'api');
251
+ if (!fs.existsSync(apiDir)) {
252
+ fs.mkdirSync(apiDir, { recursive: true });
253
+ console.log(' Created api/');
254
+ }
255
+
256
+ if (cfFramework === 'hono') {
257
+ pkg.dependencies.hono = "^4.3.0";
258
+ const workerPath = path.join(cwd, 'api/worker.js');
259
+ if (!fs.existsSync(workerPath)) {
260
+ fs.writeFileSync(workerPath, `import { Hono } from 'hono';\n\nconst app = new Hono();\n\napp.get('/api', (c) => {\n return c.json({ message: 'Hello from Hono API!' });\n});\n\nexport default app;\n`);
261
+ console.log(' Created api/worker.js (Hono API)');
262
+ }
263
+ } else {
264
+ const workerPath = path.join(cwd, 'api/worker.js');
265
+ if (!fs.existsSync(workerPath)) {
266
+ fs.writeFileSync(workerPath, `export default {\n async fetch(request, env, ctx) {\n const url = new URL(request.url);\n if (url.pathname.startsWith('/api')) {\n return new Response(JSON.stringify({ message: 'Hello from Vanilla API!' }), { headers: { 'Content-Type': 'application/json' } });\n }\n return new Response('Not found', { status: 404 });\n }\n};\n`);
267
+ console.log(' Created api/worker.js (Vanilla API)');
268
+ }
269
+ }
270
+ }
271
+
272
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
273
+ console.log(' Created/Updated package.json');
274
+
275
+ if (pullSonic) {
276
+ console.log('\nPulling Sonic JS CMS into cms/ directory...');
277
+ try {
278
+ if (fs.existsSync(path.join(cwd, 'cms'))) {
279
+ console.log(' Directory cms/ already exists, skipping clone.');
280
+ } else {
281
+ execSync('git clone https://github.com/lane711/sonicjs.git cms', { stdio: 'inherit', cwd });
282
+ fs.rmSync(path.join(cwd, 'cms', '.git'), { recursive: true, force: true });
283
+ console.log(' Successfully cloned Sonic JS CMS.');
284
+ }
285
+ } catch (e) {
286
+ console.error(' Failed to pull Sonic JS:', e.message);
287
+ }
288
+ }
289
+
290
+ const gitignorePath = path.join(cwd, '.gitignore');
291
+ if (!fs.existsSync(gitignorePath)) {
292
+ fs.writeFileSync(gitignorePath, `node_modules/\n.DS_Store\n.env\n.dev.vars\n.wrangler/\n`);
293
+ console.log(' Created .gitignore');
294
+ }
295
+
296
+ if (createDeploy) {
297
+ console.log('\nConfiguring GitHub Actions deploy.yml...');
298
+ const githubDir = path.join(cwd, '.github', 'workflows');
299
+ if (!fs.existsSync(githubDir)) {
300
+ fs.mkdirSync(githubDir, { recursive: true });
301
+ console.log(' Created .github/workflows/');
302
+ }
303
+
304
+ let deploySteps = ` - name: Checkout repository
305
+ uses: actions/checkout@v4
306
+
307
+ - name: Setup Node.js
308
+ uses: actions/setup-node@v4
309
+ with:
310
+ node-version: '20'
311
+
312
+ - name: Install dependencies
313
+ run: npm install
314
+ `;
315
+
316
+ if (includeCloudflare) {
317
+ deploySteps += `
318
+ - name: Build and Deploy Site
319
+ run: npm run deploy:prod
320
+ env:
321
+ CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}
322
+ CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
323
+ `;
324
+ } else {
325
+ deploySteps += `
326
+ - name: Build Site
327
+ run: npm run build:prod
328
+ `;
329
+ }
330
+
331
+ if (includeSonic && pullSonic) {
332
+ deploySteps += `
333
+ - name: Install CMS dependencies
334
+ run: npm install
335
+ working-directory: ./cms
336
+
337
+ - name: Deploy Sonic CMS
338
+ run: npm run deploy
339
+ working-directory: ./cms
340
+ env:
341
+ CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}
342
+ CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
343
+ `;
344
+ }
345
+ const deployYmlContent = `name: Deploy
346
+
347
+ on:
348
+ push:
349
+ branches:
350
+ - main
351
+
352
+ jobs:
353
+ deploy:
354
+ runs-on: ubuntu-latest
355
+ steps:
356
+ ${deploySteps}`;
357
+
358
+ const deployYmlPath = path.join(githubDir, 'deploy.yml');
359
+ fs.writeFileSync(deployYmlPath, deployYmlContent);
360
+ console.log(' Created .github/workflows/deploy.yml');
361
+ if (includeCloudflare || (includeSonic && pullSonic)) {
362
+ console.log(' NOTE: Make sure to add CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID to your GitHub repository secrets.');
363
+ }
364
+ }
365
+
366
+ const basebrickConfigPath = path.join(cwd, '.basebrick.config');
367
+ const basebrickConfig = {
368
+ projectName,
369
+ githubRepo,
370
+ includeDemo,
371
+ includeSonic,
372
+ pullSonic,
373
+ includeCloudflare,
374
+ cfFramework,
375
+ createDeploy
376
+ };
377
+ fs.writeFileSync(basebrickConfigPath, JSON.stringify(basebrickConfig, null, 2));
378
+ console.log(' Created .basebrick.config');
379
+
380
+ console.log('\n✅ Initialization complete!');
381
+ console.log('\nNext steps:');
382
+ console.log(' 1. Run `npm install` to install dependencies');
383
+ console.log(' 2. Run `npm run build` to build the static site (or `npm run build:prod` for production)');
384
+ if (includeCloudflare) {
385
+ console.log(' 3. Run `npm run start` to test your Cloudflare Worker locally');
386
+ console.log(' 4. Run `npm run deploy:preview` to deploy to a Cloudflare preview environment');
387
+ console.log(' 5. Run `npm run deploy:prod` to deploy to Cloudflare production\n');
388
+ } else {
389
+ console.log('');
390
+ }
391
+
392
+ rl.close();
393
+ }
package/bin/manage.js ADDED
@@ -0,0 +1,88 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+ import os from 'os';
5
+
6
+ const rl = readline.createInterface({
7
+ input: process.stdin,
8
+ output: process.stdout
9
+ });
10
+
11
+ const question = (query) => new Promise((resolve) => rl.question(query, resolve));
12
+
13
+ export async function manageProject(cwd) {
14
+ const localConfigPath = path.join(cwd, '.basebrick.config');
15
+
16
+ if (!fs.existsSync(localConfigPath)) {
17
+ console.error('❌ No .basebrick.config found in this directory. Run `bricklayer init` first.');
18
+ rl.close();
19
+ return;
20
+ }
21
+
22
+ const globalConfigPath = path.join(os.homedir(), '.bricklayer-manager.json');
23
+ let managerConfig = {};
24
+
25
+ if (fs.existsSync(globalConfigPath)) {
26
+ try {
27
+ managerConfig = JSON.parse(fs.readFileSync(globalConfigPath, 'utf8'));
28
+ } catch (e) {
29
+ console.error('Warning: Could not parse global manager config.');
30
+ }
31
+ }
32
+
33
+ if (!managerConfig.url) {
34
+ const urlRaw = await question('What is your Bricklayer Manager URL? ');
35
+ managerConfig.url = urlRaw.trim();
36
+ }
37
+
38
+ if (!managerConfig.token) {
39
+ const tokenRaw = await question('What is your transfer token? ');
40
+ managerConfig.token = tokenRaw.trim();
41
+ }
42
+
43
+ if (!managerConfig.url || !managerConfig.token) {
44
+ console.error('❌ URL and token are required.');
45
+ rl.close();
46
+ return;
47
+ }
48
+
49
+ // Save for next time
50
+ fs.writeFileSync(globalConfigPath, JSON.stringify(managerConfig, null, 2));
51
+
52
+ let projectConfig;
53
+ try {
54
+ projectConfig = JSON.parse(fs.readFileSync(localConfigPath, 'utf8'));
55
+ } catch(e) {
56
+ console.error('❌ Could not parse .basebrick.config');
57
+ rl.close();
58
+ return;
59
+ }
60
+
61
+ console.log(`\nSending project configuration to ${managerConfig.url}...`);
62
+
63
+ let endpoint = managerConfig.url;
64
+ if (!endpoint.startsWith('http')) {
65
+ endpoint = `https://${endpoint}`;
66
+ }
67
+
68
+ try {
69
+ const response = await fetch(endpoint, {
70
+ method: 'POST',
71
+ headers: {
72
+ 'Content-Type': 'application/json',
73
+ 'Authorization': `Bearer ${managerConfig.token}`
74
+ },
75
+ body: JSON.stringify(projectConfig)
76
+ });
77
+
78
+ if (response.ok) {
79
+ console.log('✅ Successfully registered with Bricklayer Manager!');
80
+ } else {
81
+ console.error(`❌ Manager returned error: ${response.status} ${response.statusText}`);
82
+ }
83
+ } catch(e) {
84
+ console.error(`❌ Failed to connect to manager: ${e.message}`);
85
+ }
86
+
87
+ rl.close();
88
+ }
package/history.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Version History
2
2
 
3
+ ## 1.1.0 (May 2026)
4
+
5
+ - **Interactive Scaffolding**: Added `bricklayer init` command to scaffold new projects via an interactive wizard, generating `.gitignore` and `.basebrick.config` automatically.
6
+ - **Automated Deployments**: Scaffolding now optionally generates a tailored GitHub Actions `deploy.yml` workflow that orchestrates the site (and an isolated Sonic JS worker, if configured).
7
+ - **Cloudflare Native**: Integrated native Cloudflare Workers support (Vanilla or Hono) and static asset routing into the scaffolding workflow, automatically generating `deploy:prod` and `deploy:preview` environments in `wrangler.toml` and `package.json`.
8
+ - **Automated Sonic JS CMS**: Scaffolding now supports automatically cloning the Sonic JS repository into a local `/cms` folder and binding the frontend.
9
+ - **Centralized Management**: Added `bricklayer manage` command to securely register and sync local `.basebrick.config` data with a central Manager API.
10
+ - **Asset Enhancements**: The init command now automatically generates `src/assets/images` and downloads starter media directly.
11
+ - **Enhanced Logging**: Replaced intimidating Node.js fetch stack traces with clean, user-friendly warnings when remote CMS connections fail.
12
+ - **CLI Helpers**: The CLI help menu and post-init success output now clearly document the generated `npm run` workflow commands.
13
+
14
+ ## 1.0.1 (May 2026)
15
+
16
+ - **Environment Variables**: Added native support for loading `.env` and `.dev.vars` (Cloudflare Pages) files. Environment variables are now globally accessible in Nunjucks templates via the `env` object and can be interpolated directly within `generic.json` configurations.
17
+
3
18
  ## 1.0.0 (May 2026)
4
19
 
5
20
  - **Initial Extraction**: Extracted the build pipeline from the BaseBrick frontend into a standalone, reusable Node module (`bricklayer`).
package/index.js CHANGED
@@ -5,9 +5,34 @@ import nunjucks from 'nunjucks';
5
5
  import matter from 'gray-matter';
6
6
  import { execSync } from 'child_process';
7
7
 
8
+ // Simple environment variable loader (.env and .dev.vars)
9
+ function loadEnv(cwd) {
10
+ const envFiles = ['.env', '.dev.vars'];
11
+ for (const file of envFiles) {
12
+ const envPath = path.join(cwd, file);
13
+ if (fs.existsSync(envPath)) {
14
+ const lines = fs.readFileSync(envPath, 'utf8').split('\n');
15
+ for (const line of lines) {
16
+ const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
17
+ if (match) {
18
+ const key = match[1];
19
+ let value = match[2] || '';
20
+ value = value.replace(/(^['"]|['"]$)/g, '').trim();
21
+ if (process.env[key] === undefined) {
22
+ process.env[key] = value;
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
29
+
8
30
  export async function buildSite(options = {}) {
9
31
  const cwd = options.cwd || process.cwd();
10
32
  const isProd = options.isProd || false;
33
+
34
+ // Load environment variables from .env file
35
+ loadEnv(cwd);
11
36
 
12
37
  // Conditionally import minifiers so build doesn't fail if they aren't installed yet
13
38
  let minifyHtml;
@@ -102,6 +127,9 @@ export async function buildSite(options = {}) {
102
127
  autoescape: false,
103
128
  noCache: true
104
129
  });
130
+
131
+ // Support environment variables in Nunjucks templates
132
+ env.addGlobal('env', process.env);
105
133
 
106
134
  // Fetch remote content based on generic.json
107
135
  const genericJsonPath = path.join(config.srcDir, 'components', 'generic.json');
@@ -110,7 +138,12 @@ export async function buildSite(options = {}) {
110
138
 
111
139
  if (fs.existsSync(genericJsonPath)) {
112
140
  try {
113
- cmsConfig = JSON.parse(fs.readFileSync(genericJsonPath, 'utf-8'));
141
+ let jsonString = fs.readFileSync(genericJsonPath, 'utf-8');
142
+
143
+ // Substitute environment variables in the format ${VAR_NAME}
144
+ jsonString = jsonString.replace(/\$\{([^}]+)\}/g, (_, varName) => process.env[varName] || '');
145
+
146
+ cmsConfig = JSON.parse(jsonString);
114
147
  const baseUrl = isProd ? cmsConfig.productionUrl : cmsConfig.locaLUrl;
115
148
  const apiUrl = `${baseUrl.replace(/\/$/, '')}/${cmsConfig.apiUrl.replace(/^\//, '')}`;
116
149
 
@@ -157,7 +190,11 @@ export async function buildSite(options = {}) {
157
190
 
158
191
  console.log(`Successfully fetched ${remoteData.length} items from remote source.`);
159
192
  } catch (e) {
160
- console.error('Error fetching remote content:', e);
193
+ if ((e.cause && e.cause.code === 'ECONNREFUSED') || e.message.includes('fetch failed')) {
194
+ console.warn(`\n⚠️ CMS Not Found: Could not connect to remote API. Make sure your CMS is running.\n`);
195
+ } else {
196
+ console.error('Error fetching remote content:', e.message || e);
197
+ }
161
198
  }
162
199
  }
163
200
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basebrick-bricklayer",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "BaseBrick static site generator (JamBrick)",
6
6
  "main": "index.js",
package/todo.md ADDED
@@ -0,0 +1,3 @@
1
+ add preview instances for cloudflare
2
+ add the ability to add to the manager, it will ask you manager code and token
3
+ the manager is an idea i have had to manage sites he will send a payload to it with git url, sitemap, settings and this will let you manage the distrubation so everytime we build a new site it can be managed in the