quackage 1.0.51 → 1.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/source/Quackage-CLIProgram.js +6 -1
- package/source/commands/html_example_serving/Quackage-Command-Examples.js +67 -0
- package/source/commands/html_example_serving/Quackage-Command-ExamplesBuild.js +201 -0
- package/source/commands/html_example_serving/Quackage-Command-ExamplesServe.js +325 -0
- package/test/Quackage_tests.js +810 -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
|
|
@@ -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 — 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;
|