create-gasket-app 7.4.11 → 7.4.13
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/EXAMPLES.md +48 -0
- package/README.md +46 -0
- package/lib/commands/create.js +97 -31
- package/lib/index.d.ts +15 -1
- package/lib/index.js +5 -2
- package/lib/internal.d.ts +10 -0
- package/lib/scaffold/actions/copy-template.js +22 -0
- package/lib/scaffold/actions/customize-template.js +92 -0
- package/lib/scaffold/actions/install-template-deps.js +61 -0
- package/lib/scaffold/actions/load-template.js +110 -0
- package/lib/scaffold/create-context.js +8 -3
- package/package.json +19 -20
package/EXAMPLES.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Create Gasket App Examples
|
|
2
|
+
|
|
3
|
+
This document provides working examples for interfaces and utilities from the `create-gasket-app` package.
|
|
4
|
+
|
|
5
|
+
## Primary Usage (CLI)
|
|
6
|
+
|
|
7
|
+
The main interface for `create-gasket-app` is the command line:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Basic app creation
|
|
11
|
+
npx create-gasket-app@latest my-app
|
|
12
|
+
|
|
13
|
+
# With presets
|
|
14
|
+
npx create-gasket-app@latest my-app --presets @gasket/preset-nextjs
|
|
15
|
+
|
|
16
|
+
# With package manager selection
|
|
17
|
+
npx create-gasket-app@latest my-app --package-manager pnpm
|
|
18
|
+
|
|
19
|
+
# With configuration
|
|
20
|
+
npx create-gasket-app@latest my-app --config '{"typescript": true}'
|
|
21
|
+
|
|
22
|
+
# With config file
|
|
23
|
+
npx create-gasket-app@latest my-app --config-file ./create-config.json
|
|
24
|
+
|
|
25
|
+
# Skip prompts (for CI)
|
|
26
|
+
npx create-gasket-app@latest my-app --presets @gasket/preset-nextjs --no-prompts
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Templates
|
|
30
|
+
|
|
31
|
+
Templates provide complete Gasket applications ready to use:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Use an npm template package
|
|
35
|
+
npx create-gasket-app@latest my-app --template @gasket/template-nextjs-pages-js
|
|
36
|
+
|
|
37
|
+
# Use a versioned template
|
|
38
|
+
npx create-gasket-app@latest my-app --template @gasket/template-api@^2.0.0
|
|
39
|
+
|
|
40
|
+
# Use a tagged template
|
|
41
|
+
npx create-gasket-app@latest my-app --template @gasket/template-nextjs@beta
|
|
42
|
+
|
|
43
|
+
# Use a local template during development
|
|
44
|
+
npx create-gasket-app@latest my-app --template-path ./my-gasket-template
|
|
45
|
+
|
|
46
|
+
# Use local template with absolute path
|
|
47
|
+
npx create-gasket-app@latest my-app --template-path /path/to/my-template
|
|
48
|
+
```
|
package/README.md
CHANGED
|
@@ -43,6 +43,10 @@ Options:
|
|
|
43
43
|
-p, --presets [presets] Initial Gasket preset(s) to use.
|
|
44
44
|
Can be set as short name with version (e.g. --presets nextjs@^1.0.0)
|
|
45
45
|
Or other (multiple) custom presets (e.g. --presets my-gasket-preset@1.0.0.beta-1,nextjs@^1.0.0)
|
|
46
|
+
--template [template] Selects which template you would like to use during
|
|
47
|
+
installation. (e.g. --template @gasket/template-nextjs-pages-js)
|
|
48
|
+
--template-path [template-path] (INTERNAL) Path to a local template package. Can be absolute
|
|
49
|
+
or relative to the current working directory.
|
|
46
50
|
--package-manager [package-manager] Selects which package manager you would like to use during
|
|
47
51
|
installation. (e.g. --package-manager yarn)
|
|
48
52
|
-r, --require [require] Require module(s) before Gasket is initialized
|
|
@@ -51,6 +55,48 @@ Options:
|
|
|
51
55
|
-h, --help display help for command
|
|
52
56
|
```
|
|
53
57
|
|
|
58
|
+
#### Templates
|
|
59
|
+
|
|
60
|
+
Templates provide a complete Gasket application ready to use, unlike presets which build apps through a plugin-based creation process.
|
|
61
|
+
|
|
62
|
+
##### Using Templates
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Use an official template from npm
|
|
66
|
+
npx create-gasket-app@latest my-app --template @gasket/template-nextjs-pages-js
|
|
67
|
+
|
|
68
|
+
# Use a versioned template
|
|
69
|
+
npx create-gasket-app@latest my-app --template @gasket/template-api@^2.0.0
|
|
70
|
+
|
|
71
|
+
# Use a local template (development)
|
|
72
|
+
npx create-gasket-app@latest my-app --template-path ./path/to/my-template
|
|
73
|
+
|
|
74
|
+
# Use a template with tag
|
|
75
|
+
npx create-gasket-app@latest my-app --template @gasket/template-nextjs-pages-js@beta
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
##### Template Structure
|
|
79
|
+
|
|
80
|
+
Templates are npm packages that follow this structure:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
@gasket/template-example/
|
|
84
|
+
├── package.json
|
|
85
|
+
├── template/
|
|
86
|
+
│ ├── package.json # App's package.json
|
|
87
|
+
│ ├── gasket.js # App's gasket config
|
|
88
|
+
│ ├── src/ # App source files
|
|
89
|
+
│ └── ... # Other app files
|
|
90
|
+
└── README.md
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
When using templates:
|
|
94
|
+
- The entire `template/` directory is copied to your new app
|
|
95
|
+
- Template dependencies are installed with `npm ci`
|
|
96
|
+
- No preset processing or plugin creation flows are run
|
|
97
|
+
|
|
98
|
+
**Note:** Templates currently require npm as they come with `package-lock.json` files.
|
|
99
|
+
|
|
54
100
|
#### Package Managers
|
|
55
101
|
|
|
56
102
|
With `create-gasket-app`, you can choose either [npm] or [yarn] as the package
|
package/lib/commands/create.js
CHANGED
|
@@ -6,6 +6,10 @@ import { rm } from 'fs/promises';
|
|
|
6
6
|
import { makeGasket } from '@gasket/core';
|
|
7
7
|
import globalPrompts from '../scaffold/actions/global-prompts.js';
|
|
8
8
|
import loadPreset from '../scaffold/actions/load-preset.js';
|
|
9
|
+
import loadTemplate from '../scaffold/actions/load-template.js';
|
|
10
|
+
import copyTemplate from '../scaffold/actions/copy-template.js';
|
|
11
|
+
import customizeTemplate from '../scaffold/actions/customize-template.js';
|
|
12
|
+
import installTemplateDep from '../scaffold/actions/install-template-deps.js';
|
|
9
13
|
import presetPromptHooks from '../scaffold/actions/preset-prompt-hooks.js';
|
|
10
14
|
import presetConfigHooks from '../scaffold/actions/preset-config-hooks.js';
|
|
11
15
|
import promptHooks from '../scaffold/actions/prompt-hooks.js';
|
|
@@ -45,6 +49,17 @@ const createCommand = {
|
|
|
45
49
|
Or other (multiple) custom presets (e.g. --presets my-gasket-preset@1.0.0.beta-1,nextjs@^1.0.0)`,
|
|
46
50
|
parse: commasToArray
|
|
47
51
|
},
|
|
52
|
+
{
|
|
53
|
+
name: 'template',
|
|
54
|
+
description: `Selects which template you would like to use during
|
|
55
|
+
installation. (e.g. --template @gasket/template-nextjs-pages-js)`
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'template-path',
|
|
59
|
+
description: `(INTERNAL) Path to a local template package. Can be absolute
|
|
60
|
+
or relative to the current working directory.`,
|
|
61
|
+
hidden: true
|
|
62
|
+
},
|
|
48
63
|
{
|
|
49
64
|
name: 'package-manager',
|
|
50
65
|
description: `Selects which package manager you would like to use during
|
|
@@ -90,6 +105,86 @@ const createCommand = {
|
|
|
90
105
|
]
|
|
91
106
|
};
|
|
92
107
|
|
|
108
|
+
/**
|
|
109
|
+
* validateOption - We require at least one of the options to be provided
|
|
110
|
+
* @param {import('../internal.js').PartialCreateContext} context
|
|
111
|
+
* @returns {import('../internal.js').PartialCreateContext}
|
|
112
|
+
*/
|
|
113
|
+
function validateOptions(context) {
|
|
114
|
+
const hasOption = (context) => Boolean(context.template || context.templatePath || context.rawPresets.length || context.localPresets.length);
|
|
115
|
+
|
|
116
|
+
if (!hasOption(context)) {
|
|
117
|
+
console.warn('Warning: At least one of the options is required: --template, --template-path, --presets, --preset-path');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return context;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* handleTemplate - Handles the template creation
|
|
126
|
+
* @param {import('../internal.js').PartialCreateContext} context
|
|
127
|
+
* @returns {Promise<void>}
|
|
128
|
+
*/
|
|
129
|
+
async function handleTemplate(context) {
|
|
130
|
+
await mkDir({ context });
|
|
131
|
+
await loadTemplate({ context });
|
|
132
|
+
await copyTemplate({ context });
|
|
133
|
+
await customizeTemplate({ context });
|
|
134
|
+
await installTemplateDep({ context });
|
|
135
|
+
printReport({ context });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* handlePresets - Handles the preset creation
|
|
141
|
+
* @param {import('../internal.js').PartialCreateContext} context
|
|
142
|
+
* @returns {Promise<void>}
|
|
143
|
+
*/
|
|
144
|
+
async function handlePresets(context) {
|
|
145
|
+
await loadPreset({ context });
|
|
146
|
+
await globalPrompts({ context });
|
|
147
|
+
|
|
148
|
+
const presetGasket = makeGasket({
|
|
149
|
+
plugins: context.presets
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await presetPromptHooks({ gasket: presetGasket, context });
|
|
153
|
+
await presetConfigHooks({ gasket: presetGasket, context });
|
|
154
|
+
|
|
155
|
+
const pluginGasket = makeGasket({
|
|
156
|
+
...context.presetConfig,
|
|
157
|
+
plugins: context.presetConfig.plugins.concat(context.presets)
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await promptHooks({ gasket: pluginGasket, context });
|
|
161
|
+
await mkDir({ context });
|
|
162
|
+
await setupPkg({ context });
|
|
163
|
+
await createHooks({ gasket: pluginGasket, context });
|
|
164
|
+
await writePkg({ context });
|
|
165
|
+
await generateFiles({ context });
|
|
166
|
+
await writeGasketConfig({ context });
|
|
167
|
+
await installModules({ context });
|
|
168
|
+
await linkModules({ context });
|
|
169
|
+
await postCreateHooks({ gasket: pluginGasket, context });
|
|
170
|
+
if (context.tmpDir) await rm(context.tmpDir, { recursive: true });
|
|
171
|
+
printReport({ context });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* handleCreate - Handles the create creation
|
|
176
|
+
* @param {import('../internal.js').PartialCreateContext} context
|
|
177
|
+
* @returns {Promise<void>}
|
|
178
|
+
*/
|
|
179
|
+
async function handleCreate(context) {
|
|
180
|
+
const { template, templatePath } = context;
|
|
181
|
+
if (template || templatePath) {
|
|
182
|
+
await handleTemplate(context);
|
|
183
|
+
} else {
|
|
184
|
+
await handlePresets(context);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
93
188
|
/**
|
|
94
189
|
* createCommand action
|
|
95
190
|
* @type {import('../index.js').createCommandAction}
|
|
@@ -98,39 +193,10 @@ createCommand.action = async function run(appname, options, command) {
|
|
|
98
193
|
// eslint-disable-next-line no-process-env
|
|
99
194
|
process.env.GASKET_ENV = 'create';
|
|
100
195
|
const context = makeCreateContext([appname], options);
|
|
101
|
-
|
|
196
|
+
validateOptions(context);
|
|
102
197
|
|
|
103
198
|
try {
|
|
104
|
-
await
|
|
105
|
-
|
|
106
|
-
if (rawPresets.length || localPresets.length) {
|
|
107
|
-
await loadPreset({ context });
|
|
108
|
-
|
|
109
|
-
const presetGasket = makeGasket({
|
|
110
|
-
plugins: context.presets
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
await presetPromptHooks({ gasket: presetGasket, context });
|
|
114
|
-
await presetConfigHooks({ gasket: presetGasket, context });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const pluginGasket = makeGasket({
|
|
118
|
-
...context.presetConfig,
|
|
119
|
-
plugins: context.presetConfig.plugins.concat(context.presets)
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
await promptHooks({ gasket: pluginGasket, context });
|
|
123
|
-
await mkDir({ context });
|
|
124
|
-
await setupPkg({ context });
|
|
125
|
-
await createHooks({ gasket: pluginGasket, context });
|
|
126
|
-
await writePkg({ context });
|
|
127
|
-
await generateFiles({ context });
|
|
128
|
-
await writeGasketConfig({ context });
|
|
129
|
-
await installModules({ context });
|
|
130
|
-
await linkModules({ context });
|
|
131
|
-
await postCreateHooks({ gasket: pluginGasket, context });
|
|
132
|
-
if (context.tmpDir) await rm(context.tmpDir, { recursive: true });
|
|
133
|
-
printReport({ context });
|
|
199
|
+
await handleCreate(context);
|
|
134
200
|
} catch (err) {
|
|
135
201
|
// eslint-disable-next-line no-console
|
|
136
202
|
console.error(chalk.red('Exiting with errors.'));
|
package/lib/index.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ interface CommandOption {
|
|
|
47
47
|
|
|
48
48
|
export interface CreateCommandOptions {
|
|
49
49
|
presets?: string[];
|
|
50
|
+
template?: string;
|
|
51
|
+
templatePath?: string;
|
|
50
52
|
npmLink?: string[];
|
|
51
53
|
presetPath?: string[];
|
|
52
54
|
packageManager?: string;
|
|
@@ -459,6 +461,18 @@ export interface CreateContext {
|
|
|
459
461
|
/** temporary directory */
|
|
460
462
|
tmpDir: string;
|
|
461
463
|
|
|
464
|
+
/** template to use for app creation */
|
|
465
|
+
template?: string;
|
|
466
|
+
|
|
467
|
+
/** path to local template package */
|
|
468
|
+
templatePath?: string;
|
|
469
|
+
|
|
470
|
+
/** resolved template directory path */
|
|
471
|
+
templateDir?: string;
|
|
472
|
+
|
|
473
|
+
/** resolved template name for display */
|
|
474
|
+
templateName?: string;
|
|
475
|
+
|
|
462
476
|
/** Default to object w/empty plugins array to be populated by `presetConfig` hook */
|
|
463
477
|
presetConfig: GasketConfigDefinition;
|
|
464
478
|
|
|
@@ -505,7 +519,7 @@ export interface CreateContext {
|
|
|
505
519
|
|
|
506
520
|
/** Use to add content to the README.md */
|
|
507
521
|
readme: Readme;
|
|
508
|
-
|
|
522
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-new
|
|
509
523
|
constructor(initContext?: Partial<CreateContext>);
|
|
510
524
|
runWith(plugin: Plugin): CreateContext;
|
|
511
525
|
/** Flag indicating if typescript is enabled */
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,10 @@ const gasketBin = program
|
|
|
17
17
|
.version(pkg.version)
|
|
18
18
|
.addHelpText('beforeAll', logo);
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The main entry point for the create-gasket-app CLI.
|
|
22
|
+
* @returns {Promise<Command>} promise
|
|
23
|
+
*/
|
|
21
24
|
async function main() {
|
|
22
25
|
// Suppress deprecation warning for punycode in node 22
|
|
23
26
|
process.noDeprecation = true;
|
|
@@ -30,4 +33,4 @@ async function main() {
|
|
|
30
33
|
return await gasketBin.parseAsync();
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
main();
|
|
36
|
+
void main();
|
package/lib/internal.d.ts
CHANGED
|
@@ -104,6 +104,7 @@ export function promptForTestPlugin(
|
|
|
104
104
|
export function allowExtantOverwriting(context: PartialCreateContext, prompt: CreatePrompt): Promise<void>;
|
|
105
105
|
export function globalPrompts(params: { context: PartialCreateContext }): Promise<void>;
|
|
106
106
|
export function loadPresets(params: { context: PartialCreateContext }): Promise<void>;
|
|
107
|
+
|
|
107
108
|
export function presetPromptHooks(params: { gasket?: Gasket; context: PartialCreateContext }): Promise<void>;
|
|
108
109
|
export function presetConfigHooks(params: { gasket?: Gasket; context: PartialCreateContext }): Promise<void>;
|
|
109
110
|
export function promptHooks(params: { gasket?: Gasket; context: PartialCreateContext }): Promise<void>;
|
|
@@ -124,6 +125,15 @@ export type Descriptior = {
|
|
|
124
125
|
from: string;
|
|
125
126
|
overrides?: string;
|
|
126
127
|
}
|
|
128
|
+
|
|
129
|
+
/** Templates */
|
|
130
|
+
export function loadTemplate(params: { context: PartialCreateContext }): Promise<void>;
|
|
131
|
+
export function copyTemplate(params: { context: PartialCreateContext }): Promise<void>;
|
|
132
|
+
export function customizeTemplate(params: { context: PartialCreateContext }): Promise<void>;
|
|
133
|
+
export function installTemplateDeps(params: { context: PartialCreateContext }): Promise<void>;
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
/** Presets */
|
|
127
137
|
export function generateFiles({ context, spinner }: { context: CreateContext, spinner: Ora }): Promise<void>;
|
|
128
138
|
export function performGenerate(context: CreateContext, descriptors: Descriptior[]): boolean[];
|
|
129
139
|
export function getDescriptors(context: CreateContext): Promise<Descriptior[]>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { withSpinner } from '../with-spinner.js';
|
|
3
|
+
import { cp, readdir } from 'fs/promises';
|
|
4
|
+
|
|
5
|
+
const EXCLUDED_FILES = ['node_modules'];
|
|
6
|
+
|
|
7
|
+
/** @type {import('../../internal.d.ts').copyTemplate} */
|
|
8
|
+
async function copyTemplate({ context }) {
|
|
9
|
+
if (!context.templateDir) return;
|
|
10
|
+
|
|
11
|
+
const entries = await readdir(context.templateDir, { withFileTypes: true });
|
|
12
|
+
const filesToCopy = entries.filter(entry => !EXCLUDED_FILES.includes(entry.name));
|
|
13
|
+
|
|
14
|
+
for (const entry of filesToCopy) {
|
|
15
|
+
const sourcePath = path.join(context.templateDir, entry.name);
|
|
16
|
+
const destPath = path.join(context.dest, entry.name);
|
|
17
|
+
await cp(sourcePath, destPath, { recursive: true });
|
|
18
|
+
context.generatedFiles.add(path.relative(context.cwd, destPath));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default withSpinner('Copy template files', copyTemplate);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { withSpinner } from '../with-spinner.js';
|
|
3
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
4
|
+
|
|
5
|
+
/** @type {import('../../internal.d.ts').customizeTemplate} */
|
|
6
|
+
async function customizeTemplate({ context }) {
|
|
7
|
+
if (!context.templateDir || !context.appName) return;
|
|
8
|
+
|
|
9
|
+
await Promise.all([
|
|
10
|
+
updatePackageJson(context),
|
|
11
|
+
updateReadme(context),
|
|
12
|
+
updateTemplateFiles(context)
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Updates package.json name field with app name
|
|
18
|
+
* @param {object} context - Gasket context
|
|
19
|
+
* @returns {Promise<void>}
|
|
20
|
+
*/
|
|
21
|
+
async function updatePackageJson(context) {
|
|
22
|
+
const packageJsonPath = path.join(context.dest, 'package.json');
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const content = await readFile(packageJsonPath, 'utf8');
|
|
26
|
+
const packageJson = JSON.parse(content);
|
|
27
|
+
|
|
28
|
+
packageJson.name = context.appName;
|
|
29
|
+
|
|
30
|
+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// Ignore if package.json doesn't exist or is malformed
|
|
33
|
+
if (error.code !== 'ENOENT') {
|
|
34
|
+
console.warn(`Warning: Could not update package.json: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Updates README.md with app name placeholders replaced
|
|
41
|
+
* @param {object} context - Gasket context
|
|
42
|
+
* @returns {Promise<void>}
|
|
43
|
+
*/
|
|
44
|
+
async function updateReadme(context) {
|
|
45
|
+
const readmePath = path.join(context.dest, 'README.md');
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const content = await readFile(readmePath, 'utf8');
|
|
49
|
+
|
|
50
|
+
// Replace all {appName} placeholders with actual app name
|
|
51
|
+
const updatedContent = content.replace(/{{{appName}}}/g, context.appName);
|
|
52
|
+
|
|
53
|
+
await writeFile(readmePath, updatedContent);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Ignore if README.md doesn't exist
|
|
56
|
+
if (error.code !== 'ENOENT') {
|
|
57
|
+
console.warn(`Warning: Could not update README.md: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Updates template files with app name placeholders replaced
|
|
64
|
+
* @param {object} context - Gasket context
|
|
65
|
+
* @returns {Promise<void>}
|
|
66
|
+
*/
|
|
67
|
+
async function updateTemplateFiles(context) {
|
|
68
|
+
const templateFilePaths = [
|
|
69
|
+
path.join(context.dest, 'pages/index.tsx'),
|
|
70
|
+
path.join(context.dest, 'app/page.tsx')
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const updatePromises = templateFilePaths.map(async (filePath) => {
|
|
74
|
+
try {
|
|
75
|
+
const content = await readFile(filePath, 'utf8');
|
|
76
|
+
|
|
77
|
+
// Replace all {appName} placeholders with actual app name
|
|
78
|
+
const updatedContent = content.replace(/{{{appName}}}/g, context.appName);
|
|
79
|
+
|
|
80
|
+
await writeFile(filePath, updatedContent);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
// Ignore if file doesn't exist (not all templates have these files)
|
|
83
|
+
if (error.code !== 'ENOENT') {
|
|
84
|
+
console.warn(`Warning: Could not update ${filePath}: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await Promise.all(updatePromises);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default withSpinner('Customize template files', customizeTemplate);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { withSpinner } from '../with-spinner.js';
|
|
2
|
+
import { PackageManager } from '@gasket/utils';
|
|
3
|
+
import { rm } from 'fs/promises';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Safely cleans up temporary directory
|
|
7
|
+
* @param {string} tmpDir - Temporary directory path
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
async function cleanupTempDir(tmpDir) {
|
|
11
|
+
if (!tmpDir) return;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await rm(tmpDir, { recursive: true });
|
|
15
|
+
} catch {
|
|
16
|
+
// Ignore cleanup errors
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Checks if an error is related to peer dependencies
|
|
22
|
+
* @param {Error & { stderr?: string }} error - The error to check
|
|
23
|
+
* @returns {boolean} True if error is peer dep related
|
|
24
|
+
*/
|
|
25
|
+
function isPeerDependencyError(error) {
|
|
26
|
+
const errorMessage = (error.stderr || error.message || '').toLowerCase();
|
|
27
|
+
return errorMessage.includes('peer dep') ||
|
|
28
|
+
errorMessage.includes('peerinvalid') ||
|
|
29
|
+
errorMessage.includes('peer dependencies') ||
|
|
30
|
+
errorMessage.includes('eresolve');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @type {import('../../internal.d.ts').installTemplateDeps} */
|
|
34
|
+
async function installTemplateDeps({ context }) {
|
|
35
|
+
const destPkgManager = new PackageManager({
|
|
36
|
+
packageManager: 'npm',
|
|
37
|
+
dest: context.dest
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await destPkgManager.exec('ci');
|
|
42
|
+
await cleanupTempDir(context.tmpDir);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (isPeerDependencyError(error)) {
|
|
45
|
+
console.warn('Peer dependency conflict detected, retrying with --legacy-peer-deps...');
|
|
46
|
+
try {
|
|
47
|
+
await destPkgManager.exec('ci', ['--legacy-peer-deps']);
|
|
48
|
+
await cleanupTempDir(context.tmpDir);
|
|
49
|
+
context.messages.push('Dependencies were installed with --legacy-peer-deps due to confilct');
|
|
50
|
+
} catch (retryError) {
|
|
51
|
+
await cleanupTempDir(context.tmpDir);
|
|
52
|
+
throw new Error(`Failed to install dependencies even with --legacy-peer-deps: ${retryError.message}`);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
await cleanupTempDir(context.tmpDir);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default withSpinner('Install template dependencies', installTemplateDeps);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import { withSpinner } from '../with-spinner.js';
|
|
4
|
+
import { PackageManager } from '@gasket/utils';
|
|
5
|
+
import { mkdtemp, rm } from 'fs/promises';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parses template string into name and version components
|
|
9
|
+
* @param {string} template - Template string (e.g., "@gasket/template-api@1.0.0")
|
|
10
|
+
* @returns {{ name: string, version: string }} Parsed template name and version
|
|
11
|
+
*/
|
|
12
|
+
function parseTemplateNameAndVersion(template) {
|
|
13
|
+
// capture name and version, excluding `@`
|
|
14
|
+
const reNameVersion = /(^.[^@]+)[@](.+$)/i;
|
|
15
|
+
const [
|
|
16
|
+
name,
|
|
17
|
+
version = 'latest'
|
|
18
|
+
] = template.split(reNameVersion).filter(Boolean);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
name,
|
|
22
|
+
version
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Safely cleans up temporary directory
|
|
28
|
+
* @param {string} tmpDir - Temporary directory path
|
|
29
|
+
* @returns {Promise<void>}
|
|
30
|
+
*/
|
|
31
|
+
async function cleanupTempDir(tmpDir) {
|
|
32
|
+
if (!tmpDir) return;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await rm(tmpDir, { recursive: true });
|
|
36
|
+
} catch {
|
|
37
|
+
// Ignore cleanup errors
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Downloads and installs a remote template package
|
|
43
|
+
* @param {string} template - Template name with optional version
|
|
44
|
+
* @param {string} appName - App name for temp directory
|
|
45
|
+
* @returns {Promise<{ templateDir: string, templateName: string, tmpDir: string }>} Template installation details
|
|
46
|
+
*/
|
|
47
|
+
async function installRemoteTemplate(template, appName) {
|
|
48
|
+
const tmpDir = await mkdtemp(path.join(os.tmpdir(), `gasket-template-${appName}`));
|
|
49
|
+
const modPath = path.join(tmpDir, 'node_modules');
|
|
50
|
+
const pkgManager = new PackageManager({
|
|
51
|
+
packageManager: 'npm',
|
|
52
|
+
dest: tmpDir
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const { name, version } = parseTemplateNameAndVersion(template);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await pkgManager.exec('install', [`${name}@${version}`]);
|
|
59
|
+
return {
|
|
60
|
+
templateDir: path.join(modPath, name, 'template'),
|
|
61
|
+
templateName: `${name}@${version}`,
|
|
62
|
+
tmpDir
|
|
63
|
+
};
|
|
64
|
+
} catch (err) {
|
|
65
|
+
await cleanupTempDir(tmpDir);
|
|
66
|
+
|
|
67
|
+
const errorMessage = err.stderr || err.message;
|
|
68
|
+
if (err.stderr?.includes('is not in this registry')) {
|
|
69
|
+
const message = `Template not found in registry: ${name}@${version}. ` +
|
|
70
|
+
'Use npm_config_registry=<registry> to use privately scoped templates.';
|
|
71
|
+
throw new Error(message);
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`Failed to install template ${name}@${version}: ${errorMessage}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Gets template directory and name based on context
|
|
79
|
+
* @param {object} context - Gasket creation context
|
|
80
|
+
* @returns {Promise<{ templateDir: string, templateName: string, tmpDir?: string }>} Template details
|
|
81
|
+
*/
|
|
82
|
+
async function getTemplateDetails(context) {
|
|
83
|
+
if (context.templatePath) {
|
|
84
|
+
return {
|
|
85
|
+
templateDir: path.resolve(context.templatePath, 'template'),
|
|
86
|
+
templateName: `local template at ${path.resolve(context.templatePath, 'template')}`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = await installRemoteTemplate(context.template, context.appName);
|
|
91
|
+
return {
|
|
92
|
+
templateDir: result.templateDir,
|
|
93
|
+
templateName: result.templateName,
|
|
94
|
+
tmpDir: result.tmpDir
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @type {import('../../internal.d.ts').loadTemplate} */
|
|
99
|
+
async function loadTemplate({ context }) {
|
|
100
|
+
if (!context.template && !context.templatePath) return;
|
|
101
|
+
|
|
102
|
+
const { templateDir, templateName, tmpDir } = await getTemplateDetails(context);
|
|
103
|
+
|
|
104
|
+
// Store template information in context for other actions to use
|
|
105
|
+
context.templateDir = templateDir;
|
|
106
|
+
context.templateName = templateName;
|
|
107
|
+
context.tmpDir = tmpDir;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default withSpinner('Loading template', loadTemplate);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* eslint-disable max-
|
|
1
|
+
/* eslint-disable max-statements */
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { readConfig } from '../scaffold/utils.js';
|
|
@@ -53,9 +53,10 @@ function makeCreateRuntime(context, source) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
|
|
57
|
+
const flatten = (acc, values) => {
|
|
57
58
|
return acc.concat(values);
|
|
58
|
-
}
|
|
59
|
+
};
|
|
59
60
|
|
|
60
61
|
export class CreateContext {
|
|
61
62
|
constructor(initContext = {}) {
|
|
@@ -73,6 +74,8 @@ export function makeCreateContext(argv = [], options = {}) {
|
|
|
73
74
|
const appName = argv[0] || 'templated-app';
|
|
74
75
|
const {
|
|
75
76
|
presets = [],
|
|
77
|
+
template,
|
|
78
|
+
templatePath,
|
|
76
79
|
npmLink = [],
|
|
77
80
|
presetPath = [],
|
|
78
81
|
packageManager,
|
|
@@ -106,6 +109,8 @@ export function makeCreateContext(argv = [], options = {}) {
|
|
|
106
109
|
pkgLinks,
|
|
107
110
|
localPresets,
|
|
108
111
|
rawPresets,
|
|
112
|
+
template,
|
|
113
|
+
templatePath,
|
|
109
114
|
messages: [],
|
|
110
115
|
warnings: [],
|
|
111
116
|
errors: [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-gasket-app",
|
|
3
|
-
"version": "7.4.
|
|
3
|
+
"version": "7.4.13",
|
|
4
4
|
"description": "starter pack for creating a gasket app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"main": "lib/index.js",
|
|
10
10
|
"types": "lib/index.d.ts",
|
|
11
11
|
"files": [
|
|
12
|
-
"lib"
|
|
12
|
+
"lib",
|
|
13
|
+
"EXAMPLES.md"
|
|
13
14
|
],
|
|
14
15
|
"repository": "godaddy/gasket.git",
|
|
15
16
|
"publishConfig": {
|
|
@@ -31,33 +32,31 @@
|
|
|
31
32
|
"mkdirp": "^1.0.4",
|
|
32
33
|
"ora": "^3.4.0",
|
|
33
34
|
"semver": "^7.7.1",
|
|
34
|
-
"@gasket/
|
|
35
|
-
"@gasket/plugin-
|
|
36
|
-
"@gasket/plugin-
|
|
37
|
-
"@gasket/
|
|
38
|
-
"@gasket/plugin-
|
|
39
|
-
"@gasket/plugin-git": "^7.4.
|
|
40
|
-
"@gasket/plugin-logger": "^7.3.
|
|
41
|
-
"@gasket/
|
|
42
|
-
"@gasket/
|
|
43
|
-
"@gasket/utils": "^7.6.
|
|
35
|
+
"@gasket/core": "^7.6.3",
|
|
36
|
+
"@gasket/plugin-command": "^7.5.3",
|
|
37
|
+
"@gasket/plugin-docs": "^7.4.8",
|
|
38
|
+
"@gasket/plugin-docusaurus": "^7.4.6",
|
|
39
|
+
"@gasket/plugin-dynamic-plugins": "^7.4.2",
|
|
40
|
+
"@gasket/plugin-git": "^7.4.7",
|
|
41
|
+
"@gasket/plugin-logger": "^7.3.8",
|
|
42
|
+
"@gasket/plugin-metadata": "^7.5.4",
|
|
43
|
+
"@gasket/request": "^7.5.4",
|
|
44
|
+
"@gasket/utils": "^7.6.3"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@typescript-eslint/parser": "^8.38.0",
|
|
48
|
+
"@vitest/coverage-v8": "^3.2.0",
|
|
47
49
|
"dotenv": "^16.4.7",
|
|
48
|
-
"
|
|
49
|
-
"@gasket/request": "^7.5.
|
|
50
|
-
},
|
|
51
|
-
"jest": {
|
|
52
|
-
"transform": {}
|
|
50
|
+
"vitest": "^3.2.0",
|
|
51
|
+
"@gasket/request": "^7.5.4"
|
|
53
52
|
},
|
|
54
53
|
"scripts": {
|
|
55
54
|
"lint": "eslint .",
|
|
56
55
|
"lint:fix": "pnpm run lint --fix",
|
|
57
56
|
"posttest": "pnpm run lint && pnpm run typecheck",
|
|
58
|
-
"test": "
|
|
59
|
-
"test:coverage": "
|
|
60
|
-
"test:watch": "
|
|
57
|
+
"test": "vitest run --globals",
|
|
58
|
+
"test:coverage": "vitest run --coverage",
|
|
59
|
+
"test:watch": "vitest",
|
|
61
60
|
"typecheck": "tsc",
|
|
62
61
|
"typecheck:watch": "tsc --watch"
|
|
63
62
|
}
|