@pixpilot/scaffoldfy-configs 0.46.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -134,16 +134,6 @@ Usage:
|
|
|
134
134
|
npx @pixpilot/scaffoldfy@latest --config https://unpkg.com/@pixpilot/scaffoldfy-configs@latest/update-root-package-json/scaffoldfy.json
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
### workspace-generator
|
|
138
|
-
|
|
139
|
-
Generate workspace with apps and packages
|
|
140
|
-
|
|
141
|
-
Usage:
|
|
142
|
-
|
|
143
|
-
```sh
|
|
144
|
-
npx @pixpilot/scaffoldfy@latest --config https://unpkg.com/@pixpilot/scaffoldfy-configs@latest/workspace-generator/scaffoldfy.jsonc
|
|
145
|
-
```
|
|
146
|
-
|
|
147
137
|
### workspace-initializer
|
|
148
138
|
|
|
149
139
|
Initial setup for a pnpm + Turbo monorepo template, including project info, license, and initial package generation.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixpilot/scaffoldfy-configs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.48.0",
|
|
5
5
|
"author": "PixPilot <m.doaie@hotmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"@types/node": "^22.18.11",
|
|
31
31
|
"eslint": "^9.38.0",
|
|
32
32
|
"@internal/eslint-config": "0.3.0",
|
|
33
|
-
"@internal/tsdown-config": "0.1.0",
|
|
34
33
|
"@internal/prettier-config": "0.0.1",
|
|
34
|
+
"@internal/tsdown-config": "0.1.0",
|
|
35
35
|
"@internal/vitest-config": "0.1.0",
|
|
36
|
-
"@pixpilot/scaffoldfy": "0.
|
|
36
|
+
"@pixpilot/scaffoldfy": "0.58.0"
|
|
37
37
|
},
|
|
38
38
|
"prettier": "@internal/prettier-config",
|
|
39
39
|
"publishConfig": {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
loadTasksWithInheritance,
|
|
4
|
+
resolveConfigAnswers,
|
|
5
|
+
validateScaffoldfyJsonFile,
|
|
6
|
+
} from '@pixpilot/scaffoldfy';
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
// The repo's own package generator (`pnpm gen:package`), which answers the prompts of
|
|
10
|
+
// workspace-package-generator in its `answers` field
|
|
11
|
+
const generatorPath = path.join(
|
|
12
|
+
__dirname,
|
|
13
|
+
'..',
|
|
14
|
+
'..',
|
|
15
|
+
'..',
|
|
16
|
+
'generators',
|
|
17
|
+
'package',
|
|
18
|
+
'scaffoldfy.jsonc',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
describe('package generator answers', () => {
|
|
22
|
+
it('should be a valid config', () => {
|
|
23
|
+
expect(validateScaffoldfyJsonFile(generatorPath).valid).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should answer existing workspace-package-generator prompts', async () => {
|
|
27
|
+
const { configs = [] } = await loadTasksWithInheritance(generatorPath, {
|
|
28
|
+
sequential: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(resolveConfigAnswers(configs)).toEqual({
|
|
32
|
+
workspace: 'packages',
|
|
33
|
+
bundler: 'tsdown',
|
|
34
|
+
author: 'PixPilot',
|
|
35
|
+
authorEmail: 'm.doaie@hotmail.com',
|
|
36
|
+
repoOwner: 'pixpilot',
|
|
37
|
+
orgName: 'pixpilot',
|
|
38
|
+
licenseType: 'MIT',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import process from 'node:process';
|
|
6
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
7
|
-
|
|
8
|
-
const SCRIPT_PATH = path.join(
|
|
9
|
-
__dirname,
|
|
10
|
-
'..',
|
|
11
|
-
'workspace-generator',
|
|
12
|
-
'scripts',
|
|
13
|
-
'setup-workspace.cjs',
|
|
14
|
-
);
|
|
15
|
-
const temporaryDirectories: string[] = [];
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
for (const directory of temporaryDirectories.splice(0)) {
|
|
19
|
-
fs.rmSync(directory, { force: true, recursive: true });
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
function createFakeGit(testDirectory: string): string {
|
|
24
|
-
const binDirectory = path.join(testDirectory, 'bin');
|
|
25
|
-
const fakeGitPath = path.join(binDirectory, 'fake-git.cjs');
|
|
26
|
-
const gitCommandPath = path.join(
|
|
27
|
-
binDirectory,
|
|
28
|
-
process.platform === 'win32' ? 'git.exe' : 'git',
|
|
29
|
-
);
|
|
30
|
-
const pnpmCommandPath = path.join(
|
|
31
|
-
binDirectory,
|
|
32
|
-
process.platform === 'win32' ? 'pnpm.exe' : 'pnpm',
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
fs.mkdirSync(binDirectory, { recursive: true });
|
|
36
|
-
fs.writeFileSync(
|
|
37
|
-
fakeGitPath,
|
|
38
|
-
[
|
|
39
|
-
"'use strict';",
|
|
40
|
-
"const fs = require('node:fs');",
|
|
41
|
-
"const Module = require('node:module');",
|
|
42
|
-
"const path = require('node:path');",
|
|
43
|
-
'function applyGitOperation(command, args) {',
|
|
44
|
-
" if (command === 'clone') {",
|
|
45
|
-
' const destination = args.at(-1);',
|
|
46
|
-
" if (destination === '.') {",
|
|
47
|
-
" fs.writeFileSync(path.join(process.cwd(), 'base-template.txt'), 'base');",
|
|
48
|
-
" fs.writeFileSync(path.join(process.cwd(), 'package.json'), JSON.stringify({ scripts: { lint: 'base-lint' }, devDependencies: { 'base-dependency': '1.0.0' } }));",
|
|
49
|
-
" fs.writeFileSync(path.join(process.cwd(), 'pnpm-workspace.yaml'), \"packages:\\n - 'packages/**'\\n - 'tooling/**'\\ncatalogs:\\n dev:\\n tsdown: ^1.0.0\\n typescript-eslint: ^1.0.0\\n\");",
|
|
50
|
-
' } else {',
|
|
51
|
-
' fs.mkdirSync(destination, { recursive: true });',
|
|
52
|
-
' }',
|
|
53
|
-
" } else if (command === 'pnpm' || command === 'install') {",
|
|
54
|
-
" fs.writeFileSync(path.join(process.cwd(), 'pnpm-lock.yaml'), 'lock');",
|
|
55
|
-
" } else if (command === 'sparse-checkout') {",
|
|
56
|
-
" const sourceDirectories = args.filter((arg) => !arg.startsWith('-') && arg !== 'set');",
|
|
57
|
-
' for (const sourceDirectory of sourceDirectories) {',
|
|
58
|
-
" if (sourceDirectory === 'package.json') {",
|
|
59
|
-
" fs.writeFileSync(path.join(process.cwd(), sourceDirectory), JSON.stringify({ scripts: { 'web:dev': 'source-web-dev', 'web:build': 'source-web-build', dev: 'source-dev', native: 'source-native', 'expo:clean:install': 'source-expo-clean-install', 'expo:clean:run': 'source-expo-clean-run', 'android:dev': 'source-android-dev', 'android:clean:dev': 'source-android-clean-dev', 'ios:dev': 'source-ios-dev', 'ios:clean:dev': 'source-ios-clean-dev' }, devDependencies: { 'template-dependency': '1.0.0' } }));",
|
|
60
|
-
" } else if (sourceDirectory === 'pnpm-workspace.yaml') {",
|
|
61
|
-
' fs.writeFileSync(path.join(process.cwd(), sourceDirectory), "packages:\\n - apps/*\\n - packages/*\\n - tooling/*\\ncatalogs:\\n dev:\\n template-dependency: ^1.0.0\\n \\"typescript-eslint\\": ^2.0.0\\n");',
|
|
62
|
-
" } else if (sourceDirectory === '.github/workflows') {",
|
|
63
|
-
' fs.mkdirSync(path.join(process.cwd(), sourceDirectory), { recursive: true });',
|
|
64
|
-
" fs.writeFileSync(path.join(process.cwd(), sourceDirectory, 'ci.yml'), 'ci');",
|
|
65
|
-
' } else {',
|
|
66
|
-
' const templateDirectory = path.join(process.cwd(), sourceDirectory);',
|
|
67
|
-
' fs.mkdirSync(templateDirectory, { recursive: true });',
|
|
68
|
-
" fs.writeFileSync(path.join(templateDirectory, 'template.txt'), sourceDirectory);",
|
|
69
|
-
' }',
|
|
70
|
-
' }',
|
|
71
|
-
'}',
|
|
72
|
-
'}',
|
|
73
|
-
"if (process.env.SCAFFOLDFY_FAKE_GIT_MODE === 'preload') {",
|
|
74
|
-
' const resolveFilename = Module._resolveFilename;',
|
|
75
|
-
' Module._resolveFilename = function resolveFakeGit(request, ...args) {',
|
|
76
|
-
' const command = path.basename(request);',
|
|
77
|
-
" if (command === 'clone' || command === 'install' || command === 'pnpm' || command === 'sparse-checkout') {",
|
|
78
|
-
" applyGitOperation(command === 'install' ? 'pnpm' : command, process.argv.slice(2));",
|
|
79
|
-
" return path.join(__dirname, 'noop.cjs');",
|
|
80
|
-
' }',
|
|
81
|
-
' return resolveFilename.call(this, request, ...args);',
|
|
82
|
-
' };',
|
|
83
|
-
'} else {',
|
|
84
|
-
' const [command, ...args] = process.argv.slice(2);',
|
|
85
|
-
' applyGitOperation(command, args);',
|
|
86
|
-
'}',
|
|
87
|
-
].join('\n'),
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
if (process.platform === 'win32') {
|
|
91
|
-
fs.writeFileSync(path.join(binDirectory, 'noop.cjs'), "'use strict';\n");
|
|
92
|
-
fs.copyFileSync(process.execPath, gitCommandPath);
|
|
93
|
-
fs.copyFileSync(process.execPath, pnpmCommandPath);
|
|
94
|
-
} else {
|
|
95
|
-
fs.writeFileSync(
|
|
96
|
-
gitCommandPath,
|
|
97
|
-
`#!${process.execPath}\nrequire('./fake-git.cjs');\n`,
|
|
98
|
-
);
|
|
99
|
-
fs.chmodSync(gitCommandPath, 0o755);
|
|
100
|
-
fs.writeFileSync(
|
|
101
|
-
pnpmCommandPath,
|
|
102
|
-
`#!${process.execPath}\nrequire('./fake-git.cjs');\n`,
|
|
103
|
-
);
|
|
104
|
-
fs.chmodSync(pnpmCommandPath, 0o755);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return binDirectory;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function runSetup(workspaceApps: string): string {
|
|
111
|
-
const testDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'scaffoldfy-workspace-'));
|
|
112
|
-
const workspaceDirectory = path.join(testDirectory, 'workspace');
|
|
113
|
-
temporaryDirectories.push(testDirectory);
|
|
114
|
-
fs.mkdirSync(workspaceDirectory);
|
|
115
|
-
const gitBinDirectory = createFakeGit(testDirectory);
|
|
116
|
-
const fakeGitPath = path.join(gitBinDirectory, 'fake-git.cjs');
|
|
117
|
-
|
|
118
|
-
execFileSync(process.execPath, [SCRIPT_PATH], {
|
|
119
|
-
cwd: workspaceDirectory,
|
|
120
|
-
stdio: 'pipe',
|
|
121
|
-
env: {
|
|
122
|
-
...process.env,
|
|
123
|
-
PATH: `${gitBinDirectory}${path.delimiter}${process.env['PATH'] ?? ''}`,
|
|
124
|
-
...(process.platform === 'win32' && {
|
|
125
|
-
NODE_OPTIONS: `--require=${fakeGitPath} ${process.env['NODE_OPTIONS'] ?? ''}`,
|
|
126
|
-
Path: `${gitBinDirectory}${path.delimiter}${process.env['Path'] ?? ''}`,
|
|
127
|
-
SCAFFOLDFY_FAKE_GIT_MODE: 'preload',
|
|
128
|
-
}),
|
|
129
|
-
WORKSPACE_APPS: workspaceApps,
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
return workspaceDirectory;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
describe('workspace-generator setup script', () => {
|
|
137
|
-
it('should clone the base template when only Library is selected', () => {
|
|
138
|
-
const workspaceDirectory = runSetup('library');
|
|
139
|
-
|
|
140
|
-
expect(
|
|
141
|
-
fs.readFileSync(path.join(workspaceDirectory, 'base-template.txt'), 'utf8'),
|
|
142
|
-
).toBe('base');
|
|
143
|
-
expect(fs.existsSync(path.join(workspaceDirectory, 'apps'))).toBe(false);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should add the selected app and its required workspace files', () => {
|
|
147
|
-
const workspaceDirectory = runSetup('nextjs-cloudflare');
|
|
148
|
-
|
|
149
|
-
expect(
|
|
150
|
-
fs.readFileSync(path.join(workspaceDirectory, 'base-template.txt'), 'utf8'),
|
|
151
|
-
).toBe('base');
|
|
152
|
-
expect(
|
|
153
|
-
fs.readFileSync(
|
|
154
|
-
path.join(workspaceDirectory, 'apps', 'web', 'template.txt'),
|
|
155
|
-
'utf8',
|
|
156
|
-
),
|
|
157
|
-
).toBe('apps/web');
|
|
158
|
-
expect(
|
|
159
|
-
fs.readFileSync(path.join(workspaceDirectory, 'packages', 'template.txt'), 'utf8'),
|
|
160
|
-
).toBe('packages');
|
|
161
|
-
expect(
|
|
162
|
-
fs.readFileSync(path.join(workspaceDirectory, 'tooling', 'template.txt'), 'utf8'),
|
|
163
|
-
).toBe('tooling');
|
|
164
|
-
expect(
|
|
165
|
-
fs.readFileSync(
|
|
166
|
-
path.join(workspaceDirectory, '.github', 'workflows', 'ci.yml'),
|
|
167
|
-
'utf8',
|
|
168
|
-
),
|
|
169
|
-
).toBe('ci');
|
|
170
|
-
|
|
171
|
-
const packageJson = JSON.parse(
|
|
172
|
-
fs.readFileSync(path.join(workspaceDirectory, 'package.json'), 'utf8'),
|
|
173
|
-
);
|
|
174
|
-
expect(packageJson.scripts).toMatchObject({
|
|
175
|
-
lint: 'base-lint',
|
|
176
|
-
'web:dev': 'source-web-dev',
|
|
177
|
-
});
|
|
178
|
-
expect(packageJson.devDependencies).toMatchObject({
|
|
179
|
-
'base-dependency': '1.0.0',
|
|
180
|
-
'template-dependency': '1.0.0',
|
|
181
|
-
});
|
|
182
|
-
expect(
|
|
183
|
-
fs.readFileSync(path.join(workspaceDirectory, 'pnpm-workspace.yaml'), 'utf8'),
|
|
184
|
-
).toContain(' - apps/*');
|
|
185
|
-
expect(
|
|
186
|
-
fs.readFileSync(path.join(workspaceDirectory, 'pnpm-workspace.yaml'), 'utf8'),
|
|
187
|
-
).not.toContain('apps/**');
|
|
188
|
-
expect(
|
|
189
|
-
fs.readFileSync(path.join(workspaceDirectory, 'pnpm-workspace.yaml'), 'utf8'),
|
|
190
|
-
).toContain(' tsdown: ^1.0.0');
|
|
191
|
-
expect(
|
|
192
|
-
fs.readFileSync(path.join(workspaceDirectory, 'pnpm-workspace.yaml'), 'utf8'),
|
|
193
|
-
).toContain(' template-dependency: ^1.0.0');
|
|
194
|
-
expect(fs.readFileSync(path.join(workspaceDirectory, 'pnpm-lock.yaml'), 'utf8')).toBe(
|
|
195
|
-
'lock',
|
|
196
|
-
);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should add every selected app to the base workspace', () => {
|
|
200
|
-
const workspaceDirectory = runSetup(
|
|
201
|
-
'library,nextjs-cloudflare,chrome-extension,expo',
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
expect(
|
|
205
|
-
fs.readFileSync(
|
|
206
|
-
path.join(workspaceDirectory, 'apps', 'web', 'template.txt'),
|
|
207
|
-
'utf8',
|
|
208
|
-
),
|
|
209
|
-
).toBe('apps/web');
|
|
210
|
-
expect(
|
|
211
|
-
fs.readFileSync(
|
|
212
|
-
path.join(workspaceDirectory, 'apps', 'chrome-extension', 'template.txt'),
|
|
213
|
-
'utf8',
|
|
214
|
-
),
|
|
215
|
-
).toBe('apps/chrome-extension');
|
|
216
|
-
expect(
|
|
217
|
-
fs.readFileSync(
|
|
218
|
-
path.join(workspaceDirectory, 'apps', 'expo', 'template.txt'),
|
|
219
|
-
'utf8',
|
|
220
|
-
),
|
|
221
|
-
).toBe('apps/expo');
|
|
222
|
-
|
|
223
|
-
const packageJson = JSON.parse(
|
|
224
|
-
fs.readFileSync(path.join(workspaceDirectory, 'package.json'), 'utf8'),
|
|
225
|
-
);
|
|
226
|
-
expect(packageJson.scripts).toMatchObject({
|
|
227
|
-
dev: 'source-dev',
|
|
228
|
-
native: 'source-native',
|
|
229
|
-
'web:dev': 'source-web-dev',
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('should fail when no workspace app is selected', () => {
|
|
234
|
-
expect(() => runSetup('')).toThrow('Select at least one workspace app.');
|
|
235
|
-
});
|
|
236
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "../node_modules/@pixpilot/scaffoldfy/schema/scaffoldfy.schema.json",
|
|
3
|
-
// "extends": ["../workspace-initializer/scaffoldfy.json"],
|
|
4
|
-
"name": "pnpm-turbo-monorepo-template-setup",
|
|
5
|
-
"description": "Generate workspace with apps and packages",
|
|
6
|
-
"prompts": [
|
|
7
|
-
{
|
|
8
|
-
"id": "workspaceApps",
|
|
9
|
-
"type": "checkbox",
|
|
10
|
-
"message": "Select the apps to include:",
|
|
11
|
-
"choices": [
|
|
12
|
-
{
|
|
13
|
-
"name": "Library",
|
|
14
|
-
"value": "library"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"name": "Next.js Cloudflare",
|
|
18
|
-
"value": "nextjs-cloudflare"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"name": "Chrome Extension",
|
|
22
|
-
"value": "chrome-extension"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"name": "Expo",
|
|
26
|
-
"value": "expo"
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
// {
|
|
31
|
-
// "id": "workspaceTools",
|
|
32
|
-
// "type": "checkbox",
|
|
33
|
-
// "message": "Select the packages to include:",
|
|
34
|
-
// "choices": [
|
|
35
|
-
// {
|
|
36
|
-
// "name": "storybook",
|
|
37
|
-
// "value": "storybook"
|
|
38
|
-
// }
|
|
39
|
-
// ]
|
|
40
|
-
// }
|
|
41
|
-
// {
|
|
42
|
-
// "id": "nextjsVisualTest",
|
|
43
|
-
// "type": "input",
|
|
44
|
-
// "message": "Next.js visual-test value:",
|
|
45
|
-
// "enabled": {
|
|
46
|
-
// "type": "condition",
|
|
47
|
-
// "value": "workspaceApps.includes('nextjs')"
|
|
48
|
-
// }
|
|
49
|
-
// },
|
|
50
|
-
],
|
|
51
|
-
"tasks": [
|
|
52
|
-
{
|
|
53
|
-
"id": "setup-workspace",
|
|
54
|
-
"name": "Set up workspace apps",
|
|
55
|
-
"description": "Clone the monorepo template and add each selected app",
|
|
56
|
-
"type": "exec-file",
|
|
57
|
-
"config": {
|
|
58
|
-
"file": "./scripts/setup-workspace.cjs",
|
|
59
|
-
"runtime": "node",
|
|
60
|
-
"parameters": {
|
|
61
|
-
"WORKSPACE_APPS": "{{workspaceApps}}"
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
]
|
|
66
|
-
}
|
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const { execFileSync } = require('node:child_process');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const os = require('node:os');
|
|
7
|
-
const path = require('node:path');
|
|
8
|
-
const process = require('node:process');
|
|
9
|
-
|
|
10
|
-
const BASE_TEMPLATE_REPOSITORY =
|
|
11
|
-
'https://github.com/pixpilot/pnpm-turbo-monorepo-template.git';
|
|
12
|
-
|
|
13
|
-
const APP_TEMPLATES = {
|
|
14
|
-
'nextjs-cloudflare': {
|
|
15
|
-
repository: 'https://github.com/ccpu/nextjs-cloudflare-monorepo-template.git',
|
|
16
|
-
sourceDirectory: 'apps/web',
|
|
17
|
-
destinationDirectory: 'apps/web',
|
|
18
|
-
rootScripts: ['web:dev', 'web:build'],
|
|
19
|
-
},
|
|
20
|
-
'chrome-extension': {
|
|
21
|
-
repository: 'https://github.com/ccpu/chrome-extension-monorepo-template.git',
|
|
22
|
-
sourceDirectory: 'apps/chrome-extension',
|
|
23
|
-
destinationDirectory: 'apps/chrome-extension',
|
|
24
|
-
rootScripts: ['dev'],
|
|
25
|
-
},
|
|
26
|
-
expo: {
|
|
27
|
-
repository: 'https://github.com/ccpu/full-stack-starter.git',
|
|
28
|
-
sourceDirectory: 'apps/expo',
|
|
29
|
-
destinationDirectory: 'apps/expo',
|
|
30
|
-
rootScripts: [
|
|
31
|
-
'native',
|
|
32
|
-
'expo:clean:install',
|
|
33
|
-
'expo:clean:run',
|
|
34
|
-
'android:dev',
|
|
35
|
-
'android:clean:dev',
|
|
36
|
-
'ios:dev',
|
|
37
|
-
'ios:clean:dev',
|
|
38
|
-
],
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const TEMPLATE_SOURCE_PATHS = [
|
|
43
|
-
'package.json',
|
|
44
|
-
'pnpm-workspace.yaml',
|
|
45
|
-
'packages',
|
|
46
|
-
'tooling',
|
|
47
|
-
'.github/workflows',
|
|
48
|
-
];
|
|
49
|
-
const JSON_INDENTATION = 2;
|
|
50
|
-
const LINE_BREAK_PATTERN = /\r?\n/u;
|
|
51
|
-
const TOP_LEVEL_SECTION_PATTERN = /^[^\s#][^:]*:/u;
|
|
52
|
-
const INDENTED_LIST_ENTRY_PATTERN = /^ {2}- /u;
|
|
53
|
-
|
|
54
|
-
function runGit(args, cwd) {
|
|
55
|
-
execFileSync('git', args, { cwd, stdio: 'inherit' });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function refreshLockfile(cwd) {
|
|
59
|
-
execFileSync('pnpm', ['install', '--lockfile-only', '--ignore-scripts'], {
|
|
60
|
-
cwd,
|
|
61
|
-
stdio: 'inherit',
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function getSelectedApps() {
|
|
66
|
-
return new Set(
|
|
67
|
-
(process.env.WORKSPACE_APPS ?? '')
|
|
68
|
-
.split(',')
|
|
69
|
-
.map((app) => app.trim())
|
|
70
|
-
.filter(Boolean),
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function ensureEmptyDirectory(directory) {
|
|
75
|
-
if (fs.readdirSync(directory).length > 0) {
|
|
76
|
-
throw new Error(`Workspace directory must be empty: ${directory}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function copyDirectoryContents(sourceDirectory, destinationDirectory) {
|
|
81
|
-
for (const entry of fs.readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
82
|
-
fs.cpSync(
|
|
83
|
-
path.join(sourceDirectory, entry.name),
|
|
84
|
-
path.join(destinationDirectory, entry.name),
|
|
85
|
-
{ recursive: entry.isDirectory(), errorOnExist: true, force: false },
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function overlayDirectoryContents(sourceDirectory, destinationDirectory) {
|
|
91
|
-
if (!fs.existsSync(sourceDirectory)) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
fs.mkdirSync(destinationDirectory, { recursive: true });
|
|
96
|
-
|
|
97
|
-
for (const entry of fs.readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
98
|
-
fs.cpSync(
|
|
99
|
-
path.join(sourceDirectory, entry.name),
|
|
100
|
-
path.join(destinationDirectory, entry.name),
|
|
101
|
-
{ recursive: entry.isDirectory(), force: true },
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function mergeDependencySections(basePackage, templatePackage) {
|
|
107
|
-
return [
|
|
108
|
-
'dependencies',
|
|
109
|
-
'devDependencies',
|
|
110
|
-
'optionalDependencies',
|
|
111
|
-
'peerDependencies',
|
|
112
|
-
].reduce(
|
|
113
|
-
(mergedPackage, section) =>
|
|
114
|
-
templatePackage[section]
|
|
115
|
-
? {
|
|
116
|
-
...mergedPackage,
|
|
117
|
-
[section]: {
|
|
118
|
-
...templatePackage[section],
|
|
119
|
-
...mergedPackage[section],
|
|
120
|
-
},
|
|
121
|
-
}
|
|
122
|
-
: mergedPackage,
|
|
123
|
-
basePackage,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function mergeRootPackageManifest(template, repositoryDirectory, workspaceDirectory) {
|
|
128
|
-
const packageJsonPath = path.join(workspaceDirectory, 'package.json');
|
|
129
|
-
const templatePackageJsonPath = path.join(repositoryDirectory, 'package.json');
|
|
130
|
-
const basePackage = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
131
|
-
const templatePackage = JSON.parse(fs.readFileSync(templatePackageJsonPath, 'utf8'));
|
|
132
|
-
const packageWithDependencies = mergeDependencySections(basePackage, templatePackage);
|
|
133
|
-
const templateScripts = Object.fromEntries(
|
|
134
|
-
template.rootScripts.flatMap((scriptName) => {
|
|
135
|
-
const script = templatePackage.scripts?.[scriptName];
|
|
136
|
-
|
|
137
|
-
return script ? [[scriptName, script]] : [];
|
|
138
|
-
}),
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const mergedPackage = {
|
|
142
|
-
...packageWithDependencies,
|
|
143
|
-
scripts: {
|
|
144
|
-
...templateScripts,
|
|
145
|
-
...packageWithDependencies.scripts,
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
fs.writeFileSync(
|
|
150
|
-
packageJsonPath,
|
|
151
|
-
`${JSON.stringify(mergedPackage, null, JSON_INDENTATION)}\n`,
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function escapeRegularExpression(value) {
|
|
156
|
-
return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function normalizeYamlKey(value) {
|
|
160
|
-
return value.replace(/^['"]|['"]$/gu, '');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function getCatalogEntries(workspaceManifest) {
|
|
164
|
-
const catalogEntries = new Map();
|
|
165
|
-
let isInCatalogsSection = false;
|
|
166
|
-
let catalogName;
|
|
167
|
-
|
|
168
|
-
for (const line of workspaceManifest.split(LINE_BREAK_PATTERN)) {
|
|
169
|
-
if (line === 'catalogs:') {
|
|
170
|
-
isInCatalogsSection = true;
|
|
171
|
-
} else if (isInCatalogsSection && TOP_LEVEL_SECTION_PATTERN.test(line)) {
|
|
172
|
-
isInCatalogsSection = false;
|
|
173
|
-
} else if (isInCatalogsSection) {
|
|
174
|
-
const catalogMatch = line.match(/^ {2}(?<catalogName>[^:\r\n]+):[\t ]*$/u);
|
|
175
|
-
const entryMatch = line.match(/^ {4}(?<key>['"][^'"]+['"]|[^:]+):/u);
|
|
176
|
-
|
|
177
|
-
if (catalogMatch) {
|
|
178
|
-
const { catalogName: nextCatalogName } = catalogMatch.groups;
|
|
179
|
-
catalogName = nextCatalogName;
|
|
180
|
-
catalogEntries.set(catalogName, []);
|
|
181
|
-
} else if (catalogName && entryMatch) {
|
|
182
|
-
const { key } = entryMatch.groups;
|
|
183
|
-
catalogEntries.get(catalogName).push({ key, line });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return catalogEntries;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function getYamlListEntries(workspaceManifest, sectionName) {
|
|
192
|
-
const entries = [];
|
|
193
|
-
let isInSection = false;
|
|
194
|
-
|
|
195
|
-
for (const line of workspaceManifest.split(LINE_BREAK_PATTERN)) {
|
|
196
|
-
if (line === `${sectionName}:`) {
|
|
197
|
-
isInSection = true;
|
|
198
|
-
} else if (isInSection && TOP_LEVEL_SECTION_PATTERN.test(line)) {
|
|
199
|
-
isInSection = false;
|
|
200
|
-
} else if (isInSection && INDENTED_LIST_ENTRY_PATTERN.test(line)) {
|
|
201
|
-
entries.push(line);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return entries;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function mergeCatalogEntries(baseWorkspaceManifest, templateWorkspaceManifest) {
|
|
209
|
-
let mergedWorkspaceManifest = baseWorkspaceManifest;
|
|
210
|
-
const lineEnding = baseWorkspaceManifest.includes('\r\n') ? '\r\n' : '\n';
|
|
211
|
-
|
|
212
|
-
for (const [catalogName, entries] of getCatalogEntries(templateWorkspaceManifest)) {
|
|
213
|
-
const catalogHeader = new RegExp(
|
|
214
|
-
`^ {2}${escapeRegularExpression(catalogName)}:[\\t ]*(?:\\r?\\n|$)`,
|
|
215
|
-
'mu',
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
if (!catalogHeader.test(mergedWorkspaceManifest)) {
|
|
219
|
-
mergedWorkspaceManifest = mergedWorkspaceManifest.replace(
|
|
220
|
-
/^catalogs:[\t ]*(?:\r?\n|$)/mu,
|
|
221
|
-
(header) =>
|
|
222
|
-
`${header} ${catalogName}:${lineEnding}${entries.map((entry) => entry.line).join(lineEnding)}${lineEnding}`,
|
|
223
|
-
);
|
|
224
|
-
} else {
|
|
225
|
-
for (const entry of entries) {
|
|
226
|
-
const entryPattern = new RegExp(
|
|
227
|
-
`^ {4}['"]?${escapeRegularExpression(normalizeYamlKey(entry.key))}['"]?:[^\\r\\n]*`,
|
|
228
|
-
'mu',
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
if (entryPattern.test(mergedWorkspaceManifest)) {
|
|
232
|
-
mergedWorkspaceManifest = mergedWorkspaceManifest.replace(
|
|
233
|
-
entryPattern,
|
|
234
|
-
entry.line,
|
|
235
|
-
);
|
|
236
|
-
} else {
|
|
237
|
-
const catalogMatch = catalogHeader.exec(mergedWorkspaceManifest);
|
|
238
|
-
const catalogContents = mergedWorkspaceManifest.slice(
|
|
239
|
-
catalogMatch.index + catalogMatch[0].length,
|
|
240
|
-
);
|
|
241
|
-
const nextCatalogMatch = catalogContents.match(
|
|
242
|
-
/^(?: {2})?[^\s#][^:]*:[\t ]*(?:\r?\n|$)/mu,
|
|
243
|
-
);
|
|
244
|
-
const insertionIndex = nextCatalogMatch
|
|
245
|
-
? catalogMatch.index + catalogMatch[0].length + nextCatalogMatch.index
|
|
246
|
-
: mergedWorkspaceManifest.length;
|
|
247
|
-
|
|
248
|
-
mergedWorkspaceManifest = `${mergedWorkspaceManifest.slice(0, insertionIndex)}${entry.line}${lineEnding}${mergedWorkspaceManifest.slice(insertionIndex)}`;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return mergedWorkspaceManifest;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function mergeYamlListEntries(
|
|
258
|
-
baseWorkspaceManifest,
|
|
259
|
-
templateWorkspaceManifest,
|
|
260
|
-
sectionName,
|
|
261
|
-
) {
|
|
262
|
-
const templateEntries = getYamlListEntries(templateWorkspaceManifest, sectionName);
|
|
263
|
-
if (templateEntries.length === 0) {
|
|
264
|
-
return baseWorkspaceManifest;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const lineEnding = baseWorkspaceManifest.includes('\r\n') ? '\r\n' : '\n';
|
|
268
|
-
const lines = baseWorkspaceManifest.split(LINE_BREAK_PATTERN);
|
|
269
|
-
const sectionIndex = lines.indexOf(`${sectionName}:`);
|
|
270
|
-
|
|
271
|
-
if (sectionIndex === -1) {
|
|
272
|
-
return `${baseWorkspaceManifest.trimEnd()}${lineEnding}${sectionName}:${lineEnding}${templateEntries.join(lineEnding)}${lineEnding}`;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
let sectionEndIndex = sectionIndex + 1;
|
|
276
|
-
while (
|
|
277
|
-
sectionEndIndex < lines.length &&
|
|
278
|
-
!TOP_LEVEL_SECTION_PATTERN.test(lines[sectionEndIndex])
|
|
279
|
-
) {
|
|
280
|
-
sectionEndIndex += 1;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const existingEntries = new Set(
|
|
284
|
-
lines
|
|
285
|
-
.slice(sectionIndex + 1, sectionEndIndex)
|
|
286
|
-
.filter((line) => INDENTED_LIST_ENTRY_PATTERN.test(line)),
|
|
287
|
-
);
|
|
288
|
-
const missingEntries = templateEntries.filter((entry) => !existingEntries.has(entry));
|
|
289
|
-
|
|
290
|
-
lines.splice(sectionEndIndex, 0, ...missingEntries);
|
|
291
|
-
return lines.join(lineEnding);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function mergeWorkspaceManifest(templateWorkspaceManifestPath, workspaceManifestPath) {
|
|
295
|
-
const baseWorkspaceManifest = fs.readFileSync(workspaceManifestPath, 'utf8');
|
|
296
|
-
const templateWorkspaceManifest = fs.readFileSync(
|
|
297
|
-
templateWorkspaceManifestPath,
|
|
298
|
-
'utf8',
|
|
299
|
-
);
|
|
300
|
-
const catalogMergedWorkspaceManifest = mergeCatalogEntries(
|
|
301
|
-
baseWorkspaceManifest,
|
|
302
|
-
templateWorkspaceManifest,
|
|
303
|
-
);
|
|
304
|
-
const mergedWorkspaceManifest = mergeYamlListEntries(
|
|
305
|
-
catalogMergedWorkspaceManifest,
|
|
306
|
-
templateWorkspaceManifest,
|
|
307
|
-
'packages',
|
|
308
|
-
);
|
|
309
|
-
const workspaceManifestWithTemplatePackages = mergeYamlListEntries(
|
|
310
|
-
mergedWorkspaceManifest,
|
|
311
|
-
templateWorkspaceManifest,
|
|
312
|
-
'onlyBuiltDependencies',
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
fs.writeFileSync(workspaceManifestPath, workspaceManifestWithTemplatePackages);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function addAppTemplate(template, workspaceDirectory) {
|
|
319
|
-
const destinationDirectory = path.join(
|
|
320
|
-
workspaceDirectory,
|
|
321
|
-
template.destinationDirectory,
|
|
322
|
-
);
|
|
323
|
-
fs.mkdirSync(destinationDirectory, { recursive: true });
|
|
324
|
-
|
|
325
|
-
if (fs.readdirSync(destinationDirectory).length > 0) {
|
|
326
|
-
throw new Error(`App destination already exists: ${destinationDirectory}`);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'scaffoldfy-app-'));
|
|
330
|
-
const repositoryDirectory = path.join(temporaryDirectory, 'repository');
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
runGit(
|
|
334
|
-
[
|
|
335
|
-
'clone',
|
|
336
|
-
'--depth',
|
|
337
|
-
'1',
|
|
338
|
-
'--filter=blob:none',
|
|
339
|
-
'--sparse',
|
|
340
|
-
template.repository,
|
|
341
|
-
repositoryDirectory,
|
|
342
|
-
],
|
|
343
|
-
workspaceDirectory,
|
|
344
|
-
);
|
|
345
|
-
runGit(
|
|
346
|
-
[
|
|
347
|
-
'sparse-checkout',
|
|
348
|
-
'set',
|
|
349
|
-
'--skip-checks',
|
|
350
|
-
template.sourceDirectory,
|
|
351
|
-
...TEMPLATE_SOURCE_PATHS,
|
|
352
|
-
],
|
|
353
|
-
repositoryDirectory,
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
const sourceDirectory = path.join(repositoryDirectory, template.sourceDirectory);
|
|
357
|
-
if (!fs.existsSync(sourceDirectory)) {
|
|
358
|
-
throw new Error(
|
|
359
|
-
`Template directory "${template.sourceDirectory}" was not found in ${template.repository}`,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
copyDirectoryContents(sourceDirectory, destinationDirectory);
|
|
364
|
-
overlayDirectoryContents(
|
|
365
|
-
path.join(repositoryDirectory, 'packages'),
|
|
366
|
-
path.join(workspaceDirectory, 'packages'),
|
|
367
|
-
);
|
|
368
|
-
overlayDirectoryContents(
|
|
369
|
-
path.join(repositoryDirectory, 'tooling'),
|
|
370
|
-
path.join(workspaceDirectory, 'tooling'),
|
|
371
|
-
);
|
|
372
|
-
overlayDirectoryContents(
|
|
373
|
-
path.join(repositoryDirectory, '.github', 'workflows'),
|
|
374
|
-
path.join(workspaceDirectory, '.github', 'workflows'),
|
|
375
|
-
);
|
|
376
|
-
mergeRootPackageManifest(template, repositoryDirectory, workspaceDirectory);
|
|
377
|
-
|
|
378
|
-
const templateWorkspaceManifestPath = path.join(
|
|
379
|
-
repositoryDirectory,
|
|
380
|
-
'pnpm-workspace.yaml',
|
|
381
|
-
);
|
|
382
|
-
const workspaceManifestPath = path.join(workspaceDirectory, 'pnpm-workspace.yaml');
|
|
383
|
-
|
|
384
|
-
mergeWorkspaceManifest(templateWorkspaceManifestPath, workspaceManifestPath);
|
|
385
|
-
} finally {
|
|
386
|
-
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function main() {
|
|
391
|
-
const workspaceDirectory = process.cwd();
|
|
392
|
-
const selectedApps = getSelectedApps();
|
|
393
|
-
const selectedTemplates = [...selectedApps].filter((app) => APP_TEMPLATES[app]);
|
|
394
|
-
|
|
395
|
-
if (selectedApps.size === 0) {
|
|
396
|
-
throw new Error('Select at least one workspace app.');
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
ensureEmptyDirectory(workspaceDirectory);
|
|
400
|
-
runGit(['clone', '--depth', '1', BASE_TEMPLATE_REPOSITORY, '.'], workspaceDirectory);
|
|
401
|
-
|
|
402
|
-
for (const app of selectedTemplates) {
|
|
403
|
-
addAppTemplate(APP_TEMPLATES[app], workspaceDirectory);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (selectedTemplates.length > 0) {
|
|
407
|
-
refreshLockfile(workspaceDirectory);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
main();
|