mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -1,7 +1,20 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Roujtes",
|
||||
"program": "${workspaceFolder}/build/debug/macos-x86_64/esdev",
|
||||
"args": [
|
||||
"./routes",
|
||||
"--resolve=dev",
|
||||
"--outdir=out"
|
||||
// "--public-url=https://localhost:9000/"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/demos/css-stress-test",
|
||||
"console": "internalConsole"
|
||||
},
|
||||
|
||||
// {
|
||||
// "type": "lldb",
|
||||
|
||||
30
build.zig
30
build.zig
@@ -13,6 +13,8 @@ pub fn addPicoHTTP(step: *std.build.LibExeObjStep, dir: []const u8) void {
|
||||
step.addIncludeDir("src/deps");
|
||||
}
|
||||
|
||||
const ENABLE_JAVASCRIPT_BUILD = false;
|
||||
|
||||
pub fn build(b: *std.build.Builder) void {
|
||||
// Standard target options allows the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
@@ -118,26 +120,34 @@ pub fn build(b: *std.build.Builder) void {
|
||||
// exe.want_lto = true;
|
||||
if (!target.getCpuArch().isWasm()) {
|
||||
addPicoHTTP(exe, cwd);
|
||||
javascript = b.addExecutable("spjs", "src/main_javascript.zig");
|
||||
addPicoHTTP(javascript, cwd);
|
||||
javascript.packages = std.ArrayList(std.build.Pkg).fromOwnedSlice(std.heap.page_allocator, std.heap.page_allocator.dupe(std.build.Pkg, exe.packages.items) catch unreachable);
|
||||
javascript.setOutputDir(output_dir);
|
||||
javascript.setBuildMode(mode);
|
||||
javascript.linkLibC();
|
||||
if (ENABLE_JAVASCRIPT_BUILD) {
|
||||
javascript = b.addExecutable("spjs", "src/main_javascript.zig");
|
||||
addPicoHTTP(javascript, cwd);
|
||||
javascript.packages = std.ArrayList(std.build.Pkg).fromOwnedSlice(std.heap.page_allocator, std.heap.page_allocator.dupe(std.build.Pkg, exe.packages.items) catch unreachable);
|
||||
javascript.setOutputDir(output_dir);
|
||||
javascript.setBuildMode(mode);
|
||||
javascript.linkLibC();
|
||||
}
|
||||
|
||||
// javascript.linkLibCpp();
|
||||
|
||||
if (target.getOsTag() == .macos) {
|
||||
javascript.linkFramework("JavaScriptCore");
|
||||
if (ENABLE_JAVASCRIPT_BUILD) {
|
||||
javascript.linkFramework("JavaScriptCore");
|
||||
}
|
||||
exe.linkFramework("JavascriptCore");
|
||||
}
|
||||
|
||||
javascript.strip = false;
|
||||
if (ENABLE_JAVASCRIPT_BUILD) {
|
||||
javascript.strip = false;
|
||||
}
|
||||
}
|
||||
|
||||
exe.install();
|
||||
|
||||
if (!target.getCpuArch().isWasm()) {
|
||||
javascript.install();
|
||||
if (ENABLE_JAVASCRIPT_BUILD) {
|
||||
javascript.install();
|
||||
}
|
||||
}
|
||||
|
||||
const run_cmd = exe.run();
|
||||
|
||||
2
demos/css-stress-test/routes/[dynamic-dir]/[id].js
Normal file
2
demos/css-stress-test/routes/[dynamic-dir]/[id].js
Normal file
@@ -0,0 +1,2 @@
|
||||
import { Button } from "../../src/components/button.tsx";
|
||||
console.log("hi1.js", Button.name);
|
||||
@@ -0,0 +1,2 @@
|
||||
import { Button } from "../../src/components/button.tsx";
|
||||
console.log("hi1.js", Button.name);
|
||||
2
demos/css-stress-test/routes/hello/2nd-level.jsx
Normal file
2
demos/css-stress-test/routes/hello/2nd-level.jsx
Normal file
@@ -0,0 +1,2 @@
|
||||
import { Button } from "../../src/components/button.tsx";
|
||||
console.log("hi1.js", Button.name);
|
||||
2
demos/css-stress-test/routes/hi1.js
Normal file
2
demos/css-stress-test/routes/hi1.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import { Button } from "../src/components/button.tsx";
|
||||
console.log("hi1.js", Button.name);
|
||||
6
packages/speedy-nextjs/package.json
Normal file
6
packages/speedy-nextjs/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "speedy-nextjs",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -123,10 +123,7 @@ pub fn BSSList(comptime ValueType: type, comptime _count: anytype) type {
|
||||
pub var backing_buf_used: u16 = 0;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Self = @This();
|
||||
pub const ListIndex = packed struct {
|
||||
index: u31,
|
||||
is_overflowing: bool = false,
|
||||
};
|
||||
|
||||
overflow_list: std.ArrayListUnmanaged(ValueType),
|
||||
allocator: *Allocator,
|
||||
|
||||
@@ -145,10 +142,10 @@ pub fn BSSList(comptime ValueType: type, comptime _count: anytype) type {
|
||||
return backing_buf_used >= @as(u16, count);
|
||||
}
|
||||
|
||||
pub fn at(self: *const Self, index: ListIndex) ?*ValueType {
|
||||
pub fn at(self: *const Self, index: IndexType) ?*ValueType {
|
||||
if (index.index == NotFound.index or index.index == Unassigned.index) return null;
|
||||
|
||||
if (index.is_overflowing) {
|
||||
if (index.is_overflow) {
|
||||
return &self.overflow_list.items[index.index];
|
||||
} else {
|
||||
return &backing_buf[index.index];
|
||||
@@ -159,9 +156,9 @@ pub fn BSSList(comptime ValueType: type, comptime _count: anytype) type {
|
||||
return isSliceInBuffer(value, backing_buf);
|
||||
}
|
||||
|
||||
pub fn append(self: *Self, value: ValueType) !ListIndex {
|
||||
var result = ListIndex{ .index = std.math.maxInt(u31), .is_overflowing = backing_buf_used > max_index };
|
||||
if (result.is_overflowing) {
|
||||
pub fn append(self: *Self, value: ValueType) !IndexType {
|
||||
var result = IndexType{ .index = std.math.maxInt(u31), .is_overflow = backing_buf_used > max_index };
|
||||
if (result.is_overflow) {
|
||||
result.index = @intCast(u31, self.overflow_list.items.len);
|
||||
try self.overflow_list.append(self.allocator, value);
|
||||
} else {
|
||||
@@ -176,10 +173,10 @@ pub fn BSSList(comptime ValueType: type, comptime _count: anytype) type {
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn update(self: *Self, result: *ListIndex, value: ValueType) !*ValueType {
|
||||
pub fn update(self: *Self, result: *IndexType, value: ValueType) !*ValueType {
|
||||
if (result.index.index == NotFound.index or result.index.index == Unassigned.index) {
|
||||
result.index.is_overflowing = backing_buf_used > max_index;
|
||||
if (result.index.is_overflowing) {
|
||||
result.index.is_overflow = backing_buf_used > max_index;
|
||||
if (result.index.is_overflow) {
|
||||
result.index.index = @intCast(u31, self.overflow_list.items.len);
|
||||
} else {
|
||||
result.index.index = backing_buf_used;
|
||||
@@ -190,7 +187,7 @@ pub fn BSSList(comptime ValueType: type, comptime _count: anytype) type {
|
||||
}
|
||||
}
|
||||
|
||||
if (result.index.is_overflowing) {
|
||||
if (result.index.is_overflow) {
|
||||
if (self.overflow_list.items.len == result.index.index) {
|
||||
const real_index = self.overflow_list.items.len;
|
||||
try self.overflow_list.append(self.allocator, value);
|
||||
@@ -206,7 +203,7 @@ pub fn BSSList(comptime ValueType: type, comptime _count: anytype) type {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(self: *Self, index: ListIndex) void {
|
||||
pub fn remove(self: *Self, index: IndexType) void {
|
||||
@compileError("Not implemented yet.");
|
||||
// switch (index) {
|
||||
// Unassigned.index => {
|
||||
@@ -234,7 +231,9 @@ pub fn BSSList(comptime ValueType: type, comptime _count: anytype) type {
|
||||
}
|
||||
};
|
||||
}
|
||||
pub fn BSSStringList(comptime _count: usize, comptime item_length: usize) type {
|
||||
pub fn BSSStringList(comptime _count: usize, comptime _item_length: usize) type {
|
||||
// + 1 for sentinel
|
||||
const item_length = _item_length + 1;
|
||||
const count = _count * 2;
|
||||
const max_index = count - 1;
|
||||
const ValueType = []const u8;
|
||||
@@ -246,10 +245,7 @@ pub fn BSSStringList(comptime _count: usize, comptime item_length: usize) type {
|
||||
pub var backing_buf_used: u64 = undefined;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Self = @This();
|
||||
pub const ListIndex = packed struct {
|
||||
index: u31,
|
||||
is_overflowing: bool = false,
|
||||
};
|
||||
|
||||
overflow_list: std.ArrayListUnmanaged(ValueType),
|
||||
allocator: *Allocator,
|
||||
|
||||
@@ -271,7 +267,7 @@ pub fn BSSStringList(comptime _count: usize, comptime item_length: usize) type {
|
||||
pub fn at(self: *const Self, index: IndexType) ?ValueType {
|
||||
if (index.index == NotFound.index or index.index == Unassigned.index) return null;
|
||||
|
||||
if (index.is_overflowing) {
|
||||
if (index.is_overflow) {
|
||||
return &self.overflow_list.items[index.index];
|
||||
} else {
|
||||
return &slice_buf[index.index];
|
||||
@@ -286,20 +282,67 @@ pub fn BSSStringList(comptime _count: usize, comptime item_length: usize) type {
|
||||
return constStrToU8(slice);
|
||||
}
|
||||
|
||||
pub fn append(self: *Self, _value: anytype) ![]const u8 {
|
||||
var value = _value;
|
||||
if (value.len + backing_buf_used < backing_buf.len - 1) {
|
||||
pub fn append(self: *Self, comptime AppendType: type, _value: AppendType) ![]const u8 {
|
||||
const value_len: usize = brk: {
|
||||
switch (comptime AppendType) {
|
||||
[]const u8, []u8 => {
|
||||
break :brk _value.len;
|
||||
},
|
||||
else => {
|
||||
var len: usize = 0;
|
||||
for (_value) |val| {
|
||||
len += val.len;
|
||||
}
|
||||
break :brk len;
|
||||
},
|
||||
}
|
||||
unreachable;
|
||||
} + 1;
|
||||
|
||||
var value: [:0]u8 = undefined;
|
||||
if (value_len + backing_buf_used < backing_buf.len - 1) {
|
||||
const start = backing_buf_used;
|
||||
backing_buf_used += value.len;
|
||||
std.mem.copy(u8, backing_buf[start..backing_buf_used], _value);
|
||||
value = backing_buf[start..backing_buf_used];
|
||||
backing_buf_used += value_len;
|
||||
|
||||
switch (AppendType) {
|
||||
[]const u8, []u8 => {
|
||||
std.mem.copy(u8, backing_buf[start .. backing_buf_used - 1], _value);
|
||||
backing_buf[backing_buf_used - 1] = 0;
|
||||
},
|
||||
else => {
|
||||
var remainder = backing_buf[start..];
|
||||
for (_value) |val| {
|
||||
std.mem.copy(u8, remainder, val);
|
||||
remainder = remainder[val.len..];
|
||||
}
|
||||
remainder[0] = 0;
|
||||
},
|
||||
}
|
||||
|
||||
value = backing_buf[start .. backing_buf_used - 1 :0];
|
||||
} else {
|
||||
value = try self.allocator.dupe(u8, _value);
|
||||
var value_buf = try self.allocator.alloc(u8, value_len);
|
||||
|
||||
switch (comptime AppendType) {
|
||||
[]const u8, []u8 => {
|
||||
std.mem.copy(u8, value_buf, _value);
|
||||
},
|
||||
else => {
|
||||
var remainder = value_buf;
|
||||
for (_value) |val| {
|
||||
std.mem.copy(u8, remainder, val);
|
||||
remainder = remainder[val.len..];
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
value_buf[value_len - 1] = 0;
|
||||
value = value_buf[0 .. value_len - 1 :0];
|
||||
}
|
||||
|
||||
var result = ListIndex{ .index = std.math.maxInt(u31), .is_overflowing = slice_buf_used > max_index };
|
||||
var result = IndexType{ .index = std.math.maxInt(u31), .is_overflow = slice_buf_used > max_index };
|
||||
|
||||
if (result.is_overflowing) {
|
||||
if (result.is_overflow) {
|
||||
result.index = @intCast(u31, self.overflow_list.items.len);
|
||||
} else {
|
||||
result.index = slice_buf_used;
|
||||
@@ -309,7 +352,7 @@ pub fn BSSStringList(comptime _count: usize, comptime item_length: usize) type {
|
||||
}
|
||||
}
|
||||
|
||||
if (result.is_overflowing) {
|
||||
if (result.is_overflow) {
|
||||
if (self.overflow_list.items.len == result.index) {
|
||||
const real_index = self.overflow_list.items.len;
|
||||
try self.overflow_list.append(self.allocator, value);
|
||||
@@ -325,7 +368,7 @@ pub fn BSSStringList(comptime _count: usize, comptime item_length: usize) type {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(self: *Self, index: ListIndex) void {
|
||||
pub fn remove(self: *Self, index: IndexType) void {
|
||||
@compileError("Not implemented yet.");
|
||||
// switch (index) {
|
||||
// Unassigned.index => {
|
||||
|
||||
16
src/api/schema.d.ts
vendored
16
src/api/schema.d.ts
vendored
@@ -234,6 +234,15 @@ type uint32 = number;
|
||||
loaders: Loader[];
|
||||
}
|
||||
|
||||
export interface FrameworkConfig {
|
||||
entry_point?: string;
|
||||
}
|
||||
|
||||
export interface RouteConfig {
|
||||
dir?: string;
|
||||
extensions?: string[];
|
||||
}
|
||||
|
||||
export interface TransformOptions {
|
||||
jsx?: JSX;
|
||||
tsconfig_override?: string;
|
||||
@@ -256,7 +265,8 @@ type uint32 = number;
|
||||
only_scan_dependencies?: ScanDependencyMode;
|
||||
generate_node_module_bundle?: boolean;
|
||||
node_modules_bundle_path?: string;
|
||||
javascript_framework_file?: string;
|
||||
framework?: FrameworkConfig;
|
||||
router?: RouteConfig;
|
||||
}
|
||||
|
||||
export interface FileHandle {
|
||||
@@ -408,6 +418,10 @@ type uint32 = number;
|
||||
export declare function decodeStringMap(buffer: ByteBuffer): StringMap;
|
||||
export declare function encodeLoaderMap(message: LoaderMap, bb: ByteBuffer): void;
|
||||
export declare function decodeLoaderMap(buffer: ByteBuffer): LoaderMap;
|
||||
export declare function encodeFrameworkConfig(message: FrameworkConfig, bb: ByteBuffer): void;
|
||||
export declare function decodeFrameworkConfig(buffer: ByteBuffer): FrameworkConfig;
|
||||
export declare function encodeRouteConfig(message: RouteConfig, bb: ByteBuffer): void;
|
||||
export declare function decodeRouteConfig(buffer: ByteBuffer): RouteConfig;
|
||||
export declare function encodeTransformOptions(message: TransformOptions, bb: ByteBuffer): void;
|
||||
export declare function decodeTransformOptions(buffer: ByteBuffer): TransformOptions;
|
||||
export declare function encodeFileHandle(message: FileHandle, bb: ByteBuffer): void;
|
||||
|
||||
@@ -569,6 +569,81 @@ bb.writeByte(encoded);
|
||||
|
||||
}
|
||||
|
||||
function decodeFrameworkConfig(bb) {
|
||||
var result = {};
|
||||
|
||||
while (true) {
|
||||
switch (bb.readByte()) {
|
||||
case 0:
|
||||
return result;
|
||||
|
||||
case 1:
|
||||
result["entry_point"] = bb.readString();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Attempted to parse invalid message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function encodeFrameworkConfig(message, bb) {
|
||||
|
||||
var value = message["entry_point"];
|
||||
if (value != null) {
|
||||
bb.writeByte(1);
|
||||
bb.writeString(value);
|
||||
}
|
||||
bb.writeByte(0);
|
||||
|
||||
}
|
||||
|
||||
function decodeRouteConfig(bb) {
|
||||
var result = {};
|
||||
|
||||
while (true) {
|
||||
switch (bb.readByte()) {
|
||||
case 0:
|
||||
return result;
|
||||
|
||||
case 1:
|
||||
result["dir"] = bb.readString();
|
||||
break;
|
||||
|
||||
case 2:
|
||||
var length = bb.readVarUint();
|
||||
var values = result["extensions"] = Array(length);
|
||||
for (var i = 0; i < length; i++) values[i] = bb.readString();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Attempted to parse invalid message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function encodeRouteConfig(message, bb) {
|
||||
|
||||
var value = message["dir"];
|
||||
if (value != null) {
|
||||
bb.writeByte(1);
|
||||
bb.writeString(value);
|
||||
}
|
||||
|
||||
var value = message["extensions"];
|
||||
if (value != null) {
|
||||
bb.writeByte(2);
|
||||
var values = value, n = values.length;
|
||||
bb.writeVarUint(n);
|
||||
for (var i = 0; i < n; i++) {
|
||||
value = values[i];
|
||||
bb.writeString(value);
|
||||
}
|
||||
}
|
||||
bb.writeByte(0);
|
||||
|
||||
}
|
||||
|
||||
function decodeTransformOptions(bb) {
|
||||
var result = {};
|
||||
|
||||
@@ -672,7 +747,11 @@ function decodeTransformOptions(bb) {
|
||||
break;
|
||||
|
||||
case 22:
|
||||
result["javascript_framework_file"] = bb.readString();
|
||||
result["framework"] = decodeFrameworkConfig(bb);
|
||||
break;
|
||||
|
||||
case 23:
|
||||
result["router"] = decodeRouteConfig(bb);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -840,10 +919,16 @@ bb.writeByte(encoded);
|
||||
bb.writeString(value);
|
||||
}
|
||||
|
||||
var value = message["javascript_framework_file"];
|
||||
var value = message["framework"];
|
||||
if (value != null) {
|
||||
bb.writeByte(22);
|
||||
bb.writeString(value);
|
||||
encodeFrameworkConfig(value, bb);
|
||||
}
|
||||
|
||||
var value = message["router"];
|
||||
if (value != null) {
|
||||
bb.writeByte(23);
|
||||
encodeRouteConfig(value, bb);
|
||||
}
|
||||
bb.writeByte(0);
|
||||
|
||||
@@ -1789,6 +1874,10 @@ export { decodeStringMap }
|
||||
export { encodeStringMap }
|
||||
export { decodeLoaderMap }
|
||||
export { encodeLoaderMap }
|
||||
export { decodeFrameworkConfig }
|
||||
export { encodeFrameworkConfig }
|
||||
export { decodeRouteConfig }
|
||||
export { encodeRouteConfig }
|
||||
export { decodeTransformOptions }
|
||||
export { encodeTransformOptions }
|
||||
export { decodeFileHandle }
|
||||
|
||||
@@ -132,6 +132,15 @@ struct LoaderMap {
|
||||
Loader[] loaders;
|
||||
}
|
||||
|
||||
message FrameworkConfig {
|
||||
string entry_point = 1;
|
||||
}
|
||||
|
||||
message RouteConfig {
|
||||
string dir = 1;
|
||||
string[] extensions = 2;
|
||||
}
|
||||
|
||||
message TransformOptions {
|
||||
JSX jsx = 1;
|
||||
string tsconfig_override = 2;
|
||||
@@ -169,7 +178,8 @@ message TransformOptions {
|
||||
|
||||
string node_modules_bundle_path = 21;
|
||||
|
||||
string javascript_framework_file = 22;
|
||||
FrameworkConfig framework = 22;
|
||||
RouteConfig router = 23;
|
||||
}
|
||||
|
||||
struct FileHandle {
|
||||
|
||||
@@ -759,6 +759,82 @@ pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
|
||||
|
||||
};
|
||||
|
||||
pub const FrameworkConfig = struct {
|
||||
/// entry_point
|
||||
entry_point: ?[]const u8 = null,
|
||||
|
||||
|
||||
pub fn decode(reader: anytype) anyerror!FrameworkConfig {
|
||||
var this = std.mem.zeroes(FrameworkConfig);
|
||||
|
||||
while(true) {
|
||||
switch (try reader.readByte()) {
|
||||
0 => { return this; },
|
||||
|
||||
1 => {
|
||||
this.entry_point = try reader.readValue([]const u8);
|
||||
},
|
||||
else => {
|
||||
return error.InvalidMessage;
|
||||
},
|
||||
}
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
|
||||
pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
|
||||
if (this.entry_point) |entry_point| {
|
||||
try writer.writeFieldID(1);
|
||||
try writer.writeValue(entry_point);
|
||||
}
|
||||
try writer.endMessage();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
pub const RouteConfig = struct {
|
||||
/// dir
|
||||
dir: ?[]const u8 = null,
|
||||
|
||||
/// extensions
|
||||
extensions: []const []const u8,
|
||||
|
||||
|
||||
pub fn decode(reader: anytype) anyerror!RouteConfig {
|
||||
var this = std.mem.zeroes(RouteConfig);
|
||||
|
||||
while(true) {
|
||||
switch (try reader.readByte()) {
|
||||
0 => { return this; },
|
||||
|
||||
1 => {
|
||||
this.dir = try reader.readValue([]const u8);
|
||||
},
|
||||
2 => {
|
||||
this.extensions = try reader.readArray([]const u8);
|
||||
},
|
||||
else => {
|
||||
return error.InvalidMessage;
|
||||
},
|
||||
}
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
|
||||
pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
|
||||
if (this.dir) |dir| {
|
||||
try writer.writeFieldID(1);
|
||||
try writer.writeValue(dir);
|
||||
}
|
||||
if (this.extensions) |extensions| {
|
||||
try writer.writeFieldID(2);
|
||||
try writer.writeArray([]const u8, extensions);
|
||||
}
|
||||
try writer.endMessage();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
pub const TransformOptions = struct {
|
||||
/// jsx
|
||||
jsx: ?Jsx = null,
|
||||
@@ -823,8 +899,11 @@ generate_node_module_bundle: ?bool = null,
|
||||
/// node_modules_bundle_path
|
||||
node_modules_bundle_path: ?[]const u8 = null,
|
||||
|
||||
/// javascript_framework_file
|
||||
javascript_framework_file: ?[]const u8 = null,
|
||||
/// framework
|
||||
framework: ?FrameworkConfig = null,
|
||||
|
||||
/// router
|
||||
router: ?RouteConfig = null,
|
||||
|
||||
|
||||
pub fn decode(reader: anytype) anyerror!TransformOptions {
|
||||
@@ -898,7 +977,10 @@ pub fn decode(reader: anytype) anyerror!TransformOptions {
|
||||
this.node_modules_bundle_path = try reader.readValue([]const u8);
|
||||
},
|
||||
22 => {
|
||||
this.javascript_framework_file = try reader.readValue([]const u8);
|
||||
this.framework = try reader.readValue(FrameworkConfig);
|
||||
},
|
||||
23 => {
|
||||
this.router = try reader.readValue(RouteConfig);
|
||||
},
|
||||
else => {
|
||||
return error.InvalidMessage;
|
||||
@@ -993,9 +1075,13 @@ if (this.node_modules_bundle_path) |node_modules_bundle_path| {
|
||||
try writer.writeFieldID(21);
|
||||
try writer.writeValue(node_modules_bundle_path);
|
||||
}
|
||||
if (this.javascript_framework_file) |javascript_framework_file| {
|
||||
if (this.framework) |framework| {
|
||||
try writer.writeFieldID(22);
|
||||
try writer.writeValue(javascript_framework_file);
|
||||
try writer.writeValue(framework);
|
||||
}
|
||||
if (this.router) |router| {
|
||||
try writer.writeFieldID(23);
|
||||
try writer.writeValue(router);
|
||||
}
|
||||
try writer.endMessage();
|
||||
}
|
||||
|
||||
141
src/bundler.zig
141
src/bundler.zig
@@ -30,6 +30,7 @@ const hash_map = @import("hash_map.zig");
|
||||
const PackageJSON = @import("./resolver/package_json.zig").PackageJSON;
|
||||
const DebugLogs = _resolver.DebugLogs;
|
||||
const NodeModuleBundle = @import("./node_module_bundle.zig").NodeModuleBundle;
|
||||
const Router = @import("./router.zig");
|
||||
|
||||
const Css = @import("css_scanner.zig");
|
||||
|
||||
@@ -149,6 +150,8 @@ pub fn NewBundler(cache_files: bool) type {
|
||||
resolve_queue: ResolveQueue,
|
||||
elapsed: i128 = 0,
|
||||
needs_runtime: bool = false,
|
||||
router: ?Router = null,
|
||||
|
||||
linker: Linker,
|
||||
timer: Timer = Timer{},
|
||||
|
||||
@@ -166,7 +169,10 @@ pub fn NewBundler(cache_files: bool) type {
|
||||
) !ThisBundler {
|
||||
js_ast.Expr.Data.Store.create(allocator);
|
||||
js_ast.Stmt.Data.Store.create(allocator);
|
||||
var fs = try Fs.FileSystem.init1(allocator, opts.absolute_working_dir, opts.serve orelse false);
|
||||
var fs = try Fs.FileSystem.init1(
|
||||
allocator,
|
||||
opts.absolute_working_dir,
|
||||
);
|
||||
const bundle_options = try options.BundleOptions.fromApi(
|
||||
allocator,
|
||||
fs,
|
||||
@@ -206,6 +212,72 @@ pub fn NewBundler(cache_files: bool) type {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn configureFramework(this: *ThisBundler) !void {
|
||||
if (this.options.framework) |*framework| {
|
||||
var framework_file = this.normalizeEntryPointPath(framework.entry_point);
|
||||
var resolved = this.resolver.resolve(
|
||||
this.fs.top_level_dir,
|
||||
framework_file,
|
||||
.entry_point,
|
||||
) catch |err| {
|
||||
Output.prettyErrorln("Failed to load framework: {s}", .{@errorName(err)});
|
||||
Output.flush();
|
||||
this.options.framework = null;
|
||||
return;
|
||||
};
|
||||
|
||||
framework.entry_point = try this.allocator.dupe(u8, resolved.path_pair.primary.text);
|
||||
}
|
||||
}
|
||||
pub fn configureRouter(this: *ThisBundler) !void {
|
||||
try this.configureFramework();
|
||||
|
||||
// if you pass just a directory, activate the router configured for the pages directory
|
||||
// for now:
|
||||
// - "." is not supported
|
||||
// - multiple pages directories is not supported
|
||||
if (this.options.route_config == null and this.options.entry_points.len == 1) {
|
||||
|
||||
// When inferring:
|
||||
// - pages directory with a file extension is not supported. e.g. "pages.app/" won't work.
|
||||
// This is a premature optimization to avoid this magical auto-detection we do here from meaningfully increasing startup time if you're just passing a file
|
||||
// readDirInfo is a recursive lookup, top-down instead of bottom-up. It opens each folder handle and potentially reads the package.jsons
|
||||
// So it is not fast! Unless it's already cached.
|
||||
var paths = [_]string{std.mem.trimLeft(u8, this.options.entry_points[0], "./")};
|
||||
if (std.mem.indexOfScalar(u8, paths[0], '.') == null) {
|
||||
var pages_dir_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||
var entry = this.fs.absBuf(&paths, &pages_dir_buf);
|
||||
|
||||
if (std.fs.path.extension(entry).len == 0) {
|
||||
allocators.constStrToU8(entry).ptr[entry.len] = '/';
|
||||
|
||||
// Only throw if they actually passed in a route config and the directory failed to load
|
||||
var dir_info_ = this.resolver.readDirInfo(entry) catch return;
|
||||
var dir_info = dir_info_ orelse return;
|
||||
|
||||
this.options.route_config = options.RouteConfig{
|
||||
.dir = dir_info.abs_path,
|
||||
.extensions = std.mem.span(&options.RouteConfig.DefaultExtensions),
|
||||
};
|
||||
this.router = try Router.init(this.fs, this.allocator, this.options.route_config.?);
|
||||
try this.router.?.loadRoutes(dir_info, Resolver, &this.resolver, std.math.maxInt(u16), true);
|
||||
}
|
||||
}
|
||||
} else if (this.options.route_config) |*route_config| {
|
||||
var paths = [_]string{route_config.dir};
|
||||
var entry = this.fs.abs(&paths);
|
||||
var dir_info_ = try this.resolver.readDirInfo(entry);
|
||||
var dir_info = dir_info_ orelse return error.MissingRoutesDir;
|
||||
|
||||
this.options.route_config = options.RouteConfig{
|
||||
.dir = dir_info.abs_path,
|
||||
.extensions = route_config.extensions,
|
||||
};
|
||||
this.router = try Router.init(this.fs, this.allocator, this.options.route_config.?);
|
||||
try this.router.?.loadRoutes(dir_info, Resolver, &this.resolver, std.math.maxInt(u16), true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resetStore(bundler: *ThisBundler) void {
|
||||
js_ast.Expr.Data.Store.reset();
|
||||
js_ast.Stmt.Data.Store.reset();
|
||||
@@ -989,7 +1061,7 @@ pub fn NewBundler(cache_files: bool) type {
|
||||
// Step 1. Parse & scan
|
||||
const loader = bundler.options.loaders.get(resolve_result.path_pair.primary.name.ext) orelse .file;
|
||||
var file_path = resolve_result.path_pair.primary;
|
||||
file_path.pretty = Linker.relative_paths_list.append(bundler.fs.relativeTo(file_path.text)) catch unreachable;
|
||||
file_path.pretty = Linker.relative_paths_list.append(string, bundler.fs.relativeTo(file_path.text)) catch unreachable;
|
||||
|
||||
var output_file = options.OutputFile{
|
||||
.input = file_path,
|
||||
@@ -1134,7 +1206,7 @@ pub fn NewBundler(cache_files: bool) type {
|
||||
// Step 1. Parse & scan
|
||||
const loader = bundler.options.loaders.get(resolve_result.path_pair.primary.name.ext) orelse .file;
|
||||
var file_path = resolve_result.path_pair.primary;
|
||||
file_path.pretty = Linker.relative_paths_list.append(bundler.fs.relativeTo(file_path.text)) catch unreachable;
|
||||
file_path.pretty = Linker.relative_paths_list.append(string, bundler.fs.relativeTo(file_path.text)) catch unreachable;
|
||||
|
||||
switch (loader) {
|
||||
.jsx, .tsx, .js, .ts, .json => {
|
||||
@@ -1609,32 +1681,11 @@ pub fn NewBundler(cache_files: bool) type {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn bundle(
|
||||
allocator: *std.mem.Allocator,
|
||||
log: *logger.Log,
|
||||
opts: Api.TransformOptions,
|
||||
) !options.TransformResult {
|
||||
var bundler = try ThisBundler.init(allocator, log, opts, null);
|
||||
bundler.configureLinker();
|
||||
|
||||
if (bundler.options.write and bundler.options.output_dir.len > 0) {}
|
||||
|
||||
// 100.00 µs std.fifo.LinearFifo(resolver.Result,std.fifo.LinearFifoBufferType { .Dynamic = {}}).writeItemAssumeCapacity
|
||||
if (bundler.options.resolve_mode != .lazy) {
|
||||
try bundler.resolve_queue.ensureUnusedCapacity(24);
|
||||
}
|
||||
|
||||
var entry_points = try allocator.alloc(_resolver.Result, bundler.options.entry_points.len);
|
||||
|
||||
if (log.level == .verbose) {
|
||||
bundler.resolver.debug_logs = try DebugLogs.init(allocator);
|
||||
}
|
||||
|
||||
var rfs: *Fs.FileSystem.RealFS = &bundler.fs.fs;
|
||||
|
||||
fn enqueueEntryPoints(bundler: *ThisBundler, entry_points: []_resolver.Result, comptime normalize_entry_point: bool) void {
|
||||
var entry_point_i: usize = 0;
|
||||
|
||||
for (bundler.options.entry_points) |_entry| {
|
||||
var entry: string = bundler.normalizeEntryPointPath(_entry);
|
||||
var entry: string = if (comptime normalize_entry_point) bundler.normalizeEntryPointPath(_entry) else _entry;
|
||||
|
||||
defer {
|
||||
js_ast.Expr.Data.Store.reset();
|
||||
@@ -1651,7 +1702,7 @@ pub fn NewBundler(cache_files: bool) type {
|
||||
continue;
|
||||
}
|
||||
|
||||
try bundler.resolve_results.put(key, result);
|
||||
bundler.resolve_results.put(key, result) catch unreachable;
|
||||
entry_points[entry_point_i] = result;
|
||||
|
||||
if (isDebug) {
|
||||
@@ -1661,6 +1712,40 @@ pub fn NewBundler(cache_files: bool) type {
|
||||
entry_point_i += 1;
|
||||
bundler.resolve_queue.writeItem(result) catch unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bundle(
|
||||
allocator: *std.mem.Allocator,
|
||||
log: *logger.Log,
|
||||
opts: Api.TransformOptions,
|
||||
) !options.TransformResult {
|
||||
var bundler = try ThisBundler.init(allocator, log, opts, null);
|
||||
bundler.configureLinker();
|
||||
try bundler.configureRouter();
|
||||
|
||||
var skip_normalize = false;
|
||||
if (bundler.router) |router| {
|
||||
bundler.options.entry_points = try router.getEntryPoints(allocator);
|
||||
skip_normalize = true;
|
||||
}
|
||||
|
||||
if (bundler.options.write and bundler.options.output_dir.len > 0) {}
|
||||
|
||||
// 100.00 µs std.fifo.LinearFifo(resolver.Result,std.fifo.LinearFifoBufferType { .Dynamic = {}}).writeItemAssumeCapacity
|
||||
if (bundler.options.resolve_mode != .lazy) {
|
||||
try bundler.resolve_queue.ensureUnusedCapacity(24);
|
||||
}
|
||||
|
||||
var entry_points = try allocator.alloc(_resolver.Result, bundler.options.entry_points.len);
|
||||
if (skip_normalize) {
|
||||
bundler.enqueueEntryPoints(entry_points, false);
|
||||
} else {
|
||||
bundler.enqueueEntryPoints(entry_points, true);
|
||||
}
|
||||
|
||||
if (log.level == .verbose) {
|
||||
bundler.resolver.debug_logs = try DebugLogs.init(allocator);
|
||||
}
|
||||
|
||||
if (bundler.options.output_dir_handle == null) {
|
||||
const outstream = std.io.getStdOut();
|
||||
|
||||
23
src/cli.zig
23
src/cli.zig
@@ -130,9 +130,8 @@ pub const Cli = struct {
|
||||
clap.parseParam("--scan Instead of bundling or transpiling, print a list of every file imported by an entry point, recursively") catch unreachable,
|
||||
clap.parseParam("--new-jsb Generate a new node_modules.jsb file from node_modules and entry point(s)") catch unreachable,
|
||||
clap.parseParam("--jsb <STR> Use a Speedy JavaScript Bundle (default: \"./node_modules.jsb\" if exists)") catch unreachable,
|
||||
clap.parseParam("--framework <STR> Use a JavaScript framework (file path) with --serve") catch unreachable,
|
||||
// clap.parseParam("--no-jsb Use a Speedy JavaScript Bundle (default: \"./node_modules.jsb\" if exists)") catch unreachable,
|
||||
clap.parseParam("<POS>... Entry points to use") catch unreachable,
|
||||
clap.parseParam("--framework <STR> Use a JavaScript framework (module path)") catch unreachable,
|
||||
clap.parseParam("<POS>... Entry point(s) to use. Can be individual files, npm packages, or one directory. If one directory, it will auto-detect entry points using a filesystem router. If you're using a framework, passing entry points are optional.") catch unreachable,
|
||||
};
|
||||
|
||||
var diag = clap.Diagnostic{};
|
||||
@@ -184,7 +183,7 @@ pub const Cli = struct {
|
||||
var jsx_production = args.flag("--jsx-production");
|
||||
var react_fast_refresh = false;
|
||||
|
||||
var javascript_framework = args.option("--framework");
|
||||
var framework_entry_point = args.option("--framework");
|
||||
|
||||
if (serve or args.flag("--new-jsb")) {
|
||||
react_fast_refresh = true;
|
||||
@@ -277,16 +276,20 @@ pub const Cli = struct {
|
||||
};
|
||||
}
|
||||
|
||||
if (entry_points.len == 0) {
|
||||
var javascript_framework: ?Api.FrameworkConfig = null;
|
||||
|
||||
if (framework_entry_point) |entry| {
|
||||
javascript_framework = Api.FrameworkConfig{
|
||||
.entry_point = entry,
|
||||
};
|
||||
}
|
||||
|
||||
if (entry_points.len == 0 and javascript_framework == null) {
|
||||
try clap.help(stderr.writer(), ¶ms);
|
||||
try diag.report(stderr.writer(), error.MissingEntryPoint);
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
if (!serve) {
|
||||
javascript_framework = null;
|
||||
}
|
||||
|
||||
return Api.TransformOptions{
|
||||
.jsx = jsx,
|
||||
.output_dir = output_dir,
|
||||
@@ -314,7 +317,7 @@ pub const Cli = struct {
|
||||
.platform = platform,
|
||||
.only_scan_dependencies = if (args.flag("--scan")) Api.ScanDependencyMode.all else Api.ScanDependencyMode._none,
|
||||
.generate_node_module_bundle = if (args.flag("--new-jsb")) true else false,
|
||||
.javascript_framework_file = javascript_framework,
|
||||
.framework = javascript_framework,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,6 +54,9 @@ pub const Request = struct {
|
||||
0,
|
||||
);
|
||||
|
||||
// Leave a sentinel value, for JavaScriptCore support.
|
||||
path.ptr[path.len] = 0;
|
||||
|
||||
return switch (rc) {
|
||||
-1 => error.BadRequest,
|
||||
-2 => error.ShortRead,
|
||||
|
||||
@@ -90,7 +90,8 @@
|
||||
CHECK_EOF(); \
|
||||
} \
|
||||
tok = tok_start; \
|
||||
toklen = buf - tok_start; \
|
||||
toklen = buf - tok_start;
|
||||
\
|
||||
} while (0)
|
||||
|
||||
static const char *token_char_map = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
|
||||
|
||||
@@ -31,6 +31,8 @@ pub const verbose_watcher = true;
|
||||
|
||||
pub const css_supports_fence = true;
|
||||
|
||||
pub const disable_entry_cache = false;
|
||||
|
||||
pub const CSSModulePolyfill = enum {
|
||||
// When you import a .css file and you reference the import in JavaScript
|
||||
// Just return whatever the property key they referenced was
|
||||
|
||||
140
src/fs.zig
140
src/fs.zig
@@ -68,7 +68,10 @@ pub const FileSystem = struct {
|
||||
ENOTDIR,
|
||||
};
|
||||
|
||||
pub fn init1(allocator: *std.mem.Allocator, top_level_dir: ?string, enable_watcher: bool) !*FileSystem {
|
||||
pub fn init1(
|
||||
allocator: *std.mem.Allocator,
|
||||
top_level_dir: ?string,
|
||||
) !*FileSystem {
|
||||
var _top_level_dir = top_level_dir orelse (if (isBrowser) "/project/" else try std.process.getCwdAlloc(allocator));
|
||||
|
||||
// Ensure there's a trailing separator in the top level directory
|
||||
@@ -86,7 +89,10 @@ pub const FileSystem = struct {
|
||||
instance = FileSystem{
|
||||
.allocator = allocator,
|
||||
.top_level_dir = _top_level_dir,
|
||||
.fs = Implementation.init(allocator, _top_level_dir, enable_watcher),
|
||||
.fs = Implementation.init(
|
||||
allocator,
|
||||
_top_level_dir,
|
||||
),
|
||||
// .stats = std.StringHashMap(Stat).init(allocator),
|
||||
.dirname_store = DirnameStore.init(allocator),
|
||||
.filename_store = FilenameStore.init(allocator),
|
||||
@@ -98,7 +104,7 @@ pub const FileSystem = struct {
|
||||
}
|
||||
|
||||
pub const DirEntry = struct {
|
||||
pub const EntryMap = hash_map.StringHashMap(EntryStore.ListIndex);
|
||||
pub const EntryMap = hash_map.StringHashMap(allocators.IndexType);
|
||||
pub const EntryStore = allocators.BSSList(Entry, Preallocate.Counts.files);
|
||||
dir: string,
|
||||
fd: StoredFileDescriptorType = 0,
|
||||
@@ -122,7 +128,20 @@ pub const FileSystem = struct {
|
||||
},
|
||||
}
|
||||
// entry.name only lives for the duration of the iteration
|
||||
var name = FileSystem.FilenameStore.editableSlice(try FileSystem.FilenameStore.instance.append(entry.name));
|
||||
|
||||
var name: []u8 = undefined;
|
||||
|
||||
switch (_kind) {
|
||||
.file => {
|
||||
name = FileSystem.FilenameStore.editableSlice(try FileSystem.FilenameStore.instance.append(@TypeOf(entry.name), entry.name));
|
||||
},
|
||||
.dir => {
|
||||
// FileSystem.FilenameStore here because it's not an absolute path
|
||||
// it's a path relative to the parent directory
|
||||
// so it's a tiny path like "foo" instead of "/bar/baz/foo"
|
||||
name = FileSystem.FilenameStore.editableSlice(try FileSystem.FilenameStore.instance.append(@TypeOf(entry.name), entry.name));
|
||||
},
|
||||
}
|
||||
|
||||
for (entry.name) |c, i| {
|
||||
name[i] = std.ascii.toLower(c);
|
||||
@@ -380,9 +399,9 @@ pub const FileSystem = struct {
|
||||
|
||||
threadlocal var realpath_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||
pub fn resolveAlloc(f: *@This(), allocator: *std.mem.Allocator, parts: anytype) !string {
|
||||
const joined = f.join(parts);
|
||||
const joined = f.abs(parts);
|
||||
|
||||
const realpath = try std.fs.realpath(joined, (&realpath_buffer));
|
||||
const realpath = f.resolvePath(joined);
|
||||
|
||||
return try allocator.dupe(u8, realpath);
|
||||
}
|
||||
@@ -395,10 +414,7 @@ pub const FileSystem = struct {
|
||||
entries_mutex: Mutex = Mutex.init(),
|
||||
entries: *EntriesOption.Map,
|
||||
allocator: *std.mem.Allocator,
|
||||
do_not_cache_entries: bool = false,
|
||||
limiter: Limiter,
|
||||
watcher: ?std.StringHashMap(WatchData) = null,
|
||||
watcher_mutex: Mutex = Mutex.init(),
|
||||
cwd: string,
|
||||
parent_fs: *FileSystem = undefined,
|
||||
file_limit: usize = 32,
|
||||
@@ -440,7 +456,10 @@ pub const FileSystem = struct {
|
||||
return limit.cur;
|
||||
}
|
||||
|
||||
pub fn init(allocator: *std.mem.Allocator, cwd: string, enable_watcher: bool) RealFS {
|
||||
pub fn init(
|
||||
allocator: *std.mem.Allocator,
|
||||
cwd: string,
|
||||
) RealFS {
|
||||
const file_limit = adjustUlimit();
|
||||
return RealFS{
|
||||
.entries = EntriesOption.Map.init(allocator),
|
||||
@@ -449,7 +468,6 @@ pub const FileSystem = struct {
|
||||
.file_limit = file_limit,
|
||||
.file_quota = file_limit,
|
||||
.limiter = Limiter.init(allocator, file_limit),
|
||||
.watcher = if (enable_watcher) std.StringHashMap(WatchData).init(allocator) else null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -518,39 +536,8 @@ pub const FileSystem = struct {
|
||||
pub const SafetyGap = 3;
|
||||
};
|
||||
|
||||
fn modKeyError(fs: *RealFS, path: string, err: anyerror) void {
|
||||
if (fs.watcher) |*watcher| {
|
||||
fs.watcher_mutex.lock();
|
||||
defer fs.watcher_mutex.unlock();
|
||||
var state = WatchData.State.file_missing;
|
||||
|
||||
switch (err) {
|
||||
error.Unusable => {
|
||||
state = WatchData.State.file_unusable_mod_key;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
var entry = watcher.getOrPutValue(path, WatchData{ .state = state }) catch unreachable;
|
||||
entry.value_ptr.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modKeyWithFile(fs: *RealFS, path: string, file: anytype) anyerror!ModKey {
|
||||
const key = ModKey.generate(fs, path, file) catch |err| {
|
||||
fs.modKeyError(path, err);
|
||||
return err;
|
||||
};
|
||||
|
||||
if (fs.watcher) |*watcher| {
|
||||
fs.watcher_mutex.lock();
|
||||
defer fs.watcher_mutex.unlock();
|
||||
|
||||
var entry = watcher.getOrPutValue(path, WatchData{ .state = .file_has_mod_key, .mod_key = key }) catch unreachable;
|
||||
entry.value_ptr.mod_key = key;
|
||||
}
|
||||
|
||||
return key;
|
||||
return try ModKey.generate(fs, path, file);
|
||||
}
|
||||
|
||||
pub fn modKey(fs: *RealFS, path: string) anyerror!ModKey {
|
||||
@@ -565,24 +552,6 @@ pub const FileSystem = struct {
|
||||
return try fs.modKeyWithFile(path, file);
|
||||
}
|
||||
|
||||
pub const WatchData = struct {
|
||||
dir_entries: []string = &([_]string{}),
|
||||
file_contents: string = "",
|
||||
mod_key: ModKey = ModKey{},
|
||||
watch_mutex: Mutex = Mutex.init(),
|
||||
state: State = State.none,
|
||||
|
||||
pub const State = enum {
|
||||
none,
|
||||
dir_has_entries,
|
||||
dir_missing,
|
||||
file_has_mod_key,
|
||||
file_need_mod_key,
|
||||
file_missing,
|
||||
file_unusable_mod_key,
|
||||
};
|
||||
};
|
||||
|
||||
pub const EntriesOption = union(Tag) {
|
||||
entries: DirEntry,
|
||||
err: DirEntry.Err,
|
||||
@@ -654,13 +623,7 @@ pub const FileSystem = struct {
|
||||
}
|
||||
|
||||
fn readDirectoryError(fs: *RealFS, dir: string, err: anyerror) !*EntriesOption {
|
||||
if (fs.watcher) |*watcher| {
|
||||
fs.watcher_mutex.lock();
|
||||
defer fs.watcher_mutex.unlock();
|
||||
try watcher.put(dir, WatchData{ .state = .dir_missing });
|
||||
}
|
||||
|
||||
if (!fs.do_not_cache_entries) {
|
||||
if (FeatureFlags.disable_entry_cache) {
|
||||
fs.entries_mutex.lock();
|
||||
defer fs.entries_mutex.unlock();
|
||||
var get_or_put_result = try fs.entries.getOrPut(dir);
|
||||
@@ -679,11 +642,11 @@ pub const FileSystem = struct {
|
||||
|
||||
threadlocal var temp_entries_option: EntriesOption = undefined;
|
||||
|
||||
pub fn readDirectory(fs: *RealFS, _dir: string, _handle: ?std.fs.Dir, recursive: bool) !*EntriesOption {
|
||||
pub fn readDirectory(fs: *RealFS, _dir: string, _handle: ?std.fs.Dir) !*EntriesOption {
|
||||
var dir = _dir;
|
||||
var cache_result: ?allocators.Result = null;
|
||||
|
||||
if (!fs.do_not_cache_entries) {
|
||||
if (FeatureFlags.disable_entry_cache) {
|
||||
fs.entries_mutex.lock();
|
||||
defer fs.entries_mutex.unlock();
|
||||
|
||||
@@ -706,7 +669,7 @@ pub const FileSystem = struct {
|
||||
|
||||
// if we get this far, it's a real directory, so we can just store the dir name.
|
||||
if (_handle == null) {
|
||||
dir = try FilenameStore.instance.append(_dir);
|
||||
dir = try DirnameStore.instance.append(string, _dir);
|
||||
}
|
||||
|
||||
// Cache miss: read the directory entries
|
||||
@@ -717,23 +680,7 @@ pub const FileSystem = struct {
|
||||
return fs.readDirectoryError(dir, err) catch unreachable;
|
||||
};
|
||||
|
||||
// if (fs.watcher) |*watcher| {
|
||||
// fs.watcher_mutex.lock();
|
||||
// defer fs.watcher_mutex.unlock();
|
||||
// var _entries = watcher.iterator();
|
||||
// const names = try fs.allocator.alloc([]const u8, _entries.len);
|
||||
// for (_entries) |entry, i| {
|
||||
// names[i] = try fs.allocator.dupe(u8, entry.key);
|
||||
// }
|
||||
// strings.sortAsc(names);
|
||||
|
||||
// try watcher.put(
|
||||
// try fs.allocator.dupe(u8, dir),
|
||||
// WatchData{ .dir_entries = names, .state = .dir_has_entries },
|
||||
// );
|
||||
// }
|
||||
|
||||
if (!fs.do_not_cache_entries) {
|
||||
if (FeatureFlags.disable_entry_cache) {
|
||||
fs.entries_mutex.lock();
|
||||
defer fs.entries_mutex.unlock();
|
||||
const result = EntriesOption{
|
||||
@@ -748,14 +695,7 @@ pub const FileSystem = struct {
|
||||
return &temp_entries_option;
|
||||
}
|
||||
|
||||
fn readFileError(fs: *RealFS, path: string, err: anyerror) void {
|
||||
if (fs.watcher) |*watcher| {
|
||||
fs.watcher_mutex.lock();
|
||||
defer fs.watcher_mutex.unlock();
|
||||
var res = watcher.getOrPutValue(path, WatchData{ .state = .file_missing }) catch unreachable;
|
||||
res.value_ptr.state = .file_missing;
|
||||
}
|
||||
}
|
||||
fn readFileError(fs: *RealFS, path: string, err: anyerror) void {}
|
||||
|
||||
pub fn readFileWithHandle(
|
||||
fs: *RealFS,
|
||||
@@ -804,14 +744,6 @@ pub const FileSystem = struct {
|
||||
file_contents = buf[0..read_count];
|
||||
}
|
||||
|
||||
if (fs.watcher) |*watcher| {
|
||||
fs.watcher_mutex.lock();
|
||||
defer fs.watcher_mutex.unlock();
|
||||
var res = watcher.getOrPutValue(path, WatchData{}) catch unreachable;
|
||||
res.value_ptr.state = .file_need_mod_key;
|
||||
res.value_ptr.file_contents = file_contents;
|
||||
}
|
||||
|
||||
return File{ .path = Path.init(path), .contents = file_contents };
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ pub const Output = struct {
|
||||
}
|
||||
|
||||
pub fn printErrorable(comptime fmt: string, args: anytype) !void {
|
||||
if (isWasm) {
|
||||
if (comptime isWasm) {
|
||||
try source.stream.seekTo(0);
|
||||
try source.stream.writer().print(fmt, args);
|
||||
const root = @import("root");
|
||||
@@ -105,7 +105,7 @@ pub const Output = struct {
|
||||
}
|
||||
|
||||
pub fn print(comptime fmt: string, args: anytype) void {
|
||||
if (isWasm) {
|
||||
if (comptime isWasm) {
|
||||
source.stream.seekTo(0) catch return;
|
||||
source.stream.writer().print(fmt, args) catch return;
|
||||
const root = @import("root");
|
||||
@@ -287,7 +287,7 @@ pub const Output = struct {
|
||||
}
|
||||
|
||||
pub fn printError(comptime fmt: string, args: anytype) void {
|
||||
if (isWasm) {
|
||||
if (comptime isWasm) {
|
||||
source.error_stream.seekTo(0) catch return;
|
||||
source.error_stream.writer().print(fmt, args) catch unreachable;
|
||||
const root = @import("root");
|
||||
@@ -300,16 +300,36 @@ pub const Output = struct {
|
||||
|
||||
pub const Global = struct {
|
||||
pub fn panic(comptime fmt: string, args: anytype) noreturn {
|
||||
if (isWasm) {
|
||||
Output.print(fmt, args);
|
||||
if (comptime isWasm) {
|
||||
Output.printErrorln(fmt, args);
|
||||
Output.flush();
|
||||
@panic(fmt);
|
||||
} else {
|
||||
Output.prettyErrorln(fmt, args);
|
||||
Output.flush();
|
||||
std.debug.panic(fmt, args);
|
||||
}
|
||||
}
|
||||
|
||||
// std.debug.assert but happens at runtime
|
||||
pub fn invariant(condition: bool, comptime fmt: string, args: anytype) void {
|
||||
if (!condition) {
|
||||
_invariant(fmt, args);
|
||||
}
|
||||
}
|
||||
|
||||
inline fn _invariant(comptime fmt: string, args: anytype) noreturn {
|
||||
if (comptime isWasm) {
|
||||
Output.printErrorln(fmt, args);
|
||||
Output.flush();
|
||||
@panic(fmt);
|
||||
} else {
|
||||
Output.prettyErrorln(fmt, args);
|
||||
Output.flush();
|
||||
std.os.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notimpl() noreturn {
|
||||
Global.panic("Not implemented yet!!!!!", .{});
|
||||
}
|
||||
@@ -327,3 +347,32 @@ pub const FileDescriptorType = if (isBrowser) u0 else std.os.fd_t;
|
||||
// such is often the case with macOS
|
||||
// As a useful optimization, we can store file descriptors and just keep them open...forever
|
||||
pub const StoredFileDescriptorType = if (isWindows or isBrowser) u0 else std.os.fd_t;
|
||||
|
||||
pub const PathBuilder = struct {
|
||||
const StringBuilderType = NewStringBuilder(std.fs.MAX_PATH_BYTES);
|
||||
builder: StringBuilderType = StringBuilderType.init(),
|
||||
|
||||
pub fn init() PathBuilder {
|
||||
return PathBuilder{};
|
||||
}
|
||||
|
||||
fn load(this: *PathBuilder) void {
|
||||
return @call(.{ .modifier = .always_inline }, StringBuilderType.load, .{this.builder});
|
||||
}
|
||||
|
||||
pub fn append(this: *PathBuilder, str: string) void {
|
||||
return @call(.{ .modifier = .always_inline }, StringBuilderType.append, .{ this.builder, str });
|
||||
}
|
||||
|
||||
pub fn pop(this: *PathBuilder, count: usize) void {
|
||||
return @call(.{ .modifier = .always_inline }, StringBuilderType.pop, .{ this.builder, count });
|
||||
}
|
||||
|
||||
pub fn str(this: *PathBuilder) string {
|
||||
return @call(.{ .modifier = .always_inline }, StringBuilderType.str, .{this.builder});
|
||||
}
|
||||
|
||||
pub fn reset(this: *PathBuilder) void {
|
||||
return @call(.{ .modifier = .always_inline }, StringBuilderType.reset, .{this.builder});
|
||||
}
|
||||
};
|
||||
|
||||
28
src/http.zig
28
src/http.zig
@@ -59,6 +59,7 @@ const HTTPStatusCode = u10;
|
||||
pub const URLPath = struct {
|
||||
extname: string = "",
|
||||
path: string = "",
|
||||
pathname: string = "",
|
||||
first_segment: string = "",
|
||||
query_string: string = "",
|
||||
|
||||
@@ -119,6 +120,7 @@ pub const URLPath = struct {
|
||||
|
||||
return URLPath{
|
||||
.extname = extname,
|
||||
.pathname = raw_path,
|
||||
.first_segment = first_segment,
|
||||
.path = if (raw_path.len == 1) "." else path,
|
||||
.query_string = if (question_mark_i > -1) raw_path[@intCast(usize, question_mark_i)..@intCast(usize, raw_path.len)] else "",
|
||||
@@ -641,6 +643,7 @@ pub const RequestContext = struct {
|
||||
|
||||
pub const HandlerThread = struct {
|
||||
args: Api.TransformOptions,
|
||||
framework: Options.Framework,
|
||||
existing_bundle: ?*NodeModuleBundle,
|
||||
log: ?*logger.Log = null,
|
||||
};
|
||||
@@ -677,8 +680,9 @@ pub const RequestContext = struct {
|
||||
js_ast.Expr.Data.Store.create(std.heap.c_allocator);
|
||||
|
||||
defer Output.flush();
|
||||
var boot = handler.args.javascript_framework_file.?;
|
||||
var vm = try JavaScript.VirtualMachine.init(std.heap.c_allocator, handler.args, handler.existing_bundle, handler.log);
|
||||
|
||||
const boot = handler.framework.entry_point;
|
||||
defer vm.deinit();
|
||||
|
||||
var resolved_entry_point = try vm.bundler.resolver.resolve(
|
||||
@@ -750,6 +754,7 @@ pub const RequestContext = struct {
|
||||
try JavaScriptHandler.spawnThread(
|
||||
HandlerThread{
|
||||
.args = server.transform_options,
|
||||
.framework = server.bundler.options.framework.?,
|
||||
.existing_bundle = server.bundler.options.node_modules_bundle,
|
||||
.log = &server.log,
|
||||
},
|
||||
@@ -1668,7 +1673,7 @@ pub const Server = struct {
|
||||
}
|
||||
|
||||
if (req_ctx.url.extname.len == 0 and !RequestContext.JavaScriptHandler.javascript_disabled) {
|
||||
if (server.transform_options.javascript_framework_file != null) {
|
||||
if (server.bundler.options.framework != null) {
|
||||
RequestContext.JavaScriptHandler.enqueue(&req_ctx, server) catch unreachable;
|
||||
server.javascript_enabled = !RequestContext.JavaScriptHandler.javascript_disabled;
|
||||
}
|
||||
@@ -1719,24 +1724,7 @@ pub const Server = struct {
|
||||
};
|
||||
server.bundler = try Bundler.init(allocator, &server.log, options, null);
|
||||
server.bundler.configureLinker();
|
||||
|
||||
load_framework: {
|
||||
if (options.javascript_framework_file) |framework_file_path| {
|
||||
var framework_file = server.bundler.normalizeEntryPointPath(framework_file_path);
|
||||
var resolved = server.bundler.resolver.resolve(
|
||||
server.bundler.fs.top_level_dir,
|
||||
framework_file,
|
||||
.entry_point,
|
||||
) catch |err| {
|
||||
Output.prettyError("Failed to load framework: {s}", .{@errorName(err)});
|
||||
Output.flush();
|
||||
server.transform_options.javascript_framework_file = null;
|
||||
break :load_framework;
|
||||
};
|
||||
|
||||
server.transform_options.javascript_framework_file = try server.allocator.dupe(u8, resolved.path_pair.primary.text);
|
||||
}
|
||||
}
|
||||
try server.bundler.configureRouter();
|
||||
|
||||
try server.initWatcher();
|
||||
|
||||
|
||||
134
src/javascript/jsc/api/router.zig
Normal file
134
src/javascript/jsc/api/router.zig
Normal file
@@ -0,0 +1,134 @@
|
||||
usingnamespace @import("../base.zig");
|
||||
const std = @import("std");
|
||||
const Api = @import("../../../api/schema.zig").Api;
|
||||
const FilesystemRouter = @import("../../../router.zig");
|
||||
const JavaScript = @import("../javascript.zig");
|
||||
|
||||
pub const Router = struct {
|
||||
match: FilesystemRouter.RouteMap.MatchedRoute,
|
||||
file_path_str: js.JSStringRef = null,
|
||||
pathname_str: js.JSStringRef = null,
|
||||
|
||||
pub const Class = NewClass(
|
||||
Router,
|
||||
"Router",
|
||||
.{
|
||||
.finalize = finalize,
|
||||
},
|
||||
.{
|
||||
.@"pathname" = .{
|
||||
.get = getPathname,
|
||||
.ro = true,
|
||||
.ts = .{
|
||||
.@"return" = "string",
|
||||
.@"tsdoc" = "URL path as appears in a web browser's address bar",
|
||||
},
|
||||
},
|
||||
.@"filepath" = .{
|
||||
.get = getPageFilePath,
|
||||
.ro = true,
|
||||
.ts = .{
|
||||
.@"return" = "string",
|
||||
.@"tsdoc" =
|
||||
\\Project-relative filesystem path to the route file
|
||||
\\
|
||||
\\@example
|
||||
\\
|
||||
\\```tsx
|
||||
\\const PageComponent = (await import(route.filepath)).default;
|
||||
\\ReactDOMServer.renderToString(<PageComponent query={route.query} />);
|
||||
\\```
|
||||
,
|
||||
},
|
||||
},
|
||||
.@"route" = .{
|
||||
.@"get" = getRoute,
|
||||
.ro = true,
|
||||
},
|
||||
.query = .{
|
||||
.@"get" = getQuery,
|
||||
.ro = true,
|
||||
},
|
||||
.pageFilePath = .{
|
||||
.@"get" = getPageFilePath,
|
||||
.ro = true,
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
pub fn getPageFilePath(
|
||||
this: *Router,
|
||||
ctx: js.JSContextRef,
|
||||
thisObject: js.JSObjectRef,
|
||||
prop: js.JSStringRef,
|
||||
exception: js.ExceptionRef,
|
||||
) js.JSValueRef {
|
||||
if (this.file_path_str == null) {
|
||||
this.file_path_str = js.JSStringCreateWithUTF8CString(this.match.file_path[0.. :0]);
|
||||
}
|
||||
|
||||
return js.JSValueMakeString(ctx, this.file_path_str);
|
||||
}
|
||||
|
||||
pub fn finalize(
|
||||
this: *Router,
|
||||
ctx: js.JSObjectRef,
|
||||
) void {
|
||||
// this.deinit();
|
||||
}
|
||||
|
||||
pub fn requirePage(
|
||||
this: *Router,
|
||||
ctx: js.JSContextRef,
|
||||
function: js.JSObjectRef,
|
||||
thisObject: js.JSObjectRef,
|
||||
arguments: []const js.JSValueRef,
|
||||
exception: js.ExceptionRef,
|
||||
) js.JSValueRef {}
|
||||
|
||||
pub fn getPathname(
|
||||
this: *Router,
|
||||
ctx: js.JSContextRef,
|
||||
thisObject: js.JSObjectRef,
|
||||
prop: js.JSStringRef,
|
||||
exception: js.ExceptionRef,
|
||||
) js.JSValueRef {
|
||||
if (this.pathname_str == null) {
|
||||
this.pathname_str = js.JSStringCreateWithUTF8CString(this.match.pathname[0.. :0]);
|
||||
}
|
||||
|
||||
return js.JSValueMakeString(ctx, this.pathname_str);
|
||||
}
|
||||
|
||||
pub fn getAsPath(
|
||||
this: *Router,
|
||||
ctx: js.JSContextRef,
|
||||
thisObject: js.JSObjectRef,
|
||||
prop: js.JSStringRef,
|
||||
exception: js.ExceptionRef,
|
||||
) js.JSValueRef {
|
||||
return js.JSValueMakeString(ctx, Properties.Refs.default);
|
||||
}
|
||||
|
||||
pub fn getRoute(
|
||||
this: *Router,
|
||||
ctx: js.JSContextRef,
|
||||
thisObject: js.JSObjectRef,
|
||||
prop: js.JSStringRef,
|
||||
exception: js.ExceptionRef,
|
||||
) js.JSValueRef {
|
||||
return js.JSValueMakeString(ctx, Properties.Refs.default);
|
||||
}
|
||||
|
||||
pub fn getQuery(
|
||||
this: *Router,
|
||||
ctx: js.JSContextRef,
|
||||
thisObject: js.JSObjectRef,
|
||||
prop: js.JSStringRef,
|
||||
exception: js.ExceptionRef,
|
||||
) js.JSValueRef {
|
||||
return js.JSValueMakeString(ctx, Properties.Refs.default);
|
||||
}
|
||||
};
|
||||
@@ -33,7 +33,7 @@ pub const To = struct {
|
||||
return function;
|
||||
}
|
||||
|
||||
pub fn Finalize(n
|
||||
pub fn Finalize(
|
||||
comptime ZigContextType: type,
|
||||
comptime ctxfn: fn (
|
||||
this: *ZigContextType,
|
||||
|
||||
@@ -768,6 +768,8 @@ pub const Module = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
var module = this;
|
||||
|
||||
var total_len: usize = 0;
|
||||
for (arguments) |argument| {
|
||||
const len = js.JSStringGetLength(argument);
|
||||
@@ -785,7 +787,6 @@ pub const Module = struct {
|
||||
const end = js.JSStringGetUTF8CString(argument, require_buf.list.items.ptr, require_buf.list.items.len);
|
||||
total_len += end;
|
||||
const import_path = require_buf.list.items[0 .. end - 1];
|
||||
var module = this;
|
||||
|
||||
if (this.vm.bundler.linker.resolver.resolve(module.path.name.dirWithTrailingSlash(), import_path, .require)) |resolved| {
|
||||
var load_result = Module.loadFromResolveResult(this.vm, ctx, resolved, exception) catch |err| {
|
||||
@@ -835,7 +836,7 @@ pub const Module = struct {
|
||||
for (arguments) |argument| {
|
||||
const end = js.JSStringGetUTF8CString(argument, remainder.ptr, total_len - used_len);
|
||||
used_len += end;
|
||||
remainder[end - 1] = ",";
|
||||
remainder[end - 1] = ',';
|
||||
remainder = remainder[end..];
|
||||
}
|
||||
|
||||
@@ -1126,7 +1127,7 @@ pub const Module = struct {
|
||||
}
|
||||
var module: *Module = undefined;
|
||||
|
||||
if (needs_reload) {
|
||||
if (reload_pending) {
|
||||
module = vm.require_cache.get(hash).?;
|
||||
} else {
|
||||
module = try vm.allocator.create(Module);
|
||||
@@ -1134,12 +1135,12 @@ pub const Module = struct {
|
||||
}
|
||||
|
||||
errdefer {
|
||||
if (!needs_reload) {
|
||||
if (!reload_pending) {
|
||||
vm.allocator.destroy(module);
|
||||
}
|
||||
}
|
||||
|
||||
if (needs_reload) {
|
||||
if (reload_pending) {
|
||||
try Module.load(
|
||||
module,
|
||||
vm,
|
||||
@@ -1345,7 +1346,7 @@ pub const EventListenerMixin = struct {
|
||||
fetch,
|
||||
err,
|
||||
|
||||
const SizeMatcher = strings.ExactSizeMatcher("fetch".len);
|
||||
const SizeMatcher = strings.ExactSizeMatcher(8);
|
||||
|
||||
pub fn match(str: string) ?EventType {
|
||||
return switch (SizeMatcher.match(str)) {
|
||||
|
||||
@@ -2,7 +2,7 @@ usingnamespace @import("../base.zig");
|
||||
const std = @import("std");
|
||||
const Api = @import("../../../api/schema.zig").Api;
|
||||
const http = @import("../../../http.zig");
|
||||
pub const JavaScript = @import("../javascript.zig");
|
||||
const JavaScript = @import("../javascript.zig");
|
||||
pub const Response = struct {
|
||||
pub const Class = NewClass(
|
||||
Response,
|
||||
@@ -1129,5 +1129,4 @@ pub const FetchEvent = struct {
|
||||
) js.JSValueRef {
|
||||
return js.JSValueMakeUndefined(ctx);
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/// This file is mostly the API schema but with all the options normalized.
|
||||
/// Normalization is necessary because most fields in the API schema are optional
|
||||
const std = @import("std");
|
||||
const logger = @import("logger.zig");
|
||||
const Fs = @import("fs.zig");
|
||||
@@ -8,7 +10,6 @@ const Api = api.Api;
|
||||
const defines = @import("./defines.zig");
|
||||
const resolve_path = @import("./resolver/resolve_path.zig");
|
||||
const NodeModuleBundle = @import("./node_module_bundle.zig").NodeModuleBundle;
|
||||
|
||||
usingnamespace @import("global.zig");
|
||||
|
||||
const assert = std.debug.assert;
|
||||
@@ -25,8 +26,8 @@ pub fn validatePath(log: *logger.Log, fs: *Fs.FileSystem.Implementation, cwd: st
|
||||
}
|
||||
const paths = [_]string{ cwd, rel_path };
|
||||
const out = std.fs.path.resolve(allocator, &paths) catch |err| {
|
||||
log.addErrorFmt(null, logger.Loc{}, allocator, "Invalid {s}: {s}", .{ path_kind, rel_path }) catch unreachable;
|
||||
Global.panic("", .{});
|
||||
Global.invariant(false, "<r><red>{s}<r> resolving external: <b>\"{s}\"<r>", .{ @errorName(err), rel_path });
|
||||
return "";
|
||||
};
|
||||
|
||||
return out;
|
||||
@@ -597,6 +598,8 @@ pub fn loadersFromTransformOptions(allocator: *std.mem.Allocator, _loaders: ?Api
|
||||
return loaders;
|
||||
}
|
||||
|
||||
/// BundleOptions is used when ResolveMode is not set to "disable".
|
||||
/// BundleOptions is effectively webpack + babel
|
||||
pub const BundleOptions = struct {
|
||||
footer: string = "",
|
||||
banner: string = "",
|
||||
@@ -632,6 +635,8 @@ pub const BundleOptions = struct {
|
||||
extension_order: []const string = &Defaults.ExtensionOrder,
|
||||
out_extensions: std.StringHashMap(string),
|
||||
import_path_format: ImportPathFormat = ImportPathFormat.relative,
|
||||
framework: ?Framework = null,
|
||||
route_config: ?RouteConfig = null,
|
||||
|
||||
pub fn asJavascriptBundleConfig(this: *const BundleOptions) Api.JavascriptBundleConfig {}
|
||||
|
||||
@@ -701,6 +706,14 @@ pub const BundleOptions = struct {
|
||||
opts.external = ExternalModules.init(allocator, &fs.fs, fs.top_level_dir, transform.external, log, opts.platform);
|
||||
opts.out_extensions = opts.platform.outExtensions(allocator);
|
||||
|
||||
if (transform.framework) |_framework| {
|
||||
opts.framework = try Framework.fromApi(_framework);
|
||||
}
|
||||
|
||||
if (transform.router) |route_config| {
|
||||
opts.route_config = try RouteConfig.fromApi(route_config, allocator);
|
||||
}
|
||||
|
||||
if (transform.serve orelse false) {
|
||||
opts.preserve_extensions = true;
|
||||
opts.append_package_version_in_query_string = true;
|
||||
@@ -720,25 +733,25 @@ pub const BundleOptions = struct {
|
||||
defer allocator.free(check_static);
|
||||
|
||||
std.fs.accessAbsolute(check_static, .{}) catch {
|
||||
Output.printError("warn: \"public\" folder missing. If there are external assets used in your project, pass --public-dir=\"public-folder-name\"", .{});
|
||||
Output.prettyErrorln("warn: \"public\" folder missing. If there are external assets used in your project, pass --public-dir=\"public-folder-name\"", .{});
|
||||
did_warn = true;
|
||||
};
|
||||
}
|
||||
|
||||
if (!did_warn) {
|
||||
Output.printError("warn: \"public\" folder missing. If you want to use \"static\" as the public folder, pass --public-dir=\"static\".", .{});
|
||||
Output.prettyErrorln("warn: \"public\" folder missing. If you want to use \"static\" as the public folder, pass --public-dir=\"static\".", .{});
|
||||
}
|
||||
opts.public_dir_enabled = false;
|
||||
},
|
||||
error.AccessDenied => {
|
||||
Output.printError(
|
||||
Output.prettyErrorln(
|
||||
"error: access denied when trying to open public_dir: \"{s}\".\nPlease re-open Speedy with access to this folder or pass a different folder via \"--public-dir\". Note: --public-dir is relative to --cwd (or the process' current working directory).\n\nThe public folder is where static assets such as images, fonts, and .html files go.",
|
||||
.{opts.public_dir},
|
||||
);
|
||||
std.process.exit(1);
|
||||
},
|
||||
else => {
|
||||
Output.printError(
|
||||
Output.prettyErrorln(
|
||||
"error: \"{s}\" when accessing public folder: \"{s}\"",
|
||||
.{ @errorName(err), opts.public_dir },
|
||||
);
|
||||
@@ -749,7 +762,7 @@ pub const BundleOptions = struct {
|
||||
break :brk null;
|
||||
};
|
||||
|
||||
// Windows has weird locking rules for files
|
||||
// Windows has weird locking rules for file access.
|
||||
// so it's a bad idea to keep a file handle open for a long time on Windows.
|
||||
if (isWindows and opts.public_dir_handle != null) {
|
||||
opts.public_dir_handle.?.close();
|
||||
@@ -1084,3 +1097,73 @@ pub const TransformResult = struct {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Framework = struct {
|
||||
entry_point: string,
|
||||
|
||||
pub fn fromApi(
|
||||
transform: Api.FrameworkConfig,
|
||||
) !Framework {
|
||||
return Framework{
|
||||
.entry_point = transform.entry_point.?,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const RouteConfig = struct {
|
||||
///
|
||||
dir: string,
|
||||
// TODO: do we need a separate list for data-only extensions?
|
||||
// e.g. /foo.json just to get the data for the route, without rendering the html
|
||||
// I think it's fine to hardcode as .json for now, but if I personally were writing a framework
|
||||
// I would consider using a custom binary format to minimize request size
|
||||
// maybe like CBOR
|
||||
extensions: []const string,
|
||||
|
||||
pub const DefaultDir = "pages";
|
||||
pub const DefaultExtensions = [_]string{ "tsx", "ts", "mjs", "jsx", "js" };
|
||||
|
||||
pub fn fromApi(router_: Api.RouteConfig, allocator: *std.mem.Allocator) !RouteConfig {
|
||||
var router = RouteConfig{
|
||||
.dir = DefaultDir,
|
||||
.extensions = std.mem.span(&DefaultExtensions),
|
||||
};
|
||||
|
||||
var router_dir: string = std.mem.trimRight(u8, router_.dir orelse "", "/\\");
|
||||
|
||||
if (router_dir.len != 0) {
|
||||
router.dir = router_dir;
|
||||
}
|
||||
|
||||
if (router_.extensions.len > 0) {
|
||||
var count: usize = 0;
|
||||
for (router_.extensions) |_ext| {
|
||||
const ext = std.mem.trimLeft(u8, _ext, ".");
|
||||
|
||||
if (ext.len == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
}
|
||||
|
||||
var extensions = try allocator.alloc(string, count);
|
||||
var remainder = extensions;
|
||||
|
||||
for (router_.extensions) |_ext| {
|
||||
const ext = std.mem.trimLeft(u8, _ext, ".");
|
||||
|
||||
if (ext.len == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
remainder[0] = ext;
|
||||
remainder = remainder[1..];
|
||||
}
|
||||
|
||||
router.extensions = extensions;
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -57,7 +57,7 @@ pub const PackageJSON = struct {
|
||||
const parts = [_]string{ input_path, "package.json" };
|
||||
|
||||
const package_json_path_ = r.fs.abs(&parts);
|
||||
const package_json_path = r.fs.filename_store.append(package_json_path_) catch unreachable;
|
||||
const package_json_path = r.fs.filename_store.append(@TypeOf(package_json_path_), package_json_path_) catch unreachable;
|
||||
|
||||
const entry = r.caches.fs.readFile(
|
||||
r.fs,
|
||||
|
||||
@@ -682,7 +682,7 @@ pub fn joinStringBuf(buf: []u8, _parts: anytype, comptime _platform: Platform) [
|
||||
}
|
||||
|
||||
// Preserve leading separator
|
||||
if (_parts[0][0] == _platform.separator()) {
|
||||
if (_parts[0].len > 0 and _parts[0][0] == _platform.separator()) {
|
||||
const out = switch (platform) {
|
||||
.loose => normalizeStringLooseBuf(parser_join_input_buffer[0..written], buf[1..], false, false),
|
||||
.windows => normalizeStringWindows(parser_join_input_buffer[0..written], buf[1..], false, false),
|
||||
@@ -769,7 +769,7 @@ pub fn joinAbsStringBuf(_cwd: []const u8, buf: []u8, _parts: anytype, comptime _
|
||||
|
||||
const offset = out;
|
||||
out += normalized_part.len;
|
||||
std.debug.assert(out < buf.len);
|
||||
std.debug.assert(out <= buf.len);
|
||||
std.mem.copy(u8, buf[offset..out], normalized_part);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,18 @@ pub const DirInfo = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getEntriesConst(dirinfo: *const DirInfo) ?*const Fs.FileSystem.DirEntry {
|
||||
const entries_ptr = Fs.FileSystem.instance.fs.entries.atIndex(dirinfo.entries) orelse return null;
|
||||
switch (entries_ptr.*) {
|
||||
.entries => |entr| {
|
||||
return &entries_ptr.entries;
|
||||
},
|
||||
.err => {
|
||||
return null;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getParent(i: *const DirInfo) ?*DirInfo {
|
||||
return HashMap.instance.atIndex(i.parent);
|
||||
}
|
||||
@@ -588,7 +600,7 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
}
|
||||
|
||||
return Result{
|
||||
.path_pair = .{ .primary = Path.init(r.fs.filename_store.append(abs_path) catch unreachable) },
|
||||
.path_pair = .{ .primary = Path.init(r.fs.filename_store.append(@TypeOf(abs_path), abs_path) catch unreachable) },
|
||||
.is_external = true,
|
||||
};
|
||||
}
|
||||
@@ -603,7 +615,7 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
if (r.checkBrowserMap(pkg, rel_path)) |remap| {
|
||||
// Is the path disabled?
|
||||
if (remap.len == 0) {
|
||||
var _path = Path.init(r.fs.filename_store.append(abs_path) catch unreachable);
|
||||
var _path = Path.init(r.fs.filename_store.append(string, abs_path) catch unreachable);
|
||||
_path.is_disabled = true;
|
||||
return Result{
|
||||
.path_pair = PathPair{
|
||||
@@ -872,14 +884,14 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
// this might leak
|
||||
if (!std.fs.path.isAbsolute(result.base_url)) {
|
||||
const paths = [_]string{ file_dir, result.base_url };
|
||||
result.base_url = r.fs.filename_store.append(r.fs.absBuf(&paths, &tsconfig_base_url_buf)) catch unreachable;
|
||||
result.base_url = r.fs.filename_store.append(string, r.fs.absBuf(&paths, &tsconfig_base_url_buf)) catch unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.paths.count() > 0 and (result.base_url_for_paths.len == 0 or !std.fs.path.isAbsolute(result.base_url_for_paths))) {
|
||||
// this might leak
|
||||
const paths = [_]string{ file_dir, result.base_url };
|
||||
result.base_url_for_paths = r.fs.filename_store.append(r.fs.absBuf(&paths, &tsconfig_base_url_buf)) catch unreachable;
|
||||
result.base_url_for_paths = r.fs.filename_store.append(string, r.fs.absBuf(&paths, &tsconfig_base_url_buf)) catch unreachable;
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -904,7 +916,28 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
}
|
||||
}
|
||||
|
||||
fn dirInfoCached(r: *ThisResolver, path: string) !?*DirInfo {
|
||||
fn dirInfoCached(
|
||||
r: *ThisResolver,
|
||||
path: string,
|
||||
) !?*DirInfo {
|
||||
return try r.dirInfoCachedMaybeLog(path, true);
|
||||
}
|
||||
|
||||
pub fn readDirInfo(
|
||||
r: *ThisResolver,
|
||||
path: string,
|
||||
) !?*DirInfo {
|
||||
return try r.dirInfoCachedMaybeLog(path, false);
|
||||
}
|
||||
|
||||
pub fn readDirInfoIgnoreError(
|
||||
r: *ThisResolver,
|
||||
path: string,
|
||||
) ?*const DirInfo {
|
||||
return r.dirInfoCachedMaybeLog(path, false) catch null;
|
||||
}
|
||||
|
||||
inline fn dirInfoCachedMaybeLog(r: *ThisResolver, path: string, comptime enable_logging: bool) !?*DirInfo {
|
||||
const top_result = try r.dir_cache.getOrPut(path);
|
||||
if (top_result.status != .unknown) {
|
||||
return r.dir_cache.atIndex(top_result.index);
|
||||
@@ -1021,18 +1054,20 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
var cached_dir_entry_result = rfs.entries.getOrPut(queue_top.unsafe_path) catch unreachable;
|
||||
r.dir_cache.markNotFound(queue_top.result);
|
||||
rfs.entries.markNotFound(cached_dir_entry_result);
|
||||
const pretty = r.prettyPath(Path.init(queue_top.unsafe_path));
|
||||
if (comptime enable_logging) {
|
||||
const pretty = r.prettyPath(Path.init(queue_top.unsafe_path));
|
||||
|
||||
r.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc{},
|
||||
r.allocator,
|
||||
"Cannot read directory \"{s}\": {s}",
|
||||
.{
|
||||
pretty,
|
||||
@errorName(err),
|
||||
},
|
||||
) catch {};
|
||||
r.log.addErrorFmt(
|
||||
null,
|
||||
logger.Loc{},
|
||||
r.allocator,
|
||||
"Cannot read directory \"{s}\": {s}",
|
||||
.{
|
||||
pretty,
|
||||
@errorName(err),
|
||||
},
|
||||
) catch {};
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1043,9 +1078,15 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
_open_dirs[open_dir_count] = open_dir;
|
||||
open_dir_count += 1;
|
||||
|
||||
// ensure trailing slash
|
||||
if (_safe_path == null) {
|
||||
// Now that we've opened the topmost directory successfully, it's reasonable to store the slice.
|
||||
_safe_path = try r.fs.dirname_store.append(path);
|
||||
if (path[path.len - 1] != '/') {
|
||||
var parts = [_]string{ path, "/" };
|
||||
_safe_path = try r.fs.dirname_store.append(@TypeOf(parts), parts);
|
||||
} else {
|
||||
_safe_path = try r.fs.dirname_store.append(string, path);
|
||||
}
|
||||
}
|
||||
const safe_path = _safe_path.?;
|
||||
|
||||
@@ -1609,7 +1650,10 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
|
||||
const dir_path = std.fs.path.dirname(path) orelse "/";
|
||||
|
||||
const dir_entry: *Fs.FileSystem.RealFS.EntriesOption = rfs.readDirectory(dir_path, null, false) catch {
|
||||
const dir_entry: *Fs.FileSystem.RealFS.EntriesOption = rfs.readDirectory(
|
||||
dir_path,
|
||||
null,
|
||||
) catch {
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1644,7 +1688,7 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
debug.addNoteFmt("Found file \"{s}\" ", .{base}) catch {};
|
||||
}
|
||||
const abs_path_parts = [_]string{ query.entry.dir, query.entry.base };
|
||||
const abs_path = r.fs.filename_store.append(r.fs.joinBuf(&abs_path_parts, &TemporaryBuffer.ExtensionPathBuf)) catch unreachable;
|
||||
const abs_path = r.fs.filename_store.append(string, r.fs.joinBuf(&abs_path_parts, &TemporaryBuffer.ExtensionPathBuf)) catch unreachable;
|
||||
|
||||
return LoadResult{
|
||||
.path = abs_path,
|
||||
@@ -1673,7 +1717,7 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
|
||||
// now that we've found it, we allocate it.
|
||||
return LoadResult{
|
||||
.path = r.fs.filename_store.append(buffer) catch unreachable,
|
||||
.path = r.fs.filename_store.append(@TypeOf(buffer), buffer) catch unreachable,
|
||||
.diff_case = query.diff_case,
|
||||
.dirname_fd = entries.fd,
|
||||
};
|
||||
@@ -1715,7 +1759,7 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
}
|
||||
|
||||
return LoadResult{
|
||||
.path = r.fs.filename_store.append(buffer) catch unreachable,
|
||||
.path = r.fs.filename_store.append(@TypeOf(buffer), buffer) catch unreachable,
|
||||
.diff_case = query.diff_case,
|
||||
.dirname_fd = entries.fd,
|
||||
};
|
||||
@@ -1752,6 +1796,7 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
|
||||
var info = DirInfo{
|
||||
.abs_path = path,
|
||||
// .abs_real_path = path,
|
||||
.parent = parent_index,
|
||||
.entries = dir_entry_index,
|
||||
};
|
||||
@@ -1787,7 +1832,7 @@ pub fn NewResolver(cache_files: bool) type {
|
||||
} else if (parent.?.abs_real_path.len > 0) {
|
||||
// this might leak a little i'm not sure
|
||||
const parts = [_]string{ parent.?.abs_real_path, base };
|
||||
symlink = r.fs.filename_store.append(r.fs.joinBuf(&parts, &dir_info_uncached_filename_buf)) catch unreachable;
|
||||
symlink = r.fs.filename_store.append(string, r.fs.joinBuf(&parts, &dir_info_uncached_filename_buf)) catch unreachable;
|
||||
|
||||
if (r.debug_logs) |*logs| {
|
||||
try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }) catch unreachable);
|
||||
|
||||
644
src/router.zig
Normal file
644
src/router.zig
Normal file
@@ -0,0 +1,644 @@
|
||||
// This is a Next.js-compatible file-system router.
|
||||
// It uses the filesystem to infer entry points.
|
||||
// Despite being Next.js-compatible, it's not tied to Next.js.
|
||||
// It does not handle the framework parts of rendering pages.
|
||||
// All it does is resolve URL paths to the appropriate entry point and parse URL params/query.
|
||||
const Router = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const DirInfo = @import("./resolver/resolver.zig").DirInfo;
|
||||
usingnamespace @import("global.zig");
|
||||
const Fs = @import("./fs.zig");
|
||||
const Options = @import("./options.zig");
|
||||
const allocators = @import("./allocators.zig");
|
||||
const URLPath = @import("./http.zig").URLPath;
|
||||
|
||||
const index_route_hash = @truncate(u32, std.hash.Wyhash.hash(0, "index"));
|
||||
const arbitrary_max_route = 4096;
|
||||
|
||||
dir: StoredFileDescriptorType = 0,
|
||||
routes: RouteMap,
|
||||
loaded_routes: bool = false,
|
||||
allocator: *std.mem.Allocator,
|
||||
fs: *Fs.FileSystem,
|
||||
config: Options.RouteConfig,
|
||||
|
||||
pub fn init(
|
||||
fs: *Fs.FileSystem,
|
||||
allocator: *std.mem.Allocator,
|
||||
config: Options.RouteConfig,
|
||||
) !Router {
|
||||
return Router{
|
||||
.routes = RouteMap{
|
||||
.routes = Route.List{},
|
||||
.index = null,
|
||||
.allocator = allocator,
|
||||
.config = config,
|
||||
},
|
||||
.fs = fs,
|
||||
.allocator = allocator,
|
||||
.config = config,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getEntryPoints(this: *const Router, allocator: *std.mem.Allocator) ![]const string {
|
||||
var i: u16 = 0;
|
||||
const route_count: u16 = @truncate(u16, this.routes.routes.len);
|
||||
|
||||
var count: usize = 0;
|
||||
var str_len: usize = 0;
|
||||
|
||||
while (i < route_count) : (i += 1) {
|
||||
const children = this.routes.routes.items(.children)[i];
|
||||
count += @intCast(
|
||||
usize,
|
||||
@boolToInt(children.len == 0),
|
||||
);
|
||||
if (children.len == 0) {
|
||||
if (Fs.FileSystem.DirEntry.EntryStore.instance.at(this.routes.routes.items(.entry_index)[i])) |entry| {
|
||||
str_len += entry.base.len + entry.dir.len;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var buffer = try allocator.alloc(u8, str_len + count);
|
||||
var remain = buffer;
|
||||
var entry_points = try allocator.alloc(string, count);
|
||||
|
||||
i = 0;
|
||||
var entry_point_i: usize = 0;
|
||||
while (i < route_count) : (i += 1) {
|
||||
const children = this.routes.routes.items(.children)[i];
|
||||
if (children.len == 0) {
|
||||
if (Fs.FileSystem.DirEntry.EntryStore.instance.at(this.routes.routes.items(.entry_index)[i])) |entry| {
|
||||
var parts = [_]string{ entry.dir, entry.base };
|
||||
entry_points[entry_point_i] = this.fs.absBuf(&parts, remain);
|
||||
remain = remain[entry_points[entry_point_i].len..];
|
||||
entry_point_i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entry_points;
|
||||
}
|
||||
|
||||
const banned_dirs = [_]string{
|
||||
"node_modules",
|
||||
};
|
||||
|
||||
// This loads routes recursively, in depth-first order.
|
||||
// it does not currently handle duplicate exact route matches. that's undefined behavior, for now.
|
||||
pub fn loadRoutes(
|
||||
this: *Router,
|
||||
root_dir_info: *const DirInfo,
|
||||
comptime ResolverType: type,
|
||||
resolver: *ResolverType,
|
||||
parent: u16,
|
||||
comptime is_root: bool,
|
||||
) anyerror!void {
|
||||
var fs = &this.fs.fs;
|
||||
if (root_dir_info.getEntriesConst()) |entries| {
|
||||
var iter = entries.data.iterator();
|
||||
outer: while (iter.next()) |entry_ptr| {
|
||||
const entry = Fs.FileSystem.DirEntry.EntryStore.instance.at(entry_ptr.value) orelse continue;
|
||||
if (entry.base[0] == '.') {
|
||||
continue :outer;
|
||||
}
|
||||
|
||||
switch (entry.kind(fs)) {
|
||||
.dir => {
|
||||
inline for (banned_dirs) |banned_dir| {
|
||||
if (strings.eqlComptime(entry.base, comptime banned_dir)) {
|
||||
continue :outer;
|
||||
}
|
||||
}
|
||||
var abs_parts = [_]string{ entry.dir, entry.base };
|
||||
if (resolver.readDirInfoIgnoreError(this.fs.abs(&abs_parts))) |_dir_info| {
|
||||
const dir_info: *const DirInfo = _dir_info;
|
||||
|
||||
var route: Route = Route.parse(
|
||||
entry.base,
|
||||
entry.dir[this.config.dir.len..],
|
||||
"",
|
||||
entry_ptr.value,
|
||||
);
|
||||
|
||||
route.parent = parent;
|
||||
route.children.offset = @truncate(u16, this.routes.routes.len);
|
||||
try this.routes.routes.append(this.allocator, route);
|
||||
|
||||
// potential stack overflow!
|
||||
try this.loadRoutes(
|
||||
dir_info,
|
||||
ResolverType,
|
||||
resolver,
|
||||
route.children.offset,
|
||||
false,
|
||||
);
|
||||
|
||||
this.routes.routes.items(.children)[route.children.offset].len = @truncate(u16, this.routes.routes.len) - route.children.offset;
|
||||
}
|
||||
},
|
||||
|
||||
.file => {
|
||||
const extname = std.fs.path.extension(entry.base);
|
||||
// exclude "." or ""
|
||||
if (extname.len < 2) continue;
|
||||
|
||||
for (this.config.extensions) |_extname| {
|
||||
if (strings.eql(extname[1..], _extname)) {
|
||||
var route = Route.parse(
|
||||
entry.base,
|
||||
entry.dir[this.config.dir.len..],
|
||||
extname,
|
||||
entry_ptr.value,
|
||||
);
|
||||
route.parent = parent;
|
||||
|
||||
if (comptime is_root) {
|
||||
if (strings.eqlComptime(route.name, "index")) {
|
||||
this.routes.index = @truncate(u32, this.routes.routes.len);
|
||||
}
|
||||
}
|
||||
|
||||
try this.routes.routes.append(
|
||||
this.allocator,
|
||||
route,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TinyPtr = packed struct {
|
||||
offset: u16 = 0,
|
||||
len: u16 = 0,
|
||||
};
|
||||
|
||||
const Param = struct {
|
||||
key: string,
|
||||
value: string,
|
||||
|
||||
pub const List = std.MultiArrayList(Param);
|
||||
};
|
||||
|
||||
pub const Route = struct {
|
||||
part: RoutePart,
|
||||
name: string,
|
||||
path: string,
|
||||
hash: u32,
|
||||
children: Ptr = Ptr{},
|
||||
parent: u16 = top_level_parent,
|
||||
entry_index: allocators.IndexType,
|
||||
|
||||
full_hash: u32,
|
||||
|
||||
pub const top_level_parent = std.math.maxInt(u16);
|
||||
|
||||
pub const List = std.MultiArrayList(Route);
|
||||
pub const Ptr = TinyPtr;
|
||||
|
||||
pub fn parse(base: string, dir: string, extname: string, entry_index: allocators.IndexType) Route {
|
||||
var parts = [_]string{ dir, base };
|
||||
// this isn't really absolute, it's relative to the pages dir
|
||||
const absolute = Fs.FileSystem.instance.abs(&parts);
|
||||
const name = base[0 .. base.len - extname.len];
|
||||
|
||||
return Route{
|
||||
.name = name,
|
||||
.path = base,
|
||||
.entry_index = entry_index,
|
||||
.hash = @truncate(
|
||||
u32,
|
||||
std.hash.Wyhash.hash(
|
||||
0,
|
||||
name,
|
||||
),
|
||||
),
|
||||
.full_hash = @truncate(
|
||||
u32,
|
||||
std.hash.Wyhash.hash(
|
||||
0,
|
||||
absolute[0 .. absolute.len - extname.len],
|
||||
),
|
||||
),
|
||||
.part = RoutePart.parse(name),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Reference: https://nextjs.org/docs/routing/introduction
|
||||
// Examples:
|
||||
// - pages/index.js => /
|
||||
// - pages/foo.js => /foo
|
||||
// - pages/foo/index.js => /foo
|
||||
// - pages/foo/[bar] => {/foo/bacon, /foo/bar, /foo/baz, /foo/10293012930}
|
||||
// - pages/foo/[...bar] => {/foo/bacon/toast, /foo/bar/what, /foo/baz, /foo/10293012930}
|
||||
// Syntax:
|
||||
// - [param-name]
|
||||
// - Catch All: [...param-name]
|
||||
// - Optional Catch All: [[...param-name]]
|
||||
// Invalid syntax:
|
||||
// - pages/foo/hello-[bar]
|
||||
// - pages/foo/[bar]-foo
|
||||
pub const RouteMap = struct {
|
||||
routes: Route.List,
|
||||
index: ?u32,
|
||||
allocator: *std.mem.Allocator,
|
||||
config: Options.RouteConfig,
|
||||
|
||||
pub threadlocal var segments_buf: [128]string = undefined;
|
||||
pub threadlocal var segments_hash: [128]u32 = undefined;
|
||||
|
||||
pub fn routePathLen(this: *const RouteMap, _ptr: u16) u16 {
|
||||
return this.appendRoutePath(_ptr, &[_]u8{}, false);
|
||||
}
|
||||
|
||||
// This is probably really slow
|
||||
// But it might be fine because it's mostly looking up within the same array
|
||||
// and that array is probably in the cache line
|
||||
var ptr_buf: [arbitrary_max_route]u16 = undefined;
|
||||
// TODO: skip copying parent dirs when it's another file in the same parent dir
|
||||
pub fn appendRoutePath(this: *const RouteMap, tail: u16, buf: []u8, comptime write: bool) u16 {
|
||||
var head: u16 = this.routes.items(.parent)[tail];
|
||||
|
||||
var ptr_buf_count: i32 = 0;
|
||||
var written: u16 = 0;
|
||||
while (!(head == Route.top_level_parent)) : (ptr_buf_count += 1) {
|
||||
ptr_buf[@intCast(usize, ptr_buf_count)] = head;
|
||||
head = this.routes.items(.parent)[head];
|
||||
}
|
||||
|
||||
var i: usize = @intCast(usize, ptr_buf_count);
|
||||
var remain = buf;
|
||||
while (i > 0) : (i -= 1) {
|
||||
const path = this.routes.items(.path)[
|
||||
@intCast(
|
||||
usize,
|
||||
ptr_buf[i],
|
||||
)
|
||||
];
|
||||
if (comptime write) {
|
||||
std.mem.copy(u8, remain, path);
|
||||
|
||||
remain = remain[path.len..];
|
||||
remain[0] = std.fs.path.sep;
|
||||
remain = remain[1..];
|
||||
}
|
||||
written += @truncate(u16, path.len + 1);
|
||||
}
|
||||
|
||||
{
|
||||
const path = this.routes.items(.path)[tail];
|
||||
if (comptime write) {
|
||||
std.mem.copy(u8, remain, path);
|
||||
}
|
||||
written += @truncate(u16, path.len);
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
pub const MatchedRoute = struct {
|
||||
/// normalized url path from the request
|
||||
path: string,
|
||||
/// raw url path from the request
|
||||
pathname: string,
|
||||
/// absolute filesystem path to the entry point
|
||||
file_path: string,
|
||||
/// route name, like `"posts/[id]"`
|
||||
name: string,
|
||||
|
||||
/// basename of the route in the file system, including file extension
|
||||
basename: string,
|
||||
|
||||
hash: u32,
|
||||
params: *Param.List,
|
||||
redirect_path: ?string = null,
|
||||
query_string: string = "",
|
||||
};
|
||||
|
||||
const MatchContext = struct {
|
||||
params: *Param.List,
|
||||
segments: []string,
|
||||
hashes: []u32,
|
||||
map: *RouteMap,
|
||||
allocator: *std.mem.Allocator,
|
||||
redirect_path: ?string = "",
|
||||
url_path: URLPath,
|
||||
|
||||
matched_route_name: PathBuilder = PathBuilder.init(),
|
||||
matched_route_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined,
|
||||
|
||||
file_path: string = "",
|
||||
|
||||
pub fn matchDynamicRoute(
|
||||
this: *MatchContext,
|
||||
head_i: u16,
|
||||
segment_i: u16,
|
||||
) ?MatchedRoute {
|
||||
var match = this._matchDynamicRoute(head_i, segment_i) orelse return null;
|
||||
this.matched_route_name.append("/");
|
||||
this.matched_route_name.append(match.name);
|
||||
return match;
|
||||
}
|
||||
|
||||
fn _matchDynamicRoute(
|
||||
this: *MatchContext,
|
||||
head_i: u16,
|
||||
segment_i: u16,
|
||||
) ?MatchedRoute {
|
||||
const start_len = this.params.len;
|
||||
var head = this.map.routes.get(head_i);
|
||||
const segment = this.segments[segment_i];
|
||||
const remaining = this.segments[segment_i..];
|
||||
|
||||
if (remaining.len > 0 and head.children.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (head.part.tag) {
|
||||
.exact => {
|
||||
if (this.hashes[segment_i] != head.hash) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
var match_result: MatchedRoute = undefined;
|
||||
if (head.children.len > 0 and remaining.len > 0) {
|
||||
var child_i = head.children.offset;
|
||||
const last = child_i + head.children.len;
|
||||
var matched = false;
|
||||
while (child_i < last) : (child_i += 1) {
|
||||
if (this.matchDynamicRoute(child_i, segment_i + 1)) |res| {
|
||||
match_result = res;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
this.params.shrinkRetainingCapacity(start_len);
|
||||
return null;
|
||||
}
|
||||
// this is a folder
|
||||
} else if (remaining.len == 0 and head.children.len > 0) {
|
||||
this.params.shrinkRetainingCapacity(start_len);
|
||||
return null;
|
||||
} else {
|
||||
match_result = MatchedRoute{
|
||||
.path = head.path,
|
||||
.name = head.name,
|
||||
.params = this.params,
|
||||
.hash = head.full_hash,
|
||||
.query_string = this.url_path.query_string,
|
||||
.pathname = this.url_path.pathname,
|
||||
};
|
||||
|
||||
if (Fs.FileSystem.DirEntry.EntryStore.instance.at(head.entry_index)) |entry| {
|
||||
var parts = [_]string{ entry.dir, entry.base };
|
||||
match_result.file_path = Fs.FileSystem.instance.absBuf(&parts, this.matched_route_buf);
|
||||
this.matched_route_buf[match_result.file_path.len] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we know for sure the route will match, we append the param
|
||||
switch (head.part.tag) {
|
||||
.param => {
|
||||
this.params.append(
|
||||
this.allocator,
|
||||
.{
|
||||
.key = head.part.str(head.name),
|
||||
.value = segment,
|
||||
},
|
||||
) catch unreachable;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
return match_result;
|
||||
}
|
||||
};
|
||||
|
||||
// This makes many passes over the list of routes
|
||||
// However, most of those passes are basically array.indexOf(number) and then smallerArray.indexOf(number)
|
||||
pub fn matchPage(this: *RouteMap, url_path: URLPath, params: *Param.List) ?MatchedRoute {
|
||||
// Trim trailing slash
|
||||
var path = url_path.path;
|
||||
var redirect = false;
|
||||
|
||||
// Normalize trailing slash
|
||||
// "/foo/bar/index/" => "/foo/bar/index"
|
||||
if (path.len > 0 and path[path.len - 1] == '/') {
|
||||
path = path[0 .. path.len - 1];
|
||||
redirect = true;
|
||||
}
|
||||
|
||||
// Normal case: "/foo/bar/index" => "/foo/bar"
|
||||
// Pathological: "/foo/bar/index/index/index/index/index/index" => "/foo/bar"
|
||||
// Extremely pathological: "/index/index/index/index/index/index/index" => "index"
|
||||
while (strings.endsWith(path, "/index")) {
|
||||
path = path[0 .. path.len - "/index".len];
|
||||
redirect = true;
|
||||
}
|
||||
|
||||
if (strings.eqlComptime(path, "index")) {
|
||||
path = "";
|
||||
redirect = true;
|
||||
}
|
||||
|
||||
if (path.len == 0) {
|
||||
if (this.index) |index| {
|
||||
return MatchedRoute{
|
||||
.params = params,
|
||||
.name = "index",
|
||||
.path = this.routes.items(.path)[index],
|
||||
.pathname = url_path.pathname,
|
||||
.hash = index_route_hash,
|
||||
.query_string = url_path.query_string,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const full_hash = @truncate(u32, std.hash.Wyhash.hash(0, path));
|
||||
|
||||
// Check for an exact match
|
||||
// These means there are no params.
|
||||
if (std.mem.indexOfScalar(u32, this.routes.items(.full_hash), full_hash)) |exact_match| {
|
||||
const route = this.routes.get(exact_match);
|
||||
// It might be a folder with an index route
|
||||
// /bacon/index.js => /bacon
|
||||
if (route.children.len > 0) {
|
||||
const children = this.routes.items(.hash)[route.children.offset .. route.children.offset + route.children.len];
|
||||
for (children) |child_hash, i| {
|
||||
if (child_hash == index_route_hash) {
|
||||
return MatchedRoute{
|
||||
.params = params,
|
||||
.name = this.routes.items(.name)[i],
|
||||
.path = this.routes.items(.path)[i],
|
||||
.pathname = url_path.pathname,
|
||||
.hash = child_hash,
|
||||
.query_string = url_path.query_string,
|
||||
};
|
||||
}
|
||||
}
|
||||
// It's an exact route, there are no params
|
||||
// /foo/bar => /foo/bar.js
|
||||
} else {
|
||||
return MatchedRoute{
|
||||
.params = params,
|
||||
.name = route.name,
|
||||
.path = route.path,
|
||||
.redirect_path = if (redirect) path else null,
|
||||
.hash = full_hash,
|
||||
.pathname = url_path.pathname,
|
||||
.query_string = url_path.query_string,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var last_slash_i: usize = 0;
|
||||
var segments: []string = segments_buf[0..];
|
||||
var hashes: []u32 = segments_hash[0..];
|
||||
var segment_i: usize = 0;
|
||||
for (path) |i, c| {
|
||||
if (c == '/') {
|
||||
// if the URL is /foo/./foo
|
||||
// rewrite it as /foo/foo
|
||||
segments[segment_i] = path[last_slash_i..i];
|
||||
hashes[segment_i] = @truncate(u32, std.hash.Wyhash.hash(0, segments[segment_i]));
|
||||
|
||||
if (!(segments[segment_i].len == 1 and segments[segment_i][0] == '.')) {
|
||||
segment_i += 1;
|
||||
}
|
||||
|
||||
last_slash_i = i + 1;
|
||||
}
|
||||
}
|
||||
segments = segments[0..segment_i];
|
||||
|
||||
var ctx = MatchContext{
|
||||
.params = params,
|
||||
.segments = segments,
|
||||
.hashes = hashes,
|
||||
.map = this,
|
||||
.redirect_path = if (redirect) path else null,
|
||||
.allocator = this.allocator,
|
||||
.url_path = url_path,
|
||||
};
|
||||
|
||||
if (ctx.matchDynamicRoute(0, 0)) |dynamic_route| {
|
||||
dynamic_route.name = ctx.matched_route_name.str();
|
||||
return dynamic_route;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// This is a u32
|
||||
pub const RoutePart = packed struct {
|
||||
name: Ptr,
|
||||
tag: Tag,
|
||||
|
||||
pub fn str(this: RoutePart, name: string) string {
|
||||
return switch (this.tag) {
|
||||
.exact => name,
|
||||
else => name[this.name.offset..][0..this.name.len],
|
||||
};
|
||||
}
|
||||
|
||||
pub const Ptr = packed struct {
|
||||
offset: u14,
|
||||
len: u14,
|
||||
};
|
||||
|
||||
pub const Tag = enum(u4) {
|
||||
optional_catch_all = 1,
|
||||
catch_all = 2,
|
||||
param = 3,
|
||||
exact = 4,
|
||||
};
|
||||
|
||||
pub fn parse(base: string) RoutePart {
|
||||
std.debug.assert(base.len > 0);
|
||||
|
||||
var part = RoutePart{
|
||||
.name = Ptr{ .offset = 0, .len = @truncate(u14, base.len) },
|
||||
.tag = .exact,
|
||||
};
|
||||
|
||||
if (base[0] == '[') {
|
||||
if (base.len > 1) {
|
||||
switch (base[1]) {
|
||||
']' => {},
|
||||
|
||||
'[' => {
|
||||
// optional catch all
|
||||
if (strings.eqlComptime(base[1..std.math.min(base.len, 5)], "[...")) {
|
||||
part.name.len = @truncate(u14, std.mem.indexOfScalar(u8, base[5..], ']') orelse return part);
|
||||
part.name.offset = 5;
|
||||
part.tag = .optional_catch_all;
|
||||
}
|
||||
},
|
||||
'.' => {
|
||||
// regular catch all
|
||||
if (strings.eqlComptime(base[1..std.math.min(base.len, 4)], "...")) {
|
||||
part.name.len = @truncate(u14, std.mem.indexOfScalar(u8, base[4..], ']') orelse return part);
|
||||
part.name.offset = 4;
|
||||
part.tag = .catch_all;
|
||||
}
|
||||
},
|
||||
else => {
|
||||
part.name.len = @truncate(u14, std.mem.indexOfScalar(u8, base[1..], ']') orelse return part);
|
||||
part.tag = .param;
|
||||
part.name.offset = 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return part;
|
||||
}
|
||||
};
|
||||
|
||||
threadlocal var params_list: Param.List = undefined;
|
||||
pub fn match(app: *Router, comptime RequestContextType: type, ctx: *RequestContextType) !void {
|
||||
// If there's an extname assume it's an asset and not a page
|
||||
switch (ctx.url.extname.len) {
|
||||
0 => {},
|
||||
// json is used for updating the route client-side without a page reload
|
||||
"json".len => {
|
||||
if (!strings.eqlComptime(ctx.url.extname, "json")) {
|
||||
try ctx.handleRequest();
|
||||
return;
|
||||
}
|
||||
},
|
||||
else => {
|
||||
try ctx.handleRequest();
|
||||
return;
|
||||
},
|
||||
}
|
||||
|
||||
params_list.shrinkRetainingCapacity(0);
|
||||
if (app.routes.matchPage(0, ctx.url, ¶ms_list)) |route| {
|
||||
if (route.redirect_path) |redirect| {
|
||||
try ctx.handleRedirect(redirect);
|
||||
return;
|
||||
}
|
||||
|
||||
std.debug.assert(route.path.len > 0);
|
||||
|
||||
// ??? render javascript ??
|
||||
try ctx.handleRoute(route);
|
||||
}
|
||||
|
||||
try ctx.handleRequest();
|
||||
}
|
||||
@@ -9,6 +9,46 @@ pub const MutableString = mutable.MutableString;
|
||||
|
||||
pub const eql = std.meta.eql;
|
||||
|
||||
pub fn NewStringBuilder(comptime size: usize) type {
|
||||
return struct {
|
||||
const This = @This();
|
||||
buffer: [size + 1]u8 = undefined,
|
||||
remain: []u8 = undefined,
|
||||
|
||||
pub fn init() This {
|
||||
var instance = This{};
|
||||
instance.load();
|
||||
return instance;
|
||||
}
|
||||
|
||||
fn load(this: *This) void {
|
||||
this.remain = (&this.buffer)[0..size];
|
||||
}
|
||||
|
||||
pub fn append(this: *This, str: string) void {
|
||||
std.mem.copy(u8, this.remain, str);
|
||||
this.remain = this.remain[str.len..];
|
||||
}
|
||||
|
||||
pub fn str(this: *This) string {
|
||||
var buf = this.buffer[0 .. @ptrToInt(this.remain.ptr) - @ptrToInt(&this.buffer)];
|
||||
// Always leave a sentinel so that anything that expects a sentinel Just Works
|
||||
// specifically, the reason for this is so C-based APIs can be used without an extra copy.
|
||||
// one byte is cheap...right?
|
||||
this.buffer[buf.len] = 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
pub fn pop(this: *This, count: usize) string {
|
||||
this.remain = this.buffer[0 .. @ptrToInt(this.remain.ptr) - @ptrToInt(&this.buffer) - count];
|
||||
}
|
||||
|
||||
pub fn reset(this: *This) void {
|
||||
this.load();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn nql(a: anytype, b: @TypeOf(a)) bool {
|
||||
return !eql(a, b);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user