From 675e9932f2ac0cc54e3e34db56c3fccbd5ebd34f Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Wed, 24 Jan 2024 13:10:50 +0000 Subject: [PATCH 1/7] Fix relative urls in JS --- lib/propshaft/assembly.rb | 1 + lib/propshaft/compiler/js_asset_urls.rb | 31 +++++++++++++++++++++++++ lib/propshaft/railtie.rb | 3 ++- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 lib/propshaft/compiler/js_asset_urls.rb diff --git a/lib/propshaft/assembly.rb b/lib/propshaft/assembly.rb index 3fbb11c..db00d7f 100644 --- a/lib/propshaft/assembly.rb +++ b/lib/propshaft/assembly.rb @@ -5,6 +5,7 @@ require "propshaft/processor" require "propshaft/compilers" require "propshaft/compiler/css_asset_urls" +require "propshaft/compiler/js_asset_urls" require "propshaft/compiler/source_mapping_urls" class Propshaft::Assembly diff --git a/lib/propshaft/compiler/js_asset_urls.rb b/lib/propshaft/compiler/js_asset_urls.rb new file mode 100644 index 0000000..3d5e9ae --- /dev/null +++ b/lib/propshaft/compiler/js_asset_urls.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "propshaft/compiler" + +class Propshaft::Compiler::JsAssetUrls < Propshaft::Compiler + ASSET_URL_PATTERN = /((?:import|export)(?:\s*|.*from\s*))["']((?:\.\/|\.\.\/|\/).+\.js)["']/ + + def compile(logical_path, input) + input.gsub(ASSET_URL_PATTERN) { asset_url resolve_path(logical_path.dirname, $2), logical_path, $2, $1 } + end + + private + def resolve_path(directory, filename) + if filename.start_with?("../") + Pathname.new(directory + filename).relative_path_from("").to_s + elsif filename.start_with?("/") + filename.delete_prefix("/").to_s + else + (directory + filename.delete_prefix("./")).to_s + end + end + + def asset_url(resolved_path, logical_path, pattern, import) + if asset = assembly.load_path.find(resolved_path) + %[#{import} "#{url_prefix}/#{asset.digested_path}"] + else + Propshaft.logger.warn "Unable to resolve '#{pattern}' for missing asset '#{resolved_path}' in #{logical_path}" + %[#{import} "#{pattern}"] + end + end +end diff --git a/lib/propshaft/railtie.rb b/lib/propshaft/railtie.rb index ad8d769..65dd5fa 100644 --- a/lib/propshaft/railtie.rb +++ b/lib/propshaft/railtie.rb @@ -13,7 +13,8 @@ class Railtie < ::Rails::Railtie config.assets.compilers = [ [ "text/css", Propshaft::Compiler::CssAssetUrls ], [ "text/css", Propshaft::Compiler::SourceMappingUrls ], - [ "text/javascript", Propshaft::Compiler::SourceMappingUrls ] + [ "text/javascript", Propshaft::Compiler::SourceMappingUrls ], + [ "text/javascript", Propshaft::Compiler::JsAssetUrls ] ] config.assets.sweep_cache = Rails.env.development? config.assets.server = Rails.env.development? || Rails.env.test? From 6daf090030a7cc37f94988b255533075bdfa7228 Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Wed, 24 Jan 2024 13:22:21 +0000 Subject: [PATCH 2/7] Update regex --- lib/propshaft/compiler/js_asset_urls.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/propshaft/compiler/js_asset_urls.rb b/lib/propshaft/compiler/js_asset_urls.rb index 3d5e9ae..07111a4 100644 --- a/lib/propshaft/compiler/js_asset_urls.rb +++ b/lib/propshaft/compiler/js_asset_urls.rb @@ -3,7 +3,7 @@ require "propshaft/compiler" class Propshaft::Compiler::JsAssetUrls < Propshaft::Compiler - ASSET_URL_PATTERN = /((?:import|export)(?:\s*|.*from\s*))["']((?:\.\/|\.\.\/|\/).+\.js)["']/ + ASSET_URL_PATTERN = /((?:import|export)(?:\s*|[^]*?from\s*))(?:["']((?:\.\/|\.\.\/|\/)[^"']+)["'])/ def compile(logical_path, input) input.gsub(ASSET_URL_PATTERN) { asset_url resolve_path(logical_path.dirname, $2), logical_path, $2, $1 } @@ -22,10 +22,10 @@ def resolve_path(directory, filename) def asset_url(resolved_path, logical_path, pattern, import) if asset = assembly.load_path.find(resolved_path) - %[#{import} "#{url_prefix}/#{asset.digested_path}"] + %[#{import} "#{url_prefix}/#{asset.digested_path} /* hello */"] else Propshaft.logger.warn "Unable to resolve '#{pattern}' for missing asset '#{resolved_path}' in #{logical_path}" - %[#{import} "#{pattern}"] + %[#{import} "#{pattern}" /* world */] end end end From 664d1765bdd6387703a45728657709e789e0e211 Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Wed, 24 Jan 2024 13:24:41 +0000 Subject: [PATCH 3/7] Use multiline mode --- lib/propshaft/compiler/js_asset_urls.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/propshaft/compiler/js_asset_urls.rb b/lib/propshaft/compiler/js_asset_urls.rb index 07111a4..9ac680c 100644 --- a/lib/propshaft/compiler/js_asset_urls.rb +++ b/lib/propshaft/compiler/js_asset_urls.rb @@ -3,7 +3,7 @@ require "propshaft/compiler" class Propshaft::Compiler::JsAssetUrls < Propshaft::Compiler - ASSET_URL_PATTERN = /((?:import|export)(?:\s*|[^]*?from\s*))(?:["']((?:\.\/|\.\.\/|\/)[^"']+)["'])/ + ASSET_URL_PATTERN = /((?:import|export)(?:\s*|.*?from\s*))(?:["']((?:\.\/|\.\.\/|\/)[^"']+)["'])/m def compile(logical_path, input) input.gsub(ASSET_URL_PATTERN) { asset_url resolve_path(logical_path.dirname, $2), logical_path, $2, $1 } From 927b79e7f26e775b02401657b4a93f14ae1343de Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Wed, 24 Jan 2024 13:26:41 +0000 Subject: [PATCH 4/7] Remove debugging comments --- lib/propshaft/compiler/js_asset_urls.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/propshaft/compiler/js_asset_urls.rb b/lib/propshaft/compiler/js_asset_urls.rb index 9ac680c..5b9a9cf 100644 --- a/lib/propshaft/compiler/js_asset_urls.rb +++ b/lib/propshaft/compiler/js_asset_urls.rb @@ -22,10 +22,10 @@ def resolve_path(directory, filename) def asset_url(resolved_path, logical_path, pattern, import) if asset = assembly.load_path.find(resolved_path) - %[#{import} "#{url_prefix}/#{asset.digested_path} /* hello */"] + %[#{import} "#{url_prefix}/#{asset.digested_path}"] else Propshaft.logger.warn "Unable to resolve '#{pattern}' for missing asset '#{resolved_path}' in #{logical_path}" - %[#{import} "#{pattern}" /* world */] + %[#{import} "#{pattern}"] end end end From c1a46840d6c9146939bbbbe872636f939043cc68 Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Thu, 25 Jan 2024 12:03:55 +0000 Subject: [PATCH 5/7] Updated names and comments --- .devcontainer/devcontainer.json | 30 ++-- lib/propshaft/assembly.rb | 2 +- lib/propshaft/compiler/js_asset_urls.rb | 31 ---- lib/propshaft/compiler/js_import_urls.rb | 53 +++++++ lib/propshaft/railtie.rb | 2 +- .../propshaft/compiler/js_import_urls_test.rb | 140 ++++++++++++++++++ 6 files changed, 209 insertions(+), 49 deletions(-) delete mode 100644 lib/propshaft/compiler/js_asset_urls.rb create mode 100644 lib/propshaft/compiler/js_import_urls.rb create mode 100644 test/propshaft/compiler/js_import_urls_test.rb diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2a059da..749773d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,3 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.205.2/containers/ruby { "name": "Ruby", "build": { @@ -15,15 +13,15 @@ }, // Configure tool-specific properties. - "customizations": { - // Configure properties specific to VS Code. - "vscode": { - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "Shopify.ruby-lsp" - ] - } - }, +//"customizations": { +// // Configure properties specific to VS Code. +// "vscode": { +// // Add the IDs of extensions you want installed when the container is created. +// "extensions": [ +// "Shopify.ruby-lsp" +// ] +// } +//}, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], @@ -32,9 +30,9 @@ // "postCreateCommand": "ruby --version", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode", - "features": { - "github-cli": "latest" - } +//"remoteUser": "vscode", +//"features": { +// "github-cli": "latest" +//} -} \ No newline at end of file +} diff --git a/lib/propshaft/assembly.rb b/lib/propshaft/assembly.rb index db00d7f..c3a889c 100644 --- a/lib/propshaft/assembly.rb +++ b/lib/propshaft/assembly.rb @@ -5,7 +5,7 @@ require "propshaft/processor" require "propshaft/compilers" require "propshaft/compiler/css_asset_urls" -require "propshaft/compiler/js_asset_urls" +require "propshaft/compiler/js_import_urls" require "propshaft/compiler/source_mapping_urls" class Propshaft::Assembly diff --git a/lib/propshaft/compiler/js_asset_urls.rb b/lib/propshaft/compiler/js_asset_urls.rb deleted file mode 100644 index 5b9a9cf..0000000 --- a/lib/propshaft/compiler/js_asset_urls.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require "propshaft/compiler" - -class Propshaft::Compiler::JsAssetUrls < Propshaft::Compiler - ASSET_URL_PATTERN = /((?:import|export)(?:\s*|.*?from\s*))(?:["']((?:\.\/|\.\.\/|\/)[^"']+)["'])/m - - def compile(logical_path, input) - input.gsub(ASSET_URL_PATTERN) { asset_url resolve_path(logical_path.dirname, $2), logical_path, $2, $1 } - end - - private - def resolve_path(directory, filename) - if filename.start_with?("../") - Pathname.new(directory + filename).relative_path_from("").to_s - elsif filename.start_with?("/") - filename.delete_prefix("/").to_s - else - (directory + filename.delete_prefix("./")).to_s - end - end - - def asset_url(resolved_path, logical_path, pattern, import) - if asset = assembly.load_path.find(resolved_path) - %[#{import} "#{url_prefix}/#{asset.digested_path}"] - else - Propshaft.logger.warn "Unable to resolve '#{pattern}' for missing asset '#{resolved_path}' in #{logical_path}" - %[#{import} "#{pattern}"] - end - end -end diff --git a/lib/propshaft/compiler/js_import_urls.rb b/lib/propshaft/compiler/js_import_urls.rb new file mode 100644 index 0000000..d2affb9 --- /dev/null +++ b/lib/propshaft/compiler/js_import_urls.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "propshaft/compiler" + +class Propshaft::Compiler::JsImportUrls < Propshaft::Compiler + # Sample of syntax captured by regex: + # Import and export declarations: + # import defaultExport, { export1, /* … */ } from "module-name"; + # import defaultExport, * as name from "module-name"; + # import "module-name"; + # export * from "module-name"; + # Dymaic imports: + # import("/modules/my-module.js") + # + # ( # Caputre 1: + # (?:import|export) # Matches import or export + # (?:\s*|.*?from\s*) # Matches any whitespace OR anything followed by "from" followed by any whitespace + # ) + # (?:\(\s)? # Optionally matches ( followed by any whitespace + # ["'] # Matches " or ' + # + # ( # Capture 2: + # (?:\.\/|\.\.\/|\/) # Matches ./ OR ../ OR / + # [^"']+ # Matches any characters that aren't " or ' + # ) + # ["'] # Matches " or ' + # (?:\s*\))? # Optionally matches any whitespace followed by ) + IMPORT_URL_PATTERN = /((?:import|export)(?:\s*|.*?from\s*))(?:\(\s)?["']((?:\.\/|\.\.\/|\/)[^"']+)["'](?:\s*\))?/m + + def compile(logical_path, input) + input.gsub(IMPORT_URL_PATTERN) { asset_url resolve_path(logical_path.dirname, $2), logical_path, $2, $1 } + end + + private + def resolve_path(directory, filename) + if filename.start_with?("../") + Pathname.new(directory + filename).relative_path_from("").to_s + elsif filename.start_with?("/") + filename.delete_prefix("/").to_s + else + (directory + filename.delete_prefix("./")).to_s + end + end + + def asset_url(resolved_path, logical_path, pattern, import) + if asset = assembly.load_path.find(resolved_path) + %[#{import} "#{url_prefix}/#{asset.digested_path}"] + else + Propshaft.logger.warn "Unable to resolve '#{pattern}' for missing asset '#{resolved_path}' in #{logical_path}" + %[#{import} "#{pattern}"] + end + end +end diff --git a/lib/propshaft/railtie.rb b/lib/propshaft/railtie.rb index 65dd5fa..7946ec0 100644 --- a/lib/propshaft/railtie.rb +++ b/lib/propshaft/railtie.rb @@ -14,7 +14,7 @@ class Railtie < ::Rails::Railtie [ "text/css", Propshaft::Compiler::CssAssetUrls ], [ "text/css", Propshaft::Compiler::SourceMappingUrls ], [ "text/javascript", Propshaft::Compiler::SourceMappingUrls ], - [ "text/javascript", Propshaft::Compiler::JsAssetUrls ] + [ "text/javascript", Propshaft::Compiler::JsImportUrls ] ] config.assets.sweep_cache = Rails.env.development? config.assets.server = Rails.env.development? || Rails.env.test? diff --git a/test/propshaft/compiler/js_import_urls_test.rb b/test/propshaft/compiler/js_import_urls_test.rb new file mode 100644 index 0000000..5c8535a --- /dev/null +++ b/test/propshaft/compiler/js_import_urls_test.rb @@ -0,0 +1,140 @@ +require "test_helper" +require "minitest/mock" +require "propshaft/asset" +require "propshaft/assembly" +require "propshaft/compilers" + +class Propshaft::Compiler::JsImportUrlsTest < ActiveSupport::TestCase + setup do + @options = ActiveSupport::OrderedOptions.new.tap { |config| + config.paths = [ Pathname.new("#{__dir__}/../../fixtures/assets/vendor") ] + config.output_path = Pathname.new("#{__dir__}/../../fixtures/output") + config.prefix = "/assets" + } + end + + test "basic" do + compiled = compile_asset_with_content(%({ background: url(file.jpg); })) + assert_match(/{ background: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + end + + test "blank spaces around name" do + compiled = compile_asset_with_content(%({ background: url( file.jpg ); })) + assert_match(/{ background: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + end + + test "quotes around name" do + compiled = compile_asset_with_content(%({ background: url("file.jpg"); })) + assert_match(/{ background: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + end + + test "single quotes around name" do + compiled = compile_asset_with_content(%({ background: url('file.jpg'); })) + assert_match(/{ background: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + end + + test "root directory" do + compiled = compile_asset_with_content(%({ background: url('/file.jpg'); })) + assert_match(/{ background: url\("\/assets\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + end + + test "same directory" do + compiled = compile_asset_with_content(%({ background: url('./file.jpg'); })) + assert_match(/{ background: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + end + + test "subdirectory" do + compiled = compile_asset_with_content(%({ background: url('./images/file.jpg'); })) + assert_match(/{ background: url\("\/assets\/foobar\/source\/images\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + end + + test "parent directory" do + compiled = compile_asset_with_content(%({ background: url('../file.jpg'); })) + assert_match(/{ background: url\("\/assets\/foobar\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + end + + test "grandparent directory" do + compiled = compile_asset_with_content(%({ background: url('../../file.jpg'); })) + assert_match(/{ background: url\("\/assets\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + end + + test "sibling directory" do + compiled = compile_asset_with_content(%({ background: url('../sibling/file.jpg'); })) + assert_match(/{ background: url\("\/assets\/foobar\/sibling\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + end + + test "mixed" do + compiled = compile_asset_with_content(%({ mask-image: image(url(file.jpg), skyblue, linear-gradient(rgba(0, 0, 0, 1.0), transparent)); })) + assert_match(/{ mask-image: image\(url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\), skyblue, linear-gradient\(rgba\(0, 0, 0, 1.0\), transparent\)\); }/, compiled) + end + + test "multiple" do + compiled = compile_asset_with_content(%({ content: url(file.svg) url(file.svg); })) + assert_match(/{ content: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.svg"\) url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.svg"\); }/, compiled) + end + + test "url" do + compiled = compile_asset_with_content(%({ background: url('https://rubyonrails.org/images/rails-logo.svg'); })) + assert_match "{ background: url('https://rubyonrails.org/images/rails-logo.svg'); }", compiled + end + + test "relative protocol url" do + compiled = compile_asset_with_content(%({ background: url('//rubyonrails.org/images/rails-logo.svg'); })) + assert_match "{ background: url('//rubyonrails.org/images/rails-logo.svg'); }", compiled + end + + test "data" do + compiled = compile_asset_with_content(%({ background: url(data:image/png;base64,iRxVB0); })) + assert_match "{ background: url(data:image/png;base64,iRxVB0); }", compiled + end + + test "anchor" do + compiled = compile_asset_with_content(%({ background: url(#IDofSVGpath); })) + assert_match "{ background: url(#IDofSVGpath); }", compiled + end + + test "fingerprint" do + compiled = compile_asset_with_content(%({ background: url('/file.jpg?30af91bf14e37666a085fb8a161ff36d'); })) + assert_match(/{ background: url\("\/assets\/file-[a-z0-9]{8}.jpg\?30af91bf14e37666a085fb8a161ff36d"\); }/, compiled) + end + + test "svg anchor" do + compiled = compile_asset_with_content(%({ content: url(file.svg#rails); })) + assert_match(/{ content: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.svg#rails"\); }/, compiled) + end + + test "svg mask encoded anchor" do + compiled = compile_asset_with_content(%({ background: url("data:image/svg+xml;charset=utf-8,%3Csvg mask='url(%23MyMask)'%3E%3C/svg%3E"); })) + assert_match "{ background: url(\"data:image/svg+xml;charset=utf-8,%3Csvg mask='url(%23MyMask)'%3E%3C/svg%3E\"); }", compiled + end + + test "non greedy anchors" do + compiled = compile_asset_with_content(%({ content: url(file.svg#demo) url(file.svg#demo); })) + assert_match(/{ content: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.svg#demo"\) url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.svg#demo"\); }/, compiled) + end + + test "missing asset" do + compiled = compile_asset_with_content(%({ background: url("file-not-found.jpg"); })) + assert_match(/{ background: url\("file-not-found.jpg"\); }/, compiled) + end + + test "relative url root" do + @options.relative_url_root = "/url-root" + + compiled = compile_asset_with_content(%({ background: url(file.jpg); })) + assert_match(/{ background: url\("\/url-root\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + end + + private + def compile_asset_with_content(content) + root_path = Pathname.new("#{__dir__}/../../fixtures/assets/vendor") + logical_path = "foobar/source/test.js" + + asset = Propshaft::Asset.new(root_path.join(logical_path), logical_path: logical_path) + asset.stub :content, content do + assembly = Propshaft::Assembly.new(@options) + assembly.compilers.register "text/javascript", Propshaft::Compiler::JsImportUrls + assembly.compilers.compile(asset) + end + end +end From 31644df6ccba5308df3b57d3d5ccd3f76afa885d Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Thu, 25 Jan 2024 20:23:18 +0000 Subject: [PATCH 6/7] Added dynamic imports and preserve quote type --- lib/propshaft/compiler/js_import_urls.rb | 30 ++-- test/fixtures/assets/vendor/file.js | 1 + test/fixtures/assets/vendor/foobar/file.js | 1 + .../assets/vendor/foobar/source/file.js | 1 + .../propshaft/compiler/js_import_urls_test.rb | 165 +++++++++--------- 5 files changed, 104 insertions(+), 94 deletions(-) create mode 100644 test/fixtures/assets/vendor/file.js create mode 100644 test/fixtures/assets/vendor/foobar/file.js create mode 100644 test/fixtures/assets/vendor/foobar/source/file.js diff --git a/lib/propshaft/compiler/js_import_urls.rb b/lib/propshaft/compiler/js_import_urls.rb index d2affb9..fd35ec3 100644 --- a/lib/propshaft/compiler/js_import_urls.rb +++ b/lib/propshaft/compiler/js_import_urls.rb @@ -9,26 +9,30 @@ class Propshaft::Compiler::JsImportUrls < Propshaft::Compiler # import defaultExport, * as name from "module-name"; # import "module-name"; # export * from "module-name"; - # Dymaic imports: - # import("/modules/my-module.js") + # Dynamic imports: + # import("./file.js").then((module) => ...) # # ( # Caputre 1: # (?:import|export) # Matches import or export # (?:\s*|.*?from\s*) # Matches any whitespace OR anything followed by "from" followed by any whitespace # ) - # (?:\(\s)? # Optionally matches ( followed by any whitespace - # ["'] # Matches " or ' - # - # ( # Capture 2: + # ( # Caputre 2: + # (?:\(\s*)? # Optionally matches ( followed by any whitespace + # ["'] # Matches " or ' + # ) + # ( # Capture 3: # (?:\.\/|\.\.\/|\/) # Matches ./ OR ../ OR / + # [^\/] # Matches any character that isn't /. Prevents imports strating with relative url protocol // # [^"']+ # Matches any characters that aren't " or ' # ) - # ["'] # Matches " or ' - # (?:\s*\))? # Optionally matches any whitespace followed by ) - IMPORT_URL_PATTERN = /((?:import|export)(?:\s*|.*?from\s*))(?:\(\s)?["']((?:\.\/|\.\.\/|\/)[^"']+)["'](?:\s*\))?/m + # ( # Caputre 4: + # ["'] # Matches " or ' + # (?:\s*\))? # Optionally matches any whitespace followed by a ) + # ) + IMPORT_URL_PATTERN = /((?:import|export)(?:\s*|.*?from\s*))((?:\(\s*)?["'])((?:\.\/|\.\.\/|\/)[^\/][^"']+)(["'](?:\s*\))?)/m def compile(logical_path, input) - input.gsub(IMPORT_URL_PATTERN) { asset_url resolve_path(logical_path.dirname, $2), logical_path, $2, $1 } + input.gsub(IMPORT_URL_PATTERN) { asset_url resolve_path(logical_path.dirname, $3), logical_path, $3, $1, $2, $4 } end private @@ -42,12 +46,12 @@ def resolve_path(directory, filename) end end - def asset_url(resolved_path, logical_path, pattern, import) + def asset_url(resolved_path, logical_path, pattern, import, open, close) if asset = assembly.load_path.find(resolved_path) - %[#{import} "#{url_prefix}/#{asset.digested_path}"] + %[#{import}#{open}#{url_prefix}/#{asset.digested_path}#{close}] else Propshaft.logger.warn "Unable to resolve '#{pattern}' for missing asset '#{resolved_path}' in #{logical_path}" - %[#{import} "#{pattern}"] + %[#{import}#{open}#{pattern}#{close}] end end end diff --git a/test/fixtures/assets/vendor/file.js b/test/fixtures/assets/vendor/file.js new file mode 100644 index 0000000..9bea59b --- /dev/null +++ b/test/fixtures/assets/vendor/file.js @@ -0,0 +1 @@ +console.log("foobar") diff --git a/test/fixtures/assets/vendor/foobar/file.js b/test/fixtures/assets/vendor/foobar/file.js new file mode 100644 index 0000000..28f27a1 --- /dev/null +++ b/test/fixtures/assets/vendor/foobar/file.js @@ -0,0 +1 @@ +console.log("world") diff --git a/test/fixtures/assets/vendor/foobar/source/file.js b/test/fixtures/assets/vendor/foobar/source/file.js new file mode 100644 index 0000000..7728117 --- /dev/null +++ b/test/fixtures/assets/vendor/foobar/source/file.js @@ -0,0 +1 @@ +console.log("hello") diff --git a/test/propshaft/compiler/js_import_urls_test.rb b/test/propshaft/compiler/js_import_urls_test.rb index 5c8535a..5a1fcd8 100644 --- a/test/propshaft/compiler/js_import_urls_test.rb +++ b/test/propshaft/compiler/js_import_urls_test.rb @@ -13,116 +13,119 @@ class Propshaft::Compiler::JsImportUrlsTest < ActiveSupport::TestCase } end - test "basic" do - compiled = compile_asset_with_content(%({ background: url(file.jpg); })) - assert_match(/{ background: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + test "basic relative imports and exports to file in same folder" do + compiled = compile_asset_with_content(%(import "./file.js")) + assert_match(/import "\/assets\/foobar\/source\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(import * from "./file.js")) + assert_match(/import \* from "\/assets\/foobar\/source\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(export * from "./file.js")) + assert_match(/export \* from "\/assets\/foobar\/source\/file-[a-z0-9]{8}.js"/, compiled) end - test "blank spaces around name" do - compiled = compile_asset_with_content(%({ background: url( file.jpg ); })) - assert_match(/{ background: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + test "basic relative imports and exports to file with single quotes" do + compiled = compile_asset_with_content(%(import './file.js')) + assert_match(/import '\/assets\/foobar\/source\/file-[a-z0-9]{8}.js'/, compiled) + compiled = compile_asset_with_content(%(import * from './file.js')) + assert_match(/import \* from '\/assets\/foobar\/source\/file-[a-z0-9]{8}.js'/, compiled) + compiled = compile_asset_with_content(%(export * from './file.js')) + assert_match(/export \* from '\/assets\/foobar\/source\/file-[a-z0-9]{8}.js'/, compiled) end - test "quotes around name" do - compiled = compile_asset_with_content(%({ background: url("file.jpg"); })) - assert_match(/{ background: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + test "multiline imports and exports to file in same folder" do + compiled = compile_asset_with_content("import {\na as b\n} from \"./file.js\"") + assert_match(/import {\na as b\n} from "\/assets\/foobar\/source\/file-[a-z0-9]{8}.js"/m, compiled) + compiled = compile_asset_with_content("export {\na as b\n} from \"./file.js\"") + assert_match(/export {\na as b\n} from "\/assets\/foobar\/source\/file-[a-z0-9]{8}.js"/m, compiled) end - test "single quotes around name" do - compiled = compile_asset_with_content(%({ background: url('file.jpg'); })) - assert_match(/{ background: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + test "imports and exports with excess space to file in same folder" do + compiled = compile_asset_with_content(%(import "./file.js")) + assert_match(/import "\/assets\/foobar\/source\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(import * from "./file.js")) + assert_match(/import \* from "\/assets\/foobar\/source\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(export * from "./file.js")) + assert_match(/export \* from "\/assets\/foobar\/source\/file-[a-z0-9]{8}.js"/, compiled) end - test "root directory" do - compiled = compile_asset_with_content(%({ background: url('/file.jpg'); })) - assert_match(/{ background: url\("\/assets\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + test "basic relative imports and exports to file in same parent" do + compiled = compile_asset_with_content(%(import "../file.js")) + assert_match(/import "\/assets\/foobar\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(import * from "../file.js")) + assert_match(/import \* from "\/assets\/foobar\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(export * from "../file.js")) + assert_match(/export \* from "\/assets\/foobar\/file-[a-z0-9]{8}.js"/, compiled) end - test "same directory" do - compiled = compile_asset_with_content(%({ background: url('./file.jpg'); })) - assert_match(/{ background: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + test "multiline imports and exports to file in same parent" do + compiled = compile_asset_with_content("import {\na as b\n} from \"../file.js\"") + assert_match(/import {\na as b\n} from "\/assets\/foobar\/file-[a-z0-9]{8}.js"/m, compiled) + compiled = compile_asset_with_content("export {\na as b\n} from \"../file.js\"") + assert_match(/export {\na as b\n} from "\/assets\/foobar\/file-[a-z0-9]{8}.js"/m, compiled) end - test "subdirectory" do - compiled = compile_asset_with_content(%({ background: url('./images/file.jpg'); })) - assert_match(/{ background: url\("\/assets\/foobar\/source\/images\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + test "imports and exports with excess space to file in same parent" do + compiled = compile_asset_with_content(%(import "../file.js")) + assert_match(/import "\/assets\/foobar\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(import * from "../file.js")) + assert_match(/import \* from "\/assets\/foobar\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(export * from "../file.js")) + assert_match(/export \* from "\/assets\/foobar\/file-[a-z0-9]{8}.js"/, compiled) end - test "parent directory" do - compiled = compile_asset_with_content(%({ background: url('../file.jpg'); })) - assert_match(/{ background: url\("\/assets\/foobar\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + test "basic relative imports and exports to file in the root" do + compiled = compile_asset_with_content(%(import "/file.js")) + assert_match(/import "\/assets\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(import * from "/file.js")) + assert_match(/import \* from "\/assets\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(export * from "/file.js")) + assert_match(/export \* from "\/assets\/file-[a-z0-9]{8}.js"/, compiled) end - test "grandparent directory" do - compiled = compile_asset_with_content(%({ background: url('../../file.jpg'); })) - assert_match(/{ background: url\("\/assets\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + test "multiline imports and exports to file in the root" do + compiled = compile_asset_with_content("import {\na as b\n} from \"/file.js\"") + assert_match(/import {\na as b\n} from "\/assets\/file-[a-z0-9]{8}.js"/m, compiled) + compiled = compile_asset_with_content("export {\na as b\n} from \"/file.js\"") + assert_match(/export {\na as b\n} from "\/assets\/file-[a-z0-9]{8}.js"/m, compiled) end - test "sibling directory" do - compiled = compile_asset_with_content(%({ background: url('../sibling/file.jpg'); })) - assert_match(/{ background: url\("\/assets\/foobar\/sibling\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + test "imports and exports with excess space to file in the root" do + compiled = compile_asset_with_content(%(import "/file.js")) + assert_match(/import "\/assets\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(import * from "/file.js")) + assert_match(/import \* from "\/assets\/file-[a-z0-9]{8}.js"/, compiled) + compiled = compile_asset_with_content(%(export * from "/file.js")) + assert_match(/export \* from "\/assets\/file-[a-z0-9]{8}.js"/, compiled) end - test "mixed" do - compiled = compile_asset_with_content(%({ mask-image: image(url(file.jpg), skyblue, linear-gradient(rgba(0, 0, 0, 1.0), transparent)); })) - assert_match(/{ mask-image: image\(url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\), skyblue, linear-gradient\(rgba\(0, 0, 0, 1.0\), transparent\)\); }/, compiled) + test "basic relative dynamic imports to file in same folder" do + compiled = compile_asset_with_content(%(import("./file.js").then())) + assert_match(/import\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.js"\).then\(\)/, compiled) end - test "multiple" do - compiled = compile_asset_with_content(%({ content: url(file.svg) url(file.svg); })) - assert_match(/{ content: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.svg"\) url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.svg"\); }/, compiled) + test "basic relative dynamic imports to file with single quotes" do + compiled = compile_asset_with_content(%(import('./file.js'))) + assert_match(/import\('\/assets\/foobar\/source\/file-[a-z0-9]{8}.js'\)/, compiled) end - test "url" do - compiled = compile_asset_with_content(%({ background: url('https://rubyonrails.org/images/rails-logo.svg'); })) - assert_match "{ background: url('https://rubyonrails.org/images/rails-logo.svg'); }", compiled - end - - test "relative protocol url" do - compiled = compile_asset_with_content(%({ background: url('//rubyonrails.org/images/rails-logo.svg'); })) - assert_match "{ background: url('//rubyonrails.org/images/rails-logo.svg'); }", compiled - end - - test "data" do - compiled = compile_asset_with_content(%({ background: url(data:image/png;base64,iRxVB0); })) - assert_match "{ background: url(data:image/png;base64,iRxVB0); }", compiled - end - - test "anchor" do - compiled = compile_asset_with_content(%({ background: url(#IDofSVGpath); })) - assert_match "{ background: url(#IDofSVGpath); }", compiled - end - - test "fingerprint" do - compiled = compile_asset_with_content(%({ background: url('/file.jpg?30af91bf14e37666a085fb8a161ff36d'); })) - assert_match(/{ background: url\("\/assets\/file-[a-z0-9]{8}.jpg\?30af91bf14e37666a085fb8a161ff36d"\); }/, compiled) - end - - test "svg anchor" do - compiled = compile_asset_with_content(%({ content: url(file.svg#rails); })) - assert_match(/{ content: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.svg#rails"\); }/, compiled) - end - - test "svg mask encoded anchor" do - compiled = compile_asset_with_content(%({ background: url("data:image/svg+xml;charset=utf-8,%3Csvg mask='url(%23MyMask)'%3E%3C/svg%3E"); })) - assert_match "{ background: url(\"data:image/svg+xml;charset=utf-8,%3Csvg mask='url(%23MyMask)'%3E%3C/svg%3E\"); }", compiled - end - - test "non greedy anchors" do - compiled = compile_asset_with_content(%({ content: url(file.svg#demo) url(file.svg#demo); })) - assert_match(/{ content: url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.svg#demo"\) url\("\/assets\/foobar\/source\/file-[a-z0-9]{8}.svg#demo"\); }/, compiled) + test "dynamic imports with excess space to file in same folder" do + compiled = compile_asset_with_content(%(import \( "./file.js" \) )) + assert_match(/import \( "\/assets\/foobar\/source\/file-[a-z0-9]{8}.js" \)/, compiled) end test "missing asset" do - compiled = compile_asset_with_content(%({ background: url("file-not-found.jpg"); })) - assert_match(/{ background: url\("file-not-found.jpg"\); }/, compiled) + compiled = compile_asset_with_content(%(import "./nothere.js")) + assert_match(/import "\.\/nothere.js"/, compiled) + compiled = compile_asset_with_content(%(import * from "./nothere.js")) + assert_match(/import \* from "\.\/nothere.js"/, compiled) + compiled = compile_asset_with_content(%(export * from "./nothere.js")) + assert_match(/export \* from "\.\/nothere.js"/, compiled) + compiled = compile_asset_with_content(%(import("./nothere.js").then())) + assert_match(/import\("\.\/nothere.js"\).then\(\)/, compiled) end - test "relative url root" do - @options.relative_url_root = "/url-root" - - compiled = compile_asset_with_content(%({ background: url(file.jpg); })) - assert_match(/{ background: url\("\/url-root\/assets\/foobar\/source\/file-[a-z0-9]{8}.jpg"\); }/, compiled) + test "relative protocol url" do + compiled = compile_asset_with_content(%(import "//rubyonrails.org/assets/main.js")) + assert_match(/import "\/\/rubyonrails\.org\/assets\/main\.js"/, compiled) end private From fbae0e4f9b389b172fc3d1eec75598d7bd4af894 Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Thu, 25 Jan 2024 20:25:21 +0000 Subject: [PATCH 7/7] Restore dev container --- .devcontainer/devcontainer.json | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 749773d..2a059da 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,3 +1,5 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.205.2/containers/ruby { "name": "Ruby", "build": { @@ -13,15 +15,15 @@ }, // Configure tool-specific properties. -//"customizations": { -// // Configure properties specific to VS Code. -// "vscode": { -// // Add the IDs of extensions you want installed when the container is created. -// "extensions": [ -// "Shopify.ruby-lsp" -// ] -// } -//}, + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "Shopify.ruby-lsp" + ] + } + }, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], @@ -30,9 +32,9 @@ // "postCreateCommand": "ruby --version", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. -//"remoteUser": "vscode", -//"features": { -// "github-cli": "latest" -//} + "remoteUser": "vscode", + "features": { + "github-cli": "latest" + } -} +} \ No newline at end of file