[Alt] Add new methods; [Server] Slight improvements to example; [Build] Add full HTML build

This commit is contained in:
octospacc 2025-03-09 00:54:55 +01:00
parent 5338c8aaf6
commit fedb597dda
16 changed files with 1310 additions and 84 deletions

18
BuildExamples.sh Normal file
View File

@ -0,0 +1,18 @@
#!/bin/sh
. ./BuildLib.sh
buildHtml(){
useBuilder "Build.BuildHtmlFile('$1', { outputFile: '$2' })"
}
for example in Server
do
example="Example.${example}"
file="./${example}/index.js"
node "${file}" writeStaticHtml 0 "./${example}/index.html"
node "${file}" writeStaticHtml 1 "./Build/${example}.html"
done
for example in Build
do buildHtml "./SpaccDotWeb.${example}/Example.html" "SpaccDotWeb.${example}.Example.html"
done

19
BuildLib.sh Executable file → Normal file
View File

@ -1,4 +1,21 @@
#!/bin/sh
useBuilder(){
node ./SpaccDotWeb.Build/node.js "$1"
}
buildScript(){
useBuilder "Build.BuildScriptFile('$1')"
}
for file in ./SpaccDotWeb.js ./SpaccDotWeb.*.js
do node ./SpaccDotWeb.Build.js "BuildScriptFile('${file}')"
do buildScript "${file}"
done
for lib in Build
do
lib="SpaccDotWeb.${lib}"
output="./Build/${lib}.bundle.min.js"
npx esbuild "./${lib}/browser.js" --bundle --minify --outfile="${output}"
#buildScript "${output}"
done

View File

@ -21,3 +21,17 @@ div#transition {
cursor: progress;
display: none;
}
.particles {
position: absolute;
overflow: hidden;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
z-index: -1;
}
.particles > * {
position: absolute;
}

5
Example.Server/index.js Executable file → Normal file
View File

@ -14,7 +14,10 @@ const server = SpaccDotWebServer.setup({
});
if (SpaccDotWebServer.envIsNode && ['dump', 'html', 'writeStaticHtml'].includes(process.argv[2])) {
const fileName = server.writeStaticHtml(Number(process.argv[3] || 0));
const fileName = server.writeStaticHtml({
selfContained: Number(process.argv[3] || 0),
htmlFilePath: process.argv[4],
});
console.log(`Dumped Static HTML to '${fileName}'!`);
} else {
const serverData = server.initServer({

View File

@ -1,10 +1,11 @@
window.addEventListener('load', function(){
var c = Object.assign(document.createElement('div'), { className: "particles" });
document.body.appendChild(c);
for (var i=0; i<(window.innerWidth * window.innerHeight / 6000); i++) (function(){
var v = (Math.random() * window.innerHeight), h = (100 * Math.random());
var n = document.createElement('span');
n.textContent = '✨️';
n.style.position = 'absolute';
document.body.appendChild(n);
c.appendChild(n);
var e = setInterval(function(){
var r = Math.random();
n.style.top = (v += 1).toString() + 'px';

View File

@ -29,7 +29,6 @@
document.body.appendChild(scriptElem);
}));
};
// .RequireScripts = (...) => {}
SpaccDotWeb.RequireScript = SpaccDotWeb.requireScript;
SpaccDotWeb.showModal = async (params) => {
@ -76,7 +75,7 @@
buttonCancel = modal.querySelector('button[name="cancel"]');
buttonCancel.onclick = (event) => {
if (params.actionCancel) {
output = actionCancel(event, buttonCancel);
output = params.actionCancel(event, buttonCancel);
}
modal.close();
return output;
@ -93,5 +92,40 @@
SpaccDotWeb.sleep = (ms) => (new Promise((resolve) => setTimeout(resolve, ms)));
SpaccDotWeb.Sleep = SpaccDotWeb.sleep;
SpaccDotWeb.$ = (query) => {
query = query.trim();
return (query.startsWith('::')
? arrayFrom(document.querySelectorAll(domSpecialQuery(query.slice(2).trim())))
: document.querySelector(domSpecialQuery(query))
);
}
function domSpecialQuery (query) {
const chars = [];
let buffer = [];
let brackets = 0;
for (const char of query) {
if (brackets === 0) {
if (buffer.length > 0) {
buffer = buffer.join('');
if (!buffer.includes('=')) {
buffer = `name=${buffer}`;
}
chars.push(buffer);
buffer = [];
}
chars.push(char);
} else {
buffer.push(char);
}
if (char === '[') {
brackets++;
} else if (char === ']' && brackets > 0) {
brackets--;
}
}
return chars.join('');
}
window.SpaccDotWeb ||= SpaccDotWeb;
})(document.currentScript);

0
SpaccDotWeb.Android/gradlew vendored Executable file → Normal file
View File

View File

@ -1,52 +0,0 @@
#!/usr/bin/env node
const Lib = {
fs: require('fs'),
mime: require('mime-types'),
crypto: require('crypto'),
babel: require('@babel/core'),
uglify: require('uglify-js'),
};
const BuildScriptFile = (scriptFile, options) => {
options = {
forceResult: false,
checkHash: true,
...options };
Lib.fs.mkdirSync(`${__dirname}/Build`, { recursive: true });
const __scriptname = scriptFile.split('/').slice(-1)[0].split('.').slice(0, -1).join('.');
const scriptScript = Lib.fs.readFileSync(scriptFile, 'utf8');
const compiledPath = `${__dirname}/Build/${__scriptname}.js`;
const minifiedPath = `${__dirname}/Build/${__scriptname}.min.js`;
const hashPath = `${__dirname}/Build/${__scriptname}.js.hash`;
const hashOld = (Lib.fs.existsSync(hashPath) && Lib.fs.readFileSync(hashPath, 'utf8'));
const hashNew = Lib.crypto.createHash('sha256').update(scriptScript).digest('hex');
if (!options.checkHash || !Lib.fs.existsSync(compiledPath) || !Lib.fs.existsSync(minifiedPath) || !(hashOld === hashNew)) {
const compiledScript = Lib.babel.transformSync(scriptScript,
JSON.parse(Lib.fs.readFileSync(`${__dirname}/babel.config.json`, 'utf8'))).code;
const minifiedScript = Lib.uglify.minify(compiledScript).code;
Lib.fs.writeFileSync(compiledPath, compiledScript);
Lib.fs.writeFileSync(minifiedPath, minifiedScript);
Lib.fs.writeFileSync(hashPath, hashNew);
return { compiled: compiledScript, minified: minifiedScript };
}
return { notice: `Target "${scriptFile}" is up-to-date.`, ...(options.forceResult && {
compiled: Lib.fs.readFileSync(compiledPath, 'utf8'),
minified: Lib.fs.readFileSync(minifiedPath, 'utf8'),
}) };
};
//const BuildHtmlFile = (htmlFile) => {
//
//}
const EncodeStaticFiles = (files, /* encoding='base64' */) => {
const data = {};
files.forEach(file => (data[file] = `data:${Lib.mime.lookup(file)};base64,${Lib.fs.readFileSync(file).toString('base64')}`));
return data;
};
module.exports = { BuildScriptFile, EncodeStaticFiles };
if (require.main === module) {
console.log(eval(process.argv.slice(-1)[0]));
}

View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<meta charset="utf8" />
<style> textarea { width: 100% !important; height: 8rem; min-height: 2em; box-sizing: border-box; } </style>
<script src="../Build/SpaccDotWeb.Build.bundle.min.js" data-SpaccDotWeb='{"compile":false,"minify":false}'></script>
<fieldset class="script">
<legend>Script</legend>
<textarea id="script" readonly="true">
const alerter = (text) => alert(`THIS IS AN ALERT ❗️\n\n${text}`);
alerter("It's a nice day today, innit?");
</textarea>
<span>ES6</span>
</fieldset>
<fieldset class="script">
<legend>Compiled</legend>
<textarea id="compiled" readonly="true"></textarea>
<span>ES5</span>
</fieldset>
<fieldset class="script">
<legend>Compiled+Minified</legend>
<textarea id="minified" readonly="true"></textarea>
<span>ES5</span>
</fieldset>
<fieldset class="html">
<legend>HTML</legend>
<textarea id="html" readonly="true"></textarea>
<span>ES5, Compiled+Minified</span>
</fieldset>
<script>
const BuildScript = window.SpaccDotWeb.Build.BuildScript;
const $ = (id) => document.getElementById(id);
const scriptText = $('script').textContent = $('script').textContent.trimEnd();
// Actually build the script
const builtScript = BuildScript(scriptText);
// Show the build result
$('compiled').textContent = builtScript.compiled;
$('minified').textContent = builtScript.minified;
// Execute the compiled result
//setTimeout('eval(builtScript.compiled);', 100);
setTimeout(function(){
Array.from(document.querySelectorAll('script')).filter(el => !el.dataset.spaccdotweb).forEach(scriptElement => {
scriptElement.textContent = BuildScript(scriptElement.textContent).minified;
});
$('html').textContent = `<!DOCTYPE html>\n${document.documentElement.outerHTML}`;
}, 100);
document.querySelectorAll('fieldset.script').forEach(fieldset => {
const script = fieldset.querySelector('textarea').textContent;
fieldset.querySelector('span').innerHTML += `, ${script.length}b`;
fieldset.appendChild(Object.assign(document.createElement('button'), {
innerHTML: "Execute",
onclick: () => eval(script),
}));
});
</script>

View File

@ -0,0 +1,7 @@
window.global ||= window.globalThis;
window.SpaccDotWeb ||= {};
window.SpaccDotWeb.Build = require('./lib.js')({
mime: require('mime-types-browser/dist/mime-types-browser'),
babel: require('@babel/standalone'),
uglify: require('uglifyjs-browser'),
});

102
SpaccDotWeb.Build/lib.js Normal file
View File

@ -0,0 +1,102 @@
module.exports = (Lib) => {
const envIsBrowser = (typeof window !== 'undefined' && typeof window.document !== 'undefined');
let babelPreset, makeHtmlDom;
if (envIsBrowser) {
Lib.babel.registerPreset(null, {
presets: [
[Lib.babel.availablePresets['preset-env']],
],
});
babelPreset = 'env';
makeHtmlDom = (html) => (new DOMParser()).parseFromString(html, 'text/html');
} else {
babelPreset = '@babel/preset-env';
makeHtmlDom = (html) => (new Lib.jsdom(html)).window.document;
}
const babelConfig = {
"presets": [
[
babelPreset,
{
"targets": {
"chrome": "4",
"edge": "12",
"firefox": "2",
"ie": "6",
"safari": "3.1",
},
},
],
],
};
const findPath = (path, folder) => {
for (const prefix of [folder, __dirname]) {
path = Lib.path.join(prefix, path);
if (Lib.fs.existsSync(path)) {
return path;
}
}
}
const fileToBase64 = (path, content) => `data:${Lib.mime.lookup(path)};base64,${content || Lib.fs.readFileSync(findPath(path)).toString('base64')}`;
const isUrlAbsolute = (url) => (url && ['http:', 'https:', ''].includes(url.split('/')[0]));
const BuildScript = (scriptText, options) => {
options = {
minify: true,
...options };
const compiled = (Lib.babel.transformSync || Lib.babel.transform)(scriptText, babelConfig).code;
const minified = (options.minify && Lib.uglify.minify(compiled).code);
return { compiled, minified };
}
const BuildHtml = async (html, options) => {
options = {
compileScripts: true,
minifyScripts: true,
compileStyles: true,
inputFolder: '.',
...options };
const dom = makeHtmlDom(html);
for (const element of dom.querySelectorAll('script, [src], link[rel=stylesheet][href]')) {
if (isUrlAbsolute(element.src || element.href)) {
return;
}
if (element.tagName === 'SCRIPT') {
const scriptOptions = JSON.parse(element.dataset.spaccdotweb || '{}');
const minifyScripts = (scriptOptions.minify ?? options.minifyScripts);
let scriptText = (element.src
? Lib.fs.readFileSync(findPath(element.src, options.inputFolder), 'utf8')
: element.textContent);
if (scriptOptions.compile ?? options.compileScripts) {
scriptText = BuildScript(scriptText, { minify: minifyScripts })[minifyScripts ? 'minified' : 'compiled'];
}
element.removeAttribute('src');
element.textContent = scriptText;
} else if (element.tagName === 'LINK') {
const stylePath = findPath(element.href, options.inputFolder);
let styleText = Lib.fs.readFileSync(stylePath, 'utf8');
if (options.compileStyles) {
styleText = (await Lib.postcss([
Lib.postcssImport(),
Lib.postcssUrl({ url: 'inline' }),
]).process(styleText, { from: stylePath })).css;
}
element.parentElement.insertBefore(Object.assign(dom.createElement('style'), { textContent: styleText }), element);
element.remove();
} else {
element.src = fileToBase64(element.src);
}
}
return `<!DOCTYPE html>\n${dom.documentElement.outerHTML}`;
}
return { BuildScript, BuildHtml, fileToBase64 };
}

64
SpaccDotWeb.Build/node.js Normal file
View File

@ -0,0 +1,64 @@
#!/usr/bin/env node
const Lib = {
fs: require('fs'),
path: require('path'),
mime: require('mime-types'),
crypto: require('crypto'),
babel: require('@babel/core'),
uglify: require('uglify-js'),
postcss: require('postcss'),
postcssImport: require('postcss-import'),
postcssUrl: require('postcss-url'),
jsdom: require('jsdom').JSDOM,
};
let Build = require('./lib.js')(Lib);
const BuildScriptFile = (scriptFile, options) => {
options = {
forceResult: false,
checkHash: true,
outputFolder: './Build',
...options };
Lib.fs.mkdirSync(options.outputFolder, { recursive: true });
const __scriptname = scriptFile.split('/').slice(-1)[0].split('.').slice(0, -1).join('.');
const scriptText = Lib.fs.readFileSync(scriptFile, 'utf8');
const compiledPath = `${options.outputFolder}/${__scriptname}.js`;
const minifiedPath = `${options.outputFolder}/${__scriptname}.min.js`;
const hashPath = `${options.outputFolder}/${__scriptname}.js.hash`;
const hashOld = (Lib.fs.existsSync(hashPath) && Lib.fs.readFileSync(hashPath, 'utf8'));
const hashNew = Lib.crypto.createHash('sha256').update(scriptText).digest('hex');
if (!options.checkHash || !Lib.fs.existsSync(compiledPath) || !Lib.fs.existsSync(minifiedPath) || !(hashOld === hashNew)) {
const builtScript = Build.BuildScript(scriptText, /* JSON.parse(Lib.fs.readFileSync(`${__dirname}/babel.config.json`, 'utf8')) */);
Lib.fs.writeFileSync(compiledPath, builtScript.compiled);
Lib.fs.writeFileSync(minifiedPath, builtScript.minified);
Lib.fs.writeFileSync(hashPath, hashNew);
return builtScript;
}
return { notice: `Target "${scriptFile}" is up-to-date.`, ...(options.forceResult && {
compiled: Lib.fs.readFileSync(compiledPath, 'utf8'),
minified: Lib.fs.readFileSync(minifiedPath, 'utf8'),
}) };
};
const BuildHtmlFile = (htmlFile, options) => {
options = {
outputFolder: './Build',
outputFile: htmlFile,
inputFolder: Lib.path.dirname(htmlFile),
...options };
const outputPath = `${options.outputFolder}/${options.outputFile}`;
Build.BuildHtml(Lib.fs.readFileSync(htmlFile, 'utf8'), options).then(html => Lib.fs.writeFileSync(outputPath, html));
return outputPath;
};
const EncodeStaticFiles = (files, /* encoding='base64' */) => {
const data = {};
files.forEach(file => (data[file] = Build.fileToBase64(file)));
return data;
};
module.exports = Build = { ...Build, BuildScriptFile, BuildHtmlFile, EncodeStaticFiles };
if (require.main === module) {
console.log(eval(process.argv.slice(-1)[0]));
}

View File

@ -82,16 +82,21 @@ const initServer = (serverOptions) => {
const navigateClientPage = (forceUrl) => ((!forceUrl || (window.location.hash === forceUrl))
&& handleRequest({ url: window.location.hash.slice(1), method: 'GET' }));
const writeStaticHtml = (selfContained=false) => {
const writeStaticHtml = (options={}) => {
const appFilePath = process.mainModule.filename;
const htmlFilePath = (appFilePath.split('.').slice(0, -1).join('.') + '.html');
options.selfContained ??= false;
options.htmlFilePath ??= (appFilePath.split('.').slice(0, -1).join('.') + '.html');
// path.relative seems to always append an extra '../', so we must slice it
const libraryPath = path.relative(appFilePath, __filename).split(path.sep).slice(1).join(path.sep);
const libraryFolder = libraryPath.split(path.sep).slice(0, -1).join(path.sep);
let libraryPath = path.relative(appFilePath, __filename).split(path.sep).slice(1).join(path.sep);
let libraryFolder = libraryPath.split(path.sep).slice(0, -1).join(path.sep);
if (path.sep === '\\') {
libraryPath = libraryPath.replaceAll('\\', '/');
libraryFolder = libraryFolder.replaceAll('\\', '/');
}
const context = { envIsNode: false, envIsBrowser: true };
fs.writeFileSync(htmlFilePath, allOpts.global.htmlPager(`
${makeHtmlScriptFragment(libraryPath, selfContained)}
${makeHtmlScriptFragment(((libraryFolder && (libraryFolder + '/')) + 'SpaccDotWeb.Alt.js'), selfContained)}
fs.writeFileSync(options.htmlFilePath, allOpts.global.htmlPager(`
${makeHtmlScriptFragment(libraryPath, options.selfContained)}
${makeHtmlScriptFragment(((libraryFolder && (libraryFolder + '/')) + 'SpaccDotWeb.Alt.js'), options.selfContained)}
<${'script'}>
window.require = () => {
window.require = async (src, type) => {
@ -99,15 +104,15 @@ const writeStaticHtml = (selfContained=false) => {
};
return window.SpaccDotWebServer;
};
window.SpaccDotWebServer.staticFilesData = { ${selfContained ? allOpts.global.staticFiles.map((file) => {
window.SpaccDotWebServer.staticFilesData = { ${options.selfContained ? allOpts.global.staticFiles.map((file) => {
// TODO check if these paths are correct or must still be fixed
const filePath = (appFilePath.split(path.sep).slice(0, -1).join(path.sep) + path.sep + file);
return `"${file}":"data:${mime.lookup(filePath)};base64,${fs.readFileSync(filePath).toString('base64')}"`;
}).join() : ''} };
</${'script'}>
${makeHtmlScriptFragment(path.basename(appFilePath), selfContained)}
`, null, { selfContained, context }, context));
return htmlFilePath;
${makeHtmlScriptFragment(path.basename(appFilePath), options.selfContained)}
`, null, { selfContained: options.selfContained, context }, context));
return options.htmlFilePath;
};
const makeHtmlStyleFragment = (path, getContent) => {
@ -115,15 +120,18 @@ const makeHtmlStyleFragment = (path, getContent) => {
return (data[1] ? `<style>${data[1]}</style>` : `<link rel="stylesheet" href="${data[0]}"/>`);
};
const makeHtmlScriptFragment = (path, getContent) => {
const data = getFilePathContent(path, getContent);
const makeHtmlScriptFragment = (patha, getContent) => {
const data = getFilePathContent(patha, getContent);
return `<${'script'}${data[1] ? `>${data[1]}` : ` src="${data[0]}">`}</${'script'}>`;
};
const getFilePathContent = (path, getContent) => ([
(allOpts.global.staticFiles.includes(path) ? (allOpts.global.staticPrefix + path) : ('./' + path)),
(getContent && fs.existsSync(path) && fs.readFileSync(path)),
]);
const getFilePathContent = (filePath, getContent) => {
const realPath = path.join(path.dirname(process.mainModule.filename), filePath);
return [
(allOpts.global.staticFiles.includes(filePath) ? (allOpts.global.staticPrefix + filePath) : ('./' + filePath)),
(getContent && fs.existsSync(realPath) && fs.readFileSync(realPath)),
];
};
const handleRequest = async (request, response={}) => {
// build request context and handle special tasks

0
SpaccDotWeb.js Executable file → Normal file
View File

954
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,19 +3,27 @@
"version": "indev",
"scripts": {
"build:lib": "sh ./BuildLib.sh",
"build:examples": "sh ./BuildExamples.sh",
"build:clear": "rm -rf ./Build"
},
"devDependencies": {
"@babel/cli": "^7.24.8",
"@babel/core": "^7.23.0",
"@babel/preset-env": "^7.22.20",
"@babel/standalone": "^7.26.9",
"core-js": "^3.35.0",
"dialog-polyfill": "^0.5.6",
"esbuild": "^0.25.0",
"jsdom": "^22.1.0",
"uglify-js": "^3.17.4"
"postcss": "^8.5.3",
"postcss-import": "^16.1.0",
"postcss-url": "^10.1.3",
"uglify-js": "^3.17.4",
"uglifyjs-browser": "^3.0.0"
},
"dependencies": {
"mime-types": "^2.1.35",
"mime-types-browser": "^0.0.3",
"parse-multipart-data": "^1.5.0"
}
}