Skip to content

Commit 26ab212

Browse files
justin808claude
andauthored
refactor: clarify RSC migration parser and release safeguards (#3343)
## Summary Follow-up to #3219 / fixes #3258. This PR is now rebased onto current `origin/main`. ### RSC migration parser cleanup - Rename `rsc_plugin_without_client_references?` to `any_rsc_plugin_missing_client_references?` and document the existential semantics. - Consolidate lightweight JS scanner support/limitation notes onto `advance_js_scan_state`, including caller coverage and regex/template-literal caveats. - Clarify why `add_rsc_client_references_setup` keeps defensive duplicate-helper guards. ### Adjacent hardening - Strengthen Node renderer `SECURITY:` comments for CommonJS wrapping via both `additionalContext` and `supportModules`. - Make the package-json publish rewrite use a same-directory temp file plus atomic rename before marking the file as changed. - Add direct coverage for the package-json pin fallback warning formatter. - Keep the retry-sleep guard in release metadata lookup explicit and make the webpack helper test description path-agnostic. ## Review Follow-Ups - **Must-fix:** none. Thread-aware review scan currently shows 0 unresolved review threads. - **Optional fixed:** added the direct `supportModules` security anchor and the `package_json_pin_fallback_warning` formatter spec. - **Discuss / advice:** the prior `Generator tests / examples (3.2, minimum)` failure was caused by `npm install shakapacker@10.1.0` returning `ETARGET` before that npm version was available. The registry now resolves `shakapacker@10.1.0`, and the React 16 pinned example task passed locally. - **Discuss / non-code CI:** the current `claude-review` failure is the Claude Action hitting its weekly limit (`resets May 29, 3am UTC`), not a repository test failure. ## CI / Test Plan - [x] Rebased onto `origin/main` - [x] `npm view shakapacker@10.1.0 version` - [x] `(cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_react16)` - [x] `(cd react_on_rails && BUNDLE_FORCE_RUBY_PLATFORM=true bundle exec rspec spec/react_on_rails/shakapacker_examples_rake_spec.rb spec/react_on_rails/release_rake_helpers_spec.rb spec/react_on_rails/generators/js_dependency_manager_spec.rb)` - [x] Pre-push hook: branch Ruby RuboCop on changed Ruby files <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes RSC config auto-migration control flow and npm publish package.json handling; mistakes could corrupt webpack configs or leave bad publish manifests, though new specs cover the release path. > > **Overview** > This PR tightens **RSC webpack config migration** logic and documents **Node renderer sandboxing**, plus small **release** and **generator** hardening. > > **RSC clientReferences migration** renames `rsc_plugin_without_client_references?` to **`any_rsc_plugin_missing_client_references?`** and documents that it is an existential check (not the complement of “all plugins define clientReferences”). **`prepare_rsc_client_references_rewrite!`** replaces the old prepare helper so helper injection and rewrite decisions stay correct when some plugins already use scoped `clientReferences`. **`rsc_plugin_sections_safe_to_rewrite?`** now takes **`is_server:`** when counting unparseable sections. Lightweight JS scanner limits and caller contracts are **centralized on `advance_js_scan_state`**, with extra comments on defensive duplicate-helper guards. > > **Node renderer config** comments add explicit **SECURITY** notes that **`supportModules: true`** and a plain-object **`additionalContext`** both enable CommonJS wrapping and host **`require`**. > > **npm publish** rewrites `package.json` via a **same-directory temp file and atomic rename**, sets the restore **`changed`** flag only after rename succeeds, and adds a spec for failed rename cleanup. > > **JS dependency fallback** emits a clearer warning listing **`name@version`** pins written to `package.json` when install fails. The webpack helper test description is made path-agnostic. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 92f41ed. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * More robust npm publishing via atomic package.json replacement. * Improved React Server Components setup handling for multi-plugin configurations. * **Documentation** * Expanded security notes for supportModules and sandboxing behavior. * **Improvements** * Clearer, better-formatted package manager warning messages. * **Tests** * Updated/added tests and path-agnostic test descriptions for related warnings. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/shakacode/react_on_rails/pull/3343?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ab50dec commit 26ab212

8 files changed

Lines changed: 165 additions & 49 deletions

File tree

packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,23 @@ export interface Config {
5050
// See docs/oss/building-features/node-renderer/js-configuration.md#runtime-globals-for-ssr-and-rsc.
5151
// This option is required to equal `true` if you want to use loadable components.
5252
// Setting this value to false causes the NodeRenderer to behave like ExecJS.
53+
// SECURITY: When `supportModules: true`, the renderer also wraps the bundle and injects the
54+
// host `require` regardless of `additionalContext`. See the detailed `additionalContext`
55+
// security note below.
5356
supportModules: boolean;
5457
// additionalContext enables you to specify additional NodeJS objects (usually from
5558
// https://nodejs.org/api/globals.html) to add to the VM context in addition to our supportModules defaults.
5659
// Object shorthand notation may be used, but is not required.
5760
// Example: { URL, URLSearchParams, Crypto }
58-
// NOTE: Any plain object value (including an empty `{}`) puts the renderer into CommonJS
59-
// execution mode, wrapping the bundle and granting it access to the host's `require`
60-
// (e.g. `require('fs')`, `require('child_process')`). An empty `{}` is treated the same
61-
// as any other plain object -- it opts into CommonJS mode even though it adds no globals.
62-
// Use only with trusted bundle sources.
61+
// SECURITY: Any plain object value (including an empty `{}`) puts the renderer into CommonJS
62+
// execution mode. The bundle is wrapped via `module.wrap()` and receives the host process's
63+
// unrestricted `require`, granting full access to Node.js built-ins such as `fs`,
64+
// `child_process`, and `os`. This disables VM sandboxing for the bundle, even when no globals
65+
// are added. Only use with fully trusted, first-party bundle sources.
6366
// To keep the VM sandboxed without `require`, set BOTH `additionalContext: null` AND
64-
// `supportModules: false`. When `supportModules: true`, the renderer wraps the bundle and
65-
// injects the host `require` regardless of `additionalContext`.
67+
// `supportModules: false`.
68+
// SECURITY: When `supportModules: true`, the renderer also wraps the bundle and injects the
69+
// host `require` regardless of `additionalContext`.
6670
// Mechanically, "wrapping" means the renderer passes the bundle source through `module.wrap()`
6771
// (the standard CommonJS `(function (exports, require, module, __filename, __dirname) { ... })`
6872
// wrapper) and then invokes the wrapped function with the host `require`. See the `buildVM`

packages/react-on-rails/tests/webpackHelpers.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('webpackHelpers', () => {
88
expect(reactDomClientWarning.test(message)).toBe(true);
99
});
1010

11-
it('matches the webpack 4 "Module not found" warning for react-dom/client', () => {
11+
it('matches the "Module not found" warning regardless of file path', () => {
1212
const message =
1313
"Module not found: Error: Can't resolve 'react-dom/client' in '/app/node_modules/react-on-rails/src'";
1414
expect(reactDomClientWarning.test(message)).toBe(true);

rakelib/release.rake

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -980,14 +980,35 @@ def replace_workspace_protocol_dependencies_for_publish!(package_json, package_v
980980
changed
981981
end
982982

983+
def write_publishable_package_json(package_json_path, package_json)
984+
tmp = Tempfile.create(["package-json-", ".json"], File.dirname(package_json_path))
985+
tmp_path = tmp.path
986+
renamed = false
987+
988+
begin
989+
tmp.write("#{JSON.pretty_generate(package_json)}\n")
990+
tmp.chmod(File.stat(package_json_path).mode & 0o777)
991+
tmp.close
992+
File.rename(tmp_path, package_json_path)
993+
renamed = true
994+
ensure
995+
tmp.close unless tmp.closed?
996+
File.unlink(tmp_path) if !renamed && File.exist?(tmp_path)
997+
end
998+
end
999+
9831000
def with_publishable_package_json(dir, package_version)
9841001
package_json_path = File.join(dir, "package.json")
9851002
changed = false
9861003
original_content = File.read(package_json_path)
9871004
package_json = JSON.parse(original_content)
9881005

989-
changed = replace_workspace_protocol_dependencies_for_publish!(package_json, package_version)
990-
File.write(package_json_path, "#{JSON.pretty_generate(package_json)}\n") if changed
1006+
if replace_workspace_protocol_dependencies_for_publish!(package_json, package_version)
1007+
write_publishable_package_json(package_json_path, package_json)
1008+
# Only flip `changed` after the atomic same-directory rename succeeds so the `ensure`
1009+
# restore runs only after package.json was actually replaced.
1010+
changed = true
1011+
end
9911012

9921013
yield
9931014
ensure
@@ -1027,10 +1048,10 @@ def fetch_npm_package_metadata_with_retries(package_ref, registry_url:, attempts
10271048

10281049
last_output = output
10291050
last_status = status
1030-
next if attempt == attempts - 1
1031-
1032-
puts "npm did not return #{package_ref} yet; retrying in #{retry_delay_seconds} seconds..."
1033-
sleep retry_delay_seconds
1051+
unless attempt == attempts - 1
1052+
puts "npm did not return #{package_ref} yet; retrying in #{retry_delay_seconds} seconds..."
1053+
sleep retry_delay_seconds
1054+
end
10341055
end
10351056

10361057
[last_output, last_status]

react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,12 +578,19 @@ def write_versioned_package_specs_to_package_json(packages, dev:)
578578
end
579579

580580
File.write("package.json", "#{JSON.pretty_generate(content)}\n")
581+
GeneratorMessages.add_warning(package_json_pin_fallback_warning(versioned_packages))
581582
true
582583
rescue StandardError => e
583584
GeneratorMessages.add_warning("⚠️ Could not write dependency pins to package.json: #{e.message}")
584585
false
585586
end
586587

588+
def package_json_pin_fallback_warning(versioned_packages)
589+
pinned_list = versioned_packages.map { |name, version| "#{name}@#{version}" }.join(", ")
590+
"⚠️ Package manager install failed. Wrote the following version pins to package.json " \
591+
"so you can rerun your package manager manually: #{pinned_list}"
592+
end
593+
587594
def fallback_package_manager
588595
package_manager = GeneratorMessages.detect_package_manager(app_root: destination_root)
589596
return package_manager if GeneratorMessages.supported_package_manager?(package_manager)

react_on_rails/lib/generators/react_on_rails/rsc_setup/client_references.rb

Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -121,41 +121,42 @@ def js_line_ending(content)
121121
end
122122

123123
def update_existing_rsc_webpack_config(config_path, content, is_server:)
124-
return unless rsc_plugin_sections_safe_to_rewrite?(config_path, content)
124+
return unless rsc_plugin_sections_safe_to_rewrite?(config_path, content, is_server: is_server)
125125
return if rsc_plugin_uses_scoped_client_references?(content, is_server: is_server)
126-
127-
# May inject the scoped helper before the rewrite step re-reads the config from disk.
128-
return unless prepare_rsc_client_references_setup(config_path, content, is_server: is_server)
129-
return unless rsc_plugin_needs_client_references_rewrite?(content, is_server: is_server)
126+
return unless prepare_rsc_client_references_rewrite!(config_path, content, is_server: is_server)
130127

131128
return if rewrite_rsc_plugin_client_references(config_path, is_server: is_server)
132129

133130
rollback_incomplete_rsc_client_references_setup(config_path, content)
134131
warn_missing_rsc_plugin_target(config_path, is_server: is_server)
135132
end
136133

137-
def prepare_rsc_client_references_setup(config_path, content, is_server:)
134+
def prepare_rsc_client_references_rewrite!(config_path, content, is_server:)
135+
# This prepares the rewrite and returns whether it should continue. When it returns true,
136+
# the scoped helper may already have been injected on disk so
137+
# `rewrite_rsc_plugin_client_references` can re-read fresh content with valid offsets.
138138
if rsc_plugin_references_any_scoped_client_references?(content, is_server: is_server)
139-
return ensure_rsc_client_references_setup(config_path, content, is_server: is_server)
139+
return false unless ensure_rsc_client_references_setup(config_path, content, is_server: is_server)
140+
141+
return rsc_plugin_needs_client_references_rewrite?(content, is_server: is_server)
140142
end
141143

142144
rewritable_rsc_plugin?(config_path, content, is_server: is_server) &&
143145
ensure_rsc_client_references_setup(config_path, content, is_server: is_server)
144146
end
145147

146148
def rsc_plugin_needs_client_references_rewrite?(content, is_server:)
147-
rsc_plugin_without_client_references?(content, is_server: is_server)
149+
any_rsc_plugin_missing_client_references?(content, is_server: is_server)
148150
end
149151

150152
# Detects RSCWebpackPlugin option blocks that the lightweight JS scanner could not parse
151153
# cleanly (most often a regex literal with an unmatched `{` / `}` that walks the depth
152154
# counter past the real closing brace). When found, we warn and refuse to rewrite anything
153155
# in the file so a sibling rewrite cannot accidentally splice into a wrong location.
154-
def rsc_plugin_sections_safe_to_rewrite?(config_path, content)
155-
# The unparseable count is file-wide: the partition increments it for every invocation
156-
# that cannot be parsed, regardless of the `is_server` argument. The argument only
157-
# filters the target-specific safe bucket, which is ignored here.
158-
unparseable = rsc_plugin_option_sections_partition(content, is_server: true).fetch(:unparseable)
156+
def rsc_plugin_sections_safe_to_rewrite?(config_path, content, is_server:)
157+
# `:unparseable` is file-wide: the partitioner increments it before filtering parseable
158+
# sections by the current `is_server` target.
159+
unparseable = rsc_plugin_option_sections_partition(content, is_server: is_server).fetch(:unparseable)
159160
return true if unparseable.zero?
160161

161162
warn_unparseable_rsc_plugin_sections(config_path, unparseable)
@@ -165,7 +166,7 @@ def rsc_plugin_sections_safe_to_rewrite?(config_path, content)
165166
def rewritable_rsc_plugin?(config_path, content, is_server:)
166167
# Mixed same-target plugins are still rewritable: the later rewrite only updates plugins
167168
# missing clientReferences and leaves sibling custom clientReferences untouched.
168-
return true if rsc_plugin_without_client_references?(content, is_server: is_server)
169+
return true if any_rsc_plugin_missing_client_references?(content, is_server: is_server)
169170

170171
if rsc_plugin_defines_client_references?(content, is_server: is_server)
171172
GeneratorMessages.add_warning(
@@ -263,7 +264,12 @@ def rsc_plugin_defines_client_references?(content, is_server:)
263264
end
264265
end
265266

266-
def rsc_plugin_without_client_references?(content, is_server:)
267+
# Existential check: returns true when at least one matching plugin section is missing a
268+
# top-level `clientReferences:` key. Pairs with `rsc_plugin_defines_client_references?`,
269+
# which uses the same any-section semantics for the opposite condition. The two are not
270+
# complements when multiple plugin sections exist — a file with one configured plugin and
271+
# one unconfigured plugin returns true from both.
272+
def any_rsc_plugin_missing_client_references?(content, is_server:)
267273
rsc_plugin_option_sections(content, is_server: is_server).any? do |section|
268274
!rsc_plugin_body_has_top_level_key?(section.fetch(:body), "clientReferences")
269275
end
@@ -273,8 +279,8 @@ def rsc_plugin_without_client_references?(content, is_server:)
273279
# so `clientReferences:` / `isServer:` substrings inside strings are not mis-detected.
274280
# Shares the `advance_js_scan_state` family used by `js_top_level_position?` and
275281
# `matching_js_closing_brace` so all JS-aware passes follow the same comment/string rules.
276-
# Regex literals (e.g. `/a{2}/`) are still outside this scanner's supported surface
277-
# because brace quantifiers can confuse `matching_js_closing_brace`'s depth counter.
282+
# See `advance_js_scan_state` for the scanner's supported surface (including the regex-
283+
# literal and nested-template-literal limits that callers must be aware of).
278284
def rsc_plugin_options_without_comments(options)
279285
result = String.new(capacity: options.length)
280286
state = nil
@@ -434,15 +440,12 @@ def first_significant_js_index(content, start_index)
434440
end
435441

436442
# Expects `content[open_index] == "{"`; callers pass the options-object opening brace.
437-
# This lightweight scanner treats template literals as opaque strings (backtick to backtick).
438-
# Simple `${...}` expressions are handled correctly: while in the backtick state every
439-
# character — including `{` and `}` inside the expression — is consumed as string content
440-
# and never reaches the depth counter. The real unsupported case is *nested* template
441-
# literals (e.g. `` `outer ${`inner`}` ``) where the inner backtick falsely closes the outer
442-
# string state, exposing later braces to the depth counter. Callers detect that via
443-
# `rsc_plugin_options_followed_by_close_paren?` and mark the section unparseable rather
444-
# than producing a corrupt rewrite. Regex literals are outside this scanner's supported
445-
# surface for the same reason.
443+
# See `advance_js_scan_state` for the scanner's supported surface — in short, simple
444+
# `${...}` interpolations inside template literals stay inside the string state, while
445+
# nested template literals and regex literals fall outside the scanner. When the depth
446+
# counter is confused by either, the section is caught downstream via
447+
# `rsc_plugin_options_followed_by_close_paren?` and marked unparseable so the migration
448+
# warns the user instead of corrupting the rewrite.
446449
def matching_js_closing_brace(content, open_index)
447450
depth = 0
448451
index = open_index
@@ -475,8 +478,51 @@ def matching_js_closing_brace(content, open_index)
475478
nil
476479
end
477480

478-
# Return index is the last consumed character. Line comments leave the newline
479-
# for the caller's normal index increment; block comments consume the closing slash.
481+
# Central dispatcher for the lightweight JS scanner shared by every JS-aware pass in this
482+
# generator (`matching_js_closing_brace`, `js_top_level_position?`, `js_code_position?`,
483+
# `rsc_plugin_options_without_comments`, `first_significant_js_index`,
484+
# `rsc_plugin_options_followed_by_close_paren?`, `last_js_code_char_index`,
485+
# `last_js_code_line_start`). Return index is the last consumed character. Line comments
486+
# leave the newline for the caller's normal index increment; block comments consume the
487+
# closing slash.
488+
#
489+
# Supported lexical constructs:
490+
# - Line comments (`// ...\n`) and block comments (`/* ... */`).
491+
# - Single-quoted (`'...'`), double-quoted (`"..."`), and template-literal (`` `...` ``)
492+
# strings, including escape sequences and the simple `${expr}` interpolation form
493+
# (interpolation braces stay inside the string state and never reach the depth counter).
494+
#
495+
# Outside the supported surface for callers that do **not** run `js_regex_literal_start?`
496+
# preprocessing (e.g. `matching_js_closing_brace`, `rsc_plugin_options_without_comments`),
497+
# the scanner cannot distinguish these from the syntax they shadow, so `{`/`}` characters
498+
# they contain can confuse the depth counter. `js_top_level_position?` and
499+
# `js_code_position?` handle regex literals via their own pre-pass and are unaffected.
500+
# - Regex literals (e.g. `/a{2}/`, `/\{/`, `/[{]/`): not recognized as a distinct state,
501+
# so brace-containing patterns walk the depth counter past the real options close. The
502+
# user-facing warning text in `warn_unparseable_rsc_plugin_sections` calls these out
503+
# explicitly.
504+
# - Nested template literals (`` `outer ${`inner`}` ``): the inner backtick falsely closes
505+
# the outer string state, exposing later braces to the depth counter.
506+
#
507+
# The downstream `rsc_plugin_option_sections_partition` catches both failure modes by
508+
# requiring the matched closing `}` to be followed by `)`. When it isn't, the section is
509+
# marked unparseable and `warn_unparseable_rsc_plugin_sections` asks the user to add
510+
# `clientReferences:` manually — the migration declines to rewrite rather than risk
511+
# corrupting the config.
512+
#
513+
# Future expansion (only worth doing if a real-world RSC plugin options block needs it):
514+
# 1. Add a `:regex_literal` state alongside the string and comment states. Track regex
515+
# contexts by detecting `/` after a token that legally precedes a regex literal
516+
# (`=`, `(`, `,`, `:`, `;`, `?`, `!`, `&&`, `||`, `return`, `typeof`, etc.) and consume
517+
# until the unescaped closing `/` plus any flags. The token-context check is necessary
518+
# because the same `/` character means division in expression position.
519+
# 2. Add a stack-based template-literal state so nested `` `...${`inner`}...` `` pairs
520+
# track depth instead of toggling a single boolean state.
521+
# Regex literals require expanding `advance_js_default_scan_state`; nested template
522+
# literals would also require replacing `advance_js_string_state` with stack-aware
523+
# handling. Both changes need a new state-machine branch; the current callers were
524+
# specifically designed around the simpler scanner and would need re-validation against
525+
# the expanded state set.
480526
#
481527
# IMPORTANT CALLER CONTRACT — block-comment exit:
482528
# When this returns from a `*/` exit, `state` is cleared, but `char` is still the `*` and
@@ -915,9 +961,16 @@ def rsc_client_references_setup_anchor?(content, is_server:)
915961
# this helper deliberately omits the `RSCWebpackPlugin` import that `inject_rsc_*_imports`
916962
# adds on the from-scratch path — adding it here would produce a duplicate import.
917963
def add_rsc_client_references_setup(config_path, content, existing_imports_content, is_server:)
918-
# Belt-and-suspenders: the only caller, `ensure_rsc_client_references_setup`, already
919-
# checks both `scoped_rsc_client_references_defined?` and `rsc_client_references_defined?`
920-
# before delegating here. The guards are kept so the helper is safe to call directly.
964+
# The only caller, `ensure_rsc_client_references_setup`, already runs these same checks
965+
# before delegating here, so in normal flow both conditions evaluate to `false` and no
966+
# early return is triggered. They are kept (rather than deleted) so a future second
967+
# caller — or a refactor that bypasses `ensure_rsc_client_references_setup` — cannot
968+
# accidentally splice a second `const rscClientReferences = { ... }` into a file that
969+
# already declares one. JavaScript would reject that with an
970+
# `Identifier 'rscClientReferences' has already been declared` SyntaxError at config
971+
# load, and the cost of the duplicate check is two boolean ops on the already-loaded
972+
# file body. Leaving the method defensive is cheaper than re-deriving the precondition
973+
# at each new call site.
921974
return false if scoped_rsc_client_references_defined?(content)
922975
return false if rsc_client_references_defined?(content)
923976

react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,16 @@ def errors
228228
expect(package_json.dig("dependencies", "react-on-rails-pro")).to eq("16.7.0-rc.1")
229229
end
230230

231+
it "formats the package.json pin fallback warning with name@version package specs" do
232+
warning = instance.send(
233+
:package_json_pin_fallback_warning,
234+
[["react", "18.0.0"], ["react-dom", "18.0.0"]]
235+
)
236+
237+
expect(warning).to include("react@18.0.0, react-dom@18.0.0")
238+
expect(warning).to include("Wrote the following version pins to package.json")
239+
end
240+
231241
it "does not write unversioned dependencies to package.json when package-manager install fails" do
232242
File.write("package.json", "#{JSON.pretty_generate({ 'dependencies' => {} })}\n")
233243
instance.add_npm_dependencies_result = false

0 commit comments

Comments
 (0)