Compare commits
No commits in common. "master" and "1.0.0" have entirely different histories.
501 changed files with 4432 additions and 32057 deletions
51
.air.toml
Normal file
51
.air.toml
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main ."
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules", "js"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
app_port = 0
|
||||||
|
enabled = false
|
||||||
|
proxy_port = 0
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
|
|
@ -1,3 +0,0 @@
|
||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: [maddalax]
|
|
||||||
52
.github/workflows/release-auth-example.yml
vendored
52
.github/workflows/release-auth-example.yml
vendored
|
|
@ -1,52 +0,0 @@
|
||||||
name: Build and Deploy htmgo auth example
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
workflow_dispatch: # Trigger on manual workflow_dispatch
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master # Trigger on pushes to master
|
|
||||||
paths:
|
|
||||||
- 'examples/simple-auth/**' # Trigger only if files in this directory change
|
|
||||||
- "framework-ui/**"
|
|
||||||
- "cli/**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get short commit hash
|
|
||||||
id: vars
|
|
||||||
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
run: |
|
|
||||||
cd ./examples/simple-auth && docker build -t ghcr.io/${{ github.repository_owner }}/simple-auth:${{ steps.vars.outputs.short_sha }} .
|
|
||||||
|
|
||||||
- name: Tag as latest Docker image
|
|
||||||
run: |
|
|
||||||
docker tag ghcr.io/${{ github.repository_owner }}/simple-auth:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/simple-auth:latest
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Push Docker image
|
|
||||||
run: |
|
|
||||||
docker push ghcr.io/${{ github.repository_owner }}/simple-auth:latest
|
|
||||||
50
.github/workflows/release-chat-example.yml
vendored
50
.github/workflows/release-chat-example.yml
vendored
|
|
@ -1,50 +0,0 @@
|
||||||
name: Build and Deploy htmgo.dev chat example
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
workflow_dispatch: # Trigger on manual workflow_dispatch
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master # Trigger on pushes to master
|
|
||||||
paths:
|
|
||||||
- 'examples/chat/**' # Trigger only if files in this directory change
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get short commit hash
|
|
||||||
id: vars
|
|
||||||
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
run: |
|
|
||||||
cd ./examples/chat && docker build -t ghcr.io/${{ github.repository_owner }}/htmgo-chat-example:${{ steps.vars.outputs.short_sha }} .
|
|
||||||
|
|
||||||
- name: Tag as latest Docker image
|
|
||||||
run: |
|
|
||||||
docker tag ghcr.io/${{ github.repository_owner }}/htmgo-chat-example:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/htmgo-chat-example:latest
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Push Docker image
|
|
||||||
run: |
|
|
||||||
docker push ghcr.io/${{ github.repository_owner }}/htmgo-chat-example:latest
|
|
||||||
52
.github/workflows/release-hn-clone.yml
vendored
52
.github/workflows/release-hn-clone.yml
vendored
|
|
@ -1,52 +0,0 @@
|
||||||
name: Build and Deploy htmgo hackernews clone
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
workflow_dispatch: # Trigger on manual workflow_dispatch
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master # Trigger on pushes to master
|
|
||||||
paths:
|
|
||||||
- 'examples/hackernews/**' # Trigger only if files in this directory change
|
|
||||||
- "framework-ui/**"
|
|
||||||
- "cli/**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get short commit hash
|
|
||||||
id: vars
|
|
||||||
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
run: |
|
|
||||||
cd ./examples/hackernews && docker build -t ghcr.io/${{ github.repository_owner }}/hackernews:${{ steps.vars.outputs.short_sha }} .
|
|
||||||
|
|
||||||
- name: Tag as latest Docker image
|
|
||||||
run: |
|
|
||||||
docker tag ghcr.io/${{ github.repository_owner }}/hackernews:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/hackernews:latest
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Push Docker image
|
|
||||||
run: |
|
|
||||||
docker push ghcr.io/${{ github.repository_owner }}/hackernews:latest
|
|
||||||
52
.github/workflows/release-site.yml
vendored
52
.github/workflows/release-site.yml
vendored
|
|
@ -1,52 +0,0 @@
|
||||||
name: Build and Deploy htmgo.dev
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
workflow_dispatch: # Trigger on manual workflow_dispatch
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master # Trigger on pushes to master
|
|
||||||
paths:
|
|
||||||
- 'htmgo-site/**' # Trigger only if files in this directory change
|
|
||||||
- "framework-ui/**"
|
|
||||||
- "cli/**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get short commit hash
|
|
||||||
id: vars
|
|
||||||
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
run: |
|
|
||||||
cd ./htmgo-site && docker build -t ghcr.io/${{ github.repository_owner }}/htmgo-site:${{ steps.vars.outputs.short_sha }} .
|
|
||||||
|
|
||||||
- name: Tag as latest Docker image
|
|
||||||
run: |
|
|
||||||
docker tag ghcr.io/${{ github.repository_owner }}/htmgo-site:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/htmgo-site:latest
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Push Docker image
|
|
||||||
run: |
|
|
||||||
docker push ghcr.io/${{ github.repository_owner }}/htmgo-site:latest
|
|
||||||
50
.github/workflows/release-starter-template.yml
vendored
50
.github/workflows/release-starter-template.yml
vendored
|
|
@ -1,50 +0,0 @@
|
||||||
name: Build and Deploy starter template
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
workflow_dispatch: # Trigger on manual workflow_dispatch
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master # Trigger on pushes to master
|
|
||||||
paths:
|
|
||||||
- 'templates/starter/**' # Trigger only if files in this directory change
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get short commit hash
|
|
||||||
id: vars
|
|
||||||
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
run: |
|
|
||||||
cd ./templates/starter && docker build -t ghcr.io/${{ github.repository_owner }}/starter-template:${{ steps.vars.outputs.short_sha }} .
|
|
||||||
|
|
||||||
- name: Tag as latest Docker image
|
|
||||||
run: |
|
|
||||||
docker tag ghcr.io/${{ github.repository_owner }}/starter-template:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/starter-template:latest
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Push Docker image
|
|
||||||
run: |
|
|
||||||
docker push ghcr.io/${{ github.repository_owner }}/starter-template:latest
|
|
||||||
50
.github/workflows/release-todo-example.yml
vendored
50
.github/workflows/release-todo-example.yml
vendored
|
|
@ -1,50 +0,0 @@
|
||||||
name: Build and Deploy htmgo.dev todo example
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
workflow_dispatch: # Trigger on manual workflow_dispatch
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master # Trigger on pushes to master
|
|
||||||
paths:
|
|
||||||
- 'examples/todo-list/**' # Trigger only if files in this directory change
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get short commit hash
|
|
||||||
id: vars
|
|
||||||
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
run: |
|
|
||||||
cd ./examples/todo-list && docker build -t ghcr.io/${{ github.repository_owner }}/htmgo-todo-example:${{ steps.vars.outputs.short_sha }} .
|
|
||||||
|
|
||||||
- name: Tag as latest Docker image
|
|
||||||
run: |
|
|
||||||
docker tag ghcr.io/${{ github.repository_owner }}/htmgo-todo-example:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/htmgo-todo-example:latest
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Push Docker image
|
|
||||||
run: |
|
|
||||||
docker push ghcr.io/${{ github.repository_owner }}/htmgo-todo-example:latest
|
|
||||||
48
.github/workflows/release-ws-test.yml
vendored
48
.github/workflows/release-ws-test.yml
vendored
|
|
@ -1,48 +0,0 @@
|
||||||
name: Build and Deploy ws-test
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: [ "Update HTMGO Framework Dependency" ] # The name of the first workflow
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- ws-testing
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get short commit hash
|
|
||||||
id: vars
|
|
||||||
run: echo "::set-output name=short_sha::$(echo $GITHUB_SHA | cut -c1-7)"
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
run: |
|
|
||||||
cd ./examples/ws-example && docker build -t ghcr.io/${{ github.repository_owner }}/ws-example:${{ steps.vars.outputs.short_sha }} .
|
|
||||||
|
|
||||||
- name: Tag as latest Docker image
|
|
||||||
run: |
|
|
||||||
docker tag ghcr.io/${{ github.repository_owner }}/ws-example:${{ steps.vars.outputs.short_sha }} ghcr.io/${{ github.repository_owner }}/ws-example:latest
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Push Docker image
|
|
||||||
run: |
|
|
||||||
docker push ghcr.io/${{ github.repository_owner }}/ws-example:latest
|
|
||||||
33
.github/workflows/run-cli-tests.yml
vendored
33
.github/workflows/run-cli-tests.yml
vendored
|
|
@ -1,33 +0,0 @@
|
||||||
name: CLI Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- '**' # Runs on any pull request to any branch
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: '1.23' # Specify the Go version you need
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: cd ./cli/htmgo && go mod download
|
|
||||||
|
|
||||||
- name: Run Go tests
|
|
||||||
run: cd ./cli/htmgo/tasks/astgen && go test ./... -coverprofile=coverage.txt
|
|
||||||
|
|
||||||
- name: Upload results to Codecov
|
|
||||||
uses: codecov/codecov-action@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
33
.github/workflows/run-framework-tests.yml
vendored
33
.github/workflows/run-framework-tests.yml
vendored
|
|
@ -1,33 +0,0 @@
|
||||||
name: Framework Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- '**' # Runs on any pull request to any branch
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: '1.23' # Specify the Go version you need
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: cd ./framework && go mod download
|
|
||||||
|
|
||||||
- name: Run Go tests
|
|
||||||
run: cd ./framework && go test ./... -coverprofile=coverage.txt
|
|
||||||
|
|
||||||
- name: Upload results to Codecov
|
|
||||||
uses: codecov/codecov-action@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
39
.github/workflows/update-framework-dep.yml
vendored
39
.github/workflows/update-framework-dep.yml
vendored
|
|
@ -1,39 +0,0 @@
|
||||||
name: Update HTMGO Framework Dependency
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch: # Trigger on manual workflow_dispatch
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master # Trigger on pushes to master
|
|
||||||
paths:
|
|
||||||
- 'framework/**'
|
|
||||||
- 'tools/html-to-htmgo/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-htmgo-dep:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
persist-credentials: false # Necessary to avoid using the runner's credentials for commit
|
|
||||||
fetch-depth: 0 # Full history for committing back changes
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: ">=1.20"
|
|
||||||
|
|
||||||
- name: Run update-htmgo-dep.go script
|
|
||||||
run: go run tools/update-htmgo-dep.go
|
|
||||||
|
|
||||||
- name: Commit changes
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add .
|
|
||||||
git commit -m "Auto-update HTMGO framework version"
|
|
||||||
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git master
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
65
.github/workflows/verify-installer-works.yml
vendored
65
.github/workflows/verify-installer-works.yml
vendored
|
|
@ -1,65 +0,0 @@
|
||||||
name: Build and Verify Installer Works
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# Step 1: Checkout the repository
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# Step 2: Set up Go
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: '>=1.20'
|
|
||||||
|
|
||||||
# Step 3: Install htmgo CLI
|
|
||||||
- name: Install htmgo CLI
|
|
||||||
run: |
|
|
||||||
GOPRIVATE=github.com/maddalax GOPROXY=direct go install github.com/maddalax/htmgo/cli/htmgo@latest
|
|
||||||
|
|
||||||
# Step 4: Generate template using htmgo
|
|
||||||
- name: Generate myapp template
|
|
||||||
run: |
|
|
||||||
htmgo template myapp
|
|
||||||
|
|
||||||
# Step 5: Build the app
|
|
||||||
- name: Build myapp
|
|
||||||
run: |
|
|
||||||
cd myapp
|
|
||||||
htmgo build
|
|
||||||
|
|
||||||
# Step 6: Verify that the dist directory exists
|
|
||||||
- name: Verify build output
|
|
||||||
run: |
|
|
||||||
if [ ! -d "./myapp/dist" ]; then
|
|
||||||
echo "Build directory ./dist/myapp does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
|
|
||||||
# Step 7: Start the server
|
|
||||||
- name: Start myapp server
|
|
||||||
run: |
|
|
||||||
nohup ./myapp/dist/myapp &
|
|
||||||
|
|
||||||
# Step 8: Wait for server to start
|
|
||||||
- name: Wait for server startup
|
|
||||||
run: sleep 5
|
|
||||||
|
|
||||||
# Step 9: Send curl request to verify the server is running
|
|
||||||
- name: Test server with curl
|
|
||||||
run: |
|
|
||||||
curl --fail http://localhost:3000 || exit 1
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -4,8 +4,3 @@ node_modules/
|
||||||
dist/
|
dist/
|
||||||
js/dist
|
js/dist
|
||||||
js/node_modules
|
js/node_modules
|
||||||
go.work
|
|
||||||
go.work.sum
|
|
||||||
.idea
|
|
||||||
!framework/assets/dist
|
|
||||||
/**/__htmgo
|
|
||||||
5
.idea/.gitignore
vendored
Normal file
5
.idea/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
18
.idea/dataSources.local.xml
Normal file
18
.idea/dataSources.local.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="dataSourceStorageLocal" created-in="GO-242.21829.220">
|
||||||
|
<data-source name="site.db" uuid="6b63d5bd-e451-4904-b659-21db5c54c16d">
|
||||||
|
<database-info product="SQLite" version="3.40.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.40.1.0" dbms="SQLITE" exact-version="3.40.1" exact-driver-version="3.40">
|
||||||
|
<identifier-quote-string>"</identifier-quote-string>
|
||||||
|
</database-info>
|
||||||
|
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
|
||||||
|
<secret-storage>master_key</secret-storage>
|
||||||
|
<auth-provider>no-auth</auth-provider>
|
||||||
|
<schema-mapping>
|
||||||
|
<introspection-scope>
|
||||||
|
<node kind="schema" qname="@" />
|
||||||
|
</introspection-scope>
|
||||||
|
</schema-mapping>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/dataSources.xml
Normal file
12
.idea/dataSources.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="site.db" uuid="6b63d5bd-e451-4904-b659-21db5c54c16d">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/site.db</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1473
.idea/dataSources/6b63d5bd-e451-4904-b659-21db5c54c16d.xml
Normal file
1473
.idea/dataSources/6b63d5bd-e451-4904-b659-21db5c54c16d.xml
Normal file
File diff suppressed because it is too large
Load diff
16
.idea/inspectionProfiles/Project_Default.xml
Normal file
16
.idea/inspectionProfiles/Project_Default.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myValues">
|
||||||
|
<value>
|
||||||
|
<list size="1">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="x:onclick" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="myCustomValuesEnabled" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="XmlUnboundNsPrefix" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
13
.idea/mhtml.iml
Normal file
13
.idea/mhtml.iml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/mhtml.iml" filepath="$PROJECT_DIR$/.idea/mhtml.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
4
.idea/watcherTasks.xml
Normal file
4
.idea/watcherTasks.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectTasksOptions" suppressed-tasks="Pug/Jade" />
|
||||||
|
</project>
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation in our
|
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
|
||||||
and orientation.
|
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
||||||
diverse, inclusive, and healthy community.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment for our
|
|
||||||
community include:
|
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
|
||||||
* Giving and gracefully accepting constructive feedback
|
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
||||||
and learning from the experience
|
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
|
||||||
overall community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
|
||||||
advances of any kind
|
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or email
|
|
||||||
address, without their explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
|
||||||
|
|
||||||
Community leaders are responsible for clarifying and enforcing our standards of
|
|
||||||
acceptable behavior and will take appropriate and fair corrective action in
|
|
||||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
||||||
or harmful.
|
|
||||||
|
|
||||||
Community leaders have the right and responsibility to remove, edit, or reject
|
|
||||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
||||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
||||||
decisions when appropriate.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also applies when
|
|
||||||
an individual is officially representing the community in public spaces.
|
|
||||||
Examples of representing our community include using an official e-mail address,
|
|
||||||
posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported to the community leaders responsible for enforcement at
|
|
||||||
maddox@htmgo.dev.
|
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
|
||||||
reporter of any incident.
|
|
||||||
|
|
||||||
## Enforcement Guidelines
|
|
||||||
|
|
||||||
Community leaders will follow these Community Impact Guidelines in determining
|
|
||||||
the consequences for any action they deem in violation of this Code of Conduct:
|
|
||||||
|
|
||||||
### 1. Correction
|
|
||||||
|
|
||||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
||||||
unprofessional or unwelcome in the community.
|
|
||||||
|
|
||||||
**Consequence**: A private, written warning from community leaders, providing
|
|
||||||
clarity around the nature of the violation and an explanation of why the
|
|
||||||
behavior was inappropriate. A public apology may be requested.
|
|
||||||
|
|
||||||
### 2. Warning
|
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
|
||||||
of actions.
|
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
|
||||||
interaction with the people involved, including unsolicited interaction with
|
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
|
||||||
like social media. Violating these terms may lead to a temporary or
|
|
||||||
permanent ban.
|
|
||||||
|
|
||||||
### 3. Temporary Ban
|
|
||||||
|
|
||||||
**Community Impact**: A serious violation of community standards, including
|
|
||||||
sustained inappropriate behavior.
|
|
||||||
|
|
||||||
**Consequence**: A temporary ban from any sort of interaction or public
|
|
||||||
communication with the community for a specified period of time. No public or
|
|
||||||
private interaction with the people involved, including unsolicited interaction
|
|
||||||
with those enforcing the Code of Conduct, is allowed during this period.
|
|
||||||
Violating these terms may lead to a permanent ban.
|
|
||||||
|
|
||||||
### 4. Permanent Ban
|
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
|
||||||
the community.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
||||||
version 2.0, available at
|
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
|
||||||
enforcement ladder](https://github.com/mozilla/diversity).
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
|
||||||
https://www.contributor-covenant.org/faq. Translations are available at
|
|
||||||
https://www.contributor-covenant.org/translations.
|
|
||||||
21
LICENSE
21
LICENSE
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024 maddalax
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
50
README.md
50
README.md
|
|
@ -1,50 +0,0 @@
|
||||||
## **htmgo**
|
|
||||||
|
|
||||||
### build simple and scalable systems with go + htmx
|
|
||||||
|
|
||||||
-------
|
|
||||||
[](https://goreportcard.com/report/github.com/maddalax/htmgo)
|
|
||||||

|
|
||||||
[](https://htmgo.dev/docs)
|
|
||||||
[](https://codecov.io/github/maddalax/htmgo)
|
|
||||||
[](https://htmgo.dev/discord)
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<sup>looking for a python version? check out: https://fastht.ml</sup>
|
|
||||||
|
|
||||||
**introduction:**
|
|
||||||
|
|
||||||
htmgo is a lightweight pure go way to build interactive websites / web applications using go & htmx.
|
|
||||||
|
|
||||||
By combining the speed & simplicity of go + hypermedia attributes ([htmx](https://htmx.org)) to add interactivity to websites, all conveniently wrapped in pure go, you can build simple, fast, interactive websites without touching javascript. All compiled to a **single deployable binary**.
|
|
||||||
|
|
||||||
```go
|
|
||||||
func IndexPage(ctx *h.RequestContext) *h.Page {
|
|
||||||
now := time.Now()
|
|
||||||
return h.NewPage(
|
|
||||||
h.Div(
|
|
||||||
h.Class("flex gap-2"),
|
|
||||||
h.TextF("the current time is %s", now.String())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**core features:**
|
|
||||||
|
|
||||||
1. deployable single binary
|
|
||||||
2. live reload (rebuilds css, go, ent schema, and routes upon change)
|
|
||||||
3. automatic page and partial registration based on file path
|
|
||||||
4. built in tailwindcss support, no need to configure anything by default
|
|
||||||
5. custom [htmx extensions](https://github.com/maddalax/htmgo/tree/b610aefa36e648b98a13823a6f8d87566120cfcc/framework/assets/js/htmxextensions) to reduce boilerplate with common tasks
|
|
||||||
|
|
||||||
**get started:**
|
|
||||||
|
|
||||||
View documentation on [htmgo.dev](https://htmgo.dev/docs).
|
|
||||||
|
|
||||||
## Star History
|
|
||||||
|
|
||||||
[](https://star-history.com/#maddalax/htmgo&Date)
|
|
||||||
14
assets/css/tailwind.config.js
Normal file
14
assets/css/tailwind.config.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
const {join} = require("node:path");
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
const root = join(__dirname, "../../");
|
||||||
|
const content = join(root, "**/*.go");
|
||||||
|
|
||||||
|
console.log(content)
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
content: [content],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
|
@ -4,9 +4,9 @@ htmx.defineExtension("debug", {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
onEvent: function (name, evt) {
|
onEvent: function (name, evt) {
|
||||||
if (console.debug) {
|
if (console.debug) {
|
||||||
console.debug(name, evt);
|
console.debug(name);
|
||||||
} else if (console) {
|
} else if (console) {
|
||||||
console.log("DEBUG:", name, evt);
|
console.log("DEBUG:", name);
|
||||||
} else {
|
} else {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
@ -12,10 +12,8 @@ htmx.defineExtension("mutation-error", {
|
||||||
}
|
}
|
||||||
const status = evt.detail.xhr.status;
|
const status = evt.detail.xhr.status;
|
||||||
if (status >= 400) {
|
if (status >= 400) {
|
||||||
document.querySelectorAll("*").forEach((element) => {
|
htmx.findAll("[hx-on\\:\\:mutation-error]").forEach((element) => {
|
||||||
if (element.hasAttribute("hx-on::on-mutation-error")) {
|
htmx.trigger(element, "htmx:mutation-error", { status });
|
||||||
htmx.trigger(element, "htmx:on-mutation-error", { status });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
43
assets/js/extensions/trigger-children.ts
Normal file
43
assets/js/extensions/trigger-children.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import htmx, { HtmxSettleInfo, HtmxSwapStyle } from "htmx.org";
|
||||||
|
|
||||||
|
htmx.defineExtension("trigger-children", {
|
||||||
|
onEvent: (name, evt: Event | CustomEvent) => {
|
||||||
|
if (!(evt instanceof CustomEvent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const target = evt.detail.target as HTMLElement;
|
||||||
|
if (target && target.children) {
|
||||||
|
Array.from(target.children).forEach((e) => {
|
||||||
|
htmx.trigger(e, name, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
init: function (api: any): void {},
|
||||||
|
transformResponse: function (
|
||||||
|
text: string,
|
||||||
|
xhr: XMLHttpRequest,
|
||||||
|
elt: Element,
|
||||||
|
): string {
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
isInlineSwap: function (swapStyle: HtmxSwapStyle): boolean {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
handleSwap: function (
|
||||||
|
swapStyle: HtmxSwapStyle,
|
||||||
|
target: Node,
|
||||||
|
fragment: Node,
|
||||||
|
settleInfo: HtmxSettleInfo,
|
||||||
|
): boolean | Node[] {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
encodeParameters: function (
|
||||||
|
xhr: XMLHttpRequest,
|
||||||
|
parameters: FormData,
|
||||||
|
elt: Node,
|
||||||
|
) {},
|
||||||
|
getSelectors: function (): string[] | null {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -1,17 +1,9 @@
|
||||||
import htmx from "htmx.org";
|
import htmx from "htmx.org";
|
||||||
import "./htmxextensions/pathdeps";
|
import "./extensions/pathdeps";
|
||||||
import "./htmxextensions/trigger-children";
|
import "./extensions/trigger-children";
|
||||||
import "./htmxextensions/debug";
|
import "./extensions/debug";
|
||||||
import "./htmxextensions/response-targets";
|
import "./extensions/response-targets";
|
||||||
import "./htmxextensions/mutation-error";
|
import "./extensions/mutation-error";
|
||||||
import "./htmxextensions/livereload"
|
|
||||||
import "./htmxextensions/htmgo";
|
|
||||||
import "./htmxextensions/sse"
|
|
||||||
import "./htmxextensions/ws"
|
|
||||||
import "./htmxextensions/ws-event-handler"
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
window.htmx = htmx;
|
|
||||||
|
|
||||||
function watchUrl(callback: (oldUrl: string, newUrl: string) => void) {
|
function watchUrl(callback: (oldUrl: string, newUrl: string) => void) {
|
||||||
let lastUrl = window.location.href;
|
let lastUrl = window.location.href;
|
||||||
|
|
@ -20,7 +12,7 @@ function watchUrl(callback: (oldUrl: string, newUrl: string) => void) {
|
||||||
callback(lastUrl, window.location.href);
|
callback(lastUrl, window.location.href);
|
||||||
lastUrl = window.location.href;
|
lastUrl = window.location.href;
|
||||||
}
|
}
|
||||||
}, 101);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
watchUrl((_, newUrl) => {
|
watchUrl((_, newUrl) => {
|
||||||
|
|
@ -46,6 +38,7 @@ function onUrlChange(newUrl: string) {
|
||||||
for (let [key, values] of url.searchParams) {
|
for (let [key, values] of url.searchParams) {
|
||||||
let eventName = "qs:" + key;
|
let eventName = "qs:" + key;
|
||||||
if (triggers.includes(eventName)) {
|
if (triggers.includes(eventName)) {
|
||||||
|
console.log("triggering", eventName);
|
||||||
htmx.trigger(element, eventName, null);
|
htmx.trigger(element, eventName, null);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -82,17 +75,3 @@ function onUrlChange(newUrl: string) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
400s should allow swapping by default, as it's useful to show error messages
|
|
||||||
*/
|
|
||||||
document.addEventListener('htmx:beforeSwap', function(evt) {
|
|
||||||
if(evt instanceof CustomEvent) {
|
|
||||||
// Allow 422 and 400 responses to swap
|
|
||||||
// We treat these as form validation errors
|
|
||||||
if (evt.detail.xhr.status === 422 || evt.detail.xhr.status === 400) {
|
|
||||||
evt.detail.shouldSwap = true;
|
|
||||||
evt.detail.isError = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "htmgo-js",
|
"name": "mhtml-js",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "htmgo-js",
|
"name": "mhtml-js",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
"@swc/core": "^1.7.26",
|
"@swc/core": "^1.7.26",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.4",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"shiki": "^1.17.6",
|
|
||||||
"tailwindcss": "^3.4.11",
|
"tailwindcss": "^3.4.11",
|
||||||
"tsup": "^8.2.4",
|
"tsup": "^8.2.4",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
|
|
@ -735,56 +734,6 @@
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/core": {
|
|
||||||
"version": "1.17.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.17.6.tgz",
|
|
||||||
"integrity": "sha512-9ztslig6/YmCg/XwESAXbKjAjOhaq6HVced9NY6qcbDz1X5g/S90Wco2vMjBNX/6V71ASkzri76JewSGPa7kiQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@shikijs/engine-javascript": "1.17.6",
|
|
||||||
"@shikijs/engine-oniguruma": "1.17.6",
|
|
||||||
"@shikijs/types": "1.17.6",
|
|
||||||
"@shikijs/vscode-textmate": "^9.2.2",
|
|
||||||
"@types/hast": "^3.0.4",
|
|
||||||
"hast-util-to-html": "^9.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@shikijs/engine-javascript": {
|
|
||||||
"version": "1.17.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.17.6.tgz",
|
|
||||||
"integrity": "sha512-5EEZj8tVcierNxm4V0UMS2PVoflb0UJPalWWV8l9rRg+oOfnr5VivqBJbkyq5grltVPvByIXvVbY8GSM/356jQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@shikijs/types": "1.17.6",
|
|
||||||
"oniguruma-to-js": "0.4.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@shikijs/engine-oniguruma": {
|
|
||||||
"version": "1.17.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.17.6.tgz",
|
|
||||||
"integrity": "sha512-NLfWDMXFYe0nDHFbEoyZdz89aIIey3bTfF3zLYSUNTXks5s4uinZVmuPOFf1HfTeGqIn8uErJSBc3VnpJO7Alw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@shikijs/types": "1.17.6",
|
|
||||||
"@shikijs/vscode-textmate": "^9.2.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@shikijs/types": {
|
|
||||||
"version": "1.17.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.17.6.tgz",
|
|
||||||
"integrity": "sha512-ndTFa2TJi2w51ddKQDn3Jy8f6K4E5Q2x3dA3Hmsd3+YmxDQ10UWHjcw7VbVbKzv3VcUvYPLy+z9neqytSzUMUg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@shikijs/vscode-textmate": "^9.2.2",
|
|
||||||
"@types/hast": "^3.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@shikijs/vscode-textmate": {
|
|
||||||
"version": "9.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.2.2.tgz",
|
|
||||||
"integrity": "sha512-TMp15K+GGYrWlZM8+Lnj9EaHEFmOen0WJBrfa17hF7taDOYthuPPV0GWzfd/9iMij0akS/8Yw2ikquH7uVi/fg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@swc/core": {
|
"node_modules/@swc/core": {
|
||||||
"version": "1.7.26",
|
"version": "1.7.26",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz",
|
||||||
|
|
@ -1004,24 +953,6 @@
|
||||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/hast": {
|
|
||||||
"version": "3.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
|
||||||
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/mdast": {
|
|
||||||
"version": "4.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
|
||||||
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.5.4",
|
"version": "22.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
|
||||||
|
|
@ -1031,18 +962,6 @@
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/unist": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@ungap/structured-clone": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||||
|
|
@ -1173,36 +1092,6 @@
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ccount": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/character-entities-html4": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/character-entities-legacy": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
|
@ -1245,16 +1134,6 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/comma-separated-tokens": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
|
|
@ -1316,28 +1195,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dequal": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/devlop": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"dequal": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
|
|
@ -1600,52 +1457,6 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hast-util-to-html": {
|
|
||||||
"version": "9.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.2.tgz",
|
|
||||||
"integrity": "sha512-RP5wNpj5nm1Z8cloDv4Sl4RS8jH5HYa0v93YB6Wb4poEzgMo/dAAL0KcT4974dCjcNG5pkLqTImeFHHCwwfY3g==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"@types/unist": "^3.0.0",
|
|
||||||
"ccount": "^2.0.0",
|
|
||||||
"comma-separated-tokens": "^2.0.0",
|
|
||||||
"hast-util-whitespace": "^3.0.0",
|
|
||||||
"html-void-elements": "^3.0.0",
|
|
||||||
"mdast-util-to-hast": "^13.0.0",
|
|
||||||
"property-information": "^6.0.0",
|
|
||||||
"space-separated-tokens": "^2.0.0",
|
|
||||||
"stringify-entities": "^4.0.0",
|
|
||||||
"zwitch": "^2.0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hast-util-whitespace": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/html-void-elements": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/htmx.org": {
|
"node_modules/htmx.org": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.2.tgz",
|
||||||
|
|
@ -1825,27 +1636,6 @@
|
||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/mdast-util-to-hast": {
|
|
||||||
"version": "13.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
|
|
||||||
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"@types/mdast": "^4.0.0",
|
|
||||||
"@ungap/structured-clone": "^1.0.0",
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"micromark-util-sanitize-uri": "^2.0.0",
|
|
||||||
"trim-lines": "^3.0.0",
|
|
||||||
"unist-util-position": "^5.0.0",
|
|
||||||
"unist-util-visit": "^5.0.0",
|
|
||||||
"vfile": "^6.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/merge-stream": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
|
|
@ -1861,95 +1651,6 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromark-util-character": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "GitHub Sponsors",
|
|
||||||
"url": "https://github.com/sponsors/unifiedjs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "OpenCollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"micromark-util-symbol": "^2.0.0",
|
|
||||||
"micromark-util-types": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/micromark-util-encode": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "GitHub Sponsors",
|
|
||||||
"url": "https://github.com/sponsors/unifiedjs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "OpenCollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/micromark-util-sanitize-uri": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "GitHub Sponsors",
|
|
||||||
"url": "https://github.com/sponsors/unifiedjs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "OpenCollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"micromark-util-character": "^2.0.0",
|
|
||||||
"micromark-util-encode": "^2.0.0",
|
|
||||||
"micromark-util-symbol": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/micromark-util-symbol": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "GitHub Sponsors",
|
|
||||||
"url": "https://github.com/sponsors/unifiedjs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "OpenCollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/micromark-util-types": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "GitHub Sponsors",
|
|
||||||
"url": "https://github.com/sponsors/unifiedjs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "OpenCollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
|
|
@ -2085,18 +1786,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/oniguruma-to-js": {
|
|
||||||
"version": "0.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz",
|
|
||||||
"integrity": "sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"regex": "^4.3.2"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/antfu"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/package-json-from-dist": {
|
"node_modules/package-json-from-dist": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
|
||||||
|
|
@ -2344,16 +2033,6 @@
|
||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/property-information": {
|
|
||||||
"version": "6.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
|
|
||||||
"integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
@ -2404,12 +2083,6 @@
|
||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/regex": {
|
|
||||||
"version": "4.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/regex/-/regex-4.3.2.tgz",
|
|
||||||
"integrity": "sha512-kK/AA3A9K6q2js89+VMymcboLOlF5lZRCYJv3gzszXFHBr6kO6qLGzbm+UIugBEV8SMMKCTR59txoY6ctRHYVw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.8",
|
"version": "1.22.8",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||||
|
|
@ -2525,20 +2198,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/shiki": {
|
|
||||||
"version": "1.17.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.17.6.tgz",
|
|
||||||
"integrity": "sha512-RejGugKpDM75vh6YtF9R771acxHRDikC/01kxsUGW+Pnaz3pTY+c8aZB5CnD7p0vuFPs1HaoAIU/4E+NCfS+mQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@shikijs/core": "1.17.6",
|
|
||||||
"@shikijs/engine-javascript": "1.17.6",
|
|
||||||
"@shikijs/engine-oniguruma": "1.17.6",
|
|
||||||
"@shikijs/types": "1.17.6",
|
|
||||||
"@shikijs/vscode-textmate": "^9.2.2",
|
|
||||||
"@types/hast": "^3.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
|
|
@ -2575,16 +2234,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/space-separated-tokens": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
|
|
@ -2644,20 +2293,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stringify-entities": {
|
|
||||||
"version": "4.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
|
||||||
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"character-entities-html4": "^2.0.0",
|
|
||||||
"character-entities-legacy": "^3.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||||
|
|
@ -2894,16 +2529,6 @@
|
||||||
"tree-kill": "cli.js"
|
"tree-kill": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/trim-lines": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ts-interface-checker": {
|
"node_modules/ts-interface-checker": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||||
|
|
@ -2980,108 +2605,12 @@
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/unist-util-is": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "^3.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unist-util-position": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "^3.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unist-util-stringify-position": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "^3.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unist-util-visit": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "^3.0.0",
|
|
||||||
"unist-util-is": "^6.0.0",
|
|
||||||
"unist-util-visit-parents": "^6.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unist-util-visit-parents": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "^3.0.0",
|
|
||||||
"unist-util-is": "^6.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/vfile": {
|
|
||||||
"version": "6.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
|
||||||
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "^3.0.0",
|
|
||||||
"vfile-message": "^4.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vfile-message": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "^3.0.0",
|
|
||||||
"unist-util-stringify-position": "^4.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
||||||
|
|
@ -3216,16 +2745,6 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/zwitch": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "htmgo-js",
|
"name": "mhtml-js",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "htmgo.js",
|
"main": "mhtml.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "tsup --watch --config ./tsup.config.ts --sourcemap inline",
|
"watch": "tsup ./mhtml.ts --watch --config ./tsup.config.ts --sourcemap inline",
|
||||||
"build": "tsup --minify --config ./tsup.config.ts",
|
"build": "tsup ./mhtml.ts --minify --config ./tsup.config.ts",
|
||||||
"tailwind:watch": "npx tailwindcss -i ./input.css -o ./output.css --watch",
|
"tailwind:watch": "npx tailwindcss -i ./input.css -o ./output.css --watch",
|
||||||
"pretty": "prettier --write ."
|
"pretty": "prettier --write ."
|
||||||
},
|
},
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
"@swc/core": "^1.7.26",
|
"@swc/core": "^1.7.26",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.4",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"shiki": "^1.17.6",
|
|
||||||
"tailwindcss": "^3.4.11",
|
"tailwindcss": "^3.4.11",
|
||||||
"tsup": "^8.2.4",
|
"tsup": "^8.2.4",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
|
|
@ -2,23 +2,23 @@ import { defineConfig } from "tsup";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
format: ["esm"],
|
format: ["esm"],
|
||||||
entry: ["htmgo.ts"],
|
entry: ["./src/mhtml.ts"],
|
||||||
outDir: "./../dist",
|
outDir: "./../dist",
|
||||||
dts: false,
|
dts: false,
|
||||||
shims: true,
|
shims: true,
|
||||||
skipNodeModulesBundle: true,
|
skipNodeModulesBundle: true,
|
||||||
clean: false,
|
clean: false,
|
||||||
target: "esnext",
|
target: "esnext",
|
||||||
treeshake: true,
|
treeshake: false,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
outExtension: () => {
|
outExtension: () => {
|
||||||
return {
|
return {
|
||||||
js: ".js",
|
js: ".js",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
minify: true,
|
minify: false,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
splitting: true,
|
|
||||||
// https://github.com/egoist/tsup/issues/619
|
// https://github.com/egoist/tsup/issues/619
|
||||||
noExternal: [/(.*)/],
|
noExternal: [/(.*)/],
|
||||||
|
splitting: false,
|
||||||
});
|
});
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
module github.com/maddalax/htmgo/cli/htmgo
|
|
||||||
|
|
||||||
go 1.23.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
|
||||||
github.com/google/uuid v1.6.0
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
|
||||||
github.com/maddalax/htmgo/tools/html-to-htmgo v0.0.0-20250703190716-06f01b3d7c1b
|
|
||||||
github.com/stretchr/testify v1.9.0
|
|
||||||
golang.org/x/mod v0.21.0
|
|
||||||
golang.org/x/sys v0.26.0
|
|
||||||
golang.org/x/tools v0.25.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.7.1
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
|
||||||
golang.org/x/net v0.30.0 // indirect
|
|
||||||
golang.org/x/text v0.19.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
|
||||||
github.com/maddalax/htmgo/tools/html-to-htmgo v0.0.0-20250703190716-06f01b3d7c1b h1:jvfp35fig2TzBjAgw82fe8+7cvaLX9EbipZUlj8FDDY=
|
|
||||||
github.com/maddalax/htmgo/tools/html-to-htmgo v0.0.0-20250703190716-06f01b3d7c1b/go.mod h1:FraJsj3NRuLBQDk83ZVa+psbNRNLe+rajVtVhYMEme4=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
|
||||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
|
||||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
|
||||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
|
||||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
|
||||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Debouncer is a struct that holds the debounce logic
|
|
||||||
type Debouncer struct {
|
|
||||||
delay time.Duration
|
|
||||||
timer *time.Timer
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDebouncer creates a new Debouncer with the specified delay
|
|
||||||
func NewDebouncer(delay time.Duration) *Debouncer {
|
|
||||||
return &Debouncer{
|
|
||||||
delay: delay,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do calls the provided function after the delay, resetting the delay if called again
|
|
||||||
func (d *Debouncer) Do(f func()) {
|
|
||||||
d.mu.Lock()
|
|
||||||
defer d.mu.Unlock()
|
|
||||||
|
|
||||||
// If there's an existing timer, stop it
|
|
||||||
if d.timer != nil {
|
|
||||||
d.timer.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new timer
|
|
||||||
d.timer = time.AfterFunc(d.delay, f)
|
|
||||||
}
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
package dirutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"github.com/maddalax/htmgo/framework/config"
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HasFileFromRoot(file string) bool {
|
|
||||||
cwd := process.GetWorkingDir()
|
|
||||||
path := filepath.Join(cwd, file)
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetConfig() *config.ProjectConfig {
|
|
||||||
return config.FromConfigFile(process.GetWorkingDir())
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateHtmgoDir() {
|
|
||||||
if !HasFileFromRoot("__htmgo") {
|
|
||||||
CreateDirFromRoot("__htmgo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateDirFromRoot(dir string) error {
|
|
||||||
cwd := process.GetWorkingDir()
|
|
||||||
path := filepath.Join(cwd, dir)
|
|
||||||
return os.MkdirAll(path, 0700)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyDir(srcDir, dstDir string, predicate func(path string, exists bool) bool) error {
|
|
||||||
// Walk the source directory tree.
|
|
||||||
return filepath.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Construct the corresponding destination path.
|
|
||||||
relPath, err := filepath.Rel(srcDir, srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dstPath := filepath.Join(dstDir, relPath)
|
|
||||||
if info.IsDir() {
|
|
||||||
// If it's a directory, create the corresponding directory in the destination.
|
|
||||||
err := os.MkdirAll(dstPath, 0700)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create directory: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
exists := true
|
|
||||||
if _, err := os.Stat(dstPath); errors.Is(err, os.ErrNotExist) {
|
|
||||||
exists = false
|
|
||||||
}
|
|
||||||
if predicate(srcPath, exists) {
|
|
||||||
err := CopyFile(srcPath, dstPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If it's a file, copy the file.
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func MoveFile(src, dst string) error {
|
|
||||||
slog.Debug("moving file", slog.String("src", src), slog.String("dst", dst))
|
|
||||||
// Copy the file.
|
|
||||||
err := CopyFile(src, dst)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to copy file: %v", err)
|
|
||||||
}
|
|
||||||
// Remove the source file.
|
|
||||||
err = os.Remove(src)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to remove source file: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyFile(src, dst string) error {
|
|
||||||
slog.Debug("copying file", slog.String("src", src), slog.String("dst", dst))
|
|
||||||
// Open the source file for reading.
|
|
||||||
srcFile, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open source file: %v", err)
|
|
||||||
}
|
|
||||||
defer srcFile.Close()
|
|
||||||
// Create the destination file.
|
|
||||||
dstFile, err := os.Create(dst)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create destination file: %v", err)
|
|
||||||
}
|
|
||||||
defer dstFile.Close()
|
|
||||||
// Copy the content from srcFile to dstFile.
|
|
||||||
_, err = io.Copy(dstFile, srcFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to copy file contents: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteDir(dir string) error {
|
|
||||||
return os.RemoveAll(dir)
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
package dirutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/bmatcuk/doublestar/v4"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func matchesAny(patterns []string, path string) bool {
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
matched, err :=
|
|
||||||
doublestar.Match(strings.ReplaceAll(pattern, `\`, "/"), strings.ReplaceAll(path, `\`, "/"))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error matching pattern: %v\n", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if matched {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsGlobExclude(path string, excludePatterns []string) bool {
|
|
||||||
return matchesAny(excludePatterns, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsGlobMatch(path string, patterns []string, excludePatterns []string) bool {
|
|
||||||
if matchesAny(excludePatterns, path) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return matchesAny(patterns, path)
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetLogLevel() slog.Level {
|
|
||||||
// Get the log level from the environment variable
|
|
||||||
logLevel := os.Getenv("LOG_LEVEL")
|
|
||||||
switch strings.ToUpper(logLevel) {
|
|
||||||
case "DEBUG":
|
|
||||||
return slog.LevelDebug
|
|
||||||
case "INFO":
|
|
||||||
return slog.LevelInfo
|
|
||||||
case "WARN":
|
|
||||||
return slog.LevelWarn
|
|
||||||
case "ERROR":
|
|
||||||
return slog.LevelError
|
|
||||||
default:
|
|
||||||
// Default to INFO if no valid log level is set
|
|
||||||
return slog.LevelInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/astgen"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/css"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/downloadtemplate"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/formatter"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/reloader"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/run"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const version = "1.0.6"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
needsSignals := true
|
|
||||||
|
|
||||||
commandMap := make(map[string]*flag.FlagSet)
|
|
||||||
commands := []string{"template", "run", "watch", "build", "setup", "css", "schema", "generate", "format", "version"}
|
|
||||||
|
|
||||||
for _, command := range commands {
|
|
||||||
commandMap[command] = flag.NewFlagSet(command, flag.ExitOnError)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(os.Args) < 2 {
|
|
||||||
fmt.Println(fmt.Sprintf("Usage: htmgo [%s]", strings.Join(commands, " | ")))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := commandMap[os.Args[1]]
|
|
||||||
|
|
||||||
if c == nil {
|
|
||||||
fmt.Println(fmt.Sprintf("Usage: htmgo [%s]", strings.Join(commands, " | ")))
|
|
||||||
os.Exit(1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := c.Parse(os.Args[2:])
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.SetLogLoggerLevel(internal.GetLogLevel())
|
|
||||||
|
|
||||||
taskName := os.Args[1]
|
|
||||||
|
|
||||||
slog.Debug("Running task:", slog.String("task", taskName))
|
|
||||||
slog.Debug("working dir:", slog.String("dir", process.GetWorkingDir()))
|
|
||||||
|
|
||||||
if taskName == "format" {
|
|
||||||
needsSignals = false
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan bool, 1)
|
|
||||||
if needsSignals {
|
|
||||||
done = RegisterSignals()
|
|
||||||
}
|
|
||||||
|
|
||||||
if taskName == "watch" {
|
|
||||||
fmt.Printf("Running in watch mode\n")
|
|
||||||
os.Setenv("ENV", "development")
|
|
||||||
os.Setenv("WATCH_MODE", "true")
|
|
||||||
fmt.Printf("Starting processes...\n")
|
|
||||||
|
|
||||||
copyassets.CopyAssets()
|
|
||||||
|
|
||||||
fmt.Printf("Generating CSS...\n")
|
|
||||||
css.GenerateCss(process.ExitOnError)
|
|
||||||
|
|
||||||
// generate ast needs to be run after css generation
|
|
||||||
astgen.GenAst(process.ExitOnError)
|
|
||||||
run.EntGenerate()
|
|
||||||
|
|
||||||
fmt.Printf("Starting server...\n")
|
|
||||||
process.KillAll()
|
|
||||||
go func() {
|
|
||||||
_ = run.Server()
|
|
||||||
}()
|
|
||||||
startWatcher(reloader.OnFileChange)
|
|
||||||
} else {
|
|
||||||
if taskName == "version" {
|
|
||||||
fmt.Printf("htmgo cli version %s\n", version)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
if taskName == "format" {
|
|
||||||
if len(os.Args) < 3 {
|
|
||||||
fmt.Println(fmt.Sprintf("Usage: htmgo format <file>"))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
file := os.Args[2]
|
|
||||||
if file == "." {
|
|
||||||
formatter.FormatDir(process.GetWorkingDir())
|
|
||||||
} else {
|
|
||||||
formatter.FormatFile(os.Args[2])
|
|
||||||
}
|
|
||||||
} else if taskName == "schema" {
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
fmt.Print("Enter entity name:")
|
|
||||||
text, _ := reader.ReadString('\n')
|
|
||||||
text = strings.TrimSuffix(text, "\n")
|
|
||||||
run.EntNewSchema(text)
|
|
||||||
} else if taskName == "generate" {
|
|
||||||
run.EntGenerate()
|
|
||||||
astgen.GenAst(process.ExitOnError)
|
|
||||||
} else if taskName == "setup" {
|
|
||||||
run.Setup()
|
|
||||||
} else if taskName == "css" {
|
|
||||||
_ = css.GenerateCss(process.ExitOnError)
|
|
||||||
} else if taskName == "ast" {
|
|
||||||
css.GenerateCss(process.ExitOnError)
|
|
||||||
_ = astgen.GenAst(process.ExitOnError)
|
|
||||||
} else if taskName == "run" {
|
|
||||||
run.MakeBuildable()
|
|
||||||
_ = run.Server(process.ExitOnError)
|
|
||||||
} else if taskName == "template" {
|
|
||||||
name := ""
|
|
||||||
if len(os.Args) > 2 {
|
|
||||||
name = os.Args[2]
|
|
||||||
} else {
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
fmt.Print("What would you like to call your new app?: ")
|
|
||||||
name, _ = reader.ReadString('\n')
|
|
||||||
}
|
|
||||||
name = strings.TrimSuffix(name, "\n")
|
|
||||||
name = strings.ReplaceAll(name, " ", "-")
|
|
||||||
name = strings.ToLower(name)
|
|
||||||
downloadtemplate.DownloadTemplate(fmt.Sprintf("./%s", name))
|
|
||||||
} else if taskName == "build" {
|
|
||||||
run.Build()
|
|
||||||
} else if taskName == "generate" {
|
|
||||||
astgen.GenAst(process.ExitOnError)
|
|
||||||
} else {
|
|
||||||
fmt.Println(fmt.Sprintf("Usage: htmgo [%s]", strings.Join(commands, " | ")))
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RegisterSignals() chan bool {
|
|
||||||
// Create a channel to receive OS signals
|
|
||||||
sigs := make(chan os.Signal, 1)
|
|
||||||
// Register the channel to receive interrupt and terminate signals
|
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
|
|
||||||
done := make(chan bool, 1)
|
|
||||||
// Run a goroutine to handle signals
|
|
||||||
go func() {
|
|
||||||
// Block until a signal is received
|
|
||||||
sig := <-sigs
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Received signal:", sig)
|
|
||||||
// Perform cleanup
|
|
||||||
fmt.Println("Cleaning up...")
|
|
||||||
|
|
||||||
process.OnShutdown()
|
|
||||||
// Signal that cleanup is done
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
return done
|
|
||||||
}
|
|
||||||
|
|
@ -1,555 +0,0 @@
|
||||||
package astgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
|
||||||
"go/parser"
|
|
||||||
"go/token"
|
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"golang.org/x/mod/modfile"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Page struct {
|
|
||||||
Path string
|
|
||||||
FuncName string
|
|
||||||
Package string
|
|
||||||
Import string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Partial struct {
|
|
||||||
FuncName string
|
|
||||||
Package string
|
|
||||||
Import string
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
const GeneratedDirName = "__htmgo"
|
|
||||||
const HttpModuleName = "net/http"
|
|
||||||
const ChiModuleName = "github.com/go-chi/chi/v5"
|
|
||||||
const ModuleName = "github.com/maddalax/htmgo/framework/h"
|
|
||||||
|
|
||||||
var PackageName = fmt.Sprintf("package %s", GeneratedDirName)
|
|
||||||
var GeneratedFileLine = fmt.Sprintf("// Package %s THIS FILE IS GENERATED. DO NOT EDIT.", GeneratedDirName)
|
|
||||||
|
|
||||||
func toPascaleCase(input string) string {
|
|
||||||
words := strings.Split(input, "_")
|
|
||||||
for i := range words {
|
|
||||||
words[i] = strings.Title(strings.ToLower(words[i]))
|
|
||||||
}
|
|
||||||
return strings.Join(words, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidGoVariableName(name string) bool {
|
|
||||||
// Variable name must not be empty
|
|
||||||
if name == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// First character must be a letter or underscore
|
|
||||||
if !unicode.IsLetter(rune(name[0])) && name[0] != '_' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Remaining characters must be letters, digits, or underscores
|
|
||||||
for _, char := range name[1:] {
|
|
||||||
if !unicode.IsLetter(char) && !unicode.IsDigit(char) && char != '_' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePath(path string) string {
|
|
||||||
return strings.ReplaceAll(path, `\`, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sliceCommonPrefix(dir1, dir2 string) string {
|
|
||||||
// Use filepath.Clean to normalize the paths
|
|
||||||
dir1 = filepath.Clean(dir1)
|
|
||||||
dir2 = filepath.Clean(dir2)
|
|
||||||
|
|
||||||
// Find the common prefix
|
|
||||||
commonPrefix := dir1
|
|
||||||
if len(dir1) > len(dir2) {
|
|
||||||
commonPrefix = dir2
|
|
||||||
}
|
|
||||||
|
|
||||||
for !strings.HasPrefix(dir1, commonPrefix) {
|
|
||||||
commonPrefix = filepath.Dir(commonPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slice off the common prefix
|
|
||||||
slicedDir1 := strings.TrimPrefix(dir1, commonPrefix)
|
|
||||||
slicedDir2 := strings.TrimPrefix(dir2, commonPrefix)
|
|
||||||
|
|
||||||
// Remove leading slashes
|
|
||||||
slicedDir1 = strings.TrimPrefix(slicedDir1, string(filepath.Separator))
|
|
||||||
slicedDir2 = strings.TrimPrefix(slicedDir2, string(filepath.Separator))
|
|
||||||
|
|
||||||
// Return the longer one
|
|
||||||
if len(slicedDir1) > len(slicedDir2) {
|
|
||||||
return normalizePath(slicedDir1)
|
|
||||||
}
|
|
||||||
return normalizePath(slicedDir2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasOnlyReqContextParam(funcType *ast.FuncType) bool {
|
|
||||||
if len(funcType.Params.List) != 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if funcType.Params.List[0].Names == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(funcType.Params.List[0].Names) != 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
t := funcType.Params.List[0].Type
|
|
||||||
name, ok := t.(*ast.StarExpr)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
selectorExpr, ok := name.X.(*ast.SelectorExpr)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ident, ok := selectorExpr.X.(*ast.Ident)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return ident.Name == "h" && selectorExpr.Sel.Name == "RequestContext"
|
|
||||||
}
|
|
||||||
|
|
||||||
func findPublicFuncsReturningHPartial(dir string, predicate func(partial Partial) bool) ([]Partial, error) {
|
|
||||||
var partials []Partial
|
|
||||||
cwd := process.GetWorkingDir()
|
|
||||||
|
|
||||||
// Walk through the directory to find all Go files.
|
|
||||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only process Go files.
|
|
||||||
if !strings.HasSuffix(path, ".go") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the Go file.
|
|
||||||
fset := token.NewFileSet()
|
|
||||||
node, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inspect the AST for function declarations.
|
|
||||||
ast.Inspect(node, func(n ast.Node) bool {
|
|
||||||
// Check if the node is a function declaration.
|
|
||||||
if funcDecl, ok := n.(*ast.FuncDecl); ok {
|
|
||||||
// Only consider exported (public) partials.
|
|
||||||
if funcDecl.Name.IsExported() {
|
|
||||||
// Check the return type.
|
|
||||||
if funcDecl.Type.Results != nil {
|
|
||||||
for _, result := range funcDecl.Type.Results.List {
|
|
||||||
// Check if the return type is *h.Partial.
|
|
||||||
if starExpr, ok := result.Type.(*ast.StarExpr); ok {
|
|
||||||
if selectorExpr, ok := starExpr.X.(*ast.SelectorExpr); ok {
|
|
||||||
// Check if the package name is 'h' and type is 'Partial'.
|
|
||||||
if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "h" {
|
|
||||||
if selectorExpr.Sel.Name == "Partial" && hasOnlyReqContextParam(funcDecl.Type) {
|
|
||||||
p := Partial{
|
|
||||||
Package: node.Name.Name,
|
|
||||||
Path: normalizePath(sliceCommonPrefix(cwd, path)),
|
|
||||||
Import: sliceCommonPrefix(cwd, normalizePath(filepath.Dir(path))),
|
|
||||||
FuncName: funcDecl.Name.Name,
|
|
||||||
}
|
|
||||||
if predicate(p) {
|
|
||||||
partials = append(partials, p)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return partials, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findPublicFuncsReturningHPage(dir string) ([]Page, error) {
|
|
||||||
var pages = make([]Page, 0)
|
|
||||||
|
|
||||||
// Walk through the directory to find all Go files.
|
|
||||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only process Go files.
|
|
||||||
if !strings.HasSuffix(path, ".go") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the Go file.
|
|
||||||
fset := token.NewFileSet()
|
|
||||||
node, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inspect the AST for function declarations.
|
|
||||||
ast.Inspect(node, func(n ast.Node) bool {
|
|
||||||
// Check if the node is a function declaration.
|
|
||||||
if funcDecl, ok := n.(*ast.FuncDecl); ok {
|
|
||||||
// Only consider exported (public) functions.
|
|
||||||
if funcDecl.Name.IsExported() {
|
|
||||||
// Check the return type.
|
|
||||||
if funcDecl.Type.Results != nil {
|
|
||||||
for _, result := range funcDecl.Type.Results.List {
|
|
||||||
// Check if the return type is *h.Partial.
|
|
||||||
if starExpr, ok := result.Type.(*ast.StarExpr); ok {
|
|
||||||
if selectorExpr, ok := starExpr.X.(*ast.SelectorExpr); ok {
|
|
||||||
// Check if the package name is 'h' and type is 'Partial'.
|
|
||||||
if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "h" {
|
|
||||||
if selectorExpr.Sel.Name == "Page" && hasOnlyReqContextParam(funcDecl.Type) {
|
|
||||||
pages = append(pages, Page{
|
|
||||||
Package: node.Name.Name,
|
|
||||||
Import: normalizePath(filepath.Dir(path)),
|
|
||||||
Path: normalizePath(path),
|
|
||||||
FuncName: funcDecl.Name.Name,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return pages, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildGetPartialFromContext(builder *CodeBuilder, partials []Partial) {
|
|
||||||
moduleName := GetModuleName()
|
|
||||||
|
|
||||||
var routerHandlerMethod = func(path string, caller string) string {
|
|
||||||
return fmt.Sprintf(`
|
|
||||||
router.Handle("%s", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
cc := r.Context().Value(h.RequestContextKey).(*h.RequestContext)
|
|
||||||
partial := %s(cc)
|
|
||||||
if partial == nil {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.PartialView(w, partial)
|
|
||||||
}))`, path, caller)
|
|
||||||
}
|
|
||||||
|
|
||||||
handlerMethods := make([]string, 0)
|
|
||||||
|
|
||||||
for _, f := range partials {
|
|
||||||
caller := fmt.Sprintf("%s.%s", f.Package, f.FuncName)
|
|
||||||
path := fmt.Sprintf("/%s/%s.%s", moduleName, f.Import, f.FuncName)
|
|
||||||
handlerMethods = append(handlerMethods, routerHandlerMethod(path, caller))
|
|
||||||
}
|
|
||||||
|
|
||||||
registerFunction := fmt.Sprintf(`
|
|
||||||
func RegisterPartials(router *chi.Mux) {
|
|
||||||
%s
|
|
||||||
}
|
|
||||||
`, strings.Join(handlerMethods, "\n"))
|
|
||||||
|
|
||||||
builder.AppendLine(registerFunction)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writePartialsFile() {
|
|
||||||
config := dirutil.GetConfig()
|
|
||||||
|
|
||||||
cwd := process.GetWorkingDir()
|
|
||||||
partialPath := filepath.Join(cwd)
|
|
||||||
partials, err := findPublicFuncsReturningHPartial(partialPath, func(partial Partial) bool {
|
|
||||||
return partial.FuncName != "GetPartialFromContext"
|
|
||||||
})
|
|
||||||
|
|
||||||
partials = h.Filter(partials, func(partial Partial) bool {
|
|
||||||
return !dirutil.IsGlobExclude(partial.Path, config.AutomaticPartialRoutingIgnore)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := NewCodeBuilder(nil)
|
|
||||||
builder.AppendLine(GeneratedFileLine)
|
|
||||||
builder.AppendLine(PackageName)
|
|
||||||
builder.AddImport(ChiModuleName)
|
|
||||||
|
|
||||||
if len(partials) > 0 {
|
|
||||||
builder.AddImport(ModuleName)
|
|
||||||
builder.AddImport(HttpModuleName)
|
|
||||||
}
|
|
||||||
|
|
||||||
moduleName := GetModuleName()
|
|
||||||
for _, partial := range partials {
|
|
||||||
builder.AddImport(fmt.Sprintf(`%s/%s`, moduleName, partial.Import))
|
|
||||||
}
|
|
||||||
|
|
||||||
buildGetPartialFromContext(builder, partials)
|
|
||||||
|
|
||||||
WriteFile(filepath.Join(GeneratedDirName, "partials-generated.go"), func(content *ast.File) string {
|
|
||||||
return builder.String()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatRoute(path string) string {
|
|
||||||
path = strings.TrimSuffix(path, "index.go")
|
|
||||||
path = strings.TrimSuffix(path, ".go")
|
|
||||||
path = strings.TrimPrefix(path, "pages/")
|
|
||||||
path = strings.TrimPrefix(path, "pages\\")
|
|
||||||
path = strings.ReplaceAll(path, "$", ":")
|
|
||||||
path = strings.ReplaceAll(path, "_", "/")
|
|
||||||
path = strings.ReplaceAll(path, ".", "/")
|
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
|
||||||
|
|
||||||
parts := strings.Split(path, "/")
|
|
||||||
|
|
||||||
for i, part := range parts {
|
|
||||||
if strings.HasPrefix(part, ":") {
|
|
||||||
parts[i] = fmt.Sprintf("{%s}", part[1:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
path = strings.Join(parts, "/")
|
|
||||||
|
|
||||||
if path == "" {
|
|
||||||
return "/"
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(path, "/") {
|
|
||||||
path = filepath.Join("/", path)
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(path, "/") {
|
|
||||||
return strings.ReplaceAll(path[:len(path)-1], `\`, "/")
|
|
||||||
}
|
|
||||||
return strings.ReplaceAll(filepath.Clean(path), `\`, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func writePagesFile() {
|
|
||||||
config := dirutil.GetConfig()
|
|
||||||
|
|
||||||
builder := NewCodeBuilder(nil)
|
|
||||||
builder.AppendLine(GeneratedFileLine)
|
|
||||||
builder.AppendLine(PackageName)
|
|
||||||
builder.AddImport(HttpModuleName)
|
|
||||||
builder.AddImport(ChiModuleName)
|
|
||||||
|
|
||||||
pages, _ := findPublicFuncsReturningHPage("pages")
|
|
||||||
|
|
||||||
pages = h.Filter(pages, func(page Page) bool {
|
|
||||||
return !dirutil.IsGlobExclude(page.Path, config.AutomaticPageRoutingIgnore)
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(pages) > 0 {
|
|
||||||
builder.AddImport(ModuleName)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, page := range pages {
|
|
||||||
if page.Import != "" {
|
|
||||||
moduleName := GetModuleName()
|
|
||||||
builder.AddImport(
|
|
||||||
fmt.Sprintf(`%s/%s`, moduleName, page.Import),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fName := "RegisterPages"
|
|
||||||
body := `
|
|
||||||
`
|
|
||||||
|
|
||||||
for _, page := range pages {
|
|
||||||
call := fmt.Sprintf("%s.%s", page.Package, page.FuncName)
|
|
||||||
|
|
||||||
body += fmt.Sprintf(
|
|
||||||
`
|
|
||||||
router.Get("%s", func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
cc := request.Context().Value(h.RequestContextKey).(*h.RequestContext)
|
|
||||||
h.HtmlView(writer, %s(cc))
|
|
||||||
})
|
|
||||||
`, formatRoute(page.Path), call,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
f := Function{
|
|
||||||
Name: fName,
|
|
||||||
Parameters: []NameType{
|
|
||||||
{Name: "router", Type: "*chi.Mux"},
|
|
||||||
},
|
|
||||||
Body: body,
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.Append(builder.BuildFunction(f))
|
|
||||||
|
|
||||||
WriteFile(filepath.Join(GeneratedDirName, "pages-generated.go"), func(content *ast.File) string {
|
|
||||||
return builder.String()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeAssetsFile() {
|
|
||||||
cwd := process.GetWorkingDir()
|
|
||||||
config := dirutil.GetConfig()
|
|
||||||
|
|
||||||
slog.Debug("writing assets file", slog.String("cwd", cwd), slog.String("config", config.PublicAssetPath))
|
|
||||||
|
|
||||||
distAssets := filepath.Join(cwd, "assets", "dist")
|
|
||||||
hasAssets := false
|
|
||||||
|
|
||||||
builder := strings.Builder{}
|
|
||||||
|
|
||||||
builder.WriteString(`package assets`)
|
|
||||||
builder.WriteString("\n")
|
|
||||||
|
|
||||||
filepath.WalkDir(distAssets, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(d.Name(), ".") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
path = strings.ReplaceAll(path, distAssets, "")
|
|
||||||
httpUrl := normalizePath(fmt.Sprintf("%s%s", config.PublicAssetPath, path))
|
|
||||||
|
|
||||||
path = normalizePath(path)
|
|
||||||
path = strings.ReplaceAll(path, "/", "_")
|
|
||||||
path = strings.ReplaceAll(path, "//", "_")
|
|
||||||
|
|
||||||
name := strings.ReplaceAll(path, ".", "_")
|
|
||||||
name = strings.ReplaceAll(name, "-", "_")
|
|
||||||
|
|
||||||
name = toPascaleCase(name)
|
|
||||||
|
|
||||||
if isValidGoVariableName(name) {
|
|
||||||
builder.WriteString(fmt.Sprintf(`const %s = "%s"`, name, httpUrl))
|
|
||||||
builder.WriteString("\n")
|
|
||||||
hasAssets = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
builder.WriteString("\n")
|
|
||||||
|
|
||||||
str := builder.String()
|
|
||||||
|
|
||||||
if hasAssets {
|
|
||||||
WriteFile(filepath.Join(GeneratedDirName, "assets", "assets-generated.go"), func(content *ast.File) string {
|
|
||||||
return str
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func HasModuleFile(path string) bool {
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
return !os.IsNotExist(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckPagesDirectory(path string) error {
|
|
||||||
pagesPath := filepath.Join(path, "pages")
|
|
||||||
_, err := os.Stat(pagesPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("The directory pages does not exist.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetModuleName() string {
|
|
||||||
wd := process.GetWorkingDir()
|
|
||||||
modPath := filepath.Join(wd, "go.mod")
|
|
||||||
|
|
||||||
if HasModuleFile(modPath) == false {
|
|
||||||
fmt.Fprintf(os.Stderr, "Module not found: go.mod file does not exist.")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
checkDir := CheckPagesDirectory(wd)
|
|
||||||
if checkDir != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, checkDir.Error())
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
goModBytes, err := os.ReadFile(modPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "error reading go.mod: %v\n", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
modName := modfile.ModulePath(goModBytes)
|
|
||||||
return modName
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenAst(flags ...process.RunFlag) error {
|
|
||||||
if GetModuleName() == "" {
|
|
||||||
if slices.Contains(flags, process.ExitOnError) {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("error getting module name")
|
|
||||||
}
|
|
||||||
writePartialsFile()
|
|
||||||
writePagesFile()
|
|
||||||
writeAssetsFile()
|
|
||||||
|
|
||||||
WriteFile("__htmgo/setup-generated.go", func(content *ast.File) string {
|
|
||||||
|
|
||||||
return fmt.Sprintf(`
|
|
||||||
// Package __htmgo THIS FILE IS GENERATED. DO NOT EDIT.
|
|
||||||
package __htmgo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"%s"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Register(r *chi.Mux) {
|
|
||||||
RegisterPartials(r)
|
|
||||||
RegisterPages(r)
|
|
||||||
}
|
|
||||||
`, ChiModuleName)
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
/assets/dist
|
|
||||||
tmp
|
|
||||||
node_modules
|
|
||||||
.idea
|
|
||||||
__htmgo
|
|
||||||
dist
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
//go:build !prod
|
|
||||||
// +build !prod
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"astgen-project-sample/internal/embedded"
|
|
||||||
"io/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetStaticAssets() fs.FS {
|
|
||||||
return embedded.NewOsFs()
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
//go:build prod
|
|
||||||
// +build prod
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed assets/dist/*
|
|
||||||
var staticAssets embed.FS
|
|
||||||
|
|
||||||
func GetStaticAssets() fs.FS {
|
|
||||||
return staticAssets
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
module astgen-project-sample
|
|
||||||
|
|
||||||
go 1.23.0
|
|
||||||
|
|
||||||
require github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
# htmgo configuration
|
|
||||||
|
|
||||||
# if tailwindcss is enabled, htmgo will automatically compile your tailwind and output it to assets/dist
|
|
||||||
tailwind: true
|
|
||||||
|
|
||||||
# which directories to ignore when watching for changes, supports glob patterns through https://github.com/bmatcuk/doublestar
|
|
||||||
watch_ignore: [".git", "node_modules", "dist/*"]
|
|
||||||
|
|
||||||
# files to watch for changes, supports glob patterns through https://github.com/bmatcuk/doublestar
|
|
||||||
watch_files: ["**/*.go", "**/*.css", "**/*.md"]
|
|
||||||
|
|
||||||
# files or directories to ignore when automatically registering routes for pages
|
|
||||||
# supports glob patterns through https://github.com/bmatcuk/doublestar
|
|
||||||
automatic_page_routing_ignore: ["root.go"]
|
|
||||||
|
|
||||||
# files or directories to ignore when automatically registering routes for partials
|
|
||||||
# supports glob patterns through https://github.com/bmatcuk/doublestar
|
|
||||||
automatic_partial_routing_ignore: []
|
|
||||||
|
|
||||||
# url path of where the public assets are located
|
|
||||||
public_asset_path: "/public"
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package embedded
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OsFs struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (receiver OsFs) Open(name string) (fs.File, error) {
|
|
||||||
return os.Open(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOsFs() OsFs {
|
|
||||||
return OsFs{}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"astgen-project-sample/__htmgo"
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/framework/config"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
locator := service.NewLocator()
|
|
||||||
cfg := config.Get()
|
|
||||||
|
|
||||||
h.Start(h.AppOpts{
|
|
||||||
ServiceLocator: locator,
|
|
||||||
LiveReload: true,
|
|
||||||
Register: func(app *h.App) {
|
|
||||||
sub, err := fs.Sub(GetStaticAssets(), "assets/dist")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.FileServerFS(sub)
|
|
||||||
|
|
||||||
// change this in htmgo.yml (public_asset_path)
|
|
||||||
app.Router.Handle(fmt.Sprintf("%s/*", cfg.PublicAssetPath),
|
|
||||||
http.StripPrefix(cfg.PublicAssetPath, http.FileServerFS(sub)))
|
|
||||||
|
|
||||||
__htmgo.Register(app.Router)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
package pages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
)
|
|
||||||
|
|
||||||
func IndexPage(ctx *h.RequestContext) *h.Page {
|
|
||||||
return RootPage(
|
|
||||||
h.Div(
|
|
||||||
h.Class("flex flex-col gap-4 items-center pt-24 min-h-screen bg-neutral-100"),
|
|
||||||
h.H3(
|
|
||||||
h.Id("intro-text"),
|
|
||||||
h.Text("hello htmgo"),
|
|
||||||
h.Class("text-5xl"),
|
|
||||||
),
|
|
||||||
h.Div(
|
|
||||||
h.Class("mt-3"),
|
|
||||||
),
|
|
||||||
h.Div(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPartial(ctx *h.RequestContext) *h.Partial {
|
|
||||||
return h.NewPartial(
|
|
||||||
h.Div(
|
|
||||||
h.Text("Hello World"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package pages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RootPage(children ...h.Ren) *h.Page {
|
|
||||||
title := "htmgo template"
|
|
||||||
description := "an example of the htmgo template"
|
|
||||||
author := "htmgo"
|
|
||||||
url := "https://htmgo.dev"
|
|
||||||
|
|
||||||
return h.NewPage(
|
|
||||||
h.Html(
|
|
||||||
h.HxExtensions(
|
|
||||||
h.BaseExtensions(),
|
|
||||||
),
|
|
||||||
h.Head(
|
|
||||||
h.Title(
|
|
||||||
h.Text(title),
|
|
||||||
),
|
|
||||||
h.Meta("viewport", "width=device-width, initial-scale=1"),
|
|
||||||
h.Meta("title", title),
|
|
||||||
h.Meta("charset", "utf-8"),
|
|
||||||
h.Meta("author", author),
|
|
||||||
h.Meta("description", description),
|
|
||||||
h.Meta("og:title", title),
|
|
||||||
h.Meta("og:url", url),
|
|
||||||
h.Link("canonical", url),
|
|
||||||
h.Meta("og:description", description),
|
|
||||||
),
|
|
||||||
h.Body(
|
|
||||||
h.Div(
|
|
||||||
h.Class("flex flex-col gap-2 bg-white h-full"),
|
|
||||||
h.Fragment(children...),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package partials
|
|
||||||
|
|
||||||
import "github.com/maddalax/htmgo/framework/h"
|
|
||||||
|
|
||||||
func CountersPartial(ctx *h.RequestContext) *h.Partial {
|
|
||||||
return h.NewPartial(
|
|
||||||
h.Div(
|
|
||||||
h.Text("my counter"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SwapFormError(ctx *h.RequestContext, error string) *h.Partial {
|
|
||||||
return h.SwapPartial(
|
|
||||||
ctx,
|
|
||||||
h.Div(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
package astgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAstGen(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
workingDir, err := filepath.Abs("./project-sample")
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
process.SetWorkingDir(workingDir)
|
|
||||||
assert.NoError(t, os.Chdir(workingDir))
|
|
||||||
|
|
||||||
err = dirutil.DeleteDir(filepath.Join(process.GetWorkingDir(), "__htmgo"))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = process.Run(process.NewRawCommand("", "go build ."))
|
|
||||||
assert.Error(t, err)
|
|
||||||
err = GenAst()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// project was buildable after astgen, confirmed working
|
|
||||||
err = process.Run(process.NewRawCommand("server", "go run ."))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}()
|
|
||||||
|
|
||||||
time.Sleep(time.Second * 1)
|
|
||||||
|
|
||||||
urls := []string{
|
|
||||||
"/astgen-project-sample/partials.CountersPartial",
|
|
||||||
"/",
|
|
||||||
"/astgen-project-sample/pages.TestPartial",
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
serverProcess := process.GetProcessByName("server")
|
|
||||||
assert.NotNil(t, serverProcess)
|
|
||||||
process.KillProcess(*serverProcess)
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
|
|
||||||
for _, url := range urls {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
// ensure we can get a 200 response on the partials
|
|
||||||
resp, e := http.Get(fmt.Sprintf("http://localhost:3000%s", url))
|
|
||||||
assert.NoError(t, e)
|
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode, fmt.Sprintf("%s was not a 200 response", url))
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package astgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func PanicF(format string, args ...interface{}) {
|
|
||||||
panic(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
package copyassets
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/module"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"golang.org/x/mod/modfile"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getModuleVersion(modulePath string) (string, error) {
|
|
||||||
// Read the go.mod file
|
|
||||||
data, err := os.ReadFile(process.GetPathRelativeToCwd("go.mod"))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error reading go.mod: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the go.mod file
|
|
||||||
modFile, err := modfile.Parse(process.GetPathRelativeToCwd("go.mod"), data, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error parsing go.mod: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the module version
|
|
||||||
for _, req := range modFile.Require {
|
|
||||||
if req.Mod.Path == modulePath {
|
|
||||||
return req.Mod.Version, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("module %s not found in go.mod", modulePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyAssets() {
|
|
||||||
dirutil.CreateHtmgoDir()
|
|
||||||
|
|
||||||
moduleName := "github.com/maddalax/htmgo/framework"
|
|
||||||
modulePath := module.GetDependencyPath(moduleName)
|
|
||||||
|
|
||||||
assetDir := ""
|
|
||||||
// Is hosted version and not local version from .work file
|
|
||||||
if strings.HasPrefix(modulePath, "github.com/") {
|
|
||||||
version, err := getModuleVersion(modulePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
dirname, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
assetDir = fmt.Sprintf("%s/go/pkg/mod/%s@%s/assets", dirname, modulePath, version)
|
|
||||||
} else {
|
|
||||||
assetDir = filepath.Join(modulePath, "assets")
|
|
||||||
}
|
|
||||||
|
|
||||||
assetDistDir := filepath.Join(assetDir, "dist")
|
|
||||||
assetCssDir := filepath.Join(assetDir, "css")
|
|
||||||
|
|
||||||
cwd := process.GetWorkingDir()
|
|
||||||
|
|
||||||
destDir := filepath.Join(cwd, "assets")
|
|
||||||
destDirDist := filepath.Join(destDir, "dist")
|
|
||||||
destDirCss := filepath.Join(destDir, "css")
|
|
||||||
|
|
||||||
err := dirutil.CopyDir(assetDistDir, destDirDist, func(path string, exists bool) bool {
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = dirutil.CopyDir(assetCssDir, destDirCss, func(path string, exists bool) bool {
|
|
||||||
if strings.HasSuffix(path, "tailwind.config.js") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !exists
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dirutil.HasFileFromRoot("assets/public") {
|
|
||||||
err = dirutil.CopyDir(filepath.Join(process.GetWorkingDir(), "assets/public"), filepath.Join(process.GetWorkingDir(), "assets/dist"),
|
|
||||||
func(path string, exists bool) bool {
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if dirutil.GetConfig().Tailwind && !dirutil.HasFileFromRoot("tailwind.config.js") {
|
|
||||||
err = dirutil.CopyFile(
|
|
||||||
filepath.Join(assetCssDir, "tailwind.config.js"),
|
|
||||||
filepath.Join(process.GetWorkingDir(), "tailwind.config.js"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := fmt.Sprintf("cd %s && git add .", destDirCss)
|
|
||||||
process.Run(process.NewRawCommand(cmd, cmd, process.Silent))
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
package css
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"log"
|
|
||||||
"log/slog"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
func IsTailwindEnabled() bool {
|
|
||||||
return dirutil.GetConfig().Tailwind && dirutil.HasFileFromRoot("tailwind.config.js")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Setup() bool {
|
|
||||||
if !IsTailwindEnabled() {
|
|
||||||
slog.Debug("Tailwind is not enabled. Skipping CSS generation.")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
downloadTailwindCli()
|
|
||||||
|
|
||||||
if !dirutil.HasFileFromRoot("assets/css/input.css") {
|
|
||||||
copyassets.CopyAssets()
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetTailwindExecutableName() string {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return "./__htmgo/tailwind.exe"
|
|
||||||
} else {
|
|
||||||
return "./__htmgo/tailwind"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateCss(flags ...process.RunFlag) error {
|
|
||||||
if !Setup() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
exec := GetTailwindExecutableName()
|
|
||||||
cmd := fmt.Sprintf("%s -i ./assets/css/input.css -o ./assets/dist/main.css -c ./tailwind.config.js", exec)
|
|
||||||
return process.Run(process.NewRawCommand("tailwind", cmd, append(flags, process.Silent)...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadTailwindCli() {
|
|
||||||
|
|
||||||
if dirutil.HasFileFromRoot(GetTailwindExecutableName()) {
|
|
||||||
slog.Debug("Tailwind CLI already exists. Skipping download.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !IsTailwindEnabled() {
|
|
||||||
slog.Debug("Tailwind is not enabled. Skipping tailwind cli download.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
distro := ""
|
|
||||||
os := runtime.GOOS
|
|
||||||
arch := runtime.GOARCH
|
|
||||||
switch {
|
|
||||||
case os == "darwin" && arch == "arm64":
|
|
||||||
distro = "macos-arm64"
|
|
||||||
case os == "darwin" && arch == "amd64":
|
|
||||||
distro = "macos-x64"
|
|
||||||
case os == "linux" && arch == "arm64":
|
|
||||||
distro = "linux-arm64"
|
|
||||||
case os == "linux" && arch == "amd64":
|
|
||||||
distro = "linux-x64"
|
|
||||||
case os == "windows" && arch == "amd64":
|
|
||||||
distro = "windows-x64.exe"
|
|
||||||
case os == "windows" && arch == "arm64":
|
|
||||||
distro = "windows-arm64.exe"
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Fatal(fmt.Sprintf("Unsupported OS/ARCH: %s/%s", os, arch))
|
|
||||||
}
|
|
||||||
fileName := fmt.Sprintf(`tailwindcss-%s`, distro)
|
|
||||||
url := fmt.Sprintf(`https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.16/%s`, fileName)
|
|
||||||
|
|
||||||
cmd := fmt.Sprintf(`curl -LO %s`, url)
|
|
||||||
process.Run(process.NewRawCommand("tailwind-cli-download", cmd, process.ExitOnError))
|
|
||||||
|
|
||||||
outputFileName := GetTailwindExecutableName()
|
|
||||||
newPath := filepath.Join(process.GetWorkingDir(), outputFileName)
|
|
||||||
|
|
||||||
err := dirutil.MoveFile(
|
|
||||||
filepath.Join(process.GetWorkingDir(), fileName),
|
|
||||||
newPath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error moving file: %s\n", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if os != "windows" {
|
|
||||||
err = process.Run(process.NewRawCommand("chmod-tailwind-cli",
|
|
||||||
fmt.Sprintf(`chmod +x %s`, newPath),
|
|
||||||
process.ExitOnError))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error moving file: %s\n", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Debug("Successfully downloaded Tailwind CLI", slog.String("url", url))
|
|
||||||
}
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
package downloadtemplate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/run"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/util"
|
|
||||||
"log"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DownloadTemplate(outPath string) {
|
|
||||||
cwd, _ := os.Getwd()
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
outPath = strings.ReplaceAll(outPath, "\n", "")
|
|
||||||
outPath = strings.ReplaceAll(outPath, "\r", "")
|
|
||||||
outPath = strings.ReplaceAll(outPath, " ", "-")
|
|
||||||
outPath = strings.ToLower(outPath)
|
|
||||||
|
|
||||||
if outPath == "" {
|
|
||||||
fmt.Println("Please provide a name for your app.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
templateName := "starter-template"
|
|
||||||
templatePath := filepath.Join("templates", "starter")
|
|
||||||
|
|
||||||
re := regexp.MustCompile(`[^a-zA-Z]+`)
|
|
||||||
// Replace all non-alphabetic characters with an empty string
|
|
||||||
newModuleName := re.ReplaceAllString(outPath, "")
|
|
||||||
|
|
||||||
tempOut := newModuleName + "_temp_" + strconv.FormatInt(time.Now().Unix(), 10)
|
|
||||||
|
|
||||||
fmt.Printf("Downloading template %s\n to %s", templateName, tempOut)
|
|
||||||
|
|
||||||
err := process.Run(process.NewRawCommand("clone-template", "git clone https://github.com/maddalax/htmgo --depth=1 "+tempOut, process.ExitOnError))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error cloning the template, error: %s\n", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Debug("provided out path", slog.String("outPath", outPath))
|
|
||||||
slog.Debug("new module name", slog.String("newModuleName", newModuleName))
|
|
||||||
slog.Debug("cwd", slog.String("cwd", cwd))
|
|
||||||
|
|
||||||
newDir := filepath.Join(cwd, outPath)
|
|
||||||
|
|
||||||
slog.Debug("Copying template files to", slog.String("dir", newDir))
|
|
||||||
|
|
||||||
dirutil.CopyDir(filepath.Join(tempOut, templatePath), newDir, func(path string, exists bool) bool {
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
dirutil.DeleteDir(tempOut)
|
|
||||||
|
|
||||||
process.SetWorkingDir(newDir)
|
|
||||||
|
|
||||||
slog.Debug("current working dir", slog.String("cwd", process.GetWorkingDir()))
|
|
||||||
|
|
||||||
commands := [][]string{
|
|
||||||
{"git", "init"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, command := range commands {
|
|
||||||
process.Run(process.NewRawCommand("", strings.Join(command, " "), process.ExitOnError))
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = util.ReplaceTextInFile(filepath.Join(newDir, "go.mod"),
|
|
||||||
fmt.Sprintf("module %s", templateName),
|
|
||||||
fmt.Sprintf("module %s", newModuleName))
|
|
||||||
|
|
||||||
_ = util.ReplaceTextInDirRecursive(newDir, templateName, newModuleName, func(file string) bool {
|
|
||||||
return strings.HasSuffix(file, ".go") || strings.HasPrefix(file, "Dockerfile")
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Printf("Setting up the project in %s\n", newDir)
|
|
||||||
process.SetWorkingDir(newDir)
|
|
||||||
run.Setup()
|
|
||||||
process.SetWorkingDir("")
|
|
||||||
|
|
||||||
fmt.Println("Template downloaded successfully.")
|
|
||||||
fmt.Println("To start the development server, run the following commands:")
|
|
||||||
fmt.Printf("cd %s && htmgo watch\n", outPath)
|
|
||||||
|
|
||||||
fmt.Printf("To build the project, run the following command:\n")
|
|
||||||
fmt.Printf("cd %s && htmgo build\n", outPath)
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
package formatter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/tools/html-to-htmgo/htmltogo"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FormatDir(dir string) {
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error reading dir: %s\n", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() {
|
|
||||||
FormatDir(filepath.Join(dir, file.Name()))
|
|
||||||
} else {
|
|
||||||
FormatFile(filepath.Join(dir, file.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func FormatFile(file string) {
|
|
||||||
if !strings.HasSuffix(file, ".go") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("formatting file: %s\n", file)
|
|
||||||
|
|
||||||
source, err := os.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error reading file: %s\n", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
str := string(source)
|
|
||||||
|
|
||||||
if !strings.Contains(str, "github.com/maddalax/htmgo/framework/h") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed := htmltogo.Indent(str)
|
|
||||||
|
|
||||||
os.WriteFile(file, []byte(parsed), 0644)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package module
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetDependencyPath(dep string) string {
|
|
||||||
cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", dep)
|
|
||||||
cmd.Dir = process.GetWorkingDir()
|
|
||||||
// Run the command and capture the output
|
|
||||||
output, err := cmd.CombinedOutput() // Use CombinedOutput to capture both stdout and stderr
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Command execution failed: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert output to string
|
|
||||||
dir := strings.TrimSuffix(string(output), "\n")
|
|
||||||
if strings.Contains(dir, "not a known dependency") {
|
|
||||||
return dep
|
|
||||||
}
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
//go:build linux || darwin
|
|
||||||
|
|
||||||
package process
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func KillProcess(process CmdWithFlags) error {
|
|
||||||
if process.Cmd == nil || process.Cmd.Process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
slog.Debug("killing process",
|
|
||||||
slog.String("name", process.Name),
|
|
||||||
slog.Int("pid", process.Cmd.Process.Pid))
|
|
||||||
_ = syscall.Kill(-process.Cmd.Process.Pid, syscall.SIGKILL)
|
|
||||||
time.Sleep(time.Millisecond * 50)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrepareCommand(command *exec.Cmd) {
|
|
||||||
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
func PidExists(pid int32) bool {
|
|
||||||
if pid <= 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
proc, err := os.FindProcess(int(pid))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
err = proc.Signal(syscall.Signal(0))
|
|
||||||
if err == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if err.Error() == "os: process already finished" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var errno syscall.Errno
|
|
||||||
ok := errors.As(err, &errno)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch errno {
|
|
||||||
case syscall.ESRCH:
|
|
||||||
return false
|
|
||||||
case syscall.EPERM:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
package process
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
import "golang.org/x/sys/windows"
|
|
||||||
|
|
||||||
func KillProcess(process CmdWithFlags) error {
|
|
||||||
if process.Cmd == nil || process.Cmd.Process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(process.Cmd.Process.Pid)).Run()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
time.Sleep(time.Millisecond * 50)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrepareCommand(command *exec.Cmd) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func PidExists(pid int32) bool {
|
|
||||||
var handle, err = windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer windows.CloseHandle(handle)
|
|
||||||
|
|
||||||
var exitCode uint32
|
|
||||||
err = windows.GetExitCodeProcess(handle, &exitCode)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return exitCode == 259
|
|
||||||
}
|
|
||||||
|
|
@ -1,293 +0,0 @@
|
||||||
package process
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CmdWithFlags struct {
|
|
||||||
Flags []RunFlag
|
|
||||||
Name string
|
|
||||||
Cmd *exec.Cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
type RawCommand struct {
|
|
||||||
Name string
|
|
||||||
Args string
|
|
||||||
Flags []RunFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRawCommand(name string, args string, flags ...RunFlag) RawCommand {
|
|
||||||
if name == "" {
|
|
||||||
name = args
|
|
||||||
}
|
|
||||||
c := RawCommand{Name: name, Args: args, Flags: flags}
|
|
||||||
if c.Flags == nil {
|
|
||||||
c.Flags = make([]RunFlag, 0)
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
var workingDir string
|
|
||||||
var commands = make(map[string]CmdWithFlags)
|
|
||||||
|
|
||||||
func AppendRunning(cmd *exec.Cmd, raw RawCommand) {
|
|
||||||
slog.Debug("running", slog.String("command", strings.Join(cmd.Args, " ")),
|
|
||||||
slog.String("dir", cmd.Dir),
|
|
||||||
slog.String("cwd", GetWorkingDir()))
|
|
||||||
|
|
||||||
commands[raw.Name] = CmdWithFlags{Flags: raw.Flags, Name: raw.Name, Cmd: cmd}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetWorkingDir() string {
|
|
||||||
if workingDir == "" {
|
|
||||||
wd, _ := os.Getwd()
|
|
||||||
return wd
|
|
||||||
}
|
|
||||||
return workingDir
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetWorkingDir(dir string) {
|
|
||||||
workingDir = dir
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetPathRelativeToCwd(path string) string {
|
|
||||||
return filepath.Join(GetWorkingDir(), path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldSkipKilling(flags []RunFlag, skipFlag []RunFlag) bool {
|
|
||||||
for _, flag := range flags {
|
|
||||||
if slices.Contains(skipFlag, flag) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartLogger() {
|
|
||||||
if internal.GetLogLevel() != slog.LevelDebug {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
time.Sleep(time.Second * 5)
|
|
||||||
items := make([]map[string]string, 0)
|
|
||||||
for _, cmd := range commands {
|
|
||||||
data := make(map[string]string)
|
|
||||||
data["command"] = fmt.Sprintf("%s %s", cmd.Cmd.Path, strings.Join(cmd.Cmd.Args, " "))
|
|
||||||
if cmd.Cmd.Process != nil {
|
|
||||||
data["pid"] = fmt.Sprintf("%d", cmd.Cmd.Process.Pid)
|
|
||||||
}
|
|
||||||
items = append(items, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Running processes:\n")
|
|
||||||
for i, item := range items {
|
|
||||||
fmt.Printf("%d: %+v\n", i, item)
|
|
||||||
}
|
|
||||||
fmt.Printf("\n")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetProcessByName(name string) *CmdWithFlags {
|
|
||||||
for _, cmd := range commands {
|
|
||||||
if cmd.Name == name {
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func OnShutdown() {
|
|
||||||
// request for shutdown
|
|
||||||
for _, cmd := range commands {
|
|
||||||
if cmd.Cmd != nil && cmd.Cmd.Process != nil {
|
|
||||||
cmd.Cmd.Process.Signal(os.Interrupt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// give it a second
|
|
||||||
time.Sleep(time.Second * 1)
|
|
||||||
// force kill
|
|
||||||
KillAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
func KillAll(skipFlag ...RunFlag) {
|
|
||||||
|
|
||||||
tries := 0
|
|
||||||
updatedCommands := make(map[string]CmdWithFlags)
|
|
||||||
for {
|
|
||||||
tries++
|
|
||||||
allFinished := true
|
|
||||||
for _, cmd := range commands {
|
|
||||||
if cmd.Cmd.Process == nil {
|
|
||||||
allFinished = false
|
|
||||||
if tries > 50 {
|
|
||||||
args := strings.Join(cmd.Cmd.Args, " ")
|
|
||||||
slog.Debug("process is not running after 50 tries, breaking.", slog.String("command", args))
|
|
||||||
allFinished = true
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
time.Sleep(time.Millisecond * 50)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updatedCommands[cmd.Name] = cmd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if allFinished {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commands = make(map[string]CmdWithFlags)
|
|
||||||
for _, command := range updatedCommands {
|
|
||||||
if command.Cmd != nil && command.Cmd.Process != nil {
|
|
||||||
commands[command.Name] = command
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, command := range commands {
|
|
||||||
if shouldSkipKilling(command.Flags, skipFlag) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := KillProcess(command)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
finished := true
|
|
||||||
for _, c := range commands {
|
|
||||||
if c.Cmd.Process == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if shouldSkipKilling(c.Flags, skipFlag) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
exists := PidExists(int32(c.Cmd.Process.Pid))
|
|
||||||
if exists {
|
|
||||||
KillProcess(c)
|
|
||||||
finished = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if finished {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
slog.Debug("waiting for all processes to exit\n")
|
|
||||||
time.Sleep(time.Millisecond * 5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commands = make(map[string]CmdWithFlags)
|
|
||||||
slog.Debug("all processes killed\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunOrExit(command RawCommand) {
|
|
||||||
command.Flags = append(command.Flags, ExitOnError)
|
|
||||||
_ = Run(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
type RunFlag int
|
|
||||||
|
|
||||||
const (
|
|
||||||
ExitOnError RunFlag = iota
|
|
||||||
Silent
|
|
||||||
KillOnlyOnExit
|
|
||||||
)
|
|
||||||
|
|
||||||
func RunMany(commands ...RawCommand) error {
|
|
||||||
for _, command := range commands {
|
|
||||||
err := Run(command)
|
|
||||||
if err != nil {
|
|
||||||
if slices.Contains(command.Flags, ExitOnError) {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var mutex = &sync.Mutex{}
|
|
||||||
|
|
||||||
func Run(command RawCommand) error {
|
|
||||||
mutex.Lock()
|
|
||||||
|
|
||||||
parts := strings.Fields(command.Args)
|
|
||||||
|
|
||||||
args := make([]string, 0)
|
|
||||||
if len(parts) > 1 {
|
|
||||||
args = parts[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
path := parts[0]
|
|
||||||
|
|
||||||
existing := GetProcessByName(command.Name)
|
|
||||||
|
|
||||||
if existing != nil {
|
|
||||||
slog.Debug("process already running, killing it", slog.String("command", command.Name))
|
|
||||||
KillProcess(*existing)
|
|
||||||
time.Sleep(time.Millisecond * 50)
|
|
||||||
} else {
|
|
||||||
slog.Debug("no existing process found for %s, safe to run...", slog.String("command", command.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(path, args...)
|
|
||||||
|
|
||||||
PrepareCommand(cmd)
|
|
||||||
|
|
||||||
if slices.Contains(command.Flags, Silent) {
|
|
||||||
cmd.Stdout = nil
|
|
||||||
cmd.Stderr = nil
|
|
||||||
} else {
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
if workingDir != "" {
|
|
||||||
cmd.Dir = workingDir
|
|
||||||
}
|
|
||||||
|
|
||||||
AppendRunning(cmd, command)
|
|
||||||
|
|
||||||
mutex.Unlock()
|
|
||||||
|
|
||||||
err := cmd.Run()
|
|
||||||
|
|
||||||
slog.Debug("command finished",
|
|
||||||
slog.String("command", command.Name),
|
|
||||||
slog.String("args", command.Args),
|
|
||||||
slog.String("dir", cmd.Dir),
|
|
||||||
slog.String("cwd", GetWorkingDir()),
|
|
||||||
slog.String("error", fmt.Sprintf("%v", err)))
|
|
||||||
|
|
||||||
delete(commands, command.Name)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(err.Error(), "signal: killed") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(command.Flags, ExitOnError) {
|
|
||||||
slog.Error("Error running command: ",
|
|
||||||
slog.String("error", err.Error()),
|
|
||||||
slog.String("command", command.Name))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
package reloader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/astgen"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/css"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/run"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/util"
|
|
||||||
"log/slog"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Change struct {
|
|
||||||
name string
|
|
||||||
op fsnotify.Op
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChange(event *fsnotify.Event) *Change {
|
|
||||||
return &Change{name: event.Name, op: event.Op}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Change) Name() string {
|
|
||||||
return c.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Change) HasAnyPrefix(prefix ...string) bool {
|
|
||||||
for _, s := range prefix {
|
|
||||||
if strings.HasPrefix(c.name, s) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Change) HasAnySuffix(suffix ...string) bool {
|
|
||||||
for _, s := range suffix {
|
|
||||||
if strings.HasSuffix(c.name, s) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Change) IsGenerated() bool {
|
|
||||||
return c.HasAnySuffix("generated.go")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Change) IsWrite() bool {
|
|
||||||
return c.op == fsnotify.Write
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Change) IsGo() bool {
|
|
||||||
return c.HasAnySuffix(".go")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Tasks struct {
|
|
||||||
AstGen bool
|
|
||||||
Run bool
|
|
||||||
Ent bool
|
|
||||||
Css bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func OnFileChange(version string, events []*fsnotify.Event) {
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
tasks := Tasks{}
|
|
||||||
hasTask := false
|
|
||||||
|
|
||||||
for _, event := range events {
|
|
||||||
c := NewChange(event)
|
|
||||||
|
|
||||||
if c.HasAnySuffix(".go~", ".css~") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.IsGenerated() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.IsGo() && c.HasAnyPrefix("pages/", "partials/") {
|
|
||||||
tasks.AstGen = true
|
|
||||||
hasTask = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.IsGo() {
|
|
||||||
tasks.Run = true
|
|
||||||
tasks.Css = true
|
|
||||||
hasTask = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.HasAnySuffix(".md") {
|
|
||||||
tasks.Run = true
|
|
||||||
hasTask = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.HasAnyPrefix("ent/schema") {
|
|
||||||
tasks.Ent = true
|
|
||||||
hasTask = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// framework assets changed
|
|
||||||
if c.HasAnySuffix("assets/dist/htmgo.js") {
|
|
||||||
copyassets.CopyAssets()
|
|
||||||
//tasks.Run = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// something in public folder changed
|
|
||||||
if c.HasAnyPrefix("assets/public/") {
|
|
||||||
copyassets.CopyAssets()
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasTask {
|
|
||||||
slog.Info("file changed", slog.String("version", version), slog.String("file", c.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasTask {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deps := make([]func() any, 0)
|
|
||||||
|
|
||||||
if tasks.AstGen {
|
|
||||||
go func() {
|
|
||||||
util.Trace("generate ast", func() any {
|
|
||||||
astgen.GenAst()
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
if tasks.Css {
|
|
||||||
deps = append(deps, func() any {
|
|
||||||
return util.Trace("generate css", func() any {
|
|
||||||
css.GenerateCss()
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if tasks.Ent {
|
|
||||||
deps = append(deps, func() any {
|
|
||||||
return util.Trace("generate ent", func() any {
|
|
||||||
run.EntGenerate()
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
|
|
||||||
for _, dep := range deps {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(dep func() any) {
|
|
||||||
defer wg.Done()
|
|
||||||
err := dep()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
}(dep)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if tasks.Run {
|
|
||||||
go run.Server()
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("reloaded in", slog.Duration("duration", time.Since(now)))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package run
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/astgen"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/css"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MakeBuildable() {
|
|
||||||
copyassets.CopyAssets()
|
|
||||||
css.GenerateCss(process.ExitOnError)
|
|
||||||
astgen.GenAst(process.ExitOnError)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Build() {
|
|
||||||
MakeBuildable()
|
|
||||||
|
|
||||||
_ = os.RemoveAll("./dist")
|
|
||||||
|
|
||||||
err := os.Mkdir("./dist", 0755)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error creating dist directory", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("SKIP_GO_BUILD") != "1" {
|
|
||||||
process.RunOrExit(process.NewRawCommand("", fmt.Sprintf("go build -tags prod -o ./dist")))
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Executable built at %s\n", process.GetPathRelativeToCwd("dist"))
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package run
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
func EntNewSchema(name string) {
|
|
||||||
process.RunOrExit(process.NewRawCommand("", "GOWORK=off go run -mod=mod entgo.io/ent/cmd/ent new "+name))
|
|
||||||
}
|
|
||||||
|
|
||||||
func EntGenerate() {
|
|
||||||
if dirutil.HasFileFromRoot("ent/schema") {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
process.RunOrExit(process.NewRawCommand("ent-generate", "go generate ./ent"))
|
|
||||||
} else {
|
|
||||||
process.RunOrExit(process.NewRawCommand("ent-generate", "bash -c GOWORK=off go generate ./ent"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
package run
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Server(flags ...process.RunFlag) error {
|
|
||||||
buildDir := "./__htmgo/temp-build"
|
|
||||||
_ = os.RemoveAll(buildDir)
|
|
||||||
err := os.Mkdir(buildDir, 0755)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
process.RunOrExit(process.NewRawCommand("", fmt.Sprintf("go build -o %s", buildDir)))
|
|
||||||
|
|
||||||
binaryPath := ""
|
|
||||||
|
|
||||||
// find the binary that was built
|
|
||||||
err = filepath.WalkDir(buildDir, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if d.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
binaryPath = path
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if binaryPath == "" {
|
|
||||||
return fmt.Errorf("could not find the binary")
|
|
||||||
}
|
|
||||||
|
|
||||||
return process.Run(process.NewRawCommand("run-server", fmt.Sprintf("./%s", binaryPath), flags...))
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package run
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/process"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Setup() {
|
|
||||||
process.RunOrExit(process.NewRawCommand("", "go mod download"))
|
|
||||||
process.RunOrExit(process.NewRawCommand("", "go mod tidy"))
|
|
||||||
MakeBuildable()
|
|
||||||
EntGenerate()
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ReplaceTextInFile(file string, text string, replacement string) error {
|
|
||||||
bytes, err := os.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
str := string(bytes)
|
|
||||||
updated := strings.ReplaceAll(str, text, replacement)
|
|
||||||
return os.WriteFile(file, []byte(updated), 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReplaceTextInDirRecursive(dir string, text string, replacement string, filter func(file string) bool) error {
|
|
||||||
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if filter(filepath.Base(path)) {
|
|
||||||
_ = ReplaceTextInFile(path, text, replacement)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Trace(name string, cb func() any) any {
|
|
||||||
now := time.Now()
|
|
||||||
result := cb()
|
|
||||||
slog.Debug("trace", slog.String("name", name), slog.Duration("duration", time.Since(now)))
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/internal/dirutil"
|
|
||||||
"github.com/maddalax/htmgo/cli/htmgo/tasks/module"
|
|
||||||
"log"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func startWatcher(cb func(version string, file []*fsnotify.Event)) {
|
|
||||||
events := make([]*fsnotify.Event, 0)
|
|
||||||
debouncer := internal.NewDebouncer(500 * time.Millisecond)
|
|
||||||
config := dirutil.GetConfig()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
slog.Debug("Recovered from fatal error:", slog.String("error", r.(error).Error()))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// Create new watcher.
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer watcher.Close()
|
|
||||||
// Start listening for events.
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event, ok := <-watcher.Events:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Has(fsnotify.Remove) {
|
|
||||||
if dirutil.IsGlobMatch(event.Name, config.WatchFiles, config.WatchIgnore) {
|
|
||||||
watcher.Remove(event.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Has(fsnotify.Create) {
|
|
||||||
if dirutil.IsGlobMatch(event.Name, config.WatchFiles, config.WatchIgnore) {
|
|
||||||
watcher.Add(event.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
info, err := os.Stat(event.Name)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Error getting file info:", slog.String("path", event.Name), slog.String("error", err.Error()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if info.IsDir() {
|
|
||||||
err = watcher.Add(event.Name)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Error adding directory to watcher:", slog.String("path", event.Name), slog.String("error", err.Error()))
|
|
||||||
} else {
|
|
||||||
slog.Debug("Watching directory:", slog.String("path", event.Name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Has(fsnotify.Write) || event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) {
|
|
||||||
if !dirutil.IsGlobMatch(event.Name, config.WatchFiles, config.WatchIgnore) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
events = append(events, &event)
|
|
||||||
debouncer.Do(func() {
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
dedupe := make([]*fsnotify.Event, 0)
|
|
||||||
for _, e := range events {
|
|
||||||
if _, ok := seen[e.Name]; !ok {
|
|
||||||
seen[e.Name] = true
|
|
||||||
dedupe = append(dedupe, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cb(uuid.NewString()[0:6], dedupe)
|
|
||||||
events = make([]*fsnotify.Event, 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case err, ok := <-watcher.Errors:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
slog.Error("error:", slog.String("error", err.Error()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
rootDir := "."
|
|
||||||
|
|
||||||
frameworkPath := module.GetDependencyPath("github.com/maddalax/htmgo/framework")
|
|
||||||
|
|
||||||
if !strings.HasPrefix(frameworkPath, "github.com/") {
|
|
||||||
assetPath := filepath.Join(frameworkPath, "assets", "dist")
|
|
||||||
slog.Debug("Watching directory:", slog.String("path", assetPath))
|
|
||||||
watcher.Add(assetPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk through the root directory and add all subdirectories to the watcher
|
|
||||||
err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Ignore directories in the ignoredDirs list
|
|
||||||
if dirutil.IsGlobExclude(path, config.WatchIgnore) {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only watch directories
|
|
||||||
if info.IsDir() {
|
|
||||||
err = watcher.Add(path)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Error adding directory to watcher:", slog.String("error", err.Error()))
|
|
||||||
} else {
|
|
||||||
slog.Debug("Watching directory:", slog.String("path", path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
done := RegisterSignals()
|
|
||||||
<-done
|
|
||||||
println("process exited")
|
|
||||||
}
|
|
||||||
136
database/database.go
Normal file
136
database/database.go
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
rdb *redis.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
func Connect() *redis.Client {
|
||||||
|
once.Do(func() {
|
||||||
|
var ctx = context.Background()
|
||||||
|
var err error
|
||||||
|
rdb = redis.NewClient(&redis.Options{
|
||||||
|
Addr: "localhost:6379",
|
||||||
|
Password: "", // no password set
|
||||||
|
DB: 0, // use default DB
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := rdb.Ping(ctx)
|
||||||
|
|
||||||
|
if cmd.Err() != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return rdb
|
||||||
|
}
|
||||||
|
|
||||||
|
func Incr(key string) int64 {
|
||||||
|
db := Connect()
|
||||||
|
result := db.Incr(context.Background(), key)
|
||||||
|
return result.Val()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Set[T any](key string, value T) error {
|
||||||
|
db := Connect()
|
||||||
|
serialized, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result := db.Set(context.Background(), key, serialized, time.Duration(0))
|
||||||
|
return result.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func HSet[T any](set string, key string, value T) error {
|
||||||
|
db := Connect()
|
||||||
|
serialized, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result := db.HSet(context.Background(), set, key, serialized)
|
||||||
|
return result.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func HIncr(set string, key string) int64 {
|
||||||
|
db := Connect()
|
||||||
|
result := db.HIncrBy(context.Background(), set, key, 1)
|
||||||
|
return result.Val()
|
||||||
|
}
|
||||||
|
|
||||||
|
func HGet[T any](set string, key string) *T {
|
||||||
|
db := Connect()
|
||||||
|
val, err := db.HGet(context.Background(), set, key).Result()
|
||||||
|
if err != nil || val == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := new(T)
|
||||||
|
err = json.Unmarshal([]byte(val), result)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOrSet[T any](key string, cb func() T) (*T, error) {
|
||||||
|
db := Connect()
|
||||||
|
val, err := db.Get(context.Background(), key).Result()
|
||||||
|
if err == nil {
|
||||||
|
result := new(T)
|
||||||
|
err = json.Unmarshal([]byte(val), result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
value := cb()
|
||||||
|
err = Set(key, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get[T any](key string) (*T, error) {
|
||||||
|
db := Connect()
|
||||||
|
val, err := db.Get(context.Background(), key).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := new(T)
|
||||||
|
err = json.Unmarshal([]byte(val), result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func HList[T any](key string) ([]*T, error) {
|
||||||
|
db := Connect()
|
||||||
|
val, err := db.HGetAll(context.Background(), key).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]*T, len(val))
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for _, t := range val {
|
||||||
|
item := new(T)
|
||||||
|
err = json.Unmarshal([]byte(t), item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result[count] = item
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# Project exclude paths
|
|
||||||
/tmp/
|
|
||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
js/dist
|
|
||||||
js/node_modules
|
|
||||||
go.work
|
|
||||||
go.work.sum
|
|
||||||
.idea
|
|
||||||
!framework/assets/dist
|
|
||||||
__htmgo
|
|
||||||
6
examples/chat/.gitignore
vendored
6
examples/chat/.gitignore
vendored
|
|
@ -1,6 +0,0 @@
|
||||||
/assets/dist
|
|
||||||
tmp
|
|
||||||
node_modules
|
|
||||||
.idea
|
|
||||||
__htmgo
|
|
||||||
dist
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
# Stage 1: Build the Go binary
|
|
||||||
FROM golang:1.23 AS builder
|
|
||||||
|
|
||||||
# Set the working directory inside the container
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy go.mod and go.sum files
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
|
|
||||||
# Download and cache the Go modules
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
# Copy the source code into the container
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the Go binary for Linux
|
|
||||||
RUN CGO_ENABLED=0 GOPRIVATE=github.com/maddalax LOG_LEVEL=debug go run github.com/maddalax/htmgo/cli/htmgo@latest build
|
|
||||||
|
|
||||||
RUN CGO_ENABLED=1 GOOS=linux go build -tags prod -o ./dist -a -ldflags '-linkmode external -extldflags "-static"' .
|
|
||||||
|
|
||||||
|
|
||||||
# Stage 2: Create the smallest possible image
|
|
||||||
FROM gcr.io/distroless/base-debian11
|
|
||||||
|
|
||||||
# Set the working directory inside the container
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the Go binary from the builder stage
|
|
||||||
COPY --from=builder /app/dist .
|
|
||||||
|
|
||||||
# Expose the necessary port (replace with your server port)
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
|
|
||||||
# Command to run the binary
|
|
||||||
CMD ["./chat"]
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
version: '3'
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
run:
|
|
||||||
cmds:
|
|
||||||
- go run github.com/maddalax/htmgo/cli/htmgo@latest run
|
|
||||||
silent: true
|
|
||||||
|
|
||||||
build:
|
|
||||||
cmds:
|
|
||||||
- go run github.com/maddalax/htmgo/cli/htmgo@latest build
|
|
||||||
|
|
||||||
docker:
|
|
||||||
cmds:
|
|
||||||
- docker build .
|
|
||||||
|
|
||||||
watch:
|
|
||||||
cmds:
|
|
||||||
- go run github.com/maddalax/htmgo/cli/htmgo@latest watch
|
|
||||||
silent: true
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
//go:build !prod
|
|
||||||
// +build !prod
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chat/internal/embedded"
|
|
||||||
"io/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetStaticAssets() fs.FS {
|
|
||||||
return embedded.NewOsFs()
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
//go:build prod
|
|
||||||
// +build prod
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed assets/dist/*
|
|
||||||
var staticAssets embed.FS
|
|
||||||
|
|
||||||
func GetStaticAssets() fs.FS {
|
|
||||||
return staticAssets
|
|
||||||
}
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
package chat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chat/internal/db"
|
|
||||||
"chat/sse"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
socketManager *sse.SocketManager
|
|
||||||
queries *db.Queries
|
|
||||||
service *Service
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewManager(locator *service.Locator) *Manager {
|
|
||||||
return &Manager{
|
|
||||||
socketManager: service.Get[sse.SocketManager](locator),
|
|
||||||
queries: service.Get[db.Queries](locator),
|
|
||||||
service: NewService(locator),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) StartListener() {
|
|
||||||
c := make(chan sse.SocketEvent, 1)
|
|
||||||
m.socketManager.Listen(c)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <-c:
|
|
||||||
switch event.Type {
|
|
||||||
case sse.ConnectedEvent:
|
|
||||||
m.OnConnected(event)
|
|
||||||
case sse.DisconnectedEvent:
|
|
||||||
m.OnDisconnected(event)
|
|
||||||
case sse.MessageEvent:
|
|
||||||
m.onMessage(event)
|
|
||||||
default:
|
|
||||||
fmt.Printf("Unknown event type: %s\n", event.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) dispatchConnectedUsers(roomId string, predicate func(conn sse.SocketConnection) bool) {
|
|
||||||
|
|
||||||
connectedUsers := make([]db.User, 0)
|
|
||||||
|
|
||||||
// backfill all existing clients to the connected client
|
|
||||||
m.socketManager.ForEachSocket(roomId, func(conn sse.SocketConnection) {
|
|
||||||
if !predicate(conn) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user, err := m.queries.GetUserBySessionId(context.Background(), conn.Id)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
connectedUsers = append(connectedUsers, user)
|
|
||||||
})
|
|
||||||
|
|
||||||
m.socketManager.ForEachSocket(roomId, func(conn sse.SocketConnection) {
|
|
||||||
m.socketManager.SendText(conn.Id, h.Render(ConnectedUsers(connectedUsers, conn.Id)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) OnConnected(e sse.SocketEvent) {
|
|
||||||
room, _ := m.service.GetRoom(e.RoomId)
|
|
||||||
|
|
||||||
if room == nil {
|
|
||||||
m.socketManager.CloseWithMessage(e.Id, "invalid room")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
m.socketManager.CloseWithMessage(e.Id, "invalid user")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("User %s connected to %s\n", user.Name, e.RoomId)
|
|
||||||
|
|
||||||
m.dispatchConnectedUsers(e.RoomId, func(conn sse.SocketConnection) bool {
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
m.backFill(e.Id, e.RoomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) OnDisconnected(e sse.SocketEvent) {
|
|
||||||
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
room, err := m.service.GetRoom(e.RoomId)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("User %s disconnected from %s\n", user.Name, room.ID)
|
|
||||||
m.dispatchConnectedUsers(e.RoomId, func(conn sse.SocketConnection) bool {
|
|
||||||
return conn.Id != e.Id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) backFill(socketId string, roomId string) {
|
|
||||||
messages, _ := m.queries.GetLastMessages(context.Background(), db.GetLastMessagesParams{
|
|
||||||
ChatRoomID: roomId,
|
|
||||||
Limit: 200,
|
|
||||||
})
|
|
||||||
for _, message := range messages {
|
|
||||||
parsed, _ := time.Parse("2006-01-02 15:04:05", message.CreatedAt)
|
|
||||||
m.socketManager.SendText(socketId,
|
|
||||||
h.Render(MessageRow(&Message{
|
|
||||||
UserId: message.UserID,
|
|
||||||
UserName: message.UserName,
|
|
||||||
Message: message.Message,
|
|
||||||
CreatedAt: parsed,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) onMessage(e sse.SocketEvent) {
|
|
||||||
message := e.Payload["message"].(string)
|
|
||||||
|
|
||||||
if message == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := m.queries.GetUserBySessionId(context.Background(), e.Id)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error getting user: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
saved := m.service.InsertMessage(
|
|
||||||
&user,
|
|
||||||
e.RoomId,
|
|
||||||
message,
|
|
||||||
)
|
|
||||||
|
|
||||||
if saved != nil {
|
|
||||||
m.socketManager.BroadcastText(
|
|
||||||
e.RoomId,
|
|
||||||
h.Render(MessageRow(saved)),
|
|
||||||
func(conn sse.SocketConnection) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
package chat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chat/internal/db"
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MessageRow(message *Message) *h.Element {
|
|
||||||
return h.Div(
|
|
||||||
h.Attribute("hx-swap-oob", "beforeend"),
|
|
||||||
h.Class("flex flex-col gap-4 w-full break-words whitespace-normal"),
|
|
||||||
// Ensure container breaks long words
|
|
||||||
h.Id("messages"),
|
|
||||||
h.Div(
|
|
||||||
h.Class("flex flex-col gap-1"),
|
|
||||||
h.Div(
|
|
||||||
h.Class("flex gap-2 items-center"),
|
|
||||||
h.Pf(
|
|
||||||
message.UserName,
|
|
||||||
h.Class("font-bold"),
|
|
||||||
),
|
|
||||||
h.Pf(message.CreatedAt.In(time.Local).Format("01/02 03:04 PM")),
|
|
||||||
),
|
|
||||||
h.Article(
|
|
||||||
h.Class("break-words whitespace-normal"),
|
|
||||||
// Ensure message text wraps correctly
|
|
||||||
h.P(
|
|
||||||
h.Text(message.Message),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ConnectedUsers(users []db.User, myId string) *h.Element {
|
|
||||||
return h.Ul(
|
|
||||||
h.Attribute("hx-swap-oob", "outerHTML"),
|
|
||||||
h.Id("connected-users"),
|
|
||||||
h.Class("flex flex-col"),
|
|
||||||
h.List(users, func(user db.User, index int) *h.Element {
|
|
||||||
return connectedUser(user.Name, user.SessionID == myId)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func connectedUser(username string, isMe bool) *h.Element {
|
|
||||||
id := fmt.Sprintf("connected-user-%s", strings.ReplaceAll(username, "#", "-"))
|
|
||||||
return h.Li(
|
|
||||||
h.Id(id),
|
|
||||||
h.ClassX("truncate text-slate-700", h.ClassMap{
|
|
||||||
"font-bold": isMe,
|
|
||||||
}),
|
|
||||||
h.Text(username),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
package chat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chat/internal/db"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
UserId int64 `json:"userId"`
|
|
||||||
UserName string `json:"userName"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
queries *db.Queries
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService(locator *service.Locator) *Service {
|
|
||||||
return &Service{
|
|
||||||
queries: service.Get[db.Queries](locator),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) InsertMessage(user *db.User, roomId string, message string) *Message {
|
|
||||||
err := s.queries.InsertMessage(context.Background(), db.InsertMessageParams{
|
|
||||||
UserID: user.ID,
|
|
||||||
Username: user.Name,
|
|
||||||
ChatRoomID: roomId,
|
|
||||||
Message: message,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to insert message: %v\n", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &Message{
|
|
||||||
UserId: user.ID,
|
|
||||||
UserName: user.Name,
|
|
||||||
Message: message,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetUserBySession(sessionId string) (*db.User, error) {
|
|
||||||
user, err := s.queries.GetUserBySessionId(context.Background(), sessionId)
|
|
||||||
return &user, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CreateUser(name string) (*db.CreateUserRow, error) {
|
|
||||||
nameWithHash := fmt.Sprintf("%s#%s", name, uuid.NewString()[0:4])
|
|
||||||
sessionId := fmt.Sprintf("session-%s-%s", uuid.NewString(), uuid.NewString())
|
|
||||||
user, err := s.queries.CreateUser(context.Background(), db.CreateUserParams{
|
|
||||||
Name: nameWithHash,
|
|
||||||
SessionID: sessionId,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CreateRoom(name string) (*db.CreateChatRoomRow, error) {
|
|
||||||
room, err := s.queries.CreateChatRoom(context.Background(), db.CreateChatRoomParams{
|
|
||||||
ID: fmt.Sprintf("room-%s-%s", uuid.NewString()[0:8], name),
|
|
||||||
Name: name,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &room, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetRoom(id string) (*db.ChatRoom, error) {
|
|
||||||
room, err := s.queries.GetChatRoom(context.Background(), id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &room, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
package components
|
|
||||||
|
|
||||||
import "github.com/maddalax/htmgo/framework/h"
|
|
||||||
|
|
||||||
type ButtonProps struct {
|
|
||||||
Id string
|
|
||||||
Text string
|
|
||||||
Target string
|
|
||||||
Type string
|
|
||||||
Trigger string
|
|
||||||
Get string
|
|
||||||
Class string
|
|
||||||
Children []h.Ren
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrimaryButton(props ButtonProps) h.Ren {
|
|
||||||
props.Class = h.MergeClasses(props.Class, "border-slate-800 bg-slate-900 hover:bg-slate-800 text-white")
|
|
||||||
return Button(props)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SecondaryButton(props ButtonProps) h.Ren {
|
|
||||||
props.Class = h.MergeClasses(props.Class, "border-gray-700 bg-gray-700 text-white")
|
|
||||||
return Button(props)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Button(props ButtonProps) h.Ren {
|
|
||||||
|
|
||||||
text := h.Text(props.Text)
|
|
||||||
|
|
||||||
button := h.Button(
|
|
||||||
h.If(
|
|
||||||
props.Id != "",
|
|
||||||
h.Id(props.Id),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.Children != nil,
|
|
||||||
h.Children(props.Children...),
|
|
||||||
),
|
|
||||||
h.Class("flex gap-1 items-center justify-center border p-4 rounded cursor-hover", props.Class),
|
|
||||||
h.If(
|
|
||||||
props.Get != "",
|
|
||||||
h.Get(props.Get),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.Target != "",
|
|
||||||
h.HxTarget(props.Target),
|
|
||||||
),
|
|
||||||
h.IfElse(
|
|
||||||
props.Type != "",
|
|
||||||
h.Type(props.Type),
|
|
||||||
h.Type("button"),
|
|
||||||
),
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
|
|
||||||
return button
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package components
|
|
||||||
|
|
||||||
import "github.com/maddalax/htmgo/framework/h"
|
|
||||||
|
|
||||||
func FormError(error string) *h.Element {
|
|
||||||
return h.Div(
|
|
||||||
h.Id("form-error"),
|
|
||||||
h.Text(error),
|
|
||||||
h.If(
|
|
||||||
error != "",
|
|
||||||
h.Class("p-4 bg-rose-400 text-white rounded"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
package components
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/maddalax/htmgo/framework/hx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InputProps struct {
|
|
||||||
Id string
|
|
||||||
Label string
|
|
||||||
Name string
|
|
||||||
Type string
|
|
||||||
DefaultValue string
|
|
||||||
Placeholder string
|
|
||||||
Required bool
|
|
||||||
ValidationPath string
|
|
||||||
Error string
|
|
||||||
Children []h.Ren
|
|
||||||
}
|
|
||||||
|
|
||||||
func Input(props InputProps) *h.Element {
|
|
||||||
validation := h.If(
|
|
||||||
props.ValidationPath != "",
|
|
||||||
h.Children(
|
|
||||||
h.Post(props.ValidationPath, hx.BlurEvent),
|
|
||||||
h.Attribute("hx-swap", "innerHTML transition:true"),
|
|
||||||
h.Attribute("hx-target", "next div"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if props.Type == "" {
|
|
||||||
props.Type = "text"
|
|
||||||
}
|
|
||||||
|
|
||||||
input := h.Input(
|
|
||||||
props.Type,
|
|
||||||
h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"),
|
|
||||||
h.If(
|
|
||||||
props.Name != "",
|
|
||||||
h.Name(props.Name),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.Children != nil,
|
|
||||||
h.Children(props.Children...),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.Required,
|
|
||||||
h.Required(),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.Placeholder != "",
|
|
||||||
h.Placeholder(props.Placeholder),
|
|
||||||
),
|
|
||||||
h.If(
|
|
||||||
props.DefaultValue != "",
|
|
||||||
h.Attribute("value", props.DefaultValue),
|
|
||||||
),
|
|
||||||
validation,
|
|
||||||
)
|
|
||||||
|
|
||||||
wrapped := h.Div(
|
|
||||||
h.If(
|
|
||||||
props.Id != "",
|
|
||||||
h.Id(props.Id),
|
|
||||||
),
|
|
||||||
h.Class("flex flex-col gap-1"),
|
|
||||||
h.If(
|
|
||||||
props.Label != "",
|
|
||||||
h.Label(
|
|
||||||
h.Text(props.Label),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
input,
|
|
||||||
h.Div(
|
|
||||||
h.Id(props.Id+"-error"),
|
|
||||||
h.Class("text-red-500"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return wrapped
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
module chat
|
|
||||||
|
|
||||||
go 1.23.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
|
||||||
github.com/google/uuid v1.6.0
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.23
|
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
|
||||||
)
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b h1:m+xI+HBEQdie/Rs+mYI0HTFTMlYQSCv0l/siPDoywA4=
|
|
||||||
github.com/maddalax/htmgo/framework v1.0.7-0.20250703190716-06f01b3d7c1b/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DBTX interface {
|
|
||||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
|
||||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
|
||||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
|
||||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(db DBTX) *Queries {
|
|
||||||
return &Queries{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Queries struct {
|
|
||||||
db DBTX
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
|
||||||
return &Queries{
|
|
||||||
db: tx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ChatRoom struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
LastMessageSentAt sql.NullString
|
|
||||||
CreatedAt string
|
|
||||||
UpdatedAt string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
ID int64
|
|
||||||
ChatRoomID string
|
|
||||||
UserID int64
|
|
||||||
Username string
|
|
||||||
Message string
|
|
||||||
CreatedAt string
|
|
||||||
UpdatedAt string
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
ID int64
|
|
||||||
Name string
|
|
||||||
CreatedAt string
|
|
||||||
UpdatedAt string
|
|
||||||
SessionID string
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
_ "embed"
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed schema.sql
|
|
||||||
var ddl string
|
|
||||||
|
|
||||||
func Provide() *Queries {
|
|
||||||
db, err := sql.Open("sqlite3", "file:chat.db?cache=shared&_fk=1")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := db.ExecContext(context.Background(), ddl); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return New(db)
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
-- name: CreateChatRoom :one
|
|
||||||
INSERT INTO chat_rooms (id, name, created_at, updated_at)
|
|
||||||
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
RETURNING id, name, created_at, updated_at, last_message_sent_at;
|
|
||||||
|
|
||||||
-- name: InsertMessage :exec
|
|
||||||
INSERT INTO messages (chat_room_id, user_id, username, message, created_at, updated_at)
|
|
||||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
RETURNING id, chat_room_id, user_id, username, message, created_at, updated_at;
|
|
||||||
|
|
||||||
-- name: UpdateChatRoomLastMessageSentAt :exec
|
|
||||||
UPDATE chat_rooms
|
|
||||||
SET last_message_sent_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = ?;
|
|
||||||
|
|
||||||
-- name: GetChatRoom :one
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
last_message_sent_at,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
FROM chat_rooms
|
|
||||||
WHERE chat_rooms.id = ?;
|
|
||||||
|
|
||||||
-- name: CreateUser :one
|
|
||||||
INSERT INTO users (name, session_id, created_at, updated_at)
|
|
||||||
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
RETURNING id, name, session_id, created_at, updated_at;
|
|
||||||
|
|
||||||
-- name: GetLastMessages :many
|
|
||||||
SELECT
|
|
||||||
messages.id,
|
|
||||||
messages.chat_room_id,
|
|
||||||
messages.user_id,
|
|
||||||
users.name AS user_name,
|
|
||||||
messages.message,
|
|
||||||
messages.created_at,
|
|
||||||
messages.updated_at
|
|
||||||
FROM messages
|
|
||||||
JOIN users ON messages.user_id = users.id
|
|
||||||
WHERE messages.chat_room_id = ?
|
|
||||||
ORDER BY messages.created_at
|
|
||||||
LIMIT ?;
|
|
||||||
|
|
||||||
-- name: GetUserBySessionId :one
|
|
||||||
SELECT * FROM users WHERE session_id = ?;
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.27.0
|
|
||||||
// source: queries.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
const createChatRoom = `-- name: CreateChatRoom :one
|
|
||||||
INSERT INTO chat_rooms (id, name, created_at, updated_at)
|
|
||||||
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
RETURNING id, name, created_at, updated_at, last_message_sent_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateChatRoomParams struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateChatRoomRow struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
CreatedAt string
|
|
||||||
UpdatedAt string
|
|
||||||
LastMessageSentAt sql.NullString
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateChatRoom(ctx context.Context, arg CreateChatRoomParams) (CreateChatRoomRow, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createChatRoom, arg.ID, arg.Name)
|
|
||||||
var i CreateChatRoomRow
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.LastMessageSentAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const createUser = `-- name: CreateUser :one
|
|
||||||
INSERT INTO users (name, session_id, created_at, updated_at)
|
|
||||||
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
RETURNING id, name, session_id, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateUserParams struct {
|
|
||||||
Name string
|
|
||||||
SessionID string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateUserRow struct {
|
|
||||||
ID int64
|
|
||||||
Name string
|
|
||||||
SessionID string
|
|
||||||
CreatedAt string
|
|
||||||
UpdatedAt string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createUser, arg.Name, arg.SessionID)
|
|
||||||
var i CreateUserRow
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.SessionID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getChatRoom = `-- name: GetChatRoom :one
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
last_message_sent_at,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
FROM chat_rooms
|
|
||||||
WHERE chat_rooms.id = ?
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetChatRoom(ctx context.Context, id string) (ChatRoom, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getChatRoom, id)
|
|
||||||
var i ChatRoom
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.LastMessageSentAt,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLastMessages = `-- name: GetLastMessages :many
|
|
||||||
SELECT
|
|
||||||
messages.id,
|
|
||||||
messages.chat_room_id,
|
|
||||||
messages.user_id,
|
|
||||||
users.name AS user_name,
|
|
||||||
messages.message,
|
|
||||||
messages.created_at,
|
|
||||||
messages.updated_at
|
|
||||||
FROM messages
|
|
||||||
JOIN users ON messages.user_id = users.id
|
|
||||||
WHERE messages.chat_room_id = ?
|
|
||||||
ORDER BY messages.created_at
|
|
||||||
LIMIT ?
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetLastMessagesParams struct {
|
|
||||||
ChatRoomID string
|
|
||||||
Limit int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetLastMessagesRow struct {
|
|
||||||
ID int64
|
|
||||||
ChatRoomID string
|
|
||||||
UserID int64
|
|
||||||
UserName string
|
|
||||||
Message string
|
|
||||||
CreatedAt string
|
|
||||||
UpdatedAt string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetLastMessages(ctx context.Context, arg GetLastMessagesParams) ([]GetLastMessagesRow, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, getLastMessages, arg.ChatRoomID, arg.Limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetLastMessagesRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetLastMessagesRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ChatRoomID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.UserName,
|
|
||||||
&i.Message,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserBySessionId = `-- name: GetUserBySessionId :one
|
|
||||||
SELECT id, name, created_at, updated_at, session_id FROM users WHERE session_id = ?
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetUserBySessionId(ctx context.Context, sessionID string) (User, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getUserBySessionId, sessionID)
|
|
||||||
var i User
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.SessionID,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertMessage = `-- name: InsertMessage :exec
|
|
||||||
INSERT INTO messages (chat_room_id, user_id, username, message, created_at, updated_at)
|
|
||||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
RETURNING id, chat_room_id, user_id, username, message, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type InsertMessageParams struct {
|
|
||||||
ChatRoomID string
|
|
||||||
UserID int64
|
|
||||||
Username string
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) InsertMessage(ctx context.Context, arg InsertMessageParams) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, insertMessage,
|
|
||||||
arg.ChatRoomID,
|
|
||||||
arg.UserID,
|
|
||||||
arg.Username,
|
|
||||||
arg.Message,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateChatRoomLastMessageSentAt = `-- name: UpdateChatRoomLastMessageSentAt :exec
|
|
||||||
UPDATE chat_rooms
|
|
||||||
SET last_message_sent_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = ?
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) UpdateChatRoomLastMessageSentAt(ctx context.Context, id string) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, updateChatRoomLastMessageSentAt, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
CREATE TABLE IF NOT EXISTS users
|
|
||||||
(
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT NOT NULL UNIQUE,
|
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
session_id TEXT NOT NULL
|
|
||||||
) STRICT;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS chat_rooms
|
|
||||||
(
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
last_message_sent_at TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
) STRICT;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS messages
|
|
||||||
(
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
chat_room_id TEXT NOT NULL,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
username TEXT NOT NULL,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (chat_room_id) REFERENCES chat_rooms (id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
|
||||||
) STRICT;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_chat_room_id ON messages (chat_room_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages (user_id);
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package embedded
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OsFs struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (receiver OsFs) Open(name string) (fs.File, error) {
|
|
||||||
return os.Open(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOsFs() OsFs {
|
|
||||||
return OsFs{}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package routine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DebugLongRunning(name string, f func()) {
|
|
||||||
now := time.Now()
|
|
||||||
done := make(chan struct{}, 1)
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(time.Second * 5)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
elapsed := time.Since(now).Milliseconds()
|
|
||||||
fmt.Printf("function %s has not finished after %dms\n", name, elapsed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
f()
|
|
||||||
done <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chat/__htmgo"
|
|
||||||
"chat/chat"
|
|
||||||
"chat/internal/db"
|
|
||||||
"chat/sse"
|
|
||||||
"fmt"
|
|
||||||
"github.com/maddalax/htmgo/framework/h"
|
|
||||||
"github.com/maddalax/htmgo/framework/service"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
locator := service.NewLocator()
|
|
||||||
|
|
||||||
service.Set[db.Queries](locator, service.Singleton, db.Provide)
|
|
||||||
service.Set[sse.SocketManager](locator, service.Singleton, func() *sse.SocketManager {
|
|
||||||
return sse.NewSocketManager()
|
|
||||||
})
|
|
||||||
|
|
||||||
chatManager := chat.NewManager(locator)
|
|
||||||
go chatManager.StartListener()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
count := runtime.NumGoroutine()
|
|
||||||
fmt.Printf("goroutines: %d\n", count)
|
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
h.Start(h.AppOpts{
|
|
||||||
ServiceLocator: locator,
|
|
||||||
LiveReload: true,
|
|
||||||
Register: func(app *h.App) {
|
|
||||||
sub, err := fs.Sub(GetStaticAssets(), "assets/dist")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.FileServerFS(sub)
|
|
||||||
|
|
||||||
app.Router.Handle("/public/*", http.StripPrefix("/public", http.FileServerFS(sub)))
|
|
||||||
app.Router.Handle("/sse/chat/{id}", sse.Handle())
|
|
||||||
|
|
||||||
__htmgo.Register(app.Router)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue