quackage 1.0.50 → 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.
@@ -0,0 +1,50 @@
1
+ # Contributing to Retold
2
+
3
+ We welcome contributions to Retold and its modules. This guide covers the expectations and process for contributing.
4
+
5
+ ## Code of Conduct
6
+
7
+ The Retold community values **empathy**, **equity**, **kindness**, and **thoughtfulness**. We expect all participants to treat each other with respect, assume good intent, and engage constructively. These values apply to all interactions: pull requests, issues, discussions, and code review.
8
+
9
+ ## How to Contribute
10
+
11
+ ### Pull Requests
12
+
13
+ Pull requests are the preferred method for contributing changes. To submit one:
14
+
15
+ 1. Fork the module repository you want to change
16
+ 2. Create a branch for your work
17
+ 3. Make your changes, following the code style of the module you are editing
18
+ 4. Ensure your changes have test coverage (see below)
19
+ 5. Open a pull request against the module's main branch
20
+
21
+ **Submitting a pull request does not guarantee it will be accepted.** Maintainers review contributions for fit, quality, and alignment with the project's direction. A PR may be declined, or you may be asked to revise it. This is normal and not a reflection on the quality of your effort.
22
+
23
+ ### Reporting Issues
24
+
25
+ If you find a bug or have a feature suggestion, open an issue on the relevant module's repository. Include enough detail to reproduce the problem or understand the proposal.
26
+
27
+ ## Test Coverage
28
+
29
+ Every commit must include test coverage for the changes it introduces. Retold modules use Mocha in TDD style. Before submitting:
30
+
31
+ - **Write tests** for any new functionality or bug fixes
32
+ - **Run the existing test suite** with `npm test` and confirm all tests pass
33
+ - **Check coverage** with `npm run coverage` if the module supports it
34
+
35
+ Pull requests that break existing tests or lack coverage for new code will not be merged.
36
+
37
+ ## Code Style
38
+
39
+ Follow the conventions of the module you are working in. The general Retold style is:
40
+
41
+ - **Tabs** for indentation, never spaces
42
+ - **Plain JavaScript** only (no TypeScript)
43
+ - **Allman-style braces** (opening brace on its own line)
44
+ - **Variable naming:** `pVariable` for parameters, `tmpVariable` for temporaries, `libSomething` for imports
45
+
46
+ When in doubt, match what the surrounding code does.
47
+
48
+ ## Repository Structure
49
+
50
+ Each module is its own git repository. The [retold](https://github.com/stevenvelozo/retold) repository tracks module organization but does not contain module source code. Direct your pull request to the specific module repository where your change belongs.
package/README.md CHANGED
@@ -49,4 +49,16 @@ npx quack test
49
49
 
50
50
  ```shell
51
51
  npx quack enhance-my-package
52
- ```
52
+ ```
53
+
54
+ ## Related Packages
55
+
56
+ - [indoctrinate](https://github.com/stevenvelozo/indoctrinate) - Documentation scaffolding
57
+
58
+ ## License
59
+
60
+ MIT
61
+
62
+ ## Contributing
63
+
64
+ Pull requests are welcome. For details on our code of conduct, contribution process, and testing requirements, see the [Retold Contributing Guide](https://github.com/stevenvelozo/retold/blob/main/docs/contributing.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quackage",
3
- "version": "1.0.50",
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;