basebrick-bricklayer 1.0.1 → 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 +34 -2
- package/bin/bricklayer.js +45 -2
- package/bin/init.js +393 -0
- package/bin/manage.js +88 -0
- package/history.md +15 -0
- package/index.js +40 -3
- package/package.json +1 -1
- package/todo.md +3 -0
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
|
-
|
|
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
|
|
6
|
-
|
|
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">← 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">← 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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -244,7 +281,7 @@ export async function buildSite(options = {}) {
|
|
|
244
281
|
console.log('Building Tailwind CSS...');
|
|
245
282
|
try {
|
|
246
283
|
const tailwindArgs = isProd ? '--minify' : '';
|
|
247
|
-
execSync(`npx tailwindcss -i "${config.css.input}" -o "${config.css.output}" ${tailwindArgs}`, { stdio: 'inherit' });
|
|
284
|
+
execSync(`npx @tailwindcss/cli -i "${config.css.input}" -o "${config.css.output}" ${tailwindArgs}`, { stdio: 'inherit' });
|
|
248
285
|
console.log('Tailwind CSS built successfully!');
|
|
249
286
|
} catch (error) {
|
|
250
287
|
console.error('Failed to build Tailwind CSS:', error);
|
package/package.json
CHANGED
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
|