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:
Tom Wiesing 2022-10-15 14:10:32 +02:00
parent ebdbe9fabd
commit ccab2883a6
No known key found for this signature in database
42 changed files with 408 additions and 177 deletions

1
.gitignore vendored
View file

@ -9,4 +9,5 @@ go.work.sum
node_modules node_modules
.parcel-cache .parcel-cache
.entry-cache
yarn-error.log yarn-error.log

View file

@ -1,14 +1,11 @@
.PHONY: clean all deps .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 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: deps:
cd internal/component/static/ && yarn install cd internal/component/static/ && yarn install

View file

@ -47,7 +47,7 @@ func (home *Home) updateRender(ctx context.Context, io stream.IOStream) {
//go:embed "home.html" //go:embed "home.html"
var homeHTMLStr string var homeHTMLStr string
var homeTemplate = static.EntryHome.MustParse(homeHTMLStr) var homeTemplate = static.AssetsHomeHome.MustParse(homeHTMLStr)
func (home *Home) homeRender() ([]byte, error) { func (home *Home) homeRender() ([]byte, error) {
var context HomeContext var context HomeContext

View file

@ -13,7 +13,6 @@
<p> <p>
<a class="pure-button" href="/dis/index">Control</a> &gt; <a class="pure-button" href="/dis/index">Control</a> &gt;
<a class="pure-button pure-button-primary" href="/dis/instance/{{ .Info.Slug }}">Instance</a> <a class="pure-button pure-button-primary" href="/dis/instance/{{ .Info.Slug }}">Instance</a>
</p> </p>
</header> </header>
@ -79,7 +78,7 @@
<tr> <tr>
<th colspan="2"> <th colspan="2">
Build 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> </th>
</tr> </tr>
</thead> </thead>
@ -255,7 +254,7 @@
<div class="pure-u-1-1"> <div class="pure-u-1-1">
<h2 id="snapshots">Snapshots</h2> <h2 id="snapshots">Snapshots</h2>
<p> <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> </p>
</div> </div>

View file

@ -15,7 +15,7 @@ import (
//go:embed "html/index.html" //go:embed "html/index.html"
var indexTemplateStr string var indexTemplateStr string
var indexTemplate = static.EntryControlIndex.MustParse(indexTemplateStr) var indexTemplate = static.AssetsControlIndex.MustParse(indexTemplateStr)
type indexPageContext struct { type indexPageContext struct {
Time time.Time Time time.Time

View file

@ -14,7 +14,7 @@ import (
//go:embed "html/instance.html" //go:embed "html/instance.html"
var instanceTemplateString string var instanceTemplateString string
var instanceTemplate = static.EntryControlInstance.MustParse(instanceTemplateString) var instanceTemplate = static.AssetsControlInstance.MustParse(instanceTemplateString)
type instancePageContext struct { type instancePageContext struct {
Time time.Time Time time.Time

View 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) },
})
}

View 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">`,
}

View 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.')

File diff suppressed because one or more lines are too long

View 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}

View 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}

File diff suppressed because one or more lines are too long

View 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");

View 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")}();

View file

View 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

View file

@ -1 +0,0 @@
<link rel="stylesheet" href="/static/control/index.b3c7c0f7.css"><script type="module" src="/static/control/index.dbb01556.js"></script>

View file

@ -1 +0,0 @@
<link rel="stylesheet" href="/static/control/index.b3c7c0f7.css"><script type="module" src="/static/control/index.dbb01556.js"></script>

View file

@ -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)));

View file

@ -1 +0,0 @@
<link rel="stylesheet" href="/static/control/index.b3c7c0f7.css"><script type="module" src="/static/home/index.d87c909c.js"></script>

View file

@ -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))
}

View file

@ -1,13 +1,11 @@
{ {
"name": "wisski-distillery-frontend", "name": "wisski-distillery-frontend",
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "AGPL-3.0-only",
"private": true, "private": true,
"dependencies": { "dependencies": {
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"node-html-parser": "^6.1.1",
"parcel": "^2.7.0" "parcel": "^2.7.0"
},
"scripts": {
"dist": "parcel build --dist-dir dist --public-url /static/ src/entry/*/*.html --no-source-maps"
} }
} }

View file

@ -1,5 +1,2 @@
import "~/src/base/index"
import "./index.css"
import "~/src/lib/remote" import "~/src/lib/remote"
import "~/src/lib/highlight" import "~/src/lib/highlight"

View file

@ -0,0 +1 @@
@import url("../ControlIndex/index.css")

View file

@ -0,0 +1 @@
import "../ControlIndex/index"

View file

@ -1 +0,0 @@
<script type="module" src="./index.ts"></script>

View file

@ -1 +0,0 @@
<script type="module" src="./index.ts"></script>

View file

@ -1 +0,0 @@
<script type="module" src="./index.ts"></script>

View file

@ -1 +0,0 @@
import "~/src/base/index"

View file

@ -49,6 +49,7 @@ function makeTextBuffer(target: HTMLElement, scrollContainer: HTMLElement, size:
const elements = document.getElementsByClassName('remote-action') const elements = document.getElementsByClassName('remote-action')
Array.from(elements).forEach((element) => { Array.from(elements).forEach((element) => {
const action = element.getAttribute('data-action') as string; 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 param = element.getAttribute('data-param') as string | undefined;
const bufferSize = (function () { const bufferSize = (function () {
const number = parseInt(element.getAttribute('data-buffer') ?? "", 10) ?? 0; 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 // create a button to eventually close everything
const button = document.createElement("button") const button = document.createElement("button")
button.className = "pure-button pure-button-success" button.className = "pure-button pure-button-success"
button.append("Close") button.append(reload ? "Close & Reload" : "Close")
button.addEventListener('click', function (event) { button.addEventListener('click', function (event) {
event.preventDefault(); event.preventDefault();
if (reload) {
button.setAttribute('disabled', 'disabled');
target.innerHTML = 'Reloading page ...'
location.reload()
return;
}
modal.parentNode?.removeChild(modal); modal.parentNode?.removeChild(modal);
}) })

View file

@ -6,10 +6,8 @@ import (
"embed" "embed"
"io/fs" "io/fs"
"net/http" "net/http"
"strings"
"github.com/FAU-CDI/wisski-distillery/internal/component" "github.com/FAU-CDI/wisski-distillery/internal/component"
"github.com/FAU-CDI/wisski-distillery/pkg/fsx"
"github.com/tkw1536/goprogram/stream" "github.com/tkw1536/goprogram/stream"
) )
@ -21,21 +19,16 @@ func (*Static) Name() string { return "static" }
func (*Static) Routes() []string { return []string{"/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) { 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 { if err != nil {
return nil, err 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 // and serve it
return http.StripPrefix(route, http.FileServer(http.FS(fs))), nil return http.StripPrefix(route, http.FileServer(http.FS(fs))), nil
} }
//go:embed dist
var distStaticFS embed.FS

View file

@ -871,6 +871,17 @@ css-select@^4.1.3:
domutils "^2.8.0" domutils "^2.8.0"
nth-check "^2.0.1" 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: css-tree@^1.1.2, css-tree@^1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" 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" mdn-data "2.0.14"
source-map "^0.6.1" source-map "^0.6.1"
css-what@^6.0.1: css-what@^6.0.1, css-what@^6.1.0:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
@ -910,7 +921,16 @@ dom-serializer@^1.0.1:
domhandler "^4.2.0" domhandler "^4.2.0"
entities "^2.0.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" version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
@ -922,6 +942,13 @@ domhandler@^4.2.0, domhandler@^4.2.2, domhandler@^4.3.1:
dependencies: dependencies:
domelementtype "^2.2.0" 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: domutils@^2.8.0:
version "2.8.0" version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
@ -931,6 +958,15 @@ domutils@^2.8.0:
domelementtype "^2.2.0" domelementtype "^2.2.0"
domhandler "^4.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: dotenv-expand@^5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" 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" resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== 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: error-ex@^1.3.1:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" 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" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 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: htmlnano@^2.0.0:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-2.0.2.tgz#3e3170941e2446a86211196d740272ebca78f878" 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" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== 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: node-releases@^2.0.6:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" 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

View file

@ -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)
}