feat(templates): ship release/CI pipeline and scope switch in every scaffold

Every template now carries .github/workflows/release.yml, a matching ci.yml,
.sdk-scope.json, scripts/switch-sdk-scope.mjs, and a typecheck script.
initPlugin applies the scope config on scaffold so @drop-oss specifiers and
workspace:* deps are rewritten to the concrete @droposs versions, letting a
freshly scaffolded repo install, typecheck, build, and release without manual
edits. Release/CI install steps fall back to npm install when no lockfile is
present.
This commit is contained in:
BillyOutlast committed 2026-09-17 23:12:33 -04:00
1 parent adb96c6b33
commit dfeb00b326
45 files changed
+1517 -19

No files matched your search

+1 -1
View File
@@ -48,7 +48,7 @@ The CLI:
You can automate building, testing, signing, and releasing `.dropplugin` packages on every Git tag release.
Use `templates/release-action.yml` as the canonical per-repository release workflow: it runs `npm ci`, builds, tests, validates the manifest, packs the archive, attaches a `.sha256` checksum, and publishes a prerelease automatically for tags like `v1.0.0-rc.1`.
Use `templates/release-action.yml` as the canonical per-repository release workflow: it installs dependencies (`npm ci` when a lockfile exists, `npm install` otherwise), builds, tests, validates the manifest, packs the archive, attaches a `.sha256` checksum, and publishes a prerelease automatically for tags like `v1.0.0-rc.1`. Every scaffolded template under `templates/` ships a copy of this workflow, a matching `ci.yml`, a `typecheck` script, and a `.sdk-scope.json` + `scripts/switch-sdk-scope.mjs` pair, so a freshly scaffolded repo can tag `v1.0.0` and release without manual edits.
---
+146 -1
View File
@@ -1,5 +1,13 @@
import path from "node:path";
import { cp, mkdir, readFile, writeFile, stat } from "node:fs/promises";
import {
cp,
mkdir,
readFile,
writeFile,
stat,
readdir,
realpath,
} from "node:fs/promises";
export type TemplateType =
| "starter"
@@ -16,6 +24,139 @@ export interface InitOptions {
template?: string;
}
/** npm scopes the templates may resolve the SDK/CLI from. */
const SDK_SCOPES = ["@drop-oss", "@droposs", "@drop"];
const SKIP_DIRS = new Set([
"node_modules",
".git",
"dist",
"dist-package",
"dist-packages",
]);
const TEXT_EXT = new Set([
".ts",
".tsx",
".js",
".mjs",
".cjs",
".json",
".md",
".yml",
".yaml",
]);
interface SdkScopeConfig {
sdk: string;
sdkVersion: string;
cliVersion: string;
}
function parseSdkScope(text: string): SdkScopeConfig | null {
let parsed: Record<string, unknown>;
try {
parsed = JSON.parse(text);
} catch {
return null;
}
const { sdk, sdkVersion, cliVersion } = parsed;
if (
typeof sdk !== "string" ||
typeof sdkVersion !== "string" ||
typeof cliVersion !== "string" ||
!SDK_SCOPES.includes(sdk)
) {
return null;
}
return { sdk, sdkVersion, cliVersion };
}
function rewriteSpecifiers(text: string, scope: string): string {
let out = text;
for (const other of SDK_SCOPES) {
if (other === scope) continue;
out = out
.replaceAll(`${other}/plugin-sdk`, `${scope}/plugin-sdk`)
.replaceAll(`${other}/plugin-cli`, `${scope}/plugin-cli`);
}
return out;
}
function rewritePackageDeps(text: string, config: SdkScopeConfig): string {
let pkg: Record<string, unknown>;
try {
pkg = JSON.parse(text);
} catch {
return text;
}
for (const sectionName of [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
]) {
const section = pkg[sectionName] as Record<string, string> | undefined;
if (!section) continue;
for (const dep of Object.keys(section)) {
const kind = dep.endsWith("/plugin-sdk")
? "plugin-sdk"
: dep.endsWith("/plugin-cli")
? "plugin-cli"
: null;
if (!kind) continue;
delete section[dep];
section[`${config.sdk}/${kind}`] =
kind === "plugin-sdk" ? config.sdkVersion : config.cliVersion;
}
}
return JSON.stringify(pkg, null, 2) + "\n";
}
async function walkFiles(
dir: string,
files: string[] = [],
visited = new Set<string>(),
): Promise<string[]> {
const real = await realpath(dir).catch(() => dir);
if (visited.has(real)) return files;
visited.add(real);
for (const entry of await readdir(dir, { withFileTypes: true })) {
if (SKIP_DIRS.has(entry.name)) continue;
if (entry.isSymbolicLink()) continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory()) await walkFiles(full, files, visited);
else files.push(full);
}
return files;
}
/**
* Rewrites a freshly copied template so its `@drop-oss` monorepo specifiers
* and `workspace:*` dependencies point at the scope/version declared in the
* template's `.sdk-scope.json`. Without this a scaffolded repo keeps
* `workspace:*` and cannot install outside the SDK monorepo.
*/
export async function applySdkScope(targetPath: string): Promise<boolean> {
const raw = await readFile(
path.join(targetPath, ".sdk-scope.json"),
"utf-8",
).catch(() => null);
if (raw === null) return false;
const config = parseSdkScope(raw);
if (!config) return false;
for (const file of await walkFiles(targetPath)) {
if (path.basename(file).match(/lock\.(json|yaml|lockb)$/)) continue;
if (!TEXT_EXT.has(path.extname(file))) continue;
const before = await readFile(file, "utf-8");
let after = rewriteSpecifiers(before, config.sdk);
if (path.basename(file) === "package.json") {
after = rewritePackageDeps(after, config);
}
if (after !== before) await writeFile(file, after);
}
return true;
}
export function resolveTemplateFolder(templateName: string = "starter"): string {
const norm = templateName.replace(/^template-/, "").toLowerCase();
switch (norm) {
@@ -90,6 +231,10 @@ export async function initPlugin(
},
});
// Point the scaffolded repo at the configured npm scope/version instead of
// the monorepo's `workspace:*` links.
await applySdkScope(targetPath);
const pluginId =
options.id ||
path
+37
View File
@@ -135,6 +135,43 @@ test("initPlugin scaffolds various templates correctly", async () => {
.catch(() => null),
);
// Every template ships the publishing pipeline, scope switch, typecheck
// script, and concrete (non-workspace) SDK dependencies.
for (const [dir, template] of [
[uiDir, "client-ui"],
[metaDir, "metadata"],
[storeDir, "store"],
[runnerDir, "runner"],
[fullDir, "fullstack"],
] as const) {
for (const rel of [
".github/workflows/release.yml",
".github/workflows/ci.yml",
".sdk-scope.json",
"scripts/switch-sdk-scope.mjs",
]) {
assert.ok(
await fs.stat(path.join(dir, rel)).catch(() => null),
`${template} is missing ${rel}`,
);
}
const pkg = JSON.parse(
await fs.readFile(path.join(dir, "package.json"), "utf-8"),
);
assert.ok(pkg.scripts.typecheck, `${template} is missing a typecheck script`);
assert.equal(pkg.dependencies["@droposs/plugin-sdk"], "^0.7.0");
assert.equal(pkg.devDependencies["@droposs/plugin-cli"], "^0.7.0");
assert.equal(pkg.dependencies["@drop-oss/plugin-sdk"], undefined);
assert.equal(pkg.devDependencies["@drop-oss/plugin-cli"], undefined);
}
const runnerSrc = await fs.readFile(
path.join(runnerDir, "src/client.ts"),
"utf-8",
);
assert.match(runnerSrc, /@droposs\/plugin-sdk/);
assert.doesNotMatch(runnerSrc, /@drop-oss\/plugin-sdk/);
await fs.rm(tmp, { recursive: true, force: true });
});
+11
View File
@@ -227,9 +227,20 @@ test("initPlugin scaffolds a new plugin repository", async () => {
await fs.readFile(path.join(targetDir, "package.json"), "utf-8"),
);
assert.equal(pkg.name, "drop-my-custom-plugin");
assert.equal(pkg.dependencies["@droposs/plugin-sdk"], "^0.7.0");
assert.equal(pkg.devDependencies["@droposs/plugin-cli"], "^0.7.0");
assert.equal(pkg.scripts.typecheck, "tsc --noEmit");
const srcExists = await fs.stat(path.join(targetDir, "src", "index.ts"));
assert.ok(srcExists.isFile());
const src = await fs.readFile(path.join(targetDir, "src", "index.ts"), "utf-8");
assert.match(src, /@droposs\/plugin-sdk/);
assert.doesNotMatch(src, /@drop-oss\/plugin-sdk/);
assert.ok(
await fs
.stat(path.join(targetDir, ".github", "workflows", "release.yml"))
.catch(() => null),
);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
+6 -1
View File
@@ -27,7 +27,12 @@ jobs:
cache: npm
- name: Install dependencies
run: npm ci --ignore-scripts
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
+41
View File
@@ -0,0 +1,41 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: Build, typecheck, and test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
- name: Set up Node.js 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
- name: Validate manifest
run: npx --no-install drop-plugin validate .
+6 -1
View File
@@ -27,7 +27,12 @@ jobs:
cache: npm
- name: Install dependencies
run: npm ci --ignore-scripts
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
dist-package/
*.dropplugin
+2 -2
View File
@@ -1,5 +1,5 @@
{
"sdk": "@droposs",
"sdkVersion": "^0.6.2",
"cliVersion": "^0.6.2"
"sdkVersion": "^0.7.0",
"cliVersion": "^0.7.0"
}
+41
View File
@@ -0,0 +1,41 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: Build, typecheck, and test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
- name: Set up Node.js 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
- name: Validate manifest
run: npx --no-install drop-plugin validate .
@@ -0,0 +1,65 @@
name: Release Drop Plugin
on:
push:
tags:
- "v*"
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
name: Build, validate, and publish .dropplugin
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
- name: Set up Node.js 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
- name: Test
run: npm test
- name: Validate manifest
run: npx --no-install drop-plugin validate .
- name: Pack plugin archive
run: npx --no-install drop-plugin pack . ./dist-package
- name: Generate checksums
id: pkg
run: |
FILE=$(ls dist-package/*.dropplugin | head -n 1)
echo "path=$FILE" >> "$GITHUB_OUTPUT"
echo "name=$(basename "$FILE")" >> "$GITHUB_OUTPUT"
sha256sum "$FILE" > "$FILE.sha256"
- name: Create GitHub release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
files: |
${{ steps.pkg.outputs.path }}
${{ steps.pkg.outputs.path }}.sha256
name: ${{ steps.pkg.outputs.name }}
prerelease: ${{ contains(github.ref_name, '-') }}
generate_release_notes: true
@@ -0,0 +1,5 @@
{
"sdk": "@droposs",
"sdkVersion": "^0.7.0",
"cliVersion": "^0.7.0"
}
@@ -27,16 +27,20 @@
},
"checksum": "639af61b6c28f29ede8d001f5b1545c6c78b3360af1dc68d56fcf502fadaefc7",
"files": {
".github/workflows/ci.yml": "4741172bed309e809eb7d7a1ca02ce241fc8edeb2430060bbd680a08a9c20f10",
".github/workflows/release.yml": "999abcf55a425c3c0fb4dcb8442aca189a252a4967a5a7d82c709223ef46c871",
".gitignore": "c7ecb5dd064aa6e25e6d7afb4258dc3132295a306a5c1d343676ef541763d0bd",
".sdk-scope.json": "f13936572aca04f0f47da440ed27eb69ea7d5c5b1d0901b69ebd4aa55ce455b6",
"dist/src/client.css": "a7e80bda51554013cce91e4a153dd4598f63e8334da9a1d69b6f492d0f848629",
"dist/src/client.css.map": "58159ed3c29742d2d044de6e304a03806cf1c6ef0405136cce3e2613e94ffc4f",
"dist/src/client.js": "639af61b6c28f29ede8d001f5b1545c6c78b3360af1dc68d56fcf502fadaefc7",
"dist/src/client.js.map": "0a5a2907dea7b026cccbf4469520b5a4b4a97185a2a474b6b1d25ce38ac6b640",
"dist/test/client.test.js": "7234144f610006dd7b56e689580a68e6bc090c5c4ead137c90313d0e32260708",
"package.json": "45f7f8d41de9a5a4c6c246faee3359a3de9a6e89cc2bcc38caeeaff92dabccb4",
"package.json": "4cdb8a519181b4e51355d034ff00e4159bde5fc4676e843fc62258c31655afc3",
"scripts/switch-sdk-scope.mjs": "ab740ee477a7f5eb07b5344bd12f8c311d51f49279ad4778668e7e2ea4fef798",
"src/client.ts": "d6584c900faf826bcc5f45a900853110ff586591a295414c7386ef4c7d76e4c3",
"src/components/MyPanel.vue": "f92d837fe157e4cf48ceef4c77d2b1f46e52d1b34e6548163a928ee61df8fe55",
"test/client.test.ts": "b357cbe8049dc5f58d98fc3e7f2168e2d94c3c4c4353056794e757cf5b89b2a9",
"tsconfig.json": "a31a559ca71e6b0d196fffa84189fbaa4e117b6def7c4f45bd13645fbbfe30b9"
"tsconfig.json": "ffd407ef4014fe75cbdc6ad897632157a2d8b8f66a03a4d711c6ac1374f4cde2"
}
}
@@ -7,6 +7,7 @@
"build": "drop-plugin build",
"dev": "drop-plugin dev",
"test": "node --test test/client.test.ts",
"typecheck": "tsc --noEmit",
"validate": "drop-plugin validate"
},
"license": "MIT",
@@ -0,0 +1,133 @@
#!/usr/bin/env node
/* global process, console */
import { readdirSync, readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
const LEGACY_SCOPE = "@droposs";
const NEW_SCOPE = "@drop-oss";
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "dist-package", "dist-packages"]);
const LOCKFILES = new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "deno.lock"]);
const TEXT_EXT = new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".json", ".md", ".yml", ".yaml"]);
const configPath = path.resolve(process.cwd(), ".sdk-scope.json");
if (!existsSync(configPath)) {
console.error("Missing .sdk-scope.json in repo root");
process.exit(1);
}
const config = JSON.parse(readFileSync(configPath, "utf-8"));
const scope = config.sdk;
if (scope !== NEW_SCOPE && scope !== LEGACY_SCOPE) {
console.error(`Unknown scope '${scope}'; .sdk-scope.json sdk must be '${NEW_SCOPE}' or '${LEGACY_SCOPE}'`);
process.exit(1);
}
const sdkVersion = config["sdkVersion"];
const cliVersion = config["cliVersion"];
if (typeof sdkVersion !== "string" || typeof cliVersion !== "string") {
console.error(".sdk-scope.json must define sdkVersion and cliVersion");
process.exit(1);
}
const OTHER_SCOPES = scope === NEW_SCOPE ? [LEGACY_SCOPE] : [NEW_SCOPE];
function walk(dir, files = [], visited = new Set()) {
const real = realpathSync(dir);
if (visited.has(real)) return files;
visited.add(real);
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (SKIP_DIRS.has(entry.name)) continue;
if (entry.isSymbolicLink()) continue;
if (entry.isDirectory()) walk(full, files, visited);
else files.push(full);
}
return files;
}
function normalizeSpecifiers(text) {
let out = text;
for (const other of OTHER_SCOPES) {
out = out
.replaceAll(`${other}/plugin-sdk`, `${scope}/plugin-sdk`)
.replaceAll(`${other}/plugin-cli`, `${scope}/plugin-cli`);
}
return out;
}
function rewritePackageJson(file, text) {
const indent = detectIndent(text);
let pkg;
try {
pkg = JSON.parse(text);
} catch {
return text;
}
for (const sectionName of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
const section = pkg[sectionName];
if (!section) continue;
const rebuilt = {};
for (const dep of Object.keys(section)) {
const kind = parseSdkDep(dep);
if (!kind) {
rebuilt[dep] = section[dep];
continue;
}
const target = `${scope}/${kind}`;
rebuilt[target] = kind === "plugin-sdk" ? sdkVersion : cliVersion;
}
pkg[sectionName] = rebuilt;
}
return JSON.stringify(pkg, null, indent) + (text.endsWith("\n") ? "\n" : "");
}
function parseSdkDep(dep) {
for (const scopeName of [LEGACY_SCOPE, NEW_SCOPE, "@drop"]) {
if (dep === `${scopeName}/plugin-sdk`) return "plugin-sdk";
if (dep === `${scopeName}/plugin-cli`) return "plugin-cli";
}
return null;
}
function detectIndent(text) {
const match = text.match(/\n([ \t]+)"/);
return match ? match[1] : "\t";
}
const edited = [];
const files = walk(process.cwd());
for (const file of files) {
if (LOCKFILES.has(path.basename(file))) continue;
if (!TEXT_EXT.has(path.extname(file))) continue;
const before = readFileSync(file, "utf-8");
let after = normalizeSpecifiers(before);
if (path.basename(file) === "package.json") {
after = rewritePackageJson(file, after);
}
if (after !== before) {
writeFileSync(file, after);
edited.push(path.relative(process.cwd(), file));
}
}
console.log(`Scope set to '${scope}' (sdk ${sdkVersion}, cli ${cliVersion})`);
if (edited.length) {
for (const f of edited) console.log(` rewrote ${f}`);
} else {
console.log(" no specifier changes needed");
}
const installDir = process.cwd();
if (existsSync(path.join(installDir, "package-lock.json"))) {
try {
execSync("npm install --no-audit --no-fund", { cwd: installDir, stdio: "inherit" });
} catch {
execSync("npm install --no-audit --no-fund --legacy-peer-deps", { cwd: installDir, stdio: "inherit" });
}
} else if (existsSync(path.join(installDir, "pnpm-lock.yaml"))) {
execSync("pnpm install", { cwd: installDir, stdio: "inherit" });
} else {
console.warn("no lockfile found; skipping dependency install");
}
@@ -6,6 +6,7 @@
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"allowJs": true,
"outDir": "dist"
},
"include": ["src/**/*", "test/**/*"]
+41
View File
@@ -0,0 +1,41 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: Build, typecheck, and test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
- name: Set up Node.js 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
- name: Validate manifest
run: npx --no-install drop-plugin validate .
@@ -0,0 +1,65 @@
name: Release Drop Plugin
on:
push:
tags:
- "v*"
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
name: Build, validate, and publish .dropplugin
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
- name: Set up Node.js 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
- name: Test
run: npm test
- name: Validate manifest
run: npx --no-install drop-plugin validate .
- name: Pack plugin archive
run: npx --no-install drop-plugin pack . ./dist-package
- name: Generate checksums
id: pkg
run: |
FILE=$(ls dist-package/*.dropplugin | head -n 1)
echo "path=$FILE" >> "$GITHUB_OUTPUT"
echo "name=$(basename "$FILE")" >> "$GITHUB_OUTPUT"
sha256sum "$FILE" > "$FILE.sha256"
- name: Create GitHub release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
files: |
${{ steps.pkg.outputs.path }}
${{ steps.pkg.outputs.path }}.sha256
name: ${{ steps.pkg.outputs.name }}
prerelease: ${{ contains(github.ref_name, '-') }}
generate_release_notes: true
@@ -0,0 +1,5 @@
{
"sdk": "@droposs",
"sdkVersion": "^0.7.0",
"cliVersion": "^0.7.0"
}
@@ -79,19 +79,23 @@
},
"checksum": "4be6e9eb395333d01656762da93debc7f50c101ae3e222230e2b75217770580b",
"files": {
".github/workflows/ci.yml": "4741172bed309e809eb7d7a1ca02ce241fc8edeb2430060bbd680a08a9c20f10",
".github/workflows/release.yml": "999abcf55a425c3c0fb4dcb8442aca189a252a4967a5a7d82c709223ef46c871",
".gitignore": "c7ecb5dd064aa6e25e6d7afb4258dc3132295a306a5c1d343676ef541763d0bd",
".sdk-scope.json": "f13936572aca04f0f47da440ed27eb69ea7d5c5b1d0901b69ebd4aa55ce455b6",
"dist/src/client.css": "16fe4969e12025eba710f5d71939827c8e3db98344fb4cb2f6622ff199f49ad3",
"dist/src/client.css.map": "aef5ae0e46e30bafcd54d1c0d8a712ca40ac7df0b10ec5254b112345980ef2e0",
"dist/src/client.js": "ba9114a7de9420db06682108ae4e4952bf60b570041a79d1efee536bf01f220e",
"dist/src/client.js.map": "e83e8e665da64f3cb85692f8529a32f0e10336cafc430a97a34179f4685dc2e5",
"dist/src/index.js": "4be6e9eb395333d01656762da93debc7f50c101ae3e222230e2b75217770580b",
"dist/src/index.js.map": "0c6c706d98afc1913a45cf484c12d6d79b5ef5186c32c4ef35af7b4ff1c1e18f",
"package.json": "e1a2e1aaf319cca92c451a11bebe22a65dcf8e51d6794f7ae90c35355f0e64b1",
"package.json": "5cf0bd19d8e597dac4cb1c60c67e8ef0255a5a47c5507720a481599ac7623bfb",
"scripts/switch-sdk-scope.mjs": "ab740ee477a7f5eb07b5344bd12f8c311d51f49279ad4778668e7e2ea4fef798",
"src/client.ts": "a8f767ad5ded4ae6ef767a46fcaa0c945fd9098e4cd923473053c9e437e21815",
"src/components/StatusPanel.vue": "2b8374cfad18715c235b7909ddcd2c45332d664d0f4e89b0e3e6418e5ab02fb6",
"src/index.ts": "cff4a6b75e96bae971da4272d6ededde5cd88358b466ba861db54d9051d84d60",
"test/client.test.ts": "8fe9659122b3e331688866813195b101a1ca9c543b5f79787bde8c8538211a29",
"test/index.test.ts": "a0cc24ae8d19ff423bb717a3edb4c8e029144ed639ff02200d5d5fa4b028528b",
"tsconfig.json": "a31a559ca71e6b0d196fffa84189fbaa4e117b6def7c4f45bd13645fbbfe30b9"
"test/index.test.ts": "2ca92c3a328f623e82da8d2b38e155dc24ddd91cbfedd69619b56393137bd777",
"tsconfig.json": "ffd407ef4014fe75cbdc6ad897632157a2d8b8f66a03a4d711c6ac1374f4cde2"
}
}
@@ -7,6 +7,7 @@
"build": "drop-plugin build",
"dev": "drop-plugin dev",
"test": "node --test test/index.test.ts test/client.test.ts",
"typecheck": "tsc --noEmit",
"validate": "drop-plugin validate"
},
"license": "MIT",
@@ -0,0 +1,133 @@
#!/usr/bin/env node
/* global process, console */
import { readdirSync, readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
const LEGACY_SCOPE = "@droposs";
const NEW_SCOPE = "@drop-oss";
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "dist-package", "dist-packages"]);
const LOCKFILES = new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "deno.lock"]);
const TEXT_EXT = new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".json", ".md", ".yml", ".yaml"]);
const configPath = path.resolve(process.cwd(), ".sdk-scope.json");
if (!existsSync(configPath)) {
console.error("Missing .sdk-scope.json in repo root");
process.exit(1);
}
const config = JSON.parse(readFileSync(configPath, "utf-8"));
const scope = config.sdk;
if (scope !== NEW_SCOPE && scope !== LEGACY_SCOPE) {
console.error(`Unknown scope '${scope}'; .sdk-scope.json sdk must be '${NEW_SCOPE}' or '${LEGACY_SCOPE}'`);
process.exit(1);
}
const sdkVersion = config["sdkVersion"];
const cliVersion = config["cliVersion"];
if (typeof sdkVersion !== "string" || typeof cliVersion !== "string") {
console.error(".sdk-scope.json must define sdkVersion and cliVersion");
process.exit(1);
}
const OTHER_SCOPES = scope === NEW_SCOPE ? [LEGACY_SCOPE] : [NEW_SCOPE];
function walk(dir, files = [], visited = new Set()) {
const real = realpathSync(dir);
if (visited.has(real)) return files;
visited.add(real);
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (SKIP_DIRS.has(entry.name)) continue;
if (entry.isSymbolicLink()) continue;
if (entry.isDirectory()) walk(full, files, visited);
else files.push(full);
}
return files;
}
function normalizeSpecifiers(text) {
let out = text;
for (const other of OTHER_SCOPES) {
out = out
.replaceAll(`${other}/plugin-sdk`, `${scope}/plugin-sdk`)
.replaceAll(`${other}/plugin-cli`, `${scope}/plugin-cli`);
}
return out;
}
function rewritePackageJson(file, text) {
const indent = detectIndent(text);
let pkg;
try {
pkg = JSON.parse(text);
} catch {
return text;
}
for (const sectionName of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
const section = pkg[sectionName];
if (!section) continue;
const rebuilt = {};
for (const dep of Object.keys(section)) {
const kind = parseSdkDep(dep);
if (!kind) {
rebuilt[dep] = section[dep];
continue;
}
const target = `${scope}/${kind}`;
rebuilt[target] = kind === "plugin-sdk" ? sdkVersion : cliVersion;
}
pkg[sectionName] = rebuilt;
}
return JSON.stringify(pkg, null, indent) + (text.endsWith("\n") ? "\n" : "");
}
function parseSdkDep(dep) {
for (const scopeName of [LEGACY_SCOPE, NEW_SCOPE, "@drop"]) {
if (dep === `${scopeName}/plugin-sdk`) return "plugin-sdk";
if (dep === `${scopeName}/plugin-cli`) return "plugin-cli";
}
return null;
}
function detectIndent(text) {
const match = text.match(/\n([ \t]+)"/);
return match ? match[1] : "\t";
}
const edited = [];
const files = walk(process.cwd());
for (const file of files) {
if (LOCKFILES.has(path.basename(file))) continue;
if (!TEXT_EXT.has(path.extname(file))) continue;
const before = readFileSync(file, "utf-8");
let after = normalizeSpecifiers(before);
if (path.basename(file) === "package.json") {
after = rewritePackageJson(file, after);
}
if (after !== before) {
writeFileSync(file, after);
edited.push(path.relative(process.cwd(), file));
}
}
console.log(`Scope set to '${scope}' (sdk ${sdkVersion}, cli ${cliVersion})`);
if (edited.length) {
for (const f of edited) console.log(` rewrote ${f}`);
} else {
console.log(" no specifier changes needed");
}
const installDir = process.cwd();
if (existsSync(path.join(installDir, "package-lock.json"))) {
try {
execSync("npm install --no-audit --no-fund", { cwd: installDir, stdio: "inherit" });
} catch {
execSync("npm install --no-audit --no-fund --legacy-peer-deps", { cwd: installDir, stdio: "inherit" });
}
} else if (existsSync(path.join(installDir, "pnpm-lock.yaml"))) {
execSync("pnpm install", { cwd: installDir, stdio: "inherit" });
} else {
console.warn("no lockfile found; skipping dependency install");
}
@@ -19,7 +19,7 @@ test("MyFullstackPlugin registers status and config routes", async () => {
const statusRes = (await status.handler(null, {
params: {},
query: {},
readJson: async () => ({}),
readJson: async <T = unknown>() => ({}) as T,
})) as { ok: boolean };
assert.equal(statusRes.ok, true);
@@ -28,7 +28,7 @@ test("MyFullstackPlugin registers status and config routes", async () => {
const configRes = (await config.handler(null, {
params: {},
query: {},
readJson: async () => ({ mode: "full" }),
readJson: async <T = unknown>() => ({ mode: "full" }) as T,
})) as { saved: boolean; synced: number };
assert.equal(configRes.saved, true);
assert.equal(configRes.synced, 1);
@@ -6,6 +6,7 @@
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"allowJs": true,
"outDir": "dist"
},
"include": ["src/**/*", "test/**/*"]
+41
View File
@@ -0,0 +1,41 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: Build, typecheck, and test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
- name: Set up Node.js 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
- name: Validate manifest
run: npx --no-install drop-plugin validate .
@@ -0,0 +1,65 @@
name: Release Drop Plugin
on:
push:
tags:
- "v*"
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
name: Build, validate, and publish .dropplugin
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
- name: Set up Node.js 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
- name: Test
run: npm test
- name: Validate manifest
run: npx --no-install drop-plugin validate .
- name: Pack plugin archive
run: npx --no-install drop-plugin pack . ./dist-package
- name: Generate checksums
id: pkg
run: |
FILE=$(ls dist-package/*.dropplugin | head -n 1)
echo "path=$FILE" >> "$GITHUB_OUTPUT"
echo "name=$(basename "$FILE")" >> "$GITHUB_OUTPUT"
sha256sum "$FILE" > "$FILE.sha256"
- name: Create GitHub release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
files: |
${{ steps.pkg.outputs.path }}
${{ steps.pkg.outputs.path }}.sha256
name: ${{ steps.pkg.outputs.name }}
prerelease: ${{ contains(github.ref_name, '-') }}
generate_release_notes: true
@@ -0,0 +1,5 @@
{
"sdk": "@droposs",
"sdkVersion": "^0.7.0",
"cliVersion": "^0.7.0"
}
+6 -2
View File
@@ -28,13 +28,17 @@
},
"checksum": "4b4d8aa64f747b9611e00609a3036b493ba030a3e5aadf4d68fcf7752fef88a9",
"files": {
".github/workflows/ci.yml": "4741172bed309e809eb7d7a1ca02ce241fc8edeb2430060bbd680a08a9c20f10",
".github/workflows/release.yml": "999abcf55a425c3c0fb4dcb8442aca189a252a4967a5a7d82c709223ef46c871",
".gitignore": "c7ecb5dd064aa6e25e6d7afb4258dc3132295a306a5c1d343676ef541763d0bd",
".sdk-scope.json": "f13936572aca04f0f47da440ed27eb69ea7d5c5b1d0901b69ebd4aa55ce455b6",
"dist/src/index.js": "4b4d8aa64f747b9611e00609a3036b493ba030a3e5aadf4d68fcf7752fef88a9",
"dist/src/index.js.map": "904c14711dda7e966d9d534e53f277e895c87411b486d76424eee15a1963bafb",
"dist/test/index.test.js": "640511eaf5d061c82d6cdf609bd518cd8033e05a59f7642418231ba69e9ed59a",
"package.json": "86dbe2c6a0412914c40ee2692b730262602437b19e5c06c15e0f654b27feb427",
"package.json": "d6635b8ea742bc70120ae47bfdacecc434833f0913f1d1db9c4903cce76d84c5",
"scripts/switch-sdk-scope.mjs": "ab740ee477a7f5eb07b5344bd12f8c311d51f49279ad4778668e7e2ea4fef798",
"src/index.ts": "546e73733177bd21ec77bcef1c77b11453ad15dbf3204d5aef8671e75ba19d48",
"test/index.test.ts": "efd5b6120feaee7b74fb7e774d3ff243d1bf6d9195d28181c2ab615d45908b0f",
"tsconfig.json": "a31a559ca71e6b0d196fffa84189fbaa4e117b6def7c4f45bd13645fbbfe30b9"
"tsconfig.json": "ffd407ef4014fe75cbdc6ad897632157a2d8b8f66a03a4d711c6ac1374f4cde2"
}
}
+1
View File
@@ -6,6 +6,7 @@
"scripts": {
"build": "drop-plugin build",
"test": "node --test test/index.test.ts",
"typecheck": "tsc --noEmit",
"validate": "drop-plugin validate"
},
"license": "MIT",
@@ -0,0 +1,133 @@
#!/usr/bin/env node
/* global process, console */
import { readdirSync, readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
const LEGACY_SCOPE = "@droposs";
const NEW_SCOPE = "@drop-oss";
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "dist-package", "dist-packages"]);
const LOCKFILES = new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "deno.lock"]);
const TEXT_EXT = new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".json", ".md", ".yml", ".yaml"]);
const configPath = path.resolve(process.cwd(), ".sdk-scope.json");
if (!existsSync(configPath)) {
console.error("Missing .sdk-scope.json in repo root");
process.exit(1);
}
const config = JSON.parse(readFileSync(configPath, "utf-8"));
const scope = config.sdk;
if (scope !== NEW_SCOPE && scope !== LEGACY_SCOPE) {
console.error(`Unknown scope '${scope}'; .sdk-scope.json sdk must be '${NEW_SCOPE}' or '${LEGACY_SCOPE}'`);
process.exit(1);
}
const sdkVersion = config["sdkVersion"];
const cliVersion = config["cliVersion"];
if (typeof sdkVersion !== "string" || typeof cliVersion !== "string") {
console.error(".sdk-scope.json must define sdkVersion and cliVersion");
process.exit(1);
}
const OTHER_SCOPES = scope === NEW_SCOPE ? [LEGACY_SCOPE] : [NEW_SCOPE];
function walk(dir, files = [], visited = new Set()) {
const real = realpathSync(dir);
if (visited.has(real)) return files;
visited.add(real);
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (SKIP_DIRS.has(entry.name)) continue;
if (entry.isSymbolicLink()) continue;
if (entry.isDirectory()) walk(full, files, visited);
else files.push(full);
}
return files;
}
function normalizeSpecifiers(text) {
let out = text;
for (const other of OTHER_SCOPES) {
out = out
.replaceAll(`${other}/plugin-sdk`, `${scope}/plugin-sdk`)
.replaceAll(`${other}/plugin-cli`, `${scope}/plugin-cli`);
}
return out;
}
function rewritePackageJson(file, text) {
const indent = detectIndent(text);
let pkg;
try {
pkg = JSON.parse(text);
} catch {
return text;
}
for (const sectionName of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
const section = pkg[sectionName];
if (!section) continue;
const rebuilt = {};
for (const dep of Object.keys(section)) {
const kind = parseSdkDep(dep);
if (!kind) {
rebuilt[dep] = section[dep];
continue;
}
const target = `${scope}/${kind}`;
rebuilt[target] = kind === "plugin-sdk" ? sdkVersion : cliVersion;
}
pkg[sectionName] = rebuilt;
}
return JSON.stringify(pkg, null, indent) + (text.endsWith("\n") ? "\n" : "");
}
function parseSdkDep(dep) {
for (const scopeName of [LEGACY_SCOPE, NEW_SCOPE, "@drop"]) {
if (dep === `${scopeName}/plugin-sdk`) return "plugin-sdk";
if (dep === `${scopeName}/plugin-cli`) return "plugin-cli";
}
return null;
}
function detectIndent(text) {
const match = text.match(/\n([ \t]+)"/);
return match ? match[1] : "\t";
}
const edited = [];
const files = walk(process.cwd());
for (const file of files) {
if (LOCKFILES.has(path.basename(file))) continue;
if (!TEXT_EXT.has(path.extname(file))) continue;
const before = readFileSync(file, "utf-8");
let after = normalizeSpecifiers(before);
if (path.basename(file) === "package.json") {
after = rewritePackageJson(file, after);
}
if (after !== before) {
writeFileSync(file, after);
edited.push(path.relative(process.cwd(), file));
}
}
console.log(`Scope set to '${scope}' (sdk ${sdkVersion}, cli ${cliVersion})`);
if (edited.length) {
for (const f of edited) console.log(` rewrote ${f}`);
} else {
console.log(" no specifier changes needed");
}
const installDir = process.cwd();
if (existsSync(path.join(installDir, "package-lock.json"))) {
try {
execSync("npm install --no-audit --no-fund", { cwd: installDir, stdio: "inherit" });
} catch {
execSync("npm install --no-audit --no-fund --legacy-peer-deps", { cwd: installDir, stdio: "inherit" });
}
} else if (existsSync(path.join(installDir, "pnpm-lock.yaml"))) {
execSync("pnpm install", { cwd: installDir, stdio: "inherit" });
} else {
console.warn("no lockfile found; skipping dependency install");
}
@@ -6,6 +6,7 @@
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"allowJs": true,
"outDir": "dist"
},
"include": ["src/**/*", "test/**/*"]
+41
View File
@@ -0,0 +1,41 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: Build, typecheck, and test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
- name: Set up Node.js 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
- name: Validate manifest
run: npx --no-install drop-plugin validate .
+65
View File
@@ -0,0 +1,65 @@
name: Release Drop Plugin
on:
push:
tags:
- "v*"
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
name: Build, validate, and publish .dropplugin
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
- name: Set up Node.js 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
- name: Test
run: npm test
- name: Validate manifest
run: npx --no-install drop-plugin validate .
- name: Pack plugin archive
run: npx --no-install drop-plugin pack . ./dist-package
- name: Generate checksums
id: pkg
run: |
FILE=$(ls dist-package/*.dropplugin | head -n 1)
echo "path=$FILE" >> "$GITHUB_OUTPUT"
echo "name=$(basename "$FILE")" >> "$GITHUB_OUTPUT"
sha256sum "$FILE" > "$FILE.sha256"
- name: Create GitHub release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
files: |
${{ steps.pkg.outputs.path }}
${{ steps.pkg.outputs.path }}.sha256
name: ${{ steps.pkg.outputs.name }}
prerelease: ${{ contains(github.ref_name, '-') }}
generate_release_notes: true
@@ -0,0 +1,5 @@
{
"sdk": "@droposs",
"sdkVersion": "^0.7.0",
"cliVersion": "^0.7.0"
}
+6 -2
View File
@@ -28,13 +28,17 @@
},
"checksum": "1aed97c00c0d65e276bbe6cc4fb856bcc311eba57dd02aced1050739c25b656e",
"files": {
".github/workflows/ci.yml": "4741172bed309e809eb7d7a1ca02ce241fc8edeb2430060bbd680a08a9c20f10",
".github/workflows/release.yml": "999abcf55a425c3c0fb4dcb8442aca189a252a4967a5a7d82c709223ef46c871",
".gitignore": "c7ecb5dd064aa6e25e6d7afb4258dc3132295a306a5c1d343676ef541763d0bd",
".sdk-scope.json": "f13936572aca04f0f47da440ed27eb69ea7d5c5b1d0901b69ebd4aa55ce455b6",
"dist/src/client.js": "1aed97c00c0d65e276bbe6cc4fb856bcc311eba57dd02aced1050739c25b656e",
"dist/src/client.js.map": "a33fa4205bdff524362e0ae3d9fa43160dfa494635b1119be712c91a47d5b7cf",
"dist/test/client.test.js": "30a5d956c06e06aa230bacb27537a2e02b0865c99215dcb5175733caa99769d1",
"package.json": "2f3b4ffaa8a135c11f07818c690841259402200f371bf64c832cf18731d7190c",
"package.json": "b129d1090d19b77e37c162af3a91e613f6f5955f3f8b052f0da3798e2028e899",
"scripts/switch-sdk-scope.mjs": "ab740ee477a7f5eb07b5344bd12f8c311d51f49279ad4778668e7e2ea4fef798",
"src/client.ts": "458ac509935463f9b2e2bd6c56144fb12b3434819155432f08403d7b67b03a6e",
"test/client.test.ts": "de8e38567558d71a95c36ea414f555bda0cb8b46c5e85c73b19d5cbab6bf814c",
"tsconfig.json": "a31a559ca71e6b0d196fffa84189fbaa4e117b6def7c4f45bd13645fbbfe30b9"
"tsconfig.json": "ffd407ef4014fe75cbdc6ad897632157a2d8b8f66a03a4d711c6ac1374f4cde2"
}
}
+1
View File
@@ -6,6 +6,7 @@
"scripts": {
"build": "drop-plugin build",
"test": "node --test test/client.test.ts",
"typecheck": "tsc --noEmit",
"validate": "drop-plugin validate"
},
"license": "MIT",
@@ -0,0 +1,133 @@
#!/usr/bin/env node
/* global process, console */
import { readdirSync, readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
const LEGACY_SCOPE = "@droposs";
const NEW_SCOPE = "@drop-oss";
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "dist-package", "dist-packages"]);
const LOCKFILES = new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "deno.lock"]);
const TEXT_EXT = new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".json", ".md", ".yml", ".yaml"]);
const configPath = path.resolve(process.cwd(), ".sdk-scope.json");
if (!existsSync(configPath)) {
console.error("Missing .sdk-scope.json in repo root");
process.exit(1);
}
const config = JSON.parse(readFileSync(configPath, "utf-8"));
const scope = config.sdk;
if (scope !== NEW_SCOPE && scope !== LEGACY_SCOPE) {
console.error(`Unknown scope '${scope}'; .sdk-scope.json sdk must be '${NEW_SCOPE}' or '${LEGACY_SCOPE}'`);
process.exit(1);
}
const sdkVersion = config["sdkVersion"];
const cliVersion = config["cliVersion"];
if (typeof sdkVersion !== "string" || typeof cliVersion !== "string") {
console.error(".sdk-scope.json must define sdkVersion and cliVersion");
process.exit(1);
}
const OTHER_SCOPES = scope === NEW_SCOPE ? [LEGACY_SCOPE] : [NEW_SCOPE];
function walk(dir, files = [], visited = new Set()) {
const real = realpathSync(dir);
if (visited.has(real)) return files;
visited.add(real);
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (SKIP_DIRS.has(entry.name)) continue;
if (entry.isSymbolicLink()) continue;
if (entry.isDirectory()) walk(full, files, visited);
else files.push(full);
}
return files;
}
function normalizeSpecifiers(text) {
let out = text;
for (const other of OTHER_SCOPES) {
out = out
.replaceAll(`${other}/plugin-sdk`, `${scope}/plugin-sdk`)
.replaceAll(`${other}/plugin-cli`, `${scope}/plugin-cli`);
}
return out;
}
function rewritePackageJson(file, text) {
const indent = detectIndent(text);
let pkg;
try {
pkg = JSON.parse(text);
} catch {
return text;
}
for (const sectionName of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
const section = pkg[sectionName];
if (!section) continue;
const rebuilt = {};
for (const dep of Object.keys(section)) {
const kind = parseSdkDep(dep);
if (!kind) {
rebuilt[dep] = section[dep];
continue;
}
const target = `${scope}/${kind}`;
rebuilt[target] = kind === "plugin-sdk" ? sdkVersion : cliVersion;
}
pkg[sectionName] = rebuilt;
}
return JSON.stringify(pkg, null, indent) + (text.endsWith("\n") ? "\n" : "");
}
function parseSdkDep(dep) {
for (const scopeName of [LEGACY_SCOPE, NEW_SCOPE, "@drop"]) {
if (dep === `${scopeName}/plugin-sdk`) return "plugin-sdk";
if (dep === `${scopeName}/plugin-cli`) return "plugin-cli";
}
return null;
}
function detectIndent(text) {
const match = text.match(/\n([ \t]+)"/);
return match ? match[1] : "\t";
}
const edited = [];
const files = walk(process.cwd());
for (const file of files) {
if (LOCKFILES.has(path.basename(file))) continue;
if (!TEXT_EXT.has(path.extname(file))) continue;
const before = readFileSync(file, "utf-8");
let after = normalizeSpecifiers(before);
if (path.basename(file) === "package.json") {
after = rewritePackageJson(file, after);
}
if (after !== before) {
writeFileSync(file, after);
edited.push(path.relative(process.cwd(), file));
}
}
console.log(`Scope set to '${scope}' (sdk ${sdkVersion}, cli ${cliVersion})`);
if (edited.length) {
for (const f of edited) console.log(` rewrote ${f}`);
} else {
console.log(" no specifier changes needed");
}
const installDir = process.cwd();
if (existsSync(path.join(installDir, "package-lock.json"))) {
try {
execSync("npm install --no-audit --no-fund", { cwd: installDir, stdio: "inherit" });
} catch {
execSync("npm install --no-audit --no-fund --legacy-peer-deps", { cwd: installDir, stdio: "inherit" });
}
} else if (existsSync(path.join(installDir, "pnpm-lock.yaml"))) {
execSync("pnpm install", { cwd: installDir, stdio: "inherit" });
} else {
console.warn("no lockfile found; skipping dependency install");
}
+1
View File
@@ -6,6 +6,7 @@
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"allowJs": true,
"outDir": "dist"
},
"include": ["src/**/*", "test/**/*"]
+41
View File
@@ -0,0 +1,41 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: Build, typecheck, and test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
- name: Set up Node.js 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
- name: Validate manifest
run: npx --no-install drop-plugin validate .
+65
View File
@@ -0,0 +1,65 @@
name: Release Drop Plugin
on:
push:
tags:
- "v*"
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
name: Build, validate, and publish .dropplugin
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
- name: Set up Node.js 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
npm ci --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
- name: Build
run: npm run build
- name: Test
run: npm test
- name: Validate manifest
run: npx --no-install drop-plugin validate .
- name: Pack plugin archive
run: npx --no-install drop-plugin pack . ./dist-package
- name: Generate checksums
id: pkg
run: |
FILE=$(ls dist-package/*.dropplugin | head -n 1)
echo "path=$FILE" >> "$GITHUB_OUTPUT"
echo "name=$(basename "$FILE")" >> "$GITHUB_OUTPUT"
sha256sum "$FILE" > "$FILE.sha256"
- name: Create GitHub release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
files: |
${{ steps.pkg.outputs.path }}
${{ steps.pkg.outputs.path }}.sha256
name: ${{ steps.pkg.outputs.name }}
prerelease: ${{ contains(github.ref_name, '-') }}
generate_release_notes: true
+5
View File
@@ -0,0 +1,5 @@
{
"sdk": "@droposs",
"sdkVersion": "^0.7.0",
"cliVersion": "^0.7.0"
}
+6 -2
View File
@@ -28,13 +28,17 @@
},
"checksum": "d6dd6f7ea2fce5f4d36b5e1c5d67acb4437a00dfbb908593dd6ac3191168ed2b",
"files": {
".github/workflows/ci.yml": "4741172bed309e809eb7d7a1ca02ce241fc8edeb2430060bbd680a08a9c20f10",
".github/workflows/release.yml": "999abcf55a425c3c0fb4dcb8442aca189a252a4967a5a7d82c709223ef46c871",
".gitignore": "c7ecb5dd064aa6e25e6d7afb4258dc3132295a306a5c1d343676ef541763d0bd",
".sdk-scope.json": "f13936572aca04f0f47da440ed27eb69ea7d5c5b1d0901b69ebd4aa55ce455b6",
"dist/src/client.js": "d6dd6f7ea2fce5f4d36b5e1c5d67acb4437a00dfbb908593dd6ac3191168ed2b",
"dist/src/client.js.map": "d9c5c3e51398e7987190d3d2ee0f78d0151060fd662e44a94bcbff7ee78b4cff",
"dist/test/client.test.js": "3f0b0d88df89542ad652022d50a1fa245fadc1743c9373ef37f82b1c8c15eefe",
"package.json": "9bd246d0eeba2becc4d50e63869683cb66d2aba81a780dfabd61699da91a9dfb",
"package.json": "82b8b701d226bbf45dbf9e605588f0b249a780c4dd457601202ba71d56b64a10",
"scripts/switch-sdk-scope.mjs": "ab740ee477a7f5eb07b5344bd12f8c311d51f49279ad4778668e7e2ea4fef798",
"src/client.ts": "982c9dd2afd1afd77d9d9071c7cc97ede6143648ea8a4e71a5e80e664cb449d9",
"test/client.test.ts": "b0cd225d32c199605e35532d1cab89fc5513cfffeb91f10c76e0cb4435ed9611",
"tsconfig.json": "a31a559ca71e6b0d196fffa84189fbaa4e117b6def7c4f45bd13645fbbfe30b9"
"tsconfig.json": "ffd407ef4014fe75cbdc6ad897632157a2d8b8f66a03a4d711c6ac1374f4cde2"
}
}
+1
View File
@@ -6,6 +6,7 @@
"scripts": {
"build": "drop-plugin build",
"test": "node --test test/client.test.ts",
"typecheck": "tsc --noEmit",
"validate": "drop-plugin validate"
},
"license": "MIT",
@@ -0,0 +1,133 @@
#!/usr/bin/env node
/* global process, console */
import { readdirSync, readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
const LEGACY_SCOPE = "@droposs";
const NEW_SCOPE = "@drop-oss";
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "dist-package", "dist-packages"]);
const LOCKFILES = new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "deno.lock"]);
const TEXT_EXT = new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".json", ".md", ".yml", ".yaml"]);
const configPath = path.resolve(process.cwd(), ".sdk-scope.json");
if (!existsSync(configPath)) {
console.error("Missing .sdk-scope.json in repo root");
process.exit(1);
}
const config = JSON.parse(readFileSync(configPath, "utf-8"));
const scope = config.sdk;
if (scope !== NEW_SCOPE && scope !== LEGACY_SCOPE) {
console.error(`Unknown scope '${scope}'; .sdk-scope.json sdk must be '${NEW_SCOPE}' or '${LEGACY_SCOPE}'`);
process.exit(1);
}
const sdkVersion = config["sdkVersion"];
const cliVersion = config["cliVersion"];
if (typeof sdkVersion !== "string" || typeof cliVersion !== "string") {
console.error(".sdk-scope.json must define sdkVersion and cliVersion");
process.exit(1);
}
const OTHER_SCOPES = scope === NEW_SCOPE ? [LEGACY_SCOPE] : [NEW_SCOPE];
function walk(dir, files = [], visited = new Set()) {
const real = realpathSync(dir);
if (visited.has(real)) return files;
visited.add(real);
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (SKIP_DIRS.has(entry.name)) continue;
if (entry.isSymbolicLink()) continue;
if (entry.isDirectory()) walk(full, files, visited);
else files.push(full);
}
return files;
}
function normalizeSpecifiers(text) {
let out = text;
for (const other of OTHER_SCOPES) {
out = out
.replaceAll(`${other}/plugin-sdk`, `${scope}/plugin-sdk`)
.replaceAll(`${other}/plugin-cli`, `${scope}/plugin-cli`);
}
return out;
}
function rewritePackageJson(file, text) {
const indent = detectIndent(text);
let pkg;
try {
pkg = JSON.parse(text);
} catch {
return text;
}
for (const sectionName of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
const section = pkg[sectionName];
if (!section) continue;
const rebuilt = {};
for (const dep of Object.keys(section)) {
const kind = parseSdkDep(dep);
if (!kind) {
rebuilt[dep] = section[dep];
continue;
}
const target = `${scope}/${kind}`;
rebuilt[target] = kind === "plugin-sdk" ? sdkVersion : cliVersion;
}
pkg[sectionName] = rebuilt;
}
return JSON.stringify(pkg, null, indent) + (text.endsWith("\n") ? "\n" : "");
}
function parseSdkDep(dep) {
for (const scopeName of [LEGACY_SCOPE, NEW_SCOPE, "@drop"]) {
if (dep === `${scopeName}/plugin-sdk`) return "plugin-sdk";
if (dep === `${scopeName}/plugin-cli`) return "plugin-cli";
}
return null;
}
function detectIndent(text) {
const match = text.match(/\n([ \t]+)"/);
return match ? match[1] : "\t";
}
const edited = [];
const files = walk(process.cwd());
for (const file of files) {
if (LOCKFILES.has(path.basename(file))) continue;
if (!TEXT_EXT.has(path.extname(file))) continue;
const before = readFileSync(file, "utf-8");
let after = normalizeSpecifiers(before);
if (path.basename(file) === "package.json") {
after = rewritePackageJson(file, after);
}
if (after !== before) {
writeFileSync(file, after);
edited.push(path.relative(process.cwd(), file));
}
}
console.log(`Scope set to '${scope}' (sdk ${sdkVersion}, cli ${cliVersion})`);
if (edited.length) {
for (const f of edited) console.log(` rewrote ${f}`);
} else {
console.log(" no specifier changes needed");
}
const installDir = process.cwd();
if (existsSync(path.join(installDir, "package-lock.json"))) {
try {
execSync("npm install --no-audit --no-fund", { cwd: installDir, stdio: "inherit" });
} catch {
execSync("npm install --no-audit --no-fund --legacy-peer-deps", { cwd: installDir, stdio: "inherit" });
}
} else if (existsSync(path.join(installDir, "pnpm-lock.yaml"))) {
execSync("pnpm install", { cwd: installDir, stdio: "inherit" });
} else {
console.warn("no lockfile found; skipping dependency install");
}
+1
View File
@@ -6,6 +6,7 @@
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"allowJs": true,
"outDir": "dist"
},
"include": ["src/**/*", "test/**/*"]