@emeryld/manager 0.4.4 → 0.4.6

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.
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { stdin as input } from 'node:process';
5
5
  import { askLine, promptSingleKey } from '../prompts.js';
6
6
  import { colors, logGlobal } from '../utils/log.js';
7
- import { SCRIPT_DESCRIPTIONS, workspaceRoot, } from './shared.js';
7
+ import { SCRIPT_DESCRIPTIONS, workspaceRoot, ensureWorkspaceToolingFiles, } from './shared.js';
8
8
  import { clientVariant } from './variants/client.js';
9
9
  import { contractVariant } from './variants/contract.js';
10
10
  import { dockerVariant } from './variants/docker.js';
@@ -201,7 +201,7 @@ function formatVariantLines(variants, selected) {
201
201
  lines.push(`${pointer}${numberLabel}. ${label} ${meta}`);
202
202
  });
203
203
  lines.push('');
204
- lines.push(colors.dim('Use ↑/↓ (or j/k) to move, digits (1-9,0 for 10) to pick, Enter to confirm, Esc/Ctrl+C to exit.'));
204
+ lines.push(colors.dim('Use ↑/↓ (or j/k) to move, digits (1-9,0 for 10) to run instantly, Enter to confirm, Esc/Ctrl+C to exit.'));
205
205
  return lines;
206
206
  }
207
207
  function renderInteractiveList(lines, previousLineCount) {
@@ -281,7 +281,7 @@ async function promptForVariant() {
281
281
  render();
282
282
  return;
283
283
  }
284
- if (isEnter) {
284
+ if (buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a)) {
285
285
  commitSelection(VARIANTS[selectedIndex]);
286
286
  return;
287
287
  }
@@ -307,8 +307,8 @@ async function promptForTargetDir(fallback) {
307
307
  return path.resolve(workspaceRoot, normalized);
308
308
  }
309
309
  /**
310
- * Build solution #1: after install, build the workspace graph (or at least deps)
311
- * Keeps a safe fallback to the old "build just the new package" behavior.
310
+ * Solution #1: always build the workspace dependency graph after install.
311
+ * This prevents workspace-unbuilt dist/types issues for workspace:* deps.
312
312
  */
313
313
  async function postCreateTasks(targetDir, options) {
314
314
  if (options?.skipInstall) {
@@ -327,8 +327,7 @@ async function postCreateTasks(targetDir, options) {
327
327
  logGlobal('Skipping build (flag).', colors.dim);
328
328
  return;
329
329
  }
330
- // Prefer building the dependency graph rooted at the new package (fast),
331
- // fall back to building the whole workspace, then fall back to old behavior.
330
+ // Prefer: build deps of the new package (pkg + its workspace deps)
332
331
  try {
333
332
  if (options?.pkgName) {
334
333
  logGlobal(`Building workspace deps for ${options.pkgName}…`, colors.cyan);
@@ -339,6 +338,7 @@ async function postCreateTasks(targetDir, options) {
339
338
  catch (error) {
340
339
  logGlobal(`Filtered workspace build failed; will try full workspace build: ${error instanceof Error ? error.message : String(error)}`, colors.yellow);
341
340
  }
341
+ // Fallback: build everything
342
342
  try {
343
343
  logGlobal('Building full workspace…', colors.cyan);
344
344
  await runCommand('pnpm', ['-r', 'build'], workspaceRoot);
@@ -347,7 +347,7 @@ async function postCreateTasks(targetDir, options) {
347
347
  catch (error) {
348
348
  logGlobal(`Full workspace build failed; falling back to building only the new package: ${error instanceof Error ? error.message : String(error)}`, colors.yellow);
349
349
  }
350
- // Old behavior fallback: build only the new package if it has a build script
350
+ // Final fallback (old behavior): build only the new package if it has a build script
351
351
  try {
352
352
  const pkgJsonPath = path.join(targetDir, 'package.json');
353
353
  const pkgRaw = await readFile(pkgJsonPath, 'utf8');
@@ -383,6 +383,8 @@ async function gatherTarget(initial = {}) {
383
383
  export async function createRrrPackage(options = {}) {
384
384
  const target = await gatherTarget(options);
385
385
  logGlobal(`Creating ${target.variant.label} in ${path.relative(workspaceRoot, target.targetDir) || '.'}`, colors.green);
386
+ const toolingRoot = target.variant.id === 'rrr-fullstack' ? target.targetDir : workspaceRoot;
387
+ await ensureWorkspaceToolingFiles(toolingRoot);
386
388
  await target.variant.scaffold({
387
389
  targetDir: target.targetDir,
388
390
  pkgName: target.pkgName,
@@ -1,7 +1,11 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { access, mkdir, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { promptYesNoAll } from '../prompts.js';
4
5
  export const workspaceRoot = process.cwd();
6
+ export function isWorkspaceRoot(dir) {
7
+ return path.resolve(dir) === path.resolve(workspaceRoot);
8
+ }
5
9
  function pathExists(target) {
6
10
  return access(target)
7
11
  .then(() => true)
@@ -31,26 +35,45 @@ export async function writeFileIfMissing(baseDir, relative, contents) {
31
35
  console.log(` created ${rel}`);
32
36
  return 'created';
33
37
  }
38
+ export async function writeFileWithPrompt(baseDir, relative, contents) {
39
+ const fullPath = path.join(baseDir, relative);
40
+ await mkdir(path.dirname(fullPath), { recursive: true });
41
+ const rel = path.relative(workspaceRoot, fullPath);
42
+ const exists = await pathExists(fullPath);
43
+ if (exists) {
44
+ const answer = await promptYesNoAll(`Overwrite existing ${rel}?`);
45
+ if (answer !== 'yes') {
46
+ console.log(` kept existing ${rel}`);
47
+ return 'skipped';
48
+ }
49
+ }
50
+ await writeFile(fullPath, contents, 'utf8');
51
+ console.log(` ${exists ? 'updated' : 'created'} ${rel}`);
52
+ return exists ? 'updated' : 'created';
53
+ }
34
54
  export function baseTsConfig(options) {
35
55
  const config = {
36
56
  ...(options?.extends ? { extends: options.extends } : {}),
37
57
  compilerOptions: {
38
58
  target: 'ES2020',
39
- module: 'NodeNext',
40
- moduleResolution: 'NodeNext',
41
- outDir: options?.outDir ?? 'dist',
42
- rootDir: options?.rootDir ?? 'src',
43
- declaration: true,
44
- sourceMap: true,
45
- strict: true,
46
- esModuleInterop: true,
47
- skipLibCheck: true,
59
+ module: 'ESNext',
60
+ moduleResolution: 'Bundler',
61
+ ...(options?.outDir ? { outDir: options.outDir } : {}),
62
+ ...(options?.rootDir ? { rootDir: options.rootDir } : {}),
63
+ declaration: options?.declaration ?? true,
64
+ sourceMap: options?.sourceMap ?? true,
65
+ strict: options?.strict ?? true,
66
+ esModuleInterop: options?.esModuleInterop ?? true,
67
+ skipLibCheck: options?.skipLibCheck ?? true,
68
+ resolveJsonModule: options?.resolveJsonModule ?? true,
69
+ forceConsistentCasingInFileNames: options?.forceConsistentCasingInFileNames ?? true,
70
+ baseUrl: options?.baseUrl ?? '.',
48
71
  lib: options?.lib,
49
72
  types: options?.types,
50
73
  jsx: options?.jsx,
51
74
  },
52
- include: options?.include ?? ['src/**/*'],
53
- exclude: options?.exclude ?? ['dist', 'node_modules'],
75
+ include: options?.include,
76
+ exclude: options?.exclude,
54
77
  };
55
78
  return `${JSON.stringify(config, null, 2)}\n`;
56
79
  }
@@ -145,7 +168,8 @@ const DEFAULT_GITIGNORE_ENTRIES = [
145
168
  export function gitignoreFrom(entries = DEFAULT_GITIGNORE_ENTRIES) {
146
169
  return entries.join('\n');
147
170
  }
148
- export function baseScripts(devCommand, extras) {
171
+ export function baseScripts(devCommand, extras, options) {
172
+ const includePrepare = options?.includePrepare ?? true;
149
173
  return {
150
174
  dev: devCommand,
151
175
  build: 'tsc -p tsconfig.json',
@@ -157,18 +181,36 @@ export function baseScripts(devCommand, extras) {
157
181
  'format:check': 'prettier . --check',
158
182
  clean: 'rimraf dist .turbo coverage',
159
183
  test: "node -e \"console.log('No tests yet')\"",
160
- prepare: 'husky install || true',
184
+ ...(includePrepare
185
+ ? { prepare: 'git rev-parse --is-inside-work-tree >/dev/null 2>&1 && husky || true' }
186
+ : {}),
161
187
  ...extras,
162
188
  };
163
189
  }
164
190
  export function basePackageFiles(options) {
165
191
  return {
166
- 'eslint.config.js': baseEslintConfig(options?.tsconfigPath),
192
+ '.gitignore': gitignoreFrom(options?.gitignoreEntries),
193
+ };
194
+ }
195
+ export async function ensureWorkspaceToolingFiles(baseDir, options) {
196
+ const defaultTsconfig = path.join(baseDir, 'tsconfig.json');
197
+ const resolvedRootConfig = options?.tsconfigPath
198
+ ? path.resolve(baseDir, options.tsconfigPath)
199
+ : await resolveRootTsconfig(baseDir);
200
+ const tsconfigPath = toPosixPath(path.relative(baseDir, resolvedRootConfig ?? defaultTsconfig) || './tsconfig.json');
201
+ const files = workspaceToolingFiles(tsconfigPath);
202
+ for (const [relative, contents] of Object.entries(files)) {
203
+ // eslint-disable-next-line no-await-in-loop
204
+ await writeFileWithPrompt(baseDir, relative, contents);
205
+ }
206
+ }
207
+ export function workspaceToolingFiles(tsconfigPath) {
208
+ return {
209
+ 'eslint.config.js': baseEslintConfig(tsconfigPath),
167
210
  'prettier.config.js': basePrettierConfig(),
168
211
  '.prettierignore': `${PRETTIER_IGNORE}\n`,
169
212
  '.vscode/settings.json': vscodeSettings(),
170
213
  '.husky/pre-commit': HUSKY_PRE_COMMIT,
171
- '.gitignore': gitignoreFrom(options?.gitignoreEntries),
172
214
  };
173
215
  }
174
216
  export const SCRIPT_DESCRIPTIONS = {
@@ -261,27 +303,55 @@ function readRootPackageManager() {
261
303
  return undefined;
262
304
  }
263
305
  }
264
- async function resolveRootTsconfig() {
306
+ async function resolveRootTsconfig(baseDir = workspaceRoot) {
265
307
  const candidates = ['tsconfig.base.json', 'tsconfig.json'];
266
308
  for (const candidate of candidates) {
267
- const fullPath = path.join(workspaceRoot, candidate);
309
+ const fullPath = path.join(baseDir, candidate);
268
310
  if (await pathExists(fullPath)) {
269
311
  return fullPath;
270
312
  }
271
313
  }
272
314
  return undefined;
273
315
  }
316
+ async function findNearestTsconfig(startDir) {
317
+ let current = path.resolve(startDir);
318
+ // eslint-disable-next-line no-constant-condition
319
+ while (true) {
320
+ const found = await resolveRootTsconfig(current);
321
+ if (found)
322
+ return found;
323
+ const parent = path.dirname(current);
324
+ if (parent === current)
325
+ return undefined;
326
+ current = parent;
327
+ }
328
+ }
274
329
  export async function packageTsConfig(targetDir, options) {
275
330
  const extendsFromRoot = options?.extendsFromRoot ?? true;
276
331
  let extendsPath;
277
332
  if (extendsFromRoot) {
278
- const rootConfig = await resolveRootTsconfig();
333
+ const rootConfig = await findNearestTsconfig(targetDir);
279
334
  if (rootConfig) {
280
335
  const relative = path.relative(targetDir, rootConfig);
281
- if (relative !== '') {
282
- extendsPath = toPosixPath(relative || './tsconfig.json');
283
- }
336
+ const normalized = toPosixPath(relative || './tsconfig.base.json');
337
+ extendsPath = normalized.startsWith('.') ? normalized : `./${normalized}`;
284
338
  }
285
339
  }
286
- return baseTsConfig({ ...options, extends: extendsPath });
340
+ const compilerOptions = stripUndefined({
341
+ rootDir: options?.rootDir ?? 'src',
342
+ outDir: options?.outDir ?? 'dist',
343
+ tsBuildInfoFile: 'dist/.tsbuildinfo',
344
+ jsx: options?.jsx,
345
+ types: options?.types,
346
+ lib: options?.lib,
347
+ esModuleInterop: options?.esModuleInterop ?? true,
348
+ allowSyntheticDefaultImports: options?.esModuleInterop ?? true,
349
+ skipLibCheck: options?.skipLibCheck ?? true,
350
+ });
351
+ const config = stripUndefined({
352
+ extends: extendsPath,
353
+ compilerOptions,
354
+ include: options?.include ?? ['src/**/*.ts'],
355
+ });
356
+ return `${JSON.stringify(config, null, 2)}\n`;
287
357
  }
@@ -1,4 +1,4 @@
1
- import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, isWorkspaceRoot, writeFileIfMissing, } from '../shared.js';
2
2
  const CLIENT_SCRIPTS = [
3
3
  'dev',
4
4
  'build',
@@ -29,10 +29,12 @@ export const healthGet = routeClient.build(registry.byKey['GET /api/health'])
29
29
  export const healthPost = routeClient.build(registry.byKey['POST /api/health'])
30
30
  `;
31
31
  }
32
- export function clientPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLDER) {
32
+ export function clientPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLDER, options) {
33
33
  return basePackageJson({
34
34
  name,
35
- scripts: baseScripts('tsx watch src/index.ts'),
35
+ scripts: baseScripts('tsx watch src/index.ts', undefined, {
36
+ includePrepare: options?.includePrepare,
37
+ }),
36
38
  dependencies: {
37
39
  [contractName]: 'workspace:*',
38
40
  '@emeryld/rrroutes-client': '^2.5.3',
@@ -46,12 +48,15 @@ export function clientPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLD
46
48
  });
47
49
  }
48
50
  async function clientFiles(pkgName, contractImport, targetDir) {
51
+ const includePrepare = isWorkspaceRoot(targetDir);
49
52
  const tsconfig = await packageTsConfig(targetDir, {
53
+ include: ['src/**/*.ts', 'src/**/*.tsx'],
50
54
  lib: ['ES2020', 'DOM'],
51
- types: ['node'],
55
+ types: ['react', 'react-native'],
56
+ jsx: 'react-jsx',
52
57
  });
53
58
  return {
54
- 'package.json': clientPackageJson(pkgName, contractImport),
59
+ 'package.json': clientPackageJson(pkgName, contractImport, { includePrepare }),
55
60
  'tsconfig.json': tsconfig,
56
61
  ...basePackageFiles(),
57
62
  'src/index.ts': clientIndexTs(contractImport),
@@ -1,4 +1,4 @@
1
- import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, isWorkspaceRoot, writeFileIfMissing, } from '../shared.js';
2
2
  const CONTRACT_SCRIPTS = [
3
3
  'dev',
4
4
  'build',
@@ -10,9 +10,22 @@ const CONTRACT_SCRIPTS = [
10
10
  'clean',
11
11
  'test',
12
12
  ];
13
- export const CONTRACT_TS = `import { defineSocketEvents, finalize, resource } from '@emeryld/rrroutes-contract'
13
+ /**
14
+ * IMPORTANT:
15
+ * @emeryld/rrroutes-contract appears to ship types that may not expose these as named exports
16
+ * in some module shapes (CJS export= or default export). To make scaffolds compile reliably,
17
+ * we import as a namespace and then grab from either `default` or the namespace object.
18
+ */
19
+ export const CONTRACT_TS = `import * as rrroutesContract from '@emeryld/rrroutes-contract'
14
20
  import { z } from 'zod'
15
21
 
22
+ const api = (rrroutesContract as any).default ?? rrroutesContract
23
+ const { defineSocketEvents, finalize, resource } = api as {
24
+ defineSocketEvents: (...args: any[]) => any
25
+ finalize: (...args: any[]) => any
26
+ resource: (...args: any[]) => any
27
+ }
28
+
16
29
  const routes = resource('/api')
17
30
  .sub(
18
31
  resource('health')
@@ -80,7 +93,7 @@ export const socketConfig = sockets.config
80
93
  export const socketEvents = sockets.events
81
94
  export type AppRegistry = typeof registry
82
95
  `;
83
- function contractPackageJson(name) {
96
+ function contractPackageJson(name, options) {
84
97
  return basePackageJson({
85
98
  name,
86
99
  private: false,
@@ -90,8 +103,10 @@ function contractPackageJson(name) {
90
103
  import: './dist/index.js',
91
104
  },
92
105
  },
93
- // ✅ Dev now *builds/updates dist + .d.ts continuously*
94
- scripts: baseScripts('tsc -p tsconfig.json --watch --preserveWatchOutput'),
106
+ // ✅ Solution #2: dev continuously emits dist/*.js + dist/*.d.ts
107
+ scripts: baseScripts('tsc -p tsconfig.json --watch --preserveWatchOutput', undefined, { includePrepare: options?.includePrepare }),
108
+ // You can keep this pinned if you want, but the import strategy above prevents breakage
109
+ // across different module export shapes.
95
110
  dependencies: {
96
111
  '@emeryld/rrroutes-contract': '^2.5.2',
97
112
  zod: '^4.2.1',
@@ -102,9 +117,13 @@ function contractPackageJson(name) {
102
117
  });
103
118
  }
104
119
  async function contractFiles(pkgName, targetDir) {
105
- const tsconfig = await packageTsConfig(targetDir);
120
+ const includePrepare = isWorkspaceRoot(targetDir);
121
+ const tsconfig = await packageTsConfig(targetDir, {
122
+ include: ['src/**/*.ts', 'src/**/*.tsx'],
123
+ skipLibCheck: true,
124
+ });
106
125
  return {
107
- 'package.json': contractPackageJson(pkgName),
126
+ 'package.json': contractPackageJson(pkgName, { includePrepare }),
108
127
  'tsconfig.json': tsconfig,
109
128
  ...basePackageFiles(),
110
129
  'src/index.ts': CONTRACT_TS,
@@ -1,4 +1,4 @@
1
- import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, isWorkspaceRoot, writeFileIfMissing, } from '../shared.js';
2
2
  const DOCKER_SCRIPTS = [
3
3
  'dev',
4
4
  'build',
@@ -18,7 +18,7 @@ const DOCKER_SCRIPTS = [
18
18
  'docker:clean',
19
19
  'docker:reset',
20
20
  ];
21
- function dockerPackageJson(name) {
21
+ function dockerPackageJson(name, options) {
22
22
  return basePackageJson({
23
23
  name,
24
24
  scripts: baseScripts('tsx watch src/index.ts', {
@@ -31,7 +31,7 @@ function dockerPackageJson(name) {
31
31
  'docker:stop': 'npm run docker:cli -- stop',
32
32
  'docker:clean': 'npm run docker:cli -- clean',
33
33
  'docker:reset': 'npm run docker:cli -- reset',
34
- }),
34
+ }, { includePrepare: options?.includePrepare }),
35
35
  dependencies: {
36
36
  cors: '^2.8.5',
37
37
  express: '^5.1.0',
@@ -96,9 +96,13 @@ CMD ["node", "dist/index.js"]
96
96
  `;
97
97
  }
98
98
  async function dockerFiles(pkgName, targetDir) {
99
- const tsconfig = await packageTsConfig(targetDir, { types: ['node'] });
99
+ const includePrepare = isWorkspaceRoot(targetDir);
100
+ const tsconfig = await packageTsConfig(targetDir, {
101
+ include: ['src/**/*.ts'],
102
+ types: ['node'],
103
+ });
100
104
  return {
101
- 'package.json': dockerPackageJson(pkgName),
105
+ 'package.json': dockerPackageJson(pkgName, { includePrepare }),
102
106
  'tsconfig.json': tsconfig,
103
107
  'src/index.ts': dockerIndexTs(),
104
108
  'scripts/docker.ts': dockerCliScript(pkgName),
@@ -1,4 +1,4 @@
1
- import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, isWorkspaceRoot, writeFileIfMissing, } from '../shared.js';
2
2
  const EMPTY_SCRIPTS = [
3
3
  'dev',
4
4
  'build',
@@ -10,19 +10,26 @@ const EMPTY_SCRIPTS = [
10
10
  'clean',
11
11
  'test',
12
12
  ];
13
- function emptyPackageJson(name) {
13
+ function emptyPackageJson(name, options) {
14
14
  return basePackageJson({
15
15
  name,
16
- scripts: baseScripts('tsx watch src/index.ts'),
16
+ scripts: baseScripts('tsx watch src/index.ts', undefined, {
17
+ includePrepare: options?.includePrepare,
18
+ }),
17
19
  devDependencies: {
18
20
  ...BASE_LINT_DEV_DEPENDENCIES,
19
21
  },
20
22
  });
21
23
  }
22
24
  async function emptyFiles(pkgName, targetDir) {
23
- const tsconfig = await packageTsConfig(targetDir, { types: ['node'] });
25
+ const includePrepare = isWorkspaceRoot(targetDir);
26
+ const tsconfig = await packageTsConfig(targetDir, {
27
+ include: ['src/**/*.ts', 'src/**/*.tsx'],
28
+ types: ['node'],
29
+ skipLibCheck: true,
30
+ });
24
31
  return {
25
- 'package.json': emptyPackageJson(pkgName),
32
+ 'package.json': emptyPackageJson(pkgName, { includePrepare }),
26
33
  'tsconfig.json': tsconfig,
27
34
  ...basePackageFiles(),
28
35
  'src/index.ts': "export const hello = 'world'\n",
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, baseTsConfig, writeFileIfMissing, } from '../shared.js';
2
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, writeFileIfMissing, } from '../shared.js';
3
3
  import { clientVariant } from './client.js';
4
4
  import { serverVariant } from './server.js';
5
5
  import { dockerVariant } from './docker.js';
@@ -43,6 +43,9 @@ function deriveDirs(rootDir, baseName) {
43
43
  docker: path.join(packagesRoot, `${baseName}-docker`),
44
44
  };
45
45
  }
46
+ function toPosixPath(value) {
47
+ return value.split(path.sep).join('/');
48
+ }
46
49
  function rootPackageJson(baseName) {
47
50
  const dockerPackageDir = `packages/${baseName}-docker`;
48
51
  return basePackageJson({
@@ -50,7 +53,7 @@ function rootPackageJson(baseName) {
50
53
  private: true,
51
54
  useDefaults: false,
52
55
  scripts: {
53
- setup: 'pnpm install && pnpm exec husky install',
56
+ setup: 'pnpm install && (git rev-parse --is-inside-work-tree >/dev/null 2>&1 && pnpm exec husky || true)',
54
57
  dev: 'pnpm -r dev --parallel --if-present',
55
58
  build: 'pnpm -r build',
56
59
  typecheck: 'pnpm -r typecheck',
@@ -61,7 +64,7 @@ function rootPackageJson(baseName) {
61
64
  'format:check': 'pnpm -r format:check --if-present',
62
65
  test: 'pnpm -r test --if-present',
63
66
  clean: 'pnpm -r clean --if-present && rimraf node_modules .turbo coverage',
64
- prepare: 'husky install || true',
67
+ prepare: 'git rev-parse --is-inside-work-tree >/dev/null 2>&1 && husky || true',
65
68
  'docker:up': `pnpm -C ${dockerPackageDir} run docker:up`,
66
69
  'docker:dev': `pnpm -C ${dockerPackageDir} run docker:dev`,
67
70
  'docker:logs': `pnpm -C ${dockerPackageDir} run docker:logs`,
@@ -77,6 +80,50 @@ function rootPackageJson(baseName) {
77
80
  function rootPnpmWorkspace() {
78
81
  return "packages:\n - 'packages/*'\n";
79
82
  }
83
+ function rootTsconfigBase(baseName, names) {
84
+ const paths = {
85
+ [names.contract]: [`packages/${baseName}-contract/src`],
86
+ [`${names.contract}/*`]: [`packages/${baseName}-contract/src/*`],
87
+ [names.server]: [`packages/${baseName}-server/src`],
88
+ [`${names.server}/*`]: [`packages/${baseName}-server/src/*`],
89
+ [names.client]: [`packages/${baseName}-client/src`],
90
+ [`${names.client}/*`]: [`packages/${baseName}-client/src/*`],
91
+ [names.docker]: [`packages/${baseName}-docker/src`],
92
+ [`${names.docker}/*`]: [`packages/${baseName}-docker/src/*`],
93
+ };
94
+ const config = {
95
+ $schema: 'https://json.schemastore.org/tsconfig',
96
+ compilerOptions: {
97
+ target: 'ES2020',
98
+ module: 'ESNext',
99
+ moduleResolution: 'Bundler',
100
+ jsx: 'react-jsx',
101
+ strict: true,
102
+ skipLibCheck: true,
103
+ resolveJsonModule: true,
104
+ forceConsistentCasingInFileNames: true,
105
+ sourceMap: true,
106
+ baseUrl: '.',
107
+ paths,
108
+ },
109
+ };
110
+ return `${JSON.stringify(config, null, 2)}\n`;
111
+ }
112
+ function rootSolutionTsconfig(dirs) {
113
+ const references = [
114
+ dirs.contract,
115
+ dirs.server,
116
+ dirs.client,
117
+ dirs.docker,
118
+ ].map((pkgDir) => ({ path: toPosixPath(path.relative(dirs.root, pkgDir)) }));
119
+ const config = {
120
+ $schema: 'https://json.schemastore.org/tsconfig',
121
+ extends: './tsconfig.base.json',
122
+ files: [],
123
+ references,
124
+ };
125
+ return `${JSON.stringify(config, null, 2)}\n`;
126
+ }
80
127
  function stackComposeYaml() {
81
128
  return `services:
82
129
  db:
@@ -95,16 +142,13 @@ volumes:
95
142
  db-data:
96
143
  `;
97
144
  }
98
- async function scaffoldRootFiles(baseDir, baseName) {
145
+ async function scaffoldRootFiles(baseDir, baseName, names, dirs) {
99
146
  const files = {
100
147
  'package.json': rootPackageJson(baseName),
101
148
  'pnpm-workspace.yaml': rootPnpmWorkspace(),
102
149
  'docker-compose.yml': stackComposeYaml(),
103
- 'tsconfig.json': baseTsConfig({
104
- rootDir: '.',
105
- outDir: 'dist',
106
- include: ['packages/**/*', 'scripts/**/*', 'src/**/*'],
107
- }),
150
+ 'tsconfig.base.json': rootTsconfigBase(baseName, names),
151
+ 'tsconfig.json': rootSolutionTsconfig(dirs),
108
152
  ...basePackageFiles(),
109
153
  };
110
154
  for (const [relative, contents] of Object.entries(files)) {
@@ -136,7 +180,7 @@ export const fullstackVariant = {
136
180
  const names = deriveNames(baseName);
137
181
  const dirs = deriveDirs(ctx.targetDir, baseName);
138
182
  // Root workspace files
139
- await scaffoldRootFiles(dirs.root, baseName);
183
+ await scaffoldRootFiles(dirs.root, baseName, names, dirs);
140
184
  // Contract package (reuse contract variant)
141
185
  await contractVariant.scaffold({
142
186
  targetDir: dirs.contract,
@@ -1,4 +1,4 @@
1
- import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
1
+ import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, isWorkspaceRoot, writeFileIfMissing, } from '../shared.js';
2
2
  const SERVER_SCRIPTS = [
3
3
  'dev',
4
4
  'build',
@@ -58,13 +58,13 @@ server.listen(PORT, () => {
58
58
  })
59
59
  `;
60
60
  }
61
- export function serverPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLDER) {
61
+ export function serverPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLDER, options) {
62
62
  return basePackageJson({
63
63
  name,
64
64
  private: false,
65
65
  scripts: baseScripts('tsx watch --env-file .env src/index.ts', {
66
66
  start: 'node dist/index.js',
67
- }),
67
+ }, { includePrepare: options?.includePrepare }),
68
68
  dependencies: {
69
69
  [contractName]: 'workspace:*',
70
70
  '@emeryld/rrroutes-server': '^2.4.1',
@@ -82,9 +82,13 @@ export function serverPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLD
82
82
  });
83
83
  }
84
84
  async function serverFiles(pkgName, contractImport, targetDir) {
85
- const tsconfig = await packageTsConfig(targetDir, { types: ['node'] });
85
+ const includePrepare = isWorkspaceRoot(targetDir);
86
+ const tsconfig = await packageTsConfig(targetDir, {
87
+ include: ['src/**/*.ts'],
88
+ types: ['node'],
89
+ });
86
90
  return {
87
- 'package.json': serverPackageJson(pkgName, contractImport),
91
+ 'package.json': serverPackageJson(pkgName, contractImport, { includePrepare }),
88
92
  'tsconfig.json': tsconfig,
89
93
  ...basePackageFiles(),
90
94
  'src/index.ts': serverIndexTs(contractImport),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,8 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.17.0",
32
32
  "@types/semver": "^7.7.1",
33
- "cross-env": "^7.0.3",
34
- "@emeryld/manager": "^0.4.1"
33
+ "cross-env": "^7.0.3"
35
34
  },
36
35
  "ts-node": {
37
36
  "esm": true,