@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 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,9 @@
1
+ tailwind.config = {
2
+ theme: {
3
+ extend: {
4
+ colors: {},
5
+ fontFamily: {},
6
+ spacing: {},
7
+ },
8
+ },
9
+ };
@@ -0,0 +1,12 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ['./src/**/*.{html,js}'],
4
+ theme: {
5
+ extend: {
6
+ colors: {},
7
+ fontFamily: {},
8
+ spacing: {},
9
+ },
10
+ },
11
+ plugins: [],
12
+ };
@@ -0,0 +1,9 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {}
6
+
7
+ @layer components {}
8
+
9
+ @layer utilities {}
@@ -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
+ };