@pixpilot/scaffoldfy-configs 0.31.0 → 0.33.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
@@ -1,5 +1,17 @@
1
1
  # @pixpilot/scaffoldfy-configs
2
2
 
3
+ ## 0.33.0
4
+
5
+ ### Minor Changes
6
+
7
+ - update app templates and enhance setup script
8
+
9
+ ## 0.32.0
10
+
11
+ ### Minor Changes
12
+
13
+ - support JSONC template files
14
+
3
15
  ## 0.31.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -104,6 +104,16 @@ Usage:
104
104
  npx @pixpilot/scaffoldfy@latest --config https://unpkg.com/@pixpilot/scaffoldfy-configs@latest/update-root-package-json/scaffoldfy.json
105
105
  ```
106
106
 
107
+ ### workspace-generator
108
+
109
+ Generate workspace with apps and packages
110
+
111
+ Usage:
112
+
113
+ ```sh
114
+ npx @pixpilot/scaffoldfy@latest --config https://unpkg.com/@pixpilot/scaffoldfy-configs@latest/workspace-generator/scaffoldfy.jsonc
115
+ ```
116
+
107
117
  ### workspace-initializer
108
118
 
109
119
  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.31.0",
4
+ "version": "0.33.0",
5
5
  "author": "PixPilot <m.doaie@hotmail.com>",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -20,14 +20,17 @@
20
20
  "files": [
21
21
  "*"
22
22
  ],
23
+ "dependencies": {
24
+ "jsonc-parser": "^3.3.1"
25
+ },
23
26
  "devDependencies": {
24
27
  "@types/node": "^22.18.11",
25
28
  "eslint": "^9.38.0",
26
29
  "@internal/eslint-config": "0.3.0",
30
+ "@internal/tsdown-config": "0.1.0",
27
31
  "@internal/prettier-config": "0.0.1",
28
32
  "@internal/vitest-config": "0.1.0",
29
- "@internal/tsdown-config": "0.1.0",
30
- "@pixpilot/scaffoldfy": "0.51.0"
33
+ "@pixpilot/scaffoldfy": "0.52.0"
31
34
  },
32
35
  "prettier": "@internal/prettier-config",
33
36
  "publishConfig": {
@@ -1,6 +1,7 @@
1
1
  import { readdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { parse, printParseErrorCode } from 'jsonc-parser';
4
5
 
5
6
  const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
6
7
  const readmePath = path.join(packageDirectory, 'README.md');
@@ -16,7 +17,10 @@ async function findConfigPaths(directory) {
16
17
  if (entry.isDirectory() && !ignoredDirectories.has(entry.name)) {
17
18
  return findConfigPaths(path.join(directory, entry.name));
18
19
  }
19
- if (entry.isFile() && entry.name === 'scaffoldfy.json') {
20
+ if (
21
+ entry.isFile() &&
22
+ (entry.name === 'scaffoldfy.json' || entry.name === 'scaffoldfy.jsonc')
23
+ ) {
20
24
  return [path.join(directory, entry.name)];
21
25
  }
22
26
  return [];
@@ -31,10 +35,21 @@ async function readTemplate(configPath) {
31
35
  .relative(packageDirectory, path.dirname(configPath))
32
36
  .split(path.sep)
33
37
  .join('/');
34
- const relativeConfigPath = relativeDirectory
35
- ? `${relativeDirectory}/scaffoldfy.json`
36
- : 'scaffoldfy.json';
37
- const config = JSON.parse(await readFile(configPath, 'utf8'));
38
+ const relativeConfigPath = path
39
+ .relative(packageDirectory, configPath)
40
+ .split(path.sep)
41
+ .join('/');
42
+ const errors = [];
43
+ const config = parse(await readFile(configPath, 'utf8'), errors, {
44
+ allowTrailingComma: true,
45
+ });
46
+
47
+ if (errors.length > 0) {
48
+ const details = errors
49
+ .map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`)
50
+ .join(', ');
51
+ throw new Error(`${relativeConfigPath} is not valid JSON or JSONC: ${details}`);
52
+ }
38
53
 
39
54
  if (typeof config.description !== 'string' || !config.description.trim()) {
40
55
  throw new Error(`${relativeConfigPath} has no description`);
@@ -0,0 +1,152 @@
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
+
31
+ fs.mkdirSync(binDirectory, { recursive: true });
32
+ fs.writeFileSync(
33
+ fakeGitPath,
34
+ [
35
+ "'use strict';",
36
+ "const fs = require('node:fs');",
37
+ "const Module = require('node:module');",
38
+ "const path = require('node:path');",
39
+ 'function applyGitOperation(command, args) {',
40
+ " if (command === 'clone') {",
41
+ ' const destination = args.at(-1);',
42
+ " if (destination === '.') {",
43
+ " fs.writeFileSync(path.join(process.cwd(), 'base-template.txt'), 'base');",
44
+ ' } else {',
45
+ ' fs.mkdirSync(destination, { recursive: true });',
46
+ ' }',
47
+ " } else if (command === 'sparse-checkout') {",
48
+ ' const sourceDirectory = args.at(-1);',
49
+ ' const templateDirectory = path.join(process.cwd(), sourceDirectory);',
50
+ ' fs.mkdirSync(templateDirectory, { recursive: true });',
51
+ " fs.writeFileSync(path.join(templateDirectory, 'template.txt'), sourceDirectory);",
52
+ '}',
53
+ '}',
54
+ "if (process.env.SCAFFOLDFY_FAKE_GIT_MODE === 'preload') {",
55
+ ' const resolveFilename = Module._resolveFilename;',
56
+ ' Module._resolveFilename = function resolveFakeGit(request, ...args) {',
57
+ ' const command = path.basename(request);',
58
+ " if (command === 'clone' || command === 'sparse-checkout') {",
59
+ ' applyGitOperation(command, process.argv.slice(2));',
60
+ " return path.join(__dirname, 'noop.cjs');",
61
+ ' }',
62
+ ' return resolveFilename.call(this, request, ...args);',
63
+ ' };',
64
+ '} else {',
65
+ ' const [command, ...args] = process.argv.slice(2);',
66
+ ' applyGitOperation(command, args);',
67
+ '}',
68
+ ].join('\n'),
69
+ );
70
+
71
+ if (process.platform === 'win32') {
72
+ fs.writeFileSync(path.join(binDirectory, 'noop.cjs'), "'use strict';\n");
73
+ fs.copyFileSync(process.execPath, gitCommandPath);
74
+ } else {
75
+ fs.writeFileSync(
76
+ gitCommandPath,
77
+ `#!${process.execPath}\nrequire('./fake-git.cjs');\n`,
78
+ );
79
+ fs.chmodSync(gitCommandPath, 0o755);
80
+ }
81
+
82
+ return binDirectory;
83
+ }
84
+
85
+ function runSetup(workspaceApps: string): string {
86
+ const testDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'scaffoldfy-workspace-'));
87
+ const workspaceDirectory = path.join(testDirectory, 'workspace');
88
+ temporaryDirectories.push(testDirectory);
89
+ fs.mkdirSync(workspaceDirectory);
90
+ const gitBinDirectory = createFakeGit(testDirectory);
91
+ const fakeGitPath = path.join(gitBinDirectory, 'fake-git.cjs');
92
+
93
+ execFileSync(process.execPath, [SCRIPT_PATH], {
94
+ cwd: workspaceDirectory,
95
+ stdio: 'pipe',
96
+ env: {
97
+ ...process.env,
98
+ PATH: `${gitBinDirectory}${path.delimiter}${process.env.PATH ?? ''}`,
99
+ ...(process.platform === 'win32' && {
100
+ NODE_OPTIONS: `--require=${fakeGitPath} ${process.env.NODE_OPTIONS ?? ''}`,
101
+ Path: `${gitBinDirectory}${path.delimiter}${process.env.Path ?? ''}`,
102
+ SCAFFOLDFY_FAKE_GIT_MODE: 'preload',
103
+ }),
104
+ WORKSPACE_APPS: workspaceApps,
105
+ },
106
+ });
107
+
108
+ return workspaceDirectory;
109
+ }
110
+
111
+ describe('workspace-generator setup script', () => {
112
+ it('should clone the base template when only Library is selected', () => {
113
+ const workspaceDirectory = runSetup('library');
114
+
115
+ expect(
116
+ fs.readFileSync(path.join(workspaceDirectory, 'base-template.txt'), 'utf8'),
117
+ ).toBe('base');
118
+ expect(fs.existsSync(path.join(workspaceDirectory, 'apps'))).toBe(false);
119
+ });
120
+
121
+ it('should add every selected app to the base workspace', () => {
122
+ const workspaceDirectory = runSetup(
123
+ 'library,nextjs-cloudflare,chrome-extension,expo',
124
+ );
125
+
126
+ expect(
127
+ fs.readFileSync(path.join(workspaceDirectory, 'base-template.txt'), 'utf8'),
128
+ ).toBe('base');
129
+ expect(
130
+ fs.readFileSync(
131
+ path.join(workspaceDirectory, 'apps', 'web', 'template.txt'),
132
+ 'utf8',
133
+ ),
134
+ ).toBe('apps/web');
135
+ expect(
136
+ fs.readFileSync(
137
+ path.join(workspaceDirectory, 'apps', 'chrome-extension', 'template.txt'),
138
+ 'utf8',
139
+ ),
140
+ ).toBe('apps/chrome-extension');
141
+ expect(
142
+ fs.readFileSync(
143
+ path.join(workspaceDirectory, 'apps', 'expo', 'template.txt'),
144
+ 'utf8',
145
+ ),
146
+ ).toBe('apps/expo');
147
+ });
148
+
149
+ it('should fail when no workspace app is selected', () => {
150
+ expect(() => runSetup('')).toThrow('Select at least one workspace app.');
151
+ });
152
+ });
@@ -14,8 +14,8 @@
14
14
  "value": "library"
15
15
  },
16
16
  {
17
- "name": "Next.js",
18
- "value": "nextjs"
17
+ "name": "Next.js Cloudflare",
18
+ "value": "nextjs-cloudflare"
19
19
  },
20
20
  {
21
21
  "name": "Chrome Extension",
@@ -26,34 +26,40 @@
26
26
  "value": "expo"
27
27
  }
28
28
  ]
29
- },
30
- {
31
- "id": "nextjsVisualTest",
32
- "type": "input",
33
- "message": "Next.js visual-test value:",
34
- "enabled": {
35
- "type": "condition",
36
- "value": "workspaceApps.includes('nextjs')"
37
- }
38
- },
39
- {
40
- "id": "expoVisualTest",
41
- "type": "input",
42
- "message": "Expo visual-test value:",
43
- "enabled": {
44
- "type": "condition",
45
- "value": "workspaceApps.includes('expo')"
46
- }
47
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
+ // },
48
50
  ],
49
51
  "tasks": [
50
52
  {
51
- "id": "clone-monorepo-template",
52
- "name": "Clone Monorepo Template",
53
- "description": "Clone the monorepo template",
54
- "type": "exec",
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",
55
57
  "config": {
56
- "command": "git clone https://github.com/pixpilot/pnpm-turbo-monorepo-template.git"
58
+ "file": "./scripts/setup-workspace.cjs",
59
+ "runtime": "node",
60
+ "parameters": {
61
+ "WORKSPACE_APPS": "{{workspaceApps}}"
62
+ }
57
63
  }
58
64
  }
59
65
  ]
@@ -0,0 +1,121 @@
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
+ },
19
+ 'chrome-extension': {
20
+ repository: 'https://github.com/ccpu/chrome-extension-monorepo-template.git',
21
+ sourceDirectory: 'apps/chrome-extension',
22
+ destinationDirectory: 'apps/chrome-extension',
23
+ },
24
+ expo: {
25
+ repository: 'https://github.com/ccpu/full-stack-starter.git',
26
+ sourceDirectory: 'apps/expo',
27
+ destinationDirectory: 'apps/expo',
28
+ },
29
+ };
30
+
31
+ function runGit(args, cwd) {
32
+ execFileSync('git', args, { cwd, stdio: 'inherit' });
33
+ }
34
+
35
+ function getSelectedApps() {
36
+ return new Set(
37
+ (process.env.WORKSPACE_APPS ?? '')
38
+ .split(',')
39
+ .map((app) => app.trim())
40
+ .filter(Boolean),
41
+ );
42
+ }
43
+
44
+ function ensureEmptyDirectory(directory) {
45
+ if (fs.readdirSync(directory).length > 0) {
46
+ throw new Error(`Workspace directory must be empty: ${directory}`);
47
+ }
48
+ }
49
+
50
+ function copyDirectoryContents(sourceDirectory, destinationDirectory) {
51
+ for (const entry of fs.readdirSync(sourceDirectory, { withFileTypes: true })) {
52
+ fs.cpSync(
53
+ path.join(sourceDirectory, entry.name),
54
+ path.join(destinationDirectory, entry.name),
55
+ { recursive: entry.isDirectory(), errorOnExist: true, force: false },
56
+ );
57
+ }
58
+ }
59
+
60
+ function addAppTemplate(template, workspaceDirectory) {
61
+ const destinationDirectory = path.join(
62
+ workspaceDirectory,
63
+ template.destinationDirectory,
64
+ );
65
+ fs.mkdirSync(destinationDirectory, { recursive: true });
66
+
67
+ if (fs.readdirSync(destinationDirectory).length > 0) {
68
+ throw new Error(`App destination already exists: ${destinationDirectory}`);
69
+ }
70
+
71
+ const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'scaffoldfy-app-'));
72
+ const repositoryDirectory = path.join(temporaryDirectory, 'repository');
73
+
74
+ try {
75
+ runGit(
76
+ [
77
+ 'clone',
78
+ '--depth',
79
+ '1',
80
+ '--filter=blob:none',
81
+ '--sparse',
82
+ template.repository,
83
+ repositoryDirectory,
84
+ ],
85
+ workspaceDirectory,
86
+ );
87
+ runGit(['sparse-checkout', 'set', template.sourceDirectory], repositoryDirectory);
88
+
89
+ const sourceDirectory = path.join(repositoryDirectory, template.sourceDirectory);
90
+ if (!fs.existsSync(sourceDirectory)) {
91
+ throw new Error(
92
+ `Template directory "${template.sourceDirectory}" was not found in ${template.repository}`,
93
+ );
94
+ }
95
+
96
+ copyDirectoryContents(sourceDirectory, destinationDirectory);
97
+ } finally {
98
+ fs.rmSync(temporaryDirectory, { recursive: true, force: true });
99
+ }
100
+ }
101
+
102
+ function main() {
103
+ const workspaceDirectory = process.cwd();
104
+ const selectedApps = getSelectedApps();
105
+
106
+ if (selectedApps.size === 0) {
107
+ throw new Error('Select at least one workspace app.');
108
+ }
109
+
110
+ ensureEmptyDirectory(workspaceDirectory);
111
+ runGit(['clone', '--depth', '1', BASE_TEMPLATE_REPOSITORY, '.'], workspaceDirectory);
112
+
113
+ for (const app of selectedApps) {
114
+ const template = APP_TEMPLATES[app];
115
+ if (template) {
116
+ addAppTemplate(template, workspaceDirectory);
117
+ }
118
+ }
119
+ }
120
+
121
+ main();