@bardioc/create-bardioc-app 0.4.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/LICENSE +5 -0
- package/README.md +105 -0
- package/bin/create.mjs +76 -0
- package/package.json +45 -0
- package/src/scaffold.d.ts +50 -0
- package/src/scaffold.js +379 -0
- package/templates/_base/.changeset/README.md +17 -0
- package/templates/_base/.changeset/config.json +12 -0
- package/templates/_base/.claude/commands/changeset-app.md +104 -0
- package/templates/_base/.claude/commands/refresh-bundle.md +101 -0
- package/templates/_base/README.md +89 -0
- package/templates/_base/public/app-manifest.json +10 -0
- package/templates/_base/scripts/stamp-manifest.mjs +17 -0
- package/templates/_opt-link/_npmrc +2 -0
- package/templates/_opt-link/pnpm-workspace.yaml +3 -0
- package/templates/_opt-link/scripts/link-source.mjs +188 -0
- package/templates/_opt-pipeline/bitbucket-pipelines.yml +100 -0
- package/templates/angular/.env.example +5 -0
- package/templates/angular/_gitignore +7 -0
- package/templates/angular/angular.json +64 -0
- package/templates/angular/package.json +49 -0
- package/templates/angular/postcss.config.mjs +5 -0
- package/templates/angular/public/icon.svg +1 -0
- package/templates/angular/public/runtime-env.js +1 -0
- package/templates/angular/scripts/dev-auth-proxy.mjs +92 -0
- package/templates/angular/scripts/sync-runtime-env.mjs +89 -0
- package/templates/angular/src/app/app.component.ts +181 -0
- package/templates/angular/src/app/bardioc-bridge.ts +5 -0
- package/templates/angular/src/app/bardioc.token.ts +4 -0
- package/templates/angular/src/index.html +15 -0
- package/templates/angular/src/main.ts +82 -0
- package/templates/angular/src/runtime-env.ts +17 -0
- package/templates/angular/src/styles.css +258 -0
- package/templates/angular/src/vite-env.d.ts +10 -0
- package/templates/angular/tsconfig.json +27 -0
- package/templates/manifest.json +13 -0
- package/templates/nextjs/.env.example +8 -0
- package/templates/nextjs/_gitignore +7 -0
- package/templates/nextjs/app/globals.css +75 -0
- package/templates/nextjs/app/layout.tsx +17 -0
- package/templates/nextjs/app/page.tsx +222 -0
- package/templates/nextjs/app/proxy/route.ts +85 -0
- package/templates/nextjs/global.d.ts +1 -0
- package/templates/nextjs/next-env.d.ts +5 -0
- package/templates/nextjs/next.config.ts +21 -0
- package/templates/nextjs/package.json +32 -0
- package/templates/nextjs/postcss.config.mjs +5 -0
- package/templates/nextjs/public/icon.svg +1 -0
- package/templates/nextjs/tailwind.config.ts +10 -0
- package/templates/nextjs/tsconfig.json +27 -0
- package/templates/preact/.env.example +13 -0
- package/templates/preact/_gitignore +9 -0
- package/templates/preact/index.html +13 -0
- package/templates/preact/package.json +32 -0
- package/templates/preact/public/icon.svg +1 -0
- package/templates/preact/src/App.tsx +139 -0
- package/templates/preact/src/index.css +76 -0
- package/templates/preact/src/main.tsx +46 -0
- package/templates/preact/src/vite-env.d.ts +11 -0
- package/templates/preact/tsconfig.json +19 -0
- package/templates/preact/vite.config.ts +17 -0
- package/templates/solid/.env.example +13 -0
- package/templates/solid/_gitignore +9 -0
- package/templates/solid/index.html +13 -0
- package/templates/solid/package.json +32 -0
- package/templates/solid/public/icon.svg +1 -0
- package/templates/solid/src/App.tsx +150 -0
- package/templates/solid/src/bardioc-sdk.tsx +33 -0
- package/templates/solid/src/index.css +76 -0
- package/templates/solid/src/main.tsx +50 -0
- package/templates/solid/src/vite-env.d.ts +11 -0
- package/templates/solid/tsconfig.json +20 -0
- package/templates/solid/vite.config.ts +17 -0
- package/templates/svelte/.env.example +5 -0
- package/templates/svelte/_gitignore +6 -0
- package/templates/svelte/index.html +13 -0
- package/templates/svelte/package.json +32 -0
- package/templates/svelte/public/icon.svg +1 -0
- package/templates/svelte/src/App.svelte +135 -0
- package/templates/svelte/src/index.css +76 -0
- package/templates/svelte/src/main.ts +42 -0
- package/templates/svelte/src/vite-env.d.ts +11 -0
- package/templates/svelte/tsconfig.json +13 -0
- package/templates/svelte/vite.config.ts +17 -0
- package/templates/vite/.env.example +14 -0
- package/templates/vite/CLAUDE.md +114 -0
- package/templates/vite/_gitignore +9 -0
- package/templates/vite/index.html +13 -0
- package/templates/vite/package.json +34 -0
- package/templates/vite/public/icon.svg +1 -0
- package/templates/vite/src/App.tsx +141 -0
- package/templates/vite/src/index.css +76 -0
- package/templates/vite/src/main.tsx +44 -0
- package/templates/vite/src/vite-env.d.ts +11 -0
- package/templates/vite/tsconfig.json +18 -0
- package/templates/vite/vite.config.ts +17 -0
- package/templates/vue/.env.example +5 -0
- package/templates/vue/_gitignore +6 -0
- package/templates/vue/index.html +13 -0
- package/templates/vue/package.json +32 -0
- package/templates/vue/public/icon.svg +1 -0
- package/templates/vue/src/App.vue +127 -0
- package/templates/vue/src/bardioc-sdk.ts +23 -0
- package/templates/vue/src/index.css +76 -0
- package/templates/vue/src/main.ts +43 -0
- package/templates/vue/src/vite-env.d.ts +17 -0
- package/templates/vue/tsconfig.json +26 -0
- package/templates/vue/vite.config.ts +17 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# @bardioc/create-bardioc-app
|
|
2
|
+
|
|
3
|
+
Scaffold a Vite app wired for `@bardioc/app-sdk`.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js `>=20.19.0`
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Configure the `@bardioc` scoped registry and install the CLI globally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm config set @bardioc:registry http://nexus.almato.com/repository/npm-group/ --location=user && npm install -g @bardioc/create-bardioc-app
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If your default npm registry is not `https://registry.npmjs.org/`, set it in your user-level npm config before installing app dependencies.
|
|
18
|
+
|
|
19
|
+
You can also run it without a global install:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm exec --yes @bardioc/create-bardioc-app my-app -- --port 3005
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or with pnpm:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm dlx @bardioc/create-bardioc-app my-app --port 3005
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
create-bardioc-app my-app --port 3005
|
|
35
|
+
cd apps/my-app
|
|
36
|
+
pnpm install
|
|
37
|
+
bardioc login
|
|
38
|
+
pnpm dev
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Generated app
|
|
42
|
+
|
|
43
|
+
The generated app includes:
|
|
44
|
+
|
|
45
|
+
- React + Vite project structure (or your chosen `--template`)
|
|
46
|
+
- `@bardioc/app-sdk` dependency
|
|
47
|
+
- `AppSdkProvider` wiring
|
|
48
|
+
- optional host-backed standalone dev-session wiring via `.env`
|
|
49
|
+
- a compact SDK demo with notification and transport examples
|
|
50
|
+
- `public/app-manifest.json`
|
|
51
|
+
|
|
52
|
+
### Release infra (out of the box)
|
|
53
|
+
|
|
54
|
+
Every scaffolded app ships a lightweight versioning toolkit:
|
|
55
|
+
|
|
56
|
+
- **Changesets** (`.changeset/` + `changeset`/`release:*` scripts) for semver-versioning the app
|
|
57
|
+
- **`scripts/stamp-manifest.mjs`** — folds `package.json`'s version into the built `app-manifest.json`
|
|
58
|
+
so the WebOS app store shows the real release version (wired into `pnpm build` / `pnpm bundle`)
|
|
59
|
+
- Two Claude skills under `.claude/commands/`: **`/changeset-app`** (bump locally) and
|
|
60
|
+
**`/refresh-bundle`** (rebuild the zip and swap it into a WebOS host checkout)
|
|
61
|
+
|
|
62
|
+
### Opt-in tooling
|
|
63
|
+
|
|
64
|
+
Heavier, environment-specific tooling is **off by default** — pass a flag to include it:
|
|
65
|
+
|
|
66
|
+
- **`--with-pipeline`** — a **Bitbucket pipeline** (`bitbucket-pipelines.yml`) running
|
|
67
|
+
check-types/build/audit on PRs, with a publish step on `dev`. Publishing is **dormant** while the
|
|
68
|
+
app stays `"private": true` + unscoped (`pnpm changeset publish` no-ops); the generated `README.md`
|
|
69
|
+
documents how to enable it.
|
|
70
|
+
- **`--with-link`** — the **`link:source` / `unlink:source`** dev flow (`scripts/link-source.mjs` +
|
|
71
|
+
`pnpm-workspace.yaml` + `.npmrc`) for developing against an in-tree `@bardioc/*` source checkout.
|
|
72
|
+
|
|
73
|
+
## Standalone dev session
|
|
74
|
+
|
|
75
|
+
Generated apps can opt into the same host-backed standalone dev-session flow used by the SDK examples.
|
|
76
|
+
|
|
77
|
+
1. Copy `.env.example` to `.env`
|
|
78
|
+
2. Fill in `VITE_APP_NAME`, `VITE_APP_ID`, and `DEV_AUTH_HOST_URL`
|
|
79
|
+
3. Run `bardioc login` once for that host
|
|
80
|
+
4. Set `VITE_ENABLE_DEV_AUTH=true`
|
|
81
|
+
|
|
82
|
+
Important: standalone login will not start unless `VITE_ENABLE_DEV_AUTH=true` is present in `.env`. Running `pnpm dev` with only `VITE_APP_NAME` and `VITE_APP_ID` is not enough.
|
|
83
|
+
|
|
84
|
+
When enabled, running `pnpm dev` outside the host iframe will use your stored CLI login to mint a fresh host dev session before the dev server starts, then route SDK transport calls through the local proxy into the host transport. `DEV_SESSION_KEY` no longer needs to be copied manually. If a request needs a specific scope, pass `scopeId` on that request.
|
|
85
|
+
|
|
86
|
+
For public testing, run `pnpm dev:live` and use the emitted tunnel URL as the app's live URL in the host App Store configuration.
|
|
87
|
+
|
|
88
|
+
## Options
|
|
89
|
+
|
|
90
|
+
- `--template <name>` — framework template (default `vite`)
|
|
91
|
+
- `--port <port>` — dev server port, default `3005`
|
|
92
|
+
- `--with-pipeline` — add the Bitbucket CI/publish pipeline (off by default)
|
|
93
|
+
- `--with-link` — add the `link:source` dev flow for local `@bardioc/*` checkouts (off by default)
|
|
94
|
+
|
|
95
|
+
## Support
|
|
96
|
+
|
|
97
|
+
For support, contact yevhenii.atlanov@almato.com.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT.
|
|
102
|
+
|
|
103
|
+
Copyright (c) 2026 ALMATO AG. All rights reserved.
|
|
104
|
+
|
|
105
|
+
This is an internal library for ALMATO AG.
|
package/bin/create.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { getDefaultTemplate, listTemplates, parseArgs, scaffoldApp } from '../src/scaffold.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
9
|
+
const TEMPLATES_DIR = join(__dirname, '..', 'templates');
|
|
10
|
+
const packageJsonPath = join(__dirname, '..', 'package.json');
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
16
|
+
const templates = listTemplates(TEMPLATES_DIR);
|
|
17
|
+
const defaultTemplate = getDefaultTemplate(TEMPLATES_DIR);
|
|
18
|
+
console.log(`
|
|
19
|
+
Usage: create-bardioc-app <app-name> [options]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--template <name> Framework template (default: ${defaultTemplate})
|
|
23
|
+
--port <port> Dev server port (default: 3005)
|
|
24
|
+
--with-pipeline Add the Bitbucket CI/publish pipeline (off by default)
|
|
25
|
+
--with-link Add the link:source dev flow for local @bardioc/* checkouts (off by default)
|
|
26
|
+
|
|
27
|
+
Available templates:
|
|
28
|
+
${templates.map(t => ` - ${t}`).join('\n')}
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
npm exec --yes @bardioc/create-bardioc-app my-app
|
|
32
|
+
npm exec --yes @bardioc/create-bardioc-app my-app -- --template vue
|
|
33
|
+
npm exec --yes @bardioc/create-bardioc-app my-app -- --template angular --port 3010
|
|
34
|
+
npm exec --yes @bardioc/create-bardioc-app my-app -- --with-pipeline --with-link
|
|
35
|
+
`);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const argsResult = parseArgs(args, TEMPLATES_DIR);
|
|
40
|
+
|
|
41
|
+
if (!argsResult.valid) {
|
|
42
|
+
console.error(`Error: ${argsResult.error}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { name, port, template, withPipeline, withLink } = argsResult;
|
|
47
|
+
const destBase = resolve(process.cwd());
|
|
48
|
+
const templateDir = join(TEMPLATES_DIR, template);
|
|
49
|
+
|
|
50
|
+
console.log(`\nCreating apps/${name}/ with ${template} template...\n`);
|
|
51
|
+
|
|
52
|
+
const result = scaffoldApp({
|
|
53
|
+
name,
|
|
54
|
+
port,
|
|
55
|
+
template,
|
|
56
|
+
templateDir,
|
|
57
|
+
destBase,
|
|
58
|
+
withPipeline,
|
|
59
|
+
withLink,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!result.success) {
|
|
63
|
+
console.error(`Error: ${result.error}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(` Done! Next steps:\n`);
|
|
68
|
+
console.log(` cd apps/${name}`);
|
|
69
|
+
console.log(` pnpm install`);
|
|
70
|
+
console.log(` bardioc login`);
|
|
71
|
+
console.log(` pnpm dev\n`);
|
|
72
|
+
console.log(` Open http://localhost:${port} to see your app.`);
|
|
73
|
+
console.log(
|
|
74
|
+
` Standalone host-backed auth now refreshes automatically from your CLI login during pnpm dev.`
|
|
75
|
+
);
|
|
76
|
+
console.log(` Use "pnpm dev:live" for a public tunnel/live URL.\n`);
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bardioc/create-bardioc-app",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Scaffold a Bardioc app with framework templates (vite, react, vue, angular, svelte, solid, preact, nextjs)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"private": false,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"registry": "https://registry.npmjs.org/",
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20.19.0"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"create-bardioc-app": "./bin/create.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"src",
|
|
21
|
+
"templates",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"bardioc",
|
|
27
|
+
"create",
|
|
28
|
+
"scaffold",
|
|
29
|
+
"vite",
|
|
30
|
+
"react"
|
|
31
|
+
],
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
34
|
+
"@types/node": "^25.6.0",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"vitest": "4.1.5"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"check-types": "tsc --noEmit",
|
|
40
|
+
"pack:check": "npm pack --dry-run",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:coverage": "vitest run --coverage",
|
|
43
|
+
"test:watch": "vitest"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function toPascalCase(str: string): string;
|
|
2
|
+
|
|
3
|
+
export function validateName(name: string | null): { valid: boolean; error?: string };
|
|
4
|
+
|
|
5
|
+
export function parsePort(rawPort: string): { valid: boolean; port?: number; error?: string };
|
|
6
|
+
|
|
7
|
+
export function replaceVars(content: string, vars: Record<string, string>): string;
|
|
8
|
+
|
|
9
|
+
export function isTextTemplateFile(filePath: string): boolean;
|
|
10
|
+
|
|
11
|
+
export function walkAndReplace(dir: string, vars: Record<string, string>): void;
|
|
12
|
+
|
|
13
|
+
export function loadTemplateCatalog(templatesDir?: string): {
|
|
14
|
+
schemaVersion: number;
|
|
15
|
+
defaultTemplate: string;
|
|
16
|
+
templates: string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function listTemplates(templatesDir?: string): string[];
|
|
20
|
+
|
|
21
|
+
export function getDefaultTemplate(templatesDir?: string): string;
|
|
22
|
+
|
|
23
|
+
export function parseArgs(
|
|
24
|
+
args: string[],
|
|
25
|
+
templatesDir?: string
|
|
26
|
+
): {
|
|
27
|
+
valid: boolean;
|
|
28
|
+
name?: string;
|
|
29
|
+
port?: number;
|
|
30
|
+
template?: string;
|
|
31
|
+
withPipeline?: boolean;
|
|
32
|
+
withLink?: boolean;
|
|
33
|
+
error?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function scaffoldApp(params: {
|
|
37
|
+
name: string;
|
|
38
|
+
port: number;
|
|
39
|
+
template?: string;
|
|
40
|
+
templateDir: string;
|
|
41
|
+
destBase: string;
|
|
42
|
+
withPipeline?: boolean;
|
|
43
|
+
withLink?: boolean;
|
|
44
|
+
}): {
|
|
45
|
+
success: boolean;
|
|
46
|
+
dest?: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
port?: number;
|
|
49
|
+
error?: string;
|
|
50
|
+
};
|
package/src/scaffold.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { dirname, extname, join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const TEMPLATE_MANIFEST_FILE = 'manifest.json';
|
|
13
|
+
const DEFAULT_TEMPLATE = 'vite';
|
|
14
|
+
const BASE_TEMPLATE_DIR = '_base';
|
|
15
|
+
const PIPELINE_OVERLAY_DIR = '_opt-pipeline';
|
|
16
|
+
const LINK_OVERLAY_DIR = '_opt-link';
|
|
17
|
+
const DOTFILE_RENAMES = { _gitignore: '.gitignore', _npmrc: '.npmrc' };
|
|
18
|
+
|
|
19
|
+
// Per-template values for the opt-in CI pipeline. Vite-family templates read the app slug from
|
|
20
|
+
// VITE_APP_NAME and build to dist/; only Next.js differs.
|
|
21
|
+
const DEFAULT_PIPELINE_VARS = { BUILD_ENV: 'VITE_APP_NAME', OUT_DIR: 'dist' };
|
|
22
|
+
const TEMPLATE_PIPELINE_VARS = {
|
|
23
|
+
nextjs: { BUILD_ENV: 'NEXT_PUBLIC_APP_NAME', OUT_DIR: 'out' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Added to package.json scripts only when scaffolding with --with-link.
|
|
27
|
+
const LINK_SCRIPTS = {
|
|
28
|
+
'link:source': 'node scripts/link-source.mjs',
|
|
29
|
+
'unlink:source': 'node scripts/link-source.mjs --remove',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Boolean CLI flags → the result key they set.
|
|
33
|
+
const BOOLEAN_FLAGS = {
|
|
34
|
+
'--with-pipeline': 'withPipeline',
|
|
35
|
+
'--with-link': 'withLink',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const TEXT_FILE_EXTENSIONS = new Set([
|
|
39
|
+
'.css',
|
|
40
|
+
'.d.ts',
|
|
41
|
+
'.html',
|
|
42
|
+
'.json',
|
|
43
|
+
'.js',
|
|
44
|
+
'.jsx',
|
|
45
|
+
'.md',
|
|
46
|
+
'.mjs',
|
|
47
|
+
'.mts',
|
|
48
|
+
'.svg',
|
|
49
|
+
'.ts',
|
|
50
|
+
'.tsx',
|
|
51
|
+
'.txt',
|
|
52
|
+
'.yml',
|
|
53
|
+
'.yaml',
|
|
54
|
+
]);
|
|
55
|
+
const TEXT_FILENAMES = new Set(['_gitignore', '_npmrc']);
|
|
56
|
+
const TEXT_FILE_SUFFIXES = ['.env.example'];
|
|
57
|
+
|
|
58
|
+
export function toPascalCase(str) {
|
|
59
|
+
return str
|
|
60
|
+
.split('-')
|
|
61
|
+
.map(w => w[0].toUpperCase() + w.slice(1))
|
|
62
|
+
.join('');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateName(name) {
|
|
66
|
+
if (!name) {
|
|
67
|
+
return { valid: false, error: 'app name is required' };
|
|
68
|
+
}
|
|
69
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
70
|
+
return { valid: false, error: 'name must be lowercase with hyphens (e.g. my-app)' };
|
|
71
|
+
}
|
|
72
|
+
return { valid: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function parsePort(rawPort) {
|
|
76
|
+
if (!/^\d+$/.test(rawPort)) {
|
|
77
|
+
return { valid: false, error: 'port must be an integer between 1024 and 65535' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const port = Number.parseInt(rawPort, 10);
|
|
81
|
+
|
|
82
|
+
if (Number.isNaN(port) || port < 1024 || port > 65535) {
|
|
83
|
+
return { valid: false, error: 'port must be an integer between 1024 and 65535' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { valid: true, port };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function replaceVars(content, vars) {
|
|
90
|
+
let result = content;
|
|
91
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
92
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isTextTemplateFile(filePath) {
|
|
98
|
+
return (
|
|
99
|
+
TEXT_FILENAMES.has(filePath.split('/').at(-1) ?? '') ||
|
|
100
|
+
TEXT_FILE_SUFFIXES.some(suffix => filePath.endsWith(suffix)) ||
|
|
101
|
+
TEXT_FILE_EXTENSIONS.has(extname(filePath))
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function walkAndReplace(dir, vars) {
|
|
106
|
+
for (const entry of readdirSync(dir)) {
|
|
107
|
+
const full = join(dir, entry);
|
|
108
|
+
if (statSync(full).isDirectory()) {
|
|
109
|
+
walkAndReplace(full, vars);
|
|
110
|
+
} else if (isTextTemplateFile(full)) {
|
|
111
|
+
const content = readFileSync(full, 'utf-8');
|
|
112
|
+
writeFileSync(full, replaceVars(content, vars));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function discoverTemplatesFromDirectory(templatesDir) {
|
|
118
|
+
if (!existsSync(templatesDir)) {
|
|
119
|
+
return [DEFAULT_TEMPLATE];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return readdirSync(templatesDir)
|
|
123
|
+
.filter(entry => {
|
|
124
|
+
if (entry === TEMPLATE_MANIFEST_FILE || entry.startsWith('_')) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const full = join(templatesDir, entry);
|
|
128
|
+
return statSync(full).isDirectory();
|
|
129
|
+
})
|
|
130
|
+
.sort();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function loadTemplateCatalog(templatesDir) {
|
|
134
|
+
const fallbackTemplates = discoverTemplatesFromDirectory(templatesDir);
|
|
135
|
+
const fallback = {
|
|
136
|
+
schemaVersion: 1,
|
|
137
|
+
defaultTemplate: fallbackTemplates.includes(DEFAULT_TEMPLATE)
|
|
138
|
+
? DEFAULT_TEMPLATE
|
|
139
|
+
: (fallbackTemplates[0] ?? DEFAULT_TEMPLATE),
|
|
140
|
+
templates: fallbackTemplates,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (!templatesDir) {
|
|
144
|
+
return fallback;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const manifestPath = join(templatesDir, TEMPLATE_MANIFEST_FILE);
|
|
148
|
+
if (!existsSync(manifestPath)) {
|
|
149
|
+
return fallback;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
153
|
+
|
|
154
|
+
if (manifest.schemaVersion !== 1) {
|
|
155
|
+
throw new Error(`unsupported template manifest schema version: ${manifest.schemaVersion}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!Array.isArray(manifest.templates) || manifest.templates.length === 0) {
|
|
159
|
+
throw new Error('template manifest must define a non-empty templates array');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const manifestTemplates = [...manifest.templates].sort();
|
|
163
|
+
const discoveredTemplates = [...fallbackTemplates].sort();
|
|
164
|
+
if (manifestTemplates.join(',') !== discoveredTemplates.join(',')) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`template manifest does not match templates directory. manifest=${manifestTemplates.join(',')} directory=${discoveredTemplates.join(',')}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
typeof manifest.defaultTemplate !== 'string' ||
|
|
172
|
+
!manifestTemplates.includes(manifest.defaultTemplate)
|
|
173
|
+
) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`template manifest defaultTemplate must be one of: ${manifestTemplates.join(', ')}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
schemaVersion: manifest.schemaVersion,
|
|
181
|
+
defaultTemplate: manifest.defaultTemplate,
|
|
182
|
+
templates: manifestTemplates,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function listTemplates(templatesDir) {
|
|
187
|
+
return loadTemplateCatalog(templatesDir).templates;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getDefaultTemplate(templatesDir) {
|
|
191
|
+
return loadTemplateCatalog(templatesDir).defaultTemplate;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readFlagValue(args, index, flagName) {
|
|
195
|
+
const value = args[index + 1];
|
|
196
|
+
if (!value) {
|
|
197
|
+
return { valid: false, error: `missing value for ${flagName}` };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { valid: true, value, nextIndex: index + 1 };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseTemplateArg(args, index, availableTemplates) {
|
|
204
|
+
const valueResult = readFlagValue(args, index, '--template');
|
|
205
|
+
if (!valueResult.valid) {
|
|
206
|
+
return valueResult;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!availableTemplates.includes(valueResult.value)) {
|
|
210
|
+
return {
|
|
211
|
+
valid: false,
|
|
212
|
+
error: `unknown template "${valueResult.value}". Available: ${availableTemplates.join(', ')}`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
valid: true,
|
|
218
|
+
template: valueResult.value,
|
|
219
|
+
nextIndex: valueResult.nextIndex,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parsePortArg(args, index) {
|
|
224
|
+
const valueResult = readFlagValue(args, index, '--port');
|
|
225
|
+
if (!valueResult.valid) {
|
|
226
|
+
return valueResult;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const portResult = parsePort(valueResult.value);
|
|
230
|
+
if (!portResult.valid) {
|
|
231
|
+
return portResult;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
valid: true,
|
|
236
|
+
port: /** @type {number} */ (portResult.port),
|
|
237
|
+
nextIndex: valueResult.nextIndex,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isPositionalName(arg, index, commandOffset, name) {
|
|
242
|
+
return !arg.startsWith('--') && index >= commandOffset && !name && arg !== 'create';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function scanArgs(args, commandOffset, catalog) {
|
|
246
|
+
let name = null;
|
|
247
|
+
let port = 3005;
|
|
248
|
+
let template = catalog.defaultTemplate;
|
|
249
|
+
const flags = { withPipeline: false, withLink: false };
|
|
250
|
+
const availableTemplates = catalog.templates;
|
|
251
|
+
|
|
252
|
+
let index = 0;
|
|
253
|
+
while (index < args.length) {
|
|
254
|
+
const arg = args[index];
|
|
255
|
+
|
|
256
|
+
if (arg === '--port') {
|
|
257
|
+
const portResult = parsePortArg(args, index);
|
|
258
|
+
if (!portResult.valid) {
|
|
259
|
+
return portResult;
|
|
260
|
+
}
|
|
261
|
+
port = portResult.port;
|
|
262
|
+
index = portResult.nextIndex + 1;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (arg === '--template') {
|
|
267
|
+
const templateResult = parseTemplateArg(args, index, availableTemplates);
|
|
268
|
+
if (!templateResult.valid) {
|
|
269
|
+
return templateResult;
|
|
270
|
+
}
|
|
271
|
+
template = templateResult.template;
|
|
272
|
+
index = templateResult.nextIndex + 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (arg in BOOLEAN_FLAGS) {
|
|
277
|
+
flags[BOOLEAN_FLAGS[arg]] = true;
|
|
278
|
+
index++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (isPositionalName(arg, index, commandOffset, name)) {
|
|
283
|
+
name = arg;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
index++;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { valid: true, name, port, template, ...flags };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function parseArgs(args, templatesDir) {
|
|
293
|
+
const firstPositionalArg = args.find(arg => !arg.startsWith('--'));
|
|
294
|
+
const commandOffset = firstPositionalArg === 'create' ? 1 : 0;
|
|
295
|
+
const catalog = loadTemplateCatalog(templatesDir);
|
|
296
|
+
|
|
297
|
+
const scan = scanArgs(args, commandOffset, catalog);
|
|
298
|
+
if (!scan.valid) {
|
|
299
|
+
return scan;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const nameValidation = validateName(scan.name);
|
|
303
|
+
if (!nameValidation.valid) {
|
|
304
|
+
return nameValidation;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return scan;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Merges the link:source scripts into a scaffolded package.json (used only with --with-link).
|
|
311
|
+
function addLinkScripts(dest) {
|
|
312
|
+
const pkgPath = join(dest, 'package.json');
|
|
313
|
+
if (!existsSync(pkgPath)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
317
|
+
pkg.scripts = { ...pkg.scripts, ...LINK_SCRIPTS };
|
|
318
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @param {object} params
|
|
323
|
+
* @param {string} params.name
|
|
324
|
+
* @param {number} params.port
|
|
325
|
+
* @param {string} [params.template]
|
|
326
|
+
* @param {string} params.templateDir
|
|
327
|
+
* @param {string} params.destBase
|
|
328
|
+
* @param {boolean} [params.withPipeline]
|
|
329
|
+
* @param {boolean} [params.withLink]
|
|
330
|
+
* @returns {{ success: boolean; dest?: string; name?: string; port?: number; error?: string }}
|
|
331
|
+
*/
|
|
332
|
+
export function scaffoldApp({
|
|
333
|
+
name,
|
|
334
|
+
port,
|
|
335
|
+
template,
|
|
336
|
+
templateDir,
|
|
337
|
+
destBase,
|
|
338
|
+
withPipeline = false,
|
|
339
|
+
withLink = false,
|
|
340
|
+
}) {
|
|
341
|
+
const dest = join(destBase, 'apps', name);
|
|
342
|
+
|
|
343
|
+
if (existsSync(dest)) {
|
|
344
|
+
return { success: false, error: `directory "apps/${name}" already exists` };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const vars = {
|
|
348
|
+
APP_NAME: name,
|
|
349
|
+
DISPLAY_NAME: toPascalCase(name),
|
|
350
|
+
PORT: String(port),
|
|
351
|
+
...(TEMPLATE_PIPELINE_VARS[template] ?? DEFAULT_PIPELINE_VARS),
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const templatesRoot = dirname(templateDir);
|
|
355
|
+
const baseDir = join(templatesRoot, BASE_TEMPLATE_DIR);
|
|
356
|
+
if (existsSync(baseDir)) {
|
|
357
|
+
cpSync(baseDir, dest, { recursive: true });
|
|
358
|
+
}
|
|
359
|
+
cpSync(templateDir, dest, { recursive: true });
|
|
360
|
+
|
|
361
|
+
if (withPipeline) {
|
|
362
|
+
cpSync(join(templatesRoot, PIPELINE_OVERLAY_DIR), dest, { recursive: true });
|
|
363
|
+
}
|
|
364
|
+
if (withLink) {
|
|
365
|
+
cpSync(join(templatesRoot, LINK_OVERLAY_DIR), dest, { recursive: true });
|
|
366
|
+
addLinkScripts(dest);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
walkAndReplace(dest, vars);
|
|
370
|
+
|
|
371
|
+
for (const [shipped, final] of Object.entries(DOTFILE_RENAMES)) {
|
|
372
|
+
const shippedSrc = join(dest, shipped);
|
|
373
|
+
if (existsSync(shippedSrc)) {
|
|
374
|
+
renameSync(shippedSrc, join(dest, final));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { success: true, dest, name, port };
|
|
379
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changesets
|
|
2
|
+
|
|
3
|
+
This app versions itself with [Changesets](https://github.com/changesets/changesets) — the same flow
|
|
4
|
+
as the `@bardioc/*` libraries.
|
|
5
|
+
|
|
6
|
+
**To cut a release: run the `/changeset-app` skill** (or `pnpm changeset`) — it picks the semver bump
|
|
7
|
+
and applies it locally (`pnpm release:version`), so `package.json`'s `version` always reflects the
|
|
8
|
+
release. Commit the result with your PR; merging to `dev` runs the publish step in the pipeline.
|
|
9
|
+
|
|
10
|
+
The skill is named `changeset-app` (not `changeset`) to avoid clashing with the `/changeset` command
|
|
11
|
+
in `bardioc-desktop-frontend` when both repos are open in one workspace. It's app-agnostic — it reads
|
|
12
|
+
the published package name from `package.json` — so the same skill file works in any app repo.
|
|
13
|
+
|
|
14
|
+
> **This app ships private + unscoped, so publishing is dormant.** `pnpm changeset publish` no-ops on
|
|
15
|
+
> a private package — versioning works, but nothing is pushed to a registry. To actually publish to
|
|
16
|
+
> Nexus, scope the name (e.g. `@bardioc/<app>`), set `"private": false`, and add a
|
|
17
|
+
> `publishConfig.registry`; then the pipeline's publish step starts shipping the built `dist/`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json",
|
|
3
|
+
"changelog": "@changesets/cli/changelog",
|
|
4
|
+
"commit": false,
|
|
5
|
+
"fixed": [],
|
|
6
|
+
"linked": [],
|
|
7
|
+
"access": "restricted",
|
|
8
|
+
"baseBranch": "dev",
|
|
9
|
+
"updateInternalDependencies": "patch",
|
|
10
|
+
"onlyUpdatePeerDependentsWhenParentChanged": true,
|
|
11
|
+
"ignore": []
|
|
12
|
+
}
|