@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/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.