@icebreakers/monorepo-templates 2.1.0 → 2.2.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.
Files changed (104) hide show
  1. package/assets/.agents/skills/repoctl/SKILL.md +2 -0
  2. package/assets/.agents/skills/repoctl/references/commands.md +224 -13
  3. package/assets/.agents/skills/repoctl/references/config.md +70 -6
  4. package/assets/.agents/skills/repoctl/references/dependencies.md +37 -0
  5. package/assets/.agents/skills/repoctl/references/knip.md +34 -0
  6. package/assets/.agents/skills/repoctl/references/templates.md +23 -0
  7. package/assets/.github/workflows/ci.yml +10 -10
  8. package/assets/package.json +5 -9
  9. package/assets/pnpm-workspace.yaml +2 -1
  10. package/assets/repoctl.config.ts +1 -1
  11. package/assets/turbo.json +2 -1
  12. package/dist/cli.mjs +1 -1
  13. package/dist/index.d.mts +281 -1
  14. package/dist/index.mjs +1137 -74
  15. package/dist/{prepare-2VvFwYDz.mjs → prepare-BYNSxvoB.mjs} +76 -4
  16. package/package.json +2 -1
  17. package/template-data.mjs +24 -0
  18. package/templates/client/package.json +1 -1
  19. package/templates/next/README.md +31 -0
  20. package/templates/next/eslint.config.js +5 -0
  21. package/templates/next/gitignore +7 -0
  22. package/templates/next/next.config.ts +5 -0
  23. package/templates/next/package.json +36 -0
  24. package/templates/next/src/app/api/health/route.ts +5 -0
  25. package/templates/next/src/app/counter.tsx +19 -0
  26. package/templates/next/src/app/favicon.ico +0 -0
  27. package/templates/next/src/app/globals.css +109 -0
  28. package/templates/next/src/app/layout.tsx +17 -0
  29. package/templates/next/src/app/page.tsx +16 -0
  30. package/templates/next/stylelint.config.js +3 -0
  31. package/templates/next/test/counter.test.tsx +22 -0
  32. package/templates/next/tsconfig.json +40 -0
  33. package/templates/next/turbo.json +24 -0
  34. package/templates/next/vitest.config.js +9 -0
  35. package/templates/react-lib/LICENSE +21 -0
  36. package/templates/react-lib/README.md +52 -0
  37. package/templates/react-lib/eslint.config.js +5 -0
  38. package/templates/react-lib/gitignore +4 -0
  39. package/templates/react-lib/package.json +48 -0
  40. package/templates/react-lib/src/counter.tsx +28 -0
  41. package/templates/react-lib/src/index.ts +2 -0
  42. package/templates/react-lib/src/style.css +36 -0
  43. package/templates/react-lib/stylelint.config.js +3 -0
  44. package/templates/react-lib/test/counter.test.tsx +28 -0
  45. package/templates/react-lib/test-d/index.test-d.ts +11 -0
  46. package/templates/react-lib/tsconfig.app.json +15 -0
  47. package/templates/react-lib/tsconfig.build.json +12 -0
  48. package/templates/react-lib/tsconfig.json +4 -0
  49. package/templates/react-lib/tsconfig.node.json +11 -0
  50. package/templates/react-lib/vite.config.ts +22 -0
  51. package/templates/react-lib/vitest.config.ts +5 -0
  52. package/templates/react-vite/README.md +27 -0
  53. package/templates/react-vite/eslint.config.js +5 -0
  54. package/templates/react-vite/gitignore +6 -0
  55. package/templates/react-vite/index.html +13 -0
  56. package/templates/react-vite/package.json +36 -0
  57. package/templates/react-vite/src/app.tsx +22 -0
  58. package/templates/react-vite/src/main.tsx +15 -0
  59. package/templates/react-vite/src/style.css +95 -0
  60. package/templates/react-vite/stylelint.config.js +3 -0
  61. package/templates/react-vite/test/app.test.tsx +22 -0
  62. package/templates/react-vite/tsconfig.app.json +26 -0
  63. package/templates/react-vite/tsconfig.json +11 -0
  64. package/templates/react-vite/tsconfig.node.json +18 -0
  65. package/templates/react-vite/vite.config.ts +6 -0
  66. package/templates/react-vite/vitest.config.ts +9 -0
  67. package/templates/vitepress/.vitepress/navigation/routes.ts +7 -0
  68. package/templates/vitepress/reference/artifacts.md +51 -0
  69. package/templates/vitepress/reference/commands.md +149 -0
  70. package/templates/vitepress/reference/config.md +101 -7
  71. package/templates/vitepress/reference/dependencies.md +116 -0
  72. package/templates/vitepress/reference/dependency-admission.md +67 -0
  73. package/templates/vitepress/reference/devcontainer.md +25 -0
  74. package/templates/vitepress/reference/index.md +1 -0
  75. package/templates/vitepress/reference/install-security.md +51 -0
  76. package/templates/vitepress/reference/knip.md +55 -0
  77. package/templates/vitepress/reference/move.md +39 -0
  78. package/templates/vitepress/reference/organization-presets.md +89 -0
  79. package/templates/vitepress/reference/removal.md +42 -0
  80. package/templates/vitepress/reference/template-assets.md +39 -0
  81. package/templates/vitepress/reference/templates.md +372 -9
  82. package/templates/vitepress/start/diagnose.md +19 -2
  83. package/templates/vitepress/tasks/checks.md +149 -1
  84. package/templates/vitepress/tasks/ci.md +93 -1
  85. package/templates/vitepress/tasks/release.md +78 -8
  86. package/templates/vitepress/zh/reference/artifacts.md +51 -0
  87. package/templates/vitepress/zh/reference/commands.md +148 -0
  88. package/templates/vitepress/zh/reference/config.md +94 -2
  89. package/templates/vitepress/zh/reference/dependencies.md +116 -0
  90. package/templates/vitepress/zh/reference/dependency-admission.md +67 -0
  91. package/templates/vitepress/zh/reference/devcontainer.md +25 -0
  92. package/templates/vitepress/zh/reference/index.md +1 -0
  93. package/templates/vitepress/zh/reference/install-security.md +51 -0
  94. package/templates/vitepress/zh/reference/knip.md +55 -0
  95. package/templates/vitepress/zh/reference/move.md +39 -0
  96. package/templates/vitepress/zh/reference/organization-presets.md +89 -0
  97. package/templates/vitepress/zh/reference/removal.md +42 -0
  98. package/templates/vitepress/zh/reference/template-assets.md +19 -4
  99. package/templates/vitepress/zh/reference/templates.md +352 -9
  100. package/templates/vitepress/zh/start/diagnose.md +24 -13
  101. package/templates/vitepress/zh/tasks/checks.md +150 -0
  102. package/templates/vitepress/zh/tasks/ci.md +93 -1
  103. package/templates/vitepress/zh/tasks/release.md +78 -8
  104. package/templates/vue-lib/package.json +1 -1
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@icebreakers/react-lib-template",
3
+ "type": "module",
4
+ "version": "0.0.0",
5
+ "private": true,
6
+ "description": "React component library with explicit JavaScript, types and CSS exports",
7
+ "license": "MIT",
8
+ "sideEffects": ["**/*.css"],
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./style.css": "./dist/style.css"
15
+ },
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "files": ["LICENSE", "README.md", "dist"],
19
+ "engines": { "node": ">=22.13.0" },
20
+ "scripts": {
21
+ "build": "vite build",
22
+ "lint": "eslint . && stylelint \"src/**/*.css\"",
23
+ "lint:fix": "eslint . --fix && stylelint \"src/**/*.css\" --fix",
24
+ "typecheck": "tsc -b",
25
+ "tsd": "tsd --files test-d/**/*.test-d.ts",
26
+ "test": "vitest run",
27
+ "test:dev": "vitest"
28
+ },
29
+ "peerDependencies": {
30
+ "react": "^19.3.0",
31
+ "react-dom": "^19.3.0"
32
+ },
33
+ "devDependencies": {
34
+ "@testing-library/dom": "^10.4.2",
35
+ "@testing-library/react": "^16.3.3",
36
+ "@types/react": "^19.3.0",
37
+ "@types/react-dom": "^19.3.0",
38
+ "@vitejs/plugin-react": "^6.1.1",
39
+ "jsdom": "^30.1.1",
40
+ "react": "^19.3.0",
41
+ "react-dom": "^19.3.0",
42
+ "tsd": "^0.33.0",
43
+ "typescript": "~6.0.3",
44
+ "vite": "^8.3.2",
45
+ "vite-plugin-dts": "^5.1.1",
46
+ "vitest": "~5.0.3"
47
+ }
48
+ }
@@ -0,0 +1,28 @@
1
+ import { useState } from 'react'
2
+ import './style.css'
3
+
4
+ export interface CounterProps {
5
+ /** Accessible name for this counter's current value. */
6
+ label?: string
7
+ /** Initial value, also used by the reset action. */
8
+ initialCount?: number
9
+ step?: number
10
+ disabled?: boolean
11
+ onCountChange?: (count: number) => void
12
+ }
13
+
14
+ export function Counter({ label = 'Count', initialCount = 0, step = 1, disabled = false, onCountChange }: CounterProps) {
15
+ const [count, setCount] = useState(initialCount)
16
+ function update(next: number) {
17
+ setCount(next)
18
+ onCountChange?.(next)
19
+ }
20
+
21
+ return (
22
+ <div className="repoctl-counter">
23
+ <output aria-label={label} aria-live="polite">{count}</output>
24
+ <button type="button" disabled={disabled} onClick={() => update(count + step)}>Increase</button>
25
+ <button type="button" disabled={disabled || count === initialCount} onClick={() => update(initialCount)}>Reset</button>
26
+ </div>
27
+ )
28
+ }
@@ -0,0 +1,2 @@
1
+ export { Counter } from './counter.js'
2
+ export type { CounterProps } from './counter.js'
@@ -0,0 +1,36 @@
1
+ .repoctl-counter {
2
+ display: inline-flex;
3
+ gap: 0.75rem;
4
+ align-items: center;
5
+ padding: 1rem;
6
+ font: inherit;
7
+ color: #14283a;
8
+ border: 1px solid #ccd4dc;
9
+ border-radius: 0.75rem;
10
+ }
11
+
12
+ .repoctl-counter output {
13
+ min-width: 2ch;
14
+ font-variant-numeric: tabular-nums;
15
+ text-align: center;
16
+ }
17
+
18
+ .repoctl-counter button {
19
+ padding: 0.5rem 0.75rem;
20
+ font: inherit;
21
+ color: #fff;
22
+ cursor: pointer;
23
+ background: #14283a;
24
+ border: 0;
25
+ border-radius: 0.375rem;
26
+ }
27
+
28
+ .repoctl-counter button:focus-visible {
29
+ outline: 2px solid #176bb4;
30
+ outline-offset: 3px;
31
+ }
32
+
33
+ .repoctl-counter button:disabled {
34
+ cursor: default;
35
+ opacity: 0.5;
36
+ }
@@ -0,0 +1,3 @@
1
+ import { defineStylelintConfig } from 'repoctl/tooling'
2
+
3
+ export default await defineStylelintConfig()
@@ -0,0 +1,28 @@
1
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react'
2
+ import { afterEach, expect, it, vi } from 'vitest'
3
+ import { Counter } from '..'
4
+
5
+ afterEach(cleanup)
6
+
7
+ it('uses the built public entry to update, notify and reset the initial value', () => {
8
+ const onCountChange = vi.fn()
9
+ render(<Counter label="Items" initialCount={3} step={2} onCountChange={onCountChange} />)
10
+ const count = screen.getByLabelText('Items')
11
+ const reset = screen.getByRole('button', { name: 'Reset' })
12
+ expect(count.textContent).toBe('3')
13
+ expect(reset.hasAttribute('disabled')).toBe(true)
14
+ fireEvent.click(screen.getByRole('button', { name: 'Increase' }))
15
+ expect(count.textContent).toBe('5')
16
+ expect(onCountChange).toHaveBeenLastCalledWith(5)
17
+ fireEvent.click(reset)
18
+ expect(count.textContent).toBe('3')
19
+ expect(onCountChange).toHaveBeenLastCalledWith(3)
20
+ })
21
+
22
+ it('prevents changes when disabled', () => {
23
+ const onCountChange = vi.fn()
24
+ render(<Counter disabled onCountChange={onCountChange} />)
25
+ fireEvent.click(screen.getByRole('button', { name: 'Increase' }))
26
+ expect(screen.getByLabelText('Count').textContent).toBe('0')
27
+ expect(onCountChange).not.toHaveBeenCalled()
28
+ })
@@ -0,0 +1,11 @@
1
+ import type { ComponentProps, ReactElement } from 'react'
2
+ import type { CounterProps } from '..'
3
+ import { createElement } from 'react'
4
+ import { expectAssignable, expectError, expectType } from 'tsd'
5
+ import { Counter } from '..'
6
+
7
+ expectAssignable<CounterProps>({ initialCount: 2, step: 3, onCountChange: count => expectType<number>(count) })
8
+ expectAssignable<ComponentProps<typeof Counter>>({ label: 'Items', disabled: true })
9
+ expectAssignable<ReactElement<CounterProps>>(createElement(Counter, { initialCount: 2 }))
10
+ expectError(createElement(Counter, { initialCount: '2' }))
11
+ expectError(createElement(Counter, { onCountChange: (value: string) => value }))
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5
+ "target": "ES2022",
6
+ "jsx": "react-jsx",
7
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
8
+ "types": ["vite/client"],
9
+ "noUnusedLocals": true,
10
+ "noUnusedParameters": true,
11
+ "verbatimModuleSyntax": true,
12
+ "noUncheckedSideEffectImports": true
13
+ },
14
+ "include": ["src", "test"]
15
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.app.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "declaration": true,
6
+ "declarationMap": false,
7
+ "emitDeclarationOnly": true,
8
+ "noEmit": false,
9
+ "outDir": "dist"
10
+ },
11
+ "include": ["src"]
12
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
3
+ "files": []
4
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5
+ "target": "ES2023",
6
+ "lib": ["ES2023"],
7
+ "types": ["node"],
8
+ "verbatimModuleSyntax": true
9
+ },
10
+ "include": ["vite.config.ts", "vitest.config.ts"]
11
+ }
@@ -0,0 +1,22 @@
1
+ import path from 'node:path'
2
+ import react from '@vitejs/plugin-react'
3
+ import { defineConfig } from 'vite'
4
+ import dts from 'vite-plugin-dts'
5
+
6
+ export default defineConfig({
7
+ plugins: [react(), dts({ tsconfigPath: './tsconfig.build.json', entryRoot: './src', clearPureImport: true })],
8
+ build: {
9
+ lib: {
10
+ entry: path.resolve(import.meta.dirname, 'src/index.ts'),
11
+ formats: ['es'],
12
+ fileName: 'index',
13
+ cssFileName: 'style',
14
+ },
15
+ rolldownOptions: {
16
+ // Include JSX runtimes and every peer subpath in the external boundary.
17
+ external: id => /^(?:react|react-dom)(?:\/|$)/u.test(id),
18
+ // Counter is a client component; preserve its boundary in bundled RSC consumers.
19
+ output: { banner: '\'use client\';' },
20
+ },
21
+ },
22
+ })
@@ -0,0 +1,5 @@
1
+ import { defineVitestProjectConfig } from 'repoctl/tooling'
2
+
3
+ export default await defineVitestProjectConfig({
4
+ options: { environment: 'jsdom' },
5
+ })
@@ -0,0 +1,27 @@
1
+ # React application
2
+
3
+ A small React and TypeScript SPA built with Vite. It includes an accessible counter, a Vitest interaction test, and the workspace's shared ESLint and Stylelint configuration.
4
+
5
+ From the workspace root, enable the declared package manager and install dependencies:
6
+
7
+ ```sh
8
+ corepack enable
9
+ pnpm install
10
+ ```
11
+
12
+ Run these commands from this application directory:
13
+
14
+ ```sh
15
+ pnpm dev
16
+ pnpm build
17
+ pnpm lint
18
+ pnpm typecheck
19
+ pnpm test
20
+ pnpm preview
21
+ ```
22
+
23
+ `build` creates `dist/`; `preview` serves that production output locally. `lint` checks both code and CSS. `typecheck` uses TypeScript project references for the app, tests, and Vite configuration.
24
+
25
+ To consume another workspace package, add it as a `workspace:*` dependency, import its package name, and build its public output first. Running `pnpm build` at the workspace root lets Turbo order dependency builds. Do not import another package's `src/` directory.
26
+
27
+ Router, state management, backend, and CSS framework choices are left to your application.
@@ -0,0 +1,5 @@
1
+ import { defineEslintConfig } from 'repoctl/tooling'
2
+
3
+ export default await defineEslintConfig({
4
+ options: { react: true, typescript: true },
5
+ })
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ dist
3
+ coverage
4
+ *.tsbuildinfo
5
+ .env.local
6
+ .env.*.local
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="A small React application, ready to make your own." />
7
+ <title>React workspace</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@icebreakers/react-vite-template",
3
+ "type": "module",
4
+ "version": "0.0.0",
5
+ "private": true,
6
+ "description": "Internal React, Vite and TypeScript application template shipped with repoctl",
7
+ "license": "MIT",
8
+ "engines": {
9
+ "node": ">=22.13.0"
10
+ },
11
+ "scripts": {
12
+ "dev": "vite",
13
+ "build": "vite build",
14
+ "preview": "vite preview",
15
+ "lint": "eslint . && stylelint \"src/**/*.css\"",
16
+ "lint:fix": "eslint . --fix && stylelint \"src/**/*.css\" --fix",
17
+ "typecheck": "tsc -b",
18
+ "test": "vitest run",
19
+ "test:dev": "vitest"
20
+ },
21
+ "dependencies": {
22
+ "react": "^19.3.0",
23
+ "react-dom": "^19.3.0"
24
+ },
25
+ "devDependencies": {
26
+ "@testing-library/dom": "^10.4.2",
27
+ "@testing-library/react": "^16.3.3",
28
+ "@types/react": "^19.3.0",
29
+ "@types/react-dom": "^19.3.0",
30
+ "@vitejs/plugin-react": "^6.1.1",
31
+ "jsdom": "^30.1.1",
32
+ "typescript": "~6.0.3",
33
+ "vite": "^8.3.2",
34
+ "vitest": "~5.0.3"
35
+ }
36
+ }
@@ -0,0 +1,22 @@
1
+ import { useState } from 'react'
2
+
3
+ export default function App() {
4
+ const [count, setCount] = useState(0)
5
+
6
+ return (
7
+ <main>
8
+ <p className="eyebrow">Your next idea starts here</p>
9
+ <h1>Make something useful.</h1>
10
+ <p className="intro">A small React workspace with room to grow.</p>
11
+ <section className="counter" aria-labelledby="counter-title">
12
+ <h2 id="counter-title">Try an interaction</h2>
13
+ <output aria-label="Count" aria-live="polite">{count}</output>
14
+ <div className="actions">
15
+ <button type="button" onClick={() => setCount(value => value + 1)}>Add one</button>
16
+ <button type="button" className="secondary" disabled={count === 0} onClick={() => setCount(0)}>Reset</button>
17
+ </div>
18
+ </section>
19
+ <p className="footnote">Built with React, TypeScript, and Vite.</p>
20
+ </main>
21
+ )
22
+ }
@@ -0,0 +1,15 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './app'
4
+ import './style.css'
5
+
6
+ const container = document.getElementById('root')
7
+ if (!container) {
8
+ throw new Error('The application root element is missing.')
9
+ }
10
+
11
+ createRoot(container).render(
12
+ <StrictMode>
13
+ <App />
14
+ </StrictMode>,
15
+ )
@@ -0,0 +1,95 @@
1
+ :root {
2
+ font-family: system-ui, sans-serif;
3
+ font-synthesis: none;
4
+ color: #203834;
5
+ background: #f5f4ee;
6
+ text-rendering: optimizelegibility;
7
+ -webkit-font-smoothing: antialiased;
8
+ }
9
+
10
+ * {
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ min-width: 320px;
16
+ margin: 0;
17
+ }
18
+
19
+ main {
20
+ max-width: 760px;
21
+ padding: clamp(32px, 10vw, 96px) 24px;
22
+ margin: 0 auto;
23
+ }
24
+
25
+ .eyebrow,
26
+ .footnote {
27
+ font-size: 14px;
28
+ color: #53655f;
29
+ }
30
+
31
+ h1 {
32
+ max-width: 600px;
33
+ margin: 24px 0;
34
+ font-size: clamp(40px, 8vw, 72px);
35
+ line-height: 1.05;
36
+ letter-spacing: -0.045em;
37
+ }
38
+
39
+ .intro {
40
+ font-size: 18px;
41
+ line-height: 1.6;
42
+ }
43
+
44
+ .counter {
45
+ padding: 28px;
46
+ margin: 40px 0 28px;
47
+ background: #fffefa;
48
+ border: 1px solid #c9d3ca;
49
+ border-radius: 12px;
50
+ }
51
+
52
+ h2 {
53
+ margin: 0;
54
+ font-size: 18px;
55
+ font-weight: 500;
56
+ }
57
+
58
+ output {
59
+ display: block;
60
+ margin: 16px 0;
61
+ font-size: 64px;
62
+ font-variant-numeric: tabular-nums;
63
+ }
64
+
65
+ .actions {
66
+ display: flex;
67
+ flex-wrap: wrap;
68
+ gap: 12px;
69
+ }
70
+
71
+ button {
72
+ min-height: 44px;
73
+ padding: 10px 20px;
74
+ font: inherit;
75
+ color: #fff;
76
+ cursor: pointer;
77
+ background: #245e50;
78
+ border: 1px solid #245e50;
79
+ border-radius: 6px;
80
+ }
81
+
82
+ button.secondary {
83
+ color: #245e50;
84
+ background: transparent;
85
+ }
86
+
87
+ button:focus-visible {
88
+ outline: 3px solid #ba5a20;
89
+ outline-offset: 4px;
90
+ }
91
+
92
+ button:disabled {
93
+ cursor: default;
94
+ opacity: 0.5;
95
+ }
@@ -0,0 +1,3 @@
1
+ import { defineStylelintConfig } from 'repoctl/tooling'
2
+
3
+ export default await defineStylelintConfig()
@@ -0,0 +1,22 @@
1
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react'
2
+ import { afterEach, expect, it } from 'vitest'
3
+ import App from '../src/app'
4
+
5
+ afterEach(cleanup)
6
+
7
+ it('increments the count and resets it to zero', () => {
8
+ render(<App />)
9
+ const count = screen.getByLabelText('Count')
10
+ const reset = screen.getByRole('button', { name: 'Reset' })
11
+ expect(count.textContent).toBe('0')
12
+ expect(reset.hasAttribute('disabled')).toBe(true)
13
+
14
+ fireEvent.click(screen.getByRole('button', { name: 'Add one' }))
15
+ fireEvent.click(screen.getByRole('button', { name: 'Add one' }))
16
+ expect(count.textContent).toBe('2')
17
+ expect(reset.hasAttribute('disabled')).toBe(false)
18
+
19
+ fireEvent.click(reset)
20
+ expect(count.textContent).toBe('0')
21
+ expect(reset.hasAttribute('disabled')).toBe(true)
22
+ })
@@ -0,0 +1,26 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5
+ "target": "ES2022",
6
+ "jsx": "react-jsx",
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM",
10
+ "DOM.Iterable"
11
+ ],
12
+ "types": [
13
+ "vite/client"
14
+ ],
15
+ "noFallthroughCasesInSwitch": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "verbatimModuleSyntax": true,
19
+ "erasableSyntaxOnly": true,
20
+ "noUncheckedSideEffectImports": true
21
+ },
22
+ "include": [
23
+ "src",
24
+ "test"
25
+ ]
26
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "references": [
3
+ {
4
+ "path": "./tsconfig.app.json"
5
+ },
6
+ {
7
+ "path": "./tsconfig.node.json"
8
+ }
9
+ ],
10
+ "files": []
11
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5
+ "target": "ES2023",
6
+ "lib": [
7
+ "ES2023"
8
+ ],
9
+ "types": [
10
+ "node"
11
+ ],
12
+ "verbatimModuleSyntax": true
13
+ },
14
+ "include": [
15
+ "vite.config.ts",
16
+ "vitest.config.ts"
17
+ ]
18
+ }
@@ -0,0 +1,6 @@
1
+ import react from '@vitejs/plugin-react'
2
+ import { defineConfig } from 'vite'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ })
@@ -0,0 +1,9 @@
1
+ import react from '@vitejs/plugin-react'
2
+ import { defineVitestProjectConfig } from 'repoctl/tooling'
3
+ import { mergeConfig } from 'vitest/config'
4
+
5
+ export default mergeConfig(await defineVitestProjectConfig({
6
+ options: { environment: 'jsdom' },
7
+ }), {
8
+ plugins: [react()],
9
+ })
@@ -52,7 +52,14 @@ export const routeSections = [
52
52
  items: [
53
53
  item('', 'Reference overview', '参考总览'),
54
54
  item('commands', 'Command reference', '命令参考'),
55
+ item('devcontainer', 'Dev Container preset', 'Dev Container 预设'),
55
56
  item('config', 'Configuration', '配置文件'),
57
+ item('organization-presets', 'Organization presets', '组织工程预设'),
58
+ item('dependencies', 'Dependency consistency', '依赖版本一致性'),
59
+ item('knip', 'Unused code and dependencies', '闲置代码与依赖检查'),
60
+ item('artifacts', 'Build and production artifacts', '构建与生产产物'),
61
+ item('move', 'Move or rename a workspace', '移动或重命名工作区'),
62
+ item('removal', 'Remove a workspace safely', '安全移除工作区'),
56
63
  item('execution-model', 'Execution model', '执行模型'),
57
64
  item('output', 'Output formats', '输出格式'),
58
65
  item('aliases', 'Command aliases', '命令别名'),
@@ -0,0 +1,51 @@
1
+ # Prepare build and production artifacts
2
+
3
+ `repo workspace prepare` previews one exact package. `prune` invokes locally installed Turbo 2 to produce a partial monorepo and pruned lockfile. `deploy` invokes the exact pnpm version pinned in the root `packageManager` to prepare a production directory. It prepares local files; it does not run the application, build an image or contact a deployment platform.
4
+
5
+ ```sh
6
+ # Build inputs and runtime entrypoints explicitly first.
7
+ pnpm build
8
+ repo workspace prepare @acme/service --mode prune --out ../service-build --json > ../prune-plan.json
9
+ repo workspace prepare --apply ../prune-plan.json
10
+
11
+ # Split Turbo's output into full/json Docker cache layers.
12
+ repo workspace prepare ./apps/service --mode prune --docker --out ../docker-context --json
13
+
14
+ # Production workspace dependencies; use only packages already in the local store.
15
+ repo workspace prepare @acme/service --mode deploy --entry dist/server.js --offline --out ../service-runtime --json > ../deploy-plan.json
16
+ repo workspace prepare --apply ../deploy-plan.json
17
+ ```
18
+
19
+ Preview is always read-only, including `--dry-run`; native version, capability and path-setting probes do not install tools. Save the JSON plan outside the source workspace. Applying requires a separate `--apply` command and cannot be combined with preview selections or `--dry-run`.
20
+
21
+ ## Native behavior and prerequisites
22
+
23
+ Both modes require a pnpm workspace and existing `pnpm-lock.yaml`. Select one uniquely named non-root package by exact npm name or `./directory`; globs, multiple matches and dependency selectors are rejected. The output must be outside the source workspace under an existing directory. Linked, overlapping and unrelated nonempty targets are rejected; there is no force flag.
24
+
25
+ Prune supports locally installed stable Turbo 2 and keeps native build dependency selection. `manifestCandidates` only explains manifest relationships; Turbo decides what is retained. Native task-dependency and global-file behavior follows the selected Turbo version and configuration. `--docker` retains its native `full`, `json` and lockfile layout.
26
+
27
+ Deploy supports pinned stable pnpm 10, 11 and 12 when their native command exposes the supported options. It uses `--prod`, `--ignore-scripts`, `--frozen-lockfile` and disables pnpmfile hooks. `--legacy` explicitly selects native legacy behavior, whose production layout may omit a root lockfile; only prune requires that output file. Since pnpm 12.2, native deploy supports non-injected workspace dependencies; earlier versions may require `injectWorkspacePackages` or legacy mode. Peer ambiguity and other native errors remain failures with their native diagnostic code. repoctl does not resolve or rewrite dependency graphs or lockfiles itself.
28
+
29
+ Deploy checks an existing built runtime file: explicit `--entry`, `package.main`, or a single `bin`. If native `files`/ignore rules omit it, preparation fails. Arbitrary `start` scripts are not interpreted or executed. Native dependency lifecycle scripts stay disabled, so prepare required native binaries explicitly before deployment; artifact preparation does not certify arbitrary application behavior.
30
+
31
+ ## Source and output boundaries
32
+
33
+ Native tools run against an isolated source copy, without source `node_modules`, `.git`, `.turbo` or `.repoctl` storage. Real `.env`/`.env.*`, `.npmrc`, `.netrc`, `_netrc`, Yarn authentication configuration, `.aws`, `.ssh` and `.gnupg` paths are excluded before copying. Public `.env.example` and `.env.sample` files remain. The output is checked again for these paths. This is a named-file policy, not a general secret detector; use public placeholders in example files and provide deployment credentials separately.
34
+
35
+ The source copy retains built files even if Git ignores them; native prune/package file rules still control final inclusion. Source symlinks and special files are rejected. Input and output inventories are bounded to 100,000 entries and 1 GiB. Use native tools directly when a workspace needs different copy or storage policies.
36
+
37
+ Effective pnpm store/cache/state/module/lockfile paths are inspected without dumping credential-bearing configuration. Paths that would write back into the source workspace are rejected before deploy, and module/lockfile storage must remain relative to the artifact. Staging itself must be outside the source workspace. Native global stores outside the workspace remain under pnpm's control.
38
+
39
+ Output links must resolve inside the artifact. Relative links are preserved; Windows directory junctions are recreated against the final artifact directory. When transferring a Windows artifact, use a copy/archive method that materializes junction contents. Integration tests copy the directory, remove the source and original artifact, then execute the production entry independently.
40
+
41
+ ## Review, recovery and repeated application
42
+
43
+ Plans fingerprint source files, modes, tool/version evidence, relevant path settings and options. Apply reconstructs the plan and rejects changes. Source inputs are rechecked after copying, after native execution, during publication and before completion. Concurrent source edits are preserved.
44
+
45
+ Native output is staged first. Publication creates files exclusively, records their ownership and written contents, and commits `.repoctl-artifact.json` last. Files may be visible before the receipt commits; do not consume an incomplete directory. Failures remove only unchanged operation-owned output and retain concurrent edits with their exact paths in the error. Cleanup failure after successful publication returns `cleanupPending` rather than claiming rollback.
46
+
47
+ Reapplying an unchanged plan returns `unchanged` only if its complete receipt and file inventory still match. Unknown or modified nonempty output is never overwritten. The `.repoctl/workspace-artifacts.lock` covers replay checks through native execution, publication and cleanup. After a crash, confirm no writer remains, inspect incomplete output and retained staging paths, and recover them before removing the lock.
48
+
49
+ Public APIs: `planWorkspaceArtifact(cwd, options)` and `applyWorkspaceArtifactPlan(cwd, plan, { signal, timeoutMs })`; JSON schema version is 1. Native execution defaults to a two-minute timeout, configurable up to ten minutes through the API.
50
+
51
+ References: [Turbo prune](https://turborepo.com/docs/reference/prune) and [pnpm deploy](https://pnpm.io/cli/deploy).