proscenium 0.19.0.beta6-arm64-darwin → 0.19.0.beta8-arm64-darwin

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -26
  3. data/lib/proscenium/builder.rb +12 -36
  4. data/lib/proscenium/bundled_gems.rb +37 -0
  5. data/lib/proscenium/css_module/rewriter.rb +2 -2
  6. data/lib/proscenium/css_module/transformer.rb +1 -1
  7. data/lib/proscenium/css_module.rb +4 -1
  8. data/lib/proscenium/ext/proscenium +0 -0
  9. data/lib/proscenium/ext/proscenium.h +1 -7
  10. data/lib/proscenium/helper.rb +3 -9
  11. data/lib/proscenium/importer.rb +34 -16
  12. data/lib/proscenium/log_subscriber.rb +11 -8
  13. data/lib/proscenium/middleware/base.rb +10 -8
  14. data/lib/proscenium/middleware/esbuild.rb +1 -6
  15. data/lib/proscenium/middleware/ruby_gems.rb +23 -0
  16. data/lib/proscenium/middleware.rb +26 -22
  17. data/lib/proscenium/monkey.rb +3 -5
  18. data/lib/proscenium/phlex/asset_inclusions.rb +0 -1
  19. data/lib/proscenium/phlex/css_modules.rb +1 -1
  20. data/lib/proscenium/railtie.rb +0 -27
  21. data/lib/proscenium/react_componentable.rb +0 -1
  22. data/lib/proscenium/registry/bundled_package.rb +29 -0
  23. data/lib/proscenium/registry/package.rb +95 -0
  24. data/lib/proscenium/registry/ruby_gem_package.rb +28 -0
  25. data/lib/proscenium/registry.rb +29 -0
  26. data/lib/proscenium/resolver.rb +23 -18
  27. data/lib/proscenium/ruby_gems.rb +67 -0
  28. data/lib/proscenium/side_load.rb +37 -69
  29. data/lib/proscenium/ui/flash/bun.lock +19 -0
  30. data/lib/proscenium/ui/flash/index.js +6 -2
  31. data/lib/proscenium/ui/flash/node_modules/dom-mutations/index.d.ts +33 -0
  32. data/lib/proscenium/ui/flash/node_modules/dom-mutations/index.js +44 -0
  33. data/lib/proscenium/ui/flash/node_modules/dom-mutations/license +9 -0
  34. data/lib/proscenium/ui/flash/node_modules/dom-mutations/package.json +59 -0
  35. data/lib/proscenium/ui/flash/node_modules/dom-mutations/readme.md +125 -0
  36. data/lib/proscenium/ui/flash/node_modules/sourdough-toast/LICENSE +20 -0
  37. data/lib/proscenium/ui/flash/node_modules/sourdough-toast/README.md +11 -0
  38. data/lib/proscenium/ui/flash/node_modules/sourdough-toast/package.json +44 -0
  39. data/lib/proscenium/ui/flash/node_modules/sourdough-toast/src/sourdough-toast.css +697 -0
  40. data/lib/proscenium/ui/flash/node_modules/sourdough-toast/src/sourdough-toast.js +537 -0
  41. data/lib/proscenium/ui/flash/package.json +11 -0
  42. data/lib/proscenium/ui/react-manager/index.jsx +3 -22
  43. data/lib/proscenium/ui/ujs/index.js +1 -1
  44. data/lib/proscenium/version.rb +1 -1
  45. data/lib/proscenium.rb +4 -11
  46. metadata +21 -3
  47. data/lib/proscenium/middleware/engines.rb +0 -41
@@ -14,42 +14,15 @@ module Proscenium
14
14
  config.proscenium.bundle = true
15
15
  config.proscenium.side_load = true
16
16
  config.proscenium.code_splitting = true
17
- config.proscenium.external_node_modules = false
18
17
 
19
- # Cache asset paths when building to path. Enabled by default in production.
20
- # @see Proscenium::Builder#build_to_path
21
- config.proscenium.cache = ActiveSupport::Cache::MemoryStore.new if Rails.env.production?
22
-
23
- # TODO: implement!
24
18
  config.proscenium.cache_query_string = Rails.env.production? && ENV.fetch('REVISION', nil)
25
19
  config.proscenium.cache_max_age = 2_592_000 # 30 days
26
20
 
27
- # A proc that will be given the path to build, and should return a boolean indicating whether to
28
- # cache the response.
29
- #
30
- # Example:
31
- # cache_middleware_response = ->(path) { path.start_with?('node_modules/') }
32
- config.proscenium.cache_middleware_response = nil
33
-
34
21
  # List of environment variable names that should be passed to the builder, which will then be
35
22
  # passed to esbuild's `Define` option. Being explicit about which environment variables are
36
23
  # defined means a faster build, as esbuild will have less to do.
37
24
  config.proscenium.env_vars = Set.new
38
25
 
39
- # Rails engines to expose and allow Proscenium to serve their assets.
40
- #
41
- # A Rails engine that has assets, can add Proscenium as a gem dependency, and then add itself
42
- # to this list. Proscenium will then serve the engine's assets at the URL path beginning with
43
- # the engine name.
44
- #
45
- # Example:
46
- # class Gem1::Engine < ::Rails::Engine
47
- # config.proscenium.engines[:gem1] = root
48
- # end
49
- config.proscenium.engines = {
50
- proscenium: Proscenium.ui_path
51
- }
52
-
53
26
  config.action_dispatch.rescue_templates = {
54
27
  'Proscenium::Builder::BuildError' => 'build_error'
55
28
  }
@@ -46,7 +46,6 @@ module Proscenium
46
46
  class_methods do
47
47
  def sideload(options)
48
48
  Importer.import manager, **options, js: { type: 'module' }
49
- Importer.sideload source_path, lazy: true, **options
50
49
  end
51
50
  end
52
51
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems/package'
4
+
5
+ class Proscenium::Registry
6
+ class BundledPackage < Package
7
+ def version = @version ||= spec.version.to_s
8
+
9
+ private
10
+
11
+ def package_json
12
+ @package_json ||= begin
13
+ unless (gem_path = Proscenium::BundledGems.pathname_for(gem_name))
14
+ raise PackageNotInstalledError, name
15
+ end
16
+
17
+ if (package_path = gem_path.join('package.json')).exist?
18
+ JSON.parse(package_path.read)
19
+ else
20
+ default_package_json
21
+ end
22
+ end
23
+ end
24
+
25
+ def spec
26
+ @spec ||= Bundler.load.specs[gem_name].first
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proscenium::Registry
4
+ # 1. Fetch the gem metadata from RubyGems API.
5
+ # 2. Extract any package.json from the gem, and populate the response with it.
6
+ # 3. Create a tarball containing the fetched package.json. This will be downloaded by the npm
7
+ # client, and unpacked into node_modules. Proscenium ignores this, as it will pull contents
8
+ # directly from location of the installed gem.
9
+ # 4. Return a valid npm response listing package details, tarball location, and its dependencies.
10
+ #
11
+ # See https://wiki.commonjs.org/wiki/Packages/Registry
12
+ class Package
13
+ extend Literal::Properties
14
+
15
+ prop :name, String, :positional, reader: :private
16
+ prop :version, _String?
17
+ prop :host, String
18
+
19
+ def as_json
20
+ {
21
+ name:,
22
+ 'dist-tags': {
23
+ latest: version
24
+ },
25
+ versions: {
26
+ version => {
27
+ name:,
28
+ version:,
29
+ dependencies: package_json['dependencies'] || {},
30
+ dist: {
31
+ tarball:,
32
+ integrity:,
33
+ shasum:
34
+ }
35
+ }
36
+ }
37
+ }
38
+ end
39
+
40
+ def validate!
41
+ return self if name.start_with?('@rubygems/')
42
+
43
+ raise PackageUnsupportedError, name
44
+ end
45
+
46
+ def gem_name = @gem_name ||= name.gsub('@rubygems/', '')
47
+ def version = @version # rubocop:disable Style/TrivialAccessors
48
+ def shasum = Digest::SHA1.file(tarball_path).hexdigest
49
+ def integrity = "sha512-#{Digest::SHA512.file(tarball_path).base64digest}"
50
+
51
+ private
52
+
53
+ def tarball
54
+ create_tarball unless tarball_path.exist?
55
+
56
+ "#{@host}/#{tarball_path.relative_path_from(Rails.public_path)}"
57
+ end
58
+
59
+ def tarball_name
60
+ @tarball_name ||= "#{gem_name}-#{version}"
61
+ end
62
+
63
+ def tarball_path
64
+ @tarball_path ||= Rails.public_path.join('proscenium_registry_tarballs')
65
+ .join("@rubygems/#{gem_name}/#{tarball_name}.tgz")
66
+ end
67
+
68
+ def create_tarball
69
+ FileUtils.mkdir_p(File.dirname(tarball_path))
70
+
71
+ File.open(tarball_path, 'wb') do |file|
72
+ Zlib::GzipWriter.wrap(file) do |gz|
73
+ Gem::Package::TarWriter.new(gz) do |tar|
74
+ contents = package_json.to_json
75
+ tar.add_file_simple('package/package.json', 0o444, contents.length) do |io|
76
+ io.write contents
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ def package_json
84
+ @package_json ||= default_package_json
85
+ end
86
+
87
+ def default_package_json
88
+ {
89
+ name:,
90
+ version:,
91
+ dependencies: {}
92
+ }
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proscenium::Registry
4
+ class RubyGemPackage < Package
5
+ def version = spec['version']
6
+
7
+ private
8
+
9
+ def package_json
10
+ @package_json ||= begin
11
+ package_path = Proscenium::RubyGems.path_for(gem_name, version).join('package.json')
12
+ if package_path.exist?
13
+ JSON.parse path.read
14
+ else
15
+ default_package_json
16
+ end
17
+ end
18
+ end
19
+
20
+ def spec
21
+ @spec ||= if @version.present?
22
+ Gems::V2.info gem_name, @version
23
+ else
24
+ Gems.info(gem_name)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proscenium::Registry
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Package
7
+ autoload :BundledPackage
8
+ autoload :RubyGemPackage
9
+
10
+ class PackageUnsupportedError < StandardError
11
+ def initialize(name)
12
+ super("Package `#{name}` is not valid; only Ruby gems are supported via the @rubygems scope.")
13
+ end
14
+ end
15
+
16
+ class PackageNotInstalledError < StandardError
17
+ def initialize(name)
18
+ super("Package `#{name}` is not found in your bundle; have you installed the Ruby gem?")
19
+ end
20
+ end
21
+
22
+ def self.bundled_package(name, host:)
23
+ BundledPackage.new(name, host:).validate!
24
+ end
25
+
26
+ def self.ruby_gem_package(name, version, host:)
27
+ RubyGemPackage.new(name, version:, host:).validate!
28
+ end
29
+ end
@@ -4,31 +4,36 @@ require 'active_support/current_attributes'
4
4
 
5
5
  module Proscenium
6
6
  class Resolver < ActiveSupport::CurrentAttributes
7
- # TODO: cache this across requests in production.
8
- attribute :resolved
7
+ attribute :resolved unless Rails.env.production?
8
+ mattr_accessor :resolved if Rails.env.production?
9
9
 
10
- # Resolve the given `path` to a URL path.
10
+ # Resolve the given `path` to a fully qualified URL path.
11
11
  #
12
- # @param path [String] Can be URL path, file system path, or bare specifier (ie. NPM package).
12
+ # @param path [String] URL path, file system path, or bare specifier (ie. NPM package).
13
13
  # @return [String] URL path.
14
14
  def self.resolve(path)
15
15
  self.resolved ||= {}
16
16
 
17
- self.resolved[path] ||= begin
18
- if path.start_with?('./', '../')
19
- raise ArgumentError, 'path must be an absolute file system or URL path'
20
- end
21
-
22
- if path.start_with?('proscenium/')
23
- "/#{path}"
24
- elsif (engine = Proscenium.config.engines.find { |_, v| path.start_with? "#{v}/" })
25
- path.sub(/^#{engine.last}/, "/#{engine.first}")
26
- elsif path.start_with?("#{Rails.root}/")
27
- path.delete_prefix Rails.root.to_s
28
- else
29
- Builder.resolve path
30
- end
17
+ if path.start_with?('./', '../')
18
+ raise ArgumentError, '`path` must be an absolute file system or URL path'
31
19
  end
20
+
21
+ self.resolved[path] ||= if (gem = BundledGems.paths.find { |_, v| path.start_with? "#{v}/" })
22
+ # If the path is a rubygem, and it is installed with npm via the
23
+ # @rubygems scope, then resolve the path to the symlinked location
24
+ # in node_modules.
25
+ # npm_path = Rails.root.join("node_modules/@rubygems/#{gem.first}")
26
+ # if npm_path.symlink?
27
+ # npm_path.realpath.join(path.sub(/^#{gem.last}/, '.')).to_s
28
+ # .delete_prefix(Rails.root.to_s)
29
+ # else
30
+ path.sub(/^#{gem.last}/, "/node_modules/@rubygems/#{gem.first}")
31
+ # end
32
+ elsif path.start_with?("#{Rails.root}/")
33
+ path.delete_prefix Rails.root.to_s
34
+ else
35
+ Builder.resolve path
36
+ end
32
37
  end
33
38
  end
34
39
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems/package'
4
+ require 'rubygems/remote_fetcher'
5
+
6
+ module Proscenium
7
+ class RubyGems
8
+ def self.path_for(name, version = nil)
9
+ Pathname new(name, version).path
10
+ end
11
+
12
+ def initialize(name, version = nil)
13
+ @name = name
14
+ @version = version
15
+ end
16
+
17
+ def path
18
+ dependency = Gem::Dependency.new @name, @version
19
+ path = gem_path dependency
20
+
21
+ raise "Gem '#{@name}' not installed nor fetchable." unless path
22
+
23
+ basename = File.basename path, '.gem'
24
+ target_dir = File.expand_path basename, Rails.root.join('tmp', 'unpacked_gems')
25
+
26
+ Gem::Package.new(path).extract_files target_dir
27
+
28
+ target_dir
29
+ end
30
+
31
+ # Find cached filename in Gem.path. Returns nil if the file cannot be found.
32
+ def find_in_cache(filename)
33
+ Gem.path.each do |path|
34
+ this_path = File.join(path, 'cache', filename)
35
+ return this_path if File.exist? this_path
36
+ end
37
+
38
+ nil
39
+ end
40
+
41
+ # Return the full path to the cached gem file matching the given
42
+ # name and version requirement. Returns 'nil' if no match.
43
+ #
44
+ # Example:
45
+ #
46
+ # get_path 'rake', '> 0.4' # "/usr/lib/ruby/gems/1.8/cache/rake-0.4.2.gem"
47
+ # get_path 'rake', '< 0.1' # nil
48
+ # get_path 'rak' # nil (exact name required)
49
+ def gem_path(dependency)
50
+ return dependency.name if /\.gem$/i.match?(dependency.name)
51
+
52
+ specs = dependency.matching_specs
53
+ selected = specs.max_by(&:version)
54
+
55
+ return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless selected
56
+ return unless /^#{selected.name}$/i.match?(dependency.name)
57
+
58
+ # We expect to find (basename).gem in the 'cache' directory. Furthermore,
59
+ # the name match must be exact (ignoring case).
60
+ path = find_in_cache File.basename selected.cache_file
61
+
62
+ return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless path
63
+
64
+ path
65
+ end
66
+ end
67
+ end
@@ -4,7 +4,6 @@ module Proscenium
4
4
  class SideLoad
5
5
  JS_COMMENT = '<!-- [PROSCENIUM_JAVASCRIPTS] -->'
6
6
  CSS_COMMENT = '<!-- [PROSCENIUM_STYLESHEETS] -->'
7
- LAZY_COMMENT = '<!-- [PROSCENIUM_LAZY_SCRIPTS] -->'
8
7
 
9
8
  module Controller
10
9
  def self.included(child)
@@ -35,28 +34,16 @@ module Proscenium
35
34
 
36
35
  return if !fragments && !included_comment
37
36
 
38
- imports = Proscenium::Importer.imported.dup
39
- paths_to_build = []
40
- Proscenium::Importer.each_stylesheet(delete: true) do |x, _|
41
- paths_to_build << x.delete_prefix('/')
42
- end
43
-
44
- result = Proscenium::Builder.build_to_path(paths_to_build.join(';'))
45
-
46
37
  out = []
47
- result.split(';').each do |x|
48
- inpath, outpath = x.split('::')
49
- inpath.prepend '/'
50
- outpath.delete_prefix! 'public'
51
-
52
- next unless imports.key?(inpath)
53
-
54
- import = imports[inpath]
55
- opts = import[:css].is_a?(Hash) ? import[:css] : {}
38
+ Proscenium::Importer.each_stylesheet(delete: true) do |path, opts|
39
+ opts = opts[:css].is_a?(Hash) ? opts[:css] : {}
56
40
  opts[:preload_links_header] = false if fragments
57
41
  opts[:data] ||= {}
58
- opts[:data][:original_href] = inpath
59
- out << helpers.stylesheet_link_tag(outpath, extname: false, **opts)
42
+
43
+ if Proscenium.config.cache_query_string.present?
44
+ path += "?#{Proscenium.config.cache_query_string}"
45
+ end
46
+ out << helpers.stylesheet_link_tag(path, extname: false, **opts)
60
47
  end
61
48
 
62
49
  if fragments
@@ -70,72 +57,43 @@ module Proscenium
70
57
  return if response_body.nil?
71
58
  return if response_body.first.blank? || !Proscenium::Importer.js_imported?
72
59
 
73
- imports = Proscenium::Importer.imported.dup
74
- paths_to_build = []
75
- Proscenium::Importer.each_javascript(delete: true) do |x, _|
76
- paths_to_build << x.delete_prefix('/')
77
- end
78
-
79
- result = Proscenium::Builder.build_to_path(paths_to_build.join(';'))
80
-
81
- included_js_comment = response_body.first.include?(JS_COMMENT)
82
- included_lazy_comment = response_body.first.include?(LAZY_COMMENT)
60
+ included_comment = response_body.first.include?(JS_COMMENT)
83
61
  fragments = if (fragment_header = request.headers['X-Fragment'])
84
62
  fragment_header.split
85
63
  end
86
64
 
87
- if fragments || included_js_comment
88
- out = []
89
- scripts = {}
90
- result.split(';').each do |x|
91
- inpath, outpath = x.split('::')
92
- inpath.prepend '/'
93
- outpath.delete_prefix! 'public'
94
-
95
- next unless imports.key?(inpath)
96
-
97
- if (import = imports[inpath]).delete(:lazy)
98
- scripts[inpath] = import.merge(outpath:)
99
- else
100
- opts = import[:js].is_a?(Hash) ? import[:js] : {}
101
- opts[:preload_links_header] = false if fragments
102
- out << helpers.javascript_include_tag(outpath, extname: false, **opts)
103
- end
104
- end
65
+ return if !fragments && !included_comment
105
66
 
106
- if fragments
107
- response_body.first.prepend out.join.html_safe
108
- elsif included_js_comment
109
- response_body.first.gsub! JS_COMMENT, out.join.html_safe
110
- end
111
- end
67
+ out = []
68
+ Proscenium::Importer.each_javascript(delete: true) do |path, opts|
69
+ next if opts.delete(:lazy)
112
70
 
113
- return if !fragments && !included_lazy_comment
71
+ opts = opts[:js].is_a?(Hash) ? opts[:js] : {}
72
+ opts[:preload_links_header] = false if fragments
114
73
 
115
- lazy_script = ''
116
- if scripts.present?
117
- lazy_script = helpers.content_tag 'script', type: 'application/json',
118
- id: 'prosceniumLazyScripts' do
119
- scripts.to_json.html_safe
74
+ if Proscenium.config.cache_query_string.present?
75
+ path += "?#{Proscenium.config.cache_query_string}"
120
76
  end
77
+ out << helpers.javascript_include_tag(path, extname: false, **opts)
121
78
  end
122
79
 
123
80
  if fragments
124
- response_body.first.prepend lazy_script
125
- elsif included_lazy_comment
126
- response_body.first.gsub! LAZY_COMMENT, lazy_script
81
+ response_body.first.prepend out.join.html_safe
82
+ elsif included_comment
83
+ response_body.first.gsub! JS_COMMENT, out.join.html_safe
127
84
  end
128
85
  end
129
86
  end
130
87
 
131
88
  class << self
132
- # Side loads the class, and its super classes that respond to `.source_path`.
89
+ # Side loads assets for the class, and its super classes that respond to `.source_path`, which
90
+ # should return a Pathname of the class source file.
133
91
  #
134
92
  # Set the `abstract_class` class variable to true in any class, and it will not be side
135
93
  # loaded.
136
94
  #
137
- # If the class responds to `.sideload`, it will be called instead of the regular side loading.
138
- # You can use this to customise what is side loaded.
95
+ # If the class responds to `.sideload`, it will be called after the regular side loading. You
96
+ # can use this to customise what is side loaded.
139
97
  def sideload_inheritance_chain(obj, options)
140
98
  return unless Proscenium.config.side_load
141
99
 
@@ -159,18 +117,28 @@ module Proscenium
159
117
 
160
118
  klass = obj.class
161
119
  while klass.respond_to?(:source_path) && klass.source_path && !klass.abstract_class
162
- if klass.respond_to?(:sideload)
163
- klass.sideload options
164
- elsif options[:css] == false
120
+ if options[:css] == false
165
121
  Importer.sideload klass.source_path, **options
166
122
  else
167
123
  Importer.sideload_js klass.source_path, **options
168
124
  css_imports << klass.source_path
169
125
  end
170
126
 
127
+ klass.sideload options if klass.respond_to?(:sideload)
128
+
171
129
  klass = klass.superclass
172
130
  end
173
131
 
132
+ # All regular CSS files (*.css) are ancestrally sideloaded. However, the first CSS module
133
+ # in the ancestry is also sideloaded in addition to the regular CSS files. This is because
134
+ # the CSS module digest will be different for each file, so we only sideload the first CSS
135
+ # module.
136
+ css_imports.each do |it|
137
+ break if Importer.sideload_css_module(it, **options).present?
138
+ end
139
+
140
+ # Sideload regular CSS files in reverse order.
141
+ #
174
142
  # The reason why we sideload CSS after JS is because the order of CSS is important.
175
143
  # Basically, the layout should be loaded before the view so that CSS cascading works in the
176
144
  # right direction.
@@ -0,0 +1,19 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "@proscenium/flash",
6
+ "dependencies": {
7
+ "dom-mutations": "^1.0.0",
8
+ },
9
+ "devDependencies": {
10
+ "sourdough-toast": "latest",
11
+ },
12
+ },
13
+ },
14
+ "packages": {
15
+ "dom-mutations": ["dom-mutations@1.0.0", "", {}, "sha512-qr4ufk/qu+JKwtz7NPbu6TxpTM/7nqburohI07J+mKSM20USvhcUjEb8hWY6g2a3QYp3LtlGpi+mAZLPuxTi7g=="],
16
+
17
+ "sourdough-toast": ["sourdough-toast@0.1.0", "", {}, "sha512-ianhWqaaA5a0n9TRg6dLEt5DWflXqsQUMxEAGZi8JcHGqsPRm7XTWEfhhDKSi/zJ11CBbvh8b8oRcgxjCeYZeg=="],
18
+ }
19
+ }
@@ -1,5 +1,9 @@
1
- import domMutations from "https://esm.run/dom-mutations";
2
- import { Sourdough, toast } from "https://esm.run/sourdough-toast";
1
+ import domMutations from "dom-mutations";
2
+ import { Sourdough, toast } from "sourdough-toast";
3
+
4
+ export function foo() {
5
+ console.log("foo");
6
+ }
3
7
 
4
8
  class HueFlash extends HTMLElement {
5
9
  static observedAttributes = ["data-flash-alert", "data-flash-notice"];
@@ -0,0 +1,33 @@
1
+ export type Options = MutationObserverInit & {signal?: AbortSignal};
2
+
3
+ /**
4
+ @returns An async iterable that yields [`MutationRecord`](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord) objects representing individual mutations.
5
+
6
+ @example
7
+ ```
8
+ import domMutations from 'dom-mutations';
9
+
10
+ const target = document.querySelector('#unicorn');
11
+
12
+ for await (const mutation of domMutations(target, {childList: true})) {
13
+ console.log('Mutation:', mutation);
14
+ }
15
+ ```
16
+ */
17
+ export default function domMutations(target: Node, options?: Options): AsyncIterable<MutationRecord>;
18
+
19
+ /**
20
+ Similar to `domMutations()`, but yields batches of [`MutationRecord`](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord) objects, each batch representing a group of mutations captured together. This method is less convenient, but can be useful in some cases when you need to handle mutations together as a group.
21
+
22
+ @example
23
+ ```
24
+ import {batchedDomMutations} from 'dom-mutations';
25
+
26
+ const target = document.querySelector('#unicorn');
27
+
28
+ for await (const mutations of batchedDomMutations(target, {childList: true})) {
29
+ console.log('Batch of mutations:', mutations);
30
+ }
31
+ ```
32
+ */
33
+ export function batchedDomMutations(target: Node, options?: Options): AsyncIterable<MutationRecord[]>;
@@ -0,0 +1,44 @@
1
+ export default function domMutations(target, options = {}) {
2
+ return {
3
+ async * [Symbol.asyncIterator]() {
4
+ for await (const mutations of batchedDomMutations(target, options)) {
5
+ yield * mutations;
6
+ }
7
+ },
8
+ };
9
+ }
10
+
11
+ export function batchedDomMutations(target, {signal, ...options} = {}) {
12
+ return {
13
+ async * [Symbol.asyncIterator]() {
14
+ signal?.throwIfAborted();
15
+
16
+ let resolveMutations;
17
+ let rejectMutations;
18
+
19
+ const observer = new globalThis.MutationObserver(mutations => {
20
+ resolveMutations?.(mutations);
21
+ });
22
+
23
+ observer.observe(target, options);
24
+
25
+ signal?.addEventListener('abort', () => {
26
+ rejectMutations?.(signal.reason);
27
+ observer.disconnect();
28
+ }, {once: true});
29
+
30
+ try {
31
+ while (true) {
32
+ signal?.throwIfAborted();
33
+
34
+ yield await new Promise((resolve, reject) => { // eslint-disable-line no-await-in-loop
35
+ resolveMutations = resolve;
36
+ rejectMutations = reject;
37
+ });
38
+ }
39
+ } finally {
40
+ observer.disconnect();
41
+ }
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.