handoff-app 1.0.4 → 1.0.6-canary.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 +12 -0
- package/README.md +93 -57
- package/dist/cli.js +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/init/index.d.ts +5 -0
- package/dist/commands/init/index.js +26 -0
- package/dist/create.d.ts +5 -0
- package/dist/create.js +293 -0
- package/package.json +1 -1
- package/src/app/components/util/index.ts +8 -8
- package/src/app/pages/system/component/[component]/index.tsx +2 -2
- package/src/cli.ts +1 -1
- package/src/commands/index.ts +2 -0
- package/src/commands/init/index.ts +17 -0
- package/src/create.ts +302 -0
- package/templates/default/README.md.tpl +15 -0
- package/templates/default/components/button/Button.tsx.tpl +92 -0
- package/templates/default/components/button/button.js.tpl +62 -0
- package/templates/default/env.tpl +2 -0
- package/templates/default/gitignore.tpl +7 -0
- package/templates/default/handoff.config.js.tpl +181 -0
- package/templates/default/handoff.config.with-examples.js.tpl +182 -0
- package/templates/default/package.json.tpl +20 -0
- package/templates/default/tsconfig.json.tpl +24 -0
package/Changelog.md
CHANGED
|
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
and this project adheres to
|
|
7
7
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## \[1.0.6] - 2026-02-24
|
|
10
|
+
|
|
11
|
+
Version 1.0.4 fixed an issue when running Handoff in subpaths (eg.
|
|
12
|
+
https://mysite.com/folder/handoff). We discovered a minor bug in that fix that
|
|
13
|
+
causes some menu constructs to work poorly. This release fixes that issue.
|
|
14
|
+
|
|
15
|
+
## \[1.0.5] - 2026-02-19
|
|
16
|
+
|
|
17
|
+
This is a minor improvement release that extends the quickstart released in the
|
|
18
|
+
1.0.4. It extends the quickstart to work from the Handoff-CLI when installed
|
|
19
|
+
globally. You can use `handoff-app init` to initialize a new project.
|
|
20
|
+
|
|
9
21
|
## \[1.0.4] - 2026-02-19
|
|
10
22
|
|
|
11
23
|
Bugfixes, UX improvements to the component page, improvements to react docgen,
|
package/README.md
CHANGED
|
@@ -12,19 +12,18 @@ transform tokens into dev ready artifacts.
|
|
|
12
12
|
|
|
13
13
|
## Table of Contents
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- [License](#license)
|
|
15
|
+
* [What Is Handoff?](#what-is-handoff)
|
|
16
|
+
* [How does it work](#how-does-it-work)
|
|
17
|
+
* [Handoff is in Beta!](#handoff-is-in-beta)
|
|
18
|
+
* [Requirements](#requirements)
|
|
19
|
+
* [Get Started](#get-started)
|
|
20
|
+
* [Figma](#figma)
|
|
21
|
+
* [Create a project](#create-a-project)
|
|
22
|
+
* [Test fetch](#test-fetch)
|
|
23
|
+
* [Further Reading](#further-reading)
|
|
24
|
+
* [Maintainers](#maintainers)
|
|
25
|
+
* [Contributing](#contributing)
|
|
26
|
+
* [License](#license)
|
|
28
27
|
|
|
29
28
|
## What Is Handoff?
|
|
30
29
|
|
|
@@ -35,19 +34,19 @@ between design and development.
|
|
|
35
34
|
|
|
36
35
|
Handoff is a collection of 4 javascript tools:
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
* **Figma Token Extraction** - A framework for extracting
|
|
39
38
|
standardized design foundations and components from Figma.
|
|
40
|
-
|
|
39
|
+
* **Transformation Pipeline** - A set of transformers for producing SASS, CSS,
|
|
41
40
|
Style Dictionary, and preview snippets from that data.
|
|
42
|
-
|
|
41
|
+
* **Documentation Web App** - A static, client side, Javascript web app that
|
|
43
42
|
renders live, working previews of your components, tokens and styles.
|
|
44
|
-
|
|
43
|
+
* **Delivery Tools** - Easy build tooling and CI/CD wrapper that allows
|
|
45
44
|
automation to render and ship the various deliverables as needed.
|
|
46
45
|
|
|
47
46
|
## How does it work?
|
|
48
47
|
|
|
49
48
|
Handoff works by extracting design foundations and component data from
|
|
50
|
-
[well-formed Figma libraries](https://www.figma.com/file/IGYfyraLDa0BpVXkxHY2tE/Starter-%5BV2%5D?node-id=0%3A1
|
|
49
|
+
[well-formed Figma libraries](https://www.figma.com/file/IGYfyraLDa0BpVXkxHY2tE/Starter-%5BV2%5D?node-id=0%3A1\&t=iPYW37yDmNkJBt1t-0),
|
|
51
50
|
storing them as JSON, and then transforming them into design tokens. Those
|
|
52
51
|
design tokens are published as SASS and CSS variables.
|
|
53
52
|
|
|
@@ -56,23 +55,23 @@ any site using the [Bootstrap 5](https://getbootstrap.com/) frontend
|
|
|
56
55
|
framework. If you use another framework, or custom CSS, you can easily write
|
|
57
56
|
map files to connect the generated tokens with your site or application.
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
* [Get Started](https://www.handoff.com/docs/quickstart)
|
|
59
|
+
* [Requirements](https://www.handoff.com/docs/overview/requirements)
|
|
60
|
+
* [Integrating Tokens](https://www.handoff.com/docs/tokens/integration)
|
|
61
|
+
* [Customization](https://www.handoff.com/docs/customization)
|
|
63
62
|
|
|
64
63
|
Once Handoff extracts design tokens and variables, it builds a statically
|
|
65
64
|
generated NextJS application that can be published to the web. This asset
|
|
66
65
|
can be hosted on a static webhost (NGINX, s3/Cloudfront, Cloudflare pages
|
|
67
66
|
etc).
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
* [Tokens Overview](https://www.handoff.com/docs/tokens)
|
|
69
|
+
* [Build Site](https://www.handoff.com/docs/tokens/publishing)
|
|
71
70
|
|
|
72
71
|
This pipeline from Figma to the Documentation Web app can be automated via CI/CD
|
|
73
72
|
to provide automatic, up-to-date, easily readable developer documentation.
|
|
74
73
|
|
|
75
|
-
|
|
74
|
+
* [CI/CD Integration](https://www.handoff.com/docs/guide/cicd)
|
|
76
75
|
|
|
77
76
|
## Handoff is in Beta!
|
|
78
77
|
|
|
@@ -82,64 +81,101 @@ a use case that isn't quite met.
|
|
|
82
81
|
|
|
83
82
|
## Requirements
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
* A paid Figma account is required to publish the Figma file library
|
|
85
|
+
* Node 18.17+
|
|
86
|
+
* NPM 8+
|
|
88
87
|
|
|
89
88
|
## Get Started
|
|
90
89
|
|
|
91
90
|
### Figma
|
|
92
91
|
|
|
93
|
-
1. Open the [Handoff Figma starter](https://www.figma.com/file/IGYfyraLDa0BpVXkxHY2tE/Starter-%5BV2%5D?node-id=0%3A1
|
|
92
|
+
1. Open the [Handoff Figma starter](https://www.figma.com/file/IGYfyraLDa0BpVXkxHY2tE/Starter-%5BV2%5D?node-id=0%3A1\&t=iPYW37yDmNkJBt1t-0)
|
|
94
93
|
and duplicate this project to your account
|
|
95
94
|
|
|
96
95
|
2. Publish components to the library
|
|
97
96
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
* Click on the Figma logo at the top left
|
|
98
|
+
* Click on `Libraries`
|
|
99
|
+
* Click on the current file
|
|
100
|
+
* Click publish changes
|
|
102
101
|
|
|
103
102
|
You'll need a developer token if you don't have one already
|
|
104
103
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
* Click on the Figma logo in the top left
|
|
105
|
+
* Go to `Help and Account`
|
|
106
|
+
* Click on `Account Settings`
|
|
107
|
+
* Scroll to `Personal Access Token`
|
|
108
|
+
* Enter a token name and hit enter
|
|
109
|
+
* Note that token for the next steps
|
|
111
110
|
|
|
112
111
|
## Create a project
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
- Create a new directory for your project `mkdir my-new-project`
|
|
116
|
-
- In that folder (`cd my-new-project`), run `handoff-app fetch`
|
|
117
|
-
- Handoff will prompt for your developer token, and the file id of your figma
|
|
118
|
-
file. The file id can be found in the url of the file -
|
|
119
|
-
https://www.figma.com/file/{file_id}/{slug}
|
|
120
|
-
- Start the project `handoff-app start`
|
|
113
|
+
The easiest way to get started is using the `handoff-app init` command to scaffold a new project:
|
|
121
114
|
|
|
122
|
-
|
|
123
|
-
|
|
115
|
+
```bash
|
|
116
|
+
npm install -g handoff-app
|
|
117
|
+
handoff-app init
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This interactive CLI will guide you through:
|
|
121
|
+
|
|
122
|
+
1. **Project name** - Enter a name for your project directory
|
|
123
|
+
2. **Project type** - Choose between:
|
|
124
|
+
* **Project with sample components** - Includes example components to help you get started
|
|
125
|
+
* **Blank project** - Only the essential configuration files
|
|
126
|
+
3. **Figma configuration** - Optionally provide your Figma project ID and developer access token
|
|
127
|
+
|
|
128
|
+
The scaffolding will:
|
|
129
|
+
|
|
130
|
+
* Create a new directory with your project name
|
|
131
|
+
* Generate all necessary configuration files
|
|
132
|
+
* Install dependencies automatically
|
|
133
|
+
|
|
134
|
+
### After scaffolding
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
cd my-handoff-project
|
|
138
|
+
npm run fetch # Fetch design tokens from Figma
|
|
139
|
+
npm run start # Start the documentation site
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This will fetch the latest from your Figma file and boot a demo site at http://localhost:3000
|
|
143
|
+
|
|
144
|
+
### Alternative: Global installation
|
|
145
|
+
|
|
146
|
+
You can also install handoff-app globally:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm install -g handoff-app
|
|
150
|
+
create-handoff-app
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Or manually set up a project:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
mkdir my-new-project && cd my-new-project
|
|
157
|
+
handoff-app fetch # Will prompt for Figma credentials
|
|
158
|
+
handoff-app start
|
|
159
|
+
```
|
|
124
160
|
|
|
125
161
|
## Test Fetch
|
|
126
162
|
|
|
127
|
-
|
|
128
|
-
|
|
163
|
+
* Now go back to your Figma file and change a button color
|
|
164
|
+
* Republish the changes to the library. Click on the publish button from the main
|
|
129
165
|
dropdown. You'll see a list of changes that have been made. Clicking publish
|
|
130
166
|
will make those changes available to handoff.
|
|
131
|
-
|
|
167
|
+
* Back in your project, open a new terminal tab and type `npm run fetch`
|
|
132
168
|
|
|
133
169
|
Once that runs, your browser should update with the new colors.
|
|
134
170
|
|
|
135
171
|
## Further Reading
|
|
136
172
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
173
|
+
* [Configure your project](https://www.handoff.com/docs/customization)
|
|
174
|
+
* [Customize the content](https://www.handoff.com/docs/customization/content)
|
|
175
|
+
* [Integrate tokens with your project](https://www.handoff.com/docs/tokens/integration)
|
|
176
|
+
* [Build to Static Assets](https://www.handoff.com/docs/tokens/publishing)
|
|
177
|
+
* [Integrate with Github Actions CI/CD](https://www.handoff.com/docs/infrastructure/github/)
|
|
178
|
+
* [Integrate with Bitbucket Pipelines CI/CD](https://www.handoff.com/docs/infrastructure/bitbucket/)
|
|
143
179
|
|
|
144
180
|
## Maintainers
|
|
145
181
|
|
package/dist/cli.js
CHANGED
package/dist/commands/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const commands: import("yargs").CommandModule<{}, import("./
|
|
1
|
+
export declare const commands: import("yargs").CommandModule<{}, import("./init").InitArgs>[];
|
package/dist/commands/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const config_1 = __importDefault(require("./eject/config"));
|
|
|
11
11
|
const pages_1 = __importDefault(require("./eject/pages"));
|
|
12
12
|
const theme_1 = __importDefault(require("./eject/theme"));
|
|
13
13
|
const fetch_1 = __importDefault(require("./fetch"));
|
|
14
|
+
const init_1 = __importDefault(require("./init"));
|
|
14
15
|
const component_1 = __importDefault(require("./make/component"));
|
|
15
16
|
const page_1 = __importDefault(require("./make/page"));
|
|
16
17
|
const template_1 = __importDefault(require("./make/template"));
|
|
@@ -25,6 +26,7 @@ exports.commands = [
|
|
|
25
26
|
pages_1.default,
|
|
26
27
|
theme_1.default,
|
|
27
28
|
fetch_1.default,
|
|
29
|
+
init_1.default,
|
|
28
30
|
page_1.default,
|
|
29
31
|
component_1.default,
|
|
30
32
|
template_1.default,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const create_1 = __importDefault(require("../../create"));
|
|
16
|
+
const command = {
|
|
17
|
+
command: 'init',
|
|
18
|
+
describe: 'Initialize a new Handoff project with interactive wizard',
|
|
19
|
+
builder: (yargs) => {
|
|
20
|
+
return yargs;
|
|
21
|
+
},
|
|
22
|
+
handler: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
23
|
+
yield (0, create_1.default)();
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
exports.default = command;
|
package/dist/create.d.ts
ADDED
package/dist/create.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
26
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
27
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
28
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
29
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
30
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
31
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
35
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
|
+
};
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
const p = __importStar(require("@clack/prompts"));
|
|
39
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
/**
|
|
44
|
+
* Get the current handoff-app version from package.json
|
|
45
|
+
*/
|
|
46
|
+
const getVersion = () => {
|
|
47
|
+
try {
|
|
48
|
+
const packageJsonPath = path.resolve(__dirname, '../package.json');
|
|
49
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
50
|
+
return packageJson.version || 'unknown';
|
|
51
|
+
}
|
|
52
|
+
catch (_a) {
|
|
53
|
+
return 'unknown';
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
class CreateError extends Error {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = 'CreateError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if directory exists
|
|
64
|
+
*/
|
|
65
|
+
const directoryExists = (dirPath) => {
|
|
66
|
+
try {
|
|
67
|
+
return fs.existsSync(dirPath);
|
|
68
|
+
}
|
|
69
|
+
catch (_a) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Create directory recursively
|
|
75
|
+
*/
|
|
76
|
+
const createDirectory = (dirPath) => {
|
|
77
|
+
try {
|
|
78
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new CreateError(`Failed to create directory: ${error}`);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Get mandatory templates based on whether examples are included
|
|
86
|
+
* Uses different handoff config templates for blank vs. with-examples projects
|
|
87
|
+
*/
|
|
88
|
+
const getMandatoryTemplates = (includeExamples) => [
|
|
89
|
+
{ source: 'package.json.tpl', destination: 'package.json' },
|
|
90
|
+
{
|
|
91
|
+
source: includeExamples ? 'handoff.config.with-examples.js.tpl' : 'handoff.config.js.tpl',
|
|
92
|
+
destination: 'handoff.config.js'
|
|
93
|
+
},
|
|
94
|
+
{ source: 'tsconfig.json.tpl', destination: 'tsconfig.json' },
|
|
95
|
+
{ source: 'gitignore.tpl', destination: '.gitignore' },
|
|
96
|
+
{ source: 'env.tpl', destination: '.env' },
|
|
97
|
+
{ source: 'README.md.tpl', destination: 'README.md' },
|
|
98
|
+
];
|
|
99
|
+
// Example component files (only included when user opts for examples)
|
|
100
|
+
const exampleTemplates = [
|
|
101
|
+
{ source: 'components/button/Button.tsx.tpl', destination: 'components/button/Button.tsx', isExample: true },
|
|
102
|
+
{ source: 'components/button/button.js.tpl', destination: 'components/button/button.js', isExample: true },
|
|
103
|
+
];
|
|
104
|
+
/**
|
|
105
|
+
* Render a template string by replacing placeholders with values
|
|
106
|
+
*/
|
|
107
|
+
const renderTemplate = (content, data) => {
|
|
108
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
109
|
+
if (!(key in data)) {
|
|
110
|
+
console.warn(chalk_1.default.yellow(`Warning: Unknown template placeholder '{{${key}}}'`));
|
|
111
|
+
}
|
|
112
|
+
return data[key] || '';
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Execute npm install
|
|
117
|
+
*/
|
|
118
|
+
const runNpmInstall = (projectPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
const spinner = p.spinner();
|
|
120
|
+
spinner.start('Installing dependencies...');
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
var _a;
|
|
123
|
+
const npmProcess = (0, child_process_1.spawn)('npm', ['install'], {
|
|
124
|
+
cwd: projectPath,
|
|
125
|
+
stdio: 'pipe',
|
|
126
|
+
shell: true,
|
|
127
|
+
});
|
|
128
|
+
let stderr = '';
|
|
129
|
+
(_a = npmProcess.stderr) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
130
|
+
stderr += data.toString();
|
|
131
|
+
});
|
|
132
|
+
npmProcess.on('close', (code) => {
|
|
133
|
+
if (code === 0) {
|
|
134
|
+
spinner.stop('Dependencies installed successfully');
|
|
135
|
+
resolve();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
spinner.stop('Failed to install dependencies');
|
|
139
|
+
reject(new CreateError(`npm install failed: ${stderr}`));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
npmProcess.on('error', (error) => {
|
|
143
|
+
spinner.stop('Failed to install dependencies');
|
|
144
|
+
reject(new CreateError(`Failed to run npm install: ${error.message}`));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* Validate project name
|
|
150
|
+
*/
|
|
151
|
+
const validateProjectName = (name) => {
|
|
152
|
+
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
|
|
153
|
+
return validNameRegex.test(name) && name.length > 0;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Main create function
|
|
157
|
+
*/
|
|
158
|
+
const create = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
159
|
+
const version = getVersion();
|
|
160
|
+
p.intro(chalk_1.default.bgCyan.black(` Handoff App Creator (v${version}) `));
|
|
161
|
+
try {
|
|
162
|
+
// Get project name
|
|
163
|
+
const projectName = yield p.text({
|
|
164
|
+
message: 'Enter project name:',
|
|
165
|
+
placeholder: 'my-handoff-project',
|
|
166
|
+
validate: (value) => {
|
|
167
|
+
if (!value.trim())
|
|
168
|
+
return 'Project name is required';
|
|
169
|
+
if (!validateProjectName(value)) {
|
|
170
|
+
return 'Invalid project name. Use only letters, numbers, hyphens, and underscores.';
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
if (p.isCancel(projectName)) {
|
|
175
|
+
p.cancel('Project creation cancelled.');
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
const projectPath = path.resolve(process.cwd(), projectName);
|
|
179
|
+
// Check if directory exists
|
|
180
|
+
if (directoryExists(projectPath)) {
|
|
181
|
+
const overwrite = yield p.confirm({
|
|
182
|
+
message: `Directory "${projectName}" already exists. Do you want to overwrite it?`,
|
|
183
|
+
initialValue: false,
|
|
184
|
+
});
|
|
185
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
186
|
+
p.cancel('Project creation cancelled.');
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
// Remove existing directory
|
|
190
|
+
p.log.info('Removing existing directory...');
|
|
191
|
+
fs.rmSync(projectPath, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
// Create project directory
|
|
194
|
+
p.log.info('Creating project directory...');
|
|
195
|
+
createDirectory(projectPath);
|
|
196
|
+
// Ask if user wants a blank project or one with examples
|
|
197
|
+
p.log.step(chalk_1.default.blue('Project Configuration'));
|
|
198
|
+
const projectType = yield p.select({
|
|
199
|
+
message: 'What type of project would you like to create?',
|
|
200
|
+
options: [
|
|
201
|
+
{ value: 'with-examples', label: 'Project with sample components', hint: 'Includes example components to get started' },
|
|
202
|
+
{ value: 'blank', label: 'Blank project', hint: 'Only mandatory files, no examples' },
|
|
203
|
+
],
|
|
204
|
+
initialValue: 'with-examples',
|
|
205
|
+
});
|
|
206
|
+
if (p.isCancel(projectType)) {
|
|
207
|
+
p.cancel('Project creation cancelled.');
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
const includeExamples = projectType === 'with-examples';
|
|
211
|
+
// Get Figma project ID
|
|
212
|
+
p.log.step(chalk_1.default.blue('Figma Configuration'));
|
|
213
|
+
const figmaProjectId = yield p.text({
|
|
214
|
+
message: 'Enter Figma project ID:',
|
|
215
|
+
placeholder: 'Optional - can be configured later',
|
|
216
|
+
});
|
|
217
|
+
if (p.isCancel(figmaProjectId)) {
|
|
218
|
+
p.cancel('Project creation cancelled.');
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
// Get Figma access token
|
|
222
|
+
let figmaAccessToken = '';
|
|
223
|
+
if (figmaProjectId) {
|
|
224
|
+
figmaAccessToken = yield p.password({
|
|
225
|
+
message: 'Enter Figma developer access token:',
|
|
226
|
+
});
|
|
227
|
+
if (p.isCancel(figmaAccessToken)) {
|
|
228
|
+
p.cancel('Project creation cancelled.');
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const options = {
|
|
233
|
+
projectName: projectName,
|
|
234
|
+
figmaProjectId: (figmaProjectId || '').trim(),
|
|
235
|
+
figmaAccessToken: (figmaAccessToken || '').trim(),
|
|
236
|
+
};
|
|
237
|
+
// Create files
|
|
238
|
+
const spinner = p.spinner();
|
|
239
|
+
spinner.start('Creating project files...');
|
|
240
|
+
const templateDir = path.resolve(__dirname, '../templates/default');
|
|
241
|
+
if (!fs.existsSync(templateDir)) {
|
|
242
|
+
throw new CreateError(`Template directory not found at ${templateDir}. This may indicate a packaging issue.`);
|
|
243
|
+
}
|
|
244
|
+
// Combine mandatory templates with example templates based on user choice
|
|
245
|
+
const templates = [
|
|
246
|
+
...getMandatoryTemplates(includeExamples),
|
|
247
|
+
...(includeExamples ? exampleTemplates : []),
|
|
248
|
+
];
|
|
249
|
+
try {
|
|
250
|
+
for (const template of templates) {
|
|
251
|
+
const templatePath = path.join(templateDir, template.source);
|
|
252
|
+
const destinationPath = path.join(projectPath, template.destination);
|
|
253
|
+
try {
|
|
254
|
+
const content = fs.readFileSync(templatePath, 'utf8');
|
|
255
|
+
const rendered = renderTemplate(content, options);
|
|
256
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
257
|
+
fs.writeFileSync(destinationPath, rendered);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
throw new CreateError(`Failed to create file ${template.destination}: ${err}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
spinner.stop('Project files created');
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
spinner.stop('Failed to create project files');
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
// Install dependencies
|
|
270
|
+
yield runNpmInstall(projectPath);
|
|
271
|
+
// Success message
|
|
272
|
+
const nextSteps = [`cd ${projectName}`];
|
|
273
|
+
if (figmaProjectId && figmaAccessToken) {
|
|
274
|
+
nextSteps.push('npm run fetch');
|
|
275
|
+
}
|
|
276
|
+
nextSteps.push('npm run start');
|
|
277
|
+
p.note(nextSteps.join('\n'), 'Next steps');
|
|
278
|
+
const outroMessage = includeExamples
|
|
279
|
+
? 'Project created successfully with sample components! First, fetch your Figma data (optional), then start the documentation site.'
|
|
280
|
+
: 'Blank project created successfully! First, fetch your Figma data, then start the documentation site.';
|
|
281
|
+
p.outro(chalk_1.default.green(outroMessage));
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (error instanceof CreateError) {
|
|
285
|
+
p.cancel(chalk_1.default.red(`Error: ${error.message}`));
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
p.cancel(chalk_1.default.red(`Unexpected error: ${error}`));
|
|
289
|
+
}
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
exports.default = create;
|
package/package.json
CHANGED
|
@@ -309,7 +309,7 @@ const staticBuildComponentMenu = (type?: boolean | string) => {
|
|
|
309
309
|
if (component.name) {
|
|
310
310
|
title = component.name;
|
|
311
311
|
}
|
|
312
|
-
menuGroup.menu.push({ path: `${basePath}
|
|
312
|
+
menuGroup.menu.push({ path: `${basePath}system/component/${component.id}`, title });
|
|
313
313
|
});
|
|
314
314
|
// sort the menu group by name alphabetical
|
|
315
315
|
menuGroup.menu = menuGroup.menu.sort((a, b) => a.title.localeCompare(b.title));
|
|
@@ -326,19 +326,19 @@ const staticBuildTokensMenu = () => {
|
|
|
326
326
|
const menu = [
|
|
327
327
|
{
|
|
328
328
|
title: `Foundations`,
|
|
329
|
-
path: `${basePath}
|
|
329
|
+
path: `${basePath}system/tokens/foundations`,
|
|
330
330
|
menu: [
|
|
331
331
|
{
|
|
332
332
|
title: `Colors`,
|
|
333
|
-
path: `${basePath}
|
|
333
|
+
path: `${basePath}system/tokens/foundations/colors`,
|
|
334
334
|
},
|
|
335
335
|
{
|
|
336
336
|
title: `Effects`,
|
|
337
|
-
path: `${basePath}
|
|
337
|
+
path: `${basePath}system/tokens/foundations/effects`,
|
|
338
338
|
},
|
|
339
339
|
{
|
|
340
340
|
title: `Typography`,
|
|
341
|
-
path: `${basePath}
|
|
341
|
+
path: `${basePath}system/tokens/foundations/typography`,
|
|
342
342
|
},
|
|
343
343
|
],
|
|
344
344
|
},
|
|
@@ -358,14 +358,14 @@ const staticBuildTokensMenu = () => {
|
|
|
358
358
|
if (component.name) {
|
|
359
359
|
title = component.name;
|
|
360
360
|
}
|
|
361
|
-
componentMenuItems.push({ path: `${basePath}
|
|
361
|
+
componentMenuItems.push({ path: `${basePath}system/tokens/components/${component.id}`, title });
|
|
362
362
|
});
|
|
363
363
|
});
|
|
364
364
|
|
|
365
365
|
if (componentMenuItems.length > 0) {
|
|
366
366
|
menu.push({
|
|
367
367
|
title: `Components`,
|
|
368
|
-
path: `${basePath}
|
|
368
|
+
path: `${basePath}system/tokens/components`,
|
|
369
369
|
menu: componentMenuItems,
|
|
370
370
|
});
|
|
371
371
|
}
|
|
@@ -378,7 +378,7 @@ const staticBuildTokenMenu = () => {
|
|
|
378
378
|
|
|
379
379
|
let subSections = {
|
|
380
380
|
title: 'Tokens',
|
|
381
|
-
path: `${basePath}
|
|
381
|
+
path: `${basePath}system/tokens`,
|
|
382
382
|
menu: [],
|
|
383
383
|
};
|
|
384
384
|
const tokens = getTokens();
|