chokibasic 1.1.2 → 1.1.3
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/index.d.ts +129 -105
- package/index.js +7 -1
- package/package.json +1 -1
- package/readme.md +206 -11
- package/src/buildcss.js +29 -0
- package/src/buildjs.js +35 -0
- package/src/buildphp.js +25 -0
- package/src/export.js +134 -0
- package/src/watchers.js +203 -0
- package/src/watcher.js +0 -412
package/index.d.ts
CHANGED
|
@@ -1,128 +1,152 @@
|
|
|
1
|
-
// index.d.ts
|
|
2
1
|
|
|
3
|
-
|
|
2
|
+
declare namespace chokibasic {
|
|
3
|
+
export type GlobPattern = string;
|
|
4
4
|
|
|
5
|
-
export type IgnoreMatcher =
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export type IgnoreMatcher =
|
|
6
|
+
| string
|
|
7
|
+
| RegExp
|
|
8
|
+
| ((relPosixPath: string) => boolean);
|
|
9
9
|
|
|
10
|
-
export
|
|
11
|
-
/** Type d’évènement (dans ton code actuel: surtout "change") */
|
|
12
|
-
type: "add" | "change" | "unlink" | (string & {});
|
|
13
|
-
/** Chemin relatif à cwd, normalisé en POSIX (slash "/") */
|
|
14
|
-
file: string;
|
|
15
|
-
}
|
|
10
|
+
export type WatchEventType = "add" | "change" | "unlink" | (string & {});
|
|
16
11
|
|
|
17
|
-
export interface
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
export interface WatchEvent {
|
|
13
|
+
/** Type d’évènement (dans le code actuel: surtout "change") */
|
|
14
|
+
type: WatchEventType;
|
|
15
|
+
/** Chemin relatif à cwd, normalisé POSIX (slash "/") */
|
|
16
|
+
file: string;
|
|
17
|
+
}
|
|
20
18
|
|
|
21
|
-
export interface
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
export interface WatchRuleContext {
|
|
20
|
+
rule: WatchRule;
|
|
21
|
+
}
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
export interface WatchRule {
|
|
24
|
+
/** Nom affiché en debug */
|
|
25
|
+
name?: string;
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
/** Globs d’inclusion (ex: "src/styles/ ** / *.scss") */
|
|
28
|
+
patterns: GlobPattern | GlobPattern[];
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
/** Ignorés additionnels (globs / regex / function) */
|
|
31
|
+
ignored?: IgnoreMatcher[];
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* Le batch contient des objets { type, file }.
|
|
37
|
-
*/
|
|
38
|
-
callback: (events: WatchEvent[], ctx: WatchRuleContext) => void | Promise<void>;
|
|
39
|
-
}
|
|
33
|
+
/** Debounce en ms (défaut: 150) */
|
|
34
|
+
debounceMs?: number;
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Callback appelé avec un batch d’évènements.
|
|
38
|
+
* Le batch contient des objets { type, file }.
|
|
39
|
+
*/
|
|
40
|
+
callback: (events: WatchEvent[], ctx: WatchRuleContext) => void | Promise<void>;
|
|
41
|
+
}
|
|
45
42
|
|
|
46
|
-
export interface
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
export interface AwaitWriteFinishOptions {
|
|
44
|
+
stabilityThreshold?: number;
|
|
45
|
+
pollInterval?: number;
|
|
46
|
+
}
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
*/
|
|
54
|
-
globalIgnored?: string[];
|
|
48
|
+
export interface CreateWatchersOptions {
|
|
49
|
+
/** Répertoire racine utilisé pour calculer les chemins relatifs */
|
|
50
|
+
cwd?: string;
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Ignorés globaux (appliqués dans queue(), pas via chokidar "ignored")
|
|
54
|
+
* Défaut: ["** /node_modules/ **","** /.git/ **","** /dist/ **"]
|
|
55
|
+
*/
|
|
56
|
+
globalIgnored?: string[];
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
/** chokidar: ignoreInitial */
|
|
59
|
+
ignoreInitial?: boolean;
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
/** chokidar: awaitWriteFinish */
|
|
62
|
+
awaitWriteFinish?: boolean | AwaitWriteFinishOptions;
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
/** chokidar: usePolling */
|
|
65
|
+
usePolling?: boolean;
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
/** chokidar: interval */
|
|
68
|
+
interval?: number;
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
/** chokidar: binaryInterval */
|
|
71
|
+
binaryInterval?: number;
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
73
|
+
/** Log console */
|
|
74
|
+
debug?: boolean;
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
/** Permet d’accepter d’autres options sans casser les types */
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface WatchersController {
|
|
81
|
+
/** Ferme tous les watchers et annule les timers */
|
|
82
|
+
close: () => Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ExportDistStats {
|
|
86
|
+
copied: number;
|
|
87
|
+
skipped: number;
|
|
88
|
+
}
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
/** Options forwardées à esbuild.build() */
|
|
91
|
+
export type BuildJSOptions = Parameters<typeof import("esbuild").build>[0];
|
|
92
|
+
|
|
93
|
+
/** Options forwardées à sass.compile() */
|
|
94
|
+
export type BuildCSSOptions = NonNullable<Parameters<typeof import("sass").compile>[1]>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Crée un ou plusieurs watchers (un par règle).
|
|
98
|
+
*/
|
|
99
|
+
export function createWatchers(
|
|
100
|
+
rules: WatchRule[],
|
|
101
|
+
options?: CreateWatchersOptions
|
|
102
|
+
): WatchersController;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Exporte un dossier `src` vers `dist` en respectant .gitignore + exclusions.
|
|
106
|
+
*/
|
|
107
|
+
export function exportDist(
|
|
108
|
+
src: string,
|
|
109
|
+
dist: string,
|
|
110
|
+
banner?: string | null
|
|
111
|
+
): Promise<ExportDistStats>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Compile SCSS -> CSS minifié (csso), écrit dans outCssMin.
|
|
115
|
+
*/
|
|
116
|
+
export function buildCSS(
|
|
117
|
+
inputScss: string,
|
|
118
|
+
outCssMin: string,
|
|
119
|
+
options?: BuildCSSOptions
|
|
120
|
+
): Promise<void>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Bundle/minify JS via esbuild.
|
|
124
|
+
*/
|
|
125
|
+
export function buildJS(
|
|
126
|
+
entry: string,
|
|
127
|
+
outfile: string,
|
|
128
|
+
options?: BuildJSOptions
|
|
129
|
+
): Promise<void>;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Rend un fichier via pxpros.render(file).
|
|
133
|
+
*/
|
|
134
|
+
export function buildPHP(file: string): Promise<void>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Génère un sitemap via pxpros.sitemap(file).
|
|
138
|
+
*/
|
|
139
|
+
export function buildSitemap(file: string): Promise<void>;
|
|
86
140
|
}
|
|
87
141
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
export function exportDist(
|
|
100
|
-
src: string,
|
|
101
|
-
dist: string,
|
|
102
|
-
banner?: string | null
|
|
103
|
-
): Promise<ExportDistStats>;
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Compile SCSS -> CSS minifié (csso), écrit dans outCssMin.
|
|
107
|
-
* `options` est forwardé à sass.compile().
|
|
108
|
-
*/
|
|
109
|
-
export function buildCSS(
|
|
110
|
-
inputScss: string,
|
|
111
|
-
outCssMin: string,
|
|
112
|
-
options?: Record<string, unknown>
|
|
113
|
-
): Promise<void>;
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Bundle/minify JS via esbuild.
|
|
117
|
-
* `options` est forwardé à esbuild.build().
|
|
118
|
-
*/
|
|
119
|
-
export function buildJS(
|
|
120
|
-
entry: string,
|
|
121
|
-
outfile: string,
|
|
122
|
-
options?: Record<string, unknown>
|
|
123
|
-
): Promise<void>;
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Rend un fichier via pxpros.render(file).
|
|
127
|
-
*/
|
|
128
|
-
export function buildPHP(file: string): Promise<void>;
|
|
142
|
+
declare const chokibasic: {
|
|
143
|
+
createWatchers: typeof chokibasic.createWatchers;
|
|
144
|
+
exportDist: typeof chokibasic.exportDist;
|
|
145
|
+
buildCSS: typeof chokibasic.buildCSS;
|
|
146
|
+
buildJS: typeof chokibasic.buildJS;
|
|
147
|
+
buildPHP: typeof chokibasic.buildPHP;
|
|
148
|
+
buildSitemap: typeof chokibasic.buildSitemap;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export = chokibasic;
|
|
152
|
+
export as namespace chokibasic;
|
package/index.js
CHANGED
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
# chokibasic
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Tiny build & watch helpers for simple static projects.
|
|
4
|
+
|
|
5
|
+
## Helpers
|
|
6
|
+
|
|
4
7
|
- `createWatchers(rules, options)`
|
|
5
|
-
- `buildCSS(inputScss, outCssMin)`
|
|
6
|
-
- `buildJS(entry, outfile)`
|
|
8
|
+
- `buildCSS(inputScss, outCssMin, options?)`
|
|
9
|
+
- `buildJS(entry, outfile, options?)`
|
|
10
|
+
- `buildPHP(file)`
|
|
11
|
+
- `buildSitemap(file)`
|
|
12
|
+
- `exportDist(src, dist, bannerPath?)`
|
|
13
|
+
|
|
14
|
+
---
|
|
7
15
|
|
|
8
16
|
## Install
|
|
17
|
+
|
|
9
18
|
```bash
|
|
10
19
|
npm i chokibasic
|
|
11
|
-
|
|
20
|
+
````
|
|
21
|
+
|
|
22
|
+
---
|
|
12
23
|
|
|
13
|
-
## Usage
|
|
14
|
-
|
|
24
|
+
## Usage (watch + build) — original example (fixed)
|
|
25
|
+
|
|
26
|
+
```js
|
|
15
27
|
const { createWatchers, buildCSS, buildJS } = require("chokibasic");
|
|
16
28
|
|
|
17
29
|
const w = createWatchers(
|
|
@@ -39,19 +51,202 @@ const w = createWatchers(
|
|
|
39
51
|
|
|
40
52
|
// later: await w.close();
|
|
41
53
|
process.on("SIGINT", async () => {
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
await w.close();
|
|
55
|
+
process.exit(0);
|
|
44
56
|
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
### `createWatchers(rules, options)`
|
|
45
64
|
|
|
65
|
+
Creates one watcher per rule using `chokidar`, but with:
|
|
66
|
+
|
|
67
|
+
* **include filtering** using `rule.patterns` (glob)
|
|
68
|
+
* **ignore filtering** using `options.globalIgnored` + `rule.ignored` (glob/RegExp/function)
|
|
69
|
+
* **debounced batching**: multiple file events are grouped and sent to your callback
|
|
70
|
+
|
|
71
|
+
Each callback receives:
|
|
72
|
+
|
|
73
|
+
* `events`: an array like `{ type, file }`
|
|
74
|
+
* `ctx`: `{ rule }`
|
|
75
|
+
|
|
76
|
+
**Important note (current behavior):**
|
|
77
|
+
In the current code, only `"change"` events are enabled (`add` and `unlink` listeners are commented out).
|
|
78
|
+
|
|
79
|
+
#### Rule shape
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
{
|
|
83
|
+
name?: string,
|
|
84
|
+
patterns: string | string[],
|
|
85
|
+
ignored?: (string | RegExp | ((relPosixPath) => boolean))[],
|
|
86
|
+
debounceMs?: number, // default 150
|
|
87
|
+
callback: async (events, ctx) => {}
|
|
88
|
+
}
|
|
46
89
|
```
|
|
47
90
|
|
|
91
|
+
#### Options
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
{
|
|
95
|
+
cwd: process.cwd(),
|
|
96
|
+
globalIgnored: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
|
|
97
|
+
|
|
98
|
+
// chokidar options used internally:
|
|
99
|
+
ignoreInitial: true,
|
|
100
|
+
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 10 },
|
|
101
|
+
usePolling: false,
|
|
102
|
+
interval: 200,
|
|
103
|
+
binaryInterval: 300,
|
|
104
|
+
|
|
105
|
+
debug: true
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### `buildCSS(inputScss, outCssMin, options?)`
|
|
112
|
+
|
|
113
|
+
Compiles SCSS using `sass.compile()` and minifies using `csso`.
|
|
114
|
+
|
|
115
|
+
* Defaults include:
|
|
116
|
+
|
|
117
|
+
* `loadPaths: ["<cwd>/node_modules"]`
|
|
118
|
+
* `style: "compressed"`
|
|
119
|
+
* no sourcemap
|
|
120
|
+
* `options` is forwarded to `sass.compile()` (and can override defaults).
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
const { buildCSS } = require("chokibasic");
|
|
126
|
+
|
|
127
|
+
await buildCSS("src/styles/main.scss", "dist/app.min.css", {
|
|
128
|
+
// any sass.compile options here
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### `buildJS(entry, outfile, options?)`
|
|
135
|
+
|
|
136
|
+
Bundles & minifies JS using `esbuild`.
|
|
137
|
+
|
|
138
|
+
Defaults include:
|
|
139
|
+
|
|
140
|
+
* `bundle: true`
|
|
141
|
+
* `platform: "browser"`
|
|
142
|
+
* `minify: true`
|
|
143
|
+
* `treeShaking: true`
|
|
144
|
+
* `target: ["es2020"]`
|
|
145
|
+
* `legalComments: "none"`
|
|
146
|
+
|
|
147
|
+
`options` is forwarded to `esbuild.build()` (and can override defaults).
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
const { buildJS } = require("chokibasic");
|
|
153
|
+
|
|
154
|
+
await buildJS("src/scripts/main.js", "dist/app.min.js", {
|
|
155
|
+
// any esbuild.build options here
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### `buildPHP(file)`
|
|
162
|
+
|
|
163
|
+
Runs `pxpros.render(file)` and logs generated HTML files.
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
const { buildPHP } = require("chokibasic");
|
|
169
|
+
|
|
170
|
+
await buildPHP("src/pages/_index.php");
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### `buildSitemap(file)`
|
|
176
|
+
|
|
177
|
+
Runs `pxpros.sitemap(file)` and logs generated sitemap XML files.
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const { buildSitemap } = require("chokibasic");
|
|
183
|
+
|
|
184
|
+
await buildSitemap("src/pages/_index.php");
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### `exportDist(src, dist, bannerPath?)`
|
|
190
|
+
|
|
191
|
+
Copies your `src` folder to `dist`, while:
|
|
192
|
+
|
|
193
|
+
* respecting `.gitignore` (if present in project root)
|
|
194
|
+
* also ignoring `dist/` for safety
|
|
195
|
+
* skipping:
|
|
196
|
+
|
|
197
|
+
* any file/folder starting with `_`
|
|
198
|
+
* `.scss`
|
|
199
|
+
* `.js` files that are NOT `.min.js`
|
|
200
|
+
|
|
201
|
+
Then it can prepend a banner to:
|
|
202
|
+
|
|
203
|
+
* `.js` and `.css` files using `/*! ... */`
|
|
204
|
+
* `.html` files using `<!-- ... -->`
|
|
205
|
+
|
|
206
|
+
It also replaces placeholders:
|
|
207
|
+
|
|
208
|
+
* in `.html`:
|
|
209
|
+
|
|
210
|
+
* `###YEAR###` → current year
|
|
211
|
+
* `###TIMESTAMP###` → unix timestamp (seconds)
|
|
212
|
+
* in `sitemap.xml`:
|
|
213
|
+
|
|
214
|
+
* `###TODAY###` → `YYYY-MM-DD`
|
|
215
|
+
* in the banner text:
|
|
216
|
+
|
|
217
|
+
* `###DATE###` → formatted date (fr-CA, America/Toronto)
|
|
218
|
+
|
|
219
|
+
Example:
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
const { exportDist } = require("chokibasic");
|
|
223
|
+
|
|
224
|
+
const stats = await exportDist("src", "dist"); // uses built-in banner.txt
|
|
225
|
+
console.log(stats); // { copied: <number>, skipped: <number> }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
With a custom banner file:
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
await exportDist("src", "dist", "src/banner.txt");
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
48
236
|
## Changelog
|
|
49
237
|
|
|
238
|
+
### Version 1.1.3
|
|
239
|
+
* Complete refactor
|
|
240
|
+
* Fix buildCSS "style: expanded"
|
|
241
|
+
|
|
50
242
|
### Version 1.1.2
|
|
51
|
-
|
|
243
|
+
|
|
244
|
+
* exportDist: Add date replacement for sitemap.xml
|
|
52
245
|
|
|
53
246
|
### Version 1.1.1
|
|
54
|
-
|
|
247
|
+
|
|
248
|
+
* Update dependency: pxpros 1.0.4
|
|
55
249
|
|
|
56
250
|
### Version 1.1.0
|
|
57
|
-
|
|
251
|
+
|
|
252
|
+
* Add a sitemap builder with pxpros
|
package/src/buildcss.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const sass = require("sass");
|
|
2
|
+
const csso = require("csso");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
|
|
6
|
+
const buildCSS = async (inputScss, outCssMin, options = {}) => {
|
|
7
|
+
// let compiled;
|
|
8
|
+
try {
|
|
9
|
+
const compiled = sass.compile(inputScss, {
|
|
10
|
+
loadPaths: [path.resolve(process.cwd(), "./node_modules")],
|
|
11
|
+
style: "compressed",
|
|
12
|
+
sourceMap: false,
|
|
13
|
+
sourceMapIncludeSources: false,
|
|
14
|
+
...options
|
|
15
|
+
});
|
|
16
|
+
if(options?.style == "expanded"){
|
|
17
|
+
fs.writeFileSync(outCssMin, compiled.css);
|
|
18
|
+
} else {
|
|
19
|
+
const minified = csso.minify(compiled.css, { restructure: false });
|
|
20
|
+
fs.writeFileSync(outCssMin, minified.css);
|
|
21
|
+
}
|
|
22
|
+
console.log(`✅ CSS generated: ${outCssMin}`);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error("❌ Sass compile error:");
|
|
25
|
+
console.error(err?.formatted || err?.message || err);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { buildCSS };
|
package/src/buildjs.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const esbuild = require("esbuild");
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const buildJS = async (entry, outfile, options = {}) => {
|
|
5
|
+
try {
|
|
6
|
+
await esbuild.build({
|
|
7
|
+
entryPoints: [entry],
|
|
8
|
+
outfile,
|
|
9
|
+
bundle: true,
|
|
10
|
+
platform: "browser",
|
|
11
|
+
logLevel: "error",
|
|
12
|
+
treeShaking: true,
|
|
13
|
+
minify: true,
|
|
14
|
+
supported: { "template-literal": false },
|
|
15
|
+
target: ["es2020"],
|
|
16
|
+
legalComments: "none",
|
|
17
|
+
...options
|
|
18
|
+
});
|
|
19
|
+
console.log(`✅ JS generated: ${outfile}`);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error("❌ esbuild build failed.");
|
|
22
|
+
if (err?.errors?.length) {
|
|
23
|
+
const formatted = await esbuild.formatMessages(err.errors, {
|
|
24
|
+
kind: "error",
|
|
25
|
+
color: true,
|
|
26
|
+
terminalWidth: process.stdout.columns || 80,
|
|
27
|
+
});
|
|
28
|
+
console.error(formatted.join("\n"));
|
|
29
|
+
} else {
|
|
30
|
+
console.error(err);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { buildJS };
|
package/src/buildphp.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const pxpros = require("pxpros");
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const buildPHP = async (file) => {
|
|
5
|
+
const results = await pxpros.render(file);
|
|
6
|
+
if(results.success) {
|
|
7
|
+
results.files.forEach(file => console.log(`✅ HTML generated: ${file}`));
|
|
8
|
+
} else {
|
|
9
|
+
console.error("❌ pxpros render failed.");
|
|
10
|
+
console.error(results.error);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const buildSitemap = async (file) => {
|
|
16
|
+
const results = await pxpros.sitemap(file);
|
|
17
|
+
if(results.success) {
|
|
18
|
+
results.files.forEach(file => console.log(`✅ XML Sitemap generated: ${file}`));
|
|
19
|
+
} else {
|
|
20
|
+
console.error("❌ pxpros sitemap creation failed.");
|
|
21
|
+
console.error(results.error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { buildPHP, buildSitemap };
|
package/src/export.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const fsprom = require('fs/promises');
|
|
2
|
+
const ignore = require('ignore');
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const ROOT = process.cwd();
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async function emptyDir(dir) {
|
|
10
|
+
await fsprom.mkdir(dir, { recursive: true });
|
|
11
|
+
const entries = await fsprom.readdir(dir, { withFileTypes: true });
|
|
12
|
+
await Promise.all(entries.map((e) => fsprom.rm(path.join(dir, e.name), { recursive: true, force: true })));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
function formatFrDate(dateInput = new Date(), timeZone = 'America/Toronto') {
|
|
17
|
+
const d = (dateInput instanceof Date) ? dateInput : new Date(dateInput);
|
|
18
|
+
const fmt = new Intl.DateTimeFormat('fr-CA', {
|
|
19
|
+
weekday: 'long',
|
|
20
|
+
day: 'numeric',
|
|
21
|
+
month: 'long',
|
|
22
|
+
year: 'numeric',
|
|
23
|
+
hour: 'numeric',
|
|
24
|
+
minute: '2-digit',
|
|
25
|
+
hour12: false,
|
|
26
|
+
timeZone
|
|
27
|
+
});
|
|
28
|
+
const parts = Object.fromEntries(fmt.formatToParts(d).map(p => [p.type, p.value]));
|
|
29
|
+
const weekday = parts.weekday.charAt(0).toUpperCase() + parts.weekday.slice(1); // "Samedi"
|
|
30
|
+
return `${weekday} le ${parts.day} ${parts.month} ${parts.year} à ${parts.hour} h ${parts.minute}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
function norm(p) {
|
|
35
|
+
// normalise en chemin POSIX pour compat .gitignore
|
|
36
|
+
return p.split(path.sep).join('/');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function loadGitignore() {
|
|
40
|
+
const ig = ignore();
|
|
41
|
+
const giPath = path.join(ROOT, '.gitignore');
|
|
42
|
+
if (fs.existsSync(giPath)) {
|
|
43
|
+
const txt = await fsprom.readFile(giPath, 'utf8');
|
|
44
|
+
ig.add(txt);
|
|
45
|
+
}
|
|
46
|
+
// on ignore aussi le dossier dist par sécurité (pas nécessaire mais sain)
|
|
47
|
+
ig.add('dist/');
|
|
48
|
+
return ig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function rmDir(dir) {
|
|
52
|
+
await fsprom.rm(dir, { recursive: true, force: true });
|
|
53
|
+
await fsprom.mkdir(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function shouldExcludeFile(relFromRoot, absPath) {
|
|
57
|
+
// Exclusions de type/extension
|
|
58
|
+
const lower = absPath.toLowerCase();
|
|
59
|
+
if (path.basename(lower).startsWith('_')) return true;
|
|
60
|
+
if (lower.endsWith('.scss')) return true;
|
|
61
|
+
if (lower.endsWith('.js') && !lower.endsWith('.min.js')) return true;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function copyFilePreserveTree(absSrc, src, dist, ig) {
|
|
66
|
+
const relFromSrc = path.relative(src, absSrc);
|
|
67
|
+
const relFromRoot = path.relative(ROOT, absSrc);
|
|
68
|
+
const relPosix = norm(relFromRoot);
|
|
69
|
+
|
|
70
|
+
// 1) Exclusions via .gitignore
|
|
71
|
+
if (ig.ignores(relPosix)) return false;
|
|
72
|
+
|
|
73
|
+
// 2) Exclusions spécifiques (scss, js non minifiés)
|
|
74
|
+
if (shouldExcludeFile(relPosix, absSrc)) return false;
|
|
75
|
+
|
|
76
|
+
const absDst = path.join(dist, relFromSrc);
|
|
77
|
+
// console.log(absDst);
|
|
78
|
+
await fsprom.mkdir(path.dirname(absDst), { recursive: true });
|
|
79
|
+
await fsprom.copyFile(absSrc, absDst);
|
|
80
|
+
return absDst;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function walkAndCopy(dir, src, dest, ig, stats, banner = null) {
|
|
84
|
+
const entries = await fsprom.readdir(dir, { withFileTypes: true });
|
|
85
|
+
const bannerContent = (await fsprom.readFile(banner || path.join(__dirname, 'banner.txt'), 'utf8')).replace(/###DATE###/, formatFrDate());
|
|
86
|
+
const today = (d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`)(new Date());
|
|
87
|
+
|
|
88
|
+
for (const de of entries) {
|
|
89
|
+
const abs = path.join(dir, de.name);
|
|
90
|
+
const relFromRoot = path.relative(ROOT, abs);
|
|
91
|
+
const relPosix = norm(relFromRoot);
|
|
92
|
+
|
|
93
|
+
if (de.isDirectory()) {
|
|
94
|
+
// Si le dossier est ignoré par .gitignore, on ne descend pas
|
|
95
|
+
if (ig.ignores(relPosix + '/')) continue;
|
|
96
|
+
if(de.name.startsWith('_')) continue;
|
|
97
|
+
await walkAndCopy(abs, src, dest, ig, stats, banner);
|
|
98
|
+
} else if (de.isFile()) {
|
|
99
|
+
const copied = await copyFilePreserveTree(abs, src, dest, ig);
|
|
100
|
+
if (copied) {
|
|
101
|
+
const lower = abs.toLowerCase();
|
|
102
|
+
if (lower.endsWith('.js')) await fsprom.writeFile(copied, "/*!\n\n" + bannerContent + "\n\n*/\n" + (await fsprom.readFile(copied, 'utf8')), "utf8");
|
|
103
|
+
else if (lower.endsWith('.css')) await fsprom.writeFile(copied, "/*!\n\n" + bannerContent + "\n\n*/\n" + (await fsprom.readFile(copied, 'utf8')), "utf8");
|
|
104
|
+
else if (lower.endsWith('.html')) await fsprom.writeFile(copied, "<!--\n\n" + bannerContent + "\n\n\-->\n" + (await fsprom.readFile(copied, 'utf8'))
|
|
105
|
+
.replaceAll(/###YEAR###/g, (new Date).getFullYear())
|
|
106
|
+
.replaceAll(/###TIMESTAMP###/g, Math.floor(Date.now() / 1000)), "utf8");
|
|
107
|
+
else if (lower.endsWith('sitemap.xml')) await fsprom.writeFile(copied, (await fsprom.readFile(copied, 'utf8')).replaceAll(/###TODAY###/g, today), "utf8");
|
|
108
|
+
stats.copied++;
|
|
109
|
+
}
|
|
110
|
+
else stats.skipped++;
|
|
111
|
+
}
|
|
112
|
+
// (symlinks & autres: ignorés)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
const exportDist = async (src, dist, banner = null) => {
|
|
118
|
+
try {
|
|
119
|
+
const ig = await loadGitignore();
|
|
120
|
+
|
|
121
|
+
await emptyDir(dist);
|
|
122
|
+
if (!fs.existsSync(src)) throw new error('Folder src is invalid.');
|
|
123
|
+
|
|
124
|
+
const stats = { copied: 0, skipped: 0 };
|
|
125
|
+
await walkAndCopy(src, src, dist, ig, stats, banner);
|
|
126
|
+
|
|
127
|
+
return stats;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw new error(err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
module.exports = { exportDist };
|
package/src/watchers.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const chokidar = require("chokidar");
|
|
3
|
+
|
|
4
|
+
function toPosix(p) {
|
|
5
|
+
return p.replace(/\\/g, "/");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizePatterns(patterns) {
|
|
9
|
+
return Array.isArray(patterns) ? patterns : [patterns];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function globBaseDir(glob) {
|
|
13
|
+
const g = toPosix(glob);
|
|
14
|
+
const idx = g.search(/[*?\[]/);
|
|
15
|
+
if (idx === -1) return g;
|
|
16
|
+
const base = g.slice(0, idx);
|
|
17
|
+
return base.replace(/\/+$/, "") || ".";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function globToRegExp(glob) {
|
|
21
|
+
const g = toPosix(glob);
|
|
22
|
+
|
|
23
|
+
let re = "^";
|
|
24
|
+
for (let i = 0; i < g.length; i++) {
|
|
25
|
+
const c = g[i];
|
|
26
|
+
|
|
27
|
+
if (c === "*" && g[i + 1] === "*") {
|
|
28
|
+
i++;
|
|
29
|
+
if (g[i + 1] === "/") {
|
|
30
|
+
i++;
|
|
31
|
+
re += "(?:.*/)?";
|
|
32
|
+
} else {
|
|
33
|
+
re += ".*";
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (c === "*") { re += "[^/]*"; continue; }
|
|
39
|
+
if (c === "?") { re += "[^/]"; continue; }
|
|
40
|
+
|
|
41
|
+
if ("\\.[]{}()+-^$|".includes(c)) re += "\\" + c;
|
|
42
|
+
else re += c;
|
|
43
|
+
}
|
|
44
|
+
re += "$";
|
|
45
|
+
return new RegExp(re);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isIgnoredPosix(relPosix, ignoreMatchers) {
|
|
49
|
+
for (const m of ignoreMatchers) {
|
|
50
|
+
if (m instanceof RegExp) {
|
|
51
|
+
if (m.test(relPosix)) return true;
|
|
52
|
+
} else if (typeof m === "function") {
|
|
53
|
+
// si l'user fournit une fonction ignorée, on lui passe le rel posix
|
|
54
|
+
if (m(relPosix)) return true;
|
|
55
|
+
} else if (typeof m === "string") {
|
|
56
|
+
// strings glob déjà compilées dans ignoreMatchers (voir compileIgnoreMatchers)
|
|
57
|
+
// (fallback: on ne devrait pas arriver ici)
|
|
58
|
+
if (globToRegExp(m).test(relPosix)) return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function compileIgnoreMatchers(ignoreList) {
|
|
65
|
+
return (ignoreList || []).map((ig) => {
|
|
66
|
+
if (ig instanceof RegExp) return ig;
|
|
67
|
+
if (typeof ig === "function") return ig;
|
|
68
|
+
// string glob -> RegExp
|
|
69
|
+
return globToRegExp(ig);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* createWatchers(rules, options)
|
|
75
|
+
* - Watch les dossiers racines (baseDirs)
|
|
76
|
+
* - Filtre include via globs (rule.patterns)
|
|
77
|
+
* - Filtre ignore via globs/regex (globalIgnored + rule.ignored)
|
|
78
|
+
*/
|
|
79
|
+
function createWatchers(rules, options = {}) {
|
|
80
|
+
const opt = {
|
|
81
|
+
cwd: process.cwd(),
|
|
82
|
+
|
|
83
|
+
// Ignorés globaux (appliqués dans queue() — pas dans chokidar)
|
|
84
|
+
globalIgnored: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
|
|
85
|
+
|
|
86
|
+
ignoreInitial: true,
|
|
87
|
+
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 10 },
|
|
88
|
+
|
|
89
|
+
// Si FS weird (OneDrive, réseau, WSL/Docker bind mount), mets true:
|
|
90
|
+
usePolling: false,
|
|
91
|
+
interval: 200,
|
|
92
|
+
binaryInterval: 300,
|
|
93
|
+
|
|
94
|
+
debug: true,
|
|
95
|
+
...options,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handles = [];
|
|
99
|
+
|
|
100
|
+
for (const rule of rules) {
|
|
101
|
+
if (!rule || typeof rule.callback !== "function") {
|
|
102
|
+
throw new Error("Each rule must have a callback(events, ctx).");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const name = rule.name || "rule";
|
|
106
|
+
const patterns = normalizePatterns(rule.patterns);
|
|
107
|
+
|
|
108
|
+
// include matchers (relatif à cwd, en posix)
|
|
109
|
+
const includeMatchers = patterns.map(globToRegExp);
|
|
110
|
+
|
|
111
|
+
// ignore matchers (relatif à cwd, en posix)
|
|
112
|
+
const ignoreMatchers = compileIgnoreMatchers([
|
|
113
|
+
...(opt.globalIgnored || []),
|
|
114
|
+
...(rule.ignored || []),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
// baseDirs à watcher
|
|
118
|
+
const baseDirs = Array.from(new Set(patterns.map(globBaseDir).map((d) => d || ".")))
|
|
119
|
+
.map((d) => path.resolve(opt.cwd, d));
|
|
120
|
+
|
|
121
|
+
if (opt.debug) {
|
|
122
|
+
console.log(`\n[${name}] starting watcher`);
|
|
123
|
+
console.log(" cwd:", opt.cwd);
|
|
124
|
+
console.log(" patterns:", patterns);
|
|
125
|
+
console.log(" baseDirs:", baseDirs);
|
|
126
|
+
console.log(" ignored (applied in queue):", [...(opt.globalIgnored || []), ...(rule.ignored || [])]);
|
|
127
|
+
console.log("");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const pending = new Map();
|
|
131
|
+
let timer = null;
|
|
132
|
+
let running = false;
|
|
133
|
+
let rerun = false;
|
|
134
|
+
|
|
135
|
+
const flush = async () => {
|
|
136
|
+
if (running) { rerun = true; return; }
|
|
137
|
+
running = true;
|
|
138
|
+
try {
|
|
139
|
+
do {
|
|
140
|
+
rerun = false;
|
|
141
|
+
const batch = Array.from(pending.values());
|
|
142
|
+
pending.clear();
|
|
143
|
+
if (batch.length) await rule.callback(batch, { rule });
|
|
144
|
+
} while (rerun);
|
|
145
|
+
} finally {
|
|
146
|
+
running = false;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const queue = (type, absPath) => {
|
|
151
|
+
const rel = toPosix(path.relative(opt.cwd, absPath));
|
|
152
|
+
|
|
153
|
+
// ignore d'abord
|
|
154
|
+
if (isIgnoredPosix(rel, ignoreMatchers)) {
|
|
155
|
+
// if (opt.debug) console.log(`[${name}] ignored`, type, rel);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// include ensuite
|
|
160
|
+
const ok = includeMatchers.some((rx) => rx.test(rel));
|
|
161
|
+
if (!ok) return;
|
|
162
|
+
|
|
163
|
+
if (opt.debug) console.log(`[${name}] queue`, type, rel);
|
|
164
|
+
|
|
165
|
+
pending.set(`${type}:${rel}`, { type, file: rel });
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
timer = setTimeout(flush, rule.debounceMs ?? 150);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// ⚠️ IMPORTANT: on ne met PAS "ignored" ici
|
|
171
|
+
const watcher = chokidar.watch(baseDirs, {
|
|
172
|
+
ignoreInitial: opt.ignoreInitial,
|
|
173
|
+
awaitWriteFinish: opt.awaitWriteFinish,
|
|
174
|
+
persistent: true,
|
|
175
|
+
usePolling: opt.usePolling,
|
|
176
|
+
interval: opt.interval,
|
|
177
|
+
binaryInterval: opt.binaryInterval,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// watcher.on("ready", () => {
|
|
181
|
+
// console.log(`[${name}] ready`);
|
|
182
|
+
// });
|
|
183
|
+
|
|
184
|
+
// watcher.on("add", (p) => queue("add", p));
|
|
185
|
+
watcher.on("change", (p) => queue("change", p));
|
|
186
|
+
// watcher.on("unlink", (p) => queue("unlink", p));
|
|
187
|
+
watcher.on("error", (err) => console.error(`[${name}] watch error:`, err));
|
|
188
|
+
|
|
189
|
+
handles.push({
|
|
190
|
+
watcher,
|
|
191
|
+
stopTimers: () => { clearTimeout(timer); pending.clear(); }
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
close: async () => {
|
|
197
|
+
for (const h of handles) h.stopTimers();
|
|
198
|
+
await Promise.all(handles.map((h) => h.watcher.close()));
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = { createWatchers };
|
package/src/watcher.js
DELETED
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
const chokidar = require("chokidar");
|
|
2
|
-
const fs = require("node:fs");
|
|
3
|
-
const fsprom = require('fs/promises');
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const esbuild = require("esbuild");
|
|
6
|
-
const sass = require("sass");
|
|
7
|
-
const csso = require("csso");
|
|
8
|
-
const pxpros = require("pxpros");
|
|
9
|
-
const ignore = require('ignore');
|
|
10
|
-
|
|
11
|
-
const ROOT = process.cwd();
|
|
12
|
-
|
|
13
|
-
function toPosix(p) {
|
|
14
|
-
return p.replace(/\\/g, "/");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizePatterns(patterns) {
|
|
18
|
-
return Array.isArray(patterns) ? patterns : [patterns];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function globBaseDir(glob) {
|
|
22
|
-
const g = toPosix(glob);
|
|
23
|
-
const idx = g.search(/[*?\[]/);
|
|
24
|
-
if (idx === -1) return g;
|
|
25
|
-
const base = g.slice(0, idx);
|
|
26
|
-
return base.replace(/\/+$/, "") || ".";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function globToRegExp(glob) {
|
|
30
|
-
const g = toPosix(glob);
|
|
31
|
-
|
|
32
|
-
let re = "^";
|
|
33
|
-
for (let i = 0; i < g.length; i++) {
|
|
34
|
-
const c = g[i];
|
|
35
|
-
|
|
36
|
-
if (c === "*" && g[i + 1] === "*") {
|
|
37
|
-
i++;
|
|
38
|
-
if (g[i + 1] === "/") {
|
|
39
|
-
i++;
|
|
40
|
-
re += "(?:.*/)?";
|
|
41
|
-
} else {
|
|
42
|
-
re += ".*";
|
|
43
|
-
}
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (c === "*") { re += "[^/]*"; continue; }
|
|
48
|
-
if (c === "?") { re += "[^/]"; continue; }
|
|
49
|
-
|
|
50
|
-
if ("\\.[]{}()+-^$|".includes(c)) re += "\\" + c;
|
|
51
|
-
else re += c;
|
|
52
|
-
}
|
|
53
|
-
re += "$";
|
|
54
|
-
return new RegExp(re);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function isIgnoredPosix(relPosix, ignoreMatchers) {
|
|
58
|
-
for (const m of ignoreMatchers) {
|
|
59
|
-
if (m instanceof RegExp) {
|
|
60
|
-
if (m.test(relPosix)) return true;
|
|
61
|
-
} else if (typeof m === "function") {
|
|
62
|
-
// si l'user fournit une fonction ignorée, on lui passe le rel posix
|
|
63
|
-
if (m(relPosix)) return true;
|
|
64
|
-
} else if (typeof m === "string") {
|
|
65
|
-
// strings glob déjà compilées dans ignoreMatchers (voir compileIgnoreMatchers)
|
|
66
|
-
// (fallback: on ne devrait pas arriver ici)
|
|
67
|
-
if (globToRegExp(m).test(relPosix)) return true;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function compileIgnoreMatchers(ignoreList) {
|
|
74
|
-
return (ignoreList || []).map((ig) => {
|
|
75
|
-
if (ig instanceof RegExp) return ig;
|
|
76
|
-
if (typeof ig === "function") return ig;
|
|
77
|
-
// string glob -> RegExp
|
|
78
|
-
return globToRegExp(ig);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* createWatchers(rules, options)
|
|
84
|
-
* - Watch les dossiers racines (baseDirs)
|
|
85
|
-
* - Filtre include via globs (rule.patterns)
|
|
86
|
-
* - Filtre ignore via globs/regex (globalIgnored + rule.ignored)
|
|
87
|
-
*/
|
|
88
|
-
function createWatchers(rules, options = {}) {
|
|
89
|
-
const opt = {
|
|
90
|
-
cwd: process.cwd(),
|
|
91
|
-
|
|
92
|
-
// Ignorés globaux (appliqués dans queue() — pas dans chokidar)
|
|
93
|
-
globalIgnored: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
|
|
94
|
-
|
|
95
|
-
ignoreInitial: true,
|
|
96
|
-
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 10 },
|
|
97
|
-
|
|
98
|
-
// Si FS weird (OneDrive, réseau, WSL/Docker bind mount), mets true:
|
|
99
|
-
usePolling: false,
|
|
100
|
-
interval: 200,
|
|
101
|
-
binaryInterval: 300,
|
|
102
|
-
|
|
103
|
-
debug: true,
|
|
104
|
-
...options,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const handles = [];
|
|
108
|
-
|
|
109
|
-
for (const rule of rules) {
|
|
110
|
-
if (!rule || typeof rule.callback !== "function") {
|
|
111
|
-
throw new Error("Each rule must have a callback(events, ctx).");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const name = rule.name || "rule";
|
|
115
|
-
const patterns = normalizePatterns(rule.patterns);
|
|
116
|
-
|
|
117
|
-
// include matchers (relatif à cwd, en posix)
|
|
118
|
-
const includeMatchers = patterns.map(globToRegExp);
|
|
119
|
-
|
|
120
|
-
// ignore matchers (relatif à cwd, en posix)
|
|
121
|
-
const ignoreMatchers = compileIgnoreMatchers([
|
|
122
|
-
...(opt.globalIgnored || []),
|
|
123
|
-
...(rule.ignored || []),
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
|
-
// baseDirs à watcher
|
|
127
|
-
const baseDirs = Array.from(new Set(patterns.map(globBaseDir).map((d) => d || ".")))
|
|
128
|
-
.map((d) => path.resolve(opt.cwd, d));
|
|
129
|
-
|
|
130
|
-
if (opt.debug) {
|
|
131
|
-
console.log(`\n[${name}] starting watcher`);
|
|
132
|
-
console.log(" cwd:", opt.cwd);
|
|
133
|
-
console.log(" patterns:", patterns);
|
|
134
|
-
console.log(" baseDirs:", baseDirs);
|
|
135
|
-
console.log(" ignored (applied in queue):", [...(opt.globalIgnored || []), ...(rule.ignored || [])]);
|
|
136
|
-
console.log("");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const pending = new Map();
|
|
140
|
-
let timer = null;
|
|
141
|
-
let running = false;
|
|
142
|
-
let rerun = false;
|
|
143
|
-
|
|
144
|
-
const flush = async () => {
|
|
145
|
-
if (running) { rerun = true; return; }
|
|
146
|
-
running = true;
|
|
147
|
-
try {
|
|
148
|
-
do {
|
|
149
|
-
rerun = false;
|
|
150
|
-
const batch = Array.from(pending.values());
|
|
151
|
-
pending.clear();
|
|
152
|
-
if (batch.length) await rule.callback(batch, { rule });
|
|
153
|
-
} while (rerun);
|
|
154
|
-
} finally {
|
|
155
|
-
running = false;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const queue = (type, absPath) => {
|
|
160
|
-
const rel = toPosix(path.relative(opt.cwd, absPath));
|
|
161
|
-
|
|
162
|
-
// ignore d'abord
|
|
163
|
-
if (isIgnoredPosix(rel, ignoreMatchers)) {
|
|
164
|
-
// if (opt.debug) console.log(`[${name}] ignored`, type, rel);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// include ensuite
|
|
169
|
-
const ok = includeMatchers.some((rx) => rx.test(rel));
|
|
170
|
-
if (!ok) return;
|
|
171
|
-
|
|
172
|
-
if (opt.debug) console.log(`[${name}] queue`, type, rel);
|
|
173
|
-
|
|
174
|
-
pending.set(`${type}:${rel}`, { type, file: rel });
|
|
175
|
-
clearTimeout(timer);
|
|
176
|
-
timer = setTimeout(flush, rule.debounceMs ?? 150);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
// ⚠️ IMPORTANT: on ne met PAS "ignored" ici
|
|
180
|
-
const watcher = chokidar.watch(baseDirs, {
|
|
181
|
-
ignoreInitial: opt.ignoreInitial,
|
|
182
|
-
awaitWriteFinish: opt.awaitWriteFinish,
|
|
183
|
-
persistent: true,
|
|
184
|
-
usePolling: opt.usePolling,
|
|
185
|
-
interval: opt.interval,
|
|
186
|
-
binaryInterval: opt.binaryInterval,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// watcher.on("ready", () => {
|
|
190
|
-
// console.log(`[${name}] ready`);
|
|
191
|
-
// });
|
|
192
|
-
|
|
193
|
-
// watcher.on("add", (p) => queue("add", p));
|
|
194
|
-
watcher.on("change", (p) => queue("change", p));
|
|
195
|
-
// watcher.on("unlink", (p) => queue("unlink", p));
|
|
196
|
-
watcher.on("error", (err) => console.error(`[${name}] watch error:`, err));
|
|
197
|
-
|
|
198
|
-
handles.push({
|
|
199
|
-
watcher,
|
|
200
|
-
stopTimers: () => { clearTimeout(timer); pending.clear(); }
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
close: async () => {
|
|
206
|
-
for (const h of handles) h.stopTimers();
|
|
207
|
-
await Promise.all(handles.map((h) => h.watcher.close()));
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const buildCSS = async (inputScss, outCssMin, options = {}) => {
|
|
214
|
-
let compiled;
|
|
215
|
-
try {
|
|
216
|
-
compiled = sass.compile(inputScss, {
|
|
217
|
-
loadPaths: [path.resolve(process.cwd(), "./node_modules")],
|
|
218
|
-
style: "compressed",
|
|
219
|
-
sourceMap: false,
|
|
220
|
-
sourceMapIncludeSources: false,
|
|
221
|
-
...options
|
|
222
|
-
});
|
|
223
|
-
const minified = csso.minify(compiled.css, { restructure: false });
|
|
224
|
-
fs.writeFileSync(outCssMin, minified.css);
|
|
225
|
-
console.log(`✅ CSS generated: ${outCssMin}`);
|
|
226
|
-
} catch (err) {
|
|
227
|
-
console.error("❌ Sass compile error:");
|
|
228
|
-
console.error(err?.formatted || err?.message || err);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const buildJS = async (entry, outfile, options = {}) => {
|
|
234
|
-
try {
|
|
235
|
-
await esbuild.build({
|
|
236
|
-
entryPoints: [entry],
|
|
237
|
-
outfile,
|
|
238
|
-
bundle: true,
|
|
239
|
-
platform: "browser",
|
|
240
|
-
logLevel: "error",
|
|
241
|
-
treeShaking: true,
|
|
242
|
-
minify: true,
|
|
243
|
-
supported: { "template-literal": false },
|
|
244
|
-
target: ["es2020"],
|
|
245
|
-
legalComments: "none",
|
|
246
|
-
...options
|
|
247
|
-
});
|
|
248
|
-
console.log(`✅ JS generated: ${outfile}`);
|
|
249
|
-
} catch (err) {
|
|
250
|
-
console.error("❌ esbuild build failed.");
|
|
251
|
-
if (err?.errors?.length) {
|
|
252
|
-
const formatted = await esbuild.formatMessages(err.errors, {
|
|
253
|
-
kind: "error",
|
|
254
|
-
color: true,
|
|
255
|
-
terminalWidth: process.stdout.columns || 80,
|
|
256
|
-
});
|
|
257
|
-
console.error(formatted.join("\n"));
|
|
258
|
-
} else {
|
|
259
|
-
console.error(err);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const buildPHP = async (file) => {
|
|
266
|
-
const results = await pxpros.render(file);
|
|
267
|
-
if(results.success) {
|
|
268
|
-
results.files.forEach(file => console.log(`✅ HTML generated: ${file}`));
|
|
269
|
-
} else {
|
|
270
|
-
console.error("❌ pxpros render failed.");
|
|
271
|
-
console.error(results.error);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const buildSitemap = async (file) => {
|
|
277
|
-
const results = await pxpros.sitemap(file);
|
|
278
|
-
if(results.success) {
|
|
279
|
-
results.files.forEach(file => console.log(`✅ XML Sitemap generated: ${file}`));
|
|
280
|
-
} else {
|
|
281
|
-
console.error("❌ pxpros sitemap creation failed.");
|
|
282
|
-
console.error(results.error);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
async function emptyDir(dir) {
|
|
288
|
-
await fsprom.mkdir(dir, { recursive: true });
|
|
289
|
-
const entries = await fsprom.readdir(dir, { withFileTypes: true });
|
|
290
|
-
await Promise.all(entries.map((e) => fsprom.rm(path.join(dir, e.name), { recursive: true, force: true })));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
function formatFrDate(dateInput = new Date(), timeZone = 'America/Toronto') {
|
|
295
|
-
const d = (dateInput instanceof Date) ? dateInput : new Date(dateInput);
|
|
296
|
-
const fmt = new Intl.DateTimeFormat('fr-CA', {
|
|
297
|
-
weekday: 'long',
|
|
298
|
-
day: 'numeric',
|
|
299
|
-
month: 'long',
|
|
300
|
-
year: 'numeric',
|
|
301
|
-
hour: 'numeric',
|
|
302
|
-
minute: '2-digit',
|
|
303
|
-
hour12: false,
|
|
304
|
-
timeZone
|
|
305
|
-
});
|
|
306
|
-
const parts = Object.fromEntries(fmt.formatToParts(d).map(p => [p.type, p.value]));
|
|
307
|
-
const weekday = parts.weekday.charAt(0).toUpperCase() + parts.weekday.slice(1); // "Samedi"
|
|
308
|
-
return `${weekday} le ${parts.day} ${parts.month} ${parts.year} à ${parts.hour} h ${parts.minute}`;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
function norm(p) {
|
|
313
|
-
// normalise en chemin POSIX pour compat .gitignore
|
|
314
|
-
return p.split(path.sep).join('/');
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async function loadGitignore() {
|
|
318
|
-
const ig = ignore();
|
|
319
|
-
const giPath = path.join(ROOT, '.gitignore');
|
|
320
|
-
if (fs.existsSync(giPath)) {
|
|
321
|
-
const txt = await fsprom.readFile(giPath, 'utf8');
|
|
322
|
-
ig.add(txt);
|
|
323
|
-
}
|
|
324
|
-
// on ignore aussi le dossier dist par sécurité (pas nécessaire mais sain)
|
|
325
|
-
ig.add('dist/');
|
|
326
|
-
return ig;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async function rmDir(dir) {
|
|
330
|
-
await fsprom.rm(dir, { recursive: true, force: true });
|
|
331
|
-
await fsprom.mkdir(dir, { recursive: true });
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function shouldExcludeFile(relFromRoot, absPath) {
|
|
335
|
-
// Exclusions de type/extension
|
|
336
|
-
const lower = absPath.toLowerCase();
|
|
337
|
-
if (path.basename(lower).startsWith('_')) return true;
|
|
338
|
-
if (lower.endsWith('.scss')) return true;
|
|
339
|
-
if (lower.endsWith('.js') && !lower.endsWith('.min.js')) return true;
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
async function copyFilePreserveTree(absSrc, src, dist, ig) {
|
|
344
|
-
const relFromSrc = path.relative(src, absSrc);
|
|
345
|
-
const relFromRoot = path.relative(ROOT, absSrc);
|
|
346
|
-
const relPosix = norm(relFromRoot);
|
|
347
|
-
|
|
348
|
-
// 1) Exclusions via .gitignore
|
|
349
|
-
if (ig.ignores(relPosix)) return false;
|
|
350
|
-
|
|
351
|
-
// 2) Exclusions spécifiques (scss, js non minifiés)
|
|
352
|
-
if (shouldExcludeFile(relPosix, absSrc)) return false;
|
|
353
|
-
|
|
354
|
-
const absDst = path.join(dist, relFromSrc);
|
|
355
|
-
// console.log(absDst);
|
|
356
|
-
await fsprom.mkdir(path.dirname(absDst), { recursive: true });
|
|
357
|
-
await fsprom.copyFile(absSrc, absDst);
|
|
358
|
-
return absDst;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async function walkAndCopy(dir, src, dest, ig, stats, banner = null) {
|
|
362
|
-
const entries = await fsprom.readdir(dir, { withFileTypes: true });
|
|
363
|
-
const bannerContent = (await fsprom.readFile(banner || path.join(__dirname, 'banner.txt'), 'utf8')).replace(/###DATE###/, formatFrDate());
|
|
364
|
-
const today = (d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`)(new Date());
|
|
365
|
-
|
|
366
|
-
for (const de of entries) {
|
|
367
|
-
const abs = path.join(dir, de.name);
|
|
368
|
-
const relFromRoot = path.relative(ROOT, abs);
|
|
369
|
-
const relPosix = norm(relFromRoot);
|
|
370
|
-
|
|
371
|
-
if (de.isDirectory()) {
|
|
372
|
-
// Si le dossier est ignoré par .gitignore, on ne descend pas
|
|
373
|
-
if (ig.ignores(relPosix + '/')) continue;
|
|
374
|
-
if(de.name.startsWith('_')) continue;
|
|
375
|
-
await walkAndCopy(abs, src, dest, ig, stats, banner);
|
|
376
|
-
} else if (de.isFile()) {
|
|
377
|
-
const copied = await copyFilePreserveTree(abs, src, dest, ig);
|
|
378
|
-
if (copied) {
|
|
379
|
-
const lower = abs.toLowerCase();
|
|
380
|
-
if (lower.endsWith('.js')) await fsprom.writeFile(copied, "/*!\n\n" + bannerContent + "\n\n*/\n" + (await fsprom.readFile(copied, 'utf8')), "utf8");
|
|
381
|
-
else if (lower.endsWith('.css')) await fsprom.writeFile(copied, "/*!\n\n" + bannerContent + "\n\n*/\n" + (await fsprom.readFile(copied, 'utf8')), "utf8");
|
|
382
|
-
else if (lower.endsWith('.html')) await fsprom.writeFile(copied, "<!--\n\n" + bannerContent + "\n\n\-->\n" + (await fsprom.readFile(copied, 'utf8'))
|
|
383
|
-
.replaceAll(/###YEAR###/g, (new Date).getFullYear())
|
|
384
|
-
.replaceAll(/###TIMESTAMP###/g, Math.floor(Date.now() / 1000)), "utf8");
|
|
385
|
-
else if (lower.endsWith('sitemap.xml')) await fsprom.writeFile(copied, (await fsprom.readFile(copied, 'utf8')).replaceAll(/###TODAY###/g, today), "utf8");
|
|
386
|
-
stats.copied++;
|
|
387
|
-
}
|
|
388
|
-
else stats.skipped++;
|
|
389
|
-
}
|
|
390
|
-
// (symlinks & autres: ignorés)
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const exportDist = async (src, dist, banner = null) => {
|
|
396
|
-
try {
|
|
397
|
-
const ig = await loadGitignore();
|
|
398
|
-
|
|
399
|
-
await emptyDir(dist);
|
|
400
|
-
if (!fs.existsSync(src)) throw new error('Folder src is invalid.');
|
|
401
|
-
|
|
402
|
-
const stats = { copied: 0, skipped: 0 };
|
|
403
|
-
await walkAndCopy(src, src, dist, ig, stats, banner);
|
|
404
|
-
|
|
405
|
-
return stats;
|
|
406
|
-
} catch (err) {
|
|
407
|
-
throw new error(err);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
module.exports = { createWatchers, exportDist, buildCSS, buildJS, buildPHP, buildSitemap };
|