Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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": {
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 64 additions & 10 deletions src/update.js
Original file line number Diff line number Diff line change
@@ -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(),
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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 };
Expand All @@ -142,7 +159,6 @@ export async function update({
await run(
"pnpm",
[
"--ignore-workspace",
"--config.manage-package-manager-versions=true",
"install",
"--no-frozen-lockfile",
Expand All @@ -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();
}
8 changes: 5 additions & 3 deletions test/fixtures/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Loading
Loading