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 +1 -1
- package/source/Default-Quackage-Configuration.json +2 -0
- package/source/Quackage-CLIProgram.js +2 -0
- package/source/commands/html_example_serving/Quackage-Command-Examples.js +3 -29
- package/source/commands/html_example_serving/Quackage-Command-ExamplesBuild.js +1 -177
- package/source/commands/html_example_serving/Quackage-Command-ExamplesServe.js +1 -298
- package/source/services/Quackage-ExampleService.js +487 -0
- package/test/Quackage_tests.js +72 -56
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 — 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 — 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;
|
package/test/Quackage_tests.js
CHANGED
|
@@ -435,36 +435,29 @@ suite
|
|
|
435
435
|
{
|
|
436
436
|
test
|
|
437
437
|
(
|
|
438
|
-
'ExamplesBuild command class should require and have
|
|
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
|
|
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
|
|
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
|
-
'
|
|
478
|
+
'QuackageExampleService should be instantiated on the pict instance.',
|
|
479
479
|
function()
|
|
480
480
|
{
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
'
|
|
517
|
+
'gatherServableExamples should return empty array for nonexistent path.',
|
|
492
518
|
function()
|
|
493
519
|
{
|
|
494
|
-
let
|
|
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
|
-
'
|
|
528
|
+
'hashProjectNameToPort should return a port in the expected range.',
|
|
506
529
|
function()
|
|
507
530
|
{
|
|
508
|
-
let
|
|
509
|
-
let tmpHash = tmpExamplesServe.prototype.hashProjectNameToPort;
|
|
531
|
+
let tmpService = libQuackage.QuackageExampleService;
|
|
510
532
|
|
|
511
|
-
let tmpPort1 =
|
|
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 =
|
|
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 =
|
|
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
|
-
'
|
|
552
|
+
'formatDisplayName should convert folder names to title case.',
|
|
531
553
|
function()
|
|
532
554
|
{
|
|
533
|
-
let
|
|
534
|
-
let tmpFormat = tmpExamplesServe.prototype.formatDisplayName;
|
|
555
|
+
let tmpService = libQuackage.QuackageExampleService;
|
|
535
556
|
|
|
536
|
-
Expect(
|
|
537
|
-
Expect(
|
|
538
|
-
Expect(
|
|
539
|
-
Expect(
|
|
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
|
-
'
|
|
566
|
+
'getMimeType should return correct MIME types.',
|
|
546
567
|
function()
|
|
547
568
|
{
|
|
548
|
-
let
|
|
549
|
-
let tmpMime = tmpExamplesServe.prototype.getMimeType;
|
|
569
|
+
let tmpService = libQuackage.QuackageExampleService;
|
|
550
570
|
|
|
551
|
-
Expect(
|
|
552
|
-
Expect(
|
|
553
|
-
Expect(
|
|
554
|
-
Expect(
|
|
555
|
-
Expect(
|
|
556
|
-
Expect(
|
|
557
|
-
Expect(
|
|
558
|
-
Expect(
|
|
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
|
-
'
|
|
584
|
+
'generateIndexHTML should produce valid HTML with example links.',
|
|
565
585
|
function()
|
|
566
586
|
{
|
|
567
|
-
let
|
|
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 =
|
|
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
|
-
'
|
|
612
|
+
'gatherExampleFolders should find folders with package.json in a temp fixture.',
|
|
594
613
|
function()
|
|
595
614
|
{
|
|
596
|
-
let
|
|
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 =
|
|
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
|
-
'
|
|
648
|
+
'gatherServableExamples should find examples with dist/index.html in a temp fixture.',
|
|
631
649
|
function()
|
|
632
650
|
{
|
|
633
|
-
let
|
|
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 =
|
|
666
|
+
let tmpResult = tmpService.gatherServableExamples(tmpFixtureBase);
|
|
651
667
|
Expect(tmpResult).to.be.an('array');
|
|
652
668
|
Expect(tmpResult).to.have.length(2);
|
|
653
669
|
|