quackage 1.0.51 → 1.0.53

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quackage",
3
- "version": "1.0.51",
3
+ "version": "1.0.53",
4
4
  "description": "Building. Testing. Quacking. Reloading.",
5
5
  "main": "source/Quackage-CLIProgram.js",
6
6
  "scripts": {
@@ -49,7 +49,12 @@ let _Pict = new libCLIProgram(
49
49
  require('./commands/Quackage-Command-DocuserveInject.js'),
50
50
  require('./commands/Quackage-Command-DocuservePrepareLocal.js'),
51
51
  require('./commands/Quackage-Command-PrepareDocs.js'),
52
- require('./commands/Quackage-Command-DocuserveServe.js')
52
+ require('./commands/Quackage-Command-DocuserveServe.js'),
53
+
54
+ // HTML example application building and serving
55
+ require('./commands/html_example_serving/Quackage-Command-ExamplesBuild.js'),
56
+ require('./commands/html_example_serving/Quackage-Command-ExamplesServe.js'),
57
+ require('./commands/html_example_serving/Quackage-Command-Examples.js')
53
58
  ]);
54
59
 
55
60
  // Instantiate the file persistence service
@@ -0,0 +1,67 @@
1
+ const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
2
+ const libPath = require('path');
3
+
4
+ class QuackageCommandExamples extends libCommandLineCommand
5
+ {
6
+ constructor(pFable, pManifest, pServiceHash)
7
+ {
8
+ super(pFable, pManifest, pServiceHash);
9
+
10
+ this.options.CommandKeyword = 'examples';
11
+ this.options.Description = 'Build all example applications then serve them with an auto-generated index page.';
12
+
13
+ this.options.CommandArguments.push({ Name: '[examples_folder]', Description: 'The examples folder (defaults to ./example_applications).' });
14
+
15
+ this.options.CommandOptions.push({ Name: '-p, --port [port]', Description: 'Port to serve on (default: auto-hashed from project name between 9000-9500).', Default: '' });
16
+
17
+ this.addCommand();
18
+ }
19
+
20
+ onRunAsync(fCallback)
21
+ {
22
+ let tmpExamplesFolder = libPath.resolve(this.ArgumentString || './example_applications');
23
+ let tmpPort = this.CommandOptions.port || '';
24
+
25
+ this.log.info(`Building and serving examples from [${tmpExamplesFolder}] ...`);
26
+
27
+ // First, run examples-build
28
+ let tmpBuildCommand = this.pict.servicesMap['QuackageCommandExamplesBuild'];
29
+ if (!tmpBuildCommand)
30
+ {
31
+ this.log.error(`Could not find the examples-build command. Make sure it is registered.`);
32
+ return fCallback(new Error('examples-build command not found'));
33
+ }
34
+
35
+ // Set the argument string so examples-build uses the same folder
36
+ tmpBuildCommand.ArgumentString = tmpExamplesFolder;
37
+
38
+ tmpBuildCommand.onRunAsync(
39
+ (pBuildError) =>
40
+ {
41
+ if (pBuildError)
42
+ {
43
+ this.log.warn(`Some examples had build errors, but continuing to serve what we have...`);
44
+ }
45
+
46
+ // Now run examples-serve
47
+ let tmpServeCommand = this.pict.servicesMap['QuackageCommandExamplesServe'];
48
+ if (!tmpServeCommand)
49
+ {
50
+ this.log.error(`Could not find the examples-serve command. Make sure it is registered.`);
51
+ return fCallback(new Error('examples-serve command not found'));
52
+ }
53
+
54
+ tmpServeCommand.ArgumentString = tmpExamplesFolder;
55
+ tmpServeCommand.CommandOptions = tmpServeCommand.CommandOptions || {};
56
+ if (tmpPort)
57
+ {
58
+ tmpServeCommand.CommandOptions.port = tmpPort;
59
+ }
60
+
61
+ // examples-serve doesn't call back (long-lived server), so neither do we
62
+ tmpServeCommand.onRunAsync(fCallback);
63
+ });
64
+ }
65
+ }
66
+
67
+ module.exports = QuackageCommandExamples;
@@ -0,0 +1,201 @@
1
+ const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
2
+ const libFS = require('fs');
3
+ const libPath = require('path');
4
+
5
+ class QuackageCommandExamplesBuild extends libCommandLineCommand
6
+ {
7
+ constructor(pFable, pManifest, pServiceHash)
8
+ {
9
+ super(pFable, pManifest, pServiceHash);
10
+
11
+ this.options.CommandKeyword = 'examples-build';
12
+ this.options.Description = 'Build all example applications in the example_applications and debug folders.';
13
+
14
+ this.options.CommandArguments.push({ Name: '[examples_folder]', Description: 'The examples folder (defaults to ./example_applications).' });
15
+
16
+ this.addCommand();
17
+ }
18
+
19
+ gatherExampleFolders(pBasePath)
20
+ {
21
+ let tmpExampleFolders = [];
22
+
23
+ if (!libFS.existsSync(pBasePath))
24
+ {
25
+ return tmpExampleFolders;
26
+ }
27
+
28
+ let tmpEntries = libFS.readdirSync(pBasePath, { withFileTypes: true });
29
+ for (let i = 0; i < tmpEntries.length; i++)
30
+ {
31
+ if (!tmpEntries[i].isDirectory())
32
+ {
33
+ continue;
34
+ }
35
+
36
+ let tmpDirName = tmpEntries[i].name;
37
+
38
+ // Skip node_modules and hidden folders
39
+ if (tmpDirName === 'node_modules' || tmpDirName.startsWith('.'))
40
+ {
41
+ continue;
42
+ }
43
+
44
+ let tmpFolderPath = libPath.join(pBasePath, tmpDirName);
45
+ let tmpPackagePath = libPath.join(tmpFolderPath, 'package.json');
46
+
47
+ if (libFS.existsSync(tmpPackagePath))
48
+ {
49
+ tmpExampleFolders.push(
50
+ {
51
+ Name: tmpDirName,
52
+ Path: tmpFolderPath,
53
+ PackagePath: tmpPackagePath
54
+ });
55
+ }
56
+ }
57
+
58
+ return tmpExampleFolders;
59
+ }
60
+
61
+ onRunAsync(fCallback)
62
+ {
63
+ let tmpExamplesFolder = libPath.resolve(this.ArgumentString || './example_applications');
64
+
65
+ this.log.info(`Building all examples in [${tmpExamplesFolder}] ...`);
66
+
67
+ // Gather example folders from example_applications and debug
68
+ let tmpExampleFolders = this.gatherExampleFolders(tmpExamplesFolder);
69
+ let tmpDebugFolder = libPath.join(tmpExamplesFolder, 'debug');
70
+ // The debug folder itself may be buildable
71
+ let tmpDebugPackage = libPath.join(tmpDebugFolder, 'package.json');
72
+ if (libFS.existsSync(tmpDebugPackage))
73
+ {
74
+ // Check if debug is already in the list (it would be from the main scan)
75
+ let tmpAlreadyIncluded = tmpExampleFolders.some((pFolder) => pFolder.Name === 'debug');
76
+ if (!tmpAlreadyIncluded)
77
+ {
78
+ tmpExampleFolders.push(
79
+ {
80
+ Name: 'debug',
81
+ Path: tmpDebugFolder,
82
+ PackagePath: tmpDebugPackage
83
+ });
84
+ }
85
+ }
86
+
87
+ if (tmpExampleFolders.length < 1)
88
+ {
89
+ this.log.warn(`No example application folders with package.json found in [${tmpExamplesFolder}].`);
90
+ return fCallback();
91
+ }
92
+
93
+ this.log.info(`Found ${tmpExampleFolders.length} example application(s) to build.`);
94
+
95
+ // Build each example in series
96
+ this.fable.Utility.eachLimit(
97
+ tmpExampleFolders, 1,
98
+ (pExample, fExampleCallback) =>
99
+ {
100
+ this.log.info(`####### Building example: ${pExample.Name} #######`);
101
+
102
+ // Read the example's package.json to check for a build script
103
+ let tmpPackage;
104
+ try
105
+ {
106
+ tmpPackage = JSON.parse(libFS.readFileSync(pExample.PackagePath, 'utf8'));
107
+ }
108
+ catch (pError)
109
+ {
110
+ this.log.error(`Error reading package.json for [${pExample.Name}]: ${pError.message}`);
111
+ return fExampleCallback();
112
+ }
113
+
114
+ // Check if there is a build script
115
+ if (!tmpPackage.scripts || !tmpPackage.scripts.build)
116
+ {
117
+ this.log.warn(`No build script in [${pExample.Name}] -- skipping.`);
118
+ return fExampleCallback();
119
+ }
120
+
121
+ // Find npx (we use npx to run quack build in each folder)
122
+ let tmpNpxLocation = this.resolveExecutable('npx');
123
+ if (!tmpNpxLocation)
124
+ {
125
+ this.log.warn(`Could not find npx to build [${pExample.Name}] -- skipping.`);
126
+ return fExampleCallback();
127
+ }
128
+
129
+ // Run npm run build in the example folder
130
+ this.fable.QuackageProcess.execute(
131
+ tmpNpxLocation,
132
+ ['quack', 'build'],
133
+ { cwd: pExample.Path },
134
+ (pBuildError) =>
135
+ {
136
+ if (pBuildError)
137
+ {
138
+ this.log.error(`Build error in [${pExample.Name}]: ${pBuildError.message}`);
139
+ // Continue building other examples
140
+ }
141
+
142
+ // Now run quack copy if the package has copyFiles
143
+ if (tmpPackage.copyFiles || tmpPackage.copyFilesSettings)
144
+ {
145
+ this.log.info(`Copying files for [${pExample.Name}] ...`);
146
+ this.fable.QuackageProcess.execute(
147
+ tmpNpxLocation,
148
+ ['quack', 'copy'],
149
+ { cwd: pExample.Path },
150
+ (pCopyError) =>
151
+ {
152
+ if (pCopyError)
153
+ {
154
+ this.log.error(`Copy error in [${pExample.Name}]: ${pCopyError.message}`);
155
+ }
156
+ return fExampleCallback();
157
+ });
158
+ }
159
+ else
160
+ {
161
+ return fExampleCallback();
162
+ }
163
+ });
164
+ },
165
+ (pError) =>
166
+ {
167
+ if (pError)
168
+ {
169
+ this.log.error(`Error building examples: ${pError.message}`);
170
+ }
171
+ else
172
+ {
173
+ this.log.info(`All examples built successfully!`);
174
+ }
175
+ return fCallback(pError);
176
+ });
177
+ }
178
+
179
+ resolveExecutable(pName)
180
+ {
181
+ let tmpLocations =
182
+ [
183
+ `${this.fable.AppData.CWD}/node_modules/.bin/${pName}`,
184
+ `${__dirname}/../../../../../.bin/${pName}`,
185
+ `${__dirname}/../../../../node_modules/.bin/${pName}`
186
+ ];
187
+
188
+ for (let i = 0; i < tmpLocations.length; i++)
189
+ {
190
+ if (libFS.existsSync(tmpLocations[i]))
191
+ {
192
+ return tmpLocations[i];
193
+ }
194
+ }
195
+
196
+ // Fall back to just the bare command name (rely on PATH)
197
+ return pName;
198
+ }
199
+ }
200
+
201
+ module.exports = QuackageCommandExamplesBuild;
@@ -0,0 +1,325 @@
1
+ const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
2
+ const libFS = require('fs');
3
+ const libPath = require('path');
4
+ const libHTTP = require('http');
5
+
6
+ class QuackageCommandExamplesServe extends libCommandLineCommand
7
+ {
8
+ constructor(pFable, pManifest, pServiceHash)
9
+ {
10
+ super(pFable, pManifest, pServiceHash);
11
+
12
+ this.options.CommandKeyword = 'examples-serve';
13
+ this.options.Description = 'Serve example applications with an auto-generated index page.';
14
+
15
+ this.options.CommandArguments.push({ Name: '[examples_folder]', Description: 'The examples folder (defaults to ./example_applications).' });
16
+
17
+ this.options.CommandOptions.push({ Name: '-p, --port [port]', Description: 'Port to serve on (default: auto-hashed from project name between 9000-9500).', Default: '' });
18
+
19
+ this.addCommand();
20
+ }
21
+
22
+ hashProjectNameToPort(pProjectName)
23
+ {
24
+ let tmpHash = 0;
25
+ for (let i = 0; i < pProjectName.length; i++)
26
+ {
27
+ let tmpChar = pProjectName.charCodeAt(i);
28
+ tmpHash = ((tmpHash << 5) - tmpHash) + tmpChar;
29
+ tmpHash = tmpHash & tmpHash; // Convert to 32-bit integer
30
+ }
31
+ // Map to range 9000-9500
32
+ return 9000 + (Math.abs(tmpHash) % 501);
33
+ }
34
+
35
+ gatherServableExamples(pBasePath)
36
+ {
37
+ let tmpExamples = [];
38
+
39
+ if (!libFS.existsSync(pBasePath))
40
+ {
41
+ return tmpExamples;
42
+ }
43
+
44
+ let tmpEntries = libFS.readdirSync(pBasePath, { withFileTypes: true });
45
+ for (let i = 0; i < tmpEntries.length; i++)
46
+ {
47
+ if (!tmpEntries[i].isDirectory())
48
+ {
49
+ continue;
50
+ }
51
+
52
+ let tmpDirName = tmpEntries[i].name;
53
+
54
+ // Skip node_modules and hidden folders
55
+ if (tmpDirName === 'node_modules' || tmpDirName.startsWith('.'))
56
+ {
57
+ continue;
58
+ }
59
+
60
+ let tmpFolderPath = libPath.join(pBasePath, tmpDirName);
61
+
62
+ // Check for dist/index.html (standard example pattern)
63
+ let tmpDistIndex = libPath.join(tmpFolderPath, 'dist', 'index.html');
64
+ if (libFS.existsSync(tmpDistIndex))
65
+ {
66
+ // Try to get a nice name from package.json
67
+ let tmpDisplayName = this.formatDisplayName(tmpDirName);
68
+ let tmpPackagePath = libPath.join(tmpFolderPath, 'package.json');
69
+ if (libFS.existsSync(tmpPackagePath))
70
+ {
71
+ try
72
+ {
73
+ let tmpPkg = JSON.parse(libFS.readFileSync(tmpPackagePath, 'utf8'));
74
+ if (tmpPkg.description)
75
+ {
76
+ tmpDisplayName = tmpPkg.description;
77
+ }
78
+ }
79
+ catch (pError)
80
+ {
81
+ // Use the formatted folder name
82
+ }
83
+ }
84
+
85
+ tmpExamples.push(
86
+ {
87
+ Name: tmpDirName,
88
+ DisplayName: tmpDisplayName,
89
+ RelativePath: `${tmpDirName}/dist/index.html`,
90
+ Type: 'example'
91
+ });
92
+ continue;
93
+ }
94
+
95
+ // Check for direct index.html (debug folder pattern)
96
+ let tmpDirectIndex = libPath.join(tmpFolderPath, 'index.html');
97
+ if (libFS.existsSync(tmpDirectIndex))
98
+ {
99
+ tmpExamples.push(
100
+ {
101
+ Name: tmpDirName,
102
+ DisplayName: this.formatDisplayName(tmpDirName),
103
+ RelativePath: `${tmpDirName}/index.html`,
104
+ Type: 'debug'
105
+ });
106
+ }
107
+ }
108
+
109
+ return tmpExamples;
110
+ }
111
+
112
+ formatDisplayName(pFolderName)
113
+ {
114
+ // Convert folder_name to Title Case Display Name
115
+ return pFolderName
116
+ .split(/[-_]/)
117
+ .map((pWord) => pWord.charAt(0).toUpperCase() + pWord.slice(1))
118
+ .join(' ');
119
+ }
120
+
121
+ generateIndexHTML(pProjectName, pExamples, pPort)
122
+ {
123
+ let tmpExampleListItems = '';
124
+ for (let i = 0; i < pExamples.length; i++)
125
+ {
126
+ let tmpExample = pExamples[i];
127
+ let tmpTypeLabel = tmpExample.Type === 'debug' ? ' <span class="type-badge debug">debug</span>' : '';
128
+ tmpExampleListItems += `\t\t\t<li><a href="${tmpExample.RelativePath}">${tmpExample.DisplayName}${tmpTypeLabel}</a></li>\n`;
129
+ }
130
+
131
+ return `<!DOCTYPE html>
132
+ <html lang="en">
133
+ <head>
134
+ <meta charset="UTF-8">
135
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
136
+ <title>Examples - ${pProjectName}</title>
137
+ <style>
138
+ *, *::before, *::after { box-sizing: border-box; }
139
+ body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #FAEDCD; color: #264653; }
140
+
141
+ /* --- Header Bar --- */
142
+ .pict-example-header { display: flex; align-items: stretch; background: #264653; border-bottom: 3px solid #E76F51; }
143
+ .pict-example-badge { background: #E76F51; color: #fff; padding: 0.6rem 1rem; font-size: 0.7rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.1em; display: flex; align-items: center; gap: 0.5rem; }
144
+ .pict-example-badge svg { width: 14px; height: 14px; fill: #fff; flex-shrink: 0; }
145
+ .pict-example-app-name { padding: 0.6rem 1rem; color: #FAEDCD; font-size: 1.1rem; font-weight: 600; display: flex; align-items: center; }
146
+ .pict-example-module { margin-left: auto; padding: 0.6rem 1rem; color: #D4A373; font-size: 0.75rem; display: flex; align-items: center; letter-spacing: 0.03em; }
147
+
148
+ /* --- Content Area --- */
149
+ .pict-example-content { max-width: 800px; margin: 0 auto; padding: 2rem 1.5rem; }
150
+ .pict-example-content h1 { color: #264653; font-size: 1.5rem; margin: 0 0 0.5rem; }
151
+ .pict-example-content .subtitle { color: #6B705C; font-size: 0.85rem; margin: 0 0 1.5rem; }
152
+
153
+ /* --- Example List --- */
154
+ .example-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.5rem; }
155
+ .example-list li { background: #fff; border: 1px solid #D4A373; border-left: 4px solid #E76F51; border-radius: 4px; transition: border-color 0.15s, box-shadow 0.15s; }
156
+ .example-list li:hover { border-left-color: #264653; box-shadow: 0 2px 8px rgba(38,70,83,0.1); }
157
+ .example-list a { display: block; padding: 0.75rem 1rem; text-decoration: none; color: #264653; font-weight: 500; font-size: 0.95rem; }
158
+ .example-list a:hover { color: #E76F51; }
159
+
160
+ /* --- Type Badge --- */
161
+ .type-badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 3px; font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; vertical-align: middle; margin-left: 0.5rem; }
162
+ .type-badge.debug { background: #264653; color: #FAEDCD; }
163
+
164
+ /* --- Footer --- */
165
+ .pict-example-footer { text-align: center; padding: 1.5rem; color: #6B705C; font-size: 0.75rem; border-top: 1px solid #D4A373; margin-top: 2rem; }
166
+ </style>
167
+ </head>
168
+ <body>
169
+ <div class="pict-example-header">
170
+ <div class="pict-example-badge">
171
+ <svg viewBox="0 0 16 16"><polygon points="8,1 10,6 16,6 11,9.5 13,15 8,11.5 3,15 5,9.5 0,6 6,6"/></svg>
172
+ Pict Example
173
+ </div>
174
+ <div class="pict-example-app-name">Example Index</div>
175
+ <div class="pict-example-module">${pProjectName}</div>
176
+ </div>
177
+ <div class="pict-example-content">
178
+ <h1>Example Applications</h1>
179
+ <p class="subtitle">${pExamples.length} example(s) found &mdash; served on port ${pPort}</p>
180
+ <ul class="example-list">
181
+ ${tmpExampleListItems} </ul>
182
+ </div>
183
+ <div class="pict-example-footer">
184
+ Served by quackage examples-serve
185
+ </div>
186
+ </body>
187
+ </html>`;
188
+ }
189
+
190
+ getMimeType(pExtension)
191
+ {
192
+ let tmpMimeTypes =
193
+ {
194
+ '.html': 'text/html',
195
+ '.js': 'text/javascript',
196
+ '.css': 'text/css',
197
+ '.json': 'application/json',
198
+ '.png': 'image/png',
199
+ '.jpg': 'image/jpeg',
200
+ '.jpeg': 'image/jpeg',
201
+ '.gif': 'image/gif',
202
+ '.svg': 'image/svg+xml',
203
+ '.ico': 'image/x-icon',
204
+ '.woff': 'font/woff',
205
+ '.woff2': 'font/woff2',
206
+ '.ttf': 'font/ttf',
207
+ '.map': 'application/json'
208
+ };
209
+ return tmpMimeTypes[pExtension] || 'application/octet-stream';
210
+ }
211
+
212
+ onRunAsync(fCallback)
213
+ {
214
+ let tmpExamplesFolder = libPath.resolve(this.ArgumentString || './example_applications');
215
+ let tmpPort = this.CommandOptions.port ? parseInt(this.CommandOptions.port, 10) : 0;
216
+
217
+ let tmpProjectName = this.fable.AppData.Package.name || 'unknown-project';
218
+
219
+ if (!tmpPort)
220
+ {
221
+ tmpPort = this.hashProjectNameToPort(tmpProjectName);
222
+ }
223
+
224
+ this.log.info(`Scanning for example applications in [${tmpExamplesFolder}] ...`);
225
+
226
+ let tmpExamples = this.gatherServableExamples(tmpExamplesFolder);
227
+
228
+ if (tmpExamples.length < 1)
229
+ {
230
+ this.log.warn(`No servable examples found in [${tmpExamplesFolder}]. Looking for subfolders with dist/index.html or index.html.`);
231
+ return fCallback();
232
+ }
233
+
234
+ this.log.info(`Found ${tmpExamples.length} servable example(s).`);
235
+
236
+ let tmpIndexHTML = this.generateIndexHTML(tmpProjectName, tmpExamples, tmpPort);
237
+
238
+ // Create a simple HTTP server
239
+ let tmpServer = libHTTP.createServer(
240
+ (pRequest, pResponse) =>
241
+ {
242
+ let tmpRequestURL = pRequest.url.split('?')[0];
243
+
244
+ // Serve the in-memory index for root
245
+ if (tmpRequestURL === '/' || tmpRequestURL === '/index.html')
246
+ {
247
+ pResponse.writeHead(200, { 'Content-Type': 'text/html' });
248
+ pResponse.end(tmpIndexHTML);
249
+ return;
250
+ }
251
+
252
+ // Serve static files from the examples folder
253
+ let tmpFilePath = libPath.join(tmpExamplesFolder, decodeURIComponent(tmpRequestURL));
254
+
255
+ // Prevent path traversal
256
+ if (!tmpFilePath.startsWith(tmpExamplesFolder))
257
+ {
258
+ pResponse.writeHead(403);
259
+ pResponse.end('Forbidden');
260
+ return;
261
+ }
262
+
263
+ // If it's a directory, try index.html
264
+ if (libFS.existsSync(tmpFilePath) && libFS.statSync(tmpFilePath).isDirectory())
265
+ {
266
+ tmpFilePath = libPath.join(tmpFilePath, 'index.html');
267
+ }
268
+
269
+ if (!libFS.existsSync(tmpFilePath))
270
+ {
271
+ pResponse.writeHead(404);
272
+ pResponse.end('Not Found');
273
+ return;
274
+ }
275
+
276
+ let tmpExtension = libPath.extname(tmpFilePath).toLowerCase();
277
+ let tmpMimeType = this.getMimeType(tmpExtension);
278
+
279
+ try
280
+ {
281
+ let tmpContent = libFS.readFileSync(tmpFilePath);
282
+ pResponse.writeHead(200, { 'Content-Type': tmpMimeType });
283
+ pResponse.end(tmpContent);
284
+ }
285
+ catch (pError)
286
+ {
287
+ pResponse.writeHead(500);
288
+ pResponse.end('Internal Server Error');
289
+ }
290
+ });
291
+
292
+ tmpServer.listen(tmpPort,
293
+ () =>
294
+ {
295
+ this.log.info(`##############################################`);
296
+ this.log.info(` Example server running at http://localhost:${tmpPort}/`);
297
+ this.log.info(` Project: ${tmpProjectName}`);
298
+ this.log.info(` Serving ${tmpExamples.length} example(s):`);
299
+ for (let i = 0; i < tmpExamples.length; i++)
300
+ {
301
+ this.log.info(` - ${tmpExamples[i].DisplayName}: http://localhost:${tmpPort}/${tmpExamples[i].RelativePath}`);
302
+ }
303
+ this.log.info(`##############################################`);
304
+ this.log.info(`Press Ctrl+C to stop.`);
305
+ });
306
+
307
+ tmpServer.on('error',
308
+ (pError) =>
309
+ {
310
+ if (pError.code === 'EADDRINUSE')
311
+ {
312
+ this.log.error(`Port ${tmpPort} is already in use. Try specifying a different port with -p.`);
313
+ }
314
+ else
315
+ {
316
+ this.log.error(`Server error: ${pError.message}`);
317
+ }
318
+ return fCallback(pError);
319
+ });
320
+
321
+ // Keep the process running (don't call fCallback -- server is long-lived)
322
+ }
323
+ }
324
+
325
+ module.exports = QuackageCommandExamplesServe;