@bostonuniversity/buwp-local 0.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/.buwp-local.examplejson +27 -0
- package/.buwp-local.json +27 -0
- package/IMPLEMENTATION_SUMMARY.md +385 -0
- package/MULTI_PROJECT_GUIDE.md +929 -0
- package/PROJECT_OVERVIEW.md +307 -0
- package/QUICK_REFERENCE.md +234 -0
- package/ROADMAP.md +362 -0
- package/SHARED_ENVIRONMENT_EXAMPLES.md +578 -0
- package/USAGE.md +258 -0
- package/bin/buwp-local.js +95 -0
- package/bostonuniversity-buwp-local-0.1.0.tgz +0 -0
- package/docker-compose.yml +106 -0
- package/lib/commands/config.js +131 -0
- package/lib/commands/destroy.js +96 -0
- package/lib/commands/logs.js +66 -0
- package/lib/commands/start.js +98 -0
- package/lib/commands/stop.js +62 -0
- package/lib/compose-generator.js +279 -0
- package/lib/config.js +292 -0
- package/lib/index.js +23 -0
- package/macos-keychain-notes.md +11 -0
- package/package.json +35 -0
- package/readme.md +3 -0
package/lib/config.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for buwp-local
|
|
3
|
+
* Handles loading, validating, and merging configuration from various sources
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
|
|
10
|
+
const CONFIG_FILE_NAME = '.buwp-local.json';
|
|
11
|
+
const ENV_FILE_NAME = '.env.local';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default configuration
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_CONFIG = {
|
|
17
|
+
projectName: null, // Will be auto-generated from directory name if not set
|
|
18
|
+
image: 'ghcr.io/bu-ist/bu-wp-docker-mod_shib:arm64-latest',
|
|
19
|
+
hostname: 'wordpress.local',
|
|
20
|
+
multisite: true,
|
|
21
|
+
services: {
|
|
22
|
+
redis: true,
|
|
23
|
+
s3proxy: true,
|
|
24
|
+
shibboleth: true
|
|
25
|
+
},
|
|
26
|
+
ports: {
|
|
27
|
+
http: 80,
|
|
28
|
+
https: 443,
|
|
29
|
+
db: 3306,
|
|
30
|
+
redis: 6379
|
|
31
|
+
},
|
|
32
|
+
mappings: [],
|
|
33
|
+
env: {}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load configuration from project directory
|
|
38
|
+
* @param {string} projectPath - Path to project directory
|
|
39
|
+
* @returns {object} Merged configuration
|
|
40
|
+
*/
|
|
41
|
+
function loadConfig(projectPath = process.cwd()) {
|
|
42
|
+
const configPath = path.join(projectPath, CONFIG_FILE_NAME);
|
|
43
|
+
const envPath = path.join(projectPath, ENV_FILE_NAME);
|
|
44
|
+
|
|
45
|
+
let userConfig = {};
|
|
46
|
+
let envVars = {};
|
|
47
|
+
|
|
48
|
+
// Load config file if it exists
|
|
49
|
+
if (fs.existsSync(configPath)) {
|
|
50
|
+
try {
|
|
51
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
52
|
+
userConfig = JSON.parse(configContent);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw new Error(`Failed to parse ${CONFIG_FILE_NAME}: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Load .env.local if it exists
|
|
59
|
+
if (fs.existsSync(envPath)) {
|
|
60
|
+
try {
|
|
61
|
+
require('dotenv').config({ path: envPath });
|
|
62
|
+
envVars = extractEnvVars();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.warn(chalk.yellow(`Warning: Failed to load ${ENV_FILE_NAME}: ${err.message}`));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Merge configurations (priority: env vars > user config > defaults)
|
|
69
|
+
const config = mergeConfig(DEFAULT_CONFIG, userConfig, envVars);
|
|
70
|
+
|
|
71
|
+
// Auto-generate project name if not set
|
|
72
|
+
if (!config.projectName) {
|
|
73
|
+
config.projectName = getProjectName(projectPath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Sanitize project name (Docker project names must be lowercase alphanumeric + dash/underscore)
|
|
77
|
+
config.projectName = sanitizeProjectName(config.projectName);
|
|
78
|
+
|
|
79
|
+
return config;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract relevant environment variables
|
|
84
|
+
* @returns {object} Environment variables
|
|
85
|
+
*/
|
|
86
|
+
function extractEnvVars() {
|
|
87
|
+
return {
|
|
88
|
+
db: {
|
|
89
|
+
password: process.env.WORDPRESS_DB_PASSWORD,
|
|
90
|
+
rootPassword: process.env.DB_ROOT_PASSWORD
|
|
91
|
+
},
|
|
92
|
+
shibboleth: {
|
|
93
|
+
entityId: process.env.SP_ENTITY_ID,
|
|
94
|
+
idpEntityId: process.env.IDP_ENTITY_ID,
|
|
95
|
+
idpLogout: process.env.SHIB_IDP_LOGOUT,
|
|
96
|
+
spKey: process.env.SHIB_SP_KEY,
|
|
97
|
+
spCert: process.env.SHIB_SP_CERT
|
|
98
|
+
},
|
|
99
|
+
s3: {
|
|
100
|
+
bucket: process.env.S3_UPLOADS_BUCKET,
|
|
101
|
+
region: process.env.S3_UPLOADS_REGION,
|
|
102
|
+
accessKeyId: process.env.S3_UPLOADS_ACCESS_KEY_ID,
|
|
103
|
+
secretAccessKey: process.env.S3_UPLOADS_SECRET_ACCESS_KEY,
|
|
104
|
+
accessRulesTable: process.env.S3_ACCESS_RULES_TABLE
|
|
105
|
+
},
|
|
106
|
+
olap: {
|
|
107
|
+
name: process.env.OLAP,
|
|
108
|
+
accountNumber: process.env.OLAP_ACCT_NBR,
|
|
109
|
+
region: process.env.OLAP_REGION
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Deep merge configuration objects
|
|
116
|
+
* @param {...object} configs - Configuration objects to merge
|
|
117
|
+
* @returns {object} Merged configuration
|
|
118
|
+
*/
|
|
119
|
+
function mergeConfig(...configs) {
|
|
120
|
+
return configs.reduce((acc, config) => {
|
|
121
|
+
return deepMerge(acc, config);
|
|
122
|
+
}, {});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Deep merge utility
|
|
127
|
+
* @param {object} target - Target object
|
|
128
|
+
* @param {object} source - Source object
|
|
129
|
+
* @returns {object} Merged object
|
|
130
|
+
*/
|
|
131
|
+
function deepMerge(target, source) {
|
|
132
|
+
const output = { ...target };
|
|
133
|
+
|
|
134
|
+
if (isObject(target) && isObject(source)) {
|
|
135
|
+
Object.keys(source).forEach(key => {
|
|
136
|
+
if (isObject(source[key])) {
|
|
137
|
+
if (!(key in target)) {
|
|
138
|
+
output[key] = source[key];
|
|
139
|
+
} else {
|
|
140
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
output[key] = source[key];
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return output;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if value is an object
|
|
153
|
+
* @param {*} item - Value to check
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
function isObject(item) {
|
|
157
|
+
return item && typeof item === 'object' && !Array.isArray(item);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validate configuration
|
|
162
|
+
* @param {object} config - Configuration to validate
|
|
163
|
+
* @returns {object} Validation result { valid: boolean, errors: string[] }
|
|
164
|
+
*/
|
|
165
|
+
function validateConfig(config) {
|
|
166
|
+
const errors = [];
|
|
167
|
+
|
|
168
|
+
// Validate required fields
|
|
169
|
+
if (!config.image) {
|
|
170
|
+
errors.push('Missing required field: image');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!config.hostname) {
|
|
174
|
+
errors.push('Missing required field: hostname');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Validate mappings
|
|
178
|
+
if (config.mappings && Array.isArray(config.mappings)) {
|
|
179
|
+
config.mappings.forEach((mapping, index) => {
|
|
180
|
+
if (!mapping.local || !mapping.container) {
|
|
181
|
+
errors.push(`Mapping ${index} missing required fields (local, container)`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (mapping.local && !fs.existsSync(mapping.local)) {
|
|
185
|
+
errors.push(`Mapping ${index}: local path does not exist: ${mapping.local}`);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Validate ports
|
|
191
|
+
if (config.ports) {
|
|
192
|
+
Object.entries(config.ports).forEach(([service, port]) => {
|
|
193
|
+
if (typeof port !== 'number' || port < 1 || port > 65535) {
|
|
194
|
+
errors.push(`Invalid port for ${service}: ${port}`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
valid: errors.length === 0,
|
|
201
|
+
errors
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Initialize configuration file in project directory
|
|
207
|
+
* @param {string} projectPath - Path to project directory
|
|
208
|
+
* @param {object} options - Initialization options
|
|
209
|
+
* @param {boolean} options.plugin - Create plugin mapping
|
|
210
|
+
* @param {boolean} options.muPlugin - Create mu-plugin mapping
|
|
211
|
+
* @param {boolean} options.theme - Create theme mapping
|
|
212
|
+
* @returns {string} Path to created config file
|
|
213
|
+
*/
|
|
214
|
+
function initConfig(projectPath = process.cwd(), options = {}) {
|
|
215
|
+
const configPath = path.join(projectPath, CONFIG_FILE_NAME);
|
|
216
|
+
|
|
217
|
+
if (fs.existsSync(configPath)) {
|
|
218
|
+
throw new Error(`Configuration file already exists: ${configPath}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const projectName = getProjectName(projectPath);
|
|
222
|
+
const exampleConfig = {
|
|
223
|
+
...DEFAULT_CONFIG,
|
|
224
|
+
projectName: projectName,
|
|
225
|
+
hostname: `${projectName}.local`,
|
|
226
|
+
mappings: [],
|
|
227
|
+
env: {
|
|
228
|
+
WP_DEBUG: true,
|
|
229
|
+
XDEBUG: false
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Create smart mappings based on options
|
|
234
|
+
if (options.plugin) {
|
|
235
|
+
exampleConfig.mappings.push({
|
|
236
|
+
local: "./",
|
|
237
|
+
container: `/var/www/html/wp-content/plugins/${projectName}`
|
|
238
|
+
});
|
|
239
|
+
} else if (options.muPlugin) {
|
|
240
|
+
exampleConfig.mappings.push({
|
|
241
|
+
local: "./",
|
|
242
|
+
container: `/var/www/html/wp-content/mu-plugins/${projectName}`
|
|
243
|
+
});
|
|
244
|
+
} else if (options.theme) {
|
|
245
|
+
exampleConfig.mappings.push({
|
|
246
|
+
local: "./",
|
|
247
|
+
container: `/var/www/html/wp-content/themes/${projectName}`
|
|
248
|
+
});
|
|
249
|
+
} else {
|
|
250
|
+
// Default generic mapping with comment
|
|
251
|
+
exampleConfig.mappings.push({
|
|
252
|
+
local: "./",
|
|
253
|
+
container: "/var/www/html/wp-content/plugins/my-plugin",
|
|
254
|
+
comment: "Map current directory to a plugin location"
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fs.writeFileSync(configPath, JSON.stringify(exampleConfig, null, 2));
|
|
259
|
+
|
|
260
|
+
return configPath;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get project name from directory path
|
|
265
|
+
* @param {string} projectPath - Path to project directory
|
|
266
|
+
* @returns {string} Project name
|
|
267
|
+
*/
|
|
268
|
+
function getProjectName(projectPath) {
|
|
269
|
+
return path.basename(projectPath);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Sanitize project name for Docker compatibility
|
|
274
|
+
* Docker project names must be lowercase alphanumeric + dash/underscore
|
|
275
|
+
* @param {string} name - Project name to sanitize
|
|
276
|
+
* @returns {string} Sanitized project name
|
|
277
|
+
*/
|
|
278
|
+
function sanitizeProjectName(name) {
|
|
279
|
+
return name
|
|
280
|
+
.toLowerCase()
|
|
281
|
+
.replace(/[^a-z0-9_-]/g, '-')
|
|
282
|
+
.replace(/^-+|-+$/g, ''); // Remove leading/trailing dashes
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
loadConfig,
|
|
287
|
+
validateConfig,
|
|
288
|
+
initConfig,
|
|
289
|
+
DEFAULT_CONFIG,
|
|
290
|
+
CONFIG_FILE_NAME,
|
|
291
|
+
ENV_FILE_NAME
|
|
292
|
+
};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buwp-local - BU WordPress Local Development Environment
|
|
3
|
+
* Main library exports
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const config = require('./config');
|
|
7
|
+
const composeGenerator = require('./compose-generator');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
// Configuration
|
|
11
|
+
loadConfig: config.loadConfig,
|
|
12
|
+
validateConfig: config.validateConfig,
|
|
13
|
+
initConfig: config.initConfig,
|
|
14
|
+
|
|
15
|
+
// Docker Compose generation
|
|
16
|
+
generateComposeConfig: composeGenerator.generateComposeConfig,
|
|
17
|
+
generateComposeFile: composeGenerator.generateComposeFile,
|
|
18
|
+
|
|
19
|
+
// Constants
|
|
20
|
+
CONFIG_FILE_NAME: config.CONFIG_FILE_NAME,
|
|
21
|
+
ENV_FILE_NAME: config.ENV_FILE_NAME,
|
|
22
|
+
DEFAULT_CONFIG: config.DEFAULT_CONFIG
|
|
23
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
It's often convenient to store a password in the macOS system keychain and retrieve it inside a shell script.
|
|
2
|
+
|
|
3
|
+
Here's the bash command for setting a password:
|
|
4
|
+
|
|
5
|
+
security add-generic-password -s "Keychain item name here" -a "username here" -p
|
|
6
|
+
Then enter your password when prompted.
|
|
7
|
+
|
|
8
|
+
After you run this once, you can retrieve the password in a script like this:
|
|
9
|
+
|
|
10
|
+
PASSWORD=`security find-generic-password -s "Keychain item name here" -a "username here" -w`
|
|
11
|
+
You can then use $PASSWORD whenever you need the password in your script.
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bostonuniversity/buwp-local",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local WordPress development environment for Boston University projects",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"buwp-local": "./bin/buwp-local.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
|
+
"dev": "node bin/buwp-local.js",
|
|
12
|
+
"lint": "eslint ."
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"wordpress",
|
|
16
|
+
"docker",
|
|
17
|
+
"development",
|
|
18
|
+
"bu",
|
|
19
|
+
"local"
|
|
20
|
+
],
|
|
21
|
+
"author": "Boston University",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=16.0.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"js-yaml": "^4.1.0",
|
|
28
|
+
"chalk": "^4.1.2",
|
|
29
|
+
"commander": "^11.0.0",
|
|
30
|
+
"dotenv": "^16.3.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"eslint": "^8.50.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# BU WordPress Local Development
|
|
2
|
+
|
|
3
|
+
This repository contains resources and instructions for setting up a local WordPress development environment for Boston University projects. It uses the BU WordPress container image and provides the additional resoures needed to run it locally with Docker.
|