Skip to content

Commit 1aa24f3

Browse files
authored
feat: Move examples into separate example repository (#27)
1 parent 0d2703c commit 1aa24f3

163 files changed

Lines changed: 5249 additions & 1529 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ coverage
2424
# Build Outputs
2525
.next/
2626
out/
27+
my-project/
2728
build
2829
dist
2930
/.cache

AGENTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
Refer to the README.md for information about this repository.
44

55
## Project Structure & Module Organization
6-
`create/` contains the Node-based scaffold (`index.js` normalizes CLI options, `main.js` mutates the template). `template/` ships the starter monorepo with workspaces such as `api/` for backend endpoints, `mock-backend/` and `mock-data/` for fixtures, `prototype/` for the prototype app, `ui/` for shared components, and `docs/` for the Antora documentation site. Shared lint and TS configs live in `template/config/`, and bootstrap assets for generated apps sit under `template`.
6+
`create/` contains the Node-based scaffold (`index.js` normalizes CLI options, `main.js` mutates the template). `template/` ships the default starter monorepo with workspaces such as `api/`, `mocks/`, `msw/`, `prototype/`, `ui/`, and `docs/`. `example-templates/` contains optional independent examples that can be copied into generated projects under `examples/<name>/`. Shared lint and TS configs live in `template/config/`.
77

88
## Build, Test & Development Commands
99
Use Node 22+. Run `npm install` at the repo root before touching the CLI. Template work happens inside `template/`: `npm install` for dependencies, `npm run dev` to launch Turbo-powered development, `npm run build` for a production check, `npm run typecheck` for repository-wide TypeScript validation, `npm run test` to execute Vitest suites, and `npm run format` to apply Prettier across Markdown and TypeScript files.
1010

1111
## Coding Style & Naming Conventions
12-
Prettier enforces two-space indentation, single quotes, and trailing commas (`npm run format`). Keep imports auto-organized by the Prettier organize-imports plugin. Use `camelCase` for variables and functions, `PascalCase` for React components and types, and kebab-case for file names (e.g., `generate-data.mjs`). ESLint rules from `template/config/eslint/` run in every workspaceresolve warnings or document exceptions in-code.
12+
Prettier enforces two-space indentation, single quotes, and trailing commas (`npm run format`). Keep imports auto-organized by the Prettier organize-imports plugin. Use `camelCase` for variables and functions, `PascalCase` for React components and types, and kebab-case for file names (e.g. `generate-data.mjs`). ESLint rules from `template/config/eslint/` run in every workspace, so resolve warnings or document exceptions in-code.
1313

1414
## Testing Guidelines
15-
Vitest handles unit and integration coverage; colocate specs as `*.test.ts` or `*.spec.ts`. Run `npm run test` for the full suite, or target a package with `npm run test -- --filter=@repo/ui`. Always follow tests with `npm run typecheck` before opening a PR, and extend coverage around generators (`mock-data/cli`) and UI behavior when adding features.
15+
Vitest handles unit and integration coverage; colocate specs as `*.test.ts` or `*.spec.ts`. Run `npm run test` for the full suite, or target a package with `npm run test -- --filter=@repo/ui`. Always follow tests with `npm run typecheck` before opening a PR. When changing the scaffold, cover both the default starter and any affected example templates.
1616

1717
## Commit & Pull Request Guidelines
1818
Mirror the existing history by writing concise, imperative subjects (`add build verification for Github`). Group logically related changes per commit. Pull requests must include a summary, testing notes (`npm run build`, `npm run test`, etc.), linked issues when applicable, and screenshots for UI updates.
1919

2020
## Setup & Configuration Notes
21-
The scaffold copies `.env.example` to `.env` and swaps `planning-stack-template` tokens; keep those references in sync when editing template assets. Honor the `SKIP_SETUP` and `SKIP_FORMAT` flags in `create/main.js` so automated flows remain consistent. Update every consumer (e.g., `template/gitlab-ci.yml`) when adjusting CI scaffolding.
21+
The scaffold copies `.env.example` to `.env` and swaps `planning-stack-template` tokens in the base template. Honor the `SKIP_SETUP` and `SKIP_FORMAT` flags in `create/main.js` so automated flows remain consistent. Keep example templates independent from the base workspace so they can be deleted safely and are not built automatically by the root Turborepo configuration.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

CONTRIBUTING.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,34 @@
11

22

3-
43
## Local testing
54

65
To verify the generator before publishing:
76

8-
1. Run `npm pack` in this repository to produce a tarball (for example `create-tinker-stack-0.2.6.tgz`).
9-
2. In a temporary directory, execute `npx --yes create-tinker-stack@file://$(pwd)/create-tinker-stack-0.2.6.tgz`.
10-
- Replace the path with the absolute path to the tarball from step 1 (npm `create` does not currently support `file:` specifiers).
7+
1. Run `npm pack` in this repository to produce a tarball (for example `create-tinker-stack-0.2.8.tgz`).
8+
2. Run the generator from this repository, passing a target directory as a positional argument:
9+
10+
```bash
11+
npx --yes create-tinker-stack@file://$(pwd)/create-tinker-stack-0.2.8.tgz my-project
12+
```
13+
14+
This scaffolds a clean starter into `./my-project`.
15+
16+
### With the example bundle
17+
18+
Pass `--with-example` to also generate the `react-router` example, or name a specific example with `--example`:
19+
20+
```bash
21+
# Default example (react-router)
22+
npx --yes create-tinker-stack@file://$(pwd)/create-tinker-stack-0.2.8.tgz my-project --with-example
23+
24+
# Named example
25+
npx --yes create-tinker-stack@file://$(pwd)/create-tinker-stack-0.2.8.tgz my-project --example react-router
26+
```
27+
28+
The example is placed in `my-project/examples/react-router/` and is independent of the root workspace.
29+
30+
### Notes
31+
32+
- `$(pwd)` must be run from the repository root where the tarball was produced.
33+
- `npm create` does not currently support `file:` specifiers, so `npx` must be used.
34+
- Omit the target directory argument to scaffold into the current working directory.

README.md

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,41 @@
1-
# Frontend Monorepo for Rapid Prototyping
1+
# Tinker Stack
22

3-
This is a npm Template for a Frontend Monorepo.
3+
Tinker Stack is a project scaffold for frontend monorepos focused on rapid prototyping.
44

5-
It puts emphasis on rapid prototyping and a prototype-driven development
6-
([Pixar Planning](https://www.youtube.com/watch?v=gbuWJ48T0bE&t=1294s)).
5+
By default it generates a clean starter workspace with:
76

8-
The prototypes are using a virtual and ephemeral backend, so changes can be made very quickly without any need for
9-
data migrations on schema changes.
7+
- a minimal React Router prototype shell
8+
- shared `api`, `ui`, `msw`, and `mocks` packages
9+
- Turborepo, TypeScript, ESLint, Prettier, and Antora wiring
10+
- every example template copied into `examples/<name>/`
1011

11-
Stakeholders can test a fully working UI before a single line of backend code has been written.
12+
The scaffold no longer ships a demo application in the base workspace.
1213

13-
Test UI concepts on day 1, iterate on them with the designer on day 2 and present them to the customer on day 3.
14+
## Create a project
1415

15-
The API to the backend has usually had several major iterations before a backend developer even gets involved.
16-
17-
## What is included?
18-
19-
This monorepo contains the following packages/apps:
20-
21-
### Apps and Packages
16+
```bash
17+
npm create tinker-stack@latest
18+
```
2219

23-
- A prototype for the application, based on React Router SPA.
24-
- A package that provides the domain types and enums that can be shared with the backend.
25-
- A documentation in [Antora](https://antora.org/) (AsciiDoc) format.
26-
- A package that provides synthetic data for the applications using
27-
[Faker.js](https://fakerjs.dev/).
28-
- A package that provides a mock API via service workers using [MSW](https://mswjs.io/).
29-
- A component library that is shared by the main application and the prototype (still empty).
30-
- ESLint and TypeScript configurations that are shared throughout the monorepo.
20+
To generate the base workspace without any example folders:
3121

32-
### Features
22+
```bash
23+
npm create tinker-stack@latest -- --no-examples
24+
```
3325

34-
- [TypeScript](https://www.typescriptlang.org/) Monorepo based on ESM standards.
35-
- Turborepo setup for building and running the monorepo.
36-
- Code formatting with [Prettier](https://prettier.io/) and linting with
37-
[ESLint](https://eslint.org/).
26+
To generate only specific example templates:
3827

39-
## Documentation
28+
```bash
29+
npm create tinker-stack@latest -- --example react-router
30+
```
4031

41-
The project documentation is written in [AsciiDoc](https://asciidoctor.org/) and is generated using
42-
[Antora](https://antora.org/).
32+
By default, all available examples are copied into `examples/<name>/` inside the generated project.
33+
They are self-contained and can be deleted without affecting the main workspace.
4334

44-
## Getting Started
35+
## Example templates
4536

46-
To get started, run the following command:
37+
The generator is designed for multiple named examples. Today the repository includes:
4738

48-
```bash
49-
npm create tinker-stack@latest
50-
```
39+
- `react-router`
5140

52-
This will create a new project based on the Tinker Stack.
41+
Each example is installed and run separately from its own directory. The root project does not add example folders to its npm workspaces, so Turborepo ignores them by default.

create/index.js

Lines changed: 149 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,47 +8,173 @@ import { fileURLToPath } from 'node:url';
88
const __filename = fileURLToPath(import.meta.url);
99
const packageRoot = path.dirname(path.dirname(__filename));
1010
const templateDir = path.join(packageRoot, 'template');
11+
const exampleTemplatesDir = path.join(packageRoot, 'example-templates');
12+
const DEFAULT_EXAMPLE = 'react-router';
1113

12-
function targetFromArgv() {
13-
const argv = process.argv.slice(2);
14-
// prefer a last non-flag positional argument as the target folder
15-
for (let i = argv.length - 1; i >= 0; i--) {
16-
if (!argv[i].startsWith('-')) return argv[i];
14+
function parseCliArgs(argv = process.argv.slice(2)) {
15+
const options = {
16+
cwd: undefined,
17+
debug: undefined,
18+
install: undefined,
19+
examples: [],
20+
noExamples: false,
21+
withExample: false
22+
};
23+
const positionals = [];
24+
25+
for (let i = 0; i < argv.length; i++) {
26+
const arg = argv[i];
27+
28+
if (arg === '--example' || arg === '--examples') {
29+
const value = argv[i + 1];
30+
if (value && !value.startsWith('-')) {
31+
options.examples.push(...splitExampleNames(value));
32+
i += 1;
33+
}
34+
continue;
35+
}
36+
37+
if (arg.startsWith('--example=')) {
38+
options.examples.push(...splitExampleNames(arg.slice('--example='.length)));
39+
continue;
40+
}
41+
42+
if (arg.startsWith('--examples=')) {
43+
options.examples.push(...splitExampleNames(arg.slice('--examples='.length)));
44+
continue;
45+
}
46+
47+
if (arg === '--with-example') {
48+
options.withExample = true;
49+
continue;
50+
}
51+
52+
if (arg === '--no-examples') {
53+
options.noExamples = true;
54+
continue;
55+
}
56+
57+
if (arg === '--cwd') {
58+
const value = argv[i + 1];
59+
if (value && !value.startsWith('-')) {
60+
options.cwd = value;
61+
i += 1;
62+
}
63+
continue;
64+
}
65+
66+
if (arg.startsWith('--cwd=')) {
67+
options.cwd = arg.slice('--cwd='.length);
68+
continue;
69+
}
70+
71+
if (arg === '--debug') {
72+
options.debug = true;
73+
continue;
74+
}
75+
76+
if (arg === '--install') {
77+
options.install = true;
78+
continue;
79+
}
80+
81+
if (arg === '--no-install') {
82+
options.install = false;
83+
continue;
84+
}
85+
86+
if (!arg.startsWith('-')) {
87+
positionals.push(arg);
88+
}
89+
}
90+
91+
return {
92+
...options,
93+
positionals
94+
};
95+
}
96+
97+
function splitExampleNames(value) {
98+
return value
99+
.split(',')
100+
.map(example => example.trim())
101+
.filter(Boolean);
102+
}
103+
104+
function normalizeExampleNames(value) {
105+
if (Array.isArray(value)) {
106+
return value.flatMap(example => normalizeExampleNames(example));
17107
}
18-
return undefined;
108+
109+
if (typeof value === 'string') {
110+
return splitExampleNames(value);
111+
}
112+
113+
return [];
19114
}
20115

21116
function normalizeOptions(input = {}) {
22117
// input may be an options object passed by npm init or undefined
23118
let opts = {};
24119
if (typeof input === 'object' && input !== null) opts = input;
120+
const cli = parseCliArgs();
25121

26122
const targetDir =
27123
opts.targetDirectory ||
28124
opts.name || // common field name for npm init
29125
opts.project ||
30-
targetFromArgv();
126+
cli.positionals.at(-1);
31127

32128
const resolvedCwd =
33-
typeof opts.cwd === 'string' && opts.cwd.length > 0
34-
? path.resolve(process.cwd(), opts.cwd)
129+
typeof (opts.cwd ?? cli.cwd) === 'string' && (opts.cwd ?? cli.cwd).length > 0
130+
? path.resolve(process.cwd(), opts.cwd ?? cli.cwd)
35131
: process.cwd();
36132

37133
const resolvedTargetDir =
38134
typeof targetDir === 'string' && targetDir.length > 0
39135
? path.resolve(resolvedCwd, targetDir)
40136
: undefined;
41137

138+
const explicitExamples = [
139+
...normalizeExampleNames(opts.examples),
140+
...normalizeExampleNames(opts.example),
141+
...normalizeExampleNames(cli.examples)
142+
];
143+
144+
if (opts.withExample === true || cli.withExample) {
145+
explicitExamples.push(DEFAULT_EXAMPLE);
146+
}
147+
148+
const noExamples = Boolean(opts.noExamples ?? cli.noExamples);
149+
const hasExplicitExamples = explicitExamples.length > 0;
150+
const examples = hasExplicitExamples ? explicitExamples : noExamples ? [] : getDefaultExamples();
151+
const normalizedExamples = [...new Set(examples)];
152+
42153
return {
43154
...opts,
44155
targetDir: resolvedTargetDir,
45-
debug: !!opts.debug,
46-
install: opts.install ?? true,
156+
debug: Boolean(opts.debug ?? cli.debug),
157+
install: opts.install ?? cli.install ?? true,
47158
cwd: resolvedCwd,
48-
templateDir // always use the built-in template
159+
templateDir, // always use the built-in template
160+
exampleTemplatesDir,
161+
noExamples,
162+
examples: normalizedExamples
49163
};
50164
}
51165

166+
function getDefaultExamples() {
167+
try {
168+
return fs
169+
.readdirSync(exampleTemplatesDir, { withFileTypes: true })
170+
.filter(entry => entry.isDirectory())
171+
.map(entry => entry.name)
172+
.sort();
173+
} catch {
174+
return [];
175+
}
176+
}
177+
52178
async function main(...args) {
53179
const opts = normalizeOptions(args[0]);
54180

@@ -62,6 +188,17 @@ async function main(...args) {
62188
throw new Error(`Template directory not found: ${templateDir}\n${err.message}`);
63189
}
64190

191+
if (opts.examples.length > 0) {
192+
try {
193+
const stat = fs.statSync(exampleTemplatesDir);
194+
if (!stat.isDirectory()) {
195+
throw new Error(`Example templates path is not a directory: ${exampleTemplatesDir}`);
196+
}
197+
} catch (err) {
198+
throw new Error(`Example templates directory not found: ${exampleTemplatesDir}\n${err.message}`);
199+
}
200+
}
201+
65202
// call the implementation with simple, normalized options
66203
await createMain(opts);
67204
}

0 commit comments

Comments
 (0)