@fgv/repo-template 5.1.0-2
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/.rush/temp/45a8d0dcbb9c2e59fa02645657661dffbd25461f.tar.log +57 -0
- package/.rush/temp/92e27a75687fa5062b71fdb897f0928a8aa4e0c9.tar.log +57 -0
- package/.rush/temp/chunked-rush-logs/repo-template.build.chunks.jsonl +7 -0
- package/.rush/temp/operation/build/all.log +7 -0
- package/.rush/temp/operation/build/log-chunks.jsonl +7 -0
- package/.rush/temp/operation/build/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +576 -0
- package/README.md +216 -0
- package/bin/repo-template.js +18 -0
- package/config/rig.json +4 -0
- package/lib/cli.d.ts +14 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +126 -0
- package/lib/cli.js.map +1 -0
- package/lib/commands/create.d.ts +17 -0
- package/lib/commands/create.d.ts.map +1 -0
- package/lib/commands/create.js +212 -0
- package/lib/commands/create.js.map +1 -0
- package/lib/commands/init-library.d.ts +25 -0
- package/lib/commands/init-library.d.ts.map +1 -0
- package/lib/commands/init-library.js +217 -0
- package/lib/commands/init-library.js.map +1 -0
- package/lib/commands/patch.d.ts +15 -0
- package/lib/commands/patch.d.ts.map +1 -0
- package/lib/commands/patch.js +104 -0
- package/lib/commands/patch.js.map +1 -0
- package/lib/commands/sync.d.ts +11 -0
- package/lib/commands/sync.d.ts.map +1 -0
- package/lib/commands/sync.js +156 -0
- package/lib/commands/sync.js.map +1 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +29 -0
- package/lib/index.js.map +1 -0
- package/lib/packlets/fs/index.d.ts +40 -0
- package/lib/packlets/fs/index.d.ts.map +1 -0
- package/lib/packlets/fs/index.js +142 -0
- package/lib/packlets/fs/index.js.map +1 -0
- package/lib/packlets/jsonc/index.d.ts +27 -0
- package/lib/packlets/jsonc/index.d.ts.map +1 -0
- package/lib/packlets/jsonc/index.js +124 -0
- package/lib/packlets/jsonc/index.js.map +1 -0
- package/lib/packlets/manifest/index.d.ts +15 -0
- package/lib/packlets/manifest/index.d.ts.map +1 -0
- package/lib/packlets/manifest/index.js +61 -0
- package/lib/packlets/manifest/index.js.map +1 -0
- package/lib/packlets/manifest/types.d.ts +33 -0
- package/lib/packlets/manifest/types.d.ts.map +1 -0
- package/lib/packlets/manifest/types.js +6 -0
- package/lib/packlets/manifest/types.js.map +1 -0
- package/lib/packlets/template/index.d.ts +22 -0
- package/lib/packlets/template/index.d.ts.map +1 -0
- package/lib/packlets/template/index.js +75 -0
- package/lib/packlets/template/index.js.map +1 -0
- package/package.json +32 -0
- package/rush-logs/repo-template.build.cache.log +4 -0
- package/rush-logs/repo-template.build.log +7 -0
- package/src/cli.ts +141 -0
- package/src/commands/create.ts +216 -0
- package/src/commands/init-library.ts +249 -0
- package/src/commands/patch.ts +84 -0
- package/src/commands/sync.ts +137 -0
- package/src/index.ts +14 -0
- package/src/packlets/fs/index.ts +114 -0
- package/src/packlets/jsonc/index.ts +134 -0
- package/src/packlets/manifest/index.ts +29 -0
- package/src/packlets/manifest/types.ts +36 -0
- package/src/packlets/template/index.ts +48 -0
- package/sync-manifest.json +222 -0
- package/temp/build/typescript/ts_l9Fw4VUO.json +1 -0
- package/templates/.gitignore.tmpl +85 -0
- package/templates/ACTIVE_DEVELOPMENT.md.tmpl +58 -0
- package/templates/CLAUDE.md.tmpl +124 -0
- package/templates/command-line.json.tmpl +50 -0
- package/templates/package.json.tmpl +5 -0
- package/templates/version-policies.json.tmpl +8 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init-library command — scaffolds a new library package within an existing Rush monorepo.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { patchFile, IPatchOperation } from '../packlets/jsonc';
|
|
8
|
+
|
|
9
|
+
export type RigType = 'dual' | 'node' | 'browser';
|
|
10
|
+
export type CategoryType = 'libraries' | 'tools' | 'apps' | 'services';
|
|
11
|
+
|
|
12
|
+
export interface IInitLibraryOptions {
|
|
13
|
+
/** Package name (e.g. "ts-my-lib" — will be prefixed with @fgv/) */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Short description */
|
|
16
|
+
description: string;
|
|
17
|
+
/** Heft rig to use */
|
|
18
|
+
rig: RigType;
|
|
19
|
+
/** Category folder */
|
|
20
|
+
category: CategoryType;
|
|
21
|
+
/** Rush monorepo root */
|
|
22
|
+
repoDir: string;
|
|
23
|
+
/** Version policy name (from version-policies.json) */
|
|
24
|
+
versionPolicy: string;
|
|
25
|
+
/** Initial version */
|
|
26
|
+
version: string;
|
|
27
|
+
/** Dependency version for @fgv/* packages ("workspace:*" for fgv, "^5.1.0-0" for consumers) */
|
|
28
|
+
fgvDepVersion: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface IRigConfig {
|
|
32
|
+
rigPackageName: string;
|
|
33
|
+
rigProfile?: string;
|
|
34
|
+
rigDevDeps: Record<string, string>;
|
|
35
|
+
tsconfigExtends: string;
|
|
36
|
+
tsconfigTypes: string[];
|
|
37
|
+
tsconfigLib?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const RIG_CONFIGS: Record<RigType, IRigConfig> = {
|
|
41
|
+
dual: {
|
|
42
|
+
rigPackageName: '@fgv/heft-dual-rig',
|
|
43
|
+
rigDevDeps: {
|
|
44
|
+
'@fgv/heft-dual-rig': 'FGV_DEP',
|
|
45
|
+
'@rushstack/heft': '1.2.7',
|
|
46
|
+
'@rushstack/heft-node-rig': '2.11.27'
|
|
47
|
+
},
|
|
48
|
+
tsconfigExtends: './node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json',
|
|
49
|
+
tsconfigTypes: ['heft-jest', 'node'],
|
|
50
|
+
tsconfigLib: ['es2018']
|
|
51
|
+
},
|
|
52
|
+
node: {
|
|
53
|
+
rigPackageName: '@rushstack/heft-node-rig',
|
|
54
|
+
rigDevDeps: {
|
|
55
|
+
'@rushstack/heft': '1.2.7',
|
|
56
|
+
'@rushstack/heft-node-rig': '2.11.27'
|
|
57
|
+
},
|
|
58
|
+
tsconfigExtends: './node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json',
|
|
59
|
+
tsconfigTypes: ['heft-jest', 'node']
|
|
60
|
+
},
|
|
61
|
+
browser: {
|
|
62
|
+
rigPackageName: '@rushstack/heft-web-rig',
|
|
63
|
+
rigProfile: 'library',
|
|
64
|
+
rigDevDeps: {
|
|
65
|
+
'@rushstack/heft': '1.2.7',
|
|
66
|
+
'@rushstack/heft-web-rig': '1.4.3'
|
|
67
|
+
},
|
|
68
|
+
tsconfigExtends: './node_modules/@rushstack/heft-web-rig/profiles/library/tsconfig-base.json',
|
|
69
|
+
tsconfigTypes: ['heft-jest', 'node'],
|
|
70
|
+
tsconfigLib: ['es2018', 'DOM']
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export async function runInitLibrary(options: IInitLibraryOptions): Promise<void> {
|
|
75
|
+
const { name, description, rig, category, repoDir, versionPolicy, version, fgvDepVersion } = options;
|
|
76
|
+
|
|
77
|
+
const packageName = name.startsWith('@fgv/') ? name : `@fgv/${name}`;
|
|
78
|
+
const shortName = packageName.replace('@fgv/', '');
|
|
79
|
+
const projectFolder = `${category}/${shortName}`;
|
|
80
|
+
const projectDir = path.join(repoDir, projectFolder);
|
|
81
|
+
|
|
82
|
+
if (fs.existsSync(projectDir)) {
|
|
83
|
+
throw new Error(`Project directory already exists: ${projectDir}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const rushJsonPath = path.join(repoDir, 'rush.json');
|
|
87
|
+
if (!fs.existsSync(rushJsonPath)) {
|
|
88
|
+
throw new Error(`Not a Rush repo (no rush.json): ${repoDir}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rigConfig = RIG_CONFIGS[rig];
|
|
92
|
+
|
|
93
|
+
console.log(`Initializing library: ${packageName}`);
|
|
94
|
+
console.log(` Directory: ${projectFolder}`);
|
|
95
|
+
console.log(` Rig: ${rig} (${rigConfig.rigPackageName})`);
|
|
96
|
+
console.log(` Version: ${versionPolicy}@${version}`);
|
|
97
|
+
console.log('');
|
|
98
|
+
|
|
99
|
+
// ── Create directory structure ──
|
|
100
|
+
fs.mkdirSync(path.join(projectDir, 'src', 'test', 'unit'), { recursive: true });
|
|
101
|
+
fs.mkdirSync(path.join(projectDir, 'config'), { recursive: true });
|
|
102
|
+
|
|
103
|
+
// ── package.json ──
|
|
104
|
+
console.log('==> Creating package.json...');
|
|
105
|
+
|
|
106
|
+
const devDependencies: Record<string, string> = {};
|
|
107
|
+
// Add rig dependencies — @fgv/* packages use fgvDepVersion, others use their pinned version
|
|
108
|
+
for (const [dep, ver] of Object.entries(rigConfig.rigDevDeps)) {
|
|
109
|
+
devDependencies[dep] = dep.startsWith('@fgv/') ? fgvDepVersion : ver;
|
|
110
|
+
}
|
|
111
|
+
// Standard dev dependencies
|
|
112
|
+
devDependencies['@fgv/ts-utils-jest'] = fgvDepVersion;
|
|
113
|
+
devDependencies['@types/heft-jest'] = '1.0.6';
|
|
114
|
+
devDependencies['@types/jest'] = '^29.5.14';
|
|
115
|
+
devDependencies['@types/node'] = '^20.14.9';
|
|
116
|
+
devDependencies['typescript'] = '5.9.3';
|
|
117
|
+
devDependencies['@rushstack/eslint-config'] = '4.6.4';
|
|
118
|
+
devDependencies['eslint'] = '^9.39.2';
|
|
119
|
+
|
|
120
|
+
const packageJson: Record<string, unknown> = {
|
|
121
|
+
name: packageName,
|
|
122
|
+
version,
|
|
123
|
+
description,
|
|
124
|
+
main: 'lib/index.js',
|
|
125
|
+
types: 'lib/index.d.ts',
|
|
126
|
+
scripts: {
|
|
127
|
+
build: 'heft build --clean',
|
|
128
|
+
clean: 'heft clean',
|
|
129
|
+
test: 'heft test --clean',
|
|
130
|
+
coverage: 'jest --coverage',
|
|
131
|
+
lint: 'eslint src --ext .ts',
|
|
132
|
+
fixlint: 'eslint src --ext .ts --fix'
|
|
133
|
+
},
|
|
134
|
+
author: '',
|
|
135
|
+
license: 'MIT',
|
|
136
|
+
dependencies: {
|
|
137
|
+
'@fgv/ts-utils': fgvDepVersion,
|
|
138
|
+
'@fgv/ts-json-base': fgvDepVersion
|
|
139
|
+
},
|
|
140
|
+
devDependencies,
|
|
141
|
+
repository: {
|
|
142
|
+
type: 'git',
|
|
143
|
+
url: ''
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Add dual-emit exports for dual rig
|
|
148
|
+
if (rig === 'dual') {
|
|
149
|
+
packageJson['module'] = 'dist/index.js';
|
|
150
|
+
packageJson['exports'] = {
|
|
151
|
+
'.': {
|
|
152
|
+
types: './lib/index.d.ts',
|
|
153
|
+
import: './dist/index.js',
|
|
154
|
+
require: './lib/index.js',
|
|
155
|
+
default: './lib/index.js'
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n');
|
|
161
|
+
|
|
162
|
+
// ── tsconfig.json ──
|
|
163
|
+
console.log(' Creating tsconfig.json...');
|
|
164
|
+
|
|
165
|
+
const tsconfig: Record<string, unknown> = {
|
|
166
|
+
extends: rigConfig.tsconfigExtends,
|
|
167
|
+
compilerOptions: {
|
|
168
|
+
types: rigConfig.tsconfigTypes,
|
|
169
|
+
...(rigConfig.tsconfigLib ? { lib: rigConfig.tsconfigLib } : {})
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2) + '\n');
|
|
174
|
+
|
|
175
|
+
// ── config/rig.json ──
|
|
176
|
+
console.log(' Creating config/rig.json...');
|
|
177
|
+
|
|
178
|
+
const rigJson: Record<string, unknown> = {
|
|
179
|
+
$schema: 'https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json',
|
|
180
|
+
rigPackageName: rigConfig.rigPackageName
|
|
181
|
+
};
|
|
182
|
+
if (rigConfig.rigProfile) {
|
|
183
|
+
rigJson['rigProfile'] = rigConfig.rigProfile;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fs.writeFileSync(path.join(projectDir, 'config', 'rig.json'), JSON.stringify(rigJson, null, 2) + '\n');
|
|
187
|
+
|
|
188
|
+
// ── config/jest.config.json ──
|
|
189
|
+
console.log(' Creating config/jest.config.json...');
|
|
190
|
+
|
|
191
|
+
const jestConfig = {
|
|
192
|
+
extends: '@rushstack/heft-node-rig/profiles/default/config/jest.config.json',
|
|
193
|
+
coverageThreshold: {
|
|
194
|
+
global: {
|
|
195
|
+
branches: 100,
|
|
196
|
+
functions: 100,
|
|
197
|
+
lines: 100,
|
|
198
|
+
statements: 100
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
collectCoverage: true,
|
|
202
|
+
coverageReporters: ['text', 'lcov', 'html']
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
fs.writeFileSync(
|
|
206
|
+
path.join(projectDir, 'config', 'jest.config.json'),
|
|
207
|
+
JSON.stringify(jestConfig, null, 2) + '\n'
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// ── src/index.ts ──
|
|
211
|
+
console.log(' Creating src/index.ts...');
|
|
212
|
+
|
|
213
|
+
fs.writeFileSync(
|
|
214
|
+
path.join(projectDir, 'src', 'index.ts'),
|
|
215
|
+
`/**\n * @packageDocumentation\n * ${description}\n */\n`
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// ── Register in rush.json ──
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log('==> Registering in rush.json...');
|
|
221
|
+
|
|
222
|
+
const rushJsonOps: IPatchOperation[] = [
|
|
223
|
+
{
|
|
224
|
+
type: 'add-to-array',
|
|
225
|
+
path: 'projects',
|
|
226
|
+
value: JSON.stringify({
|
|
227
|
+
packageName,
|
|
228
|
+
projectFolder,
|
|
229
|
+
shouldPublish: true,
|
|
230
|
+
versionPolicyName: versionPolicy,
|
|
231
|
+
tags: [category]
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
patchFile(rushJsonPath, rushJsonOps);
|
|
237
|
+
console.log(` Added ${packageName} at ${projectFolder}`);
|
|
238
|
+
|
|
239
|
+
// ── Done ──
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log(`=== Library ${packageName} initialized at ${projectFolder} ===`);
|
|
242
|
+
console.log('');
|
|
243
|
+
console.log('Next steps:');
|
|
244
|
+
console.log(` 1. cd ${projectDir}`);
|
|
245
|
+
console.log(' 2. rush update');
|
|
246
|
+
console.log(' 3. rushx build');
|
|
247
|
+
console.log(' 4. Start adding code to src/');
|
|
248
|
+
console.log('');
|
|
249
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch command — apply targeted edits to JSONC config files.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import { IPatchOperation, PatchOperationType, applyOperations, parseValue } from '../packlets/jsonc';
|
|
7
|
+
|
|
8
|
+
export interface IPatchOptions {
|
|
9
|
+
file: string;
|
|
10
|
+
operations: IPatchOperation[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse CLI arguments for the patch command into operations.
|
|
15
|
+
* Expects pairs like: --set 'path=value', --uncomment 'path', etc.
|
|
16
|
+
*/
|
|
17
|
+
export function parsePatchArgs(args: string[]): IPatchOperation[] {
|
|
18
|
+
const operations: IPatchOperation[] = [];
|
|
19
|
+
const validOps: PatchOperationType[] = ['set', 'set-json', 'uncomment', 'add-to-array', 'remove'];
|
|
20
|
+
|
|
21
|
+
let i = 0;
|
|
22
|
+
while (i < args.length) {
|
|
23
|
+
const arg = args[i];
|
|
24
|
+
if (!arg.startsWith('--')) {
|
|
25
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const opType = arg.slice(2) as PatchOperationType;
|
|
29
|
+
if (!validOps.includes(opType)) {
|
|
30
|
+
throw new Error(`Unknown operation: ${arg}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
i++;
|
|
34
|
+
if (i >= args.length) {
|
|
35
|
+
throw new Error(`Missing value for ${arg}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const operand = args[i];
|
|
39
|
+
i++;
|
|
40
|
+
|
|
41
|
+
if (opType === 'uncomment' || opType === 'remove') {
|
|
42
|
+
operations.push({ type: opType, path: operand });
|
|
43
|
+
} else {
|
|
44
|
+
const eqIndex = operand.indexOf('=');
|
|
45
|
+
if (eqIndex === -1) {
|
|
46
|
+
throw new Error(`Expected path=value for ${arg}, got: ${operand}`);
|
|
47
|
+
}
|
|
48
|
+
const opPath = operand.slice(0, eqIndex);
|
|
49
|
+
const rawValue = operand.slice(eqIndex + 1);
|
|
50
|
+
|
|
51
|
+
if (opType === 'set') {
|
|
52
|
+
operations.push({ type: opType, path: opPath, value: parseValue(rawValue) });
|
|
53
|
+
} else {
|
|
54
|
+
// set-json and add-to-array pass raw JSON string
|
|
55
|
+
operations.push({ type: opType, path: opPath, value: rawValue });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return operations;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function runPatch(options: IPatchOptions): Promise<void> {
|
|
64
|
+
const { file, operations } = options;
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(file)) {
|
|
67
|
+
throw new Error(`File not found: ${file}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`Patching: ${file}`);
|
|
71
|
+
let source = fs.readFileSync(file, 'utf-8');
|
|
72
|
+
|
|
73
|
+
for (const op of operations) {
|
|
74
|
+
const desc =
|
|
75
|
+
op.type === 'uncomment' || op.type === 'remove'
|
|
76
|
+
? ` ${op.type}: ${op.path}`
|
|
77
|
+
: ` ${op.type}: ${op.path} = ${op.value}`;
|
|
78
|
+
console.log(desc);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
source = applyOperations(source, operations);
|
|
82
|
+
fs.writeFileSync(file, source);
|
|
83
|
+
console.log(` Done: ${operations.length} operation(s) applied`);
|
|
84
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync command — updates shared files in a consumer repo from the template source.
|
|
3
|
+
* Templated files are NOT touched — they are owned by the consumer after creation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { loadManifest, getDefaultManifestPath } from '../packlets/manifest';
|
|
9
|
+
import { copyFile, copyPackage, filesAreEqual, getGitCommit } from '../packlets/fs';
|
|
10
|
+
|
|
11
|
+
export interface ISyncOptions {
|
|
12
|
+
targetDir: string;
|
|
13
|
+
sourceDir: string;
|
|
14
|
+
dryRun: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runSync(options: ISyncOptions): Promise<void> {
|
|
18
|
+
const { targetDir, sourceDir, dryRun } = options;
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(targetDir)) {
|
|
21
|
+
throw new Error(`Target directory does not exist: ${targetDir}`);
|
|
22
|
+
}
|
|
23
|
+
if (!fs.existsSync(path.join(sourceDir, 'rush.json'))) {
|
|
24
|
+
throw new Error(`Source directory is not a Rush repo: ${sourceDir}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const manifestPath = getDefaultManifestPath();
|
|
28
|
+
const manifest = loadManifest(manifestPath);
|
|
29
|
+
|
|
30
|
+
console.log('Syncing shared files');
|
|
31
|
+
console.log(` Source: ${sourceDir}`);
|
|
32
|
+
console.log(` Target: ${targetDir}`);
|
|
33
|
+
if (dryRun) {
|
|
34
|
+
console.log(' Mode: DRY RUN (no changes will be made)');
|
|
35
|
+
}
|
|
36
|
+
console.log('');
|
|
37
|
+
|
|
38
|
+
let copied = 0;
|
|
39
|
+
let skipped = 0;
|
|
40
|
+
let warnings = 0;
|
|
41
|
+
|
|
42
|
+
// ── Sync individual shared files ──
|
|
43
|
+
console.log('==> Syncing shared files...');
|
|
44
|
+
|
|
45
|
+
for (const file of manifest.shared.files) {
|
|
46
|
+
const srcPath = path.join(sourceDir, file.source);
|
|
47
|
+
const dstPath = path.join(targetDir, file.destination);
|
|
48
|
+
|
|
49
|
+
if (!fs.existsSync(srcPath)) {
|
|
50
|
+
console.log(` WARNING: Source not found: ${file.source}`);
|
|
51
|
+
warnings++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (dryRun) {
|
|
56
|
+
if (fs.existsSync(dstPath) && filesAreEqual(srcPath, dstPath)) {
|
|
57
|
+
console.log(` [unchanged] ${file.destination}`);
|
|
58
|
+
skipped++;
|
|
59
|
+
} else if (fs.existsSync(dstPath)) {
|
|
60
|
+
console.log(` [would update] ${file.destination}`);
|
|
61
|
+
copied++;
|
|
62
|
+
} else {
|
|
63
|
+
console.log(` [would create] ${file.destination}`);
|
|
64
|
+
copied++;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
if (fs.existsSync(dstPath) && filesAreEqual(srcPath, dstPath)) {
|
|
68
|
+
skipped++;
|
|
69
|
+
} else {
|
|
70
|
+
copyFile(srcPath, dstPath);
|
|
71
|
+
console.log(` Updated: ${file.destination}`);
|
|
72
|
+
copied++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Sync shared packages ──
|
|
78
|
+
console.log('');
|
|
79
|
+
console.log('==> Syncing shared packages...');
|
|
80
|
+
|
|
81
|
+
for (const pkg of manifest.sharedPackages.packages) {
|
|
82
|
+
const srcPath = path.join(sourceDir, pkg.source);
|
|
83
|
+
const dstPath = path.join(targetDir, pkg.destination);
|
|
84
|
+
|
|
85
|
+
if (!fs.existsSync(srcPath)) {
|
|
86
|
+
console.log(` WARNING: Source directory not found: ${pkg.source}`);
|
|
87
|
+
warnings++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (dryRun) {
|
|
92
|
+
console.log(` [would sync] ${pkg.destination}/`);
|
|
93
|
+
copied++;
|
|
94
|
+
} else {
|
|
95
|
+
copyPackage(srcPath, dstPath);
|
|
96
|
+
console.log(` Synced package: ${pkg.destination}`);
|
|
97
|
+
copied++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Update sync metadata ──
|
|
102
|
+
if (!dryRun) {
|
|
103
|
+
const syncFilePath = path.join(targetDir, '.template-sync');
|
|
104
|
+
if (fs.existsSync(syncFilePath)) {
|
|
105
|
+
try {
|
|
106
|
+
const syncData = JSON.parse(fs.readFileSync(syncFilePath, 'utf-8'));
|
|
107
|
+
syncData.lastSyncedAt = new Date().toISOString();
|
|
108
|
+
syncData.sourceCommit = getGitCommit(sourceDir);
|
|
109
|
+
fs.writeFileSync(syncFilePath, JSON.stringify(syncData, null, 2) + '\n');
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(' Updated .template-sync');
|
|
112
|
+
} catch {
|
|
113
|
+
console.log(' WARNING: Could not update .template-sync');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Summary ──
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log('=== Sync complete ===');
|
|
121
|
+
console.log(` Updated: ${copied}`);
|
|
122
|
+
console.log(` Unchanged: ${skipped}`);
|
|
123
|
+
console.log(` Warnings: ${warnings}`);
|
|
124
|
+
|
|
125
|
+
if (dryRun) {
|
|
126
|
+
console.log('');
|
|
127
|
+
console.log('This was a dry run. No files were modified.');
|
|
128
|
+
console.log('Run without --dry-run to apply changes.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!dryRun && copied > 0) {
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log('Next steps:');
|
|
134
|
+
console.log(` 1. Review changes: cd ${targetDir} && git diff`);
|
|
135
|
+
console.log(" 2. Commit: git add -A && git commit -m 'Sync shared files from template'");
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fgv/repo-template — CLI tool for creating and maintaining fgv-derived Rush monorepos.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { RepoTemplateCli } from './cli';
|
|
8
|
+
export { runCreate, ICreateOptions } from './commands/create';
|
|
9
|
+
export { runSync, ISyncOptions } from './commands/sync';
|
|
10
|
+
export { runPatch, IPatchOptions, parsePatchArgs } from './commands/patch';
|
|
11
|
+
export { runInitLibrary, IInitLibraryOptions, RigType, CategoryType } from './commands/init-library';
|
|
12
|
+
export { applyOperations, applyOperation, patchFile, IPatchOperation } from './packlets/jsonc';
|
|
13
|
+
export { loadManifest, IManifest, ISharedFile, ISharedPackage, ITemplatedFile } from './packlets/manifest';
|
|
14
|
+
export { renderTemplate, renderTemplateFile, ITemplateVars } from './packlets/template';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File system and shell execution helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { execSync, ExecSyncOptions } from 'child_process';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Directories to exclude when copying shared packages.
|
|
11
|
+
*/
|
|
12
|
+
const EXCLUDED_DIRS = new Set(['node_modules', 'lib', 'dist', '.heft', 'temp', 'coverage', 'rush-logs']);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Copy a single file, creating parent directories as needed.
|
|
16
|
+
*/
|
|
17
|
+
export function copyFile(src: string, dest: string): void {
|
|
18
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
19
|
+
fs.copyFileSync(src, dest);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Copy a directory recursively, excluding build artifacts.
|
|
24
|
+
*/
|
|
25
|
+
export function copyPackage(src: string, dest: string): void {
|
|
26
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
27
|
+
fs.cpSync(src, dest, {
|
|
28
|
+
recursive: true,
|
|
29
|
+
filter: (source: string) => {
|
|
30
|
+
const basename = path.basename(source);
|
|
31
|
+
return !EXCLUDED_DIRS.has(basename);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compare two files for equality.
|
|
38
|
+
* Returns true if files are identical.
|
|
39
|
+
*/
|
|
40
|
+
export function filesAreEqual(pathA: string, pathB: string): boolean {
|
|
41
|
+
if (!fs.existsSync(pathA) || !fs.existsSync(pathB)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const a = fs.readFileSync(pathA);
|
|
45
|
+
const b = fs.readFileSync(pathB);
|
|
46
|
+
return Buffer.compare(a, b) === 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Execute a shell command, returning stdout.
|
|
51
|
+
*/
|
|
52
|
+
export function exec(command: string, options?: ExecSyncOptions): string {
|
|
53
|
+
const result = execSync(command, {
|
|
54
|
+
encoding: 'utf-8',
|
|
55
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
56
|
+
...options
|
|
57
|
+
});
|
|
58
|
+
return (result as string).trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Execute a shell command, inheriting stdio (output goes to terminal).
|
|
63
|
+
*/
|
|
64
|
+
export function execInherit(command: string, options?: ExecSyncOptions): void {
|
|
65
|
+
execSync(command, {
|
|
66
|
+
stdio: 'inherit',
|
|
67
|
+
...options
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Try to get the current git commit hash from a directory.
|
|
73
|
+
* Returns empty string if not a git repo or git is not available.
|
|
74
|
+
*/
|
|
75
|
+
export function getGitCommit(dir: string): string {
|
|
76
|
+
try {
|
|
77
|
+
return exec('git rev-parse HEAD', { cwd: dir });
|
|
78
|
+
} catch {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Try to get the git remote URL from a directory.
|
|
85
|
+
*/
|
|
86
|
+
export function getGitRemoteUrl(dir: string): string {
|
|
87
|
+
try {
|
|
88
|
+
return exec('git remote get-url origin', { cwd: dir });
|
|
89
|
+
} catch {
|
|
90
|
+
return 'local';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Auto-detect the fgv source repository root by walking up from a starting directory,
|
|
96
|
+
* looking for a rush.json that belongs to the fgv repo.
|
|
97
|
+
*/
|
|
98
|
+
export function detectSourceDir(startDir: string): string | undefined {
|
|
99
|
+
let dir = path.resolve(startDir);
|
|
100
|
+
const root = path.parse(dir).root;
|
|
101
|
+
|
|
102
|
+
while (dir !== root) {
|
|
103
|
+
const rushJsonPath = path.join(dir, 'rush.json');
|
|
104
|
+
if (fs.existsSync(rushJsonPath)) {
|
|
105
|
+
// Verify it looks like the fgv repo by checking for the template tool
|
|
106
|
+
const manifestPath = path.join(dir, 'tools', 'repo-template', 'sync-manifest.json');
|
|
107
|
+
if (fs.existsSync(manifestPath)) {
|
|
108
|
+
return dir;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
dir = path.dirname(dir);
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|