Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: switch to using package_json for interacting with package.json #466

Merged
merged 20 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b7403ae
feat: switch to using `package_json` for interacting with `package.json`
G-Rath Jan 26, 2024
f7be95b
refactor: replace direct reads of `package.json` with `package_json` …
G-Rath Jan 26, 2024
a2ebea5
fix: use `PackageJson` to generate native commands for scripts
G-Rath Aug 12, 2023
fd1cbf2
feat: allow using any js package manager
G-Rath Aug 18, 2023
8033f3c
ci: test against the three main package managers
G-Rath Aug 12, 2023
b3fe2cf
fix: don't `sort-package-json` for now
G-Rath Aug 12, 2023
db67841
ci: get rid of node cache all together
G-Rath Aug 15, 2023
98cb7d0
ci: use a ruby script to create fake js package managers
G-Rath Aug 15, 2023
dc878cd
fix: use bang manager methods
G-Rath Aug 16, 2023
c69482c
chore: use `direnv` to set up fake js package managers locally
G-Rath Aug 26, 2023
8b68756
feat: support Yarn Berry
G-Rath Aug 26, 2023
89715af
fix: update how Jest major version is determined
G-Rath Aug 26, 2023
471ab18
fix: add missing dependencies
G-Rath Aug 26, 2023
8513f46
feat: use new headless rendering mode
G-Rath Aug 26, 2023
ad38604
fix: always copy a babel config
G-Rath Aug 26, 2023
93a71bf
fix: support lighthouse with Yarn Berry
G-Rath Aug 27, 2023
4d0c698
feat: support `bun` as a package manager
G-Rath Sep 15, 2023
5ea3949
ci: use specific version of osv-detector to avoid rate limiting
G-Rath Nov 17, 2023
52b804e
ci: reduce noise from `npm install`
G-Rath Mar 8, 2024
e252e48
fix: add missing package dependency extensions for Yarn Berry with PnP
G-Rath Apr 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ concurrency:
# should be granted per job as needed using a dedicated `permissions` block
permissions: {}

env:
# reduces noise from npm install
DISABLE_OPENCOLLECTIVE: true
OPEN_SOURCE_CONTRIBUTOR: true
NPM_CONFIG_FUND: false
NPM_CONFIG_AUDIT: false

jobs:
audit_dependencies:
permissions:
Expand All @@ -29,6 +36,8 @@ jobs:
persist-credentials: false
- name: Audit dependencies for security vulnerabilities
uses: g-rath/check-with-osv-detector@main
with:
osv-detector-version: 0.13.0
test:
permissions:
contents: read
Expand Down Expand Up @@ -70,6 +79,24 @@ jobs:
# how many fail)
fail-fast: false
matrix:
js_package_manager:
- name: npm
installer: npm
- name: yarn_berry
installer: yarn
linker: pnp
- name: yarn_berry
installer: yarn
linker: node-modules
- name: yarn_berry
installer: yarn
linker: pnpm
- name: yarn_classic
installer: yarn
- name: pnpm
installer: pnpm
- name: bun
installer: bun
variant:
- name: defaults
config_path: 'ackama_rails_template.config.yml'
Expand Down Expand Up @@ -131,6 +158,8 @@ jobs:
# this ensures that osv-detector is available for running bin/ci-run
- name: Check dependencies for vulnerabilities (and setup osv-detector)
uses: g-rath/check-with-osv-detector@main
with:
osv-detector-version: 0.13.0

# this ensures that actionlint is available for running bin/ci-run
- name: Setup ActionLint
Expand All @@ -142,9 +171,11 @@ jobs:
- name: Install NodeJS
uses: actions/setup-node@v4
with:
cache: 'yarn'
node-version-file: '.node-version'

- name: install package manager
run: npm i -g ${{ matrix.js_package_manager.installer }}

# We don't cache gems or JS packages because we are actually testing how
# installation and setup works in this project so, while caching would
# make CI faster, it might hide problems.
Expand All @@ -161,6 +192,9 @@ jobs:
git config --global user.email "[email protected]"
git config --global user.name "Your Name"

# prettier-ignore
- run: ./ci/bin/create-fake-js-package-managers ${{ matrix.js_package_manager.installer }}

- name: Run CI script
env:
# Remember that your app name becomes a top-level constant in the
Expand All @@ -173,4 +207,7 @@ jobs:
PGUSER: postgres
PGPASSWORD: postgres
PGHOST: localhost
PACKAGE_JSON_FALLBACK_MANAGER: ${{ matrix.js_package_manager.name }}
PACKAGE_JSON_YARN_BERRY_LINKER:
${{ matrix.js_package_manager.linker }}
run: ./ci/bin/build-and-test
62 changes: 62 additions & 0 deletions ci/bin/create-fake-js-package-managers
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realised while doing the tests for Shakapacker that I could have the bin scripts as Ruby files instead of JS, and that I didn't necessarily need to modify PATH in the manner this script currently does since we're running things via Ruby - so I should be able to just modify the PATH variable in memory for the actual Ruby system calls that run the external commands.

I think what I've got currently would be fine to land but if I have a chance I might look into refactoring this to see if the result is cleaner - here's where I'm doing it in Shakapacker for reference: https://github.com/shakacode/shakapacker/pull/349/files#diff-c1038565809dc87afa90260271577a6eb687b351413beba0ff487d1b9e5b8494R307

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env ruby

# creates a set of fake JavaScript package managers in a temporary bin
# directory for GitHub Actions, _excluding_ the one passed in as an
# argument in order to assert that only that package manager is used

require "fileutils"
require "tmpdir"

# setup the bin directory we want to use
bin_dir = "tmp/fake-bin"

if ENV["GITHUB_ACTIONS"]
bin_dir = Dir.mktmpdir("rails-template-")

puts "adding #{bin_dir} to GITHUB_PATH..."

File.write(ENV.fetch("GITHUB_PATH"), "#{bin_dir}\n", mode: "a+")
elsif system("direnv --version > /dev/null 2>&1")
envrc_content = "PATH_add #{bin_dir}\n"

if File.exist?(".envrc") && File.read(".envrc").include?(envrc_content)
puts "'#{envrc_content.strip}' already exists in .envrc"
else
File.write(".envrc", envrc_content, mode: "a")
puts "Added '#{envrc_content.strip}' to .envrc"
end

# ensure the .envrc is allowed
system("direnv allow")
end

managers = %w[npm yarn pnpm bun]
manager_in_use = ARGV[0] || ""

if manager_in_use.empty?
manager_in_use = ENV.fetch("PACKAGE_JSON_FALLBACK_MANAGER", "")
.delete_suffix("_berry")
.delete_suffix("_classic")
end

Dir.chdir(bin_dir) do
managers.each do |manager|
if manager == manager_in_use
# ensure that the manager is not stubbed in case we've changed managers
FileUtils.rm_f(manager)

next
end

puts "creating #{bin_dir}/#{manager}..."
File.write(
manager,
<<~CONTENTS
#!/usr/bin/env node

throw new Error("(#{manager}) this is not the package manager you're looking for...");
CONTENTS
)
File.chmod(0o755, manager)
end
end
138 changes: 110 additions & 28 deletions template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ def puts_header(msg)
TEMPLATE_CONFIG = Config.new
TERMINAL = Terminal.new

# We need the major version of 'jest', '@types/jest', and 'ts-jest' to match
# so we can only upgrade jest when there are compatible versions available
JEST_MAJOR_VERSION = "29".freeze

def require_package_json_gem
require "bundler/inline"

gemfile(true) do
source "https://rubygems.org"
gem "package_json"
end

puts "using package_json v#{PackageJson::VERSION}"
end

def apply_template! # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
assert_minimum_rails_version
assert_valid_options
Expand Down Expand Up @@ -121,14 +136,19 @@ def apply_template! # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Met
apply "variants/backend-base/bin/template.rb"
apply "variants/backend-base/config/template.rb"
apply "variants/backend-base/doc/template.rb"
apply "variants/backend-base/lib/template.rb"
apply "variants/backend-base/public/template.rb"
apply "variants/backend-base/spec/template.rb"

# The block passed to "after_bundle" seems to run after `bundle install`
# but also after `shakapacker:install` and after Rails has initialized the git
# repo
after_bundle do # rubocop:disable Metrics/BlockLength
require_package_json_gem

apply "variants/backend-base/lib/template.rb"

template "variants/backend-base/bin/setup.tt", "bin/setup", force: true

# Remove the `test/` directory because we always use RSpec which creates
# its own `spec/` directory
remove_dir "test"
Expand All @@ -150,11 +170,11 @@ def apply_template! # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Met
apply "variants/frontend-bootstrap-typescript/template.rb" if TEMPLATE_CONFIG.apply_variant_bootstrap?
apply "variants/frontend-react-typescript/template.rb" if TEMPLATE_CONFIG.apply_variant_react?

run "yarn run typecheck"
package_json.manager.run!("typecheck")
end

# apply any js linting fixes after all frontend variants have run
run "yarn run js-lint-fix"
package_json.manager.run!("js-lint-fix")

create_initial_migration

Expand Down Expand Up @@ -235,11 +255,75 @@ def apply_readme_template
end

def apply_prettier_all_over
run "yarn run format-fix"
package_json.manager.run!("format-fix")

git commit: ". -m 'Run prettier one last time'"
end

# Sets Yarn Berry up in the project directory by initializing it with what is probably Yarn Classic.
#
# This is required as the Berry binary is actually downloaded and committed to the codebase, and
# the global yarn command passes through to it when detected (even if its Yarn Classic).
#
# This also requires us to temporarily create a package.json as otherwise Yarn Berry will
# look up the file tree and initialize itself in every directory that has a yarn.lock
def setup_yarn_berry
# safeguard against parent directories having a yarn.lock
File.write("package.json", "{}") unless File.exist?("package.json")

run "yarn init -2"
run "yarn config set enableGlobalCache true"
run "yarn config set nodeLinker #{ENV.fetch("PACKAGE_JSON_YARN_BERRY_LINKER", "node-modules")}"

ignores = <<~YARN
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
YARN
File.write(".gitignore", ignores, mode: "a")

# this will be properly (re)created later
File.unlink("package.json")
end

# Bun uses a binary-based lockfile which cannot be parsed by shakapacker or
# osv-detector, so we want to configure bun to always write a yarn.lock
# in addition so that such tools can check it
def setup_bun
File.write("bunfig.toml", <<~TOML)
[install.lockfile]
print = "yarn"
TOML
end

def add_yarn_package_extension_dependency(name, dependency)
return unless File.exist?(".yarnrc.yml")

require "yaml"

yarnrc = YAML.load_file(".yarnrc.yml")

yarnrc["packageExtensions"] ||= {}
yarnrc["packageExtensions"]["#{name}@*"] ||= {}
yarnrc["packageExtensions"]["#{name}@*"]["dependencies"] ||= {}
yarnrc["packageExtensions"]["#{name}@*"]["dependencies"][dependency] = "*"

File.write(".yarnrc.yml", yarnrc.to_yaml)
end

def package_json
if @package_json.nil?
setup_yarn_berry if ENV.fetch("PACKAGE_JSON_FALLBACK_MANAGER", nil) == "yarn_berry"
setup_bun if ENV.fetch("PACKAGE_JSON_FALLBACK_MANAGER", nil) == "bun"
end

@package_json ||= PackageJson.new
end

# Normalizes the constraints of the given hash of dependencies so that they
# all have an explicit constraint and define a minor & patch version
#
Expand All @@ -254,50 +338,48 @@ def normalize_dependency_constraints(deps)
end
end

def build_engines_field
def build_engines_field(existing)
node_version = File.read("./.node-version").strip
{
node: "^#{node_version}",
yarn: "^1.0.0"
}
end

def update_package_json(&)
package_json = JSON.load_file("./package.json").tap(&)

File.write("./package.json", "#{JSON.pretty_generate(package_json)}\n")
existing.merge({
"node" => "^#{node_version}",
"yarn" => "^1.0.0"
})
end

def cleanup_package_json
update_package_json do |package_json|
# ensure that the package name is set based on the folder
package_json["name"] = File.basename(__dir__)

# set engines constraint in package.json
package_json["engines"] = build_engines_field

# ensure that all dependency constraints are normalized
%w[dependencies devDependencies].each { |k| package_json[k] = normalize_dependency_constraints(package_json[k]) }
package_json.merge! do |pj|
{
"name" => File.basename(__dir__),
"engines" => build_engines_field(pj.fetch("engines", {})),
"dependencies" => normalize_dependency_constraints(pj.fetch("dependencies", {})),
"devDependencies" => normalize_dependency_constraints(pj.fetch("devDependencies", {}))
}
end

run "npx -y sort-package-json"
# TODO: this doesn't work when using pnpm even though it shouldn't matter? anyway, replace with 'exec' support
# run "npx -y sort-package-json"

# ensure the yarn.lock is up to date with any changes we've made to package.json
run "yarn install"
# ensure the lockfile is up to date with any changes we've made to package.json
package_json.manager.install!
end

# Adds the given <code>packages</code> as dependencies using <code>yarn add</code>
#
# @param [Array<String>] packages
def yarn_add_dependencies(packages)
run "yarn add #{packages.join " "}"
puts "adding #{packages.join(" ")} as dependencies"

package_json.manager.add!(packages)
end

# Adds the given <code>packages</code> as devDependencies using <code>yarn add --dev</code>
#
# @param [Array<String>] packages
def yarn_add_dev_dependencies(packages)
run "yarn add --dev #{packages.join " "}"
puts "adding #{packages.join(" ")} as dev dependencies"

package_json.manager.add!(packages, type: :dev)
end

# Add this template directory to source_paths so that Thor actions like
Expand Down
10 changes: 9 additions & 1 deletion variants/accessibility/spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
<<~OPTIONS
# Lighthouse Matcher options
options.add_argument("--remote-debugging-port=9222")
Lighthouse::Matchers.chrome_flags = %w[headless no-sandbox]
Lighthouse::Matchers.chrome_flags = %w[headless=new no-sandbox]
OPTIONS
end

if File.exist?(".yarnrc.yml")
insert_into_file "spec/rails_helper.rb", after: /# Lighthouse Matcher options\n/ do
<<~OPTIONS
Lighthouse::Matchers.lighthouse_cli = "yarn run lighthouse"
OPTIONS
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def setup!
test "ruby -v" => ruby_version
run "gem install bundler --no-document --conservative"
run "bundle install"
run "yarn install" if File.exist?("yarn.lock")
run "<%= package_json.manager.native_install_command.join(" ") %>" if File.exist?("package.json")
run "bundle exec overcommit --install" unless ENV["SKIP_OVERCOMMIT"] || ENV["CI"]
copy "example.env"
test_local_env_contains_required_keys
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace :assets do
desc "Ensures that dependencies required to compile assets are installed"
task install_dependencies: :environment do
raise if File.exist?("yarn.lock") && !(system "yarn install --frozen-lockfile")
raise if File.exist?("package.json") && !(system "<%= package_json.manager.native_install_command(frozen: true).join(" ") %>")
end
end

Expand Down
3 changes: 2 additions & 1 deletion variants/backend-base/lib/template.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
copy_file "variants/backend-base/lib/tasks/coverage.rake", "lib/tasks/coverage.rake"
copy_file "variants/backend-base/lib/tasks/assets.rake", "lib/tasks/assets.rake"
copy_file "variants/backend-base/lib/tasks/app.rake", "lib/tasks/app.rake"
copy_file "variants/backend-base/lib/tasks/dev.rake", "lib/tasks/dev.rake"

template "variants/backend-base/lib/tasks/assets.rake.tt", "lib/tasks/assets.rake", force: true
Loading
Loading