quackage 1.0.53 → 1.0.54

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.53",
3
+ "version": "1.0.54",
4
4
  "description": "Building. Testing. Quacking. Reloading.",
5
5
  "main": "source/Quackage-CLIProgram.js",
6
6
  "scripts": {
@@ -61,6 +61,8 @@
61
61
 
62
62
  "build": "npx quack build",
63
63
 
64
+ "example": "npx quack example",
65
+
64
66
  "docs": "npx quack prepare-docs ./docs -d ./modules",
65
67
  "docs-serve": "npx quack docs-serve ./docs"
66
68
  },
@@ -62,6 +62,8 @@ _Pict.instantiateServiceProvider('FilePersistence');
62
62
  _Pict.instantiateServiceProvider('DataGeneration');
63
63
  // Add the Quackage Process Management service
64
64
  _Pict.addAndInstantiateServiceType('QuackageProcess', require('./services/Quackage-Execute-Process.js'));
65
+ // Add the Quackage Example Service for building and serving HTML examples
66
+ _Pict.addAndInstantiateServiceType('QuackageExampleService', require('./services/Quackage-ExampleService.js'));
65
67
 
66
68
  // Grab the current working directory for the quackage
67
69
  _Pict.AppData.CWD = _Pict.QuackageProcess.cwd();
@@ -20,22 +20,11 @@ class QuackageCommandExamples extends libCommandLineCommand
20
20
  onRunAsync(fCallback)
21
21
  {
22
22
  let tmpExamplesFolder = libPath.resolve(this.ArgumentString || './example_applications');
23
- let tmpPort = this.CommandOptions.port || '';
23
+ let tmpPort = this.CommandOptions.port ? parseInt(this.CommandOptions.port, 10) : 0;
24
24
 
25
25
  this.log.info(`Building and serving examples from [${tmpExamplesFolder}] ...`);
26
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(
27
+ this.fable.QuackageExampleService.buildExamples(tmpExamplesFolder,
39
28
  (pBuildError) =>
40
29
  {
41
30
  if (pBuildError)
@@ -43,23 +32,8 @@ class QuackageCommandExamples extends libCommandLineCommand
43
32
  this.log.warn(`Some examples had build errors, but continuing to serve what we have...`);
44
33
  }
45
34
 
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
35
  // examples-serve doesn't call back (long-lived server), so neither do we
62
- tmpServeCommand.onRunAsync(fCallback);
36
+ this.fable.QuackageExampleService.serveExamples(tmpExamplesFolder, tmpPort, fCallback);
63
37
  });
64
38
  }
65
39
  }
@@ -1,5 +1,4 @@
1
1
  const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
2
- const libFS = require('fs');
3
2
  const libPath = require('path');
4
3
 
5
4
  class QuackageCommandExamplesBuild extends libCommandLineCommand
@@ -16,185 +15,10 @@ class QuackageCommandExamplesBuild extends libCommandLineCommand
16
15
  this.addCommand();
17
16
  }
18
17
 
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
18
  onRunAsync(fCallback)
62
19
  {
63
20
  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;
21
+ this.fable.QuackageExampleService.buildExamples(tmpExamplesFolder, fCallback);
198
22
  }
199
23
  }
200
24
 
@@ -1,7 +1,5 @@
1
1
  const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
2
- const libFS = require('fs');
3
2
  const libPath = require('path');
4
- const libHTTP = require('http');
5
3
 
6
4
  class QuackageCommandExamplesServe extends libCommandLineCommand
7
5
  {
@@ -19,306 +17,11 @@ class QuackageCommandExamplesServe extends libCommandLineCommand
19
17
  this.addCommand();
20
18
  }
21
19
 
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
20
  onRunAsync(fCallback)
213
21
  {
214
22
  let tmpExamplesFolder = libPath.resolve(this.ArgumentString || './example_applications');
215
23
  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)
24
+ this.fable.QuackageExampleService.serveExamples(tmpExamplesFolder, tmpPort, fCallback);
322
25
  }
323
26
  }
324
27
 
@@ -0,0 +1,487 @@
1
+ const libPict = require('pict');
2
+ const libFS = require('fs');
3
+ const libPath = require('path');
4
+ const libHTTP = require('http');
5
+
6
+ class QuackageExampleService extends libPict.ServiceProviderBase
7
+ {
8
+ constructor(pFable, pManifest, pServiceHash)
9
+ {
10
+ super(pFable, pManifest, pServiceHash);
11
+
12
+ this.serviceType = 'QuackageExampleService';
13
+ }
14
+
15
+ // --- Folder scanning ---
16
+
17
+ gatherExampleFolders(pBasePath)
18
+ {
19
+ let tmpExampleFolders = [];
20
+
21
+ if (!libFS.existsSync(pBasePath))
22
+ {
23
+ return tmpExampleFolders;
24
+ }
25
+
26
+ let tmpEntries = libFS.readdirSync(pBasePath, { withFileTypes: true });
27
+ for (let i = 0; i < tmpEntries.length; i++)
28
+ {
29
+ if (!tmpEntries[i].isDirectory())
30
+ {
31
+ continue;
32
+ }
33
+
34
+ let tmpDirName = tmpEntries[i].name;
35
+
36
+ // Skip node_modules and hidden folders
37
+ if (tmpDirName === 'node_modules' || tmpDirName.startsWith('.'))
38
+ {
39
+ continue;
40
+ }
41
+
42
+ let tmpFolderPath = libPath.join(pBasePath, tmpDirName);
43
+ let tmpPackagePath = libPath.join(tmpFolderPath, 'package.json');
44
+
45
+ if (libFS.existsSync(tmpPackagePath))
46
+ {
47
+ tmpExampleFolders.push(
48
+ {
49
+ Name: tmpDirName,
50
+ Path: tmpFolderPath,
51
+ PackagePath: tmpPackagePath
52
+ });
53
+ }
54
+ }
55
+
56
+ return tmpExampleFolders;
57
+ }
58
+
59
+ gatherServableExamples(pBasePath)
60
+ {
61
+ let tmpExamples = [];
62
+
63
+ if (!libFS.existsSync(pBasePath))
64
+ {
65
+ return tmpExamples;
66
+ }
67
+
68
+ let tmpEntries = libFS.readdirSync(pBasePath, { withFileTypes: true });
69
+ for (let i = 0; i < tmpEntries.length; i++)
70
+ {
71
+ if (!tmpEntries[i].isDirectory())
72
+ {
73
+ continue;
74
+ }
75
+
76
+ let tmpDirName = tmpEntries[i].name;
77
+
78
+ // Skip node_modules and hidden folders
79
+ if (tmpDirName === 'node_modules' || tmpDirName.startsWith('.'))
80
+ {
81
+ continue;
82
+ }
83
+
84
+ let tmpFolderPath = libPath.join(pBasePath, tmpDirName);
85
+
86
+ // Check for dist/index.html (standard example pattern)
87
+ let tmpDistIndex = libPath.join(tmpFolderPath, 'dist', 'index.html');
88
+ if (libFS.existsSync(tmpDistIndex))
89
+ {
90
+ // Try to get a nice name from package.json
91
+ let tmpDisplayName = this.formatDisplayName(tmpDirName);
92
+ let tmpPackagePath = libPath.join(tmpFolderPath, 'package.json');
93
+ if (libFS.existsSync(tmpPackagePath))
94
+ {
95
+ try
96
+ {
97
+ let tmpPkg = JSON.parse(libFS.readFileSync(tmpPackagePath, 'utf8'));
98
+ if (tmpPkg.description)
99
+ {
100
+ tmpDisplayName = tmpPkg.description;
101
+ }
102
+ }
103
+ catch (pError)
104
+ {
105
+ // Use the formatted folder name
106
+ }
107
+ }
108
+
109
+ tmpExamples.push(
110
+ {
111
+ Name: tmpDirName,
112
+ DisplayName: tmpDisplayName,
113
+ RelativePath: `${tmpDirName}/dist/index.html`,
114
+ Type: 'example'
115
+ });
116
+ continue;
117
+ }
118
+
119
+ // Check for direct index.html (debug folder pattern)
120
+ let tmpDirectIndex = libPath.join(tmpFolderPath, 'index.html');
121
+ if (libFS.existsSync(tmpDirectIndex))
122
+ {
123
+ tmpExamples.push(
124
+ {
125
+ Name: tmpDirName,
126
+ DisplayName: this.formatDisplayName(tmpDirName),
127
+ RelativePath: `${tmpDirName}/index.html`,
128
+ Type: 'debug'
129
+ });
130
+ }
131
+ }
132
+
133
+ return tmpExamples;
134
+ }
135
+
136
+ // --- Display helpers ---
137
+
138
+ formatDisplayName(pFolderName)
139
+ {
140
+ // Convert folder_name to Title Case Display Name
141
+ return pFolderName
142
+ .split(/[-_]/)
143
+ .map((pWord) => pWord.charAt(0).toUpperCase() + pWord.slice(1))
144
+ .join(' ');
145
+ }
146
+
147
+ hashProjectNameToPort(pProjectName)
148
+ {
149
+ let tmpHash = 0;
150
+ for (let i = 0; i < pProjectName.length; i++)
151
+ {
152
+ let tmpChar = pProjectName.charCodeAt(i);
153
+ tmpHash = ((tmpHash << 5) - tmpHash) + tmpChar;
154
+ tmpHash = tmpHash & tmpHash; // Convert to 32-bit integer
155
+ }
156
+ // Map to range 9000-9500
157
+ return 9000 + (Math.abs(tmpHash) % 501);
158
+ }
159
+
160
+ getMimeType(pExtension)
161
+ {
162
+ let tmpMimeTypes =
163
+ {
164
+ '.html': 'text/html',
165
+ '.js': 'text/javascript',
166
+ '.css': 'text/css',
167
+ '.json': 'application/json',
168
+ '.png': 'image/png',
169
+ '.jpg': 'image/jpeg',
170
+ '.jpeg': 'image/jpeg',
171
+ '.gif': 'image/gif',
172
+ '.svg': 'image/svg+xml',
173
+ '.ico': 'image/x-icon',
174
+ '.woff': 'font/woff',
175
+ '.woff2': 'font/woff2',
176
+ '.ttf': 'font/ttf',
177
+ '.map': 'application/json'
178
+ };
179
+ return tmpMimeTypes[pExtension] || 'application/octet-stream';
180
+ }
181
+
182
+ // --- HTML generation ---
183
+
184
+ generateIndexHTML(pProjectName, pExamples, pPort)
185
+ {
186
+ let tmpExampleListItems = '';
187
+ for (let i = 0; i < pExamples.length; i++)
188
+ {
189
+ let tmpExample = pExamples[i];
190
+ let tmpTypeLabel = tmpExample.Type === 'debug' ? ' <span class="type-badge debug">debug</span>' : '';
191
+ tmpExampleListItems += `\t\t\t<li><a href="${tmpExample.RelativePath}">${tmpExample.DisplayName}${tmpTypeLabel}</a></li>\n`;
192
+ }
193
+
194
+ return `<!DOCTYPE html>
195
+ <html lang="en">
196
+ <head>
197
+ <meta charset="UTF-8">
198
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
199
+ <title>Examples - ${pProjectName}</title>
200
+ <style>
201
+ *, *::before, *::after { box-sizing: border-box; }
202
+ body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #FAEDCD; color: #264653; }
203
+
204
+ /* --- Header Bar --- */
205
+ .pict-example-header { display: flex; align-items: stretch; background: #264653; border-bottom: 3px solid #E76F51; }
206
+ .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; }
207
+ .pict-example-badge svg { width: 14px; height: 14px; fill: #fff; flex-shrink: 0; }
208
+ .pict-example-app-name { padding: 0.6rem 1rem; color: #FAEDCD; font-size: 1.1rem; font-weight: 600; display: flex; align-items: center; }
209
+ .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; }
210
+
211
+ /* --- Content Area --- */
212
+ .pict-example-content { max-width: 800px; margin: 0 auto; padding: 2rem 1.5rem; }
213
+ .pict-example-content h1 { color: #264653; font-size: 1.5rem; margin: 0 0 0.5rem; }
214
+ .pict-example-content .subtitle { color: #6B705C; font-size: 0.85rem; margin: 0 0 1.5rem; }
215
+
216
+ /* --- Example List --- */
217
+ .example-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.5rem; }
218
+ .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; }
219
+ .example-list li:hover { border-left-color: #264653; box-shadow: 0 2px 8px rgba(38,70,83,0.1); }
220
+ .example-list a { display: block; padding: 0.75rem 1rem; text-decoration: none; color: #264653; font-weight: 500; font-size: 0.95rem; }
221
+ .example-list a:hover { color: #E76F51; }
222
+
223
+ /* --- Type Badge --- */
224
+ .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; }
225
+ .type-badge.debug { background: #264653; color: #FAEDCD; }
226
+
227
+ /* --- Footer --- */
228
+ .pict-example-footer { text-align: center; padding: 1.5rem; color: #6B705C; font-size: 0.75rem; border-top: 1px solid #D4A373; margin-top: 2rem; }
229
+ </style>
230
+ </head>
231
+ <body>
232
+ <div class="pict-example-header">
233
+ <div class="pict-example-badge">
234
+ <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>
235
+ Pict Example
236
+ </div>
237
+ <div class="pict-example-app-name">Example Index</div>
238
+ <div class="pict-example-module">${pProjectName}</div>
239
+ </div>
240
+ <div class="pict-example-content">
241
+ <h1>Example Applications</h1>
242
+ <p class="subtitle">${pExamples.length} example(s) found &mdash; served on port ${pPort}</p>
243
+ <ul class="example-list">
244
+ ${tmpExampleListItems} </ul>
245
+ </div>
246
+ <div class="pict-example-footer">
247
+ Served by quackage examples-serve
248
+ </div>
249
+ </body>
250
+ </html>`;
251
+ }
252
+
253
+ // --- Executable resolution ---
254
+
255
+ resolveExecutable(pName)
256
+ {
257
+ let tmpLocations =
258
+ [
259
+ `${this.fable.AppData.CWD}/node_modules/.bin/${pName}`,
260
+ `${__dirname}/../../../.bin/${pName}`,
261
+ `${__dirname}/../../node_modules/.bin/${pName}`
262
+ ];
263
+
264
+ for (let i = 0; i < tmpLocations.length; i++)
265
+ {
266
+ if (libFS.existsSync(tmpLocations[i]))
267
+ {
268
+ return tmpLocations[i];
269
+ }
270
+ }
271
+
272
+ // Fall back to just the bare command name (rely on PATH)
273
+ return pName;
274
+ }
275
+
276
+ // --- Build all examples ---
277
+
278
+ buildExamples(pExamplesFolder, fCallback)
279
+ {
280
+ let tmpExamplesFolder = libPath.resolve(pExamplesFolder || './example_applications');
281
+
282
+ this.log.info(`Building all examples in [${tmpExamplesFolder}] ...`);
283
+
284
+ // Gather example folders from example_applications and debug
285
+ let tmpExampleFolders = this.gatherExampleFolders(tmpExamplesFolder);
286
+ let tmpDebugFolder = libPath.join(tmpExamplesFolder, 'debug');
287
+ let tmpDebugPackage = libPath.join(tmpDebugFolder, 'package.json');
288
+ if (libFS.existsSync(tmpDebugPackage))
289
+ {
290
+ let tmpAlreadyIncluded = tmpExampleFolders.some((pFolder) => pFolder.Name === 'debug');
291
+ if (!tmpAlreadyIncluded)
292
+ {
293
+ tmpExampleFolders.push(
294
+ {
295
+ Name: 'debug',
296
+ Path: tmpDebugFolder,
297
+ PackagePath: tmpDebugPackage
298
+ });
299
+ }
300
+ }
301
+
302
+ if (tmpExampleFolders.length < 1)
303
+ {
304
+ this.log.warn(`No example application folders with package.json found in [${tmpExamplesFolder}].`);
305
+ return fCallback();
306
+ }
307
+
308
+ this.log.info(`Found ${tmpExampleFolders.length} example application(s) to build.`);
309
+
310
+ // Build each example in series
311
+ this.fable.Utility.eachLimit(
312
+ tmpExampleFolders, 1,
313
+ (pExample, fExampleCallback) =>
314
+ {
315
+ this.log.info(`####### Building example: ${pExample.Name} #######`);
316
+
317
+ let tmpPackage;
318
+ try
319
+ {
320
+ tmpPackage = JSON.parse(libFS.readFileSync(pExample.PackagePath, 'utf8'));
321
+ }
322
+ catch (pError)
323
+ {
324
+ this.log.error(`Error reading package.json for [${pExample.Name}]: ${pError.message}`);
325
+ return fExampleCallback();
326
+ }
327
+
328
+ if (!tmpPackage.scripts || !tmpPackage.scripts.build)
329
+ {
330
+ this.log.warn(`No build script in [${pExample.Name}] -- skipping.`);
331
+ return fExampleCallback();
332
+ }
333
+
334
+ let tmpNpxLocation = this.resolveExecutable('npx');
335
+
336
+ this.fable.QuackageProcess.execute(
337
+ tmpNpxLocation,
338
+ ['quack', 'build'],
339
+ { cwd: pExample.Path },
340
+ (pBuildError) =>
341
+ {
342
+ if (pBuildError)
343
+ {
344
+ this.log.error(`Build error in [${pExample.Name}]: ${pBuildError.message}`);
345
+ }
346
+
347
+ if (tmpPackage.copyFiles || tmpPackage.copyFilesSettings)
348
+ {
349
+ this.log.info(`Copying files for [${pExample.Name}] ...`);
350
+ this.fable.QuackageProcess.execute(
351
+ tmpNpxLocation,
352
+ ['quack', 'copy'],
353
+ { cwd: pExample.Path },
354
+ (pCopyError) =>
355
+ {
356
+ if (pCopyError)
357
+ {
358
+ this.log.error(`Copy error in [${pExample.Name}]: ${pCopyError.message}`);
359
+ }
360
+ return fExampleCallback();
361
+ });
362
+ }
363
+ else
364
+ {
365
+ return fExampleCallback();
366
+ }
367
+ });
368
+ },
369
+ (pError) =>
370
+ {
371
+ if (pError)
372
+ {
373
+ this.log.error(`Error building examples: ${pError.message}`);
374
+ }
375
+ else
376
+ {
377
+ this.log.info(`All examples built successfully!`);
378
+ }
379
+ return fCallback(pError);
380
+ });
381
+ }
382
+
383
+ // --- Serve examples ---
384
+
385
+ serveExamples(pExamplesFolder, pPort, fCallback)
386
+ {
387
+ let tmpExamplesFolder = libPath.resolve(pExamplesFolder || './example_applications');
388
+ let tmpProjectName = this.fable.AppData.Package.name || 'unknown-project';
389
+ let tmpPort = pPort || this.hashProjectNameToPort(tmpProjectName);
390
+
391
+ this.log.info(`Scanning for example applications in [${tmpExamplesFolder}] ...`);
392
+
393
+ let tmpExamples = this.gatherServableExamples(tmpExamplesFolder);
394
+
395
+ if (tmpExamples.length < 1)
396
+ {
397
+ this.log.warn(`No servable examples found in [${tmpExamplesFolder}]. Looking for subfolders with dist/index.html or index.html.`);
398
+ return fCallback();
399
+ }
400
+
401
+ this.log.info(`Found ${tmpExamples.length} servable example(s).`);
402
+
403
+ let tmpIndexHTML = this.generateIndexHTML(tmpProjectName, tmpExamples, tmpPort);
404
+
405
+ let tmpServer = libHTTP.createServer(
406
+ (pRequest, pResponse) =>
407
+ {
408
+ let tmpRequestURL = pRequest.url.split('?')[0];
409
+
410
+ if (tmpRequestURL === '/' || tmpRequestURL === '/index.html')
411
+ {
412
+ pResponse.writeHead(200, { 'Content-Type': 'text/html' });
413
+ pResponse.end(tmpIndexHTML);
414
+ return;
415
+ }
416
+
417
+ let tmpFilePath = libPath.join(tmpExamplesFolder, decodeURIComponent(tmpRequestURL));
418
+
419
+ if (!tmpFilePath.startsWith(tmpExamplesFolder))
420
+ {
421
+ pResponse.writeHead(403);
422
+ pResponse.end('Forbidden');
423
+ return;
424
+ }
425
+
426
+ if (libFS.existsSync(tmpFilePath) && libFS.statSync(tmpFilePath).isDirectory())
427
+ {
428
+ tmpFilePath = libPath.join(tmpFilePath, 'index.html');
429
+ }
430
+
431
+ if (!libFS.existsSync(tmpFilePath))
432
+ {
433
+ pResponse.writeHead(404);
434
+ pResponse.end('Not Found');
435
+ return;
436
+ }
437
+
438
+ let tmpExtension = libPath.extname(tmpFilePath).toLowerCase();
439
+ let tmpMimeType = this.getMimeType(tmpExtension);
440
+
441
+ try
442
+ {
443
+ let tmpContent = libFS.readFileSync(tmpFilePath);
444
+ pResponse.writeHead(200, { 'Content-Type': tmpMimeType });
445
+ pResponse.end(tmpContent);
446
+ }
447
+ catch (pError)
448
+ {
449
+ pResponse.writeHead(500);
450
+ pResponse.end('Internal Server Error');
451
+ }
452
+ });
453
+
454
+ tmpServer.listen(tmpPort,
455
+ () =>
456
+ {
457
+ this.log.info(`##############################################`);
458
+ this.log.info(` Example server running at http://localhost:${tmpPort}/`);
459
+ this.log.info(` Project: ${tmpProjectName}`);
460
+ this.log.info(` Serving ${tmpExamples.length} example(s):`);
461
+ for (let i = 0; i < tmpExamples.length; i++)
462
+ {
463
+ this.log.info(` - ${tmpExamples[i].DisplayName}: http://localhost:${tmpPort}/${tmpExamples[i].RelativePath}`);
464
+ }
465
+ this.log.info(`##############################################`);
466
+ this.log.info(`Press Ctrl+C to stop.`);
467
+ });
468
+
469
+ tmpServer.on('error',
470
+ (pError) =>
471
+ {
472
+ if (pError.code === 'EADDRINUSE')
473
+ {
474
+ this.log.error(`Port ${tmpPort} is already in use. Try specifying a different port with -p.`);
475
+ }
476
+ else
477
+ {
478
+ this.log.error(`Server error: ${pError.message}`);
479
+ }
480
+ return fCallback(pError);
481
+ });
482
+
483
+ // Keep the process running (don't call fCallback -- server is long-lived)
484
+ }
485
+ }
486
+
487
+ module.exports = QuackageExampleService;
@@ -435,36 +435,29 @@ suite
435
435
  {
436
436
  test
437
437
  (
438
- 'ExamplesBuild command class should require and have expected methods.',
438
+ 'ExamplesBuild command class should require and have onRunAsync.',
439
439
  function()
440
440
  {
441
441
  let tmpExamplesBuild = require('../source/commands/html_example_serving/Quackage-Command-ExamplesBuild.js');
442
442
  Expect(tmpExamplesBuild).to.be.a('function');
443
- Expect(tmpExamplesBuild.prototype.gatherExampleFolders).to.be.a('function');
444
- Expect(tmpExamplesBuild.prototype.resolveExecutable).to.be.a('function');
445
443
  Expect(tmpExamplesBuild.prototype.onRunAsync).to.be.a('function');
446
444
  }
447
445
  );
448
446
 
449
447
  test
450
448
  (
451
- 'ExamplesServe command class should require and have expected methods.',
449
+ 'ExamplesServe command class should require and have onRunAsync.',
452
450
  function()
453
451
  {
454
452
  let tmpExamplesServe = require('../source/commands/html_example_serving/Quackage-Command-ExamplesServe.js');
455
453
  Expect(tmpExamplesServe).to.be.a('function');
456
- Expect(tmpExamplesServe.prototype.gatherServableExamples).to.be.a('function');
457
- Expect(tmpExamplesServe.prototype.hashProjectNameToPort).to.be.a('function');
458
- Expect(tmpExamplesServe.prototype.generateIndexHTML).to.be.a('function');
459
- Expect(tmpExamplesServe.prototype.formatDisplayName).to.be.a('function');
460
- Expect(tmpExamplesServe.prototype.getMimeType).to.be.a('function');
461
454
  Expect(tmpExamplesServe.prototype.onRunAsync).to.be.a('function');
462
455
  }
463
456
  );
464
457
 
465
458
  test
466
459
  (
467
- 'Examples combined command class should require and have expected methods.',
460
+ 'Examples combined command class should require and have onRunAsync.',
468
461
  function()
469
462
  {
470
463
  let tmpExamples = require('../source/commands/html_example_serving/Quackage-Command-Examples.js');
@@ -472,15 +465,48 @@ suite
472
465
  Expect(tmpExamples.prototype.onRunAsync).to.be.a('function');
473
466
  }
474
467
  );
468
+ }
469
+ );
475
470
 
471
+ suite
472
+ (
473
+ 'QuackageExampleService',
474
+ function()
475
+ {
476
476
  test
477
477
  (
478
- 'ExamplesBuild.gatherExampleFolders should return empty array for nonexistent path.',
478
+ 'QuackageExampleService should be instantiated on the pict instance.',
479
479
  function()
480
480
  {
481
- let tmpExamplesBuild = require('../source/commands/html_example_serving/Quackage-Command-ExamplesBuild.js');
482
- // Call the method without instantiation by borrowing it with a mock context
483
- let tmpResult = tmpExamplesBuild.prototype.gatherExampleFolders.call({}, '/nonexistent/path/that/does/not/exist');
481
+ Expect(libQuackage.QuackageExampleService).to.be.an('object');
482
+ Expect(libQuackage.QuackageExampleService.serviceType).to.equal('QuackageExampleService');
483
+ }
484
+ );
485
+
486
+ test
487
+ (
488
+ 'QuackageExampleService should have expected methods.',
489
+ function()
490
+ {
491
+ let tmpService = libQuackage.QuackageExampleService;
492
+ Expect(tmpService.gatherExampleFolders).to.be.a('function');
493
+ Expect(tmpService.gatherServableExamples).to.be.a('function');
494
+ Expect(tmpService.formatDisplayName).to.be.a('function');
495
+ Expect(tmpService.hashProjectNameToPort).to.be.a('function');
496
+ Expect(tmpService.getMimeType).to.be.a('function');
497
+ Expect(tmpService.generateIndexHTML).to.be.a('function');
498
+ Expect(tmpService.resolveExecutable).to.be.a('function');
499
+ Expect(tmpService.buildExamples).to.be.a('function');
500
+ Expect(tmpService.serveExamples).to.be.a('function');
501
+ }
502
+ );
503
+
504
+ test
505
+ (
506
+ 'gatherExampleFolders should return empty array for nonexistent path.',
507
+ function()
508
+ {
509
+ let tmpResult = libQuackage.QuackageExampleService.gatherExampleFolders('/nonexistent/path/that/does/not/exist');
484
510
  Expect(tmpResult).to.be.an('array');
485
511
  Expect(tmpResult).to.have.length(0);
486
512
  }
@@ -488,13 +514,10 @@ suite
488
514
 
489
515
  test
490
516
  (
491
- 'ExamplesServe.gatherServableExamples should return empty array for nonexistent path.',
517
+ 'gatherServableExamples should return empty array for nonexistent path.',
492
518
  function()
493
519
  {
494
- let tmpExamplesServe = require('../source/commands/html_example_serving/Quackage-Command-ExamplesServe.js');
495
- let tmpResult = tmpExamplesServe.prototype.gatherServableExamples.call({
496
- formatDisplayName: tmpExamplesServe.prototype.formatDisplayName
497
- }, '/nonexistent/path/that/does/not/exist');
520
+ let tmpResult = libQuackage.QuackageExampleService.gatherServableExamples('/nonexistent/path/that/does/not/exist');
498
521
  Expect(tmpResult).to.be.an('array');
499
522
  Expect(tmpResult).to.have.length(0);
500
523
  }
@@ -502,22 +525,21 @@ suite
502
525
 
503
526
  test
504
527
  (
505
- 'ExamplesServe.hashProjectNameToPort should return a port in the expected range.',
528
+ 'hashProjectNameToPort should return a port in the expected range.',
506
529
  function()
507
530
  {
508
- let tmpExamplesServe = require('../source/commands/html_example_serving/Quackage-Command-ExamplesServe.js');
509
- let tmpHash = tmpExamplesServe.prototype.hashProjectNameToPort;
531
+ let tmpService = libQuackage.QuackageExampleService;
510
532
 
511
- let tmpPort1 = tmpHash('pict-section-form');
533
+ let tmpPort1 = tmpService.hashProjectNameToPort('pict-section-form');
512
534
  Expect(tmpPort1).to.be.at.least(9000);
513
535
  Expect(tmpPort1).to.be.at.most(9500);
514
536
 
515
- let tmpPort2 = tmpHash('pict-section-objecteditor');
537
+ let tmpPort2 = tmpService.hashProjectNameToPort('pict-section-objecteditor');
516
538
  Expect(tmpPort2).to.be.at.least(9000);
517
539
  Expect(tmpPort2).to.be.at.most(9500);
518
540
 
519
541
  // Same input should produce same output (deterministic)
520
- let tmpPort3 = tmpHash('pict-section-form');
542
+ let tmpPort3 = tmpService.hashProjectNameToPort('pict-section-form');
521
543
  Expect(tmpPort3).to.equal(tmpPort1);
522
544
 
523
545
  // Different inputs should (very likely) produce different ports
@@ -527,52 +549,49 @@ suite
527
549
 
528
550
  test
529
551
  (
530
- 'ExamplesServe.formatDisplayName should convert folder names to title case.',
552
+ 'formatDisplayName should convert folder names to title case.',
531
553
  function()
532
554
  {
533
- let tmpExamplesServe = require('../source/commands/html_example_serving/Quackage-Command-ExamplesServe.js');
534
- let tmpFormat = tmpExamplesServe.prototype.formatDisplayName;
555
+ let tmpService = libQuackage.QuackageExampleService;
535
556
 
536
- Expect(tmpFormat('simple_form')).to.equal('Simple Form');
537
- Expect(tmpFormat('complex-table')).to.equal('Complex Table');
538
- Expect(tmpFormat('debug')).to.equal('Debug');
539
- Expect(tmpFormat('my_cool-example')).to.equal('My Cool Example');
557
+ Expect(tmpService.formatDisplayName('simple_form')).to.equal('Simple Form');
558
+ Expect(tmpService.formatDisplayName('complex-table')).to.equal('Complex Table');
559
+ Expect(tmpService.formatDisplayName('debug')).to.equal('Debug');
560
+ Expect(tmpService.formatDisplayName('my_cool-example')).to.equal('My Cool Example');
540
561
  }
541
562
  );
542
563
 
543
564
  test
544
565
  (
545
- 'ExamplesServe.getMimeType should return correct MIME types.',
566
+ 'getMimeType should return correct MIME types.',
546
567
  function()
547
568
  {
548
- let tmpExamplesServe = require('../source/commands/html_example_serving/Quackage-Command-ExamplesServe.js');
549
- let tmpMime = tmpExamplesServe.prototype.getMimeType;
569
+ let tmpService = libQuackage.QuackageExampleService;
550
570
 
551
- Expect(tmpMime('.html')).to.equal('text/html');
552
- Expect(tmpMime('.js')).to.equal('text/javascript');
553
- Expect(tmpMime('.css')).to.equal('text/css');
554
- Expect(tmpMime('.json')).to.equal('application/json');
555
- Expect(tmpMime('.png')).to.equal('image/png');
556
- Expect(tmpMime('.svg')).to.equal('image/svg+xml');
557
- Expect(tmpMime('.map')).to.equal('application/json');
558
- Expect(tmpMime('.xyz')).to.equal('application/octet-stream');
571
+ Expect(tmpService.getMimeType('.html')).to.equal('text/html');
572
+ Expect(tmpService.getMimeType('.js')).to.equal('text/javascript');
573
+ Expect(tmpService.getMimeType('.css')).to.equal('text/css');
574
+ Expect(tmpService.getMimeType('.json')).to.equal('application/json');
575
+ Expect(tmpService.getMimeType('.png')).to.equal('image/png');
576
+ Expect(tmpService.getMimeType('.svg')).to.equal('image/svg+xml');
577
+ Expect(tmpService.getMimeType('.map')).to.equal('application/json');
578
+ Expect(tmpService.getMimeType('.xyz')).to.equal('application/octet-stream');
559
579
  }
560
580
  );
561
581
 
562
582
  test
563
583
  (
564
- 'ExamplesServe.generateIndexHTML should produce valid HTML with example links.',
584
+ 'generateIndexHTML should produce valid HTML with example links.',
565
585
  function()
566
586
  {
567
- let tmpExamplesServe = require('../source/commands/html_example_serving/Quackage-Command-ExamplesServe.js');
568
- let tmpGenerate = tmpExamplesServe.prototype.generateIndexHTML;
587
+ let tmpService = libQuackage.QuackageExampleService;
569
588
 
570
589
  let tmpExamples = [
571
590
  { Name: 'simple_form', DisplayName: 'Simple Form', RelativePath: 'simple_form/dist/index.html', Type: 'example' },
572
591
  { Name: 'debug', DisplayName: 'Debug', RelativePath: 'debug/index.html', Type: 'debug' }
573
592
  ];
574
593
 
575
- let tmpHTML = tmpGenerate('test-project', tmpExamples, 9123);
594
+ let tmpHTML = tmpService.generateIndexHTML('test-project', tmpExamples, 9123);
576
595
 
577
596
  Expect(tmpHTML).to.be.a('string');
578
597
  Expect(tmpHTML).to.include('<!DOCTYPE html>');
@@ -590,11 +609,10 @@ suite
590
609
 
591
610
  test
592
611
  (
593
- 'ExamplesBuild.gatherExampleFolders should find folders with package.json in a temp fixture.',
612
+ 'gatherExampleFolders should find folders with package.json in a temp fixture.',
594
613
  function()
595
614
  {
596
- let tmpExamplesBuild = require('../source/commands/html_example_serving/Quackage-Command-ExamplesBuild.js');
597
- let tmpGather = tmpExamplesBuild.prototype.gatherExampleFolders;
615
+ let tmpService = libQuackage.QuackageExampleService;
598
616
 
599
617
  // Create a temporary fixture
600
618
  let tmpFixtureBase = libPath.join(__dirname, 'tmp_fixture_examples');
@@ -609,7 +627,7 @@ suite
609
627
  libFS.writeFileSync(libPath.join(tmpFixtureAppA, 'package.json'), '{"name":"app_a"}');
610
628
  libFS.writeFileSync(libPath.join(tmpFixtureAppB, 'package.json'), '{"name":"app_b"}');
611
629
 
612
- let tmpResult = tmpGather.call({}, tmpFixtureBase);
630
+ let tmpResult = tmpService.gatherExampleFolders(tmpFixtureBase);
613
631
  Expect(tmpResult).to.be.an('array');
614
632
  Expect(tmpResult).to.have.length(2);
615
633
  Expect(tmpResult.map((pR) => pR.Name)).to.include('app_a');
@@ -627,12 +645,10 @@ suite
627
645
 
628
646
  test
629
647
  (
630
- 'ExamplesServe.gatherServableExamples should find examples with dist/index.html in a temp fixture.',
648
+ 'gatherServableExamples should find examples with dist/index.html in a temp fixture.',
631
649
  function()
632
650
  {
633
- let tmpExamplesServe = require('../source/commands/html_example_serving/Quackage-Command-ExamplesServe.js');
634
- let tmpGather = tmpExamplesServe.prototype.gatherServableExamples;
635
- let tmpFormat = tmpExamplesServe.prototype.formatDisplayName;
651
+ let tmpService = libQuackage.QuackageExampleService;
636
652
 
637
653
  // Create a temporary fixture
638
654
  let tmpFixtureBase = libPath.join(__dirname, 'tmp_fixture_serve');
@@ -647,7 +663,7 @@ suite
647
663
  libFS.writeFileSync(libPath.join(tmpFixtureDist, 'index.html'), '<html></html>');
648
664
  libFS.writeFileSync(libPath.join(tmpFixtureDebug, 'index.html'), '<html></html>');
649
665
 
650
- let tmpResult = tmpGather.call({ formatDisplayName: tmpFormat }, tmpFixtureBase);
666
+ let tmpResult = tmpService.gatherServableExamples(tmpFixtureBase);
651
667
  Expect(tmpResult).to.be.an('array');
652
668
  Expect(tmpResult).to.have.length(2);
653
669