This commit is contained in:
2025-02-23 10:34:18 +00:00
parent 1c7621f20a
commit 6820a7e9c8
21 changed files with 1088 additions and 47 deletions

51
lib/web/starlight/clean.v Normal file
View File

@@ -0,0 +1,51 @@
module starlight
import os
import strings
pub fn (mut site DocSite) clean(args ErrorArgs) ! {
toclean := '
/node_modules
babel.config.js
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
bun.lockb
bun.lock
yarn.lock
build.sh
build_dev.sh
build-dev.sh
develop.sh
install.sh
package.json
package-lock.json
pnpm-lock.yaml
sidebars.ts
tsconfig.json
'
//TODO: need better way how to deal with this
}

126
lib/web/starlight/config.v Normal file
View File

@@ -0,0 +1,126 @@
module starlight
import freeflowuniverse.herolib.core.pathlib
import json
import os
// Footer config structures
pub struct FooterItem {
pub mut:
label string
to string
href string
}
pub struct FooterLink {
pub mut:
title string
items []FooterItem
}
pub struct Footer {
pub mut:
style string = 'dark'
links []FooterLink
}
pub struct Main {
pub mut:
name string
title string = 'A Test Site'
// tagline string
url string = 'http://localhost/testsite'
// url_home string
// base_url string = '/' @[json: 'baseUrl']
// image string = 'img/tf_graph.png' @[required]
build_dest []string @[json: 'buildDest']
build_dest_dev []string @[json: 'buildDestDev']
content []ContentItem
}
// Navbar config structures
pub struct NavbarItem {
pub mut:
href string
label string
position string
}
pub struct Navbar {
pub mut:
title string
items []NavbarItem
}
// Combined config structure
pub struct Config {
pub mut:
footer Footer
main Main
navbar Navbar
}
//pulled from e.g. git and linked to a destination in the astro build location
pub struct ContentItem {
pub mut:
url string
dest string
replacer map[string]string //items we want to replace
}
// load_config loads all configuration from the specified directory
pub fn load_config(cfg_dir string) !Config {
// Ensure the config directory exists
if !os.exists(cfg_dir) {
return error('Config directory ${cfg_dir} does not exist')
}
// Load and parse footer config
footer_content := os.read_file(os.join_path(cfg_dir, 'footer.json'))!
footer := json.decode(Footer, footer_content)!
// Load and parse main config
main_config_path := os.join_path(cfg_dir, 'main.json')
main_content := os.read_file(main_config_path)!
main := json.decode(Main, main_content) or {
eprintln('main.json in ${cfg_dir} is not in the right format please fix.\nError: ${err}')
println('
## EXAMPLE OF A GOOD ONE:
- note the list for buildDest and buildDestDev
- note its the full path where the html is pushed too
{
"title": "ThreeFold Web4",
"tagline": "ThreeFold Web4",
"url": "https://docs.threefold.io",
"url_home": "docs/introduction",
"image": "img/tf_graph.png",
"buildDest":["root@info.ourworld.tf:/root/hero/www/info/tfgrid4"],
"buildDestDev":["root@info.ourworld.tf:/root/hero/www/infodev/tfgrid4"]
}
')
exit(99)
}
// Load and parse navbar config
navbar_content := os.read_file(os.join_path(cfg_dir, 'navbar.json'))!
navbar := json.decode(Navbar, navbar_content)!
return Config{
footer: footer
main: main
navbar: navbar
}
}
pub fn (c Config) write(path string) ! {
mut footer_file := pathlib.get_file(path: '${path}/footer.json', create: true)!
footer_file.write(json.encode(c.footer))!
mut main_file := pathlib.get_file(path: '${path}/main.json', create: true)!
main_file.write(json.encode(c.main))!
mut navbar_file := pathlib.get_file(path: '${path}/navbar.json', create: true)!
navbar_file.write(json.encode(c.navbar))!
}

View File

@@ -0,0 +1,42 @@
module starlight
import os
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.develop.gittools
@[heap]
pub struct StarlightFactory {
pub mut:
sites []&DocSite @[skip; str: skip]
path_build pathlib.Path
// path_publish pathlib.Path
args StarlightArgs
}
@[params]
pub struct StarlightArgs {
pub mut:
// publish_path string
build_path string
production bool
update bool
}
pub fn new(args_ StarlightArgs) !&StarlightFactory {
mut args := args_
if args.build_path == '' {
args.build_path = '${os.home_dir()}/hero/var/starlight'
}
// if args.publish_path == ""{
// args.publish_path = "${os.home_dir()}/hero/var/starlight/publish"
// }
mut ds := &StarlightFactory{
args: args_
path_build: pathlib.get_dir(path: args.build_path, create: true)!
// path_publish: pathlib.get_dir(path: args_.publish_path, create: true)!
}
ds.template_install(install:true,template_update:args.update,delete:true)!
return ds
}

24
lib/web/starlight/model.v Normal file
View File

@@ -0,0 +1,24 @@
module starlight
pub struct SiteError {
Error
pub mut:
path string
msg string
cat ErrorCat
}
pub enum ErrorCat {
unknown
image_double
file_double
file_not_found
image_not_found
page_double
page_not_found
sidebar
circular_import
def
summary
include
}

214
lib/web/starlight/site.v Normal file
View File

@@ -0,0 +1,214 @@
module starlight
import freeflowuniverse.herolib.osal.screen
import os
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.core.base
import freeflowuniverse.herolib.develop.gittools
import json
import freeflowuniverse.herolib.osal
import freeflowuniverse.herolib.ui.console
@[heap]
pub struct DocSite {
pub mut:
name string
url string
path_src pathlib.Path
path_build pathlib.Path
// path_publish pathlib.Path
args SiteGetArgs
errors []SiteError
config Config
factory &StarlightFactory @[skip; str: skip] // Reference to the parent
}
pub fn (mut s DocSite) build() ! {
s.generate()!
osal.exec(
cmd: '
cd ${s.path_build.path}
bash build.sh
'
retry: 0
)!
}
pub fn (mut s DocSite) build_dev_publish() ! {
s.generate()!
osal.exec(
cmd: '
cd ${s.path_build.path}
bash build_dev_publish.sh
'
retry: 0
)!
}
pub fn (mut s DocSite) build_publish()! {
s.generate()!
osal.exec(
cmd: '
cd ${s.path_build.path}
bash build_publish.sh
'
retry: 0
)!
}
pub fn (mut s DocSite) dev()! {
s.clean()!
s.generate()!
// Create screen session for starlight development server
mut screen_name := 'starlight'
mut sf := screen.new()!
// Add and start a new screen session
mut scr := sf.add(
name: screen_name
cmd: '/bin/bash'
start: true
attach: false
reset: true
)!
// Send commands to the screen session
scr.cmd_send('cd ${s.path_build.path}')!
scr.cmd_send('bash develop.sh')!
// Print instructions for user
console.print_header(' Starlight Development Server')
console.print_item('Development server is running in a screen session.')
console.print_item('To view the server output:')
console.print_item(' 1. Attach to screen: screen -r ${screen_name}')
console.print_item(' 2. To detach from screen: Press Ctrl+A then D')
console.print_item(' 3. To list all screens: screen -ls')
console.print_item('The site content is on::')
console.print_item(' 1. location of documents: ${s.path_src.path}/docs')
if osal.cmd_exists('code') {
console.print_item(' 2. We opened above dir in vscode.')
osal.exec(cmd: 'code ${s.path_src.path}/docs')!
}
// Start the watcher in a separate thread
// mut tf:=spawn watch_docs(docs_path, s.path_src.path, s.path_build.path)
// tf.wait()!
println('\n')
if s.args.watch_changes {
docs_path := '${s.path_src.path}/docs'
watch_docs(docs_path, s.path_src.path, s.path_build.path)!
}
}
@[params]
pub struct ErrorArgs {
pub mut:
path string
msg string
cat ErrorCat
}
pub fn (mut site DocSite) error(args ErrorArgs) {
// path2 := pathlib.get(args.path)
e := SiteError{
path: args.path
msg: args.msg
cat: args.cat
}
site.errors << e
console.print_stderr(args.msg)
}
fn check_item(item string)!{
item2:=item.trim_space().trim("/").trim_space().all_after_last("/")
if ["internal","infodev","info","dev","friends","dd","web"].contains(item2){
return error("destination path is wrong, cannot be: ${item}")
}
}
fn (mut site DocSite) check() ! {
for item in site.config.main.build_dest{
check_item(item)!
}
for item in site.config.main.build_dest_dev{
check_item(item)!
}
}
pub fn (mut site DocSite) generate() ! {
console.print_header(' site generate: ${site.name} on ${site.path_build.path}')
console.print_header(' site source on ${site.path_src.path}')
site.check()!
site.template_install()!
// Now copy all directories that exist in src to build
for item in ['src', 'static', 'cfg', 'public'] {
if os.exists('${site.path_src.path}/${item}') {
mut aa := site.path_src.dir_get(item)!
aa.copy(dest: '${site.path_build.path}/${item}')!
}
}
for item in ['docs'] {
if os.exists('${site.path_src.path}/${item}') {
mut aa := site.path_src.dir_get(item)!
aa.copy(dest: '${site.path_build.path}/${item}', delete: true)!
}
}
}
fn (mut site DocSite) template_install() ! {
mut gs := gittools.new()!
site.factory.template_install(template_update:false, install:false, delete:false)!
cfg := site.config
mut myhome:="\$\{HOME\}" //for usage in bash
profile_include := osal.profile_path_source()!.replace(os.home_dir(),myhome)
mydir:=site.path_build.path.replace(os.home_dir(),myhome)
for item in ['src', 'static'] {
mut aa := site.path_src.dir_get(item) or {continue}
aa.copy(dest: '${site.factory.path_build.path}/${item}', delete:false)!
}
develop := $tmpl('templates/develop.sh')
build := $tmpl('templates/build.sh')
build_dev_publish := $tmpl('templates/build_dev_publish.sh')
build_publish := $tmpl('templates/build_publish.sh')
mut develop_ := site.path_build.file_get_new('develop.sh')!
develop_.template_write(develop, true)!
develop_.chmod(0o700)!
mut build_ := site.path_build.file_get_new('build.sh')!
build_.template_write(build, true)!
build_.chmod(0o700)!
mut build_publish_ := site.path_build.file_get_new('build_publish.sh')!
build_publish_.template_write(build_publish, true)!
build_publish_.chmod(0o700)!
mut build_dev_publish_ := site.path_build.file_get_new('build_dev_publish.sh')!
build_dev_publish_.template_write(build_dev_publish, true)!
build_dev_publish_.chmod(0o700)!
mut develop2_ := site.path_src.file_get_new('develop.sh')!
develop2_.template_write(develop, true)!
develop2_.chmod(0o700)!
mut build2_ := site.path_src.file_get_new('build.sh')!
build2_.template_write(build, true)!
build2_.chmod(0o700)!
}

View File

@@ -0,0 +1,109 @@
module starlight
import os
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.develop.gittools
import freeflowuniverse.herolib.ui.console
@[params]
pub struct SiteGetArgs {
pub mut:
name string
nameshort string
path string
url string
publish_path string
build_path string
production bool
watch_changes bool = true
update bool
init bool //means create new one if needed
deploykey string
config ?Config
}
pub fn (mut f StarlightFactory) get(args_ SiteGetArgs) !&DocSite {
console.print_header(' Starlight: ${args_.name}')
mut args := args_
if args.build_path.len == 0 {
args.build_path = '${f.path_build.path}'
}
// if args.publish_path.len == 0 {
// args.publish_path = '${f.path_publish.path}/${args.name}'
// coderoot:"${os.home_dir()}/hero/var/publishcode"
mut gs := gittools.new(ssh_key_path: args.deploykey)!
if args.url.len > 0 {
args.path = gs.get_path(url: args.url)!
}
if args.path.trim_space() == "" {
args.path = os.getwd()
}
args.path = args.path.replace('~', os.home_dir())
mut r := gs.get_repo(
url: 'https://github.com/freeflowuniverse/starlight_template.git'
)!
mut template_path := r.patho()!
// First, check if the new site args provides a configuration that can be written instead of template cfg dir
if cfg := args.config {
cfg.write('${args.path}/cfg')!
} else {
// Then ensure cfg directory exists in src,
if !os.exists('${args.path}/cfg') {
if args.init{
// else copy config from template
mut template_cfg := template_path.dir_get('cfg')!
template_cfg.copy(dest: '${args.path}/cfg')!
}else{
return error("Can't find cfg dir in chosen starlight location: ${args.path}")
}
}
}
if !os.exists('${args.path}/src') {
if args.init{
mut template_cfg := template_path.dir_get('src')!
template_cfg.copy(dest: '${args.path}/src')!
} else{
return error("Can't find src dir in chosen starlight location: ${args.path}")
}
}
mut myconfig := load_config('${args.path}/cfg')!
if args.name == '' {
args.name = myconfig.main.name
}
if args.name.len==0{
return error("name for a site cannot be empty")
}
if args.nameshort.len == 0 {
args.nameshort = args.name
}
args.nameshort = texttools.name_fix(args.nameshort)
mut ds := DocSite{
name: args.name
url: args.url
path_src: pathlib.get_dir(path: args.path, create: false)!
path_build: f.path_build
// path_publish: pathlib.get_dir(path: args.publish_path, create: true)!
args: args
config: myconfig
factory: &f
}
ds.check()!
f.sites << &ds
return &ds
}

View File

@@ -0,0 +1,58 @@
module starlight
import freeflowuniverse.herolib.develop.gittools
import freeflowuniverse.herolib.osal
import freeflowuniverse.herolib.installers.web.bun
import freeflowuniverse.herolib.installers.web.tailwind
import os
@[params]
struct TemplateInstallArgs{
template_update bool = true
install bool
delete bool = true
}
fn (mut self StarlightFactory) template_install(args TemplateInstallArgs) ! {
mut gs := gittools.new()!
mut r := gs.get_repo(
url: 'https://github.com/freeflowuniverse/starlight_template.git'
pull: args.template_update
)!
mut template_path := r.patho()!
for item in ['public', 'src'] {
mut aa := template_path.dir_get(item) or {continue} //skip if not exist
aa.copy(dest: '${self.path_build.path}/${item}', delete: args.delete)!
}
for item in ['package.json', 'tsconfig.json', 'astro.config.mjs'] {
src_path := os.join_path(template_path.path, item)
dest_path := os.join_path(self.path_build.path, item)
os.cp(src_path, dest_path) or {
return error('Failed to copy ${item} to build path: ${err}')
}
}
if args.install{
// install bun
mut installer := bun.get()!
installer.install()!
mut installer2 := tailwind.get()!
installer2.install()!
osal.exec(
cmd: '
${osal.profile_path_source_and()!}
export PATH=/tmp/starlight_build/node_modules/.bin:${os.home_dir()}/.bun/bin/:??PATH
cd ${self.path_build.path}
bun install
'
)!
}
}

View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -ex
script_dir="???cd "???dirname "??{BASH_SOURCE[0]}")" && pwd)"
cd "??{script_dir}"
echo "Docs directory: ??script_dir"
cd "${mydir}"
export PATH=${site.path_build.path}/node_modules/.bin:??{HOME}/.bun/bin/:??PATH
rm -rf ${site.path_build.path}/build/
${profile_include}
bun run build
mkdir -p ${site.args.publish_path.trim_right("/")}
echo SYNC TO ${site.args.publish_path.trim_right("/")}
rsync -rv --delete ${site.path_build.path}/build/ ${site.args.publish_path.trim_right("/")}/

View File

@@ -0,0 +1,23 @@
#!/bin/bash
set -e
script_dir="???cd "???dirname "??{BASH_SOURCE[0]}")" && pwd)"
cd "??{script_dir}"
echo "Docs directory: ??script_dir"
cd "${mydir}"
export PATH=${site.path_build.path}/node_modules/.bin:??{HOME}/.bun/bin/:??PATH
rm -rf ${site.path_build.path}/build/
${profile_include}
bun run build
@for dest in cfg.main.build_dest_dev
rsync -rv --delete ${site.path_build.path}/build/ ${dest.trim_right("/")}/
@end

View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -ex
script_dir="???cd "???dirname "??{BASH_SOURCE[0]}")" && pwd)"
cd "??{script_dir}"
echo "Docs directory: ??script_dir"
cd "${mydir}"
export PATH=${site.path_build.path}/node_modules/.bin:??{HOME}/.bun/bin/:??PATH
rm -rf ${site.path_build.path}/build/
${profile_include}
bun run build
@for dest in cfg.main.build_dest
rsync -rv --delete ${site.path_build.path}/build/ ${dest.trim_right("/")}/
@end

View File

@@ -0,0 +1,16 @@
#!/bin/bash
set -e
script_dir="???cd "???dirname "??{BASH_SOURCE[0]}")" && pwd)"
cd "??{script_dir}"
echo "Docs directory: ??script_dir"
cd "${mydir}"
export PATH=${site.path_build.path}/node_modules/.bin:??{HOME}/.bun/bin/:??PATH
${profile_include}
bun dev

View File

@@ -0,0 +1,96 @@
module starlight
import freeflowuniverse.herolib.osal.notifier
import os
fn watch_docs(docs_path string, path_src string, path_build string) ! {
mut n := notifier.new('docsite_watcher') or {
eprintln('Failed to create watcher: ${err}')
return
}
n.args['path_src'] = path_src
n.args['path_build'] = path_build
// Add watch with captured args
n.add_watch(docs_path, fn (event notifier.NotifyEvent, path string, args map[string]string) {
handle_file_change(event, path, args) or { eprintln('Error handling file change: ${err}') }
})!
n.start()!
}
// handle_file_change processes file system events
fn handle_file_change(event notifier.NotifyEvent, path string, args map[string]string) ! {
file_base := os.base(path)
is_dir := os.is_dir(path)
// Skip files starting with #
if file_base.starts_with('#') {
return
}
// For files (not directories), check extensions
if !is_dir {
ext := os.file_ext(path).to_lower()
if ext !in ['.md', '.png', '.jpeg', '.jpg'] {
return
}
}
// Get relative path from docs directory
rel_path := path.replace('${args['path_src']}/docs/', '')
dest_path := '${args['path_build']}/docs/${rel_path}'
match event {
.create, .modify {
if is_dir {
// For directories, just ensure they exist
os.mkdir_all(dest_path) or {
return error('Failed to create directory ${dest_path}: ${err}')
}
println('Created directory: ${rel_path}')
} else {
// For files, ensure parent directory exists and copy
os.mkdir_all(os.dir(dest_path)) or {
return error('Failed to create directory ${os.dir(dest_path)}: ${err}')
}
os.cp(path, dest_path) or {
return error('Failed to copy ${path} to ${dest_path}: ${err}')
}
println('Updated: ${rel_path}')
}
}
.delete {
if os.exists(dest_path) {
if is_dir {
os.rmdir_all(dest_path) or {
return error('Failed to delete directory ${dest_path}: ${err}')
}
println('Deleted directory: ${rel_path}')
} else {
os.rm(dest_path) or { return error('Failed to delete ${dest_path}: ${err}') }
println('Deleted: ${rel_path}')
}
}
}
.rename {
// For rename events, fswatch provides the new path in the event
// The old path is already removed, so we just need to handle the new path
if is_dir {
os.mkdir_all(dest_path) or {
return error('Failed to create directory ${dest_path}: ${err}')
}
println('Renamed directory to: ${rel_path}')
} else {
os.mkdir_all(os.dir(dest_path)) or {
return error('Failed to create directory ${os.dir(dest_path)}: ${err}')
}
os.cp(path, dest_path) or {
return error('Failed to copy ${path} to ${dest_path}: ${err}')
}
println('Renamed to: ${rel_path}')
}
}
}
}