diff --git a/README.md b/README.md index dd59518..7fe8708 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,14 @@ without adding the updater as a dependency. For local development, run 1. Detect a plugin using `plugin.rb`, otherwise a theme using `about.json`. 2. Fetch current files from the matching skeleton's `raw.githubusercontent.com` `/main/` URLs, without using the GitHub API. -3. Merge package dependencies, scripts, and engine settings. Replace lint configs, - TypeScript config, Gemfile, and standard workflows; remove obsolete configs. - Append required `.gitignore` entries. Plugin TypeScript paths use its name header. +3. Merge package dependencies, scripts, and engine settings. Set the skeleton's + `pnpm-workspace.yaml` settings, keep the project's other settings, and add list + entries such as `minimumReleaseAgeExclude` to the project's own. Add the + skeleton's `.npmrc` settings that the project lacks, and remove + `auto-install-peers` and `strict-peer-dependencies`, which pnpm reads from + `pnpm-workspace.yaml`. Replace lint configs, TypeScript config, Gemfile, and + standard workflows; remove obsolete configs. Append required `.gitignore` + entries. Plugin TypeScript paths use its name header. 4. Install with pnpm, then run `bundle install`, `bundle update --bundler`, and `bundle update --all`. 5. Print lint and autofix commands for you to run. diff --git a/package.json b/package.json index 02019a1..3751427 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@discourse/update-skeleton", - "version": "0.1.0", + "version": "0.2.0", "description": "Update Discourse theme and plugin scaffolding and install its dependencies", "type": "module", "bin": { @@ -27,7 +27,8 @@ "access": "public" }, "dependencies": { - "execa": "^10.0.1" + "execa": "^10.0.1", + "yaml": "^2.9.1" }, "packageManager": "pnpm@10.28.0", "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 102340b..46fc96a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: execa: specifier: ^10.0.1 version: 10.0.1 + yaml: + specifier: ^2.9.1 + version: 2.9.1 devDependencies: '@discourse/lint-configs': specifier: 3.2.0 @@ -1587,6 +1590,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3204,6 +3212,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.9.1: {} + yocto-queue@0.1.0: {} yoctocolors@2.2.0: {} diff --git a/src/update.js b/src/update.js index 3e17bfb..a28e9f1 100644 --- a/src/update.js +++ b/src/update.js @@ -1,6 +1,13 @@ import { execa } from "execa"; import * as fs from "node:fs/promises"; import path from "node:path"; +import { parse, parseDocument } from "yaml"; + +// pnpm 11 and newer read these from pnpm-workspace.yaml, not .npmrc. +const OBSOLETE_NPMRC_SETTINGS = [ + "auto-install-peers", + "strict-peer-dependencies", +]; export async function update({ directory = process.cwd(), @@ -74,6 +81,21 @@ export async function update({ delete pkg.scripts["lint:hbs:fix"]; await write("package.json", `${JSON.stringify(pkg, null, 2)}\n`); + await write( + "pnpm-workspace.yaml", + mergeSettings( + await read("pnpm-workspace.yaml"), + await download("pnpm-workspace.yaml") + ) + ); + await write( + ".npmrc", + mergeLines(await read(".npmrc"), await download(".npmrc"), { + keyOf: npmrcSettingName, + remove: OBSOLETE_NPMRC_SETTINGS, + }) + ); + await fs.mkdir(path.join(directory, ".github/workflows"), { recursive: true, }); @@ -116,15 +138,10 @@ export async function update({ ]) { await fs.rm(path.join(directory, file), { force: true }); } - let ignore = (await read(".gitignore")) || ""; - for (const line of (await download(".gitignore")) - .split(/\r?\n/) - .filter(Boolean)) { - if (!ignore.split(/\r?\n/).includes(line)) { - ignore += `${ignore && !ignore.endsWith("\n") ? "\n" : ""}${line}\n`; - } - } - await write(".gitignore", ignore); + await write( + ".gitignore", + mergeLines(await read(".gitignore"), await download(".gitignore")) + ); // Keep registry/auth settings, but remove the calling package's metadata. const env = { ...process.env }; @@ -142,7 +159,6 @@ export async function update({ await run( "pnpm", [ - "--ignore-workspace", "--config.manage-package-manager-versions=true", "install", "--no-frozen-lockfile", @@ -160,3 +176,41 @@ export async function update({ bundle exec rubocop -A bundle exec rubocop`); } + +function mergeSettings(current, template) { + const document = parseDocument(current ?? ""); + const settings = document.toJS() ?? {}; + for (const [key, value] of Object.entries(parse(template))) { + document.set( + key, + Array.isArray(value) && Array.isArray(settings[key]) + ? [...new Set([...settings[key], ...value])] + : value + ); + } + return document.toString(); +} + +function mergeLines( + current, + template, + { keyOf = (line) => line.trimEnd(), remove = [] } = {} +) { + const lines = (current ?? "") + .split("\n") + .filter((line) => !remove.includes(keyOf(line))); + if (lines.at(-1) === "") { + lines.pop(); + } + const keys = new Set(lines.map(keyOf)); + for (const line of template.split("\n")) { + if (line && !keys.has(keyOf(line))) { + lines.push(line); + } + } + return `${lines.join("\n")}\n`; +} + +function npmrcSettingName(line) { + return line.split("=")[0].trim(); +} diff --git a/test/fixtures/plugin.json b/test/fixtures/plugin.json index 00a51bf..b1d9804 100644 --- a/test/fixtures/plugin.json +++ b/test/fixtures/plugin.json @@ -2,16 +2,18 @@ "type": "plugin", "repository": "discourse/discourse-plugin-skeleton", "files": { - "package.json": "{\n \"private\": true,\n \"devDependencies\": {\n \"@discourse/lint-configs\": \"3.2.0\",\n \"@glint/ember-tsc\": \"1.8.11\",\n \"concurrently\": \"^9.2.3\",\n \"discourse\": \"npm:@discourse/types@2026.3.0-d02b2966\",\n \"eslint\": \"10.6.0\",\n \"lint-to-the-future\": \"^2.6.4\",\n \"lint-to-the-future-eslint\": \"^3.3.0\",\n \"prettier\": \"3.8.1\",\n \"stylelint\": \"17.5.0\"\n },\n \"scripts\": {\n \"lint\": \"concurrently \\\"pnpm:lint:*(!fix)\\\" --names \\\"lint:\\\"\",\n \"lint:fix\": \"concurrently \\\"pnpm:lint:*:fix\\\" --names \\\"fix:\\\"\",\n \"lint:css\": \"pnpm stylelint 'assets/stylesheets/**/*.scss' --allow-empty-input\",\n \"lint:css:fix\": \"pnpm stylelint 'assets/stylesheets/**/*.scss' --fix --allow-empty-input\",\n \"lint:js\": \"eslint {assets,admin/assets,test}/javascripts --cache --no-error-on-unmatched-pattern\",\n \"lint:js:fix\": \"eslint {assets,admin/assets,test}/javascripts --fix --no-error-on-unmatched-pattern\",\n \"lint:prettier\": \"pnpm prettier 'assets/stylesheets/**/*.scss' '{assets,admin/assets,test}/javascripts/**/*.{js,gjs,ts,gts,mts,cts}' --check --no-error-on-unmatched-pattern\",\n \"lint:prettier:fix\": \"pnpm prettier 'assets/stylesheets/**/*.scss' '{assets,admin/assets,test}/javascripts/**/*.{js,gjs,ts,gts,mts,cts}' -w --no-error-on-unmatched-pattern\",\n \"lint:types\": \"ember-tsc -b\",\n \"lttf:ignore\": \"lint-to-the-future ignore\",\n \"update-skeleton\": \"pnpx @discourse/update-skeleton@latest\"\n },\n \"engines\": {\n \"node\": \">= 22\",\n \"npm\": \"please-use-pnpm\",\n \"yarn\": \"please-use-pnpm\",\n \"pnpm\": \"^10\"\n },\n \"packageManager\": \"pnpm@10.28.0\"\n}\n", + "package.json": "{\n \"private\": true,\n \"devDependencies\": {\n \"@discourse/lint-configs\": \"3.8.2\",\n \"@glint/ember-tsc\": \"1.11.6\",\n \"concurrently\": \"^9.2.3\",\n \"discourse\": \"npm:@discourse/types@2026.10.0-a5125163\",\n \"ember-content-mapper\": \"0.3.1\",\n \"eslint\": \"10.6.0\",\n \"lint-to-the-future\": \"^3.2.0\",\n \"lint-to-the-future-eslint\": \"^4.0.0\",\n \"prettier\": \"3.8.1\",\n \"stylelint\": \"17.5.0\",\n \"typescript\": \"7.1.0-dev.20260930.4\"\n },\n \"scripts\": {\n \"lint\": \"concurrently \\\"pnpm:lint:*(!fix)\\\" --names \\\"lint:\\\"\",\n \"lint:fix\": \"concurrently \\\"pnpm:lint:*:fix\\\" --names \\\"fix:\\\"\",\n \"lint:css\": \"pnpm stylelint 'assets/stylesheets/**/*.scss' --allow-empty-input\",\n \"lint:css:fix\": \"pnpm stylelint 'assets/stylesheets/**/*.scss' --fix --allow-empty-input\",\n \"lint:js\": \"eslint assets/javascripts admin/assets/javascripts test/javascripts --cache --no-error-on-unmatched-pattern\",\n \"lint:js:fix\": \"eslint assets/javascripts admin/assets/javascripts test/javascripts --fix --no-error-on-unmatched-pattern\",\n \"lint:prettier\": \"pnpm prettier 'assets/stylesheets/**/*.scss' '{assets,admin/assets,test}/javascripts/**/*.{js,gjs,ts,gts,mts,cts}' --check --no-error-on-unmatched-pattern\",\n \"lint:prettier:fix\": \"pnpm prettier 'assets/stylesheets/**/*.scss' '{assets,admin/assets,test}/javascripts/**/*.{js,gjs,ts,gts,mts,cts}' -w --no-error-on-unmatched-pattern\",\n \"lint:types\": \"tsc -b --runExternalCode\",\n \"lttf:ignore\": \"lint-to-the-future ignore\",\n \"update-skeleton\": \"pnpx @discourse/update-skeleton@latest\"\n },\n \"engines\": {\n \"node\": \">= 22\",\n \"npm\": \"please-use-pnpm\",\n \"yarn\": \"please-use-pnpm\",\n \"pnpm\": \"^12\"\n },\n \"packageManager\": \"pnpm@12.11.2\"\n}\n", ".gitignore": "node_modules\n/gems\n/auto_generated\n.eslintcache\n", "eslint.config.mjs": "import DiscourseRecommended from \"@discourse/lint-configs/eslint\";\n\nexport default [...DiscourseRecommended];\n", ".prettierrc.cjs": "module.exports = require(\"@discourse/lint-configs/prettier\");\n", "stylelint.config.mjs": "export default {\n extends: [\"@discourse/lint-configs/stylelint\"],\n};\n", - "tsconfig.json": "{\n \"extends\": \"discourse/tsconfig-plugin\",\n \"include\": [\n \"admin/assets/javascripts/**/*\",\n \"assets/javascripts/**/*\",\n \"test/javascripts/**/*\",\n \"node_modules/discourse/declarations/**/*\"\n ],\n \"compilerOptions\": {\n \"paths\": {\n \"discourse/plugins/discourse-plugin-skeleton/*\": [\n \"./assets/javascripts/*\"\n ],\n \"discourse/plugins/discourse-plugin-skeleton/admin/*\": [\n \"./admin/assets/javascripts/admin/*\"\n ],\n \"admin/*\": [\"./node_modules/discourse/declarations/admin/*\"],\n \"float-kit/*\": [\"./node_modules/discourse/declarations/float-kit/*\"],\n \"select-kit/*\": [\"./node_modules/discourse/declarations/select-kit/*\"],\n \"truth-helpers\": [\n \"./node_modules/discourse/declarations/truth-helpers/index.d.ts\"\n ],\n \"truth-helpers/*\": [\n \"./node_modules/discourse/declarations/truth-helpers/*\"\n ]\n }\n }\n}\n", + "tsconfig.json": "{\n \"extends\": \"discourse/tsconfig-plugin\",\n \"include\": [\n \"admin/assets/javascripts/**/*\",\n \"assets/javascripts/**/*\",\n \"test/javascripts/**/*\",\n \"node_modules/discourse/declarations/**/*\"\n ],\n \"compilerOptions\": {\n \"paths\": {\n \"discourse/plugins/discourse-plugin-skeleton/*\": [\n \"./assets/javascripts/*\",\n \"./assets/javascripts/*.gjs\",\n \"./assets/javascripts/*.gts\"\n ],\n \"discourse/plugins/discourse-plugin-skeleton/admin/*\": [\n \"./admin/assets/javascripts/admin/*\",\n \"./admin/assets/javascripts/admin/*.gjs\",\n \"./admin/assets/javascripts/admin/*.gts\"\n ],\n \"admin/*\": [\n \"./node_modules/discourse/declarations/admin/*\",\n \"./node_modules/discourse/declarations/admin/*.d.gjs.ts\",\n \"./node_modules/discourse/declarations/admin/*.d.gts.ts\"\n ],\n \"float-kit/*\": [\n \"./node_modules/discourse/declarations/float-kit/*\",\n \"./node_modules/discourse/declarations/float-kit/*.d.gjs.ts\",\n \"./node_modules/discourse/declarations/float-kit/*.d.gts.ts\"\n ],\n \"select-kit/*\": [\n \"./node_modules/discourse/declarations/select-kit/*\",\n \"./node_modules/discourse/declarations/select-kit/*.d.gjs.ts\",\n \"./node_modules/discourse/declarations/select-kit/*.d.gts.ts\"\n ],\n \"truth-helpers\": [\n \"./node_modules/discourse/declarations/truth-helpers/index.d.ts\"\n ],\n \"truth-helpers/*\": [\n \"./node_modules/discourse/declarations/truth-helpers/*\",\n \"./node_modules/discourse/declarations/truth-helpers/*.d.gjs.ts\",\n \"./node_modules/discourse/declarations/truth-helpers/*.d.gts.ts\"\n ]\n }\n },\n \"contentMappers\": [\n {\n \"package\": \"ember-content-mapper\",\n \"extensions\": [\".gts\", \".gjs\"]\n }\n ]\n}\n", ".rubocop.yml": "inherit_gem:\n rubocop-discourse: stree-compat.yml\n", ".streerc": "--print-width=100\n--plugins=plugin/trailing_comma,plugin/disable_auto_ternary\n", "Gemfile": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\ngroup :development do\n gem \"rubocop-discourse\"\n gem \"syntax_tree\"\nend\n", ".github/workflows/discourse-plugin.yml": "name: Discourse Plugin\n\non:\n push:\n branches:\n - main\n pull_request:\n\njobs:\n ci:\n uses: discourse/.github/.github/workflows/discourse-plugin.yml@v1\n", - ".github/workflows/d-compat-branch.yml": "name: Create new d-compat branch\n\non:\n workflow_dispatch:\n schedule:\n - cron: \"0 0 * * *\"\n\npermissions:\n contents: write\n\njobs:\n ci:\n uses: discourse/.github/.github/workflows/create-d-compat-branch.yml@v1\n" + ".github/workflows/d-compat-branch.yml": "name: Create new d-compat branch\n\non:\n workflow_dispatch:\n schedule:\n - cron: \"0 0 * * *\"\n\npermissions:\n contents: write\n\njobs:\n ci:\n uses: discourse/.github/.github/workflows/create-d-compat-branch.yml@v1\n", + "pnpm-workspace.yaml": "autoInstallPeers: false\nengineStrict: true\nminimumReleaseAgeExclude:\n - \"@discourse/*\"\nstrictPeerDependencies: true\n", + ".npmrc": "engine-strict = true\n" } } diff --git a/test/fixtures/theme.json b/test/fixtures/theme.json index be6261d..811c177 100644 --- a/test/fixtures/theme.json +++ b/test/fixtures/theme.json @@ -2,16 +2,18 @@ "type": "theme", "repository": "discourse/discourse-theme-skeleton", "files": { - "package.json": "{\n \"private\": true,\n \"devDependencies\": {\n \"@discourse/lint-configs\": \"3.2.0\",\n \"@glint/ember-tsc\": \"1.8.11\",\n \"concurrently\": \"^9.2.3\",\n \"discourse\": \"npm:@discourse/types@2026.3.0-d02b2966\",\n \"eslint\": \"10.6.0\",\n \"lint-to-the-future\": \"^2.6.4\",\n \"lint-to-the-future-eslint\": \"^3.3.0\",\n \"prettier\": \"3.8.1\",\n \"stylelint\": \"17.5.0\"\n },\n \"scripts\": {\n \"lint\": \"concurrently \\\"pnpm:lint:*(!fix)\\\" --names \\\"lint:\\\"\",\n \"lint:fix\": \"concurrently \\\"pnpm:lint:*:fix\\\" --names \\\"fix:\\\"\",\n \"lint:css\": \"pnpm stylelint '{javascripts,desktop,mobile,common,scss,stylesheets}/**/*.scss' --allow-empty-input\",\n \"lint:css:fix\": \"pnpm stylelint '{javascripts,desktop,mobile,common,scss,stylesheets}/**/*.scss' --fix --allow-empty-input\",\n \"lint:js\": \"eslint {javascripts,test} --cache --no-error-on-unmatched-pattern\",\n \"lint:js:fix\": \"eslint {javascripts,test} --fix --no-error-on-unmatched-pattern\",\n \"lint:prettier\": \"pnpm prettier '{javascripts,desktop,mobile,common,scss,stylesheets}/**/*.scss' '{javascripts,test}/**/*.{js,gjs,ts,gts,mts,cts}' --check --no-error-on-unmatched-pattern\",\n \"lint:prettier:fix\": \"pnpm prettier '{javascripts,desktop,mobile,common,scss,stylesheets}/**/*.scss' '{javascripts,test}/**/*.{js,gjs,ts,gts,mts,cts}' -w --no-error-on-unmatched-pattern\",\n \"lint:types\": \"ember-tsc -b\",\n \"lttf:ignore\": \"lint-to-the-future ignore\",\n \"update-skeleton\": \"pnpx @discourse/update-skeleton@latest\"\n },\n \"engines\": {\n \"node\": \">= 22\",\n \"npm\": \"please-use-pnpm\",\n \"yarn\": \"please-use-pnpm\",\n \"pnpm\": \"^10\"\n },\n \"packageManager\": \"pnpm@10.28.0\"\n}\n", + "package.json": "{\n \"private\": true,\n \"devDependencies\": {\n \"@discourse/lint-configs\": \"3.8.2\",\n \"@glint/ember-tsc\": \"1.11.6\",\n \"concurrently\": \"^9.2.3\",\n \"discourse\": \"npm:@discourse/types@2026.10.0-a5125163\",\n \"ember-content-mapper\": \"0.3.1\",\n \"eslint\": \"10.6.0\",\n \"lint-to-the-future\": \"^3.2.0\",\n \"lint-to-the-future-eslint\": \"^4.0.0\",\n \"prettier\": \"3.8.1\",\n \"stylelint\": \"17.5.0\",\n \"typescript\": \"7.1.0-dev.20260930.4\"\n },\n \"scripts\": {\n \"lint\": \"concurrently \\\"pnpm:lint:*(!fix)\\\" --names \\\"lint:\\\"\",\n \"lint:fix\": \"concurrently \\\"pnpm:lint:*:fix\\\" --names \\\"fix:\\\"\",\n \"lint:css\": \"pnpm stylelint '{javascripts,desktop,mobile,common,scss,stylesheets}/**/*.scss' --allow-empty-input\",\n \"lint:css:fix\": \"pnpm stylelint '{javascripts,desktop,mobile,common,scss,stylesheets}/**/*.scss' --fix --allow-empty-input\",\n \"lint:js\": \"eslint javascripts test --cache --no-error-on-unmatched-pattern\",\n \"lint:js:fix\": \"eslint javascripts test --fix --no-error-on-unmatched-pattern\",\n \"lint:prettier\": \"pnpm prettier '{javascripts,desktop,mobile,common,scss,stylesheets}/**/*.scss' '{javascripts,test}/**/*.{js,gjs,ts,gts,mts,cts}' --check --no-error-on-unmatched-pattern\",\n \"lint:prettier:fix\": \"pnpm prettier '{javascripts,desktop,mobile,common,scss,stylesheets}/**/*.scss' '{javascripts,test}/**/*.{js,gjs,ts,gts,mts,cts}' -w --no-error-on-unmatched-pattern\",\n \"lint:types\": \"tsc -b --runExternalCode\",\n \"lttf:ignore\": \"lint-to-the-future ignore\",\n \"update-skeleton\": \"pnpx @discourse/update-skeleton@latest\"\n },\n \"engines\": {\n \"node\": \">= 22\",\n \"npm\": \"please-use-pnpm\",\n \"yarn\": \"please-use-pnpm\",\n \"pnpm\": \"^12\"\n },\n \"packageManager\": \"pnpm@12.11.2\"\n}\n", ".gitignore": "node_modules\n.discourse-site\n.eslintcache\n", "eslint.config.mjs": "import DiscourseRecommendedTheme from \"@discourse/lint-configs/eslint-theme\";\n\nexport default [...DiscourseRecommendedTheme];\n", ".prettierrc.cjs": "module.exports = require(\"@discourse/lint-configs/prettier\");\n", "stylelint.config.mjs": "export default {\n extends: [\"@discourse/lint-configs/stylelint\"],\n};\n", - "tsconfig.json": "{\n \"extends\": \"discourse/tsconfig-plugin\",\n \"include\": [\n \"javascripts/**/*\",\n \"migrations/**/*\",\n \"test/**/*\",\n \"node_modules/discourse/declarations/**/*\"\n ],\n \"compilerOptions\": {\n \"paths\": {\n \"admin/*\": [\"./node_modules/discourse/declarations/admin/*\"],\n \"float-kit/*\": [\"./node_modules/discourse/declarations/float-kit/*\"],\n \"select-kit/*\": [\"./node_modules/discourse/declarations/select-kit/*\"],\n \"truth-helpers\": [\n \"./node_modules/discourse/declarations/truth-helpers/index.d.ts\"\n ],\n \"truth-helpers/*\": [\n \"./node_modules/discourse/declarations/truth-helpers/*\"\n ]\n }\n }\n}\n", + "tsconfig.json": "{\n \"extends\": \"discourse/tsconfig-plugin\",\n \"include\": [\n \"javascripts/**/*\",\n \"migrations/**/*\",\n \"test/**/*\",\n \"node_modules/discourse/declarations/**/*\"\n ],\n \"compilerOptions\": {\n \"paths\": {\n \"admin/*\": [\n \"./node_modules/discourse/declarations/admin/*\",\n \"./node_modules/discourse/declarations/admin/*.d.gjs.ts\",\n \"./node_modules/discourse/declarations/admin/*.d.gts.ts\"\n ],\n \"float-kit/*\": [\n \"./node_modules/discourse/declarations/float-kit/*\",\n \"./node_modules/discourse/declarations/float-kit/*.d.gjs.ts\",\n \"./node_modules/discourse/declarations/float-kit/*.d.gts.ts\"\n ],\n \"select-kit/*\": [\n \"./node_modules/discourse/declarations/select-kit/*\",\n \"./node_modules/discourse/declarations/select-kit/*.d.gjs.ts\",\n \"./node_modules/discourse/declarations/select-kit/*.d.gts.ts\"\n ],\n \"truth-helpers\": [\n \"./node_modules/discourse/declarations/truth-helpers/index.d.ts\"\n ],\n \"truth-helpers/*\": [\n \"./node_modules/discourse/declarations/truth-helpers/*\",\n \"./node_modules/discourse/declarations/truth-helpers/*.d.gjs.ts\",\n \"./node_modules/discourse/declarations/truth-helpers/*.d.gts.ts\"\n ]\n }\n },\n \"contentMappers\": [\n {\n \"package\": \"ember-content-mapper\",\n \"extensions\": [\".gts\", \".gjs\"]\n }\n ]\n}\n", ".rubocop.yml": "inherit_gem:\n rubocop-discourse: stree-compat.yml\n", ".streerc": "--print-width=100\n--plugins=plugin/trailing_comma,plugin/disable_auto_ternary\n", "Gemfile": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\ngroup :development do\n gem \"rubocop-discourse\"\n gem \"syntax_tree\"\nend\n", ".github/workflows/discourse-theme.yml": "name: Discourse Theme\n\non:\n push:\n branches:\n - main\n pull_request:\n\njobs:\n ci:\n uses: discourse/.github/.github/workflows/discourse-theme.yml@v1\n", - ".github/workflows/d-compat-branch.yml": "name: Create new d-compat branch\n\non:\n workflow_dispatch:\n schedule:\n - cron: \"0 0 * * *\"\n\npermissions:\n contents: write\n\njobs:\n ci:\n uses: discourse/.github/.github/workflows/create-d-compat-branch.yml@v1\n" + ".github/workflows/d-compat-branch.yml": "name: Create new d-compat branch\n\non:\n workflow_dispatch:\n schedule:\n - cron: \"0 0 * * *\"\n\npermissions:\n contents: write\n\njobs:\n ci:\n uses: discourse/.github/.github/workflows/create-d-compat-branch.yml@v1\n", + "pnpm-workspace.yaml": "autoInstallPeers: false\nengineStrict: true\nminimumReleaseAgeExclude:\n - \"@discourse/*\"\nstrictPeerDependencies: true\n", + ".npmrc": "engine-strict = true\n" } } diff --git a/test/update.test.js b/test/update.test.js index 491ae8e..51a81ce 100644 --- a/test/update.test.js +++ b/test/update.test.js @@ -4,6 +4,7 @@ import * as fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; +import { parse } from "yaml"; import { update } from "../src/update.js"; async function fixture(t, type = "theme") { @@ -70,6 +71,14 @@ for (const type of ["theme", "plugin"]) { "source stays unchanged" ); await fs.writeFile(path.join(f.directory, ".gitignore"), "my-cache"); + await fs.writeFile( + path.join(f.directory, ".npmrc"), + "engine-strict = true\nauto-install-peers = false\n" + ); + await fs.writeFile( + path.join(f.directory, "pnpm-workspace.yaml"), + 'allowBuilds:\n esbuild: true\nminimumReleaseAgeExclude:\n - "@my/*"\n' + ); await update(f.options); const text = await fs.readFile( path.join(f.directory, "package.json"), @@ -104,8 +113,23 @@ for (const type of ["theme", "plugin"]) { /discourse\/plugins\/my-plugin\// ); } + const workspace = await fs.readFile( + path.join(f.directory, "pnpm-workspace.yaml"), + "utf8" + ); + assert.deepEqual(parse(workspace), { + allowBuilds: { esbuild: true }, + minimumReleaseAgeExclude: ["@my/*", "@discourse/*"], + autoInstallPeers: false, + engineStrict: true, + strictPeerDependencies: true, + }); + assert.equal( + await fs.readFile(path.join(f.directory, ".npmrc"), "utf8"), + "engine-strict = true\n" + ); assert.deepEqual(f.calls, [ - "pnpm --ignore-workspace --config.manage-package-manager-versions=true install --no-frozen-lockfile --prod=false", + "pnpm --config.manage-package-manager-versions=true install --no-frozen-lockfile --prod=false", "bundle install", "bundle update --bundler", "bundle update --all", @@ -124,10 +148,38 @@ for (const type of ["theme", "plugin"]) { await fs.readFile(path.join(f.directory, ".gitignore"), "utf8"), ignore ); + assert.equal( + await fs.readFile(path.join(f.directory, "pnpm-workspace.yaml"), "utf8"), + workspace + ); assert.equal(f.calls.length, 8); }); } +test("keeps the project's .npmrc settings except those pnpm ignores there", async (t) => { + const f = await fixture(t); + await fs.writeFile( + path.join(f.directory, ".npmrc"), + "registry=https://npm.example.com/\nstrict-peer-dependencies=true\nengine-strict=false" + ); + await update(f.options); + assert.equal( + await fs.readFile(path.join(f.directory, ".npmrc"), "utf8"), + "registry=https://npm.example.com/\nengine-strict=false\n" + ); +}); + +test("copies the skeleton's .npmrc and pnpm-workspace.yaml when they are missing", async (t) => { + const f = await fixture(t); + await update(f.options); + for (const file of [".npmrc", "pnpm-workspace.yaml"]) { + assert.equal( + await fs.readFile(path.join(f.directory, file), "utf8"), + f.snapshot.files[file] + ); + } +}); + test("rejects an unrecognized project root", async (t) => { const f = await fixture(t); await fs.unlink(path.join(f.directory, "about.json"));