@rindrics/initrepo 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +3 -1
- package/.github/workflows/tagpr.yml +4 -0
- package/CHANGELOG.md +5 -0
- package/dist/cli.js +333 -64
- package/docs/adr/0002-embed-templates-at-build-time.md +126 -0
- package/package.json +3 -3
- package/scripts/embed-templates.ts +64 -0
- package/src/commands/prepare-release.ts +39 -24
- package/src/config.ts +1 -0
- package/src/generators/project.test.ts +10 -11
- package/src/generators/project.ts +22 -43
- package/src/templates/common/workflows/publish.yml.ejs +36 -0
- package/src/templates/common/workflows/tagpr.yml.ejs +4 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -19,3 +19,7 @@ jobs:
|
|
|
19
19
|
- uses: Songmu/tagpr@v1
|
|
20
20
|
env:
|
|
21
21
|
GITHUB_TOKEN: ${{ secrets.PAT_FOR_TAGPR }}
|
|
22
|
+
GIT_COMMITTER_NAME: github-actions[bot]
|
|
23
|
+
GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
|
|
24
|
+
GIT_AUTHOR_NAME: github-actions[bot]
|
|
25
|
+
GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v0.2.0](https://github.com/Rindrics/initrepo/compare/v0.1.5...v0.2.0) - 2025-12-28
|
|
4
|
+
- ci: commit as GitHub Actions bot by @Rindrics in https://github.com/Rindrics/initrepo/pull/20
|
|
5
|
+
- feat: generate publish.yml from prepare-release command by @Rindrics in https://github.com/Rindrics/initrepo/pull/21
|
|
6
|
+
- feat: embed templates at build by @Rindrics in https://github.com/Rindrics/initrepo/pull/23
|
|
7
|
+
|
|
3
8
|
## [v0.1.5](https://github.com/Rindrics/initrepo/compare/v0.1.4...v0.1.5) - 2025-12-28
|
|
4
9
|
- fix: correct binary name by @Rindrics in https://github.com/Rindrics/initrepo/pull/18
|
|
5
10
|
|
package/dist/cli.js
CHANGED
|
@@ -3321,8 +3321,8 @@ var require_light = __commonJS((exports, module) => {
|
|
|
3321
3321
|
return this.Promise.resolve();
|
|
3322
3322
|
}
|
|
3323
3323
|
yieldLoop(t = 0) {
|
|
3324
|
-
return new this.Promise(function(
|
|
3325
|
-
return setTimeout(
|
|
3324
|
+
return new this.Promise(function(resolve, reject) {
|
|
3325
|
+
return setTimeout(resolve, t);
|
|
3326
3326
|
});
|
|
3327
3327
|
}
|
|
3328
3328
|
computePenalty() {
|
|
@@ -3533,15 +3533,15 @@ var require_light = __commonJS((exports, module) => {
|
|
|
3533
3533
|
return this._queue.length === 0;
|
|
3534
3534
|
}
|
|
3535
3535
|
async _tryToRun() {
|
|
3536
|
-
var args, cb, error, reject,
|
|
3536
|
+
var args, cb, error, reject, resolve, returned, task;
|
|
3537
3537
|
if (this._running < 1 && this._queue.length > 0) {
|
|
3538
3538
|
this._running++;
|
|
3539
|
-
({ task, args, resolve
|
|
3539
|
+
({ task, args, resolve, reject } = this._queue.shift());
|
|
3540
3540
|
cb = await async function() {
|
|
3541
3541
|
try {
|
|
3542
3542
|
returned = await task(...args);
|
|
3543
3543
|
return function() {
|
|
3544
|
-
return
|
|
3544
|
+
return resolve(returned);
|
|
3545
3545
|
};
|
|
3546
3546
|
} catch (error1) {
|
|
3547
3547
|
error = error1;
|
|
@@ -3556,13 +3556,13 @@ var require_light = __commonJS((exports, module) => {
|
|
|
3556
3556
|
}
|
|
3557
3557
|
}
|
|
3558
3558
|
schedule(task, ...args) {
|
|
3559
|
-
var promise, reject,
|
|
3560
|
-
|
|
3559
|
+
var promise, reject, resolve;
|
|
3560
|
+
resolve = reject = null;
|
|
3561
3561
|
promise = new this.Promise(function(_resolve, _reject) {
|
|
3562
|
-
|
|
3562
|
+
resolve = _resolve;
|
|
3563
3563
|
return reject = _reject;
|
|
3564
3564
|
});
|
|
3565
|
-
this._queue.push({ task, args, resolve
|
|
3565
|
+
this._queue.push({ task, args, resolve, reject });
|
|
3566
3566
|
this._tryToRun();
|
|
3567
3567
|
return promise;
|
|
3568
3568
|
}
|
|
@@ -3966,14 +3966,14 @@ var require_light = __commonJS((exports, module) => {
|
|
|
3966
3966
|
counts = this._states.counts;
|
|
3967
3967
|
return counts[0] + counts[1] + counts[2] + counts[3] === at;
|
|
3968
3968
|
};
|
|
3969
|
-
return new this.Promise((
|
|
3969
|
+
return new this.Promise((resolve, reject) => {
|
|
3970
3970
|
if (finished()) {
|
|
3971
|
-
return
|
|
3971
|
+
return resolve();
|
|
3972
3972
|
} else {
|
|
3973
3973
|
return this.on("done", () => {
|
|
3974
3974
|
if (finished()) {
|
|
3975
3975
|
this.removeAllListeners("done");
|
|
3976
|
-
return
|
|
3976
|
+
return resolve();
|
|
3977
3977
|
}
|
|
3978
3978
|
});
|
|
3979
3979
|
}
|
|
@@ -4066,9 +4066,9 @@ var require_light = __commonJS((exports, module) => {
|
|
|
4066
4066
|
options = parser$5.load(options, this.jobDefaults);
|
|
4067
4067
|
}
|
|
4068
4068
|
task = (...args2) => {
|
|
4069
|
-
return new this.Promise(function(
|
|
4069
|
+
return new this.Promise(function(resolve, reject) {
|
|
4070
4070
|
return fn(...args2, function(...args3) {
|
|
4071
|
-
return (args3[0] != null ? reject :
|
|
4071
|
+
return (args3[0] != null ? reject : resolve)(args3);
|
|
4072
4072
|
});
|
|
4073
4073
|
});
|
|
4074
4074
|
};
|
|
@@ -4207,7 +4207,7 @@ var {
|
|
|
4207
4207
|
// package.json
|
|
4208
4208
|
var package_default = {
|
|
4209
4209
|
name: "@rindrics/initrepo",
|
|
4210
|
-
version: "0.
|
|
4210
|
+
version: "0.2.0",
|
|
4211
4211
|
description: "setup GitHub repo with dev tools",
|
|
4212
4212
|
type: "module",
|
|
4213
4213
|
bin: {
|
|
@@ -4215,11 +4215,11 @@ var package_default = {
|
|
|
4215
4215
|
},
|
|
4216
4216
|
scripts: {
|
|
4217
4217
|
dev: "bun run src/cli.ts",
|
|
4218
|
-
build: "bun build src/cli.ts --outdir dist --target node",
|
|
4218
|
+
build: "bun scripts/embed-templates.ts && bun build src/cli.ts --outdir dist --target node",
|
|
4219
4219
|
lint: "biome lint src",
|
|
4220
4220
|
format: "biome format src --write",
|
|
4221
4221
|
check: "biome check src",
|
|
4222
|
-
test: "bun test",
|
|
4222
|
+
test: "bun scripts/embed-templates.ts && bun test",
|
|
4223
4223
|
clean: "rm -rf test-* dist",
|
|
4224
4224
|
prepare: "husky"
|
|
4225
4225
|
},
|
|
@@ -4257,6 +4257,7 @@ import * as path from "node:path";
|
|
|
4257
4257
|
// src/config.ts
|
|
4258
4258
|
var GITHUB_ACTIONS = {
|
|
4259
4259
|
"actions/checkout": "v6",
|
|
4260
|
+
"actions/setup-node": "v4",
|
|
4260
4261
|
"Songmu/tagpr": "v1",
|
|
4261
4262
|
"oven-sh/setup-bun": "v2",
|
|
4262
4263
|
"github/codeql-action": "v3"
|
|
@@ -4317,8 +4318,272 @@ async function getLatestVersions(packageNames) {
|
|
|
4317
4318
|
return versions;
|
|
4318
4319
|
}
|
|
4319
4320
|
|
|
4321
|
+
// src/generators/embedded-templates.ts
|
|
4322
|
+
var EMBEDDED_TEMPLATES = {
|
|
4323
|
+
"common/dependabot.yml.ejs": `version: 2
|
|
4324
|
+
updates:
|
|
4325
|
+
<% if (lang === 'typescript') { -%>
|
|
4326
|
+
- package-ecosystem: "npm"
|
|
4327
|
+
directory: "/"
|
|
4328
|
+
schedule:
|
|
4329
|
+
interval: "weekly"
|
|
4330
|
+
<% } -%>
|
|
4331
|
+
- package-ecosystem: "github-actions"
|
|
4332
|
+
directory: "/"
|
|
4333
|
+
schedule:
|
|
4334
|
+
interval: "weekly"
|
|
4335
|
+
`,
|
|
4336
|
+
"common/release.yml.ejs": `changelog:
|
|
4337
|
+
exclude:
|
|
4338
|
+
labels:
|
|
4339
|
+
- tagpr
|
|
4340
|
+
`,
|
|
4341
|
+
"common/workflows/publish.yml.ejs": `name: Publish to npm
|
|
4342
|
+
|
|
4343
|
+
on:
|
|
4344
|
+
push:
|
|
4345
|
+
tags:
|
|
4346
|
+
- 'v*'
|
|
4347
|
+
workflow_dispatch:
|
|
4348
|
+
|
|
4349
|
+
permissions:
|
|
4350
|
+
contents: read
|
|
4351
|
+
id-token: write
|
|
4352
|
+
|
|
4353
|
+
jobs:
|
|
4354
|
+
publish:
|
|
4355
|
+
runs-on: ubuntu-latest
|
|
4356
|
+
steps:
|
|
4357
|
+
- uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
|
|
4358
|
+
|
|
4359
|
+
- uses: oven-sh/setup-bun@<%= actionVersions['oven-sh/setup-bun'] %>
|
|
4360
|
+
|
|
4361
|
+
- uses: actions/setup-node@<%= actionVersions['actions/setup-node'] %>
|
|
4362
|
+
with:
|
|
4363
|
+
node-version: '24'
|
|
4364
|
+
registry-url: 'https://registry.npmjs.org'
|
|
4365
|
+
|
|
4366
|
+
- run: bun install --frozen-lockfile
|
|
4367
|
+
|
|
4368
|
+
- name: Build
|
|
4369
|
+
run: bun run build
|
|
4370
|
+
|
|
4371
|
+
- name: Test
|
|
4372
|
+
run: bun test
|
|
4373
|
+
|
|
4374
|
+
- name: Publish to npm with provenance
|
|
4375
|
+
run: npm publish --access public --provenance
|
|
4376
|
+
|
|
4377
|
+
`,
|
|
4378
|
+
"common/workflows/tagpr.yml.ejs": `name: tagpr
|
|
4379
|
+
|
|
4380
|
+
on:
|
|
4381
|
+
push:
|
|
4382
|
+
branches:
|
|
4383
|
+
- main
|
|
4384
|
+
|
|
4385
|
+
permissions:
|
|
4386
|
+
contents: write
|
|
4387
|
+
pull-requests: write
|
|
4388
|
+
|
|
4389
|
+
jobs:
|
|
4390
|
+
tagpr:
|
|
4391
|
+
runs-on: ubuntu-latest
|
|
4392
|
+
steps:
|
|
4393
|
+
- uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
|
|
4394
|
+
<% if (isDevcode) { -%>
|
|
4395
|
+
# TODO: After replace-devcode, add token: \${{ secrets.PAT_FOR_TAGPR }}
|
|
4396
|
+
<% } else { -%>
|
|
4397
|
+
with:
|
|
4398
|
+
token: \${{ secrets.PAT_FOR_TAGPR }}
|
|
4399
|
+
<% } -%>
|
|
4400
|
+
|
|
4401
|
+
- uses: Songmu/tagpr@<%= actionVersions['Songmu/tagpr'] %>
|
|
4402
|
+
env:
|
|
4403
|
+
<% if (isDevcode) { -%>
|
|
4404
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
4405
|
+
# TODO: After replace-devcode, use PAT_FOR_TAGPR instead
|
|
4406
|
+
<% } else { -%>
|
|
4407
|
+
GITHUB_TOKEN: \${{ secrets.PAT_FOR_TAGPR }}
|
|
4408
|
+
GIT_COMMITTER_NAME: github-actions[bot]
|
|
4409
|
+
GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
|
|
4410
|
+
GIT_AUTHOR_NAME: github-actions[bot]
|
|
4411
|
+
GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
|
|
4412
|
+
<% } -%>
|
|
4413
|
+
`,
|
|
4414
|
+
"typescript/.tagpr.ejs": `# tagpr configuration
|
|
4415
|
+
# https://github.com/Songmu/tagpr
|
|
4416
|
+
|
|
4417
|
+
[tagpr]
|
|
4418
|
+
versionFile = "package.json"
|
|
4419
|
+
`,
|
|
4420
|
+
"typescript/codeql/codeql-config.yml.ejs": `name: "CodeQL config for <%= name %>"
|
|
4421
|
+
|
|
4422
|
+
paths:
|
|
4423
|
+
- src
|
|
4424
|
+
|
|
4425
|
+
paths-ignore:
|
|
4426
|
+
- node_modules/
|
|
4427
|
+
`,
|
|
4428
|
+
"typescript/package.json.ejs": `{
|
|
4429
|
+
"name": "<%= name %>",
|
|
4430
|
+
"version": "0.0.0",
|
|
4431
|
+
<% if (isDevcode) { -%>
|
|
4432
|
+
"private": true,
|
|
4433
|
+
<% } -%>
|
|
4434
|
+
"description": "",
|
|
4435
|
+
"type": "module",
|
|
4436
|
+
"scripts": {
|
|
4437
|
+
"dev": "bun run src/index.ts",
|
|
4438
|
+
"build": "bun build src/index.ts --outdir dist --target node",
|
|
4439
|
+
"lint": "biome lint src",
|
|
4440
|
+
"format": "biome format src --write",
|
|
4441
|
+
"check": "biome check src",
|
|
4442
|
+
"test": "bun test",
|
|
4443
|
+
"prepare": "husky"
|
|
4444
|
+
},
|
|
4445
|
+
"keywords": [],
|
|
4446
|
+
"author": "<%= author %>",
|
|
4447
|
+
"license": "MIT",
|
|
4448
|
+
"devDependencies": {
|
|
4449
|
+
"@biomejs/biome": "^<%= versions['@biomejs/biome'] %>",
|
|
4450
|
+
"@commitlint/cli": "^<%= versions['@commitlint/cli'] %>",
|
|
4451
|
+
"@commitlint/config-conventional": "^<%= versions['@commitlint/config-conventional'] %>",
|
|
4452
|
+
"bun-types": "^<%= versions['bun-types'] %>",
|
|
4453
|
+
"husky": "^<%= versions['husky'] %>",
|
|
4454
|
+
"typescript": "^<%= versions['typescript'] %>"
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
`,
|
|
4458
|
+
"typescript/src/index.ts.ejs": `console.log('Hello from <%= name %>!');
|
|
4459
|
+
`,
|
|
4460
|
+
"typescript/tsconfig.json.ejs": `{
|
|
4461
|
+
"compilerOptions": {
|
|
4462
|
+
"target": "ES2022",
|
|
4463
|
+
"module": "ESNext",
|
|
4464
|
+
"moduleResolution": "bundler",
|
|
4465
|
+
"strict": true,
|
|
4466
|
+
"esModuleInterop": true,
|
|
4467
|
+
"skipLibCheck": true,
|
|
4468
|
+
"declaration": true,
|
|
4469
|
+
"outDir": "./dist",
|
|
4470
|
+
"rootDir": "./src",
|
|
4471
|
+
"types": ["bun-types"]
|
|
4472
|
+
},
|
|
4473
|
+
"include": ["src/**/*"],
|
|
4474
|
+
"exclude": ["node_modules", "dist"]
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4477
|
+
`,
|
|
4478
|
+
"typescript/workflows/ci.yml.ejs": `name: CI
|
|
4479
|
+
|
|
4480
|
+
on:
|
|
4481
|
+
pull_request:
|
|
4482
|
+
branches:
|
|
4483
|
+
- main
|
|
4484
|
+
paths:
|
|
4485
|
+
- 'src/**'
|
|
4486
|
+
- 'package.json'
|
|
4487
|
+
- 'tsconfig.json'
|
|
4488
|
+
- 'biome.json'
|
|
4489
|
+
- 'bun.lock'
|
|
4490
|
+
- '.github/workflows/ci.yml'
|
|
4491
|
+
push:
|
|
4492
|
+
branches:
|
|
4493
|
+
- main
|
|
4494
|
+
paths:
|
|
4495
|
+
- 'src/**'
|
|
4496
|
+
- 'package.json'
|
|
4497
|
+
- 'tsconfig.json'
|
|
4498
|
+
- 'biome.json'
|
|
4499
|
+
- 'bun.lock'
|
|
4500
|
+
- '.github/workflows/ci.yml'
|
|
4501
|
+
|
|
4502
|
+
permissions:
|
|
4503
|
+
contents: read
|
|
4504
|
+
|
|
4505
|
+
jobs:
|
|
4506
|
+
check:
|
|
4507
|
+
runs-on: ubuntu-latest
|
|
4508
|
+
steps:
|
|
4509
|
+
- uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
|
|
4510
|
+
|
|
4511
|
+
- uses: oven-sh/setup-bun@<%= actionVersions['oven-sh/setup-bun'] %>
|
|
4512
|
+
with:
|
|
4513
|
+
bun-version: latest
|
|
4514
|
+
|
|
4515
|
+
- run: bun install --frozen-lockfile
|
|
4516
|
+
|
|
4517
|
+
- name: Biome check
|
|
4518
|
+
run: bun run check
|
|
4519
|
+
|
|
4520
|
+
- name: TypeScript check
|
|
4521
|
+
run: bun run build
|
|
4522
|
+
|
|
4523
|
+
test:
|
|
4524
|
+
runs-on: ubuntu-latest
|
|
4525
|
+
steps:
|
|
4526
|
+
- uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
|
|
4527
|
+
|
|
4528
|
+
- uses: oven-sh/setup-bun@<%= actionVersions['oven-sh/setup-bun'] %>
|
|
4529
|
+
with:
|
|
4530
|
+
bun-version: latest
|
|
4531
|
+
|
|
4532
|
+
- run: bun install --frozen-lockfile
|
|
4533
|
+
|
|
4534
|
+
- name: Test
|
|
4535
|
+
run: bun test
|
|
4536
|
+
`,
|
|
4537
|
+
"typescript/workflows/codeql.yml.ejs": `name: CodeQL
|
|
4538
|
+
|
|
4539
|
+
on:
|
|
4540
|
+
push:
|
|
4541
|
+
branches: [main]
|
|
4542
|
+
paths:
|
|
4543
|
+
- 'src/**'
|
|
4544
|
+
- 'package.json'
|
|
4545
|
+
- 'tsconfig.json'
|
|
4546
|
+
- '.github/workflows/codeql.yml'
|
|
4547
|
+
pull_request:
|
|
4548
|
+
branches: [main]
|
|
4549
|
+
paths:
|
|
4550
|
+
- 'src/**'
|
|
4551
|
+
- 'package.json'
|
|
4552
|
+
- 'tsconfig.json'
|
|
4553
|
+
- '.github/workflows/codeql.yml'
|
|
4554
|
+
schedule:
|
|
4555
|
+
# Run weekly on Sunday at 00:00 UTC
|
|
4556
|
+
- cron: '0 0 * * 0'
|
|
4557
|
+
workflow_dispatch:
|
|
4558
|
+
|
|
4559
|
+
jobs:
|
|
4560
|
+
analyze:
|
|
4561
|
+
name: Analyze
|
|
4562
|
+
runs-on: ubuntu-latest
|
|
4563
|
+
timeout-minutes: 10
|
|
4564
|
+
permissions:
|
|
4565
|
+
actions: read
|
|
4566
|
+
contents: read
|
|
4567
|
+
security-events: write
|
|
4568
|
+
|
|
4569
|
+
steps:
|
|
4570
|
+
- name: Checkout repository
|
|
4571
|
+
uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
|
|
4572
|
+
|
|
4573
|
+
- name: Initialize CodeQL
|
|
4574
|
+
uses: github/codeql-action/init@<%= actionVersions['github/codeql-action'] %>
|
|
4575
|
+
with:
|
|
4576
|
+
languages: javascript-typescript
|
|
4577
|
+
config-file: ./.github/codeql/codeql-config.yml
|
|
4578
|
+
|
|
4579
|
+
- name: Perform CodeQL Analysis
|
|
4580
|
+
uses: github/codeql-action/analyze@<%= actionVersions['github/codeql-action'] %>
|
|
4581
|
+
with:
|
|
4582
|
+
category: '/language:javascript-typescript'
|
|
4583
|
+
`
|
|
4584
|
+
};
|
|
4585
|
+
|
|
4320
4586
|
// src/generators/project.ts
|
|
4321
|
-
var TEMPLATES_DIR = path.join(import.meta.dir, "../templates");
|
|
4322
4587
|
var DEV_DEPENDENCIES = [
|
|
4323
4588
|
"@biomejs/biome",
|
|
4324
4589
|
"@commitlint/cli",
|
|
@@ -4338,21 +4603,11 @@ class TemplateError extends Error {
|
|
|
4338
4603
|
this.name = "TemplateError";
|
|
4339
4604
|
}
|
|
4340
4605
|
}
|
|
4341
|
-
|
|
4342
|
-
const
|
|
4343
|
-
const
|
|
4344
|
-
if (!
|
|
4345
|
-
throw new TemplateError(`
|
|
4346
|
-
}
|
|
4347
|
-
let template;
|
|
4348
|
-
try {
|
|
4349
|
-
template = await fs.readFile(fullPath, "utf-8");
|
|
4350
|
-
} catch (error) {
|
|
4351
|
-
const fsError = error;
|
|
4352
|
-
if (fsError.code === "ENOENT") {
|
|
4353
|
-
throw new TemplateError(`Template not found: "${templatePath}"`, templatePath, fsError);
|
|
4354
|
-
}
|
|
4355
|
-
throw new TemplateError(`Failed to read template "${templatePath}": ${fsError.message}`, templatePath, fsError);
|
|
4606
|
+
function loadTemplate(templatePath, data) {
|
|
4607
|
+
const normalizedPath = templatePath.replace(/\\/g, "/");
|
|
4608
|
+
const template = EMBEDDED_TEMPLATES[normalizedPath];
|
|
4609
|
+
if (!template) {
|
|
4610
|
+
throw new TemplateError(`Template not found: "${templatePath}"`, templatePath);
|
|
4356
4611
|
}
|
|
4357
4612
|
try {
|
|
4358
4613
|
return import_ejs.default.render(template, data);
|
|
@@ -4369,7 +4624,7 @@ async function generatePackageJson(options) {
|
|
|
4369
4624
|
]);
|
|
4370
4625
|
const author = detectedAuthor ?? "";
|
|
4371
4626
|
const templatePath = `${options.lang}/package.json.ejs`;
|
|
4372
|
-
const content =
|
|
4627
|
+
const content = loadTemplate(templatePath, {
|
|
4373
4628
|
name: options.projectName,
|
|
4374
4629
|
isDevcode: options.isDevcode,
|
|
4375
4630
|
author,
|
|
@@ -4381,48 +4636,52 @@ async function generatePackageJson(options) {
|
|
|
4381
4636
|
}
|
|
4382
4637
|
}
|
|
4383
4638
|
async function generateTsconfig(options) {
|
|
4384
|
-
const content =
|
|
4639
|
+
const content = loadTemplate(`${options.lang}/tsconfig.json.ejs`, {});
|
|
4385
4640
|
return { path: "tsconfig.json", content };
|
|
4386
4641
|
}
|
|
4387
4642
|
async function generateEntryPoint(options) {
|
|
4388
|
-
const content =
|
|
4643
|
+
const content = loadTemplate(`${options.lang}/src/index.ts.ejs`, {
|
|
4389
4644
|
name: options.projectName
|
|
4390
4645
|
});
|
|
4391
4646
|
return { path: "src/index.ts", content };
|
|
4392
4647
|
}
|
|
4393
4648
|
async function generateTagprConfig(options) {
|
|
4394
|
-
const content =
|
|
4649
|
+
const content = loadTemplate(`${options.lang}/.tagpr.ejs`, {});
|
|
4395
4650
|
return { path: ".tagpr", content };
|
|
4396
4651
|
}
|
|
4397
4652
|
async function generateTagprWorkflow(options, actionVersions) {
|
|
4398
|
-
const content =
|
|
4653
|
+
const content = loadTemplate("common/workflows/tagpr.yml.ejs", {
|
|
4399
4654
|
isDevcode: options.isDevcode,
|
|
4400
4655
|
actionVersions
|
|
4401
4656
|
});
|
|
4402
4657
|
return { path: ".github/workflows/tagpr.yml", content };
|
|
4403
4658
|
}
|
|
4404
4659
|
async function generateCiWorkflow(options, actionVersions) {
|
|
4405
|
-
const content =
|
|
4660
|
+
const content = loadTemplate(`${options.lang}/workflows/ci.yml.ejs`, {
|
|
4406
4661
|
actionVersions
|
|
4407
4662
|
});
|
|
4408
4663
|
return { path: ".github/workflows/ci.yml", content };
|
|
4409
4664
|
}
|
|
4410
4665
|
async function generateCodeqlWorkflow(options, actionVersions) {
|
|
4411
|
-
const content =
|
|
4666
|
+
const content = loadTemplate(`${options.lang}/workflows/codeql.yml.ejs`, {
|
|
4667
|
+
actionVersions
|
|
4668
|
+
});
|
|
4412
4669
|
return { path: ".github/workflows/codeql.yml", content };
|
|
4413
4670
|
}
|
|
4414
4671
|
async function generateCodeqlConfig(options) {
|
|
4415
|
-
const content =
|
|
4672
|
+
const content = loadTemplate(`${options.lang}/codeql/codeql-config.yml.ejs`, {
|
|
4673
|
+
name: options.projectName
|
|
4674
|
+
});
|
|
4416
4675
|
return { path: ".github/codeql/codeql-config.yml", content };
|
|
4417
4676
|
}
|
|
4418
4677
|
async function generateDependabot(options) {
|
|
4419
|
-
const content =
|
|
4678
|
+
const content = loadTemplate("common/dependabot.yml.ejs", {
|
|
4420
4679
|
lang: options.lang
|
|
4421
4680
|
});
|
|
4422
4681
|
return { path: ".github/dependabot.yml", content };
|
|
4423
4682
|
}
|
|
4424
4683
|
async function generateReleaseConfig() {
|
|
4425
|
-
const content =
|
|
4684
|
+
const content = loadTemplate("common/release.yml.ejs", {});
|
|
4426
4685
|
return { path: ".github/release.yml", content };
|
|
4427
4686
|
}
|
|
4428
4687
|
|
|
@@ -5265,8 +5524,8 @@ function withCustomRequest(customRequest) {
|
|
|
5265
5524
|
|
|
5266
5525
|
// node_modules/@octokit/auth-token/dist-bundle/index.js
|
|
5267
5526
|
var b64url = "(?:[a-zA-Z0-9_-]+)";
|
|
5268
|
-
var
|
|
5269
|
-
var jwtRE = new RegExp(`^${b64url}${
|
|
5527
|
+
var sep = "\\.";
|
|
5528
|
+
var jwtRE = new RegExp(`^${b64url}${sep}${b64url}${sep}${b64url}$`);
|
|
5270
5529
|
var isJWT = jwtRE.test.bind(jwtRE);
|
|
5271
5530
|
async function auth(token) {
|
|
5272
5531
|
const isApp = isJWT(token);
|
|
@@ -8616,7 +8875,7 @@ function getCachedAuthentication(state, auth2) {
|
|
|
8616
8875
|
return newScope === currentScope ? authentication : false;
|
|
8617
8876
|
}
|
|
8618
8877
|
async function wait(seconds) {
|
|
8619
|
-
await new Promise((
|
|
8878
|
+
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
8620
8879
|
}
|
|
8621
8880
|
async function waitForAccessToken(request2, clientId, clientType, verification) {
|
|
8622
8881
|
try {
|
|
@@ -9532,7 +9791,7 @@ async function sendRequestWithRetries(state, request2, options, createdAt, retri
|
|
|
9532
9791
|
++retries;
|
|
9533
9792
|
const awaitTime = retries * 1000;
|
|
9534
9793
|
state.log.warn(`[@octokit/auth-app] Retrying after 401 response to account for token replication delay (retry: ${retries}, wait: ${awaitTime / 1000}s)`);
|
|
9535
|
-
await new Promise((
|
|
9794
|
+
await new Promise((resolve) => setTimeout(resolve, awaitTime));
|
|
9536
9795
|
return sendRequestWithRetries(state, request2, options, createdAt, retries);
|
|
9537
9796
|
}
|
|
9538
9797
|
}
|
|
@@ -11056,25 +11315,26 @@ async function replaceInCodeqlConfig(targetDir, devcode, publishName) {
|
|
|
11056
11315
|
}
|
|
11057
11316
|
async function replaceInTagprWorkflow(targetDir, _devcode, _publishName) {
|
|
11058
11317
|
const workflowPath = path2.join(targetDir, ".github/workflows/tagpr.yml");
|
|
11059
|
-
let content;
|
|
11060
11318
|
try {
|
|
11061
|
-
|
|
11062
|
-
} catch
|
|
11063
|
-
|
|
11064
|
-
if (fsError.code === "ENOENT") {
|
|
11065
|
-
return;
|
|
11066
|
-
}
|
|
11067
|
-
throw error;
|
|
11319
|
+
await fs2.access(workflowPath);
|
|
11320
|
+
} catch {
|
|
11321
|
+
return;
|
|
11068
11322
|
}
|
|
11069
|
-
|
|
11070
|
-
content =
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11323
|
+
const actionVersions = await getLatestActionVersions();
|
|
11324
|
+
const content = await loadTemplate("common/workflows/tagpr.yml.ejs", {
|
|
11325
|
+
isDevcode: false,
|
|
11326
|
+
actionVersions
|
|
11327
|
+
});
|
|
11328
|
+
await fs2.writeFile(workflowPath, content, "utf-8");
|
|
11329
|
+
}
|
|
11330
|
+
async function generatePublishWorkflow(targetDir) {
|
|
11331
|
+
const workflowDir = path2.join(targetDir, ".github/workflows");
|
|
11332
|
+
const workflowPath = path2.join(workflowDir, "publish.yml");
|
|
11333
|
+
await fs2.mkdir(workflowDir, { recursive: true });
|
|
11334
|
+
const actionVersions = await getLatestActionVersions();
|
|
11335
|
+
const content = await loadTemplate("common/workflows/publish.yml.ejs", {
|
|
11336
|
+
actionVersions
|
|
11337
|
+
});
|
|
11078
11338
|
await fs2.writeFile(workflowPath, content, "utf-8");
|
|
11079
11339
|
}
|
|
11080
11340
|
var MANAGED_LOCATIONS = [
|
|
@@ -11157,6 +11417,14 @@ async function prepareRelease(options) {
|
|
|
11157
11417
|
console.log(` ⚠️ ${location.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
11158
11418
|
}
|
|
11159
11419
|
}
|
|
11420
|
+
console.log(`
|
|
11421
|
+
\uD83D\uDCE6 Generating release workflows:`);
|
|
11422
|
+
try {
|
|
11423
|
+
await generatePublishWorkflow(targetDir);
|
|
11424
|
+
console.log(" ✅ .github/workflows/publish.yml (npm OIDC publishing)");
|
|
11425
|
+
} catch (error) {
|
|
11426
|
+
console.log(` ⚠️ publish.yml: ${error instanceof Error ? error.message : String(error)}`);
|
|
11427
|
+
}
|
|
11160
11428
|
const unmanaged = await findUnmanagedOccurrences(targetDir, devcode);
|
|
11161
11429
|
if (unmanaged.length > 0) {
|
|
11162
11430
|
console.log(`
|
|
@@ -11172,6 +11440,7 @@ async function prepareRelease(options) {
|
|
|
11172
11440
|
console.log(` Package renamed: ${devcode} → ${options.publishName}`);
|
|
11173
11441
|
console.log(` Private flag removed`);
|
|
11174
11442
|
console.log(` Workflows updated to use PAT_FOR_TAGPR`);
|
|
11443
|
+
console.log(` publish.yml generated for npm OIDC publishing`);
|
|
11175
11444
|
console.log(`
|
|
11176
11445
|
⚠️ Action required: Set up PAT_FOR_TAGPR secret`);
|
|
11177
11446
|
console.log(` 1. Create a Personal Access Token (classic) at:`);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# ADR 0002: Embed Templates at Build Time
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`@rindrics/initrepo` uses EJS templates to generate project files. Initially, templates were loaded from the filesystem at runtime:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
const TEMPLATES_DIR = path.join(__dirname, '../templates');
|
|
13
|
+
const template = await fs.readFile(path.join(TEMPLATES_DIR, templatePath), 'utf-8');
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This approach had several issues:
|
|
17
|
+
|
|
18
|
+
1. **Bun vs Node.js incompatibility**: The code used `import.meta.dir` (Bun-specific) to resolve template paths. When running the bundled CLI with Node.js, this API is undefined, causing `path.join()` to fail with `ERR_INVALID_ARG_TYPE`.
|
|
19
|
+
|
|
20
|
+
2. **Distribution complexity**: Templates had to be copied alongside the bundled JavaScript (`cp -r src/templates dist/`), requiring:
|
|
21
|
+
- Additional build steps
|
|
22
|
+
- Correct `files` configuration in `package.json` for npm publishing
|
|
23
|
+
- Careful handling of relative paths between the bundle and templates
|
|
24
|
+
|
|
25
|
+
3. **Runtime file system dependency**: The CLI required read access to template files at runtime, adding a potential failure point.
|
|
26
|
+
|
|
27
|
+
## Alternatives Considered
|
|
28
|
+
|
|
29
|
+
### 1. Runtime file loading (original approach)
|
|
30
|
+
|
|
31
|
+
Templates as separate `.ejs` files, loaded via `fs.readFile()` at runtime.
|
|
32
|
+
|
|
33
|
+
- ✅ Simple to understand
|
|
34
|
+
- ❌ Requires correct path resolution across Bun/Node.js
|
|
35
|
+
- ❌ Templates must be distributed alongside the bundle
|
|
36
|
+
- ❌ Runtime filesystem dependency
|
|
37
|
+
|
|
38
|
+
### 2. Inline templates in source code
|
|
39
|
+
|
|
40
|
+
Templates as template literals directly in TypeScript source.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const PACKAGE_JSON_TEMPLATE = `{
|
|
44
|
+
"name": "<%= name %>",
|
|
45
|
+
...
|
|
46
|
+
}`;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- ✅ Single-file distribution
|
|
50
|
+
- ✅ No build step for templates
|
|
51
|
+
- ❌ Poor editing experience (no syntax highlighting for EJS/YAML)
|
|
52
|
+
- ❌ Difficult to review changes in PRs
|
|
53
|
+
- ❌ Escaping issues with backticks and `${}`
|
|
54
|
+
|
|
55
|
+
### 3. Build-time embedding (chosen)
|
|
56
|
+
|
|
57
|
+
Templates managed as `.ejs` files, embedded into a TypeScript module at build time.
|
|
58
|
+
|
|
59
|
+
- ✅ Single-file distribution
|
|
60
|
+
- ✅ Best editing experience (syntax highlighting, linting)
|
|
61
|
+
- ✅ Clear diffs in PRs for template changes
|
|
62
|
+
- ✅ No runtime filesystem dependency
|
|
63
|
+
- ⚠️ Requires build step (`bun scripts/embed-templates.ts`)
|
|
64
|
+
|
|
65
|
+
## Decision
|
|
66
|
+
|
|
67
|
+
Embed all EJS templates as string literals in the JavaScript bundle at build time (Alternative 3).
|
|
68
|
+
|
|
69
|
+
This is a **hybrid approach**: templates are authored as `.ejs` files for developer experience, but converted to an embedded object at build time for distribution simplicity.
|
|
70
|
+
|
|
71
|
+
### Implementation
|
|
72
|
+
|
|
73
|
+
1. **Build script** (`scripts/embed-templates.ts`):
|
|
74
|
+
- Scans `src/templates/` for `.ejs` files
|
|
75
|
+
- Generates `src/generators/embedded-templates.ts` with all templates as a `Record<string, string>`
|
|
76
|
+
|
|
77
|
+
2. **Template loading** (`src/generators/project.ts`):
|
|
78
|
+
- Changed from async file reading to synchronous lookup in the embedded map
|
|
79
|
+
- Removed filesystem and path resolution logic
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Before (runtime loading)
|
|
83
|
+
export async function loadTemplate(templatePath: string, data: Record<string, unknown>): Promise<string> {
|
|
84
|
+
const template = await fs.readFile(path.join(TEMPLATES_DIR, templatePath), 'utf-8');
|
|
85
|
+
return ejs.render(template, data);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// After (embedded templates)
|
|
89
|
+
export function loadTemplate(templatePath: string, data: Record<string, unknown>): string {
|
|
90
|
+
const template = EMBEDDED_TEMPLATES[templatePath];
|
|
91
|
+
if (!template) throw new TemplateError(`Template not found: "${templatePath}"`, templatePath);
|
|
92
|
+
return ejs.render(template, data);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
3. **Build process**:
|
|
97
|
+
```json
|
|
98
|
+
"build": "bun scripts/embed-templates.ts && bun build src/cli.ts --outdir dist --target node"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Generated File
|
|
102
|
+
|
|
103
|
+
The `embedded-templates.ts` file is auto-generated and should not be edited manually. It is gitignored and regenerated on each build.
|
|
104
|
+
|
|
105
|
+
## Consequences
|
|
106
|
+
|
|
107
|
+
### Positive
|
|
108
|
+
|
|
109
|
+
- **Single-file distribution**: The CLI is now a self-contained JavaScript file with no external dependencies on template files
|
|
110
|
+
- **Cross-runtime compatibility**: Works with both Bun and Node.js without conditional path resolution
|
|
111
|
+
- **Simpler npm publishing**: No need to configure `files` to include template directories
|
|
112
|
+
- **Faster execution**: No filesystem I/O for template loading
|
|
113
|
+
- **Synchronous API**: `loadTemplate()` is now synchronous, simplifying call sites
|
|
114
|
+
|
|
115
|
+
### Negative
|
|
116
|
+
|
|
117
|
+
- **Larger bundle size**: Templates are embedded as strings in the bundle (~20KB increase)
|
|
118
|
+
- **Build step required**: Templates must be regenerated when changed (`bun scripts/embed-templates.ts`)
|
|
119
|
+
- **Generated file**: `embedded-templates.ts` is auto-generated, which may cause confusion if edited directly
|
|
120
|
+
|
|
121
|
+
### Mitigations
|
|
122
|
+
|
|
123
|
+
- The generated file includes a clear header: `// Auto-generated by scripts/embed-templates.ts - Do not edit manually`
|
|
124
|
+
- `embedded-templates.ts` is added to `.gitignore` to prevent accidental commits
|
|
125
|
+
- The build script runs automatically as part of `pnpm build`
|
|
126
|
+
- Bundle size increase is negligible for a CLI tool
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rindrics/initrepo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "setup GitHub repo with dev tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "bun run src/cli.ts",
|
|
11
|
-
"build": "bun build src/cli.ts --outdir dist --target node",
|
|
11
|
+
"build": "bun scripts/embed-templates.ts && bun build src/cli.ts --outdir dist --target node",
|
|
12
12
|
"lint": "biome lint src",
|
|
13
13
|
"format": "biome format src --write",
|
|
14
14
|
"check": "biome check src",
|
|
15
|
-
"test": "bun test",
|
|
15
|
+
"test": "bun scripts/embed-templates.ts && bun test",
|
|
16
16
|
"clean": "rm -rf test-* dist",
|
|
17
17
|
"prepare": "husky"
|
|
18
18
|
},
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Build script to embed EJS templates as strings in the bundle.
|
|
4
|
+
* This eliminates the need to distribute template files separately.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs/promises';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
|
|
9
|
+
const TEMPLATES_DIR = path.join(import.meta.dir, '../src/templates');
|
|
10
|
+
const OUTPUT_FILE = path.join(import.meta.dir, '../src/generators/embedded-templates.ts');
|
|
11
|
+
|
|
12
|
+
async function findTemplateFiles(dir: string, basePath = ''): Promise<string[]> {
|
|
13
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
14
|
+
const files: string[] = [];
|
|
15
|
+
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const relativePath = path.join(basePath, entry.name);
|
|
18
|
+
const fullPath = path.join(dir, entry.name);
|
|
19
|
+
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
files.push(...(await findTemplateFiles(fullPath, relativePath)));
|
|
22
|
+
} else if (entry.name.endsWith('.ejs')) {
|
|
23
|
+
files.push(relativePath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return files;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function escapeTemplateString(content: string): string {
|
|
31
|
+
return content
|
|
32
|
+
.replace(/\\/g, '\\\\')
|
|
33
|
+
.replace(/`/g, '\\`')
|
|
34
|
+
.replace(/\$\{/g, '\\${');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const templateFiles = await findTemplateFiles(TEMPLATES_DIR);
|
|
39
|
+
templateFiles.sort();
|
|
40
|
+
|
|
41
|
+
const entries: string[] = [];
|
|
42
|
+
|
|
43
|
+
for (const file of templateFiles) {
|
|
44
|
+
const fullPath = path.join(TEMPLATES_DIR, file);
|
|
45
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
46
|
+
const escaped = escapeTemplateString(content);
|
|
47
|
+
// Normalize path separators for cross-platform compatibility
|
|
48
|
+
const normalizedPath = file.replace(/\\/g, '/');
|
|
49
|
+
entries.push(` '${normalizedPath}': \`${escaped}\``);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const output = `// Auto-generated by scripts/embed-templates.ts
|
|
53
|
+
// Do not edit manually
|
|
54
|
+
|
|
55
|
+
export const EMBEDDED_TEMPLATES: Record<string, string> = {
|
|
56
|
+
${entries.join(',\n')},
|
|
57
|
+
};
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
await fs.writeFile(OUTPUT_FILE, output, 'utf-8');
|
|
61
|
+
console.log(`Generated ${OUTPUT_FILE} with ${templateFiles.length} templates`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main().catch(console.error);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import type { Command } from 'commander';
|
|
4
|
+
import { loadTemplate } from '../generators/project';
|
|
5
|
+
import { getLatestActionVersions } from '../utils/github';
|
|
4
6
|
|
|
5
7
|
export interface PrepareReleaseOptions {
|
|
6
8
|
/** New package name for release */
|
|
@@ -125,8 +127,8 @@ async function replaceInCodeqlConfig(
|
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
|
-
* Updates tagpr.yml:
|
|
129
|
-
* This
|
|
130
|
+
* Updates tagpr.yml: regenerates from template with isDevcode=false
|
|
131
|
+
* This ensures all release-ready settings are applied
|
|
130
132
|
*/
|
|
131
133
|
async function replaceInTagprWorkflow(
|
|
132
134
|
targetDir: string,
|
|
@@ -135,34 +137,35 @@ async function replaceInTagprWorkflow(
|
|
|
135
137
|
): Promise<void> {
|
|
136
138
|
const workflowPath = path.join(targetDir, '.github/workflows/tagpr.yml');
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
// Check if file exists
|
|
139
141
|
try {
|
|
140
|
-
|
|
141
|
-
} catch
|
|
142
|
-
|
|
143
|
-
if (fsError.code === 'ENOENT') {
|
|
144
|
-
return; // File doesn't exist, nothing to do
|
|
145
|
-
}
|
|
146
|
-
throw error;
|
|
142
|
+
await fs.access(workflowPath);
|
|
143
|
+
} catch {
|
|
144
|
+
return; // File doesn't exist, nothing to do
|
|
147
145
|
}
|
|
148
146
|
|
|
149
|
-
|
|
150
|
-
content =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
);
|
|
147
|
+
const actionVersions = await getLatestActionVersions();
|
|
148
|
+
const content = await loadTemplate('common/workflows/tagpr.yml.ejs', {
|
|
149
|
+
isDevcode: false,
|
|
150
|
+
actionVersions,
|
|
151
|
+
});
|
|
154
152
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
/(uses: actions\/checkout@v\d+)\n(\s*)# TODO: After replace-devcode, add token: \$\{\{ secrets\.PAT_FOR_TAGPR \}\}/g,
|
|
158
|
-
'$1\n$2with:\n$2 token: ${{ secrets.PAT_FOR_TAGPR }}',
|
|
159
|
-
);
|
|
153
|
+
await fs.writeFile(workflowPath, content, 'utf-8');
|
|
154
|
+
}
|
|
160
155
|
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Generates publish.yml workflow for npm publishing with OIDC
|
|
158
|
+
*/
|
|
159
|
+
async function generatePublishWorkflow(targetDir: string): Promise<void> {
|
|
160
|
+
const workflowDir = path.join(targetDir, '.github/workflows');
|
|
161
|
+
const workflowPath = path.join(workflowDir, 'publish.yml');
|
|
163
162
|
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
await fs.mkdir(workflowDir, { recursive: true });
|
|
164
|
+
|
|
165
|
+
const actionVersions = await getLatestActionVersions();
|
|
166
|
+
const content = await loadTemplate('common/workflows/publish.yml.ejs', {
|
|
167
|
+
actionVersions,
|
|
168
|
+
});
|
|
166
169
|
|
|
167
170
|
await fs.writeFile(workflowPath, content, 'utf-8');
|
|
168
171
|
}
|
|
@@ -292,6 +295,17 @@ export async function prepareRelease(
|
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
297
|
|
|
298
|
+
// Generate publish workflow for npm OIDC publishing
|
|
299
|
+
console.log('\n📦 Generating release workflows:');
|
|
300
|
+
try {
|
|
301
|
+
await generatePublishWorkflow(targetDir);
|
|
302
|
+
console.log(' ✅ .github/workflows/publish.yml (npm OIDC publishing)');
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.log(
|
|
305
|
+
` ⚠️ publish.yml: ${error instanceof Error ? error.message : String(error)}`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
295
309
|
// Scan for unmanaged occurrences
|
|
296
310
|
const unmanaged = await findUnmanagedOccurrences(targetDir, devcode);
|
|
297
311
|
|
|
@@ -312,6 +326,7 @@ export async function prepareRelease(
|
|
|
312
326
|
console.log(` Package renamed: ${devcode} → ${options.publishName}`);
|
|
313
327
|
console.log(` Private flag removed`);
|
|
314
328
|
console.log(` Workflows updated to use PAT_FOR_TAGPR`);
|
|
329
|
+
console.log(` publish.yml generated for npm OIDC publishing`);
|
|
315
330
|
|
|
316
331
|
console.log(`\n⚠️ Action required: Set up PAT_FOR_TAGPR secret`);
|
|
317
332
|
console.log(` 1. Create a Personal Access Token (classic) at:`);
|
package/src/config.ts
CHANGED
|
@@ -29,26 +29,25 @@ describe('project generator', () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
describe('loadTemplate', () => {
|
|
32
|
-
test('should throw TemplateError for path traversal attempt',
|
|
33
|
-
|
|
32
|
+
test('should throw TemplateError for path traversal attempt', () => {
|
|
33
|
+
// With embedded templates, path traversal simply results in "not found"
|
|
34
|
+
expect(() => loadTemplate('../../../etc/passwd', {})).toThrow(
|
|
34
35
|
TemplateError,
|
|
35
36
|
);
|
|
36
|
-
expect(loadTemplate('../../../etc/passwd', {})).
|
|
37
|
-
'
|
|
37
|
+
expect(() => loadTemplate('../../../etc/passwd', {})).toThrow(
|
|
38
|
+
'Template not found',
|
|
38
39
|
);
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
test('should throw TemplateError for non-existent template',
|
|
42
|
-
expect(loadTemplate('non-existent.ejs', {})).
|
|
43
|
-
|
|
44
|
-
);
|
|
45
|
-
expect(loadTemplate('non-existent.ejs', {})).rejects.toThrow(
|
|
42
|
+
test('should throw TemplateError for non-existent template', () => {
|
|
43
|
+
expect(() => loadTemplate('non-existent.ejs', {})).toThrow(TemplateError);
|
|
44
|
+
expect(() => loadTemplate('non-existent.ejs', {})).toThrow(
|
|
46
45
|
'Template not found',
|
|
47
46
|
);
|
|
48
47
|
});
|
|
49
48
|
|
|
50
|
-
test('should load and render valid template',
|
|
51
|
-
const result =
|
|
49
|
+
test('should load and render valid template', () => {
|
|
50
|
+
const result = loadTemplate('typescript/package.json.ejs', {
|
|
52
51
|
name: 'test',
|
|
53
52
|
author: '',
|
|
54
53
|
isDevcode: false,
|
|
@@ -4,8 +4,7 @@ import ejs from 'ejs';
|
|
|
4
4
|
import type { InitOptions } from '../types';
|
|
5
5
|
import { getLatestActionVersions } from '../utils/github';
|
|
6
6
|
import { getLatestVersions, getNpmUsername } from '../utils/npm';
|
|
7
|
-
|
|
8
|
-
const TEMPLATES_DIR = path.join(import.meta.dir, '../templates');
|
|
7
|
+
import { EMBEDDED_TEMPLATES } from './embedded-templates';
|
|
9
8
|
|
|
10
9
|
const DEV_DEPENDENCIES = [
|
|
11
10
|
'@biomejs/biome',
|
|
@@ -32,39 +31,21 @@ export class TemplateError extends Error {
|
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
export
|
|
34
|
+
export function loadTemplate(
|
|
36
35
|
templatePath: string,
|
|
37
36
|
data: Record<string, unknown>,
|
|
38
|
-
):
|
|
39
|
-
|
|
40
|
-
const
|
|
37
|
+
): string {
|
|
38
|
+
// Normalize path separators
|
|
39
|
+
const normalizedPath = templatePath.replace(/\\/g, '/');
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
const template = EMBEDDED_TEMPLATES[normalizedPath];
|
|
42
|
+
if (!template) {
|
|
43
43
|
throw new TemplateError(
|
|
44
|
-
`
|
|
44
|
+
`Template not found: "${templatePath}"`,
|
|
45
45
|
templatePath,
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
let template: string;
|
|
50
|
-
try {
|
|
51
|
-
template = await fs.readFile(fullPath, 'utf-8');
|
|
52
|
-
} catch (error) {
|
|
53
|
-
const fsError = error as NodeJS.ErrnoException;
|
|
54
|
-
if (fsError.code === 'ENOENT') {
|
|
55
|
-
throw new TemplateError(
|
|
56
|
-
`Template not found: "${templatePath}"`,
|
|
57
|
-
templatePath,
|
|
58
|
-
fsError,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
throw new TemplateError(
|
|
62
|
-
`Failed to read template "${templatePath}": ${fsError.message}`,
|
|
63
|
-
templatePath,
|
|
64
|
-
fsError,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
49
|
try {
|
|
69
50
|
return ejs.render(template, data);
|
|
70
51
|
} catch (error) {
|
|
@@ -87,7 +68,7 @@ export async function generatePackageJson(
|
|
|
87
68
|
]);
|
|
88
69
|
const author = detectedAuthor ?? '';
|
|
89
70
|
const templatePath = `${options.lang}/package.json.ejs`;
|
|
90
|
-
const content =
|
|
71
|
+
const content = loadTemplate(templatePath, {
|
|
91
72
|
name: options.projectName,
|
|
92
73
|
isDevcode: options.isDevcode,
|
|
93
74
|
author,
|
|
@@ -104,14 +85,14 @@ export async function generatePackageJson(
|
|
|
104
85
|
export async function generateTsconfig(
|
|
105
86
|
options: InitOptions,
|
|
106
87
|
): Promise<GeneratedFile> {
|
|
107
|
-
const content =
|
|
88
|
+
const content = loadTemplate(`${options.lang}/tsconfig.json.ejs`, {});
|
|
108
89
|
return { path: 'tsconfig.json', content };
|
|
109
90
|
}
|
|
110
91
|
|
|
111
92
|
export async function generateEntryPoint(
|
|
112
93
|
options: InitOptions,
|
|
113
94
|
): Promise<GeneratedFile> {
|
|
114
|
-
const content =
|
|
95
|
+
const content = loadTemplate(`${options.lang}/src/index.ts.ejs`, {
|
|
115
96
|
name: options.projectName,
|
|
116
97
|
});
|
|
117
98
|
return { path: 'src/index.ts', content };
|
|
@@ -120,7 +101,7 @@ export async function generateEntryPoint(
|
|
|
120
101
|
export async function generateTagprConfig(
|
|
121
102
|
options: InitOptions,
|
|
122
103
|
): Promise<GeneratedFile> {
|
|
123
|
-
const content =
|
|
104
|
+
const content = loadTemplate(`${options.lang}/.tagpr.ejs`, {});
|
|
124
105
|
return { path: '.tagpr', content };
|
|
125
106
|
}
|
|
126
107
|
|
|
@@ -128,7 +109,7 @@ export async function generateTagprWorkflow(
|
|
|
128
109
|
options: InitOptions,
|
|
129
110
|
actionVersions: Record<string, string>,
|
|
130
111
|
): Promise<GeneratedFile> {
|
|
131
|
-
const content =
|
|
112
|
+
const content = loadTemplate('common/workflows/tagpr.yml.ejs', {
|
|
132
113
|
isDevcode: options.isDevcode,
|
|
133
114
|
actionVersions,
|
|
134
115
|
});
|
|
@@ -139,7 +120,7 @@ export async function generateCiWorkflow(
|
|
|
139
120
|
options: InitOptions,
|
|
140
121
|
actionVersions: Record<string, string>,
|
|
141
122
|
): Promise<GeneratedFile> {
|
|
142
|
-
const content =
|
|
123
|
+
const content = loadTemplate(`${options.lang}/workflows/ci.yml.ejs`, {
|
|
143
124
|
actionVersions,
|
|
144
125
|
});
|
|
145
126
|
return { path: '.github/workflows/ci.yml', content };
|
|
@@ -149,34 +130,32 @@ export async function generateCodeqlWorkflow(
|
|
|
149
130
|
options: InitOptions,
|
|
150
131
|
actionVersions: Record<string, string>,
|
|
151
132
|
): Promise<GeneratedFile> {
|
|
152
|
-
const content =
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
133
|
+
const content = loadTemplate(`${options.lang}/workflows/codeql.yml.ejs`, {
|
|
134
|
+
actionVersions,
|
|
135
|
+
});
|
|
156
136
|
return { path: '.github/workflows/codeql.yml', content };
|
|
157
137
|
}
|
|
158
138
|
|
|
159
139
|
export async function generateCodeqlConfig(
|
|
160
140
|
options: InitOptions,
|
|
161
141
|
): Promise<GeneratedFile> {
|
|
162
|
-
const content =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
);
|
|
142
|
+
const content = loadTemplate(`${options.lang}/codeql/codeql-config.yml.ejs`, {
|
|
143
|
+
name: options.projectName,
|
|
144
|
+
});
|
|
166
145
|
return { path: '.github/codeql/codeql-config.yml', content };
|
|
167
146
|
}
|
|
168
147
|
|
|
169
148
|
export async function generateDependabot(
|
|
170
149
|
options: InitOptions,
|
|
171
150
|
): Promise<GeneratedFile> {
|
|
172
|
-
const content =
|
|
151
|
+
const content = loadTemplate('common/dependabot.yml.ejs', {
|
|
173
152
|
lang: options.lang,
|
|
174
153
|
});
|
|
175
154
|
return { path: '.github/dependabot.yml', content };
|
|
176
155
|
}
|
|
177
156
|
|
|
178
157
|
export async function generateReleaseConfig(): Promise<GeneratedFile> {
|
|
179
|
-
const content =
|
|
158
|
+
const content = loadTemplate('common/release.yml.ejs', {});
|
|
180
159
|
return { path: '.github/release.yml', content };
|
|
181
160
|
}
|
|
182
161
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
|
|
18
|
+
|
|
19
|
+
- uses: oven-sh/setup-bun@<%= actionVersions['oven-sh/setup-bun'] %>
|
|
20
|
+
|
|
21
|
+
- uses: actions/setup-node@<%= actionVersions['actions/setup-node'] %>
|
|
22
|
+
with:
|
|
23
|
+
node-version: '24'
|
|
24
|
+
registry-url: 'https://registry.npmjs.org'
|
|
25
|
+
|
|
26
|
+
- run: bun install --frozen-lockfile
|
|
27
|
+
|
|
28
|
+
- name: Build
|
|
29
|
+
run: bun run build
|
|
30
|
+
|
|
31
|
+
- name: Test
|
|
32
|
+
run: bun test
|
|
33
|
+
|
|
34
|
+
- name: Publish to npm with provenance
|
|
35
|
+
run: npm publish --access public --provenance
|
|
36
|
+
|
|
@@ -28,4 +28,8 @@ jobs:
|
|
|
28
28
|
# TODO: After replace-devcode, use PAT_FOR_TAGPR instead
|
|
29
29
|
<% } else { -%>
|
|
30
30
|
GITHUB_TOKEN: ${{ secrets.PAT_FOR_TAGPR }}
|
|
31
|
+
GIT_COMMITTER_NAME: github-actions[bot]
|
|
32
|
+
GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
|
|
33
|
+
GIT_AUTHOR_NAME: github-actions[bot]
|
|
34
|
+
GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
|
|
31
35
|
<% } -%>
|