frontend: Rework asset generation
This commit reworks frontend asset generation to not need manually written html files, but instead generate them automatically.
This commit is contained in:
parent
ebdbe9fabd
commit
ccab2883a6
42 changed files with 408 additions and 177 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,4 +9,5 @@ go.work.sum
|
|||
|
||||
node_modules
|
||||
.parcel-cache
|
||||
.entry-cache
|
||||
yarn-error.log
|
||||
9
Makefile
9
Makefile
|
|
@ -1,14 +1,11 @@
|
|||
.PHONY: clean all deps
|
||||
|
||||
all: wdcli frontend
|
||||
all: wdcli
|
||||
|
||||
wdcli: internal/component/static/dist
|
||||
wdcli:
|
||||
go generate ./internal/component/static/
|
||||
go build -o ./wdcli ./cmd/wdcli
|
||||
|
||||
internal/component/static/dist: internal/component/static/src
|
||||
rm -rf internal/component/static/dist
|
||||
cd internal/component/static/ && yarn dist
|
||||
|
||||
deps:
|
||||
cd internal/component/static/ && yarn install
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ func (home *Home) updateRender(ctx context.Context, io stream.IOStream) {
|
|||
|
||||
//go:embed "home.html"
|
||||
var homeHTMLStr string
|
||||
var homeTemplate = static.EntryHome.MustParse(homeHTMLStr)
|
||||
var homeTemplate = static.AssetsHomeHome.MustParse(homeHTMLStr)
|
||||
|
||||
func (home *Home) homeRender() ([]byte, error) {
|
||||
var context HomeContext
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
<p>
|
||||
<a class="pure-button" href="/dis/index">Control</a> >
|
||||
<a class="pure-button pure-button-primary" href="/dis/instance/{{ .Info.Slug }}">Instance</a>
|
||||
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
|
@ -79,7 +78,7 @@
|
|||
<tr>
|
||||
<th colspan="2">
|
||||
Build
|
||||
<button class="remote-action pure-button pure-button-action" data-action="rebuild" data-param="{{ .Instance.Slug }}" data-buffer="1000">Rebuild</button>
|
||||
<button class="remote-action pure-button pure-button-action" data-action="rebuild" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">Rebuild</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -255,7 +254,7 @@
|
|||
<div class="pure-u-1-1">
|
||||
<h2 id="snapshots">Snapshots</h2>
|
||||
<p>
|
||||
<button class="remote-action pure-button pure-button-action" data-action="snapshot" data-param="{{ .Instance.Slug }}" data-buffer="1000">Take a snapshot</button>
|
||||
<button class="remote-action pure-button pure-button-action" data-action="snapshot" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">Take a snapshot</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import (
|
|||
|
||||
//go:embed "html/index.html"
|
||||
var indexTemplateStr string
|
||||
var indexTemplate = static.EntryControlIndex.MustParse(indexTemplateStr)
|
||||
var indexTemplate = static.AssetsControlIndex.MustParse(indexTemplateStr)
|
||||
|
||||
type indexPageContext struct {
|
||||
Time time.Time
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
|
||||
//go:embed "html/instance.html"
|
||||
var instanceTemplateString string
|
||||
var instanceTemplate = static.EntryControlInstance.MustParse(instanceTemplateString)
|
||||
var instanceTemplate = static.AssetsControlInstance.MustParse(instanceTemplateString)
|
||||
|
||||
type instancePageContext struct {
|
||||
Time time.Time
|
||||
|
|
|
|||
40
internal/component/static/assets.go
Normal file
40
internal/component/static/assets.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package static
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
)
|
||||
|
||||
// Assets represents a group of assets to be included inside a template.
|
||||
//
|
||||
// Assets are generated using the 'build.mjs' script.
|
||||
// The script is called using 'go:generate', which stores variables in the form of 'Assets{{Name}}' inside this package.
|
||||
//
|
||||
// The build script roughly works as follows:
|
||||
// - Delete any previously generated distribution directory.
|
||||
// - Bundle the entrypoint sources under 'src/entry/{{Name}}/index.{ts,css}' together with the base './src/base/index.{ts,css}'
|
||||
// - Store the output inside the 'dist' directory
|
||||
// - Generate new constants of the form {{Name}}
|
||||
//
|
||||
// Each asset group should be registered as a parameter to the 'go:generate' line.
|
||||
type Assets struct {
|
||||
Scripts string // <script> tags inserted by the asset
|
||||
Styles string // <link> tags inserted by the asset
|
||||
}
|
||||
|
||||
//go:generate node build.mjs HomeHome ControlIndex ControlInstance
|
||||
|
||||
// MustParse parses a new template from the given source
|
||||
// and registers the Asset functions to it.
|
||||
// See [Assets.RegisterFuncs].
|
||||
func (assets *Assets) MustParse(value string) *template.Template {
|
||||
return template.Must(assets.RegisterFuncs(template.New("")).Parse(value))
|
||||
}
|
||||
|
||||
// RegisterFuncs registers two new template functions called "JS" and "CSS".
|
||||
// Both take no arguments, and return a html-safe version of the Scripts and Style tags to be included.
|
||||
func (assets *Assets) RegisterFuncs(t *template.Template) *template.Template {
|
||||
return t.Funcs(template.FuncMap{
|
||||
"JS": func() template.HTML { return template.HTML(assets.Scripts) },
|
||||
"CSS": func() template.HTML { return template.HTML(assets.Styles) },
|
||||
})
|
||||
}
|
||||
21
internal/component/static/assets_dist.go
Normal file
21
internal/component/static/assets_dist.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package static
|
||||
|
||||
// This file was automatically generated. Do not edit.
|
||||
|
||||
// AssetsHomeHome contains assets for the 'HomeHome' entrypoint.
|
||||
var AssetsHomeHome = Assets{
|
||||
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script>`,
|
||||
Styles: `<link rel="stylesheet" href="/static/HomeHome.518b2dbe.css"><link rel="stylesheet" href="/static/HomeHome.38d394c2.css">`,
|
||||
}
|
||||
|
||||
// AssetsControlIndex contains assets for the 'ControlIndex' entrypoint.
|
||||
var AssetsControlIndex = Assets{
|
||||
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlIndex.43f953d2.js"></script><script src="/static/ControlIndex.c70a89e1.js" nomodule="" defer></script>`,
|
||||
Styles: `<link rel="stylesheet" href="/static/HomeHome.518b2dbe.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css">`,
|
||||
}
|
||||
|
||||
// AssetsControlInstance contains assets for the 'ControlInstance' entrypoint.
|
||||
var AssetsControlInstance = Assets{
|
||||
Scripts: `<script nomodule="" defer src="/static/ControlIndex.c70a89e1.js"></script><script type="module" src="/static/ControlIndex.43f953d2.js"></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlInstance.66b95713.js"></script><script src="/static/ControlInstance.9cc7166d.js" nomodule="" defer></script>`,
|
||||
Styles: `<link rel="stylesheet" href="/static/HomeHome.518b2dbe.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css"><link rel="stylesheet" href="/static/ControlInstance.38d394c2.css">`,
|
||||
}
|
||||
127
internal/component/static/build.mjs
Normal file
127
internal/component/static/build.mjs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { Parcel } from "@parcel/core"
|
||||
import { mkdir, rm, writeFile, readFile, unlink, rmdir, } from "fs/promises"
|
||||
import { join } from "path"
|
||||
import { parse as parseHTML } from 'node-html-parser';
|
||||
|
||||
//
|
||||
// PARAMETERS
|
||||
//
|
||||
|
||||
const ENTRYPOINTS = process.argv.slice(2)
|
||||
const ENTRY_DIR = join('.', '.entry-cache') // directory to place entries into
|
||||
const DIST_DIR = join('.', 'dist')
|
||||
const PUBLIC_DIR = '/static/'
|
||||
|
||||
const DEST_PACKAGE = process.env.GOPACKAGE ?? 'static'
|
||||
const DEST_FILE = (() => {
|
||||
const source = (process.env.GOFILE ?? 'assets.go')
|
||||
const base = source.substring(0, source.length - '.go'.length)
|
||||
return base + '_dist.go'
|
||||
})()
|
||||
|
||||
//
|
||||
// PREPARE DIRECTORIES
|
||||
//
|
||||
|
||||
process.stdout.write('Preparing directories ...')
|
||||
await Promise.all([
|
||||
mkdir(ENTRY_DIR, { recursive: true }),
|
||||
rm(DIST_DIR, { recursive: true, force: true })
|
||||
])
|
||||
console.log(' Done.')
|
||||
|
||||
|
||||
//
|
||||
// WRITE ENTRY POINTS
|
||||
//
|
||||
|
||||
process.stdout.write('Collecting entry points ')
|
||||
const entries = await Promise.all(ENTRYPOINTS.map(async (name) => {
|
||||
const entry = {
|
||||
'name': name,
|
||||
'bundleName': name + '.html',
|
||||
'src': join(ENTRY_DIR, name + '.html'),
|
||||
}
|
||||
|
||||
const content = `
|
||||
<script type='module' src='../src/base/index.ts'></script>
|
||||
<script type='module' src='../src/entry/${name}/index.ts'></script>
|
||||
<link rel='stylesheet' href='../src/entry/${name}/index.css'>
|
||||
`;
|
||||
await writeFile(entry.src, content)
|
||||
|
||||
process.stdout.write('.')
|
||||
return entry;
|
||||
}))
|
||||
console.log(' Done.')
|
||||
|
||||
//
|
||||
// BUNDLEING
|
||||
//
|
||||
|
||||
process.stdout.write('Bundleing assets ...')
|
||||
const bundler = new Parcel({
|
||||
entries: entries.map(e => e.src),
|
||||
defaultConfig: '@parcel/config-default',
|
||||
shouldDisableCache: true,
|
||||
shouldContentHash: true,
|
||||
defaultTargetOptions: {
|
||||
shouldOptimize: true,
|
||||
shouldScopeHoist: true,
|
||||
sourceMaps: false,
|
||||
distDir: DIST_DIR,
|
||||
publicUrl: PUBLIC_DIR,
|
||||
engines: {
|
||||
browsers: "defaults",
|
||||
}
|
||||
}
|
||||
});
|
||||
const { bundleGraph } = await bundler.run()
|
||||
console.log(' Done.')
|
||||
|
||||
|
||||
//
|
||||
// FIND ASSETS IN OUTPUT
|
||||
//
|
||||
|
||||
process.stdout.write('Find Assets in Output ')
|
||||
const bundles = bundleGraph.getBundles()
|
||||
const assets = await Promise.all(entries.map(async (entry) => {
|
||||
const mainBundle = bundles.find(b => b.name === entry.bundleName)
|
||||
if (mainBundle === undefined) throw new Error('Unable to find bundle for ' + entry.name)
|
||||
|
||||
// read, then delete the generated output file
|
||||
const { filePath } = mainBundle
|
||||
const html = parseHTML(await readFile(filePath))
|
||||
await unlink(filePath)
|
||||
|
||||
const scripts = html.querySelectorAll('script').map(script => script.outerHTML).join('')
|
||||
const links = html.querySelectorAll('link').map(link => link.outerHTML).join('')
|
||||
|
||||
process.stdout.write('.')
|
||||
return { ...entry, scripts, links }
|
||||
}))
|
||||
console.log(' Done.')
|
||||
|
||||
//
|
||||
// GENERATE GO
|
||||
//
|
||||
|
||||
process.stdout.write(`Writing ${DEST_FILE} ...`)
|
||||
const goAssets = assets.map(({ name, scripts, links }) => {
|
||||
return `
|
||||
// Assets${name} contains assets for the '${name}' entrypoint.
|
||||
var Assets${name} = Assets{
|
||||
\tScripts: \`${scripts}\`,
|
||||
\tStyles: \`${links}\`,\t
|
||||
}`.trim()
|
||||
}).join('\n\n')
|
||||
const goSource = `package ${DEST_PACKAGE}
|
||||
|
||||
// This file was automatically generated. Do not edit.
|
||||
|
||||
${goAssets}
|
||||
`;
|
||||
|
||||
await writeFile(DEST_FILE, goSource)
|
||||
console.log(' Done.')
|
||||
1
internal/component/static/dist/ControlIndex.43f953d2.js
vendored
Normal file
1
internal/component/static/dist/ControlIndex.43f953d2.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
internal/component/static/dist/ControlIndex.6d2ae968.css
vendored
Normal file
1
internal/component/static/dist/ControlIndex.6d2ae968.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.wisski{padding:1em}.wisski h3{padding:0}.wisski a.pure-button{float:right;position:relative;bottom:1em}.wisski.running{background-color:#9ada07}.wisski.stopped{background-color:#ff7a7a}
|
||||
1
internal/component/static/dist/ControlIndex.6d59e220.css
vendored
Normal file
1
internal/component/static/dist/ControlIndex.6d59e220.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.modal-terminal{width:66vw;height:66vh;background-color:#fff;background-clip:padding-box;-webkit-background-clip:padding-box;z-index:1000;border:17vh solid #000c;border-width:17vh 17vw;margin:-17vh -17vw;position:fixed;top:17vh;left:17vw;overflow:auto}.modal-terminal button{z-index:1001;position:fixed;top:17vh;right:17vw}.modal-terminal pre,.modal-terminal button{margin:5px}
|
||||
1
internal/component/static/dist/ControlIndex.c70a89e1.js
vendored
Normal file
1
internal/component/static/dist/ControlIndex.c70a89e1.js
vendored
Normal file
File diff suppressed because one or more lines are too long
0
internal/component/static/dist/ControlInstance.38d394c2.css
vendored
Normal file
0
internal/component/static/dist/ControlInstance.38d394c2.css
vendored
Normal file
1
internal/component/static/dist/ControlInstance.66b95713.js
vendored
Normal file
1
internal/component/static/dist/ControlInstance.66b95713.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},r={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in r){var o=r[e];delete r[e];var i={id:e,exports:{}};return n[e]=i,o.call(i.exports,i,i.exports),i.exports}var l=new Error("Cannot find module '"+e+"'");throw l.code="MODULE_NOT_FOUND",l}).register=function(e,n){r[e]=n},e.parcelRequireafa4=o),o("gJkWt");
|
||||
1
internal/component/static/dist/ControlInstance.9cc7166d.js
vendored
Normal file
1
internal/component/static/dist/ControlInstance.9cc7166d.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},o={},r=e.parcelRequireafa4;null==r&&((r=function(e){if(e in n)return n[e].exports;if(e in o){var r=o[e];delete o[e];var i={id:e,exports:{}};return n[e]=i,r.call(i.exports,i,i.exports),i.exports}var f=new Error("Cannot find module '"+e+"'");throw f.code="MODULE_NOT_FOUND",f}).register=function(e,n){o[e]=n},e.parcelRequireafa4=r),r("8s4Fe")}();
|
||||
0
internal/component/static/dist/HomeHome.38d394c2.css
vendored
Normal file
0
internal/component/static/dist/HomeHome.38d394c2.css
vendored
Normal file
0
internal/component/static/dist/HomeHome.38d394c2.js
vendored
Normal file
0
internal/component/static/dist/HomeHome.38d394c2.js
vendored
Normal file
1
internal/component/static/dist/HomeHome.518b2dbe.css
vendored
Normal file
1
internal/component/static/dist/HomeHome.518b2dbe.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
<link rel="stylesheet" href="/static/control/index.b3c7c0f7.css"><script type="module" src="/static/control/index.dbb01556.js"></script>
|
||||
|
|
@ -1 +0,0 @@
|
|||
<link rel="stylesheet" href="/static/control/index.b3c7c0f7.css"><script type="module" src="/static/control/index.dbb01556.js"></script>
|
||||
|
|
@ -1 +0,0 @@
|
|||
const e=e=>{const n=document.getElementsByTagName("h"+e);Array.from(n).forEach((e=>{void 0!==e.id&&""!==e.id&&e.appendChild((e=>{const n=document.createElement("a");return n.className="header-link",n.href="#"+e,n.innerHTML="#",n})(e.id))}))};new Array(6).fill(0).forEach(((n,r)=>e(r+1)));
|
||||
|
|
@ -1 +0,0 @@
|
|||
<link rel="stylesheet" href="/static/control/index.b3c7c0f7.css"><script type="module" src="/static/home/index.d87c909c.js"></script>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package static
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/resources"
|
||||
)
|
||||
|
||||
var EntryHome = mustParseResources("dist/home/index.html")
|
||||
var EntryControlIndex = mustParseResources("dist/control/index.html")
|
||||
var EntryControlInstance = mustParseResources("dist/control/instance.html")
|
||||
|
||||
// mustParseResources loads the resources from the provided files or panic()s
|
||||
func mustParseResources(path string) resources.Resources {
|
||||
data, err := distStaticFS.ReadFile(path)
|
||||
if err != nil {
|
||||
panic("mustParseResources: Unable to open " + path)
|
||||
}
|
||||
return resources.Parse(bytes.NewReader(data))
|
||||
}
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
{
|
||||
"name": "wisski-distillery-frontend",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"license": "AGPL-3.0-only",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.5",
|
||||
"node-html-parser": "^6.1.1",
|
||||
"parcel": "^2.7.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dist": "parcel build --dist-dir dist --public-url /static/ src/entry/*/*.html --no-source-maps"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,4 +18,4 @@
|
|||
|
||||
.wisski.stopped {
|
||||
background-color: #ff7a7a;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,2 @@
|
|||
import "~/src/base/index"
|
||||
|
||||
import "./index.css"
|
||||
import "~/src/lib/remote"
|
||||
import "~/src/lib/highlight"
|
||||
|
|
@ -0,0 +1 @@
|
|||
@import url("../ControlIndex/index.css")
|
||||
|
|
@ -0,0 +1 @@
|
|||
import "../ControlIndex/index"
|
||||
0
internal/component/static/src/entry/HomeHome/index.css
Normal file
0
internal/component/static/src/entry/HomeHome/index.css
Normal file
0
internal/component/static/src/entry/HomeHome/index.ts
Normal file
0
internal/component/static/src/entry/HomeHome/index.ts
Normal file
|
|
@ -1 +0,0 @@
|
|||
<script type="module" src="./index.ts"></script>
|
||||
|
|
@ -1 +0,0 @@
|
|||
<script type="module" src="./index.ts"></script>
|
||||
|
|
@ -1 +0,0 @@
|
|||
<script type="module" src="./index.ts"></script>
|
||||
|
|
@ -1 +0,0 @@
|
|||
import "~/src/base/index"
|
||||
|
|
@ -49,6 +49,7 @@ function makeTextBuffer(target: HTMLElement, scrollContainer: HTMLElement, size:
|
|||
const elements = document.getElementsByClassName('remote-action')
|
||||
Array.from(elements).forEach((element) => {
|
||||
const action = element.getAttribute('data-action') as string;
|
||||
const reload = element.hasAttribute('data-force-reload');
|
||||
const param = element.getAttribute('data-param') as string | undefined;
|
||||
const bufferSize = (function () {
|
||||
const number = parseInt(element.getAttribute('data-buffer') ?? "", 10) ?? 0;
|
||||
|
|
@ -72,9 +73,17 @@ Array.from(elements).forEach((element) => {
|
|||
// create a button to eventually close everything
|
||||
const button = document.createElement("button")
|
||||
button.className = "pure-button pure-button-success"
|
||||
button.append("Close")
|
||||
button.append(reload ? "Close & Reload" : "Close")
|
||||
button.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (reload) {
|
||||
button.setAttribute('disabled', 'disabled');
|
||||
target.innerHTML = 'Reloading page ...'
|
||||
location.reload()
|
||||
return;
|
||||
}
|
||||
|
||||
modal.parentNode?.removeChild(modal);
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,8 @@ import (
|
|||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/fsx"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
|
|
@ -21,21 +19,16 @@ func (*Static) Name() string { return "static" }
|
|||
|
||||
func (*Static) Routes() []string { return []string{"/static/"} }
|
||||
|
||||
//go:embed dist
|
||||
var staticFS embed.FS
|
||||
|
||||
func (static *Static) Handler(route string, context context.Context, io stream.IOStream) (http.Handler, error) {
|
||||
fs, err := fs.Sub(distStaticFS, "dist")
|
||||
// take the filesystem
|
||||
fs, err := fs.Sub(staticFS, "dist")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// censor *.html in the filesystem
|
||||
fs = fsx.Censor(fs, func(path string) bool {
|
||||
suffix := "html"
|
||||
return len(path) >= len(suffix) && strings.EqualFold(path[len(path)-len(suffix):], suffix)
|
||||
})
|
||||
|
||||
// and serve it
|
||||
return http.StripPrefix(route, http.FileServer(http.FS(fs))), nil
|
||||
}
|
||||
|
||||
//go:embed dist
|
||||
var distStaticFS embed.FS
|
||||
|
|
|
|||
|
|
@ -871,6 +871,17 @@ css-select@^4.1.3:
|
|||
domutils "^2.8.0"
|
||||
nth-check "^2.0.1"
|
||||
|
||||
css-select@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
|
||||
integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-what "^6.1.0"
|
||||
domhandler "^5.0.2"
|
||||
domutils "^3.0.1"
|
||||
nth-check "^2.0.1"
|
||||
|
||||
css-tree@^1.1.2, css-tree@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
|
||||
|
|
@ -879,7 +890,7 @@ css-tree@^1.1.2, css-tree@^1.1.3:
|
|||
mdn-data "2.0.14"
|
||||
source-map "^0.6.1"
|
||||
|
||||
css-what@^6.0.1:
|
||||
css-what@^6.0.1, css-what@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
|
||||
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
|
||||
|
|
@ -910,7 +921,16 @@ dom-serializer@^1.0.1:
|
|||
domhandler "^4.2.0"
|
||||
entities "^2.0.0"
|
||||
|
||||
domelementtype@^2.0.1, domelementtype@^2.2.0:
|
||||
dom-serializer@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
|
||||
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.2"
|
||||
entities "^4.2.0"
|
||||
|
||||
domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
|
|
@ -922,6 +942,13 @@ domhandler@^4.2.0, domhandler@^4.2.2, domhandler@^4.3.1:
|
|||
dependencies:
|
||||
domelementtype "^2.2.0"
|
||||
|
||||
domhandler@^5.0.1, domhandler@^5.0.2:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
|
||||
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
domutils@^2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
|
||||
|
|
@ -931,6 +958,15 @@ domutils@^2.8.0:
|
|||
domelementtype "^2.2.0"
|
||||
domhandler "^4.2.0"
|
||||
|
||||
domutils@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c"
|
||||
integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==
|
||||
dependencies:
|
||||
dom-serializer "^2.0.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.1"
|
||||
|
||||
dotenv-expand@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
|
||||
|
|
@ -956,6 +992,11 @@ entities@^3.0.1:
|
|||
resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
|
||||
integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
|
||||
|
||||
entities@^4.2.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174"
|
||||
integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==
|
||||
|
||||
error-ex@^1.3.1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
||||
|
|
@ -995,6 +1036,11 @@ has-flag@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
he@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
htmlnano@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-2.0.2.tgz#3e3170941e2446a86211196d740272ebca78f878"
|
||||
|
|
@ -1172,6 +1218,14 @@ node-gyp-build@^4.3.0:
|
|||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
|
||||
integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==
|
||||
|
||||
node-html-parser@^6.1.1:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.1.tgz#7f38f4427fafc242a22135d9db80c1455e837467"
|
||||
integrity sha512-eYYblUeoMg0nR6cYGM4GRb1XncNa9FXEftuKAU1qyMIr6rXVtNyUKduvzZtkqFqSHVByq2lLjC7WO8tz7VDmnA==
|
||||
dependencies:
|
||||
css-select "^5.1.0"
|
||||
he "1.2.0"
|
||||
|
||||
node-releases@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,94 +0,0 @@
|
|||
package fsx
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/tkw1536/goprogram/lib/collection"
|
||||
)
|
||||
|
||||
// Censor returns a new filesystem censors files for which the censor function returns true.
|
||||
//
|
||||
// A censored file cannot be opened by the filesystem and return [fs.ErrNotExist].
|
||||
// Hard and Soft Links pointing to the file might still read it.
|
||||
func Censor(fsys fs.FS, censor func(name string) bool) fs.FS {
|
||||
return &censorFS{
|
||||
fsys: fsys,
|
||||
censor: censor,
|
||||
}
|
||||
}
|
||||
|
||||
type censorFS struct {
|
||||
censor func(path string) bool
|
||||
fsys fs.FS
|
||||
}
|
||||
|
||||
func (cf *censorFS) Sub(path string) (fs.FS, error) {
|
||||
sub, err := fs.Sub(cf.fsys, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &censorFS{
|
||||
censor: func(name string) bool {
|
||||
return cf.censor(filepath.Join(path, name))
|
||||
},
|
||||
fsys: sub,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ef *censorFS) Open(name string) (fs.File, error) {
|
||||
if ef.censor(name) {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
|
||||
file, err := ef.fsys.Open(name)
|
||||
|
||||
// we need to also censor the ReadDir function of the returned file
|
||||
// this is to prevent the file from appearing in directory listings.
|
||||
if rdf, ok := file.(fs.ReadDirFile); ok {
|
||||
return &censorFSFile{
|
||||
ReadDirFile: rdf,
|
||||
|
||||
name: name,
|
||||
censor: ef,
|
||||
}, err
|
||||
}
|
||||
return file, err
|
||||
}
|
||||
|
||||
type censorFSFile struct {
|
||||
fs.ReadDirFile
|
||||
|
||||
name string
|
||||
censor *censorFS
|
||||
}
|
||||
|
||||
func (f *censorFSFile) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
entries, err := f.ReadDirFile.ReadDir(n)
|
||||
return f.censor.handleReadDir(f.name, entries, err)
|
||||
}
|
||||
|
||||
func (ef *censorFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
if ef.censor(name) {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
|
||||
// censor ReadDir() entries too
|
||||
entries, err := fs.ReadDir(ef.fsys, name)
|
||||
return ef.handleReadDir(name, entries, err)
|
||||
}
|
||||
|
||||
// handleReadDir censors a ReadDir call
|
||||
func (ef *censorFS) handleReadDir(base string, entries []fs.DirEntry, err error) ([]fs.DirEntry, error) {
|
||||
entries = collection.Filter(entries, func(entry fs.DirEntry) bool {
|
||||
return !ef.censor(filepath.Join(base, entry.Name()))
|
||||
})
|
||||
return entries, err
|
||||
}
|
||||
|
||||
func (ef *censorFS) ReadFile(name string) ([]byte, error) {
|
||||
if ef.censor(name) {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
return fs.ReadFile(ef.fsys, name)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue