Skip to content

Commit 5d9bad8

Browse files
authored
feat: add npm template support (#118)
1 parent e38493f commit 5d9bad8

5 files changed

Lines changed: 454 additions & 15 deletions

File tree

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,65 @@ A shared package for create-rspack, create-rsbuild, create-rspress and create-rs
1717
npm add create-rstack -D
1818
```
1919

20+
## Features
21+
22+
### NPM Template Support
23+
24+
`create-rstack` supports using npm packages as templates, allowing users to create projects from custom templates published to npm.
25+
26+
#### Usage
27+
28+
```bash
29+
# Using npm package name
30+
npm create rsbuild@latest my-project -- --template my-template-package
31+
32+
# Using scoped package
33+
npm create rsbuild@latest my-project -- --template @scope/template-package
34+
35+
# Using explicit npm: prefix
36+
npm create rsbuild@latest my-project -- --template npm:my-template-package
37+
38+
# With specific version
39+
npm create rsbuild@latest my-project -- --template my-template-package --template-version 1.2.3
40+
```
41+
42+
#### Template Package Structure
43+
44+
Your npm template package should have one of the following structures:
45+
46+
```
47+
my-template-package/
48+
├── template/ # Preferred
49+
│ ├── package.json
50+
│ └── src/
51+
├── templates/
52+
│ └── app/ # Alternative
53+
└── (root) # Fallback
54+
├── package.json
55+
└── src/
56+
```
57+
58+
#### Caching Strategy
59+
60+
- Templates with `latest` version are always re-installed to ensure the latest version
61+
- Specific versions are cached in `.temp-templates/` for faster reuse
62+
63+
#### API
64+
65+
```typescript
66+
import {
67+
isNpmTemplate,
68+
resolveCustomTemplate,
69+
resolveNpmTemplate,
70+
} from 'create-rstack';
71+
72+
// Check if template input is an npm package
73+
if (isNpmTemplate(templateInput)) {
74+
// Resolve npm template to local path
75+
const templatePath = resolveCustomTemplate(templateInput, version);
76+
}
77+
```
78+
2079
## Examples
2180

2281
| Project | Link |

src/index.ts

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,21 @@ import deepmerge from 'deepmerge';
1919
import minimist from 'minimist';
2020
import { color, logger } from 'rslog';
2121
import { x } from 'tinyexec';
22+
import { isNpmTemplate, resolveCustomTemplate } from './template-manager.js';
2223

2324
const __filename = fileURLToPath(import.meta.url);
2425
const __dirname = dirname(__filename);
2526

2627
export { autocomplete, groupMultiselect, multiselect, select, text };
2728

29+
// Export npm template utilities
30+
export {
31+
isNpmTemplate,
32+
resolveCustomTemplate,
33+
resolveNpmTemplate,
34+
sanitizeCacheKey,
35+
} from './template-manager.js';
36+
2837
function cancelAndExit() {
2938
cancel('Operation cancelled.');
3039
process.exit(0);
@@ -113,6 +122,8 @@ export type Argv = {
113122
skill?: string | string[];
114123
packageName?: string;
115124
'package-name'?: string;
125+
templateVersion?: string;
126+
'template-version'?: string;
116127
};
117128

118129
export const BUILTIN_TOOLS = ['eslint', 'rslint', 'biome', 'prettier'];
@@ -163,6 +174,7 @@ function logHelpMessage(
163174
--tools <tool> add additional tools, comma separated
164175
${skillsOptionLine} --override override files in target directory
165176
--packageName <name> specify the package name
177+
--template-version <ver> specify the npm template version
166178
167179
Available templates:
168180
${templates.join(', ')}
@@ -341,6 +353,11 @@ const parseArgv = (processArgv: string[]) => {
341353
argv.packageName = argv['package-name'];
342354
}
343355

356+
// Handle template-version alias
357+
if (argv['template-version']) {
358+
argv.templateVersion = argv['template-version'];
359+
}
360+
344361
return argv;
345362
};
346363

@@ -488,6 +505,27 @@ async function runSkillCommand(skills: ExtraSkill[], cwd: string) {
488505
installationTaskLog.success(`Installed ${skillNoun} ${skillLabel}`);
489506
}
490507

508+
function logNextStepsAndOutro(
509+
noteInformation: string[] | undefined,
510+
targetDir: string,
511+
packageManager: string,
512+
) {
513+
const nextSteps = noteInformation
514+
? noteInformation
515+
: [
516+
`1. ${color.cyan(`cd ${targetDir}`)}`,
517+
`2. ${color.cyan('git init')} ${color.dim('(optional)')}`,
518+
`3. ${color.cyan(`${packageManager} install`)}`,
519+
`4. ${color.cyan(`${packageManager} run dev`)}`,
520+
];
521+
522+
if (nextSteps.length) {
523+
note(nextSteps.map((step) => color.reset(step)).join('\n'), 'Next steps');
524+
}
525+
526+
outro('All set, happy coding!');
527+
}
528+
491529
export async function create({
492530
name,
493531
root,
@@ -592,6 +630,35 @@ export async function create({
592630
}
593631

594632
const templateName = await getTemplateName(argv);
633+
634+
const srcFolder = path.join(root, `template-${templateName}`);
635+
636+
// Handle npm template: only when the local template doesn't exist
637+
// and the template input looks like an npm package
638+
if (
639+
typeof argv.template === 'string' &&
640+
isNpmTemplate(argv.template) &&
641+
!fs.existsSync(srcFolder)
642+
) {
643+
const templateVersion = argv.templateVersion ?? argv['template-version'];
644+
const templatePath = resolveCustomTemplate(argv.template, templateVersion, {
645+
cacheDir: root,
646+
});
647+
648+
// Copy npm template directly to distFolder
649+
copyFolder({
650+
from: templatePath,
651+
to: distFolder,
652+
version,
653+
packageName,
654+
templateParameters,
655+
skipFiles,
656+
});
657+
658+
logNextStepsAndOutro(noteInformation, targetDir, packageManager);
659+
return;
660+
}
661+
595662
const tools = await getTools(argv, extraTools, templateName);
596663
const skills = await getSkills(
597664
argv,
@@ -601,7 +668,6 @@ export async function create({
601668
multiselect,
602669
);
603670

604-
const srcFolder = path.join(root, `template-${templateName}`);
605671
const commonFolder = path.join(root, 'template-common');
606672

607673
if (!fs.existsSync(srcFolder)) {
@@ -734,20 +800,7 @@ export async function create({
734800
);
735801
}
736802

737-
const nextSteps = noteInformation
738-
? noteInformation
739-
: [
740-
`1. ${color.cyan(`cd ${targetDir}`)}`,
741-
`2. ${color.cyan('git init')} ${color.dim('(optional)')}`,
742-
`3. ${color.cyan(`${packageManager} install`)}`,
743-
`4. ${color.cyan(`${packageManager} run dev`)}`,
744-
];
745-
746-
if (nextSteps.length) {
747-
note(nextSteps.map((step) => color.reset(step)).join('\n'), 'Next steps');
748-
}
749-
750-
outro('All set, happy coding!');
803+
logNextStepsAndOutro(noteInformation, targetDir, packageManager);
751804
}
752805

753806
function sortObjectKeys(obj: Record<string, unknown>) {

0 commit comments

Comments
 (0)