quackage 1.0.51 → 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 +1 -1
- package/source/Default-Quackage-Configuration.json +2 -0
- package/source/Quackage-CLIProgram.js +8 -1
- package/source/commands/html_example_serving/Quackage-Command-Examples.js +41 -0
- package/source/commands/html_example_serving/Quackage-Command-ExamplesBuild.js +25 -0
- package/source/commands/html_example_serving/Quackage-Command-ExamplesServe.js +28 -0
- package/source/services/Quackage-ExampleService.js +487 -0
- package/test/Quackage_tests.js +826 -1
package/package.json
CHANGED
|
@@ -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
|
|
@@ -57,6 +62,8 @@ _Pict.instantiateServiceProvider('FilePersistence');
|
|
|
57
62
|
_Pict.instantiateServiceProvider('DataGeneration');
|
|
58
63
|
// Add the Quackage Process Management service
|
|
59
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'));
|
|
60
67
|
|
|
61
68
|
// Grab the current working directory for the quackage
|
|
62
69
|
_Pict.AppData.CWD = _Pict.QuackageProcess.cwd();
|
|
@@ -0,0 +1,41 @@
|
|
|
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 ? parseInt(this.CommandOptions.port, 10) : 0;
|
|
24
|
+
|
|
25
|
+
this.log.info(`Building and serving examples from [${tmpExamplesFolder}] ...`);
|
|
26
|
+
|
|
27
|
+
this.fable.QuackageExampleService.buildExamples(tmpExamplesFolder,
|
|
28
|
+
(pBuildError) =>
|
|
29
|
+
{
|
|
30
|
+
if (pBuildError)
|
|
31
|
+
{
|
|
32
|
+
this.log.warn(`Some examples had build errors, but continuing to serve what we have...`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// examples-serve doesn't call back (long-lived server), so neither do we
|
|
36
|
+
this.fable.QuackageExampleService.serveExamples(tmpExamplesFolder, tmpPort, fCallback);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = QuackageCommandExamples;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
|
|
2
|
+
const libPath = require('path');
|
|
3
|
+
|
|
4
|
+
class QuackageCommandExamplesBuild extends libCommandLineCommand
|
|
5
|
+
{
|
|
6
|
+
constructor(pFable, pManifest, pServiceHash)
|
|
7
|
+
{
|
|
8
|
+
super(pFable, pManifest, pServiceHash);
|
|
9
|
+
|
|
10
|
+
this.options.CommandKeyword = 'examples-build';
|
|
11
|
+
this.options.Description = 'Build all example applications in the example_applications and debug folders.';
|
|
12
|
+
|
|
13
|
+
this.options.CommandArguments.push({ Name: '[examples_folder]', Description: 'The examples folder (defaults to ./example_applications).' });
|
|
14
|
+
|
|
15
|
+
this.addCommand();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
onRunAsync(fCallback)
|
|
19
|
+
{
|
|
20
|
+
let tmpExamplesFolder = libPath.resolve(this.ArgumentString || './example_applications');
|
|
21
|
+
this.fable.QuackageExampleService.buildExamples(tmpExamplesFolder, fCallback);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = QuackageCommandExamplesBuild;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
|
|
2
|
+
const libPath = require('path');
|
|
3
|
+
|
|
4
|
+
class QuackageCommandExamplesServe extends libCommandLineCommand
|
|
5
|
+
{
|
|
6
|
+
constructor(pFable, pManifest, pServiceHash)
|
|
7
|
+
{
|
|
8
|
+
super(pFable, pManifest, pServiceHash);
|
|
9
|
+
|
|
10
|
+
this.options.CommandKeyword = 'examples-serve';
|
|
11
|
+
this.options.Description = 'Serve example applications 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 ? parseInt(this.CommandOptions.port, 10) : 0;
|
|
24
|
+
this.fable.QuackageExampleService.serveExamples(tmpExamplesFolder, tmpPort, fCallback);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = QuackageCommandExamplesServe;
|
|
@@ -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 — 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;
|