@darkoto/gulp-template-cli 1.0.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/LICENSE +21 -0
- package/README.md +171 -0
- package/bin/cli.js +78 -0
- package/helpers.js +128 -0
- package/package.json +30 -0
- package/paths.js +71 -0
- package/setup/tailwind/config.js +112 -0
- package/setup/tailwind/setup.js +395 -0
- package/setup/tailwind/templates/tailwind.config.cdn.js +9 -0
- package/setup/tailwind/templates/tailwind.config.module.js +12 -0
- package/setup/tailwind/templates/tailwind.css +9 -0
- package/setup/tailwind/templates/tailwind.html +37 -0
- package/setup/tailwind/templates/tailwind.js +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [year] [fullname]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# @darkoto/gulp-template-cli
|
|
2
|
+
|
|
3
|
+
A CLI feature installer for [gulp-template](#). Instead of keeping a
|
|
4
|
+
`.template/` folder with setup scripts inside every cloned project,
|
|
5
|
+
the scripts live in a standalone package and copy the files you need
|
|
6
|
+
(configs, gulp tasks, styles, demo pages) directly into your project
|
|
7
|
+
on demand — patching `gulpfile.js` and `head.html` automatically.
|
|
8
|
+
|
|
9
|
+
## Why this exists
|
|
10
|
+
|
|
11
|
+
Normally a template's `.template/` setup folder gets copied along with
|
|
12
|
+
the whole repository into every new project. That causes a few problems:
|
|
13
|
+
|
|
14
|
+
- fixes/updates to the scripts have to be manually propagated to every
|
|
15
|
+
project that was already started from the template;
|
|
16
|
+
- adding a new feature (another tool, another integration) means
|
|
17
|
+
copying a folder into each project separately;
|
|
18
|
+
- dead setup-script code sits in the project repo forever, even after
|
|
19
|
+
the feature has long been installed.
|
|
20
|
+
|
|
21
|
+
`@darkoto/gulp-template-cli` fixes this: the scripts live in one place,
|
|
22
|
+
versioned via npm/semver, while the project only ends up with the
|
|
23
|
+
**results** of running them — actual config files, styles, gulp tasks.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @darkoto/gulp-template-cli --save-dev
|
|
29
|
+
# or with pnpm
|
|
30
|
+
pnpm add -D @darkoto/gulp-template-cli
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> Run the command from the **root of your project** — all paths
|
|
34
|
+
> (`src/`, `gulp/`, `gulpfile.js`, etc.) are resolved relative to
|
|
35
|
+
> `process.cwd()`.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# interactive feature picker
|
|
41
|
+
npx template-add
|
|
42
|
+
|
|
43
|
+
# run a specific feature directly
|
|
44
|
+
npx template-add tailwind
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Available features
|
|
48
|
+
|
|
49
|
+
| Feature | What it does |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `tailwind` | Installs Tailwind CSS (via a pnpm package or CDN), creates the config and stylesheet, adds a gulp task, patches `gulpfile.js` and `head.html`, and copies a demo page |
|
|
52
|
+
|
|
53
|
+
The feature list is generated automatically from the contents of the
|
|
54
|
+
`setup/` folder — new features show up in the CLI without any changes
|
|
55
|
+
to the tool itself (see [Adding a new feature](#adding-a-new-feature)).
|
|
56
|
+
|
|
57
|
+
## How it works
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
gulp-template-cli/
|
|
61
|
+
├── bin/
|
|
62
|
+
│ └── cli.js ← entry point, discovers features automatically
|
|
63
|
+
├── paths.js ← resolvePath/srcPath — resolved from the project's cwd
|
|
64
|
+
├── helpers.js ← log, copyTemplate, safeReplace, etc.
|
|
65
|
+
├── setup-env.js ← loads the consuming project's .env
|
|
66
|
+
└── setup/
|
|
67
|
+
└── tailwind/
|
|
68
|
+
├── config.js ← paths, flags, versions for this feature
|
|
69
|
+
├── setup.js ← export async function setup() { ... }
|
|
70
|
+
└── templates/ ← files that get copied into the project
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`bin/cli.js` knows nothing about specific features — it scans `setup/*`
|
|
74
|
+
and picks up any folder that has a `setup.js` exporting `setup()`.
|
|
75
|
+
|
|
76
|
+
### An important note about paths
|
|
77
|
+
|
|
78
|
+
`paths.js` resolves everything from `process.cwd()`, not from the
|
|
79
|
+
package's own location. This is intentional: the package physically
|
|
80
|
+
lives in `node_modules` (or the npx cache), but it needs to operate on
|
|
81
|
+
files belonging to the **consuming project**, not on its own files.
|
|
82
|
+
|
|
83
|
+
The one exception is `helpers.js#copyTemplate`, which looks up template
|
|
84
|
+
files (`setup/<feature>/templates/*`) relative to **the package itself**
|
|
85
|
+
(via `import.meta.url`), since those are static assets that physically
|
|
86
|
+
ship inside `@darkoto/gulp-template-cli`, not inside the consumer's
|
|
87
|
+
project.
|
|
88
|
+
|
|
89
|
+
## Adding a new feature
|
|
90
|
+
|
|
91
|
+
1. Create `setup/<feature>/config.js`, `setup/<feature>/setup.js`, and
|
|
92
|
+
`setup/<feature>/templates/`.
|
|
93
|
+
2. In `setup.js`, export:
|
|
94
|
+
```js
|
|
95
|
+
export async function setup() {
|
|
96
|
+
// ...
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
3. Use the shared utilities via relative imports:
|
|
100
|
+
```js
|
|
101
|
+
import { resolvePath } from '../../paths.js';
|
|
102
|
+
import { log, copyTemplate, safeReplace } from '../../helpers.js';
|
|
103
|
+
```
|
|
104
|
+
4. Nothing else needs to change — `bin/cli.js` will pick up the new
|
|
105
|
+
feature automatically the next time `template-add` runs.
|
|
106
|
+
|
|
107
|
+
## Requirements
|
|
108
|
+
|
|
109
|
+
- Node.js ≥ 18
|
|
110
|
+
- The target project must use ESM (`"type": "module"` in `package.json`)
|
|
111
|
+
- Expected project structure: `src/html/layouts/head.html`, a
|
|
112
|
+
`gulpfile.js` with the anchor comments
|
|
113
|
+
`// Tasks plugins (tailwind, etc.)`, `// Plugins watcher`, and
|
|
114
|
+
`mainTasks`/`buildTasks` defined as `gulp.parallel(...)`
|
|
115
|
+
|
|
116
|
+
## Known issues (Windows + pnpm)
|
|
117
|
+
|
|
118
|
+
### `pnpm link --global` fails with `Symlink path is the same as the target path`
|
|
119
|
+
|
|
120
|
+
This is a bug in pnpm's `--global` linking mechanism on Windows, not
|
|
121
|
+
related to the package itself. Workaround — use the `link:` protocol
|
|
122
|
+
directly in the consuming project's `package.json`, bypassing the
|
|
123
|
+
global store entirely:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"devDependencies": {
|
|
128
|
+
"@darkoto/gulp-template-cli": "link:../gulp-template-cli"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pnpm install
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `WARN has no binaries` when linking
|
|
138
|
+
|
|
139
|
+
Usually means the `bin` path in `package.json` isn't resolving
|
|
140
|
+
correctly (make sure the path has no leading `./`, the file uses LF
|
|
141
|
+
line endings, and starts with the shebang `#!/usr/bin/env node` on the
|
|
142
|
+
very first line). Until that's sorted out, you can call the CLI
|
|
143
|
+
directly:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
node ./node_modules/@darkoto/gulp-template-cli/bin/cli.js tailwind
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
git clone https://github.com/darkoto/gulp-template-cli.git
|
|
153
|
+
cd gulp-template-cli
|
|
154
|
+
npm install
|
|
155
|
+
node bin/cli.js
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
For local testing against a real project, use the `link:` approach
|
|
159
|
+
above instead of `npm/pnpm link --global`.
|
|
160
|
+
|
|
161
|
+
### Releasing a new version
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npm version patch # patch | minor | major
|
|
165
|
+
git push --tags
|
|
166
|
+
npm publish --access public
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* gulp-template-cli
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx gulp-template-cli → interactive feature picker
|
|
7
|
+
* npx gulp-template-cli tailwind → run a specific feature directly
|
|
8
|
+
*
|
|
9
|
+
* Add a new feature by dropping a folder under setup/<name>/
|
|
10
|
+
* with a config.js, setup.js (exporting `setup()`), and templates/.
|
|
11
|
+
* No changes to this file are needed — features are discovered automatically.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
17
|
+
import { select } from '@inquirer/prompts';
|
|
18
|
+
|
|
19
|
+
// __dirname here = location of THIS package (bin/), not the consuming project.
|
|
20
|
+
// We need this to discover which setup/<feature> folders ship with the package.
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const setupDir = path.join(__dirname, '..', 'setup');
|
|
23
|
+
|
|
24
|
+
function listFeatures() {
|
|
25
|
+
return readdirSync(setupDir).filter(name => {
|
|
26
|
+
const full = path.join(setupDir, name);
|
|
27
|
+
return statSync(full).isDirectory() && existsSync(path.join(full, 'setup.js'));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function runFeature(name) {
|
|
32
|
+
const setupPath = path.join(setupDir, name, 'setup.js');
|
|
33
|
+
|
|
34
|
+
if (!existsSync(setupPath)) {
|
|
35
|
+
console.error(`\n ✗ Unknown feature: "${name}"`);
|
|
36
|
+
console.log(` Available: ${listFeatures().join(', ')}\n`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const mod = await import(pathToFileURL(setupPath).href);
|
|
41
|
+
|
|
42
|
+
if (typeof mod.setup !== 'function') {
|
|
43
|
+
console.error(`\n ✗ setup/${name}/setup.js does not export a "setup" function.\n`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await mod.setup();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
const featureArg = process.argv[2];
|
|
52
|
+
|
|
53
|
+
if (featureArg) {
|
|
54
|
+
await runFeature(featureArg);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const features = listFeatures();
|
|
59
|
+
|
|
60
|
+
if (features.length === 0) {
|
|
61
|
+
console.log('\n No features found under setup/.\n');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const feature = await select({
|
|
66
|
+
message: 'What do you want to add to your project?',
|
|
67
|
+
choices: features.map(name => ({ name, value: name })),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await runFeature(feature);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
main()
|
|
74
|
+
.then(() => process.exit(0))
|
|
75
|
+
.catch(err => {
|
|
76
|
+
console.error('\n ✗ Unexpected error:', err.message);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
package/helpers.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { srcPath } from './paths.js';
|
|
8
|
+
|
|
9
|
+
// ─────────────────────────────────────────────────────────────
|
|
10
|
+
// Helpers
|
|
11
|
+
// ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
// __dirname here refers to THIS package's own location (not cwd).
|
|
14
|
+
// Correct on purpose: templates/ are static assets shipped with the
|
|
15
|
+
// package, they must be found regardless of which project invokes it.
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
// Log
|
|
19
|
+
export function log(message, type = 'info') {
|
|
20
|
+
const icons = { info: '◦', success: '✓', error: '✗', skip: '−' };
|
|
21
|
+
console.log(` ${icons[type]} ${message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get absolute path from relative parts (relative to the CONSUMING project)
|
|
25
|
+
export function getSourcePath(...parts) {
|
|
26
|
+
return srcPath(...parts);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Copy template file to destination
|
|
30
|
+
export function copyTemplate(folderName, templateName, destPath) {
|
|
31
|
+
const templatesDir = path.join(
|
|
32
|
+
__dirname,
|
|
33
|
+
'setup',
|
|
34
|
+
folderName,
|
|
35
|
+
'templates',
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const src = path.join(templatesDir, templateName);
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(src)) {
|
|
41
|
+
log(`Template "${templateName}" not found.`, 'error');
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (fs.existsSync(destPath)) {
|
|
46
|
+
log(`${path.basename(destPath)} already exists.`, 'skip');
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dir = path.dirname(destPath);
|
|
51
|
+
if (!fs.existsSync(dir)) {
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fs.copyFileSync(src, destPath);
|
|
56
|
+
log(`Created ${path.basename(destPath)}.`, 'success');
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Open browser with given URL
|
|
61
|
+
export function openBrowser(url) {
|
|
62
|
+
const cmd =
|
|
63
|
+
process.platform === 'win32'
|
|
64
|
+
? 'start'
|
|
65
|
+
: process.platform === 'darwin'
|
|
66
|
+
? 'open'
|
|
67
|
+
: 'xdg-open';
|
|
68
|
+
exec(`${cmd} ${url}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if dev server is running.
|
|
72
|
+
// `server` = { host, port } — pass it explicitly instead of relying
|
|
73
|
+
// on a specific feature's config (helpers.js is shared across all features).
|
|
74
|
+
export function isServerRunning(server) {
|
|
75
|
+
return new Promise(resolve => {
|
|
76
|
+
const req = http.get(`http://${server.host}:${server.port}`, () => {
|
|
77
|
+
resolve(true);
|
|
78
|
+
});
|
|
79
|
+
req.on('error', () => resolve(false));
|
|
80
|
+
req.setTimeout(1000, () => {
|
|
81
|
+
req.destroy();
|
|
82
|
+
resolve(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Reload server via BrowserSync endpoint
|
|
88
|
+
export function reloadServer(server) {
|
|
89
|
+
return new Promise(resolve => {
|
|
90
|
+
const url = `http://${server.host}:${server.port}/__browser_sync__?method=reload`;
|
|
91
|
+
http
|
|
92
|
+
.get(url, () => {
|
|
93
|
+
log('Server reloaded.', 'success');
|
|
94
|
+
resolve();
|
|
95
|
+
})
|
|
96
|
+
.on('error', () => {
|
|
97
|
+
log('Could not reload server.', 'error');
|
|
98
|
+
resolve();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Replace with validation
|
|
104
|
+
export const safeReplace = (
|
|
105
|
+
content,
|
|
106
|
+
pattern,
|
|
107
|
+
replacement,
|
|
108
|
+
description,
|
|
109
|
+
) => {
|
|
110
|
+
const nextContent = content.replace(pattern, replacement);
|
|
111
|
+
|
|
112
|
+
if (nextContent === content) {
|
|
113
|
+
log(
|
|
114
|
+
`Could not ${description} — gulpfile format may have changed.`,
|
|
115
|
+
'error',
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
content,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
content: nextContent,
|
|
127
|
+
};
|
|
128
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@darkoto/gulp-template-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Feature pnpm installer CLI for gulp-template — adds tailwind, etc. on demand into the target project.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"template-add": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"setup",
|
|
12
|
+
"paths.js",
|
|
13
|
+
"helpers.js",
|
|
14
|
+
"setup-env.js"
|
|
15
|
+
],
|
|
16
|
+
"keywords": ["gulp", "cli", "tailwind", "scaffolding", "template"],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Darkoto darkoto12@gmail.com",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/darkoto/gulp-template-cli.git"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@inquirer/prompts": "^7.0.0",
|
|
28
|
+
"dotenv": "^16.4.5"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/paths.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Paths
|
|
3
|
+
* Utilities for setup scripts in setup/*
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: rootDir = process.cwd(), NOT __dirname.
|
|
6
|
+
* This package can live in node_modules or npx cache —
|
|
7
|
+
* paths must resolve relative to the CONSUMING project,
|
|
8
|
+
* not to where this package physically sits.
|
|
9
|
+
* Run the CLI from the project root.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import path from 'path';
|
|
13
|
+
|
|
14
|
+
const rootDir = process.cwd();
|
|
15
|
+
|
|
16
|
+
// Directory names (mirrors gulp/configs/paths.js in the target project)
|
|
17
|
+
const folders = {
|
|
18
|
+
src: 'src',
|
|
19
|
+
styles: 'styles',
|
|
20
|
+
scripts: 'scripts',
|
|
21
|
+
html: 'html',
|
|
22
|
+
layouts: 'layouts',
|
|
23
|
+
pages: 'pages',
|
|
24
|
+
components: 'components',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────
|
|
28
|
+
// Path Helpers
|
|
29
|
+
// ─────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Path relative to project root (cwd)
|
|
33
|
+
*/
|
|
34
|
+
export const resolvePath = (...parts) => path.join(rootDir, ...parts);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Path to source files
|
|
38
|
+
*/
|
|
39
|
+
export const srcPath = (...parts) => resolvePath(folders.src, ...parts);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Path to gulp tasks
|
|
43
|
+
*/
|
|
44
|
+
export const gulpPath = (...parts) => resolvePath('gulp', ...parts);
|
|
45
|
+
|
|
46
|
+
// ─────────────────────────────────────────────────────────────
|
|
47
|
+
// Export
|
|
48
|
+
// ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export const templatePaths = {
|
|
51
|
+
root: rootDir,
|
|
52
|
+
|
|
53
|
+
// Computed paths
|
|
54
|
+
src: srcPath(),
|
|
55
|
+
srcStyles: srcPath(folders.styles),
|
|
56
|
+
srcScripts: srcPath(folders.scripts),
|
|
57
|
+
srcHtml: srcPath(folders.html),
|
|
58
|
+
srcHtmlLayouts: srcPath(folders.html, folders.layouts),
|
|
59
|
+
srcHtmlPages: srcPath(folders.html, folders.pages),
|
|
60
|
+
srcHtmlComponents: srcPath(folders.html, folders.components),
|
|
61
|
+
|
|
62
|
+
// Gulp
|
|
63
|
+
gulpTasks: gulpPath('tasks'),
|
|
64
|
+
gulpConfigs: gulpPath('configs'),
|
|
65
|
+
gulpUtils: gulpPath('utils'),
|
|
66
|
+
|
|
67
|
+
// Helpers
|
|
68
|
+
resolvePath,
|
|
69
|
+
srcPath,
|
|
70
|
+
gulpPath,
|
|
71
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind Setup Configuration
|
|
3
|
+
*
|
|
4
|
+
* Note: dotenv is loaded via setup-env.js (imported first in setup.js).
|
|
5
|
+
* Do NOT import dotenv here — ESM static imports evaluate before side effects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Directory names (mirrors gulp/configs/paths.js)
|
|
9
|
+
const folders = {
|
|
10
|
+
gulp: 'gulp',
|
|
11
|
+
tasks: 'tasks',
|
|
12
|
+
src: 'src',
|
|
13
|
+
styles: 'styles',
|
|
14
|
+
scripts: 'scripts',
|
|
15
|
+
html: 'html',
|
|
16
|
+
layouts: 'layouts',
|
|
17
|
+
pages: 'pages',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { folders };
|
|
21
|
+
|
|
22
|
+
// ─────────────────────────────────────────────────────────────
|
|
23
|
+
// Package
|
|
24
|
+
// ─────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export const folderName = 'tailwind';
|
|
27
|
+
export const packageName = 'tailwindcss';
|
|
28
|
+
export const version = '3.4.17';
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────
|
|
31
|
+
// Server
|
|
32
|
+
// ─────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export const server = {
|
|
35
|
+
port: process.env.PORT || 3000,
|
|
36
|
+
host: process.env.HOST || 'localhost',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ─────────────────────────────────────────────────────────────
|
|
40
|
+
// Files
|
|
41
|
+
// ─────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export const files = {
|
|
44
|
+
tailwindConfigModule: 'tailwind.config.module.js',
|
|
45
|
+
tailwindConfigCdn: 'tailwind.config.cdn.js',
|
|
46
|
+
tailwindConfig: 'tailwind.config.js',
|
|
47
|
+
tailwindCSS: 'tailwind.css',
|
|
48
|
+
tailwindDemo: 'tailwind.html',
|
|
49
|
+
tailwindTask: 'tailwind.js',
|
|
50
|
+
head: 'head.html',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────────────
|
|
54
|
+
// Paths
|
|
55
|
+
// ─────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export const paths = {
|
|
58
|
+
// Where to create tailwind config
|
|
59
|
+
configModule: {
|
|
60
|
+
dest: './',
|
|
61
|
+
filename: files.tailwindConfigModule,
|
|
62
|
+
},
|
|
63
|
+
configCdn: {
|
|
64
|
+
dest: `${folders.src}/${folders.scripts}/`,
|
|
65
|
+
filename: files.tailwindConfigCdn,
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Where to create tailwind styles
|
|
69
|
+
styles: {
|
|
70
|
+
dest: `${folders.src}/${folders.styles}/`,
|
|
71
|
+
filename: files.tailwindCSS,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Gulp task file
|
|
75
|
+
gulpTask: {
|
|
76
|
+
dest: `${folders.gulp}/${folders.tasks}/`,
|
|
77
|
+
filename: files.tailwindTask,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// HTML file to inject stylesheet link
|
|
81
|
+
head: `${folders.src}/${folders.html}/${folders.layouts}/${files.head}`,
|
|
82
|
+
|
|
83
|
+
// Demo page
|
|
84
|
+
demo: {
|
|
85
|
+
dest: `${folders.src}/${folders.html}/`,
|
|
86
|
+
filename: files.tailwindDemo,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ─────────────────────────────────────────────────────────────
|
|
91
|
+
// Options
|
|
92
|
+
// ─────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
export const options = {
|
|
95
|
+
openBrowser: true,
|
|
96
|
+
copyDemoPage: true,
|
|
97
|
+
createStyles: true,
|
|
98
|
+
injectStylesheet: true,
|
|
99
|
+
injectCdn: true,
|
|
100
|
+
injectConfig: true,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ─────────────────────────────────────────────────────────────
|
|
104
|
+
// URLs
|
|
105
|
+
// ─────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
export const urls = {
|
|
108
|
+
get demo() {
|
|
109
|
+
return `http://${server.host}:${server.port}/${paths.demo.filename}`;
|
|
110
|
+
},
|
|
111
|
+
docs: 'https://v3.tailwindcss.com',
|
|
112
|
+
};
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind CSS Setup Script (CLI)
|
|
3
|
+
* Run via: npx my-template-cli tailwind
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import * as config from './config.js';
|
|
9
|
+
import { resolvePath } from '../../paths.js';
|
|
10
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
11
|
+
import {
|
|
12
|
+
log,
|
|
13
|
+
getSourcePath,
|
|
14
|
+
copyTemplate,
|
|
15
|
+
safeReplace,
|
|
16
|
+
} from '../../helpers.js';
|
|
17
|
+
|
|
18
|
+
// ─────────────────────────────────────────────────────────────
|
|
19
|
+
// Setup Tasks
|
|
20
|
+
// ─────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function installPackage() {
|
|
23
|
+
log(`Installing ${config.packageName}@${config.version}...`, 'info');
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
execSync(`pnpm i -D ${config.packageName}@${config.version}`, {
|
|
27
|
+
stdio: 'pipe',
|
|
28
|
+
});
|
|
29
|
+
log(`Installed ${config.packageName}@${config.version}.`, 'success');
|
|
30
|
+
} catch (err) {
|
|
31
|
+
log(
|
|
32
|
+
`Failed to install ${config.packageName}: ${err.message}`,
|
|
33
|
+
'error',
|
|
34
|
+
);
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createConfig(provider = 'pnpm') {
|
|
40
|
+
let destPath;
|
|
41
|
+
if (provider === 'pnpm') {
|
|
42
|
+
destPath = resolvePath(
|
|
43
|
+
config.paths.configModule.dest,
|
|
44
|
+
config.files.tailwindConfig,
|
|
45
|
+
);
|
|
46
|
+
copyTemplate(
|
|
47
|
+
config.folderName,
|
|
48
|
+
config.paths.configModule.filename,
|
|
49
|
+
destPath,
|
|
50
|
+
);
|
|
51
|
+
} else if (provider === 'cdn') {
|
|
52
|
+
destPath = resolvePath(
|
|
53
|
+
config.paths.configCdn.dest,
|
|
54
|
+
config.files.tailwindConfig,
|
|
55
|
+
);
|
|
56
|
+
copyTemplate(
|
|
57
|
+
config.folderName,
|
|
58
|
+
config.paths.configCdn.filename,
|
|
59
|
+
destPath,
|
|
60
|
+
);
|
|
61
|
+
} else {
|
|
62
|
+
return log(`Invalid provider: ${provider}`, 'error');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createStyles() {
|
|
67
|
+
if (!config.options.createStyles) return;
|
|
68
|
+
|
|
69
|
+
const destPath = getSourcePath(
|
|
70
|
+
config.folders.styles,
|
|
71
|
+
config.files.tailwindCSS,
|
|
72
|
+
);
|
|
73
|
+
copyTemplate(config.folderName, config.files.tailwindCSS, destPath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function injectStylesheet() {
|
|
77
|
+
if (!config.options.injectStylesheet) return;
|
|
78
|
+
|
|
79
|
+
const headPath = resolvePath(config.paths.head);
|
|
80
|
+
|
|
81
|
+
if (!fs.existsSync(headPath)) {
|
|
82
|
+
log('head.html not found.', 'error');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let content = fs.readFileSync(headPath, 'utf-8');
|
|
87
|
+
|
|
88
|
+
if (content.includes('tailwind.css')) {
|
|
89
|
+
log('Tailwind stylesheet already in head.html.', 'skip');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const link = '<link rel="stylesheet" href="./styles/tailwind.css" />';
|
|
94
|
+
|
|
95
|
+
// Insert before the first stylesheet so Tailwind resets load first
|
|
96
|
+
if (content.includes('<link rel="stylesheet"')) {
|
|
97
|
+
content = content.replace(/(<link rel="stylesheet")/, `${link}\n$1`);
|
|
98
|
+
} else if (content.includes('</head>')) {
|
|
99
|
+
content = content.replace('</head>', `${link}\n</head>`);
|
|
100
|
+
} else {
|
|
101
|
+
content += `\n${link}\n`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fs.writeFileSync(headPath, content);
|
|
105
|
+
log('Added Tailwind stylesheet to head.html.', 'success');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function injectCdn() {
|
|
109
|
+
if (!config.options.injectCdn) return;
|
|
110
|
+
|
|
111
|
+
const headPath = resolvePath(config.paths.head);
|
|
112
|
+
|
|
113
|
+
if (!fs.existsSync(headPath)) {
|
|
114
|
+
log('head.html not found.', 'error');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let content = fs.readFileSync(headPath, 'utf-8');
|
|
119
|
+
|
|
120
|
+
if (content.includes('cdn.tailwindcss.com')) {
|
|
121
|
+
log('Tailwind CDN already in head.html.', 'skip');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const link = '<script src="https://cdn.tailwindcss.com"></script>';
|
|
126
|
+
|
|
127
|
+
const result = safeReplace(
|
|
128
|
+
content,
|
|
129
|
+
/(<!-- CDN -->)/,
|
|
130
|
+
`$1\n${link}`,
|
|
131
|
+
'inject tailwind CDN',
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!result.success) return;
|
|
135
|
+
|
|
136
|
+
content = result.content;
|
|
137
|
+
fs.writeFileSync(headPath, content);
|
|
138
|
+
log('Added Tailwind CDN to head.html.', 'success');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function injectConfig() {
|
|
142
|
+
if (!config.options.injectConfig) return;
|
|
143
|
+
|
|
144
|
+
const headPath = resolvePath(config.paths.head);
|
|
145
|
+
|
|
146
|
+
if (!fs.existsSync(headPath)) {
|
|
147
|
+
log('head.html not found.', 'error');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let content = fs.readFileSync(headPath, 'utf-8');
|
|
152
|
+
|
|
153
|
+
if (content.includes('tailwind.config.js')) {
|
|
154
|
+
log('Tailwind config already in head.html.', 'skip');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const link = '<script src="./scripts/tailwind.config.js"></script>';
|
|
159
|
+
|
|
160
|
+
const result = safeReplace(
|
|
161
|
+
content,
|
|
162
|
+
/(<!-- SCRIPTS -->)/,
|
|
163
|
+
`$1\n${link}`,
|
|
164
|
+
'inject tailwind config',
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!result.success) return;
|
|
168
|
+
|
|
169
|
+
content = result.content;
|
|
170
|
+
|
|
171
|
+
fs.writeFileSync(headPath, content);
|
|
172
|
+
log('Added Tailwind config to head.html.', 'success');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function copyDemoPage() {
|
|
176
|
+
if (!config.options.copyDemoPage) return;
|
|
177
|
+
|
|
178
|
+
const destPath = getSourcePath(
|
|
179
|
+
config.folders.html,
|
|
180
|
+
config.folders.pages,
|
|
181
|
+
config.paths.demo.filename,
|
|
182
|
+
);
|
|
183
|
+
copyTemplate(config.folderName, config.files.tailwindDemo, destPath);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createGulpTask() {
|
|
187
|
+
const destPath = resolvePath(
|
|
188
|
+
config.paths.gulpTask.dest,
|
|
189
|
+
config.paths.gulpTask.filename,
|
|
190
|
+
);
|
|
191
|
+
copyTemplate(config.folderName, config.files.tailwindTask, destPath);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function updateGulpfile() {
|
|
195
|
+
const gulpfilePath = resolvePath('gulpfile.js');
|
|
196
|
+
|
|
197
|
+
if (!fs.existsSync(gulpfilePath)) {
|
|
198
|
+
log('gulpfile.js not found.', 'error');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let content = fs.readFileSync(gulpfilePath, 'utf-8');
|
|
203
|
+
|
|
204
|
+
// Check if already added
|
|
205
|
+
if (content.includes("from './gulp/tasks/tailwind.js'")) {
|
|
206
|
+
log('Tailwind already in gulpfile.js.', 'skip');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Add import
|
|
211
|
+
const importLine =
|
|
212
|
+
"import { tailwind, tailwindReload } from './gulp/tasks/tailwind.js';";
|
|
213
|
+
|
|
214
|
+
const resultImport = safeReplace(
|
|
215
|
+
content,
|
|
216
|
+
/(\/\/ Tasks plugins \(tailwind, etc\.\))/,
|
|
217
|
+
`$1\n${importLine}`,
|
|
218
|
+
'inject tailwind import',
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (!resultImport.success) return;
|
|
222
|
+
|
|
223
|
+
content = resultImport.content;
|
|
224
|
+
|
|
225
|
+
// Add to watch
|
|
226
|
+
const resultWatcher = safeReplace(
|
|
227
|
+
content,
|
|
228
|
+
/(\/\/ Plugins watcher)/,
|
|
229
|
+
`$1\n gulp.watch(['tailwind.config.js', \`\${paths.srcStyles}/tailwind.css\`], gulp.series(tailwind, tailwindReload));`,
|
|
230
|
+
'add tailwind watcher',
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (!resultWatcher.success) return;
|
|
234
|
+
|
|
235
|
+
content = resultWatcher.content;
|
|
236
|
+
|
|
237
|
+
// Add to mainTasks
|
|
238
|
+
const resultMainTask = safeReplace(
|
|
239
|
+
content,
|
|
240
|
+
/(const\s+mainTasks\s*=\s*gulp\.parallel\([\s\S]*?)(\n\);)/,
|
|
241
|
+
`$1\n tailwind,$2`,
|
|
242
|
+
'add tailwind to mainTasks',
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (!resultMainTask.success) return;
|
|
246
|
+
|
|
247
|
+
content = resultMainTask.content;
|
|
248
|
+
|
|
249
|
+
// Add to buildTasks
|
|
250
|
+
const resultBuildTask = safeReplace(
|
|
251
|
+
content,
|
|
252
|
+
/(const\s+buildTasks\s*=\s*gulp\.parallel\([\s\S]*?)(\n\);)/,
|
|
253
|
+
`$1\n tailwind,$2`,
|
|
254
|
+
'add tailwind to buildTasks',
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (!resultBuildTask.success) return;
|
|
258
|
+
|
|
259
|
+
content = resultBuildTask.content;
|
|
260
|
+
|
|
261
|
+
fs.writeFileSync(gulpfilePath, content);
|
|
262
|
+
log('Updated gulpfile.js.', 'success');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isInstalled(provider = 'pnpm') {
|
|
266
|
+
let configPath;
|
|
267
|
+
|
|
268
|
+
if (provider === 'pnpm')
|
|
269
|
+
configPath = resolvePath(
|
|
270
|
+
config.paths.configModule.dest,
|
|
271
|
+
config.paths.configModule.filename,
|
|
272
|
+
);
|
|
273
|
+
else if (provider === 'cdn')
|
|
274
|
+
configPath = resolvePath(
|
|
275
|
+
config.paths.configCdn.dest,
|
|
276
|
+
config.paths.configCdn.filename,
|
|
277
|
+
);
|
|
278
|
+
else {
|
|
279
|
+
return log(`Invalid provider: ${provider}`, 'error');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const headPath = resolvePath(config.paths.head);
|
|
283
|
+
|
|
284
|
+
// Check if config exists
|
|
285
|
+
if (fs.existsSync(configPath)) return true;
|
|
286
|
+
|
|
287
|
+
if (fs.existsSync(headPath)) {
|
|
288
|
+
const content = fs.readFileSync(headPath, 'utf-8');
|
|
289
|
+
if (content.includes('tailwind.css')) return true;
|
|
290
|
+
if (content.includes('cdn.tailwindcss.com')) return true;
|
|
291
|
+
if (content.includes('tailwind.config.js')) return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ─────────────────────────────────────────────────────────────
|
|
298
|
+
// Main
|
|
299
|
+
// ─────────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
export async function setup() {
|
|
302
|
+
const provider = await select({
|
|
303
|
+
message: 'Select Tailwind installation',
|
|
304
|
+
choices: [
|
|
305
|
+
{ name: 'pnpm(npm)', value: 'pnpm' },
|
|
306
|
+
{ name: 'cdn', value: 'cdn' },
|
|
307
|
+
],
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (isInstalled(provider)) {
|
|
311
|
+
console.log(' ⚡ Tailwind CSS is already installed.\n');
|
|
312
|
+
console.log(
|
|
313
|
+
` To reinstall, remove ${config.files.tailwindConfig} and tailwind link from head.html.\n`,
|
|
314
|
+
);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
switch (provider) {
|
|
319
|
+
case 'pnpm': {
|
|
320
|
+
console.log(`\n 🎨 Tailwind CSS v${config.version} Setup (CLI)\n`);
|
|
321
|
+
|
|
322
|
+
console.log(' This will:');
|
|
323
|
+
console.log(` • Install ${config.packageName} package`);
|
|
324
|
+
console.log(' • Create tailwind.config.js');
|
|
325
|
+
console.log(' • Create tailwind.css in styles folder');
|
|
326
|
+
console.log(' • Add stylesheet link to head.html');
|
|
327
|
+
console.log(' • Create gulp task for Tailwind CLI');
|
|
328
|
+
console.log(' • Copy demo page');
|
|
329
|
+
console.log(' • Auto-generate rebuild kit in dist/\n');
|
|
330
|
+
|
|
331
|
+
const confirmed = await confirm({ message: 'Do you want to continue?' });
|
|
332
|
+
|
|
333
|
+
if (!confirmed) {
|
|
334
|
+
console.log('\n ❌ Setup cancelled.\n');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log('\n Installing...\n');
|
|
339
|
+
|
|
340
|
+
installPackage();
|
|
341
|
+
createConfig(provider);
|
|
342
|
+
createStyles();
|
|
343
|
+
createGulpTask();
|
|
344
|
+
updateGulpfile();
|
|
345
|
+
injectStylesheet();
|
|
346
|
+
copyDemoPage();
|
|
347
|
+
|
|
348
|
+
console.log(`\n ✅ Done! Docs: ${config.urls.docs}\n`);
|
|
349
|
+
|
|
350
|
+
if (config.options.copyDemoPage) {
|
|
351
|
+
console.log(' 💡 Run "pnpm dev" to view the demo page.\n');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case 'cdn': {
|
|
357
|
+
console.log(`\n 🎨 Tailwind CSS v${config.version} Setup (CDN)\n`);
|
|
358
|
+
|
|
359
|
+
console.log(' This will:');
|
|
360
|
+
console.log(' • Create tailwind.config.js');
|
|
361
|
+
console.log(' • Create tailwind.css in styles folder');
|
|
362
|
+
console.log(' • Add stylesheet link to head.html');
|
|
363
|
+
console.log(' • Add CDN link to head.html');
|
|
364
|
+
console.log(' • Add Config link to head.html');
|
|
365
|
+
console.log(' • Copy demo page \n');
|
|
366
|
+
|
|
367
|
+
const confirmed = await confirm('Do you want to continue?');
|
|
368
|
+
|
|
369
|
+
if (!confirmed) {
|
|
370
|
+
console.log('\n ❌ Setup cancelled.\n');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
console.log('\n Installing...\n');
|
|
375
|
+
|
|
376
|
+
createConfig(provider);
|
|
377
|
+
createStyles();
|
|
378
|
+
injectStylesheet();
|
|
379
|
+
injectCdn();
|
|
380
|
+
injectConfig();
|
|
381
|
+
copyDemoPage();
|
|
382
|
+
|
|
383
|
+
console.log(`\n ✅ Done! Docs: ${config.urls.docs}\n`);
|
|
384
|
+
|
|
385
|
+
if (config.options.copyDemoPage) {
|
|
386
|
+
console.log(' 💡 Run "pnpm dev" to view the demo page.\n');
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
default: {
|
|
391
|
+
console.log('Invalid selection. Exiting.');
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
@@include("../layouts/head.html", {
|
|
6
|
+
"title": "Tailwind CSS Installed",
|
|
7
|
+
"image": "./assets/img/preview.webp"
|
|
8
|
+
})
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body class="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 flex items-center justify-center p-4">
|
|
12
|
+
<div class="text-center">
|
|
13
|
+
<svg class="w-24 h-24 mx-auto mb-6" viewBox="0 0 54 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
14
|
+
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
15
|
+
d="M27 0C19.8 0 15.3 3.6 13.5 10.8C16.2 7.2 19.35 5.85 22.95 6.75C25.004 7.263 26.472 8.754 28.097 10.403C30.744 13.09 33.808 16.2 40.5 16.2C47.7 16.2 52.2 12.6 54 5.4C51.3 9 48.15 10.35 44.55 9.45C42.496 8.937 41.028 7.446 39.403 5.797C36.756 3.11 33.692 0 27 0ZM13.5 16.2C6.3 16.2 1.8 19.8 0 27C2.7 23.4 5.85 22.05 9.45 22.95C11.504 23.464 12.972 24.954 14.597 26.603C17.244 29.29 20.308 32.4 27 32.4C34.2 32.4 38.7 28.8 40.5 21.6C37.8 25.2 34.65 26.55 31.05 25.65C28.996 25.137 27.528 23.646 25.903 21.997C23.256 19.31 20.192 16.2 13.5 16.2Z"
|
|
16
|
+
fill="#38BDF8" />
|
|
17
|
+
</svg>
|
|
18
|
+
|
|
19
|
+
<h1 class="text-4xl font-bold text-white mb-4">Tailwind CSS</h1>
|
|
20
|
+
|
|
21
|
+
<p class="text-slate-400 text-lg mb-8">
|
|
22
|
+
Successfully installed via CLI
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<a href="https://v3.tailwindcss.com/docs" target="_blank"
|
|
26
|
+
class="inline-flex items-center gap-2 bg-sky-500 hover:bg-sky-400 text-white font-semibold px-6 py-3 rounded-lg transition-colors">
|
|
27
|
+
<span>Documentation</span>
|
|
28
|
+
|
|
29
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
30
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
31
|
+
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
32
|
+
</svg>
|
|
33
|
+
</a>
|
|
34
|
+
</div>
|
|
35
|
+
</body>
|
|
36
|
+
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const run = promisify(exec);
|
|
7
|
+
|
|
8
|
+
export const tailwind = async () => {
|
|
9
|
+
const { paths, config } = globalThis.app;
|
|
10
|
+
const configPath = path.join(paths.root, 'tailwind.config.js');
|
|
11
|
+
const inputPath = path.join(paths.srcStyles, 'tailwind.css');
|
|
12
|
+
|
|
13
|
+
// Skip if tailwind is not set up
|
|
14
|
+
if (!fs.existsSync(configPath) || !fs.existsSync(inputPath)) return;
|
|
15
|
+
|
|
16
|
+
// Ensure output directory exists
|
|
17
|
+
fs.mkdirSync(paths.buildStyles, { recursive: true });
|
|
18
|
+
|
|
19
|
+
const outputPath = path.join(paths.buildStyles, 'tailwind.css');
|
|
20
|
+
const minify = config.env.isProd ? '--minify' : '';
|
|
21
|
+
|
|
22
|
+
// Compile Tailwind CSS via CLI
|
|
23
|
+
await run(`npx tailwindcss -i "${inputPath}" -o "${outputPath}" --config "${configPath}" ${minify}`.trim());
|
|
24
|
+
|
|
25
|
+
// Generate rebuild kit in dist/
|
|
26
|
+
generateRebuildKit(paths.build, configPath, inputPath);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function getInstalledVersion() {
|
|
30
|
+
try {
|
|
31
|
+
const pkgPath = path.resolve('node_modules/tailwindcss/package.json');
|
|
32
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
33
|
+
} catch {
|
|
34
|
+
return '3.4.17';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate self-contained rebuild kit in dist/
|
|
40
|
+
* Allows rebuilding Tailwind CSS without the full gulp-template setup
|
|
41
|
+
*/
|
|
42
|
+
function generateRebuildKit(distRoot, configPath, inputPath) {
|
|
43
|
+
// 1. Minimal package.json with tailwindcss dep and build script
|
|
44
|
+
const pkg = JSON.stringify(
|
|
45
|
+
{
|
|
46
|
+
private: true,
|
|
47
|
+
description: 'Tailwind CSS rebuild kit — run "npm install && npm run build:css"',
|
|
48
|
+
scripts: {
|
|
49
|
+
'build:css': 'npx tailwindcss -i tailwind.input.css -o styles/tailwind.css --minify',
|
|
50
|
+
},
|
|
51
|
+
devDependencies: {
|
|
52
|
+
tailwindcss: getInstalledVersion(),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
null,
|
|
56
|
+
2
|
|
57
|
+
);
|
|
58
|
+
fs.writeFileSync(path.join(distRoot, 'package.json'), `${pkg}\n`);
|
|
59
|
+
|
|
60
|
+
// 2. Tailwind config with dist-specific content paths
|
|
61
|
+
let configContent = fs.readFileSync(configPath, 'utf-8');
|
|
62
|
+
configContent = configContent.replace(
|
|
63
|
+
/content:\s*\[.*?\]/s,
|
|
64
|
+
"content: ['./**/*.{html,php,js}', '!./node_modules/**']"
|
|
65
|
+
);
|
|
66
|
+
fs.writeFileSync(path.join(distRoot, 'tailwind.config.js'), configContent);
|
|
67
|
+
|
|
68
|
+
// 3. Input CSS (Tailwind directives)
|
|
69
|
+
fs.copyFileSync(inputPath, path.join(distRoot, 'tailwind.input.css'));
|
|
70
|
+
|
|
71
|
+
// 4. Rebuild instructions
|
|
72
|
+
const readme = [
|
|
73
|
+
'# Tailwind CSS — Rebuild Instructions',
|
|
74
|
+
'',
|
|
75
|
+
'This folder contains a pre-compiled `styles/tailwind.css`.',
|
|
76
|
+
'If you modify HTML/PHP files and need to update Tailwind CSS:',
|
|
77
|
+
'',
|
|
78
|
+
'## Quick Start',
|
|
79
|
+
'',
|
|
80
|
+
'```bash',
|
|
81
|
+
'npm install',
|
|
82
|
+
'npm run build:css',
|
|
83
|
+
'```',
|
|
84
|
+
'',
|
|
85
|
+
'## What This Does',
|
|
86
|
+
'',
|
|
87
|
+
'- Scans all `.html`, `.php`, `.js` files for Tailwind utility classes',
|
|
88
|
+
'- Generates an optimized `styles/tailwind.css` with only the classes you use',
|
|
89
|
+
'',
|
|
90
|
+
'## Files',
|
|
91
|
+
'',
|
|
92
|
+
'| File | Purpose |',
|
|
93
|
+
'| --- | --- |',
|
|
94
|
+
'| `package.json` | Dependencies and build script |',
|
|
95
|
+
'| `tailwind.config.js` | Tailwind configuration (theme, plugins) |',
|
|
96
|
+
'| `tailwind.input.css` | Source CSS with Tailwind directives |',
|
|
97
|
+
'| `styles/tailwind.css` | Compiled output (auto-generated) |',
|
|
98
|
+
'',
|
|
99
|
+
].join('\n');
|
|
100
|
+
fs.writeFileSync(path.join(distRoot, 'TAILWIND.md'), readme);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const tailwindReload = (done) => {
|
|
104
|
+
globalThis.app.plugins.browserSync.reload();
|
|
105
|
+
done();
|
|
106
|
+
};
|