@o3co/create-auth-provider 0.4.1
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/README.ja.md +107 -0
- package/README.md +107 -0
- package/dist/__tests__/index.test.d.mts +2 -0
- package/dist/__tests__/index.test.d.mts.map +1 -0
- package/dist/__tests__/index.test.mjs +226 -0
- package/dist/cli.d.mts +3 -0
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +18 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +166 -0
- package/package.json +28 -0
- package/templates/standalone/.dockerignore +9 -0
- package/templates/standalone/.env.example +21 -0
- package/templates/standalone/Dockerfile +58 -0
- package/templates/standalone/Makefile +12 -0
- package/templates/standalone/README.ja.md +205 -0
- package/templates/standalone/README.md +205 -0
- package/templates/standalone/config/application.conf +132 -0
- package/templates/standalone/config/clients.yaml +2 -0
- package/templates/standalone/config/clients.yaml.example +14 -0
- package/templates/standalone/config/development.conf +8 -0
- package/templates/standalone/config/production.conf +10 -0
- package/templates/standalone/docker-compose.test.yml +16 -0
- package/templates/standalone/docker-compose.yml +26 -0
- package/templates/standalone/package.json +45 -0
- package/templates/standalone/src/__tests__/configPath.test.mts +50 -0
- package/templates/standalone/src/__tests__/smoke.test.mts +204 -0
- package/templates/standalone/src/app.mts +201 -0
- package/templates/standalone/src/configPath.mts +37 -0
- package/templates/standalone/src/logger.mts +18 -0
- package/templates/standalone/tests/Makefile +5 -0
- package/templates/standalone/tests/did-grant.test.js +193 -0
- package/templates/standalone/tests/index.test.js +218 -0
- package/templates/standalone/tests/package-lock.json +2225 -0
- package/templates/standalone/tests/package.json +17 -0
- package/templates/standalone/tsconfig.json +12 -0
- package/templates/standalone/vitest.config.mts +7 -0
- package/templates/versions.json +9 -0
package/README.ja.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @o3co/create-auth-provider
|
|
2
|
+
|
|
3
|
+
auth.provider 用の CLI スキャフォルダーです。standalone テンプレートから新しいプロジェクトを生成します。
|
|
4
|
+
|
|
5
|
+
## 使い方
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @o3co/create-auth-provider <project-name> [--dir <dir-name>]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`<project-name>` はスコープ付き npm 名 (`@scope/pkg`) とスコープなしの名前 (`pkg`) のどちらでも指定できます。
|
|
12
|
+
|
|
13
|
+
スコープなしの例:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @o3co/create-auth-provider my-auth-server
|
|
17
|
+
cd my-auth-server
|
|
18
|
+
npm install
|
|
19
|
+
npm run debug
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
スコープ付きの例(ディレクトリ名はパッケージ部分 `auth.provider` がデフォルト):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @o3co/create-auth-provider @my-org/auth.provider
|
|
26
|
+
cd auth.provider
|
|
27
|
+
npm install
|
|
28
|
+
npm run debug
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`--dir` でディレクトリ名を明示指定:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx @o3co/create-auth-provider @my-org/auth.provider --dir provider
|
|
35
|
+
cd provider
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 動作内容
|
|
39
|
+
|
|
40
|
+
1. `<project-name>` を検証する(バリデーションルール参照)。
|
|
41
|
+
2. 生成先ディレクトリ名を決定する: `--dir <value>` が指定されていればその値、そうでなければスコープ付き名のパッケージ部分、最終的には入力値そのもの。
|
|
42
|
+
3. 生成先ディレクトリを `<cwd>/<dir-name>` として解決する。
|
|
43
|
+
4. ディレクトリがすでに存在する場合はエラーを出力して終了する。
|
|
44
|
+
5. `templates/standalone/` を生成先ディレクトリにコピーする(`node_modules/` と `dist/` は除外)。
|
|
45
|
+
6. 生成されたディレクトリの `package.json` を書き換える:
|
|
46
|
+
- `name` を `<project-name>` をそのまま設定する(スコープを保持)。
|
|
47
|
+
- `private` フィールドを削除する。
|
|
48
|
+
- すべての `workspace:*` バージョン参照を `versions.json` の公開 semver バージョンに置き換える。
|
|
49
|
+
7. 次のステップを出力する。
|
|
50
|
+
|
|
51
|
+
## バリデーションルール
|
|
52
|
+
|
|
53
|
+
`<project-name>` は以下のいずれかに一致する必要があります:
|
|
54
|
+
|
|
55
|
+
- スコープなし: `^[a-z0-9][a-z0-9-._~]*$`
|
|
56
|
+
- スコープ付き: `^@[a-z0-9][a-z0-9-._~]*/[a-z0-9][a-z0-9-._~]*$`
|
|
57
|
+
|
|
58
|
+
いずれも空文字・`.`・`..` は不可、最大 214 文字。
|
|
59
|
+
|
|
60
|
+
`--dir <value>` はスコープなしのパターンと同じ制約です。
|
|
61
|
+
|
|
62
|
+
## 既知の制約
|
|
63
|
+
|
|
64
|
+
内包されているテンプレートの `README.md` / `README.ja.md` の見出しは `@o3co/auth-provider-standalone` のままです。スコープ付きでプロジェクトを生成した場合、この見出しは生成された `package.json` の `name` と一致しません。必要に応じて手動で修正してください。
|
|
65
|
+
|
|
66
|
+
## 生成される構造
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
<プロジェクト名>/
|
|
70
|
+
├── config/
|
|
71
|
+
│ ├── application.conf # HOCON 設定(環境変数で上書き可能)
|
|
72
|
+
│ ├── clients.yaml # OAuth クライアントレジストリ
|
|
73
|
+
│ └── clients.yaml.example
|
|
74
|
+
├── src/
|
|
75
|
+
│ └── app.mts # Composition root
|
|
76
|
+
├── tests/
|
|
77
|
+
├── .dockerignore
|
|
78
|
+
├── .env.example
|
|
79
|
+
├── Dockerfile
|
|
80
|
+
├── docker-compose.yml
|
|
81
|
+
├── docker-compose.test.yml
|
|
82
|
+
├── Makefile
|
|
83
|
+
├── package.json
|
|
84
|
+
├── tsconfig.json
|
|
85
|
+
└── vitest.config.mts
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
生成後、`.env.example` をコピーして `.env` を作成し、必須の環境変数を設定してください。
|
|
89
|
+
|
|
90
|
+
## プログラマティック API
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { scaffold, main } from "@o3co/create-auth-provider";
|
|
94
|
+
|
|
95
|
+
// 絶対パスにプロジェクトを生成する
|
|
96
|
+
scaffold(targetDir: string, projectName: string): void;
|
|
97
|
+
|
|
98
|
+
// CLI エントリーポイント — process.argv を読み取り、エラー時は終了する
|
|
99
|
+
main(): void;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
テンプレートディレクトリが見つからない場合、または `workspace:*` 依存関係が `versions.json` で解決できない場合、`scaffold` はエラーをスローします。
|
|
103
|
+
|
|
104
|
+
## 関連
|
|
105
|
+
|
|
106
|
+
- [`@o3co/auth-provider-standalone`](../templates/standalone) — このツールが生成元とするテンプレート
|
|
107
|
+
- [`@o3co/auth-provider-core`](../packages/core) — コアアプリケーションファクトリ
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @o3co/create-auth-provider
|
|
2
|
+
|
|
3
|
+
CLI scaffolder for auth.provider. Generates a new project from the standalone template.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @o3co/create-auth-provider <project-name> [--dir <dir-name>]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`<project-name>` may be either a scoped npm name (`@scope/pkg`) or an unscoped name (`pkg`).
|
|
12
|
+
|
|
13
|
+
Unscoped example:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @o3co/create-auth-provider my-auth-server
|
|
17
|
+
cd my-auth-server
|
|
18
|
+
npm install
|
|
19
|
+
npm run debug
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Scoped example (directory defaults to the package portion `auth.provider`):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @o3co/create-auth-provider @my-org/auth.provider
|
|
26
|
+
cd auth.provider
|
|
27
|
+
npm install
|
|
28
|
+
npm run debug
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Override the directory name with `--dir`:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx @o3co/create-auth-provider @my-org/auth.provider --dir provider
|
|
35
|
+
cd provider
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## What It Does
|
|
39
|
+
|
|
40
|
+
1. Validates `<project-name>` (see Validation Rules).
|
|
41
|
+
2. Derives the target directory name: `--dir <value>` if given, else the unscoped part of a scoped name, else the name itself.
|
|
42
|
+
3. Resolves the target directory as `<cwd>/<dir-name>`.
|
|
43
|
+
4. Errors if the directory already exists.
|
|
44
|
+
5. Copies `templates/standalone/` to the target directory, excluding `node_modules/` and `dist/`.
|
|
45
|
+
6. Rewrites `package.json` in the generated directory:
|
|
46
|
+
- Sets `name` to `<project-name>` verbatim (scope-preserving).
|
|
47
|
+
- Removes the `private` field.
|
|
48
|
+
- Replaces all `workspace:*` version references with published semver versions from `versions.json`.
|
|
49
|
+
7. Prints next-step instructions.
|
|
50
|
+
|
|
51
|
+
## Validation Rules
|
|
52
|
+
|
|
53
|
+
`<project-name>` must match one of:
|
|
54
|
+
|
|
55
|
+
- Unscoped: `^[a-z0-9][a-z0-9-._~]*$`
|
|
56
|
+
- Scoped: `^@[a-z0-9][a-z0-9-._~]*/[a-z0-9][a-z0-9-._~]*$`
|
|
57
|
+
|
|
58
|
+
Both forms must be non-empty, not `.` or `..`, and ≤ 214 characters.
|
|
59
|
+
|
|
60
|
+
`--dir <value>` must match the unscoped pattern above (same constraints).
|
|
61
|
+
|
|
62
|
+
## Known Limitations
|
|
63
|
+
|
|
64
|
+
The bundled template's `README.md` / `README.ja.md` still carry the upstream title `@o3co/auth-provider-standalone`. When generating a scoped project, that title will not match your `package.json` name; edit it manually if it matters for your use case.
|
|
65
|
+
|
|
66
|
+
## Generated Structure
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
<project-name>/
|
|
70
|
+
├── config/
|
|
71
|
+
│ ├── application.conf # HOCON config (overridable via env vars)
|
|
72
|
+
│ ├── clients.yaml # OAuth client registry
|
|
73
|
+
│ └── clients.yaml.example
|
|
74
|
+
├── src/
|
|
75
|
+
│ └── app.mts # Composition root
|
|
76
|
+
├── tests/
|
|
77
|
+
├── .dockerignore
|
|
78
|
+
├── .env.example
|
|
79
|
+
├── Dockerfile
|
|
80
|
+
├── docker-compose.yml
|
|
81
|
+
├── docker-compose.test.yml
|
|
82
|
+
├── Makefile
|
|
83
|
+
├── package.json
|
|
84
|
+
├── tsconfig.json
|
|
85
|
+
└── vitest.config.mts
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
After generation, fill in the required environment variables in `.env` (copy from `.env.example`).
|
|
89
|
+
|
|
90
|
+
## Programmatic API
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { scaffold, main } from "@o3co/create-auth-provider";
|
|
94
|
+
|
|
95
|
+
// Generate a project at an absolute path
|
|
96
|
+
scaffold(targetDir: string, projectName: string): void;
|
|
97
|
+
|
|
98
|
+
// CLI entry point — reads process.argv and exits on error
|
|
99
|
+
main(): void;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`scaffold` throws if the template directory is missing or if a `workspace:*` dependency cannot be resolved in `versions.json`.
|
|
103
|
+
|
|
104
|
+
## See Also
|
|
105
|
+
|
|
106
|
+
- [`@o3co/auth-provider-standalone`](../templates/standalone) — The template this tool generates from
|
|
107
|
+
- [`@o3co/auth-provider-core`](../packages/core) — Core application factory
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.mts","sourceRoot":"","sources":["../../src/__tests__/index.test.mts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { isValidDirName, isValidProjectName, main, scaffold } from "../index.mjs";
|
|
6
|
+
describe("scaffold", () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tempDir = mkdtempSync(join(tmpdir(), "create-auth-provider-test-"));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it("copies template files to target directory", () => {
|
|
15
|
+
const targetDir = join(tempDir, "my-auth");
|
|
16
|
+
scaffold(targetDir, "my-auth");
|
|
17
|
+
expect(existsSync(join(targetDir, "package.json"))).toBe(true);
|
|
18
|
+
expect(existsSync(join(targetDir, "tsconfig.json"))).toBe(true);
|
|
19
|
+
expect(existsSync(join(targetDir, "src", "app.mts"))).toBe(true);
|
|
20
|
+
expect(existsSync(join(targetDir, "config", "application.conf"))).toBe(true);
|
|
21
|
+
expect(existsSync(join(targetDir, "config", "development.conf"))).toBe(true);
|
|
22
|
+
expect(existsSync(join(targetDir, "config", "production.conf"))).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it("rewrites package.json name to project name", () => {
|
|
25
|
+
const targetDir = join(tempDir, "my-auth");
|
|
26
|
+
scaffold(targetDir, "my-auth");
|
|
27
|
+
const pkg = JSON.parse(readFileSync(join(targetDir, "package.json"), "utf-8"));
|
|
28
|
+
expect(pkg.name).toBe("my-auth");
|
|
29
|
+
});
|
|
30
|
+
it("replaces all workspace:* dependencies with caret versions", () => {
|
|
31
|
+
const targetDir = join(tempDir, "my-auth");
|
|
32
|
+
scaffold(targetDir, "my-auth");
|
|
33
|
+
const pkg = JSON.parse(readFileSync(join(targetDir, "package.json"), "utf-8"));
|
|
34
|
+
// No workspace:* should remain anywhere
|
|
35
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
36
|
+
const deps = pkg[section];
|
|
37
|
+
if (!deps)
|
|
38
|
+
continue;
|
|
39
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
40
|
+
expect(version, `${section}.${name} should not be workspace:*`).not.toBe("workspace:*");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Specific checks
|
|
44
|
+
expect(pkg.dependencies["@o3co/auth-provider-core"]).toMatch(/^\^/);
|
|
45
|
+
expect(pkg.dependencies["@o3co/auth-provider-federation-google"]).toMatch(/^\^/);
|
|
46
|
+
expect(pkg.dependencies["@o3co/auth-provider-federation-github"]).toBeUndefined();
|
|
47
|
+
expect(pkg.dependencies["@o3co/auth-provider-foundation"]).toMatch(/^\^/);
|
|
48
|
+
});
|
|
49
|
+
it("removes private field from package.json", () => {
|
|
50
|
+
const targetDir = join(tempDir, "my-auth");
|
|
51
|
+
scaffold(targetDir, "my-auth");
|
|
52
|
+
const pkg = JSON.parse(readFileSync(join(targetDir, "package.json"), "utf-8"));
|
|
53
|
+
expect(pkg.private).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
it("writes scoped project name verbatim into package.json", () => {
|
|
56
|
+
const targetDir = join(tempDir, "auth.provider");
|
|
57
|
+
scaffold(targetDir, "@piratis-blossoms/auth.provider");
|
|
58
|
+
const pkg = JSON.parse(readFileSync(join(targetDir, "package.json"), "utf-8"));
|
|
59
|
+
expect(pkg.name).toBe("@piratis-blossoms/auth.provider");
|
|
60
|
+
});
|
|
61
|
+
it("ships versions for optional federation provider packages", () => {
|
|
62
|
+
const versions = JSON.parse(readFileSync(join("templates", "versions.json"), "utf-8"));
|
|
63
|
+
expect(versions["@o3co/auth-provider-federation-google"]).toBe("0.0.0");
|
|
64
|
+
expect(versions["@o3co/auth-provider-federation-github"]).toBe("0.0.0");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe("isValidProjectName", () => {
|
|
68
|
+
it.each([
|
|
69
|
+
["my-auth"],
|
|
70
|
+
["auth.provider"],
|
|
71
|
+
["a"],
|
|
72
|
+
["foo_bar~baz.1"],
|
|
73
|
+
["@piratis-blossoms/auth.provider"],
|
|
74
|
+
["@foo-bar/baz_qux~1"],
|
|
75
|
+
])("accepts %s", (name) => {
|
|
76
|
+
expect(isValidProjectName(name)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
it.each([
|
|
79
|
+
[""],
|
|
80
|
+
["."],
|
|
81
|
+
[".."],
|
|
82
|
+
["UPPER"],
|
|
83
|
+
["with space"],
|
|
84
|
+
["with/slash"],
|
|
85
|
+
["with\\back"],
|
|
86
|
+
["@"],
|
|
87
|
+
["@/"],
|
|
88
|
+
["@scope"],
|
|
89
|
+
["@/pkg"],
|
|
90
|
+
["@scope/"],
|
|
91
|
+
["@scope//pkg"],
|
|
92
|
+
["@SCOPE/pkg"],
|
|
93
|
+
["a".repeat(215)],
|
|
94
|
+
])("rejects %s", (name) => {
|
|
95
|
+
expect(isValidProjectName(name)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe("isValidDirName", () => {
|
|
99
|
+
it.each([["my-auth"], ["auth.provider"], ["a"], ["foo_bar~baz.1"]])("accepts %s", (name) => {
|
|
100
|
+
expect(isValidDirName(name)).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
it.each([
|
|
103
|
+
[""],
|
|
104
|
+
["."],
|
|
105
|
+
[".."],
|
|
106
|
+
["@scope/pkg"],
|
|
107
|
+
["with/slash"],
|
|
108
|
+
["with\\back"],
|
|
109
|
+
["@piratis-blossoms"],
|
|
110
|
+
["UPPER"],
|
|
111
|
+
["a".repeat(215)],
|
|
112
|
+
])("rejects %s", (name) => {
|
|
113
|
+
expect(isValidDirName(name)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe("main (argv parsing and directory derivation)", () => {
|
|
117
|
+
let cwdBackup;
|
|
118
|
+
let workdir;
|
|
119
|
+
let argvBackup;
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
cwdBackup = process.cwd();
|
|
122
|
+
workdir = mkdtempSync(join(tmpdir(), "create-auth-provider-main-"));
|
|
123
|
+
process.chdir(workdir);
|
|
124
|
+
argvBackup = process.argv;
|
|
125
|
+
});
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
process.chdir(cwdBackup);
|
|
128
|
+
process.argv = argvBackup;
|
|
129
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
130
|
+
});
|
|
131
|
+
const runMain = (args) => {
|
|
132
|
+
process.argv = ["node", "cli", ...args];
|
|
133
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code) => {
|
|
134
|
+
throw new Error(`__exit__:${code ?? 0}`);
|
|
135
|
+
}));
|
|
136
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
137
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
138
|
+
let exitCode = 0;
|
|
139
|
+
try {
|
|
140
|
+
main();
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
const m = /__exit__:(\d+)/.exec(e.message);
|
|
144
|
+
exitCode = m ? Number(m[1]) : null;
|
|
145
|
+
}
|
|
146
|
+
const stderr = errSpy.mock.calls.map((c) => c.join(" ")).join("\n");
|
|
147
|
+
exitSpy.mockRestore();
|
|
148
|
+
errSpy.mockRestore();
|
|
149
|
+
logSpy.mockRestore();
|
|
150
|
+
return { exitCode, stderr };
|
|
151
|
+
};
|
|
152
|
+
// Positive: unscoped, no --dir
|
|
153
|
+
it("unscoped name: dir = name, pkg.name = name", () => {
|
|
154
|
+
const r = runMain(["my-auth"]);
|
|
155
|
+
expect(r.exitCode).toBe(0);
|
|
156
|
+
const pkg = JSON.parse(readFileSync(join(workdir, "my-auth", "package.json"), "utf-8"));
|
|
157
|
+
expect(pkg.name).toBe("my-auth");
|
|
158
|
+
});
|
|
159
|
+
// Positive: scoped, no --dir
|
|
160
|
+
it("scoped name: dir = pkg part, pkg.name = full scoped", () => {
|
|
161
|
+
const r = runMain(["@piratis-blossoms/auth.provider"]);
|
|
162
|
+
expect(r.exitCode).toBe(0);
|
|
163
|
+
const pkg = JSON.parse(readFileSync(join(workdir, "auth.provider", "package.json"), "utf-8"));
|
|
164
|
+
expect(pkg.name).toBe("@piratis-blossoms/auth.provider");
|
|
165
|
+
});
|
|
166
|
+
// Positive: scoped + --dir space
|
|
167
|
+
it("scoped name with --dir <val>: dir = val, pkg.name = full scoped", () => {
|
|
168
|
+
const r = runMain(["@piratis-blossoms/auth.provider", "--dir", "provider"]);
|
|
169
|
+
expect(r.exitCode).toBe(0);
|
|
170
|
+
const pkg = JSON.parse(readFileSync(join(workdir, "provider", "package.json"), "utf-8"));
|
|
171
|
+
expect(pkg.name).toBe("@piratis-blossoms/auth.provider");
|
|
172
|
+
});
|
|
173
|
+
// Positive: scoped + --dir= equals form
|
|
174
|
+
it("scoped name with --dir=<val>: dir = val, pkg.name = full scoped", () => {
|
|
175
|
+
const r = runMain(["@piratis-blossoms/auth.provider", "--dir=provider2"]);
|
|
176
|
+
expect(r.exitCode).toBe(0);
|
|
177
|
+
const pkg = JSON.parse(readFileSync(join(workdir, "provider2", "package.json"), "utf-8"));
|
|
178
|
+
expect(pkg.name).toBe("@piratis-blossoms/auth.provider");
|
|
179
|
+
});
|
|
180
|
+
// Positive: unscoped + --dir
|
|
181
|
+
it("unscoped name with --dir <val>: dir = val, pkg.name = unscoped", () => {
|
|
182
|
+
const r = runMain(["my-auth", "--dir", "custom"]);
|
|
183
|
+
expect(r.exitCode).toBe(0);
|
|
184
|
+
const pkg = JSON.parse(readFileSync(join(workdir, "custom", "package.json"), "utf-8"));
|
|
185
|
+
expect(pkg.name).toBe("my-auth");
|
|
186
|
+
});
|
|
187
|
+
// Positive: flags may come before the positional
|
|
188
|
+
it("flags before positional: --dir custom my-auth", () => {
|
|
189
|
+
const r = runMain(["--dir", "custom", "my-auth"]);
|
|
190
|
+
expect(r.exitCode).toBe(0);
|
|
191
|
+
const pkg = JSON.parse(readFileSync(join(workdir, "custom", "package.json"), "utf-8"));
|
|
192
|
+
expect(pkg.name).toBe("my-auth");
|
|
193
|
+
});
|
|
194
|
+
// Negative cases
|
|
195
|
+
it.each([
|
|
196
|
+
{ case: "no args", args: [] },
|
|
197
|
+
{ case: "two positionals", args: ["foo", "bar"] },
|
|
198
|
+
{ case: "dot", args: ["."] },
|
|
199
|
+
{ case: "dotdot", args: [".."] },
|
|
200
|
+
{ case: "backslash in name", args: ["back\\slash"] },
|
|
201
|
+
{ case: "empty scope", args: ["@/pkg"] },
|
|
202
|
+
{ case: "empty pkg", args: ["@scope/"] },
|
|
203
|
+
{ case: "double slash", args: ["@scope//pkg"] },
|
|
204
|
+
{ case: "name too long", args: ["a".repeat(215)] },
|
|
205
|
+
{ case: "--dir invalid (dot)", args: ["foo", "--dir", "."] },
|
|
206
|
+
{ case: "--dir invalid (slash)", args: ["foo", "--dir", "a/b"] },
|
|
207
|
+
{ case: "--dir invalid (at)", args: ["foo", "--dir", "@foo"] },
|
|
208
|
+
{ case: "--dir invalid (back)", args: ["foo", "--dir", "a\\b"] },
|
|
209
|
+
{ case: "--dir empty space form", args: ["foo", "--dir", ""] },
|
|
210
|
+
{ case: "--dir empty equals form", args: ["foo", "--dir="] },
|
|
211
|
+
{ case: "--dir missing value", args: ["foo", "--dir"] },
|
|
212
|
+
{ case: "--dir duplicated", args: ["foo", "--dir", "a", "--dir", "b"] },
|
|
213
|
+
{ case: "unknown flag", args: ["foo", "--unknown"] },
|
|
214
|
+
{ case: "literal double-dash", args: ["foo", "--"] },
|
|
215
|
+
])("rejects: $case", ({ args }) => {
|
|
216
|
+
const r = runMain(args);
|
|
217
|
+
expect(r.exitCode).toBe(1);
|
|
218
|
+
expect(r.stderr.length).toBeGreaterThan(0);
|
|
219
|
+
});
|
|
220
|
+
it("target directory already exists", () => {
|
|
221
|
+
mkdirSync(join(workdir, "foo"));
|
|
222
|
+
const r = runMain(["foo"]);
|
|
223
|
+
expect(r.exitCode).toBe(1);
|
|
224
|
+
expect(r.stderr.length).toBeGreaterThan(0);
|
|
225
|
+
});
|
|
226
|
+
});
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.mts","sourceRoot":"","sources":["../src/cli.mts"],"names":[],"mappings":""}
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Copyright 2026 1o1 Co. Ltd.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { main } from "./index.mjs";
|
|
18
|
+
main();
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const isValidProjectName: (name: string) => boolean;
|
|
2
|
+
export declare const isValidDirName: (name: string) => boolean;
|
|
3
|
+
export declare const scaffold: (targetDir: string, projectName: string) => void;
|
|
4
|
+
export declare const main: () => void;
|
|
5
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAkCA,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,OAIjD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,OAI7C,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,WAAW,MAAM,EAAE,aAAa,MAAM,KAAG,IA0CjE,CAAC;AAsDF,eAAO,MAAM,IAAI,QAAO,IA+CvB,CAAC"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { dirname, resolve, sep } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const TEMPLATES_DIR = resolve(__dirname, "../templates/standalone");
|
|
21
|
+
const getPackageVersions = () => {
|
|
22
|
+
const versionFile = resolve(__dirname, "../templates/versions.json");
|
|
23
|
+
if (existsSync(versionFile)) {
|
|
24
|
+
return JSON.parse(readFileSync(versionFile, "utf-8"));
|
|
25
|
+
}
|
|
26
|
+
return {};
|
|
27
|
+
};
|
|
28
|
+
const UNSCOPED_NAME_RE = /^[a-z0-9][a-z0-9-._~]*$/;
|
|
29
|
+
const SCOPED_NAME_RE = /^@[a-z0-9][a-z0-9-._~]*\/[a-z0-9][a-z0-9-._~]*$/;
|
|
30
|
+
const MAX_NAME_LEN = 214;
|
|
31
|
+
export const isValidProjectName = (name) => {
|
|
32
|
+
if (name.length === 0 || name.length > MAX_NAME_LEN)
|
|
33
|
+
return false;
|
|
34
|
+
if (name === "." || name === "..")
|
|
35
|
+
return false;
|
|
36
|
+
return UNSCOPED_NAME_RE.test(name) || SCOPED_NAME_RE.test(name);
|
|
37
|
+
};
|
|
38
|
+
export const isValidDirName = (name) => {
|
|
39
|
+
if (name.length === 0 || name.length > MAX_NAME_LEN)
|
|
40
|
+
return false;
|
|
41
|
+
if (name === "." || name === "..")
|
|
42
|
+
return false;
|
|
43
|
+
return UNSCOPED_NAME_RE.test(name);
|
|
44
|
+
};
|
|
45
|
+
export const scaffold = (targetDir, projectName) => {
|
|
46
|
+
if (!existsSync(TEMPLATES_DIR)) {
|
|
47
|
+
throw new Error(`Template directory not found at ${TEMPLATES_DIR}. If developing locally, run the prebuild script first.`);
|
|
48
|
+
}
|
|
49
|
+
// Copy template to target
|
|
50
|
+
const EXCLUDED_DIRS = new Set(["node_modules", "dist"]);
|
|
51
|
+
cpSync(TEMPLATES_DIR, targetDir, {
|
|
52
|
+
recursive: true,
|
|
53
|
+
filter: (source) => {
|
|
54
|
+
const segments = source.split(sep);
|
|
55
|
+
return !segments.some((s) => EXCLUDED_DIRS.has(s));
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
// Rewrite package.json
|
|
59
|
+
const pkgPath = resolve(targetDir, "package.json");
|
|
60
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
61
|
+
pkg.name = projectName;
|
|
62
|
+
delete pkg.private;
|
|
63
|
+
// Replace all workspace:* references with per-package published versions
|
|
64
|
+
const versions = getPackageVersions();
|
|
65
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
66
|
+
const deps = pkg[section];
|
|
67
|
+
if (!deps)
|
|
68
|
+
continue;
|
|
69
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
70
|
+
if (version === "workspace:*") {
|
|
71
|
+
const resolved = versions[name];
|
|
72
|
+
if (!resolved) {
|
|
73
|
+
throw new Error(`Cannot resolve version for workspace dependency "${name}". Ensure versions.json includes this package.`);
|
|
74
|
+
}
|
|
75
|
+
deps[name] = `^${resolved}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
80
|
+
};
|
|
81
|
+
const parseArgs = (args) => {
|
|
82
|
+
const positionals = [];
|
|
83
|
+
let dir;
|
|
84
|
+
let dirSeen = false;
|
|
85
|
+
for (let i = 0; i < args.length; i++) {
|
|
86
|
+
const a = args[i];
|
|
87
|
+
if (a === "--dir") {
|
|
88
|
+
if (dirSeen)
|
|
89
|
+
throw new Error("--dir specified more than once");
|
|
90
|
+
if (i + 1 >= args.length)
|
|
91
|
+
throw new Error("--dir requires a value");
|
|
92
|
+
dir = args[i + 1];
|
|
93
|
+
dirSeen = true;
|
|
94
|
+
i++;
|
|
95
|
+
}
|
|
96
|
+
else if (a.startsWith("--dir=")) {
|
|
97
|
+
if (dirSeen)
|
|
98
|
+
throw new Error("--dir specified more than once");
|
|
99
|
+
dir = a.slice("--dir=".length);
|
|
100
|
+
dirSeen = true;
|
|
101
|
+
}
|
|
102
|
+
else if (a.startsWith("-")) {
|
|
103
|
+
// Treats `--` and any --unknown as an unknown flag.
|
|
104
|
+
throw new Error(`unknown flag: ${a}`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
positionals.push(a);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (positionals.length === 0)
|
|
111
|
+
throw new Error("missing <project-name>");
|
|
112
|
+
if (positionals.length > 1)
|
|
113
|
+
throw new Error("too many positional arguments");
|
|
114
|
+
return { projectName: positionals[0], dir };
|
|
115
|
+
};
|
|
116
|
+
const deriveDirName = (projectName, dir) => {
|
|
117
|
+
if (dir !== undefined)
|
|
118
|
+
return dir;
|
|
119
|
+
if (projectName.startsWith("@")) {
|
|
120
|
+
const pkgPart = projectName.split("/")[1];
|
|
121
|
+
if (!pkgPart) {
|
|
122
|
+
// Unreachable when projectName has passed isValidProjectName (SCOPED_NAME_RE
|
|
123
|
+
// guarantees a non-empty package segment after the single "/"). Guarded here
|
|
124
|
+
// so refactors that reorder validation cannot silently produce undefined.
|
|
125
|
+
throw new Error(`invariant: unvalidated scoped name ${projectName}`);
|
|
126
|
+
}
|
|
127
|
+
return pkgPart;
|
|
128
|
+
}
|
|
129
|
+
return projectName;
|
|
130
|
+
};
|
|
131
|
+
// CLI entry point
|
|
132
|
+
export const main = () => {
|
|
133
|
+
const args = process.argv.slice(2);
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = parseArgs(args);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
console.error(`Error: ${e.message}`);
|
|
140
|
+
console.error("Usage: @o3co/create-auth-provider <project-name> [--dir <dir-name>]");
|
|
141
|
+
console.error("<project-name> must be a valid npm package name (scoped like @scope/pkg, or unscoped).");
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
const { projectName, dir } = parsed;
|
|
145
|
+
if (!isValidProjectName(projectName)) {
|
|
146
|
+
console.error("Error: <project-name> must be a valid npm package name (scoped like @scope/pkg, or unscoped; max 214 chars; no backslashes; no extra '/' beyond the single scope separator).");
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
if (dir !== undefined && !isValidDirName(dir)) {
|
|
150
|
+
console.error("Error: --dir must be a valid unscoped package name (no '/', '\\', '@'; not '.' or '..'; max 214 chars).");
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
const dirName = deriveDirName(projectName, dir);
|
|
154
|
+
const targetDir = resolve(process.cwd(), dirName);
|
|
155
|
+
if (existsSync(targetDir)) {
|
|
156
|
+
console.error(`Error: Directory '${dirName}' already exists.`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
console.log(`Creating ${projectName}...`);
|
|
160
|
+
scaffold(targetDir, projectName);
|
|
161
|
+
console.log(`\nDone! Created ${projectName} at ${targetDir}`);
|
|
162
|
+
console.log(`\nNext steps:`);
|
|
163
|
+
console.log(` cd ${dirName}`);
|
|
164
|
+
console.log(" npm install");
|
|
165
|
+
console.log(" npm run debug");
|
|
166
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@o3co/create-auth-provider",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Scaffold a new o3co/auth.provider project from the standalone template",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/o3co/auth.provider.git",
|
|
10
|
+
"directory": "create-app"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"create-auth-provider": "./dist/cli.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"templates"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"prebuild": "node scripts/copy-templates.mjs",
|
|
22
|
+
"test": "vitest run"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.1.0",
|
|
26
|
+
"vitest": "^4.1.2"
|
|
27
|
+
}
|
|
28
|
+
}
|