Fix non-critical errors live mode, add transposing of standalone HTML elements to final DOM, add build-time rendering

This commit is contained in:
OctoSpacc 2023-12-18 22:03:06 +01:00
parent ddad1d4a8a
commit 37edcf8971
5 changed files with 136 additions and 73 deletions

17
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,17 @@
image: alpine:latest
before_script: |
apk update
apk add nodejs npm
pages:
stage: deploy
script: |
npm install
npm run build:lib
node ./SpaccDotWeb.js 'SpaccDotWeb.AppBuildStandalone({ Page: "Example.html" })'
mkdir -p ./public
mv ./Build/* ./public
artifacts:
paths:
- public

33
Example.html Normal file
View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<script src="SpaccDotWeb.js" module="SpaccDotWeb"></script>
<script module="Meta">({
"Name": "The Testing",
"Description": "Useful for doing The Testing",
"Uri": "https://example.com/Testing",
})</script>
<script module="Main">
const Spacc = SpaccDotWeb.AppInit();
Spacc.Select('#App').innerHTML = `
<p>Click to log!</p>
<button>Log</button>
`;
Spacc.Select('#App button').onclick = () => {
Spacc.Select('#App').innerHTML += `<xmp style="white-space: break-spaces;">
${(document.documentElement.outerHTML)}
</xmp>`;
};
</script>
<script module="Testing">
Spacc.Select('#App').Insert(Spacc.Create('p', { innerHTML: `<b>The auto-testing is complete.</b>` }));
</script>
<style>
body {
background: lightgray;
}
</style>

View File

@ -1,27 +0,0 @@
<script src="SpaccDotWeb.js"></script>
<script id="Meta" type="text/json">{
"Name": "The Testing",
"Description": "Useful for doing The Testing",
"Uri": "https://example.com/Testing"
}</script>
<script module="Main">
const Spacc = SpaccDotWeb;
Spacc.AppInit();
Spacc.Select('#App').innerHTML = `
<p>1234</p>
<button>Log</button>
`;
Spacc.Select('#App button').onclick = () => {
Spacc.Select('#App').innerHTML += `<xmp style="text-wrap: unset;">
${JSON.stringify(document.documentElement.outerHTML)}
</xmp>`;
};
</script>
<script module="Testing">
Spacc.Select('#App').Insert(Spacc.Create('p', { innerHTML: `<b>Testing is complete.</b>` }));
</script>

View File

@ -8,14 +8,16 @@ let __scriptname;
const platformIsNode = (typeof module === 'object' && typeof module.exports === 'object'); const platformIsNode = (typeof module === 'object' && typeof module.exports === 'object');
const platformIsBrowser = (typeof window !== 'undefined' && typeof window.document !== 'undefined'); const platformIsBrowser = (typeof window !== 'undefined' && typeof window.document !== 'undefined');
const __filename__ = (typeof __filename !== 'undefined' ? __filename : '');
const JsdomOptions = { /*resources: "usable",*/ runScripts: /*"dangerously"*/"outside-only" };
if (platformIsNode) { if (platformIsNode) {
Lib.fs = require('fs'); Lib.fs = require('fs');
Lib.crypto = require('crypto'); Lib.crypto = require('crypto');
Lib.childProcess = require('child_process'); Lib.childProcess = require('child_process');
Lib.jsdom = require('jsdom'); Lib.jsdom = require('jsdom');
__scriptname = __filename.split('/').slice(-1)[0]; __scriptname = __filename__.split('/').slice(-1)[0];
windowObject = new Lib.jsdom.JSDOM().window; windowObject = new Lib.jsdom.JSDOM('', JsdomOptions).window;
}; };
if (platformIsBrowser) { if (platformIsBrowser) {
@ -31,7 +33,7 @@ const SpaccDotWeb = ((args) => { //////////////////////////////////////////////
let SpaccDotWeb = {}; let SpaccDotWeb = {};
if (platformIsNode) { if (platformIsNode) {
SpaccDotWeb.AppBuildStandalone = (opts) => { SpaccDotWeb.AppBuildStandalone = (opts) => { // TODO: build result of dom after JS, to make base page usable without JS
isBuildingApp = true; isBuildingApp = true;
opts ||= {}; opts ||= {};
opts.Page ||= 'index.html'; opts.Page ||= 'index.html';
@ -40,7 +42,7 @@ if (platformIsNode) {
Lib.fs.mkdirSync(`${__dirname}/Build/App-${opts.Page}`, { recursive: true }); Lib.fs.mkdirSync(`${__dirname}/Build/App-${opts.Page}`, { recursive: true });
let htmlIndex = Lib.fs.readFileSync(opts.Page, 'utf8'); let htmlIndex = Lib.fs.readFileSync(opts.Page, 'utf8');
windowObject = new Lib.jsdom.JSDOM(htmlIndex).window; windowObject = new Lib.jsdom.JSDOM(htmlIndex, JsdomOptions).window;
documentObject = windowObject.document; documentObject = windowObject.document;
DomSetup(opts.Modules); DomSetup(opts.Modules);
@ -56,11 +58,11 @@ if (platformIsNode) {
const minifiedPath = `${__dirname}/Build/SpaccDotWeb.min.js`; const minifiedPath = `${__dirname}/Build/SpaccDotWeb.min.js`;
const hashPath = `${__dirname}/Build/SpaccDotWeb.js.hash`; const hashPath = `${__dirname}/Build/SpaccDotWeb.js.hash`;
const hashOld = (Lib.fs.existsSync(hashPath) && Lib.fs.readFileSync(hashPath, 'utf8')); const hashOld = (Lib.fs.existsSync(hashPath) && Lib.fs.readFileSync(hashPath, 'utf8'));
const hashNew = Lib.crypto.createHash('sha256').update(Lib.fs.readFileSync(__filename, 'utf8')).digest('hex'); const hashNew = Lib.crypto.createHash('sha256').update(Lib.fs.readFileSync(__filename__, 'utf8')).digest('hex');
if (!Lib.fs.existsSync(compiledPath) || !Lib.fs.existsSync(minifiedPath) || !(hashOld === hashNew)) { if (!Lib.fs.existsSync(compiledPath) || !Lib.fs.existsSync(minifiedPath) || !(hashOld === hashNew)) {
uptodate = false; uptodate = false;
Lib.fs.writeFileSync(hashPath, hashNew); Lib.fs.writeFileSync(hashPath, hashNew);
Lib.fs.writeFileSync(compiledPath, Lib.childProcess.execSync(`cat "${__filename}" | npx babel -f "${__scriptname}"`)); Lib.fs.writeFileSync(compiledPath, Lib.childProcess.execSync(`cat "${__filename__}" | npx babel -f "${__scriptname}"`));
Lib.fs.writeFileSync(minifiedPath, Lib.childProcess.execSync(`cat "${compiledPath}" | npx uglifyjs`)); Lib.fs.writeFileSync(minifiedPath, Lib.childProcess.execSync(`cat "${compiledPath}" | npx uglifyjs`));
}; };
uptodate && console.log('Library is up-to-date.'); uptodate && console.log('Library is up-to-date.');
@ -68,9 +70,10 @@ if (platformIsNode) {
}; };
}; };
SpaccDotWeb.AppInit = function AppInit(){ SpaccDotWeb.AppInit = () => {
try { try {
DomSetup(); DomSetup();
return SpaccDotWeb;
} catch(err) { console.log(err) }; } catch(err) { console.log(err) };
}; };
@ -90,46 +93,78 @@ SpaccDotWeb.Select = (query) => {
return elem; return elem;
}; };
const AppMetaGet = () => JSON.parse(SpaccDotWeb.Select('#Meta').innerHTML); SpaccDotWeb.RequireScript = (src) => {
if (platformIsBrowser) {
SpaccDotWeb.Select('body').Insert(SpaccDotWeb.Create('script', { src: src }));
//} else if (platformIsNode) {
// require(src);
}
};
// TODO: make Meta element optional without breaking things
const AppMetaGet = () => {
const elem = SpaccDotWeb.Select('script[module="Meta"]');
if (elem) {
if (['application/json', 'text/json'].includes(elem.getAttribute('type'))) {
return JSON.parse(elem.innerHTML);
} else {
return eval(elem.innerHTML);
};
};
};
const DomMakeBase = (Modules) => { const DomMakeBase = (Modules) => {
const meta = AppMetaGet(); const meta = AppMetaGet();
const htmlFrags = { if (meta) {
Title: (meta.Name ? `<title>${meta.Name}</title><meta property="og:title" content="${meta.Name}"/>` : ''), const htmlFrags = {
Description: (meta.Description ? `<meta name="description" content="${meta.Description}"/><meta property="og:description" content="${meta.Description}"/>` : ''), Title: (meta.Name ? `<title>${meta.Name}</title><meta property="og:title" content="${meta.Name}"/>` : ''),
Uri: (meta.Uri ? `<link rel="canonical" href="${meta.Uri}"/><meta property="og:url" content="${meta.Uri}"/>` : '') Description: (meta.Description ? `<meta name="description" content="${meta.Description}"/><meta property="og:description" content="${meta.Description}"/>` : ''),
}; Uri: (meta.Uri ? `<link rel="canonical" href="${meta.Uri}"/><meta property="og:url" content="${meta.Uri}"/>` : '')
let scripts = '';
if (isBuildingApp) {
scripts += `<scr`+`ipt src="http://cdn.jsdelivr.net/npm/core-js-bundle/minified.min.js"></scr`+`ipt>`;
scripts += `<scr`+`ipt src="https://cdn.jsdelivr.net/npm/core-js-bundle/minified.min.js"></scr`+`ipt>`;
scripts += `<scr`+`ipt>${SpaccDotWeb.LibBuild().minified}</scr`+`ipt>`;
for (const elem of documentObject.querySelectorAll('script[module]')) {
if (!Modules || (Modules && Modules.includes(elem.getAttribute('module')))) {
if (elem.getAttribute('src')) {
scripts += `<scr`+`ipt src="${elem.getAttribute('src')}"></scr`+`ipt>`
} else {
const tmpHash = Lib.crypto.createHash('sha256').update(elem.innerHTML).digest('hex');
const tmpPath = `${__dirname}/Build/Assets.tmp/${tmpHash}.js`;
Lib.fs.writeFileSync(tmpPath, elem.innerHTML);
scripts += `<scr`+`ipt>${Lib.childProcess.execSync(`cat "${tmpPath}" | npx babel -f "${tmpHash}.js" | npx uglifyjs`)}</scr`+`ipt>`;
};
};
}; };
};
return { let [scriptsCode, elementsHtml, scriptsHtml] = ['', '', ''];
head: `
<meta charset="utf-8"/> if (isBuildingApp) {
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> scriptsHtml += `<scr`+`ipt src="http://cdn.jsdelivr.net/npm/core-js-bundle/minified.min.js"></scr`+`ipt>`;
${htmlFrags.Title} scriptsHtml += `<scr`+`ipt src="https://cdn.jsdelivr.net/npm/core-js-bundle/minified.min.js"></scr`+`ipt>`;
${htmlFrags.Description} scriptsHtml += `<scr`+`ipt>${SpaccDotWeb.LibBuild().minified}</scr`+`ipt>`;
${htmlFrags.Uri} for (const elem of documentObject.querySelectorAll('script[module]')) {
`, //if (elem.module === 'Meta' && !['application/json', 'text/json'].includes(elem.type)) {
body: `<div id="App"></div>${scripts}`, // elem.innerHTML = `(${elem.innerHTML})`;
//};
if (elem.getAttribute('module') !== 'SpaccDotWeb' && (!Modules || (Modules && Modules.includes(elem.getAttribute('module'))))) {
if (elem.getAttribute('src')) {
scriptsHtml += `<scr`+`ipt src="${elem.getAttribute('src')}"></scr`+`ipt>`;
// TODO somehow include this in prerendered DOM?
} else {
const tmpHash = Lib.crypto.createHash('sha256').update(elem.innerHTML).digest('hex');
const tmpPath = `${__dirname}/Build/Assets.tmp/${tmpHash}.js`;
Lib.fs.writeFileSync(tmpPath, elem.innerHTML);
const scriptCode = Lib.childProcess.execSync(`cat "${tmpPath}" | npx babel -f "${tmpHash}.js" | npx uglifyjs`);
scriptsCode += scriptCode;
scriptsHtml += `<scr`+`ipt>${Lib.childProcess.execSync(`cat "${tmpPath}" | npx babel -f "${tmpHash}.js" | npx uglifyjs`)}</scr`+`ipt>`;
};
};
elem.remove();
};
// select and include all remaining actual elements, except parent meta-elements
for (const elem of documentObject.querySelectorAll('* > * > *')) {
elementsHtml += elem.outerHTML;
}
};
return {
head: `
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
${htmlFrags.Title}
${htmlFrags.Description}
${htmlFrags.Uri}
`,
body: `<div id="App"></div>${elementsHtml}${scriptsHtml}`,
scriptsCode: scriptsCode,
};
}; };
}; };
@ -139,9 +174,14 @@ const DomSetup = (Modules) => {
? documentObject.replaceChild(doctypeNew, documentObject.doctype) ? documentObject.replaceChild(doctypeNew, documentObject.doctype)
: documentObject.insertBefore(doctypeNew, documentObject.childNodes[0]); : documentObject.insertBefore(doctypeNew, documentObject.childNodes[0]);
const domBase = DomMakeBase(Modules); const domBase = DomMakeBase(Modules);
documentObject.write(domBase.head + domBase.body); if (domBase) {
documentObject.head.innerHTML = domBase.head; documentObject.write(domBase.head + domBase.body);
documentObject.body.innerHTML = domBase.body; documentObject.head.innerHTML = domBase.head;
documentObject.body.innerHTML = domBase.body;
if (isBuildingApp) {
windowObject.eval(Lib.fs.readFileSync(__filename__, 'utf-8') + domBase.scriptsCode);
};
};
}; };
return SpaccDotWeb; return SpaccDotWeb;
@ -151,7 +191,7 @@ return SpaccDotWeb;
platformIsBrowser && (window.SpaccDotWeb = SpaccDotWeb); platformIsBrowser && (window.SpaccDotWeb = SpaccDotWeb);
platformIsNode && (console.log(eval(process.argv.slice(-1)[0]))); platformIsNode && process.argv.length >= 2 && console.log(eval(process.argv.slice(-1)[0]));
return SpaccDotWeb; return SpaccDotWeb;

View File

@ -2,7 +2,7 @@
"name": "SpaccDotWeb", "name": "SpaccDotWeb",
"version": "indev", "version": "indev",
"scripts": { "scripts": {
"build:lib": "node ./SpaccDotWeb.js 'SpaccDotWeb.LibBuild()'", "build:lib": "sh -c \"node ./SpaccDotWeb.js 'SpaccDotWeb.LibBuild()'\"",
"build:clear": "rm -rf ./Build" "build:clear": "rm -rf ./Build"
}, },
"devDependencies": { "devDependencies": {