proscenium 0.26.0-x64-mingw-ucrt
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.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +1020 -0
- data/exe/proscenium +19 -0
- data/exe/psm +5 -0
- data/lib/generators/proscenium/bun/bun_generator.rb +150 -0
- data/lib/generators/proscenium/bun/templates/proscenium.preload.js +61 -0
- data/lib/proscenium/app_package.rb +90 -0
- data/lib/proscenium/builder.rb +403 -0
- data/lib/proscenium/bundled_gems.rb +140 -0
- data/lib/proscenium/cli/brackets.rb +66 -0
- data/lib/proscenium/cli/bun_linker.rb +79 -0
- data/lib/proscenium/cli/capabilities.json +29 -0
- data/lib/proscenium/cli/collisions.rb +94 -0
- data/lib/proscenium/cli/contexts.rb +133 -0
- data/lib/proscenium/cli/diff.rb +80 -0
- data/lib/proscenium/cli/error.rb +291 -0
- data/lib/proscenium/cli/gem_archive.rb +44 -0
- data/lib/proscenium/cli/gem_check.rb +150 -0
- data/lib/proscenium/cli/gitignore.rb +63 -0
- data/lib/proscenium/cli/inspect.rb +257 -0
- data/lib/proscenium/cli/install.rb +378 -0
- data/lib/proscenium/cli/manager.rb +323 -0
- data/lib/proscenium/cli/platform_variants.rb +82 -0
- data/lib/proscenium/cli/pnpm_approvals.rb +109 -0
- data/lib/proscenium/cli/pnpm_workspace.rb +149 -0
- data/lib/proscenium/cli/project_lock.rb +106 -0
- data/lib/proscenium/cli/registration.rb +140 -0
- data/lib/proscenium/cli/reporter.rb +103 -0
- data/lib/proscenium/cli/rules.rb +169 -0
- data/lib/proscenium/cli/runner.rb +142 -0
- data/lib/proscenium/cli/verify.rb +107 -0
- data/lib/proscenium/cli.rb +122 -0
- data/lib/proscenium/context_map.rb +181 -0
- data/lib/proscenium/css_module/path.rb +32 -0
- data/lib/proscenium/css_module/transformer.rb +118 -0
- data/lib/proscenium/css_module.rb +63 -0
- data/lib/proscenium/dependency_context.rb +74 -0
- data/lib/proscenium/ensure_loaded.rb +31 -0
- data/lib/proscenium/error.rb +10 -0
- data/lib/proscenium/ext/proscenium.dll +0 -0
- data/lib/proscenium/ext/proscenium.h +113 -0
- data/lib/proscenium/helper.rb +82 -0
- data/lib/proscenium/importer.rb +189 -0
- data/lib/proscenium/log_subscriber.rb +38 -0
- data/lib/proscenium/manifest.rb +48 -0
- data/lib/proscenium/mapping_generation.rb +64 -0
- data/lib/proscenium/middleware/base.rb +102 -0
- data/lib/proscenium/middleware/chunks.rb +46 -0
- data/lib/proscenium/middleware/esbuild.rb +11 -0
- data/lib/proscenium/middleware/ruby_gems.rb +37 -0
- data/lib/proscenium/middleware/silence_request.rb +31 -0
- data/lib/proscenium/middleware/vendor.rb +31 -0
- data/lib/proscenium/middleware.rb +67 -0
- data/lib/proscenium/monkey.rb +77 -0
- data/lib/proscenium/railtie.rb +106 -0
- data/lib/proscenium/railties/assets.rake +19 -0
- data/lib/proscenium/resolver.rb +67 -0
- data/lib/proscenium/runtime/bootstrap.js +266 -0
- data/lib/proscenium/runtime/bun.js +143 -0
- data/lib/proscenium/runtime/server.rb +702 -0
- data/lib/proscenium/side_load.rb +210 -0
- data/lib/proscenium/source_path.rb +15 -0
- data/lib/proscenium/stale_contexts.rb +118 -0
- data/lib/proscenium/templates/rescues/build_error.html.erb +32 -0
- data/lib/proscenium/utils.rb +104 -0
- data/lib/proscenium/version.rb +5 -0
- data/lib/proscenium/yaml_tree.rb +48 -0
- data/lib/proscenium.rb +87 -0
- metadata +148 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Proscenium
|
|
7
|
+
# A participating gem's dependency context (#154): its package.json through the
|
|
8
|
+
# `dependency-context-v1` projection, which keeps installation metadata only and is committed at
|
|
9
|
+
# `.proscenium/packages/<gem>/package.json`.
|
|
10
|
+
#
|
|
11
|
+
# The one owner of the projection and its hash, shared by the CLI and the engine, so it must not
|
|
12
|
+
# load ActiveSupport or anything else the CLI may not.
|
|
13
|
+
module DependencyContext
|
|
14
|
+
PROJECTION = 'dependency-context-v1'
|
|
15
|
+
|
|
16
|
+
# The manifest fields a context keeps, in the order it writes them. Everything else (scripts,
|
|
17
|
+
# devDependencies, version, entry points, workspaces) is left in the gem's own package.json.
|
|
18
|
+
FIELDS = %w[dependencies peerDependencies peerDependenciesMeta optionalDependencies engines os
|
|
19
|
+
cpu libc].freeze
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def name_for(gem) = "@rubygems/#{gem}"
|
|
24
|
+
|
|
25
|
+
def description_for(gem)
|
|
26
|
+
"Generated by Proscenium from the #{gem} gem. Do not edit; " \
|
|
27
|
+
'run bundle exec proscenium install.'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The context for `gem` from its parsed `manifest`. An empty field is left out, as an absent
|
|
31
|
+
# one is: the two install the same, so they hash the same.
|
|
32
|
+
def project(gem, manifest)
|
|
33
|
+
fields = FIELDS.each_with_object({}) do |key, kept|
|
|
34
|
+
value = manifest[key]
|
|
35
|
+
kept[key] = value unless value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
{ 'name' => name_for(gem), 'private' => true, 'description' => description_for(gem),
|
|
39
|
+
**fields,
|
|
40
|
+
'proscenium' => { 'projection' => PROJECTION, 'projectionSha256' => digest(gem, fields) } }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The bytes committed for a context: fixed key order, two-space indent, a trailing newline, and
|
|
44
|
+
# nothing that varies by host or time.
|
|
45
|
+
def to_json(context) = "#{JSON.pretty_generate(context)}\n"
|
|
46
|
+
|
|
47
|
+
# The projection hash. It hashes a fixed-field encoding of the projected fields, not JSON, so
|
|
48
|
+
# it does not move with the json gem's output, key order, whitespace or escaping, and an edit
|
|
49
|
+
# to anything the projection drops leaves it alone.
|
|
50
|
+
def digest(gem, fields)
|
|
51
|
+
Digest::SHA256.hexdigest("#{PROJECTION}\n#{encode(name_for(gem))}#{encode(fields)}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A self-delimiting encoding: a type tag, a length where there is one, then the value. Hash
|
|
55
|
+
# keys are sorted by their bytes, so key order in the manifest does not matter; array order
|
|
56
|
+
# does, as it does to npm.
|
|
57
|
+
def encode(value)
|
|
58
|
+
case value
|
|
59
|
+
when String then "s#{value.bytesize}:#{value.b}"
|
|
60
|
+
when true then 't'
|
|
61
|
+
when false then 'f'
|
|
62
|
+
when nil then 'n'
|
|
63
|
+
when Integer then "i#{value};"
|
|
64
|
+
when Float then "d#{value};"
|
|
65
|
+
when Array then "a#{value.size}:#{value.map { encode(it) }.join}"
|
|
66
|
+
when Hash
|
|
67
|
+
pairs = value.sort_by { |key, _| key.to_s.b }
|
|
68
|
+
"h#{value.size}:#{pairs.map { |key, val| encode(key.to_s) + encode(val) }.join}"
|
|
69
|
+
else
|
|
70
|
+
raise ArgumentError, "cannot encode #{value.class} in a dependency context"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
NotIncludedError = Class.new(Error) # rubocop:disable Style/EmptyClassDefinition
|
|
5
|
+
|
|
6
|
+
module EnsureLoaded
|
|
7
|
+
def self.included(child)
|
|
8
|
+
child.class_eval do
|
|
9
|
+
append_after_action do
|
|
10
|
+
if request.format.html? && !response.redirect? && Importer.imported?
|
|
11
|
+
msg = <<-TEXT.squish
|
|
12
|
+
There are side loaded and imported assets to be included, but they have not been
|
|
13
|
+
included in the page. Did you forget to add the `#include_assets` helper in your
|
|
14
|
+
views? These assets were imported but not included:
|
|
15
|
+
#{Importer.imported.keys.to_sentence}
|
|
16
|
+
TEXT
|
|
17
|
+
|
|
18
|
+
if Proscenium.config.ensure_loaded == :log
|
|
19
|
+
Rails.logger.warn do
|
|
20
|
+
"#{ActiveSupport::LogSubscriber.new.send(:color, ' [Proscenium]', nil,
|
|
21
|
+
bold: true)} #{msg}"
|
|
22
|
+
end
|
|
23
|
+
elsif Proscenium.config.ensure_loaded == :raise
|
|
24
|
+
raise NotIncludedError, msg
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
# The base of every error Proscenium raises.
|
|
5
|
+
#
|
|
6
|
+
# In a file of its own so that lib/proscenium/builder.rb can load without the rest of the gem:
|
|
7
|
+
# the release workflow loads the compiled library from an installed gem to prove it works, with
|
|
8
|
+
# no Rails app and no Rails, and builder.rb subclasses this at class-definition time.
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* Code generated by cmd/cgo; DO NOT EDIT. */
|
|
2
|
+
|
|
3
|
+
/* package command-line-arguments */
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
#line 1 "cgo-builtin-export-prolog"
|
|
7
|
+
|
|
8
|
+
#include <stddef.h>
|
|
9
|
+
|
|
10
|
+
#ifndef GO_CGO_EXPORT_PROLOGUE_H
|
|
11
|
+
#define GO_CGO_EXPORT_PROLOGUE_H
|
|
12
|
+
|
|
13
|
+
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
|
14
|
+
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
|
|
15
|
+
extern size_t _GoStringLen(_GoString_ s);
|
|
16
|
+
extern const char *_GoStringPtr(_GoString_ s);
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
#endif
|
|
20
|
+
|
|
21
|
+
/* Start of preamble from import "C" comments. */
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
#line 3 "main.go"
|
|
25
|
+
|
|
26
|
+
#include <stdlib.h>
|
|
27
|
+
|
|
28
|
+
struct Result {
|
|
29
|
+
int success;
|
|
30
|
+
char* response;
|
|
31
|
+
char* contentHash;
|
|
32
|
+
};
|
|
33
|
+
struct ResolveResult {
|
|
34
|
+
int success;
|
|
35
|
+
char* urlPath;
|
|
36
|
+
char* absPath;
|
|
37
|
+
};
|
|
38
|
+
struct CompileResult {
|
|
39
|
+
int success;
|
|
40
|
+
char* messages;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
#line 1 "cgo-generated-wrapper"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
/* End of preamble from import "C" comments. */
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/* Start of boilerplate cgo prologue. */
|
|
50
|
+
#line 1 "cgo-gcc-export-header-prolog"
|
|
51
|
+
|
|
52
|
+
#ifndef GO_CGO_PROLOGUE_H
|
|
53
|
+
#define GO_CGO_PROLOGUE_H
|
|
54
|
+
|
|
55
|
+
typedef signed char GoInt8;
|
|
56
|
+
typedef unsigned char GoUint8;
|
|
57
|
+
typedef short GoInt16;
|
|
58
|
+
typedef unsigned short GoUint16;
|
|
59
|
+
typedef int GoInt32;
|
|
60
|
+
typedef unsigned int GoUint32;
|
|
61
|
+
typedef long long GoInt64;
|
|
62
|
+
typedef unsigned long long GoUint64;
|
|
63
|
+
typedef GoInt64 GoInt;
|
|
64
|
+
typedef GoUint64 GoUint;
|
|
65
|
+
typedef size_t GoUintptr;
|
|
66
|
+
typedef float GoFloat32;
|
|
67
|
+
typedef double GoFloat64;
|
|
68
|
+
#ifdef _MSC_VER
|
|
69
|
+
#if !defined(__cplusplus) || _MSVC_LANG <= 201402L
|
|
70
|
+
#include <complex.h>
|
|
71
|
+
typedef _Fcomplex GoComplex64;
|
|
72
|
+
typedef _Dcomplex GoComplex128;
|
|
73
|
+
#else
|
|
74
|
+
#include <complex>
|
|
75
|
+
typedef std::complex<float> GoComplex64;
|
|
76
|
+
typedef std::complex<double> GoComplex128;
|
|
77
|
+
#endif
|
|
78
|
+
#else
|
|
79
|
+
typedef float _Complex GoComplex64;
|
|
80
|
+
typedef double _Complex GoComplex128;
|
|
81
|
+
#endif
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
static assertion to make sure the file is being used on architecture
|
|
85
|
+
at least with matching size of GoInt.
|
|
86
|
+
*/
|
|
87
|
+
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
|
|
88
|
+
|
|
89
|
+
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
|
90
|
+
typedef _GoString_ GoString;
|
|
91
|
+
#endif
|
|
92
|
+
typedef void *GoMap;
|
|
93
|
+
typedef void *GoChan;
|
|
94
|
+
typedef struct { void *t; void *v; } GoInterface;
|
|
95
|
+
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
|
|
96
|
+
|
|
97
|
+
#endif
|
|
98
|
+
|
|
99
|
+
/* End of boilerplate cgo prologue. */
|
|
100
|
+
|
|
101
|
+
#ifdef __cplusplus
|
|
102
|
+
extern "C" {
|
|
103
|
+
#endif
|
|
104
|
+
|
|
105
|
+
extern void reset_config(void);
|
|
106
|
+
extern void free_cstr(char* ptr);
|
|
107
|
+
extern struct Result build_to_string(char* filePath, char* configJson);
|
|
108
|
+
extern struct ResolveResult resolve(char* filePath, char* configJson);
|
|
109
|
+
extern struct CompileResult compile(char* configJson);
|
|
110
|
+
|
|
111
|
+
#ifdef __cplusplus
|
|
112
|
+
}
|
|
113
|
+
#endif
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
module Helper
|
|
5
|
+
# Sets the side load options of the template being rendered, for this render only. They are
|
|
6
|
+
# kept on the view, which lives for one request, keyed by template, because the template
|
|
7
|
+
# object itself is cached and shared by every request. `SideLoad.sideload_templates` scopes
|
|
8
|
+
# each value to the render that set it.
|
|
9
|
+
#
|
|
10
|
+
# Call it outside any `cache` block: a cache hit skips the block, and this call with it.
|
|
11
|
+
def sideload_assets(value)
|
|
12
|
+
# ActionView does not push a partial rendered with a block onto `@current_template`, so while
|
|
13
|
+
# its body renders, `@current_template` is still the calling template.
|
|
14
|
+
identifier = (@proscenium_block_partial || @current_template).identifier
|
|
15
|
+
|
|
16
|
+
if value.nil?
|
|
17
|
+
proscenium_sideload_assets_options.delete identifier
|
|
18
|
+
else
|
|
19
|
+
proscenium_sideload_assets_options[identifier] = value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Renders a partial given `block`, and yields the block to render it with. While the partial's
|
|
24
|
+
# body renders, `sideload_assets` applies to the partial. While the block runs, wherever it is
|
|
25
|
+
# run from, it applies to the template that passed the block. Any template rendered from
|
|
26
|
+
# either pushes itself onto `@current_template`, and `Monkey::Base` clears this while it does.
|
|
27
|
+
def proscenium_render_block_partial(partial, block)
|
|
28
|
+
previous = @proscenium_block_partial
|
|
29
|
+
calling_template = previous || @current_template
|
|
30
|
+
@proscenium_block_partial = partial
|
|
31
|
+
|
|
32
|
+
yield(proc do |*args|
|
|
33
|
+
current = @proscenium_block_partial
|
|
34
|
+
@proscenium_block_partial = calling_template
|
|
35
|
+
block.call(*args)
|
|
36
|
+
ensure
|
|
37
|
+
@proscenium_block_partial = current
|
|
38
|
+
end)
|
|
39
|
+
ensure
|
|
40
|
+
@proscenium_block_partial = previous
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Hash] the `sideload_assets` value of each template in this render, by identifier.
|
|
44
|
+
def proscenium_sideload_assets_options
|
|
45
|
+
@proscenium_sideload_assets_options ||= {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def compute_asset_path(path, options = {})
|
|
49
|
+
if %i[javascript stylesheet].include?(options[:type])
|
|
50
|
+
return Proscenium::Manifest[path] || "/#{path}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Accepts one or more CSS class names, and transforms them into CSS module names.
|
|
57
|
+
#
|
|
58
|
+
# @see CssModule::Transformer#class_names
|
|
59
|
+
# @param names [String,Symbol,nil,false,Array<String,Symbol,nil,false>]
|
|
60
|
+
# @param path [Pathname] the path to the CSS module file to use for the transformation.
|
|
61
|
+
# @return [String] the transformed CSS module names concatenated as a string.
|
|
62
|
+
def css_module(*names, path: nil)
|
|
63
|
+
path ||= Pathname.new(@lookup_context.find(@virtual_path).identifier).sub_ext('')
|
|
64
|
+
CssModule::Transformer.new(path).class_names(*names, require_prefix: false).join(' ')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def include_assets
|
|
68
|
+
include_stylesheets + include_javascripts
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def include_stylesheets
|
|
72
|
+
SideLoad::CSS_COMMENT.html_safe
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Includes all javascripts that have been imported and side loaded.
|
|
76
|
+
#
|
|
77
|
+
# @return [String] the HTML tags for the javascripts.
|
|
78
|
+
def include_javascripts
|
|
79
|
+
SideLoad::JS_COMMENT.html_safe
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/current_attributes'
|
|
4
|
+
require 'concurrent/map'
|
|
5
|
+
|
|
6
|
+
module Proscenium
|
|
7
|
+
class Importer < ActiveSupport::CurrentAttributes
|
|
8
|
+
JS_EXTENSIONS = %w[.tsx .ts .jsx .js].freeze
|
|
9
|
+
CSS_EXTENSIONS = %w[.module.css .css].freeze
|
|
10
|
+
|
|
11
|
+
# The readable suffix of a CSS module class name, by absolute path - or by URL, for a remote
|
|
12
|
+
# module, which has no file. It is a pure function of that identity, but `import` runs once per
|
|
13
|
+
# class name a view emits and `relative_path_from` is most of its cost, so it is built once per
|
|
14
|
+
# file for the life of the process.
|
|
15
|
+
SUFFIXES = Concurrent::Map.new
|
|
16
|
+
|
|
17
|
+
# Holds the JS and CSS files to include in the current request.
|
|
18
|
+
#
|
|
19
|
+
# Example:
|
|
20
|
+
# {
|
|
21
|
+
# '/path/to/input/file.js': {
|
|
22
|
+
# output: '/path/to/compiled/file.js',
|
|
23
|
+
# **options
|
|
24
|
+
# }
|
|
25
|
+
# }
|
|
26
|
+
attribute :imported
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Import the given `filepath`. This is idempotent - it will never include duplicates.
|
|
30
|
+
#
|
|
31
|
+
# @param filepath [String] Absolute URL path (relative to Rails root) of the file to import,
|
|
32
|
+
# or a URL, which has no file on disk. Should be the actual asset file, eg. app.css,
|
|
33
|
+
# some/component.js.
|
|
34
|
+
# @return [String|nil] the digest of the imported file path if a css module (*.module.css).
|
|
35
|
+
def import(filepath = nil, sideloaded: false, **)
|
|
36
|
+
self.imported ||= {}
|
|
37
|
+
|
|
38
|
+
if filepath.end_with?('.module.css')
|
|
39
|
+
manifest_path, non_manifest_path, abs_path = Resolver.resolve(filepath, as_array: true)
|
|
40
|
+
filepath = Array(manifest_path || non_manifest_path)[0]
|
|
41
|
+
|
|
42
|
+
# A URL has no file on disk, so the URL is the module's identity. Taken after the
|
|
43
|
+
# reassignment above, so the digest and the suffix cache below agree on it. An entry the
|
|
44
|
+
# stylesheet got as a plain file has no digest of its own.
|
|
45
|
+
digest = imported.dig(filepath, :digest) ||
|
|
46
|
+
Utils.css_module_digest(abs_path.presence || filepath)
|
|
47
|
+
store(filepath, sideloaded, **, digest:)
|
|
48
|
+
|
|
49
|
+
transformed_path = ''
|
|
50
|
+
# Mirrors ConfigT#ShouldMinify - the suffix exists whenever identifiers are not
|
|
51
|
+
# minified, and a class name the stylesheet does not define is worse than a long one.
|
|
52
|
+
if Proscenium.config.debug || !Rails.env.production?
|
|
53
|
+
# Keyed and built from the file's path under the app, or from the URL when there is
|
|
54
|
+
# no file: `Pathname.new('').relative_path_from(Rails.root)` raises.
|
|
55
|
+
identity = abs_path.presence || filepath
|
|
56
|
+
transformed_path = SUFFIXES.compute_if_absent(identity) do
|
|
57
|
+
relative = if abs_path.present?
|
|
58
|
+
Utils.css_module_relative_path(abs_path)
|
|
59
|
+
else
|
|
60
|
+
identity
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
"_#{Utils.css_module_suffix(relative)}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
"#{digest}#{transformed_path}"
|
|
68
|
+
else
|
|
69
|
+
Array(Resolver.resolve(filepath)).each { |fp| store(fp, sideloaded, **) }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Sideloads JS and CSS assets for the given Ruby filepath.
|
|
74
|
+
#
|
|
75
|
+
# Any files with the same base name and matching a supported extension will be sideloaded.
|
|
76
|
+
# Only one JS and one CSS file will be sideloaded, with the first match used in the following
|
|
77
|
+
# order:
|
|
78
|
+
# - JS extensions: .tsx, .ts, .jsx, and .js.
|
|
79
|
+
# - CSS extensions: .css.module, and .css.
|
|
80
|
+
#
|
|
81
|
+
# Example:
|
|
82
|
+
# - `app/views/layouts/application.rb`
|
|
83
|
+
# - `app/views/layouts/application.css`
|
|
84
|
+
# - `app/views/layouts/application.js`
|
|
85
|
+
# - `app/views/layouts/application.tsx`
|
|
86
|
+
#
|
|
87
|
+
# A request to sideload `app/views/layouts/application.rb` will result in `application.css`
|
|
88
|
+
# and `application.tsx` being sideloaded. `application.js` will not be sideloaded because the
|
|
89
|
+
# `.tsx` extension is matched first.
|
|
90
|
+
#
|
|
91
|
+
# @param filepath [Pathname] Absolute file system path of the Ruby file to sideload.
|
|
92
|
+
# @param options [Hash] Options to pass to `import`.
|
|
93
|
+
def sideload(filepath, **options)
|
|
94
|
+
return if !Proscenium.config.side_load || (options[:js] == false && options[:css] == false)
|
|
95
|
+
|
|
96
|
+
sideload_js(filepath, **options) unless options[:js] == false
|
|
97
|
+
sideload_css(filepath, **options) unless options[:css] == false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def sideload_js(filepath, **)
|
|
101
|
+
_sideload(filepath, JS_EXTENSIONS, **)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def sideload_css(filepath, **)
|
|
105
|
+
_sideload(filepath, ['.css'], **)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def sideload_css_module(filepath, **)
|
|
109
|
+
_sideload(filepath, ['.module.css'], **)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @param filepath [Pathname] Absolute file system path of the Ruby file to sideload.
|
|
113
|
+
# @param extensions [Array<String>] Supported file extensions to sideload.
|
|
114
|
+
# @raise [ArgumentError] if `filepath` is not an absolute file system path.
|
|
115
|
+
private def _sideload(filepath, extensions, **) # rubocop:disable Style/AccessModifierDeclarations
|
|
116
|
+
return unless Proscenium.config.side_load
|
|
117
|
+
|
|
118
|
+
if !filepath.is_a?(Pathname) || !filepath.absolute?
|
|
119
|
+
raise ArgumentError, "`filepath` (#{filepath}) must be a `Pathname`, and an absolute path"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Ensures extensions with more than one dot are handled correctly.
|
|
123
|
+
filepath = filepath.sub_ext('').sub_ext('')
|
|
124
|
+
|
|
125
|
+
extensions.find do |x|
|
|
126
|
+
next unless (fp = filepath.sub_ext(x)).exist?
|
|
127
|
+
|
|
128
|
+
import(fp.to_s, sideloaded: filepath, **)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def each_stylesheet(delete: false)
|
|
133
|
+
return if imported.blank?
|
|
134
|
+
|
|
135
|
+
blk = proc do |key, options|
|
|
136
|
+
if key.end_with?(*CSS_EXTENSIONS)
|
|
137
|
+
yield(key, options)
|
|
138
|
+
true
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
delete ? imported.delete_if(&blk) : imported.each(&blk)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def each_javascript(delete: false)
|
|
146
|
+
return if imported.blank?
|
|
147
|
+
|
|
148
|
+
blk = proc do |key, options|
|
|
149
|
+
if key.end_with?(*JS_EXTENSIONS)
|
|
150
|
+
yield(key, options)
|
|
151
|
+
true
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
delete ? imported.delete_if(&blk) : imported.each(&blk)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def css_imported?
|
|
158
|
+
imported&.keys&.any? { |x| x.end_with?(*CSS_EXTENSIONS) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def js_imported?
|
|
162
|
+
imported&.keys&.any? { |x| x.end_with?(*JS_EXTENSIONS) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def imported?(filepath = nil)
|
|
166
|
+
filepath ? imported&.key?(filepath) : !imported.blank?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
# Keyed by the resolved path, never the path given: a sideload passes an absolute file
|
|
172
|
+
# system path. A repeat import merges its options in, later ones winning key by key, and does
|
|
173
|
+
# not notify again; proscenium-ui's Select sideloads itself `lazy: true` over an eager
|
|
174
|
+
# sideload of the same file.
|
|
175
|
+
def store(key, sideloaded, **options)
|
|
176
|
+
if (existing = imported[key])
|
|
177
|
+
existing.merge!(options)
|
|
178
|
+
elsif sideloaded
|
|
179
|
+
ActiveSupport::Notifications.instrument 'sideload.proscenium', identifier: key,
|
|
180
|
+
sideloaded: do
|
|
181
|
+
imported[key] = options
|
|
182
|
+
end
|
|
183
|
+
else
|
|
184
|
+
imported[key] = options
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/log_subscriber'
|
|
4
|
+
|
|
5
|
+
module Proscenium
|
|
6
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
7
|
+
def sideload(event)
|
|
8
|
+
path = event.payload[:identifier]
|
|
9
|
+
sideloaded = event.payload[:sideloaded]
|
|
10
|
+
sideloaded = sideloaded.relative_path_from(Rails.root) if sideloaded.is_a?(Pathname)
|
|
11
|
+
|
|
12
|
+
info do
|
|
13
|
+
msg = " #{color('[Proscenium]', nil, bold: true)} Sideloading #{path}"
|
|
14
|
+
sideloaded.is_a?(Pathname) ? msg << " from #{sideloaded}" : msg
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def build(event)
|
|
19
|
+
path = event.payload[:identifier]
|
|
20
|
+
path = CGI.unescape(path) if path.start_with?(/https?%3A%2F%2F/)
|
|
21
|
+
|
|
22
|
+
info do
|
|
23
|
+
message = " #{color('[Proscenium]', nil, bold: true)} Building /#{path}"
|
|
24
|
+
message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resolve(event)
|
|
29
|
+
path = event.payload[:identifier]
|
|
30
|
+
path = CGI.unescape(path) if path.start_with?(/https?%3A%2F%2F/)
|
|
31
|
+
|
|
32
|
+
info do
|
|
33
|
+
message = " #{color('[Proscenium]', nil, bold: true)} Resolving #{path}"
|
|
34
|
+
message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
module Manifest
|
|
5
|
+
mattr_accessor :manifest, default: {}
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Built aside and assigned once, so a resolve during a load never sees a half-built manifest,
|
|
10
|
+
# and a manifest that fails to parse leaves the previous one in place.
|
|
11
|
+
def load!
|
|
12
|
+
public_path = Rails.configuration.paths['public'].first
|
|
13
|
+
entries = {}
|
|
14
|
+
|
|
15
|
+
if Proscenium.config.manifest_path.exist?
|
|
16
|
+
JSON.parse(Proscenium.config.manifest_path.read)['outputs'].each do |outpath, details|
|
|
17
|
+
next if !details.key?('entryPoint')
|
|
18
|
+
|
|
19
|
+
outpath = fs_path(outpath).delete_prefix "#{public_path}/"
|
|
20
|
+
|
|
21
|
+
ep = fs_path(details['entryPoint'])
|
|
22
|
+
ep = BundledGems.virtual_path(ep) || ep.delete_prefix(Rails.root.to_s)
|
|
23
|
+
|
|
24
|
+
entries[ep] = [
|
|
25
|
+
"/#{outpath}",
|
|
26
|
+
details['cssBundle'] && fs_path(details['cssBundle']).delete_prefix(public_path)
|
|
27
|
+
].compact
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
self.manifest = entries
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# esbuild records absolute paths in the metafile in the form the platform produced. Left in
|
|
35
|
+
# Windows form, every delete_prefix above failed, the manifest was keyed by full Windows
|
|
36
|
+
# paths, and every lookup missed: the app served source paths instead of the digest URLs it
|
|
37
|
+
# had just compiled, silently.
|
|
38
|
+
def fs_path(path) = Utils.fs_path(path)
|
|
39
|
+
|
|
40
|
+
# Empty, not nil or a lazy re-read: the runtime daemon relies on a reset manifest staying
|
|
41
|
+
# empty until `load!`.
|
|
42
|
+
def reset!
|
|
43
|
+
self.manifest = {}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def [](key) = manifest[key]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'bundled_gems'
|
|
4
|
+
require_relative 'context_map'
|
|
5
|
+
require_relative 'stale_contexts'
|
|
6
|
+
|
|
7
|
+
module Proscenium
|
|
8
|
+
# Which mapping generation a build uses (#154). A generation is the context map and the
|
|
9
|
+
# staleness verdict, read once and kept, so every build sees one consistent view. In production
|
|
10
|
+
# a generation lasts the process: restart after a deploy. In development and test, `refresh`
|
|
11
|
+
# looks at most once a second for a change to the files the map is built from, and starts a new
|
|
12
|
+
# generation when one changed, such as `proscenium install` finishing.
|
|
13
|
+
#
|
|
14
|
+
# Gemfile.lock is watched, but a gem added or upgraded still needs a restart: Bundler loads the
|
|
15
|
+
# bundle once per process.
|
|
16
|
+
module MappingGeneration
|
|
17
|
+
# The lockfiles too: with contexts adopted, resolved paths carry each package's version.
|
|
18
|
+
WATCHED = ['Gemfile.lock', 'package.json', 'pnpm-workspace.yaml', '.npmrc',
|
|
19
|
+
'bunfig.toml', 'pnpm-lock.yaml', 'bun.lock', ContextMap::INSTALLING].freeze
|
|
20
|
+
EVERY = 1.0
|
|
21
|
+
LOCK = Mutex.new
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# The current generation number, starting a new one first if a watched file changed and the
|
|
26
|
+
# last look was at least `every` seconds ago. Returns [number, whether it just changed].
|
|
27
|
+
def refresh(root, every: EVERY)
|
|
28
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
29
|
+
LOCK.synchronize do
|
|
30
|
+
@looked ||= {}
|
|
31
|
+
@number ||= 0
|
|
32
|
+
at, before = @looked[root]
|
|
33
|
+
return [@number, false] if at && now - at < every
|
|
34
|
+
|
|
35
|
+
after = signature(root)
|
|
36
|
+
@looked[root] = [now, after]
|
|
37
|
+
return [@number, false] if before.nil? || before == after
|
|
38
|
+
|
|
39
|
+
ContextMap.reset!
|
|
40
|
+
StaleContexts.reset!
|
|
41
|
+
[@number += 1, true]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The modification time of every file the map is built from, by path, nil for a missing one.
|
|
46
|
+
def signature(root)
|
|
47
|
+
files = WATCHED.map { File.join(root, it) } +
|
|
48
|
+
Dir.glob(File.join(root, ContextMap::CONTEXTS, '*', 'package.json')) +
|
|
49
|
+
manifests(root)
|
|
50
|
+
files.sort.to_h { [it, File.exist?(it) ? File.mtime(it).to_f : nil] }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Each participating gem's package.json: editing a path gem's manifest makes its context stale.
|
|
54
|
+
def manifests(root)
|
|
55
|
+
BundledGems.participating(BundledGems.installed_specs, overrides: BundledGems.overrides(root))
|
|
56
|
+
.values.filter_map { BundledGems.manifest_root(it) }
|
|
57
|
+
.map { File.join(it, 'package.json') }
|
|
58
|
+
rescue BundledGems::ConfigError
|
|
59
|
+
[]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def reset! = LOCK.synchronize { @looked = {} }
|
|
63
|
+
end
|
|
64
|
+
end
|