@barefootjs/mojolicious 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/__tests__/boolean-result.test.d.ts +2 -0
- package/dist/adapter/__tests__/boolean-result.test.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +42 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +1143 -0
- package/dist/adapter/mojo-adapter.d.ts +219 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -0
- package/dist/build.d.ts +28 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +1163 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1143 -0
- package/dist/test-render.d.ts +38 -0
- package/dist/test-render.d.ts.map +1 -0
- package/lib/BarefootJS.pm +745 -0
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +150 -0
- package/lib/Mojolicious/Plugin/BarefootJS.pm +104 -0
- package/package.json +65 -0
- package/src/__tests__/mojo-adapter.test.ts +940 -0
- package/src/__tests__/mojo-streaming.test.ts +136 -0
- package/src/__tests__/scaffold.test.ts +224 -0
- package/src/__tests__/template-base-name.test.ts +26 -0
- package/src/adapter/__tests__/boolean-result.test.ts +106 -0
- package/src/adapter/boolean-result.ts +126 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/mojo-adapter.ts +1931 -0
- package/src/build.ts +37 -0
- package/src/index.ts +8 -0
- package/src/test-render.ts +704 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
package Mojolicious::Plugin::BarefootJS::DevReload;
|
|
2
|
+
use Mojo::Base 'Mojolicious::Plugin', -signatures;
|
|
3
|
+
|
|
4
|
+
=head1 NAME
|
|
5
|
+
|
|
6
|
+
Mojolicious::Plugin::BarefootJS::DevReload - Dev-only browser auto-reload for BarefootJS apps
|
|
7
|
+
|
|
8
|
+
=head1 SYNOPSIS
|
|
9
|
+
|
|
10
|
+
# In your Mojolicious::Lite app (development mode)
|
|
11
|
+
plugin 'BarefootJS::DevReload';
|
|
12
|
+
|
|
13
|
+
# Then in your layout template, before </body>:
|
|
14
|
+
%== bf_dev_snippet
|
|
15
|
+
|
|
16
|
+
=head1 DESCRIPTION
|
|
17
|
+
|
|
18
|
+
Companion to C<barefoot build --watch> in C<@barefootjs/cli>. The CLI drops
|
|
19
|
+
C<< <dist>/.dev/build-id >> after every successful rebuild that changed
|
|
20
|
+
output; this plugin watches that file and streams SSE C<< event: reload >>
|
|
21
|
+
to subscribed browsers so an editor save triggers an automatic reload.
|
|
22
|
+
|
|
23
|
+
Disabled automatically when C<< $app->mode eq 'production' >> (set via
|
|
24
|
+
C<MOJO_MODE=production>). Pass C<< enabled => 0 >> to disable explicitly or
|
|
25
|
+
C<< enabled => 1 >> to force-enable.
|
|
26
|
+
|
|
27
|
+
=cut
|
|
28
|
+
|
|
29
|
+
use Mojo::ByteStream qw(b);
|
|
30
|
+
use Mojo::IOLoop;
|
|
31
|
+
use File::Spec;
|
|
32
|
+
|
|
33
|
+
# Sentinel path contract with @barefootjs/cli (DEV_SENTINEL_SUBDIR /
|
|
34
|
+
# DEV_SENTINEL_FILENAME in packages/cli/src/lib/build.ts). Duplicated so this
|
|
35
|
+
# package avoids a runtime dep on the CLI — keep in sync with the CLI.
|
|
36
|
+
my $DEV_SUBDIR = '.dev';
|
|
37
|
+
my $BUILD_ID_FILE = 'build-id';
|
|
38
|
+
my $SCROLL_STORAGE_KEY = '__bf_devreload_scroll';
|
|
39
|
+
|
|
40
|
+
# Heartbeat < any reasonable proxy/IOLoop idle timeout so a quiet connection
|
|
41
|
+
# doesn't get reaped between rebuilds.
|
|
42
|
+
my $HEARTBEAT_S = 5;
|
|
43
|
+
|
|
44
|
+
# Polling instead of Linux::Inotify2 / Mac::FSEvents keeps the runtime
|
|
45
|
+
# dependency-free. Sub-second latency is imperceptible next to browser reload.
|
|
46
|
+
my $POLL_S = 0.5;
|
|
47
|
+
|
|
48
|
+
sub register ($self, $app, $config = {}) {
|
|
49
|
+
my $dist_dir = $config->{dist_dir} // 'dist';
|
|
50
|
+
my $endpoint = $config->{endpoint} // '/_bf/reload';
|
|
51
|
+
my $enabled = exists $config->{enabled}
|
|
52
|
+
? $config->{enabled}
|
|
53
|
+
: ($app->mode ne 'production');
|
|
54
|
+
|
|
55
|
+
# Snippet helper is always registered so templates don't have to branch
|
|
56
|
+
# on mode — it simply returns an empty ByteStream when disabled.
|
|
57
|
+
$app->helper(bf_dev_snippet => sub ($c) {
|
|
58
|
+
return b('') unless $enabled;
|
|
59
|
+
return b(_snippet($endpoint));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return unless $enabled;
|
|
63
|
+
|
|
64
|
+
# Resolve dist_dir relative to the Mojolicious home when not already
|
|
65
|
+
# absolute, so both `dist_dir => 'dist'` (the common case) and
|
|
66
|
+
# `dist_dir => '/abs/path'` (tests) work.
|
|
67
|
+
my $dist_abs = File::Spec->file_name_is_absolute($dist_dir)
|
|
68
|
+
? $dist_dir
|
|
69
|
+
: $app->home->child($dist_dir)->to_string;
|
|
70
|
+
my $dev_dir = File::Spec->catdir($dist_abs, $DEV_SUBDIR);
|
|
71
|
+
my $build_id_path = File::Spec->catfile($dev_dir, $BUILD_ID_FILE);
|
|
72
|
+
mkdir $dev_dir unless -d $dev_dir;
|
|
73
|
+
|
|
74
|
+
$app->routes->get($endpoint => sub ($c) {
|
|
75
|
+
my $last_event_id = $c->req->headers->header('Last-Event-ID') // '';
|
|
76
|
+
$last_event_id =~ s/^\s+|\s+$//g;
|
|
77
|
+
|
|
78
|
+
$c->res->headers->content_type('text/event-stream');
|
|
79
|
+
$c->res->headers->cache_control('no-cache, no-transform');
|
|
80
|
+
$c->res->headers->connection('keep-alive');
|
|
81
|
+
$c->res->headers->header('X-Accel-Buffering' => 'no');
|
|
82
|
+
|
|
83
|
+
$c->write("retry: 1000\n\n");
|
|
84
|
+
|
|
85
|
+
my $initial_id = _read_build_id($build_id_path);
|
|
86
|
+
my $last_sent = '';
|
|
87
|
+
if (length $initial_id) {
|
|
88
|
+
$last_sent = $initial_id;
|
|
89
|
+
# When the client reconnects with a stale Last-Event-ID, a build
|
|
90
|
+
# happened during its disconnected window — fire `reload`
|
|
91
|
+
# immediately so the missed rebuild does not silently stay
|
|
92
|
+
# unpainted until the next change.
|
|
93
|
+
my $event = (length $last_event_id && $last_event_id ne $initial_id)
|
|
94
|
+
? 'reload' : 'hello';
|
|
95
|
+
$c->write("event: $event\nid: $initial_id\ndata: $initial_id\n\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
my ($hb_id, $poll_id);
|
|
99
|
+
$c->on(finish => sub {
|
|
100
|
+
Mojo::IOLoop->remove($hb_id) if $hb_id;
|
|
101
|
+
Mojo::IOLoop->remove($poll_id) if $poll_id;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
$hb_id = Mojo::IOLoop->recurring($HEARTBEAT_S => sub {
|
|
105
|
+
$c->write(": hb\n\n");
|
|
106
|
+
});
|
|
107
|
+
$poll_id = Mojo::IOLoop->recurring($POLL_S => sub {
|
|
108
|
+
my $id = _read_build_id($build_id_path);
|
|
109
|
+
return unless length $id;
|
|
110
|
+
return if $id eq $last_sent;
|
|
111
|
+
$last_sent = $id;
|
|
112
|
+
$c->write("event: reload\nid: $id\ndata: $id\n\n");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
sub _read_build_id ($path) {
|
|
120
|
+
return '' unless -f $path;
|
|
121
|
+
open my $fh, '<', $path or return '';
|
|
122
|
+
local $/;
|
|
123
|
+
my $content = <$fh>;
|
|
124
|
+
close $fh;
|
|
125
|
+
$content //= '';
|
|
126
|
+
$content =~ s/^\s+|\s+$//g;
|
|
127
|
+
return $content;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
sub _snippet ($endpoint) {
|
|
131
|
+
my $ep = _js_str($endpoint);
|
|
132
|
+
my $sk = _js_str($SCROLL_STORAGE_KEY);
|
|
133
|
+
# Small IIFE: EventSource subscriber + scrollY preservation. Idempotent
|
|
134
|
+
# across duplicate mounts (window.__bfDevReload guard).
|
|
135
|
+
return qq{<script>(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;try{var s=sessionStorage.getItem($sk);if(s){sessionStorage.removeItem($sk);var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};if(document.readyState==='loading'){addEventListener('DOMContentLoaded',restore,{once:true})}else{restore()}}}}catch(e){}var es=new EventSource($ep);es.addEventListener('reload',function(){try{sessionStorage.setItem($sk,String(window.scrollY))}catch(e){}location.reload()});es.addEventListener('error',function(){})})();</script>};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
sub _js_str ($s) {
|
|
139
|
+
# Minimal JS string escape for the handful of characters that can appear
|
|
140
|
+
# in a URL path or storage key. Good enough for package-internal + trusted
|
|
141
|
+
# operator-supplied strings; never interpolate untrusted input here.
|
|
142
|
+
my $t = $s;
|
|
143
|
+
$t =~ s/\\/\\\\/g;
|
|
144
|
+
$t =~ s/"/\\"/g;
|
|
145
|
+
$t =~ s/\n/\\n/g;
|
|
146
|
+
$t =~ s/\r/\\r/g;
|
|
147
|
+
return qq{"$t"};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
1;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package Mojolicious::Plugin::BarefootJS;
|
|
2
|
+
use Mojo::Base 'Mojolicious::Plugin', -signatures;
|
|
3
|
+
|
|
4
|
+
use Mojo::File qw(path);
|
|
5
|
+
use Mojo::JSON qw(decode_json);
|
|
6
|
+
|
|
7
|
+
use BarefootJS;
|
|
8
|
+
|
|
9
|
+
# Plugin entry point. Wires up:
|
|
10
|
+
#
|
|
11
|
+
# 1. The `bf` controller helper. Lazily instantiates one
|
|
12
|
+
# BarefootJS object per request and stashes it under
|
|
13
|
+
# `bf.instance`.
|
|
14
|
+
#
|
|
15
|
+
# 2. A `before_render` hook that, when the rendered template name
|
|
16
|
+
# matches a top-level component in the build manifest, fills the
|
|
17
|
+
# heavy boilerplate the user previously hand-rolled in `app.pl`:
|
|
18
|
+
# generates the scope id, registers every UI-registry child
|
|
19
|
+
# renderer from the manifest, and seeds the stash with each
|
|
20
|
+
# template variable's static default (issue #1416).
|
|
21
|
+
#
|
|
22
|
+
# Configuration (all optional):
|
|
23
|
+
# - manifest_path: absolute path to the `bf build`-emitted
|
|
24
|
+
# `manifest.json`. Defaults to `<app->home>/dist/templates/manifest.json`.
|
|
25
|
+
# Pass `undef` to disable manifest-driven auto-init entirely; the
|
|
26
|
+
# bf helper is still installed and callers can drive everything
|
|
27
|
+
# manually as before.
|
|
28
|
+
sub register ($self, $app, $config = {}) {
|
|
29
|
+
$app->helper(bf => sub ($c) {
|
|
30
|
+
$c->stash->{'bf.instance'} //= BarefootJS->new($c, $config);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
my $manifest = _load_manifest($app, $config);
|
|
34
|
+
return unless $manifest;
|
|
35
|
+
|
|
36
|
+
# Cache the set of UI-registry slot keys so we can answer
|
|
37
|
+
# "is this template name a child or a top-level page?" with a
|
|
38
|
+
# single hash lookup at render time. Top-level entries are
|
|
39
|
+
# everything that isn't `__barefoot__` and doesn't match
|
|
40
|
+
# `ui/<name>/index` — the same partition `register_components_from_manifest`
|
|
41
|
+
# applies internally.
|
|
42
|
+
my %is_child_entry;
|
|
43
|
+
for my $entry_name (keys %$manifest) {
|
|
44
|
+
next if $entry_name eq '__barefoot__';
|
|
45
|
+
next unless $entry_name =~ m{^ui/[^/]+/index$};
|
|
46
|
+
$is_child_entry{$entry_name} = 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$app->hook(before_render => sub ($c, $args) {
|
|
50
|
+
my $template = $args->{template};
|
|
51
|
+
return unless defined $template && length $template;
|
|
52
|
+
my $entry = $manifest->{$template};
|
|
53
|
+
return unless $entry;
|
|
54
|
+
return if $is_child_entry{$template};
|
|
55
|
+
# Idempotency guard for nested renders. A controller might
|
|
56
|
+
# call `render_to_string` inside an action and then `render`
|
|
57
|
+
# — without this we'd re-init `bf` on the second pass and
|
|
58
|
+
# wipe the script registrations the first pass collected.
|
|
59
|
+
return if $c->stash->{'bf.auto_init_done'};
|
|
60
|
+
|
|
61
|
+
# Escape hatch for callers that wire `bf` up by hand (the
|
|
62
|
+
# existing `render_component` helper in the showcase app does
|
|
63
|
+
# this). If `_scope_id` is already set we treat the request as
|
|
64
|
+
# "manually managed" and leave it alone — same outcome as
|
|
65
|
+
# before the plugin gained auto-init.
|
|
66
|
+
my $bf = $c->bf;
|
|
67
|
+
if (defined $bf->_scope_id && length $bf->_scope_id) {
|
|
68
|
+
$c->stash->{'bf.auto_init_done'} = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
$c->stash->{'bf.auto_init_done'} = 1;
|
|
72
|
+
|
|
73
|
+
$bf->_scope_id($template . '_' . substr(rand() =~ s/^0\.//r, 0, 6));
|
|
74
|
+
$bf->register_components_from_manifest($manifest);
|
|
75
|
+
|
|
76
|
+
# Seed each ssrDefault into the stash unless the caller has
|
|
77
|
+
# already supplied a value for that key — callers always win.
|
|
78
|
+
my $defaults = $entry->{ssrDefaults};
|
|
79
|
+
if (ref($defaults) eq 'HASH') {
|
|
80
|
+
for my $name (keys %$defaults) {
|
|
81
|
+
next if exists $c->stash->{$name};
|
|
82
|
+
my $d = $defaults->{$name};
|
|
83
|
+
my $value = ref($d) eq 'HASH' ? $d->{value} : $d;
|
|
84
|
+
$c->stash->{$name} = $value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
sub _load_manifest ($app, $config) {
|
|
91
|
+
return undef if exists $config->{manifest_path} && !defined $config->{manifest_path};
|
|
92
|
+
my $manifest_path = $config->{manifest_path}
|
|
93
|
+
// $app->home->child('dist/templates/manifest.json');
|
|
94
|
+
my $file = path($manifest_path);
|
|
95
|
+
return undef unless -r $file;
|
|
96
|
+
my $manifest = eval { decode_json($file->slurp) };
|
|
97
|
+
if ($@ || ref($manifest) ne 'HASH') {
|
|
98
|
+
$app->log->warn("BarefootJS: cannot parse manifest at $file: $@") if $@;
|
|
99
|
+
return undef;
|
|
100
|
+
}
|
|
101
|
+
return $manifest;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
1;
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@barefootjs/mojolicious",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mojolicious EP template adapter for BarefootJS - generates .html.ep files from IR",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./adapter": {
|
|
14
|
+
"types": "./dist/adapter/index.d.ts",
|
|
15
|
+
"import": "./dist/adapter/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./test-render": {
|
|
18
|
+
"bun": "./src/test-render.ts"
|
|
19
|
+
},
|
|
20
|
+
"./build": {
|
|
21
|
+
"types": "./dist/build.d.ts",
|
|
22
|
+
"import": "./dist/build.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"lib"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "bun run build:js && bun run build:types",
|
|
32
|
+
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared",
|
|
33
|
+
"build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
|
|
34
|
+
"test": "bun test",
|
|
35
|
+
"clean": "rm -rf dist",
|
|
36
|
+
"prepack": "node ../../scripts/swap-publish-config.mjs pack",
|
|
37
|
+
"postpack": "node ../../scripts/swap-publish-config.mjs unpack"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"mojolicious",
|
|
41
|
+
"mojo",
|
|
42
|
+
"perl",
|
|
43
|
+
"template",
|
|
44
|
+
"barefoot",
|
|
45
|
+
"ssr"
|
|
46
|
+
],
|
|
47
|
+
"author": "kobaken <kentafly88@gmail.com>",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "https://github.com/piconic-ai/barefootjs",
|
|
52
|
+
"directory": "packages/adapter-mojolicious"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@barefootjs/shared": "workspace:*"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@barefootjs/jsx": "workspace:*"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@barefootjs/adapter-tests": "workspace:*",
|
|
62
|
+
"@barefootjs/jsx": "workspace:*",
|
|
63
|
+
"typescript": "^5.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|