init
This commit is contained in:
commit
c3d9af72ae
21 changed files with 4525 additions and 0 deletions
13
.editorconfig
Normal file
13
.editorconfig
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*.{vue,css,ts}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.zig-cache
|
||||
zig-out
|
||||
node_modules
|
||||
3
README.md
Normal file
3
README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# OwnMedia
|
||||
|
||||
Built with Zig 0.15.1 & Vite 7.1.12
|
||||
167
build.zig
Normal file
167
build.zig
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
const std = @import("std");
|
||||
|
||||
// Although this function looks imperative, it does not perform the build
|
||||
// directly and instead it mutates the build graph (`b`) that will be then
|
||||
// executed by an external runner. The functions in `std.Build` implement a DSL
|
||||
// for defining build steps and express dependencies between them, allowing the
|
||||
// build runner to parallelize the build automatically (and the cache system to
|
||||
// know when a step doesn't need to be re-run).
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Standard target options allow the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
// Standard optimization options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
// It's also possible to define more custom flags to toggle optional features
|
||||
// of this build script using `b.option()`. All defined flags (including
|
||||
// target and optimize options) will be listed when running `zig build --help`
|
||||
// in this directory.
|
||||
|
||||
// This creates a module, which represents a collection of source files alongside
|
||||
// some compilation options, such as optimization mode and linked system libraries.
|
||||
// Zig modules are the preferred way of making Zig code available to consumers.
|
||||
// addModule defines a module that we intend to make available for importing
|
||||
// to our consumers. We must give it a name because a Zig package can expose
|
||||
// multiple modules and consumers will need to be able to specify which
|
||||
// module they want to access.
|
||||
|
||||
const zap = b.dependency("zap", .{ .target = target, .optimize = optimize }).module("zap");
|
||||
|
||||
const sqlite = b.dependency("sqlite", .{ .target = target, .optimize = optimize }).module("sqlite");
|
||||
|
||||
const mod = b.addModule("Context", .{
|
||||
// The root source file is the "entry point" of this module. Users of
|
||||
// this module will only be able to access public declarations contained
|
||||
// in this file, which means that if you have declarations that you
|
||||
// intend to expose to consumers that were defined in other files part
|
||||
// of this module, you will have to make sure to re-export them from
|
||||
// the root file.
|
||||
.root_source_file = b.path("src/context.zig"),
|
||||
// Later on we'll use this module as the root module of a test executable
|
||||
// which requires us to specify a target.
|
||||
.target = target,
|
||||
.imports = &.{
|
||||
.{ .name = "zap", .module = zap },
|
||||
.{ .name = "sqlite", .module = sqlite },
|
||||
},
|
||||
});
|
||||
|
||||
// Here we define an executable. An executable needs to have a root module
|
||||
// which needs to expose a `main` function. While we could add a main function
|
||||
// to the module defined above, it's sometimes preferable to split business
|
||||
// logic and the CLI into two separate modules.
|
||||
//
|
||||
// If your goal is to create a Zig library for others to use, consider if
|
||||
// it might benefit from also exposing a CLI tool. A parser library for a
|
||||
// data serialization format could also bundle a CLI syntax checker, for example.
|
||||
//
|
||||
// If instead your goal is to create an executable, consider if users might
|
||||
// be interested in also being able to embed the core functionality of your
|
||||
// program in their own executable in order to avoid the overhead involved in
|
||||
// subprocessing your CLI tool.
|
||||
//
|
||||
// If neither case applies to you, feel free to delete the declaration you
|
||||
// don't need and to put everything under a single module.
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "ownmedia_zap",
|
||||
.root_module = b.createModule(.{
|
||||
// b.createModule defines a new module just like b.addModule but,
|
||||
// unlike b.addModule, it does not expose the module to consumers of
|
||||
// this package, which is why in this case we don't have to give it a name.
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
// Target and optimization levels must be explicitly wired in when
|
||||
// defining an executable or library (in the root module), and you
|
||||
// can also hardcode a specific target for an executable or library
|
||||
// definition if desireable (e.g. firmware for embedded devices).
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
// List of modules available for import in source files part of the
|
||||
// root module.
|
||||
.imports = &.{
|
||||
// Here "ownmedia_zap" is the name you will use in your source code to
|
||||
// import this module (e.g. `@import("ownmedia_zap")`). The name is
|
||||
// repeated because you are allowed to rename your imports, which
|
||||
// can be extremely useful in case of collisions (which can happen
|
||||
// importing modules from different packages).
|
||||
.{ .name = "Context", .module = mod },
|
||||
.{ .name = "zap", .module = zap },
|
||||
.{ .name = "sqlite", .module = sqlite },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// This declares intent for the executable to be installed into the
|
||||
// install prefix when running `zig build` (i.e. when executing the default
|
||||
// step). By default the install prefix is `zig-out/` but can be overridden
|
||||
// by passing `--prefix` or `-p`.
|
||||
b.installArtifact(exe);
|
||||
|
||||
// This creates a top level step. Top level steps have a name and can be
|
||||
// invoked by name when running `zig build` (e.g. `zig build run`).
|
||||
// This will evaluate the `run` step rather than the default step.
|
||||
// For a top level step to actually do something, it must depend on other
|
||||
// steps (e.g. a Run step, as we will see in a moment).
|
||||
const run_step = b.step("run", "Run the app");
|
||||
|
||||
// This creates a RunArtifact step in the build graph. A RunArtifact step
|
||||
// invokes an executable compiled by Zig. Steps will only be executed by the
|
||||
// runner if invoked directly by the user (in the case of top level steps)
|
||||
// or if another step depends on it, so it's up to you to define when and
|
||||
// how this Run step will be executed. In our case we want to run it when
|
||||
// the user runs `zig build run`, so we create a dependency link.
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// By making the run step depend on the default step, it will be run from the
|
||||
// installation directory rather than directly from within the cache directory.
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
|
||||
// This allows the user to pass arguments to the application in the build
|
||||
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// // Creates an executable that will run `test` blocks from the provided module.
|
||||
// // Here `mod` needs to define a target, which is why earlier we made sure to
|
||||
// // set the releative field.
|
||||
// const mod_tests = b.addTest(.{
|
||||
// .root_module = mod,
|
||||
// });
|
||||
|
||||
// // A run step that will run the test executable.
|
||||
// const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
|
||||
// Creates an executable that will run `test` blocks from the executable's
|
||||
// root module. Note that test executables only test one module at a time,
|
||||
// hence why we have to create two separate ones.
|
||||
const exe_tests = b.addTest(.{
|
||||
.root_module = exe.root_module,
|
||||
});
|
||||
|
||||
// A run step that will run the second test executable.
|
||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||
|
||||
// A top level step for running all tests. dependOn can be called multiple
|
||||
// times and since the two run steps do not depend on one another, this will
|
||||
// make the two of them run in parallel.
|
||||
const test_step = b.step("test", "Run tests");
|
||||
// test_step.dependOn(&run_mod_tests.step);
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
|
||||
// Just like flags, top level steps are also listed in the `--help` menu.
|
||||
//
|
||||
// The Zig build system is entirely implemented in userland, which means
|
||||
// that it cannot hook into private compiler APIs. All compilation work
|
||||
// orchestrated by the build system will result in other Zig compiler
|
||||
// subcommands being invoked with the right flags defined. You can observe
|
||||
// these invocations when one fails (or you pass a flag to increase
|
||||
// verbosity) to validate assumptions and diagnose problems.
|
||||
//
|
||||
// Lastly, the Zig build system is relatively simple and self-contained,
|
||||
// and reading its source code will allow you to master it.
|
||||
}
|
||||
52
build.zig.zon
Normal file
52
build.zig.zon
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
.{
|
||||
// This is the default name used by packages depending on this one. For
|
||||
// example, when a user runs `zig fetch --save <url>`, this field is used
|
||||
// as the key in the `dependencies` table. Although the user can choose a
|
||||
// different name, most users will stick with this provided value.
|
||||
//
|
||||
// It is redundant to include "zig" in this name because it is already
|
||||
// within the Zig package namespace.
|
||||
.name = .ownmedia_zap,
|
||||
// This is a [Semantic Version](https://semver.org/).
|
||||
// In a future version of Zig it will be used for package deduplication.
|
||||
.version = "0.0.0",
|
||||
// Together with name, this represents a globally unique package
|
||||
// identifier. This field is generated by the Zig toolchain when the
|
||||
// package is first created, and then *never changes*. This allows
|
||||
// unambiguous detection of one package being an updated version of
|
||||
// another.
|
||||
//
|
||||
// When forking a Zig project, this id should be regenerated (delete the
|
||||
// field and run `zig build`) if the upstream project is still maintained.
|
||||
// Otherwise, the fork is *hostile*, attempting to take control over the
|
||||
// original project's identity. Thus it is recommended to leave the comment
|
||||
// on the following line intact, so that it shows up in code reviews that
|
||||
// modify the field.
|
||||
.fingerprint = 0x3431cbff5a7945c4, // Changing this has security and trust implications.
|
||||
// Tracks the earliest Zig version that the package considers to be a
|
||||
// supported use case.
|
||||
.minimum_zig_version = "0.15.2",
|
||||
// This field is optional.
|
||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
||||
// Once all dependencies are fetched, `zig build` no longer requires
|
||||
// internet connectivity.
|
||||
.dependencies = .{
|
||||
.zap = .{
|
||||
.url = "git+https://github.com/zigzap/zap.git#76679f308c702cd8880201e6e93914e1d836a54b",
|
||||
.hash = "zap-0.10.6-GoeB8w-EJABTkxyoGH_Z8oMBYMxTBvn7YWk-DrJOHuDO",
|
||||
},
|
||||
.sqlite = .{
|
||||
.url = "git+https://github.com/vrischmann/zig-sqlite#3547902fe8f293637ca16a2b8a0bbaa89cf592a5",
|
||||
.hash = "sqlite-3.48.0-F2R_a52ODgDFoOf0S8ZamFe2k7JJwqM0cDzOF6nxr_uO",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
// For example...
|
||||
//"LICENSE",
|
||||
//"README.md",
|
||||
},
|
||||
}
|
||||
3394
package-lock.json
generated
Normal file
3394
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
11
package.json
Normal file
11
package.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.1.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inertiajs/vue3": "^2.2.11",
|
||||
"tailwindcss": "^4.1.16"
|
||||
}
|
||||
}
|
||||
1
public/.gitignore
vendored
Normal file
1
public/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
build
|
||||
4
public/favicon.svg
Normal file
4
public/favicon.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect fill="#2f365f" width="100" height="100" rx="50"/>
|
||||
<path fill="#ffffff" d="M36.929 86c-4.764 0-8.63-3.793-8.63-8.467v-5.43C22.395 70.655 18 65.398 18 59.15V34.346C18 26.981 24.096 21 31.603 21h37.794C76.904 21 83 26.98 83 34.346v24.82c0 7.366-6.096 13.346-13.603 13.346h-15.8a2 2 0 00-1.67.881c-2.614 4.203-5.902 7.854-9.769 10.813A8.473 8.473 0 0136.928 86zm-5.403-58.352c-3.868 0-7.026 3.05-7.026 6.819V59.13c0 3.754 3.142 6.819 7.026 6.819 1.836 0 3.319 1.439 3.319 3.222v8.226c0 1.205 1.048 1.955 2.014 1.955.42 0 .806-.14 1.16-.406 3.255-2.471 6.027-5.521 8.235-9.04 1.547-2.455 4.35-3.973 7.348-3.973h15.872c3.868 0 7.026-3.05 7.026-6.818V34.467c0-3.754-3.142-6.82-7.026-6.82H31.525z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 773 B |
504
src/App.zig
Normal file
504
src/App.zig
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
//! zap.App takes the zap.Endpoint concept one step further: instead of having
|
||||
//! only per-endpoint instance data (fields of your Endpoint struct), endpoints
|
||||
//! in a zap.App easily share a global 'App Context'.
|
||||
//!
|
||||
//! In addition to the global App Context, all Endpoint request handlers also
|
||||
//! receive an arena allocator for easy, care-free allocations. There is one
|
||||
//! arena allocator per thread, and arenas are reset after each request.
|
||||
//!
|
||||
//! Just like regular / legacy zap.Endpoints, returning errors from request
|
||||
//! handlers is OK. It's decided on a per-endpoint basis how errors are dealt
|
||||
//! with, via the ErrorStrategy enum field.
|
||||
//!
|
||||
//! See `App.Create()`.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const Thread = std.Thread;
|
||||
const RwLock = Thread.RwLock;
|
||||
|
||||
const zap = @import("zap");
|
||||
const Request = zap.Request;
|
||||
const HttpListener = zap.HttpListener;
|
||||
const ErrorStrategy = zap.Endpoint.ErrorStrategy;
|
||||
|
||||
pub const AppOpts = struct {
|
||||
/// ErrorStrategy for (optional) request handler if no endpoint matches
|
||||
default_error_strategy: ErrorStrategy = .log_to_console,
|
||||
arena_retain_capacity: usize = 16 * 1024 * 1024,
|
||||
};
|
||||
|
||||
/// creates an App with custom app context
|
||||
///
|
||||
/// About App Contexts:
|
||||
///
|
||||
/// ```zig
|
||||
/// const MyContext = struct {
|
||||
/// // You may (optionally) define the following global handlers:
|
||||
/// pub fn unhandledRequest(_: *MyContext, _: Allocator, _: Request) anyerror!void {}
|
||||
/// pub fn unhandledError(_: *MyContext, _: Request, _: anyerror) void {}
|
||||
/// };
|
||||
/// ```
|
||||
pub fn Create(
|
||||
/// Your user-defined "Global App Context" type
|
||||
comptime Context: type,
|
||||
) type {
|
||||
return struct {
|
||||
const App = @This();
|
||||
|
||||
// we make the following fields static so we can access them from a
|
||||
// context-free, pure zap request handler
|
||||
const InstanceData = struct {
|
||||
context: *Context = undefined,
|
||||
gpa: Allocator = undefined,
|
||||
opts: AppOpts = undefined,
|
||||
endpoints: std.ArrayListUnmanaged(*Endpoint.Interface) = .empty,
|
||||
|
||||
there_can_be_only_one: bool = false,
|
||||
track_arenas: std.AutoHashMapUnmanaged(Thread.Id, ArenaAllocator) = .empty,
|
||||
track_arena_lock: RwLock = .{},
|
||||
|
||||
/// the internal http listener
|
||||
listener: HttpListener = undefined,
|
||||
|
||||
/// function pointer to handler for otherwise unhandled requests.
|
||||
/// Will automatically be set if your Context provides an
|
||||
/// `unhandledRequest` function of type `fn(*Context, Allocator,
|
||||
/// Request) !void`.
|
||||
unhandled_request: ?*const fn (*Context, Allocator, Request) anyerror!void = null,
|
||||
|
||||
/// function pointer to handler for unhandled errors.
|
||||
/// Errors are unhandled if they are not logged but raised by the
|
||||
/// ErrorStrategy. Will automatically be set if your Context
|
||||
/// provides an `unhandledError` function of type `fn(*Context,
|
||||
/// Allocator, Request, anyerror) void`.
|
||||
unhandled_error: ?*const fn (*Context, Request, anyerror) void = null,
|
||||
};
|
||||
var _static: InstanceData = .{};
|
||||
|
||||
pub const Endpoint = struct {
|
||||
pub const Interface = struct {
|
||||
call: *const fn (*Interface, Request) anyerror!void = undefined,
|
||||
path: []const u8,
|
||||
destroy: *const fn (*Interface, Allocator) void = undefined,
|
||||
};
|
||||
pub fn Bind(ArbitraryEndpoint: type) type {
|
||||
return struct {
|
||||
endpoint: *ArbitraryEndpoint,
|
||||
interface: Interface,
|
||||
|
||||
// tbh: unnecessary, since we have it in _static
|
||||
app_context: *Context,
|
||||
|
||||
const Bound = @This();
|
||||
|
||||
pub fn unwrap(interface: *Interface) *Bound {
|
||||
const self: *Bound = @alignCast(@fieldParentPtr("interface", interface));
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn destroy(interface: *Interface, allocator: Allocator) void {
|
||||
const self: *Bound = @alignCast(@fieldParentPtr("interface", interface));
|
||||
allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn onRequestInterface(interface: *Interface, r: Request) !void {
|
||||
var self: *Bound = Bound.unwrap(interface);
|
||||
var arena = try get_arena();
|
||||
try self.onRequest(arena.allocator(), self.app_context, r);
|
||||
_ = arena.reset(.{ .retain_with_limit = _static.opts.arena_retain_capacity });
|
||||
}
|
||||
|
||||
pub fn onRequest(self: *Bound, arena: Allocator, app_context: *Context, r: Request) !void {
|
||||
// TODO: simplitfy this with @tagName?
|
||||
const ret = switch (r.methodAsEnum()) {
|
||||
.GET => callHandlerIfExist("get", self.endpoint, arena, app_context, r),
|
||||
.POST => callHandlerIfExist("post", self.endpoint, arena, app_context, r),
|
||||
.PUT => callHandlerIfExist("put", self.endpoint, arena, app_context, r),
|
||||
.DELETE => callHandlerIfExist("delete", self.endpoint, arena, app_context, r),
|
||||
.PATCH => callHandlerIfExist("patch", self.endpoint, arena, app_context, r),
|
||||
.OPTIONS => callHandlerIfExist("options", self.endpoint, arena, app_context, r),
|
||||
.HEAD => callHandlerIfExist("head", self.endpoint, arena, app_context, r),
|
||||
else => error.UnsupportedHtmlRequestMethod,
|
||||
};
|
||||
if (ret) {
|
||||
// handled without error
|
||||
} else |err| {
|
||||
switch (self.endpoint.*.error_strategy) {
|
||||
.raise => return err,
|
||||
.log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505),
|
||||
.log_to_console => zap.log.err(
|
||||
"Error in {} {s} : {}",
|
||||
.{ Bound, r.method orelse "(no method)", err },
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(ArbitraryEndpoint: type, endpoint: *ArbitraryEndpoint) Endpoint.Bind(ArbitraryEndpoint) {
|
||||
checkEndpointType(ArbitraryEndpoint);
|
||||
const BoundEp = Endpoint.Bind(ArbitraryEndpoint);
|
||||
return .{
|
||||
.endpoint = endpoint,
|
||||
.interface = .{
|
||||
.path = endpoint.path,
|
||||
.call = BoundEp.onRequestInterface,
|
||||
.destroy = BoundEp.destroy,
|
||||
},
|
||||
.app_context = _static.context,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn checkEndpointType(T: type) void {
|
||||
if (@hasField(T, "path")) {
|
||||
if (@FieldType(T, "path") != []const u8) {
|
||||
@compileError(@typeName(@FieldType(T, "path")) ++ " has wrong type, expected: []const u8");
|
||||
}
|
||||
} else {
|
||||
@compileError(@typeName(T) ++ " has no path field");
|
||||
}
|
||||
|
||||
if (@hasField(T, "error_strategy")) {
|
||||
if (@FieldType(T, "error_strategy") != ErrorStrategy) {
|
||||
@compileError(@typeName(@FieldType(T, "error_strategy")) ++ " has wrong type, expected: zap.Endpoint.ErrorStrategy");
|
||||
}
|
||||
} else {
|
||||
@compileError(@typeName(T) ++ " has no error_strategy field");
|
||||
}
|
||||
|
||||
const methods_to_check = [_][]const u8{
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch",
|
||||
"options",
|
||||
"head",
|
||||
};
|
||||
const params_to_check = [_]type{
|
||||
*T,
|
||||
Allocator,
|
||||
*Context,
|
||||
Request,
|
||||
};
|
||||
inline for (methods_to_check) |method| {
|
||||
if (@hasDecl(T, method)) {
|
||||
const Method = @TypeOf(@field(T, method));
|
||||
const method_info = @typeInfo(Method);
|
||||
if (method_info != .@"fn") {
|
||||
@compileError("Expected `" ++ @typeName(T) ++ "." ++ method ++ "` to be a request handler method, got: " ++ @typeName(Method));
|
||||
}
|
||||
|
||||
// now check parameters
|
||||
const params = method_info.@"fn".params;
|
||||
if (params.len != params_to_check.len) {
|
||||
@compileError(std.fmt.comptimePrint(
|
||||
"Expected method `{s}.{s}` to have {d} parameters, got {d}",
|
||||
.{
|
||||
@typeName(T),
|
||||
method,
|
||||
params_to_check.len,
|
||||
params.len,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
inline for (params_to_check, 0..) |param_type_expected, i| {
|
||||
if (params[i].type.? != param_type_expected) {
|
||||
@compileError(std.fmt.comptimePrint(
|
||||
"Expected parameter {d} of method {s}.{s} to be {s}, got {s}",
|
||||
.{
|
||||
i + 1,
|
||||
@typeName(T),
|
||||
method,
|
||||
@typeName(param_type_expected),
|
||||
@typeName(params[i].type.?),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const ret_type = method_info.@"fn".return_type.?;
|
||||
const ret_info = @typeInfo(ret_type);
|
||||
if (ret_info != .error_union) {
|
||||
@compileError("Expected return type of method `" ++ @typeName(T) ++ "." ++ method ++ "` to be !void, got: " ++ @typeName(ret_type));
|
||||
}
|
||||
if (ret_info.error_union.payload != void) {
|
||||
@compileError("Expected return type of method `" ++ @typeName(T) ++ "." ++ method ++ "` to be !void, got: !" ++ @typeName(ret_info.error_union.payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap an endpoint with an Authenticator
|
||||
pub fn Authenticating(EndpointType: type, Authenticator: type) type {
|
||||
return struct {
|
||||
authenticator: *Authenticator,
|
||||
ep: *EndpointType,
|
||||
path: []const u8,
|
||||
error_strategy: ErrorStrategy,
|
||||
const AuthenticatingEndpoint = @This();
|
||||
|
||||
/// Init the authenticating endpoint. Pass in a pointer to the endpoint
|
||||
/// you want to wrap, and the Authenticator that takes care of authenticating
|
||||
/// requests.
|
||||
pub fn init(e: *EndpointType, authenticator: *Authenticator) AuthenticatingEndpoint {
|
||||
return .{
|
||||
.authenticator = authenticator,
|
||||
.ep = e,
|
||||
.path = e.path,
|
||||
.error_strategy = e.error_strategy,
|
||||
};
|
||||
}
|
||||
|
||||
/// Authenticates GET requests using the Authenticator.
|
||||
pub fn get(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: Request) anyerror!void {
|
||||
try switch (self.authenticator.authenticateRequest(&request)) {
|
||||
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
|
||||
.AuthOK => callHandlerIfExist("get", self.ep, arena, context, request),
|
||||
.Handled => {},
|
||||
};
|
||||
}
|
||||
|
||||
/// Authenticates POST requests using the Authenticator.
|
||||
pub fn post(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: Request) anyerror!void {
|
||||
try switch (self.authenticator.authenticateRequest(&request)) {
|
||||
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
|
||||
.AuthOK => callHandlerIfExist("post", self.ep, arena, context, request),
|
||||
.Handled => {},
|
||||
};
|
||||
}
|
||||
|
||||
/// Authenticates PUT requests using the Authenticator.
|
||||
pub fn put(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
|
||||
try switch (self.authenticator.authenticateRequest(&request)) {
|
||||
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
|
||||
.AuthOK => callHandlerIfExist("put", self.ep, arena, context, request),
|
||||
.Handled => {},
|
||||
};
|
||||
}
|
||||
|
||||
/// Authenticates DELETE requests using the Authenticator.
|
||||
pub fn delete(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
|
||||
try switch (self.authenticator.authenticateRequest(&request)) {
|
||||
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
|
||||
.AuthOK => callHandlerIfExist("delete", self.ep, arena, context, request),
|
||||
.Handled => {},
|
||||
};
|
||||
}
|
||||
|
||||
/// Authenticates PATCH requests using the Authenticator.
|
||||
pub fn patch(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
|
||||
try switch (self.authenticator.authenticateRequest(&request)) {
|
||||
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
|
||||
.AuthOK => callHandlerIfExist("patch", self.ep, arena, context, request),
|
||||
.Handled => {},
|
||||
};
|
||||
}
|
||||
|
||||
/// Authenticates OPTIONS requests using the Authenticator.
|
||||
pub fn options(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
|
||||
try switch (self.authenticator.authenticateRequest(&request)) {
|
||||
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
|
||||
.AuthOK => callHandlerIfExist("options", self.ep, arena, context, request),
|
||||
.Handled => {},
|
||||
};
|
||||
}
|
||||
|
||||
/// Authenticates HEAD requests using the Authenticator.
|
||||
pub fn head(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
|
||||
try switch (self.authenticator.authenticateRequest(&request)) {
|
||||
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
|
||||
.AuthOK => callHandlerIfExist("head", self.ep, arena, context, request),
|
||||
.Handled => {},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const ListenerSettings = struct {
|
||||
/// IP interface, e.g. 0.0.0.0
|
||||
interface: [*c]const u8 = null,
|
||||
/// IP port to listen on
|
||||
port: usize,
|
||||
public_folder: ?[]const u8 = null,
|
||||
max_clients: ?isize = null,
|
||||
max_body_size: ?usize = null,
|
||||
timeout: ?u8 = null,
|
||||
tls: ?zap.Tls = null,
|
||||
};
|
||||
|
||||
pub fn init(gpa_: Allocator, context_: *Context, opts_: AppOpts) !void {
|
||||
if (_static.there_can_be_only_one) {
|
||||
return error.OnlyOneAppAllowed;
|
||||
}
|
||||
_static.context = context_;
|
||||
_static.gpa = gpa_;
|
||||
_static.opts = opts_;
|
||||
_static.there_can_be_only_one = true;
|
||||
|
||||
// set unhandled_request callback if provided by Context
|
||||
if (@hasDecl(Context, "unhandledRequest")) {
|
||||
// try if we can use it
|
||||
const Unhandled = @TypeOf(@field(Context, "unhandledRequest"));
|
||||
const Expected = fn (_: *Context, _: Allocator, _: Request) anyerror!void;
|
||||
if (Unhandled != Expected) {
|
||||
@compileError("`unhandledRequest` method of " ++ @typeName(Context) ++ " has wrong type:\n" ++ @typeName(Unhandled) ++ "\nexpected:\n" ++ @typeName(Expected));
|
||||
}
|
||||
_static.unhandled_request = Context.unhandledRequest;
|
||||
}
|
||||
if (@hasDecl(Context, "unhandledError")) {
|
||||
// try if we can use it
|
||||
const Unhandled = @TypeOf(@field(Context, "unhandledError"));
|
||||
const Expected = fn (_: *Context, _: Request, _: anyerror) void;
|
||||
if (Unhandled != Expected) {
|
||||
@compileError("`unhandledError` method of " ++ @typeName(Context) ++ " has wrong type:\n" ++ @typeName(Unhandled) ++ "\nexpected:\n" ++ @typeName(Expected));
|
||||
}
|
||||
_static.unhandled_error = Context.unhandledError;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit() void {
|
||||
// we created endpoint wrappers but only tracked their interfaces
|
||||
// hence, we need to destroy the wrappers through their interfaces
|
||||
if (false) {
|
||||
var it = _static.endpoints.iterator();
|
||||
while (it.next()) |kv| {
|
||||
const interface = kv.value_ptr;
|
||||
interface.*.destroy(_static.gpa);
|
||||
}
|
||||
} else {
|
||||
for (_static.endpoints.items) |interface| {
|
||||
interface.destroy(interface, _static.gpa);
|
||||
}
|
||||
}
|
||||
_static.endpoints.deinit(_static.gpa);
|
||||
|
||||
_static.track_arena_lock.lock();
|
||||
defer _static.track_arena_lock.unlock();
|
||||
|
||||
var it = _static.track_arenas.valueIterator();
|
||||
while (it.next()) |arena| {
|
||||
arena.deinit();
|
||||
}
|
||||
_static.track_arenas.deinit(_static.gpa);
|
||||
}
|
||||
|
||||
// This can be resolved at comptime so *perhaps it does affect optimiazation
|
||||
pub fn callHandlerIfExist(comptime fn_name: []const u8, e: anytype, arena: Allocator, ctx: *Context, r: Request) anyerror!void {
|
||||
const EndPoint = @TypeOf(e.*);
|
||||
if (@hasDecl(EndPoint, fn_name)) {
|
||||
return @field(EndPoint, fn_name)(e, arena, ctx, r);
|
||||
}
|
||||
zap.log.debug(
|
||||
"Unhandled `{s}` {s} request ({s} not implemented in {s})",
|
||||
.{ r.method orelse "<unknown>", r.path orelse "", fn_name, @typeName(Endpoint) },
|
||||
);
|
||||
r.setStatus(.method_not_allowed);
|
||||
try r.sendBody("405 - method not allowed\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn get_arena() !*ArenaAllocator {
|
||||
const thread_id = std.Thread.getCurrentId();
|
||||
_static.track_arena_lock.lockShared();
|
||||
if (_static.track_arenas.getPtr(thread_id)) |arena| {
|
||||
_static.track_arena_lock.unlockShared();
|
||||
return arena;
|
||||
} else {
|
||||
_static.track_arena_lock.unlockShared();
|
||||
_static.track_arena_lock.lock();
|
||||
defer _static.track_arena_lock.unlock();
|
||||
const arena = ArenaAllocator.init(_static.gpa);
|
||||
try _static.track_arenas.put(_static.gpa, thread_id, arena);
|
||||
return _static.track_arenas.getPtr(thread_id).?;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(endpoint: anytype) !void {
|
||||
for (_static.endpoints.items) |other| {
|
||||
if (std.mem.eql(
|
||||
u8,
|
||||
other.path,
|
||||
endpoint.path,
|
||||
)) {
|
||||
return zap.Endpoint.ListenerError.EndpointPathShadowError;
|
||||
}
|
||||
}
|
||||
const EndpointType = @typeInfo(@TypeOf(endpoint)).pointer.child;
|
||||
Endpoint.checkEndpointType(EndpointType);
|
||||
const bound = try _static.gpa.create(Endpoint.Bind(EndpointType));
|
||||
bound.* = Endpoint.init(EndpointType, endpoint);
|
||||
try _static.endpoints.append(_static.gpa, &bound.interface);
|
||||
|
||||
std.mem.sort(*Endpoint.Interface, _static.endpoints.items, {}, lessThanFnEndpointInterface);
|
||||
}
|
||||
|
||||
fn lessThanFnEndpointInterface(_: void, lh: *Endpoint.Interface, rh: *Endpoint.Interface) bool {
|
||||
return lh.path.len > rh.path.len;
|
||||
}
|
||||
|
||||
pub fn listen(l: ListenerSettings) !void {
|
||||
_static.listener = HttpListener.init(.{
|
||||
.interface = l.interface,
|
||||
.port = l.port,
|
||||
.public_folder = l.public_folder,
|
||||
.max_clients = l.max_clients,
|
||||
.max_body_size = l.max_body_size,
|
||||
.timeout = l.timeout,
|
||||
.tls = l.tls,
|
||||
|
||||
.on_request = onRequest,
|
||||
});
|
||||
try _static.listener.listen();
|
||||
}
|
||||
|
||||
fn sanitizePath(path: []const u8) []const u8 {
|
||||
if (path.len > 1 and path[path.len - 1] == '/') {
|
||||
return path[0..(path.len - 1)];
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
fn onRequest(r: Request) !void {
|
||||
if (r.path) |unsan_p| {
|
||||
const p = sanitizePath(unsan_p);
|
||||
for (_static.endpoints.items) |interface| {
|
||||
if (std.mem.startsWith(u8, p, interface.path) and std.mem.eql(u8, p, interface.path)) {
|
||||
return interface.call(interface, r) catch |err| {
|
||||
// if error is not dealt with in the interface, e.g.
|
||||
// if error strategy is .raise:
|
||||
if (_static.unhandled_error) |error_cb| {
|
||||
error_cb(_static.context, r, err);
|
||||
} else {
|
||||
zap.log.err(
|
||||
"App.Endpoint onRequest error {} in endpoint interface {}\n",
|
||||
.{ err, interface },
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is basically the "not found" handler
|
||||
if (_static.unhandled_request) |user_handler| {
|
||||
var arena = try get_arena();
|
||||
user_handler(_static.context, arena.allocator(), r) catch |err| {
|
||||
switch (_static.opts.default_error_strategy) {
|
||||
.raise => if (_static.unhandled_error) |error_cb| {
|
||||
error_cb(_static.context, r, err);
|
||||
} else {
|
||||
zap.Logging.on_uncaught_error("App on_request", err);
|
||||
},
|
||||
.log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505),
|
||||
.log_to_console => zap.log.err("Error in {} {s} : {}", .{ App, r.method orelse "(no method)", err }),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
83
src/context.zig
Normal file
83
src/context.zig
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
const zap = @import("zap");
|
||||
const sqlite = @import("sqlite");
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
const optimize_mode = builtin.mode;
|
||||
|
||||
db_connection: sqlite.Db,
|
||||
counter: u32 = 0,
|
||||
|
||||
pub fn init(connection: sqlite.Db) @This() {
|
||||
return .{
|
||||
.db_connection = connection,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn SendInertiaResponse(endpoint: anytype, r: zap.Request, arena: Allocator, component: []const u8, props: anytype) !void {
|
||||
const inertia_json = try std.fmt.allocPrint(
|
||||
arena,
|
||||
"{f}",
|
||||
.{std.json.fmt(.{
|
||||
.component = component,
|
||||
.props = props,
|
||||
.url = endpoint.path,
|
||||
.version = "",
|
||||
}, .{
|
||||
.escape_unicode = true,
|
||||
})},
|
||||
);
|
||||
|
||||
if (r.getHeader("x-inertia")) |_| {
|
||||
try r.setHeader("X-Inertia", "true");
|
||||
try r.setHeader("Content-Type", "application/json");
|
||||
|
||||
r.setStatus(.ok);
|
||||
try r.sendBody(inertia_json);
|
||||
} else {
|
||||
try r.setHeader("Content-Type", "text/html");
|
||||
|
||||
const show_unbuilt_assets = (optimize_mode == .Debug) and (r.getHeader("x-vite") != null);
|
||||
|
||||
const response_text = try std.fmt.allocPrint(
|
||||
arena,
|
||||
@embedFile("root.html"),
|
||||
.{
|
||||
(if (show_unbuilt_assets)
|
||||
"/src/main.ts"
|
||||
else
|
||||
"/build/main.js"),
|
||||
(if (show_unbuilt_assets)
|
||||
"/src/style.css"
|
||||
else
|
||||
"/build/style.css"),
|
||||
HtmlEncodeFormatter{ .input = inertia_json },
|
||||
},
|
||||
);
|
||||
|
||||
r.setStatus(.ok);
|
||||
try r.sendBody(response_text);
|
||||
}
|
||||
}
|
||||
|
||||
pub const HtmlEncodeFormatter = struct {
|
||||
input: []const u8,
|
||||
pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
for (self.input) |char_byte| {
|
||||
switch (char_byte) {
|
||||
'<' => _ = try writer.write("<"),
|
||||
'>' => _ = try writer.write(">"),
|
||||
'&' => _ = try writer.write("&"),
|
||||
'"' => _ = try writer.write("""),
|
||||
'\'' => _ = try writer.write("'"),
|
||||
else => _ = try writer.writeByte(char_byte),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn unhandledRequest(ctx: *@This(), arena: Allocator, r: zap.Request) anyerror!void {
|
||||
_ = ctx;
|
||||
try SendInertiaResponse(.{ .path = r.path.? }, r, arena, "404", .{});
|
||||
}
|
||||
19
src/layout/Layout.vue
Normal file
19
src/layout/Layout.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script setup>
|
||||
import { Link } from "@inertiajs/vue3";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="p-3 bg-rose-800 dark:bg-rose-950">
|
||||
<div class="max-w-5xl flex gap-2 mx-auto">
|
||||
<Link href="/" class="text-white font-extrabold">
|
||||
<img src="../../favicon.svg" class="h-8 inline" />
|
||||
<span class="px-1 relative top-0.5">OwnMedia</span>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
<div class="p-4">
|
||||
<div class="p-4 bg-rose-950 rounded">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
15
src/main.ts
Normal file
15
src/main.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { createApp, h } from 'vue'
|
||||
import { createInertiaApp } from '@inertiajs/vue3'
|
||||
|
||||
createInertiaApp({
|
||||
resolve: name => {
|
||||
const modules = import.meta.glob('./modules/**/*.vue', { eager: false })
|
||||
return modules[`./modules/${name}.vue`]();
|
||||
},
|
||||
setup({ el, App, props, plugin }) {
|
||||
createApp({ render: () => h(App, props) })
|
||||
.use(plugin)
|
||||
.mount(el)
|
||||
},
|
||||
id: "app"
|
||||
})
|
||||
121
src/main.zig
Normal file
121
src/main.zig
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
//!
|
||||
//! Part of the Zap examples.
|
||||
//!
|
||||
//! Build me with `zig build app_basic`.
|
||||
//! Run me with `zig build run-app_basic`.
|
||||
//!
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
const optimize_mode = builtin.mode;
|
||||
|
||||
const zap = @import("zap");
|
||||
const sqlite = @import("sqlite");
|
||||
|
||||
// The global Application Context
|
||||
const Context = @import("Context");
|
||||
|
||||
// A very simple endpoint handling only GET requests
|
||||
const SimpleEndpoint = struct {
|
||||
|
||||
// zap.App.Endpoint Interface part
|
||||
path: []const u8,
|
||||
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
|
||||
|
||||
// data specific for this endpoint
|
||||
component: []const u8,
|
||||
|
||||
pub fn init(path: []const u8, data: []const u8) SimpleEndpoint {
|
||||
return .{
|
||||
.path = path,
|
||||
.component = data,
|
||||
};
|
||||
}
|
||||
|
||||
// handle GET requests
|
||||
pub fn get(e: *SimpleEndpoint, arena: Allocator, context: *Context, r: zap.Request) !void {
|
||||
const thread_id = std.Thread.getCurrentId();
|
||||
|
||||
try Context.SendInertiaResponse(e, r, arena, e.component, .{
|
||||
.thread_id = thread_id,
|
||||
.counter = context.counter,
|
||||
});
|
||||
context.*.counter += 1;
|
||||
}
|
||||
};
|
||||
|
||||
const StopEndpoint = struct {
|
||||
path: []const u8,
|
||||
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
|
||||
|
||||
pub fn get(_: *StopEndpoint, _: Allocator, context: *Context, _: zap.Request) !void {
|
||||
std.debug.print(
|
||||
\\Before I stop, let me dump the app context:
|
||||
\\db_connection='{}'
|
||||
\\
|
||||
\\
|
||||
, .{context.*.db_connection});
|
||||
zap.stop();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
// setup allocations
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{
|
||||
// just to be explicit
|
||||
.thread_safe = true,
|
||||
}) = .{};
|
||||
defer std.debug.print("\n\nLeaks detected: {}\n\n", .{gpa.deinit() != .ok});
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var db = try sqlite.Db.init(.{
|
||||
.mode = sqlite.Db.Mode{ .File = "./storage/mydata.db" },
|
||||
.open_flags = .{
|
||||
.write = true,
|
||||
.create = true,
|
||||
},
|
||||
.threading_mode = .MultiThread,
|
||||
});
|
||||
defer db.deinit();
|
||||
|
||||
// create an app context
|
||||
var my_context = Context.init(db);
|
||||
|
||||
// create an App instance
|
||||
// const App = zap.App.Create(Context);
|
||||
const App = @import("./App.zig").Create(Context);
|
||||
try App.init(allocator, &my_context, .{});
|
||||
defer App.deinit();
|
||||
|
||||
// create the endpoints
|
||||
var home_endpoint = SimpleEndpoint.init("/", "home/home");
|
||||
var test_endpoint = SimpleEndpoint.init("/test", "home/test");
|
||||
var stop_endpoint: StopEndpoint = .{ .path = "/stop" };
|
||||
|
||||
// register the endpoints with the App
|
||||
try App.register(&stop_endpoint);
|
||||
try App.register(&home_endpoint);
|
||||
try App.register(&test_endpoint);
|
||||
|
||||
// listen on the network
|
||||
try App.listen(.{
|
||||
.interface = "0.0.0.0",
|
||||
.port = 3000,
|
||||
.public_folder = "./public",
|
||||
});
|
||||
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
|
||||
|
||||
std.debug.print(
|
||||
\\ Try me via:
|
||||
\\ curl http://localhost:3000/test
|
||||
\\ Stop me via:
|
||||
\\ curl http://localhost:3000/stop
|
||||
\\
|
||||
, .{});
|
||||
|
||||
// start worker threads -- only 1 process!!!
|
||||
zap.start(.{
|
||||
.threads = 2,
|
||||
.workers = 1,
|
||||
});
|
||||
}
|
||||
13
src/modules/404.vue
Normal file
13
src/modules/404.vue
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script setup>
|
||||
import Layout from "@/layout/Layout.vue";
|
||||
import { Head, Link } from "@inertiajs/vue3";
|
||||
|
||||
defineProps({ thread_id: Number, counter: Number });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<h1>404</h1>
|
||||
<Link href="/" class="p-button mt-4">Back Home</Link>
|
||||
</Layout>
|
||||
</template>
|
||||
17
src/modules/home/home.vue
Normal file
17
src/modules/home/home.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script setup>
|
||||
import Layout from "@/layout/Layout.vue";
|
||||
import { Head, Link } from "@inertiajs/vue3";
|
||||
|
||||
defineProps({ thread_id: Number, counter: Number });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<h1>Home</h1>
|
||||
<Head title="Welcome" />
|
||||
<h1 class="">Welcome</h1>
|
||||
<p>The thread id is '{{ thread_id }}'.</p>
|
||||
<p>The counter is at '{{ counter }}'.</p>
|
||||
<Link href="/test" class="p-button mt-4">To Test</Link>
|
||||
</Layout>
|
||||
</template>
|
||||
18
src/modules/home/test.vue
Normal file
18
src/modules/home/test.vue
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script setup>
|
||||
import Layout from "@/layout/Layout.vue";
|
||||
import { Head, Link } from "@inertiajs/vue3";
|
||||
|
||||
defineProps({ thread_id: Number, counter: Number });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<h1>/test</h1>
|
||||
<Head title="Welcome" />
|
||||
<h1 class="">Welcome</h1>
|
||||
<p>The thread id is '{{ thread_id }}'.</p>
|
||||
<p>The counter is at '{{ counter }}'.</p>
|
||||
<Link href="/test" class="p-button mt-4 mr-2">Again!</Link>
|
||||
<Link href="/" class="p-button mt-4">Back Home</Link>
|
||||
</Layout>
|
||||
</template>
|
||||
11
src/root.html
Normal file
11
src/root.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg"/>
|
||||
<script type="module" src="{s}"></script>
|
||||
<link href="{s}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" data-page="{f}"></div>
|
||||
</body>
|
||||
</html>
|
||||
13
src/style.css
Normal file
13
src/style.css
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
/* @custom-variant dark (&:where(.dark, .dark *)); */
|
||||
|
||||
body {
|
||||
background: var(--color-slate-100);
|
||||
color: black;
|
||||
|
||||
@variant dark {
|
||||
color: white;
|
||||
background: var(--color-slate-900);
|
||||
}
|
||||
}
|
||||
2
storage/.gitignore
vendored
Normal file
2
storage/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
61
vite.config.ts
Normal file
61
vite.config.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { fileURLToPath, URL } from 'node:url';
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
const inputs = {
|
||||
main: "./src/main.ts",
|
||||
style: "./src/style.css",
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwindcss()],
|
||||
base: "/",
|
||||
publicDir: false,
|
||||
build: {
|
||||
outDir: "public/build",
|
||||
minify: false,
|
||||
sourcemap: "inline",
|
||||
|
||||
chunkSizeWarningLimit: 2000,
|
||||
|
||||
rollupOptions: {
|
||||
input: inputs,
|
||||
output: {
|
||||
entryFileNames: "[name].js",
|
||||
assetFileNames: ({ name }) => {
|
||||
if (name && name.endsWith(".css")) {
|
||||
return "[name].css";
|
||||
}
|
||||
return "[name].[ext]";
|
||||
},
|
||||
|
||||
inlineDynamicImports: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'^/(?!src|node_modules|@).*$': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
configure(proxy, options) {
|
||||
proxy.on('proxyReq', (proxyReq, req, res) => {
|
||||
proxyReq.setHeader('X-Vite', 'true');
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
origin: 'http://localhost:3000'
|
||||
},
|
||||
define: {
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false,
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue