From dfeb00b32661270b4f8151efcbd996333f7d09c4 Mon Sep 17 00:00:00 2001 From: BillyOutlast <172061051+BillyOutlast@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:12:33 -0400 Subject: [PATCH] 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. --- docs/publishing-and-distribution.md | 2 +- packages/plugin-cli/src/scaffolder.ts | 147 +++++++++++++++++- packages/plugin-cli/test/features.test.ts | 37 +++++ packages/plugin-cli/test/signer.test.ts | 11 ++ templates/release-action.yml | 7 +- .../starter-plugin/.github/workflows/ci.yml | 41 +++++ .../.github/workflows/release.yml | 7 +- templates/starter-plugin/.gitignore | 4 + templates/starter-plugin/.sdk-scope.json | 4 +- .../.github/workflows/ci.yml | 41 +++++ .../.github/workflows/release.yml | 65 ++++++++ templates/template-client-ui/.sdk-scope.json | 5 + templates/template-client-ui/drop-plugin.json | 8 +- templates/template-client-ui/package.json | 1 + .../scripts/switch-sdk-scope.mjs | 133 ++++++++++++++++ templates/template-client-ui/tsconfig.json | 1 + .../.github/workflows/ci.yml | 41 +++++ .../.github/workflows/release.yml | 65 ++++++++ templates/template-fullstack/.sdk-scope.json | 5 + templates/template-fullstack/drop-plugin.json | 10 +- templates/template-fullstack/package.json | 1 + .../scripts/switch-sdk-scope.mjs | 133 ++++++++++++++++ .../template-fullstack/test/index.test.ts | 4 +- templates/template-fullstack/tsconfig.json | 1 + .../.github/workflows/ci.yml | 41 +++++ .../.github/workflows/release.yml | 65 ++++++++ templates/template-metadata/.sdk-scope.json | 5 + templates/template-metadata/drop-plugin.json | 8 +- templates/template-metadata/package.json | 1 + .../scripts/switch-sdk-scope.mjs | 133 ++++++++++++++++ templates/template-metadata/tsconfig.json | 1 + .../template-runner/.github/workflows/ci.yml | 41 +++++ .../.github/workflows/release.yml | 65 ++++++++ templates/template-runner/.sdk-scope.json | 5 + templates/template-runner/drop-plugin.json | 8 +- templates/template-runner/package.json | 1 + .../scripts/switch-sdk-scope.mjs | 133 ++++++++++++++++ templates/template-runner/tsconfig.json | 1 + .../template-store/.github/workflows/ci.yml | 41 +++++ .../.github/workflows/release.yml | 65 ++++++++ templates/template-store/.sdk-scope.json | 5 + templates/template-store/drop-plugin.json | 8 +- templates/template-store/package.json | 1 + .../scripts/switch-sdk-scope.mjs | 133 ++++++++++++++++ templates/template-store/tsconfig.json | 1 + 45 files changed, 1517 insertions(+), 19 deletions(-) create mode 100644 templates/starter-plugin/.github/workflows/ci.yml create mode 100644 templates/starter-plugin/.gitignore create mode 100644 templates/template-client-ui/.github/workflows/ci.yml create mode 100644 templates/template-client-ui/.github/workflows/release.yml create mode 100644 templates/template-client-ui/.sdk-scope.json create mode 100644 templates/template-client-ui/scripts/switch-sdk-scope.mjs create mode 100644 templates/template-fullstack/.github/workflows/ci.yml create mode 100644 templates/template-fullstack/.github/workflows/release.yml create mode 100644 templates/template-fullstack/.sdk-scope.json create mode 100644 templates/template-fullstack/scripts/switch-sdk-scope.mjs create mode 100644 templates/template-metadata/.github/workflows/ci.yml create mode 100644 templates/template-metadata/.github/workflows/release.yml create mode 100644 templates/template-metadata/.sdk-scope.json create mode 100644 templates/template-metadata/scripts/switch-sdk-scope.mjs create mode 100644 templates/template-runner/.github/workflows/ci.yml create mode 100644 templates/template-runner/.github/workflows/release.yml create mode 100644 templates/template-runner/.sdk-scope.json create mode 100644 templates/template-runner/scripts/switch-sdk-scope.mjs create mode 100644 templates/template-store/.github/workflows/ci.yml create mode 100644 templates/template-store/.github/workflows/release.yml create mode 100644 templates/template-store/.sdk-scope.json create mode 100644 templates/template-store/scripts/switch-sdk-scope.mjs diff --git a/docs/publishing-and-distribution.md b/docs/publishing-and-distribution.md index 403b879..17c56a1 100644 --- a/docs/publishing-and-distribution.md +++ b/docs/publishing-and-distribution.md @@ -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. --- diff --git a/packages/plugin-cli/src/scaffolder.ts b/packages/plugin-cli/src/scaffolder.ts index 66145a5..5410ec3 100644 --- a/packages/plugin-cli/src/scaffolder.ts +++ b/packages/plugin-cli/src/scaffolder.ts @@ -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; + 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; + try { + pkg = JSON.parse(text); + } catch { + return text; + } + for (const sectionName of [ + "dependencies", + "devDependencies", + "peerDependencies", + "optionalDependencies", + ]) { + const section = pkg[sectionName] as Record | 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(), +): Promise { + 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 { + 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 diff --git a/packages/plugin-cli/test/features.test.ts b/packages/plugin-cli/test/features.test.ts index 9870cfc..7c98b5b 100644 --- a/packages/plugin-cli/test/features.test.ts +++ b/packages/plugin-cli/test/features.test.ts @@ -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 }); }); diff --git a/packages/plugin-cli/test/signer.test.ts b/packages/plugin-cli/test/signer.test.ts index fe34f9e..26fc1fc 100644 --- a/packages/plugin-cli/test/signer.test.ts +++ b/packages/plugin-cli/test/signer.test.ts @@ -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 }); } diff --git a/templates/release-action.yml b/templates/release-action.yml index c59ccb9..1c63ede 100644 --- a/templates/release-action.yml +++ b/templates/release-action.yml @@ -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 diff --git a/templates/starter-plugin/.github/workflows/ci.yml b/templates/starter-plugin/.github/workflows/ci.yml new file mode 100644 index 0000000..8e35190 --- /dev/null +++ b/templates/starter-plugin/.github/workflows/ci.yml @@ -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/checkout@v7.0.1 + + - name: Set up Node.js 22 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/setup-node@v7.0.0 + 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 . diff --git a/templates/starter-plugin/.github/workflows/release.yml b/templates/starter-plugin/.github/workflows/release.yml index 6ccdda6..a8b57d0 100644 --- a/templates/starter-plugin/.github/workflows/release.yml +++ b/templates/starter-plugin/.github/workflows/release.yml @@ -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 diff --git a/templates/starter-plugin/.gitignore b/templates/starter-plugin/.gitignore new file mode 100644 index 0000000..cde3f74 --- /dev/null +++ b/templates/starter-plugin/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +dist-package/ +*.dropplugin diff --git a/templates/starter-plugin/.sdk-scope.json b/templates/starter-plugin/.sdk-scope.json index 84c997c..eaa5902 100644 --- a/templates/starter-plugin/.sdk-scope.json +++ b/templates/starter-plugin/.sdk-scope.json @@ -1,5 +1,5 @@ { "sdk": "@droposs", - "sdkVersion": "^0.6.2", - "cliVersion": "^0.6.2" + "sdkVersion": "^0.7.0", + "cliVersion": "^0.7.0" } diff --git a/templates/template-client-ui/.github/workflows/ci.yml b/templates/template-client-ui/.github/workflows/ci.yml new file mode 100644 index 0000000..8e35190 --- /dev/null +++ b/templates/template-client-ui/.github/workflows/ci.yml @@ -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/checkout@v7.0.1 + + - name: Set up Node.js 22 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/setup-node@v7.0.0 + 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 . diff --git a/templates/template-client-ui/.github/workflows/release.yml b/templates/template-client-ui/.github/workflows/release.yml new file mode 100644 index 0000000..a8b57d0 --- /dev/null +++ b/templates/template-client-ui/.github/workflows/release.yml @@ -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/checkout@v7.0.1 + + - name: Set up Node.js 22 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/setup-node@v7.0.0 + 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 diff --git a/templates/template-client-ui/.sdk-scope.json b/templates/template-client-ui/.sdk-scope.json new file mode 100644 index 0000000..eaa5902 --- /dev/null +++ b/templates/template-client-ui/.sdk-scope.json @@ -0,0 +1,5 @@ +{ + "sdk": "@droposs", + "sdkVersion": "^0.7.0", + "cliVersion": "^0.7.0" +} diff --git a/templates/template-client-ui/drop-plugin.json b/templates/template-client-ui/drop-plugin.json index 2c0c9d0..f0a7284 100644 --- a/templates/template-client-ui/drop-plugin.json +++ b/templates/template-client-ui/drop-plugin.json @@ -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" } } diff --git a/templates/template-client-ui/package.json b/templates/template-client-ui/package.json index 8ad2762..9800f2d 100644 --- a/templates/template-client-ui/package.json +++ b/templates/template-client-ui/package.json @@ -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", diff --git a/templates/template-client-ui/scripts/switch-sdk-scope.mjs b/templates/template-client-ui/scripts/switch-sdk-scope.mjs new file mode 100644 index 0000000..af01612 --- /dev/null +++ b/templates/template-client-ui/scripts/switch-sdk-scope.mjs @@ -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"); +} diff --git a/templates/template-client-ui/tsconfig.json b/templates/template-client-ui/tsconfig.json index 8b7fffa..2f3ffd5 100644 --- a/templates/template-client-ui/tsconfig.json +++ b/templates/template-client-ui/tsconfig.json @@ -6,6 +6,7 @@ "esModuleInterop": true, "strict": true, "skipLibCheck": true, + "allowJs": true, "outDir": "dist" }, "include": ["src/**/*", "test/**/*"] diff --git a/templates/template-fullstack/.github/workflows/ci.yml b/templates/template-fullstack/.github/workflows/ci.yml new file mode 100644 index 0000000..8e35190 --- /dev/null +++ b/templates/template-fullstack/.github/workflows/ci.yml @@ -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/checkout@v7.0.1 + + - name: Set up Node.js 22 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/setup-node@v7.0.0 + 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 . diff --git a/templates/template-fullstack/.github/workflows/release.yml b/templates/template-fullstack/.github/workflows/release.yml new file mode 100644 index 0000000..a8b57d0 --- /dev/null +++ b/templates/template-fullstack/.github/workflows/release.yml @@ -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/checkout@v7.0.1 + + - name: Set up Node.js 22 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/setup-node@v7.0.0 + 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 diff --git a/templates/template-fullstack/.sdk-scope.json b/templates/template-fullstack/.sdk-scope.json new file mode 100644 index 0000000..eaa5902 --- /dev/null +++ b/templates/template-fullstack/.sdk-scope.json @@ -0,0 +1,5 @@ +{ + "sdk": "@droposs", + "sdkVersion": "^0.7.0", + "cliVersion": "^0.7.0" +} diff --git a/templates/template-fullstack/drop-plugin.json b/templates/template-fullstack/drop-plugin.json index b353a79..b4292a6 100644 --- a/templates/template-fullstack/drop-plugin.json +++ b/templates/template-fullstack/drop-plugin.json @@ -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" } } diff --git a/templates/template-fullstack/package.json b/templates/template-fullstack/package.json index 839c2a3..2192b45 100644 --- a/templates/template-fullstack/package.json +++ b/templates/template-fullstack/package.json @@ -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", diff --git a/templates/template-fullstack/scripts/switch-sdk-scope.mjs b/templates/template-fullstack/scripts/switch-sdk-scope.mjs new file mode 100644 index 0000000..af01612 --- /dev/null +++ b/templates/template-fullstack/scripts/switch-sdk-scope.mjs @@ -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"); +} diff --git a/templates/template-fullstack/test/index.test.ts b/templates/template-fullstack/test/index.test.ts index b07446f..41a61a2 100644 --- a/templates/template-fullstack/test/index.test.ts +++ b/templates/template-fullstack/test/index.test.ts @@ -19,7 +19,7 @@ test("MyFullstackPlugin registers status and config routes", async () => { const statusRes = (await status.handler(null, { params: {}, query: {}, - readJson: async () => ({}), + readJson: async () => ({}) 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 () => ({ mode: "full" }) as T, })) as { saved: boolean; synced: number }; assert.equal(configRes.saved, true); assert.equal(configRes.synced, 1); diff --git a/templates/template-fullstack/tsconfig.json b/templates/template-fullstack/tsconfig.json index 8b7fffa..2f3ffd5 100644 --- a/templates/template-fullstack/tsconfig.json +++ b/templates/template-fullstack/tsconfig.json @@ -6,6 +6,7 @@ "esModuleInterop": true, "strict": true, "skipLibCheck": true, + "allowJs": true, "outDir": "dist" }, "include": ["src/**/*", "test/**/*"] diff --git a/templates/template-metadata/.github/workflows/ci.yml b/templates/template-metadata/.github/workflows/ci.yml new file mode 100644 index 0000000..8e35190 --- /dev/null +++ b/templates/template-metadata/.github/workflows/ci.yml @@ -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/checkout@v7.0.1 + + - name: Set up Node.js 22 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/setup-node@v7.0.0 + 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 . diff --git a/templates/template-metadata/.github/workflows/release.yml b/templates/template-metadata/.github/workflows/release.yml new file mode 100644 index 0000000..a8b57d0 --- /dev/null +++ b/templates/template-metadata/.github/workflows/release.yml @@ -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/checkout@v7.0.1 + + - name: Set up Node.js 22 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/setup-node@v7.0.0 + 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 diff --git a/templates/template-metadata/.sdk-scope.json b/templates/template-metadata/.sdk-scope.json new file mode 100644 index 0000000..eaa5902 --- /dev/null +++ b/templates/template-metadata/.sdk-scope.json @@ -0,0 +1,5 @@ +{ + "sdk": "@droposs", + "sdkVersion": "^0.7.0", + "cliVersion": "^0.7.0" +} diff --git a/templates/template-metadata/drop-plugin.json b/templates/template-metadata/drop-plugin.json index d9d47fc..1dc4d92 100644 --- a/templates/template-metadata/drop-plugin.json +++ b/templates/template-metadata/drop-plugin.json @@ -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" } } diff --git a/templates/template-metadata/package.json b/templates/template-metadata/package.json index f7f8fc8..a87e732 100644 --- a/templates/template-metadata/package.json +++ b/templates/template-metadata/package.json @@ -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", diff --git a/templates/template-metadata/scripts/switch-sdk-scope.mjs b/templates/template-metadata/scripts/switch-sdk-scope.mjs new file mode 100644 index 0000000..af01612 --- /dev/null +++ b/templates/template-metadata/scripts/switch-sdk-scope.mjs @@ -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"); +} diff --git a/templates/template-metadata/tsconfig.json b/templates/template-metadata/tsconfig.json index 8b7fffa..2f3ffd5 100644 --- a/templates/template-metadata/tsconfig.json +++ b/templates/template-metadata/tsconfig.json @@ -6,6 +6,7 @@ "esModuleInterop": true, "strict": true, "skipLibCheck": true, + "allowJs": true, "outDir": "dist" }, "include": ["src/**/*", "test/**/*"] diff --git a/templates/template-runner/.github/workflows/ci.yml b/templates/template-runner/.github/workflows/ci.yml new file mode 100644 index 0000000..8e35190 --- /dev/null +++ b/templates/template-runner/.github/workflows/ci.yml @@ -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/checkout@v7.0.1 + + - name: Set up Node.js 22 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/setup-node@v7.0.0 + 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 . diff --git a/templates/template-runner/.github/workflows/release.yml b/templates/template-runner/.github/workflows/release.yml new file mode 100644 index 0000000..a8b57d0 --- /dev/null +++ b/templates/template-runner/.github/workflows/release.yml @@ -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/checkout@v7.0.1 + + - name: Set up Node.js 22 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/setup-node@v7.0.0 + 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 diff --git a/templates/template-runner/.sdk-scope.json b/templates/template-runner/.sdk-scope.json new file mode 100644 index 0000000..eaa5902 --- /dev/null +++ b/templates/template-runner/.sdk-scope.json @@ -0,0 +1,5 @@ +{ + "sdk": "@droposs", + "sdkVersion": "^0.7.0", + "cliVersion": "^0.7.0" +} diff --git a/templates/template-runner/drop-plugin.json b/templates/template-runner/drop-plugin.json index 3d8478f..4edd8ad 100644 --- a/templates/template-runner/drop-plugin.json +++ b/templates/template-runner/drop-plugin.json @@ -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" } } diff --git a/templates/template-runner/package.json b/templates/template-runner/package.json index 95e9194..5f05a80 100644 --- a/templates/template-runner/package.json +++ b/templates/template-runner/package.json @@ -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", diff --git a/templates/template-runner/scripts/switch-sdk-scope.mjs b/templates/template-runner/scripts/switch-sdk-scope.mjs new file mode 100644 index 0000000..af01612 --- /dev/null +++ b/templates/template-runner/scripts/switch-sdk-scope.mjs @@ -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"); +} diff --git a/templates/template-runner/tsconfig.json b/templates/template-runner/tsconfig.json index 8b7fffa..2f3ffd5 100644 --- a/templates/template-runner/tsconfig.json +++ b/templates/template-runner/tsconfig.json @@ -6,6 +6,7 @@ "esModuleInterop": true, "strict": true, "skipLibCheck": true, + "allowJs": true, "outDir": "dist" }, "include": ["src/**/*", "test/**/*"] diff --git a/templates/template-store/.github/workflows/ci.yml b/templates/template-store/.github/workflows/ci.yml new file mode 100644 index 0000000..8e35190 --- /dev/null +++ b/templates/template-store/.github/workflows/ci.yml @@ -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/checkout@v7.0.1 + + - name: Set up Node.js 22 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/setup-node@v7.0.0 + 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 . diff --git a/templates/template-store/.github/workflows/release.yml b/templates/template-store/.github/workflows/release.yml new file mode 100644 index 0000000..a8b57d0 --- /dev/null +++ b/templates/template-store/.github/workflows/release.yml @@ -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/checkout@v7.0.1 + + - name: Set up Node.js 22 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/setup-node@v7.0.0 + 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 diff --git a/templates/template-store/.sdk-scope.json b/templates/template-store/.sdk-scope.json new file mode 100644 index 0000000..eaa5902 --- /dev/null +++ b/templates/template-store/.sdk-scope.json @@ -0,0 +1,5 @@ +{ + "sdk": "@droposs", + "sdkVersion": "^0.7.0", + "cliVersion": "^0.7.0" +} diff --git a/templates/template-store/drop-plugin.json b/templates/template-store/drop-plugin.json index 212b8e9..c47e028 100644 --- a/templates/template-store/drop-plugin.json +++ b/templates/template-store/drop-plugin.json @@ -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" } } diff --git a/templates/template-store/package.json b/templates/template-store/package.json index 1b28385..0f39aed 100644 --- a/templates/template-store/package.json +++ b/templates/template-store/package.json @@ -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", diff --git a/templates/template-store/scripts/switch-sdk-scope.mjs b/templates/template-store/scripts/switch-sdk-scope.mjs new file mode 100644 index 0000000..af01612 --- /dev/null +++ b/templates/template-store/scripts/switch-sdk-scope.mjs @@ -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"); +} diff --git a/templates/template-store/tsconfig.json b/templates/template-store/tsconfig.json index 8b7fffa..2f3ffd5 100644 --- a/templates/template-store/tsconfig.json +++ b/templates/template-store/tsconfig.json @@ -6,6 +6,7 @@ "esModuleInterop": true, "strict": true, "skipLibCheck": true, + "allowJs": true, "outDir": "dist" }, "include": ["src/**/*", "test/**/*"]