auklet 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -74,6 +74,9 @@ The package exposes both `auk` and `auklet`.
74
74
  | `auk build-css` | Generate CSS output only. |
75
75
  | `auk build-css --watch` | Watch source/config/style files and rebuild CSS. |
76
76
  | `auk publish` | Run the pnpm-based publish workflow. |
77
+ | `auk inspect publish` | Check publish readiness without changing files or registry state. |
78
+ | `auk inspect pack` | Check package entry/export files before publishing. |
79
+ | `auk inspect css` | Explain CSS output entry, theme, and module plans. |
77
80
  | `auk owner add <user>` | Add npm owners through pnpm. |
78
81
 
79
82
  Build and dev commands can override package config for one run:
@@ -128,6 +131,9 @@ auk publish --version patch --dry-run
128
131
  auk publish --no-format
129
132
  auk publish --no-git
130
133
  auk publish --otp 123456
134
+ auk inspect publish --version patch
135
+ auk inspect pack --filter @scope/ui
136
+ auk inspect css --modules
131
137
  auk owner add alice
132
138
  auk owner add alice --filter @scope/ui --otp 123456
133
139
  ```
@@ -158,6 +164,21 @@ package directory. Package-local `.npmrc` files and
158
164
  require publish 2FA, and to `pnpm owner add` for owner management 2FA. In CI,
159
165
  prefer an npm automation token.
160
166
 
167
+ `auk inspect publish` accepts the same publish selection and version flags as
168
+ `auk publish`. It resolves the publish plan, checks package entry/export files,
169
+ then checks registry authentication and whether target versions already exist.
170
+ It does not write versions, run hooks, build, commit, tag, or publish packages.
171
+ Local package file failures or registry issues exit with code 1.
172
+
173
+ `auk inspect pack` accepts `--filter` for workspace package selection. It checks
174
+ whether `package.json` entry fields, `exports`, `bin`, `types`, CSS entry fields,
175
+ and declared `files` paths point to existing package files.
176
+
177
+ `auk inspect css` accepts the same build override flags as `auk build`. It
178
+ prints the normalized CSS plan, including output entries, theme files, and
179
+ module entries. When run from a pnpm workspace root, it inspects workspace child
180
+ packages instead of the root package. It does not write CSS output.
181
+
161
182
  ## Configuration
162
183
 
163
184
  `auklet.config.js` or `auklet.config.mjs` is loaded from the current package
@@ -0,0 +1 @@
1
+ export declare function runInspect(args: Array<string>): Promise<number>;
@@ -0,0 +1,20 @@
1
+ import { runInspectCssCli } from '#auklet/css/inspect';
2
+ import { runInspectPublishCli } from '#auklet/publish/inspect';
3
+ import { runInspectPackCli } from '#auklet/publish/inspectPack';
4
+ export async function runInspect(args) {
5
+ const normalizedArgs = args[0] === '--' ? args.slice(1) : args;
6
+ const [command, ...restArgs] = normalizedArgs;
7
+ if (command === 'css') {
8
+ return runInspectCssCli(restArgs);
9
+ }
10
+ if (command === 'publish') {
11
+ return runInspectPublishCli(restArgs);
12
+ }
13
+ if (command === 'pack') {
14
+ return runInspectPackCli(restArgs);
15
+ }
16
+ if (command) {
17
+ throw new Error(`[inspect] unknown inspect command: ${command}`);
18
+ }
19
+ throw new Error('[inspect] expected inspect command: auk inspect publish, auk inspect pack, or auk inspect css');
20
+ }
package/dist/cli/main.js CHANGED
@@ -7,6 +7,7 @@ import { runDev } from '#auklet/cli/dev';
7
7
  import { runBuildCss } from '#auklet/cli/buildCss';
8
8
  import { runBuild, runBuildJs } from '#auklet/cli/build';
9
9
  import { runOwner, runPublish } from '#auklet/cli/publish';
10
+ import { runInspect } from '#auklet/cli/inspect';
10
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
12
  const getPackageVersion = () => {
12
13
  const packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../package.json'), 'utf8'));
@@ -53,6 +54,10 @@ async function runCli(argv) {
53
54
  .command('owner [...args]', 'Manage npm package owners with pnpm')
54
55
  .allowUnknownOptions()
55
56
  .action(() => runCliCommand(runOwner, getCommandArgs(argv, 'owner')));
57
+ cli
58
+ .command('inspect [...args]', 'Inspect auklet plans without side effects')
59
+ .allowUnknownOptions()
60
+ .action(() => runCliCommand(runInspect, getCommandArgs(argv, 'inspect')));
56
61
  cli
57
62
  .command('version', 'Print auklet version')
58
63
  .action(() => runCliCommand(runVersion, []));
@@ -0,0 +1,87 @@
1
+ import type { AukletConfig, ModuleStyleBuildConfig } from '#auklet/types';
2
+ type CssInspectEntryRow = {
3
+ entry: string;
4
+ parts: Array<string>;
5
+ };
6
+ type CssInspectModuleRow = {
7
+ sourceDir: string;
8
+ imports: Array<string>;
9
+ ownStyles: Array<string>;
10
+ };
11
+ export type CssInspectModel = {
12
+ details: {
13
+ packageName: string;
14
+ packageRoot: string;
15
+ source: string;
16
+ output: string;
17
+ modules: boolean;
18
+ sourceFiles: number;
19
+ styleFiles: number;
20
+ themes: number;
21
+ moduleEntries: number;
22
+ };
23
+ packageEntries: Array<CssInspectEntryRow>;
24
+ themeFiles: Array<{
25
+ theme: string;
26
+ file: string;
27
+ }>;
28
+ styleFiles: Array<string>;
29
+ moduleEntries: Array<CssInspectModuleRow>;
30
+ };
31
+ export declare function runInspectCssCli(args: Array<string>): Promise<number>;
32
+ export declare function resolveInspectCssOptions(args: Array<string>): Promise<{
33
+ cwd: string;
34
+ targets: {
35
+ packageRoot: string;
36
+ packageName: string;
37
+ aukletConfig: {
38
+ build: {
39
+ formats?: Array<import("#auklet/types").PackageBuildFormat>;
40
+ target?: import("#auklet/types").PackageBuildTarget;
41
+ platform?: import("#auklet/types").PackageBuildPlatform;
42
+ banner?: string;
43
+ externals?: Array<string>;
44
+ alias?: Record<string, string>;
45
+ mainFields?: Array<string>;
46
+ globals?: Record<string, string>;
47
+ configureTsdown?: import("#auklet/types").ConfigureTsdown;
48
+ tsconfig?: string;
49
+ } | undefined;
50
+ source?: string;
51
+ output?: string;
52
+ modules?: boolean;
53
+ styles?: import("#auklet/types").StyleOptions;
54
+ };
55
+ config: ModuleStyleBuildConfig;
56
+ }[];
57
+ }>;
58
+ export declare function createCssInspectModel(options: {
59
+ packageRoot: string;
60
+ packageName?: string;
61
+ aukletConfig?: AukletConfig;
62
+ config?: ModuleStyleBuildConfig;
63
+ }): {
64
+ details: {
65
+ packageName: string;
66
+ packageRoot: string;
67
+ source: string;
68
+ output: string;
69
+ modules: boolean;
70
+ sourceFiles: number;
71
+ styleFiles: number;
72
+ themes: number;
73
+ moduleEntries: number;
74
+ };
75
+ packageEntries: CssInspectEntryRow[];
76
+ themeFiles: {
77
+ theme: string;
78
+ file: string;
79
+ }[];
80
+ styleFiles: string[];
81
+ moduleEntries: {
82
+ sourceDir: string;
83
+ imports: string[];
84
+ ownStyles: string[];
85
+ }[];
86
+ };
87
+ export {};
@@ -0,0 +1,303 @@
1
+ import path from 'node:path';
2
+ import { readFileSync } from 'node:fs';
3
+ import { isPlainObject, isString } from 'aidly';
4
+ import { normalizeAukletConfig } from '#auklet/config';
5
+ import { loadAukletConfig } from '#auklet/configLoader';
6
+ import { resolveBuildCliArgs } from '#auklet/cli/buildArgs';
7
+ import { moduleStyleBuildConfig } from '#auklet/css/config';
8
+ import { mergeAukletConfigOverrides } from '#auklet/build/cliOverrides';
9
+ import { createExternalEntryParts, createModuleStyleEntryPlans, createStyleEntryParts, createThemeEntryParts, } from '#auklet/css/core/style/entries';
10
+ import { toPosixPath } from '#auklet/utils';
11
+ import { createAukletLogger } from '#auklet/logger';
12
+ import { StylePackageContext } from '#auklet/css/core/stylePackageContext';
13
+ import { findWorkspaceRoot } from '#auklet/workspace/root';
14
+ import { readPnpmWorkspacePackageInfo } from '#auklet/workspace/packages';
15
+ const packageSeparator = '-'.repeat(84);
16
+ export async function runInspectCssCli(args) {
17
+ const inspectOptions = await resolveInspectCssOptions(args);
18
+ const models = inspectOptions.targets.map((target) => createCssInspectModel(target));
19
+ new CssInspectReporter(inspectOptions.cwd, models).report();
20
+ return 0;
21
+ }
22
+ export async function resolveInspectCssOptions(args) {
23
+ const buildArgs = resolveBuildCliArgs(args.filter((arg) => arg !== '--'));
24
+ if (buildArgs.args.length) {
25
+ throw new Error(`[inspect] unknown inspect css argument: ${buildArgs.args[0]}`);
26
+ }
27
+ const cwd = process.cwd();
28
+ const workspaceRoot = findWorkspaceRoot(cwd);
29
+ const packageRoots = workspaceRoot === cwd
30
+ ? (await readPnpmWorkspacePackageInfo(cwd))
31
+ .map((item) => path.resolve(item.path))
32
+ .filter((packageRoot) => packageRoot !== cwd)
33
+ : [cwd];
34
+ return {
35
+ cwd,
36
+ targets: await Promise.all(packageRoots.map(async (packageRoot) => ({
37
+ packageRoot,
38
+ packageName: readPackageName(packageRoot),
39
+ aukletConfig: mergeAukletConfigOverrides(await loadAukletConfig(packageRoot), buildArgs.config),
40
+ config: moduleStyleBuildConfig,
41
+ }))),
42
+ };
43
+ }
44
+ export function createCssInspectModel(options) {
45
+ const config = options.config ?? moduleStyleBuildConfig;
46
+ const normalizedConfig = normalizeAukletConfig(options.aukletConfig ?? {});
47
+ const context = createBuildContext(options.packageRoot, normalizedConfig);
48
+ const packageContext = new StylePackageContext({
49
+ config,
50
+ context,
51
+ normalizedConfig,
52
+ });
53
+ const moduleEntries = normalizedConfig.modules
54
+ ? createModuleStyleEntryPlans(packageContext)
55
+ : [];
56
+ const packageEntries = createCssInspectEntryRows(config, normalizedConfig, packageContext);
57
+ return {
58
+ details: {
59
+ packageName: options.packageName ?? readPackageName(context.packageRoot),
60
+ packageRoot: context.packageRoot,
61
+ source: context.sourceDir,
62
+ output: context.outputDir,
63
+ modules: normalizedConfig.modules,
64
+ sourceFiles: packageContext.sourceFiles.length,
65
+ styleFiles: packageContext.styleFiles.length,
66
+ themes: packageContext.themeFiles.size,
67
+ moduleEntries: moduleEntries.length,
68
+ },
69
+ packageEntries,
70
+ themeFiles: Array.from(packageContext.themeFiles.entries()).map(([theme, file]) => ({
71
+ theme,
72
+ file: toRelativePath(context.packageRoot, file),
73
+ })),
74
+ styleFiles: packageContext.styleFiles.map((file) => toRelativePath(context.packageRoot, file)),
75
+ moduleEntries: moduleEntries.map((entry) => ({
76
+ sourceDir: toPosixPath(entry.sourceDir),
77
+ imports: sortStyleDependencies(entry.moduleStyleImports),
78
+ ownStyles: entry.ownStyleFiles.map((file) => toRelativePath(context.packageRoot, file)),
79
+ })),
80
+ };
81
+ }
82
+ const createCssInspectEntryRows = (config, normalizedConfig, packageContext) => {
83
+ const rows = [];
84
+ const styleEntryParts = createStyleEntryParts(normalizedConfig);
85
+ const externalEntryParts = createExternalEntryParts(normalizedConfig);
86
+ if (packageContext.styleFiles.length ||
87
+ packageContext.themeFiles.size ||
88
+ hasDependencyParts(styleEntryParts)) {
89
+ rows.push({
90
+ entry: config.output.indexStyleFile,
91
+ parts: styleEntryParts.map(formatEntryPart),
92
+ });
93
+ }
94
+ if (hasDependencyParts(externalEntryParts)) {
95
+ rows.push({
96
+ entry: path.posix.join(config.output.styleDir, config.output.externalStyleFile),
97
+ parts: externalEntryParts.map(formatEntryPart),
98
+ });
99
+ }
100
+ rows.push(...packageContext.themeNames.map((themeName) => ({
101
+ entry: path.posix.join(config.output.styleDir, `${themeName}.css`),
102
+ parts: createThemeEntryParts(normalizedConfig, themeName).map(formatEntryPart),
103
+ })));
104
+ return rows;
105
+ };
106
+ const hasDependencyParts = (parts) => {
107
+ return parts.some((part) => {
108
+ if (part.type !== 'dependencies')
109
+ return false;
110
+ const specifiers = Reflect.get(part, 'specifiers');
111
+ return specifiers.length > 0;
112
+ });
113
+ };
114
+ const createBuildContext = (packageRoot, config) => {
115
+ return {
116
+ packageRoot,
117
+ sourceDir: config.source,
118
+ outputDir: config.output,
119
+ };
120
+ };
121
+ const readPackageName = (packageRoot) => {
122
+ const packageJson = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
123
+ if (!isPlainObject(packageJson) || !isString(packageJson.name)) {
124
+ return path.basename(packageRoot);
125
+ }
126
+ return packageJson.name;
127
+ };
128
+ const formatEntryPart = (part) => {
129
+ if (part.type === 'dependencies') {
130
+ const specifiers = Reflect.get(part, 'specifiers');
131
+ return specifiers.length
132
+ ? `dependencies: ${specifiers.join(', ')}`
133
+ : 'dependencies: none';
134
+ }
135
+ if (part.type === 'themes') {
136
+ const themeNames = Reflect.get(part, 'themeNames');
137
+ return themeNames.length ? `themes: ${themeNames.join(', ')}` : 'themes';
138
+ }
139
+ if (part.type === 'theme') {
140
+ return `theme: ${String(Reflect.get(part, 'themeName'))}`;
141
+ }
142
+ return part.type;
143
+ };
144
+ const toRelativePath = (root, file) => {
145
+ return toPosixPath(path.relative(root, file));
146
+ };
147
+ const sortStyleDependencies = (dependencies) => {
148
+ return [...dependencies].sort((left, right) => {
149
+ const leftExternal = isExternalStyleDependency(left);
150
+ const rightExternal = isExternalStyleDependency(right);
151
+ if (leftExternal !== rightExternal)
152
+ return leftExternal ? -1 : 1;
153
+ return left.localeCompare(right);
154
+ });
155
+ };
156
+ const isExternalStyleDependency = (dependency) => {
157
+ return !dependency.startsWith('.') && !dependency.startsWith('/');
158
+ };
159
+ class CssInspectReporter {
160
+ cwd;
161
+ models;
162
+ logger = createAukletLogger();
163
+ constructor(cwd, models) {
164
+ this.cwd = cwd;
165
+ this.models = models;
166
+ }
167
+ report() {
168
+ this.logger.newline();
169
+ this.logger.result({
170
+ title: this.logger.colors.bold(this.logger.colors.green('CSS inspect')),
171
+ status: 'info',
172
+ body: [this.logger.colors.gray('Read-only CSS plan. No files changed.')],
173
+ details: {
174
+ packages: this.formatValue(this.models.length),
175
+ styles: this.formatValue(this.models.reduce((total, model) => total + model.details.styleFiles, 0)),
176
+ themes: this.formatValue(this.models.reduce((total, model) => total + model.details.themes, 0)),
177
+ entries: this.formatValue(this.models.reduce((total, model) => total + model.details.moduleEntries, 0)),
178
+ },
179
+ });
180
+ for (const [index, model] of this.models.entries()) {
181
+ this.logger.newline();
182
+ if (index > 0) {
183
+ this.writePackageSeparator();
184
+ this.logger.newline();
185
+ }
186
+ this.reportPackage(model);
187
+ }
188
+ this.logger.newline();
189
+ }
190
+ reportPackage(model) {
191
+ this.logger.result({
192
+ title: this.formatPackage(model.details.packageName),
193
+ status: 'info',
194
+ details: {
195
+ root: this.logger.path(toRelativePath(this.cwd, model.details.packageRoot) || '.'),
196
+ source: this.logger.path(model.details.source),
197
+ output: this.logger.path(model.details.output),
198
+ modules: this.formatValue(model.details.modules ? 'on' : 'off'),
199
+ styles: this.formatValue(model.details.styleFiles),
200
+ themes: this.formatValue(model.details.themes),
201
+ entries: this.formatValue(model.details.moduleEntries),
202
+ },
203
+ });
204
+ this.logger.newline();
205
+ this.writeSectionTitle('Output entries');
206
+ this.logger.rows({
207
+ columns: this.formatColumns(['entry', 'parts']),
208
+ rows: model.packageEntries.map((entry) => [
209
+ this.logger.path(entry.entry),
210
+ this.formatEntryParts(entry.parts),
211
+ ]),
212
+ empty: this.logger.colors.gray('No package-level CSS entries found.'),
213
+ });
214
+ this.logger.newline();
215
+ this.writeSectionTitle('Theme files');
216
+ this.logger.rows({
217
+ columns: this.formatColumns(['theme', 'file']),
218
+ rows: model.themeFiles.map((theme) => [
219
+ this.formatTheme(theme.theme),
220
+ this.logger.path(theme.file),
221
+ ]),
222
+ empty: this.logger.colors.gray('No theme files configured.'),
223
+ });
224
+ this.logger.newline();
225
+ this.writeSectionTitle('Module entries');
226
+ this.writeModuleEntries(model);
227
+ }
228
+ writeSectionTitle(title) {
229
+ this.logger.raw(title);
230
+ }
231
+ writePackageSeparator() {
232
+ this.logger.raw(this.logger.colors.gray(packageSeparator));
233
+ }
234
+ writeModuleEntries(model) {
235
+ if (!model.moduleEntries.length) {
236
+ this.logger.empty(model.details.modules
237
+ ? this.logger.colors.gray('No module style entries found.')
238
+ : this.logger.colors.gray('Module output is disabled.'));
239
+ return;
240
+ }
241
+ for (const [index, entry] of model.moduleEntries.entries()) {
242
+ if (index > 0)
243
+ this.logger.newline();
244
+ this.logger.rows({
245
+ title: this.formatSource(entry.sourceDir),
246
+ columns: this.formatColumns(['type', 'file']),
247
+ rows: [
248
+ ...this.createModuleEntryRows('own style', entry.ownStyles, (value) => this.formatOwnStyle(value)),
249
+ ...this.createModuleEntryRows('dependency', entry.imports, (value) => this.formatDependency(value)),
250
+ ],
251
+ });
252
+ }
253
+ }
254
+ createModuleEntryRows(title, values, format) {
255
+ if (!values.length) {
256
+ return [
257
+ [this.logger.colors.gray(title), this.logger.colors.gray('none')],
258
+ ];
259
+ }
260
+ return values.map((value) => [
261
+ this.logger.colors.gray(title),
262
+ format(value),
263
+ ]);
264
+ }
265
+ formatColumns(columns) {
266
+ return columns.map((column) => this.logger.colors.gray(column));
267
+ }
268
+ formatEntryParts(parts) {
269
+ return parts.flatMap((part, index) => [
270
+ ...(index > 0 ? [this.logger.colors.gray(' | ')] : []),
271
+ this.formatEntryPart(part),
272
+ ]);
273
+ }
274
+ formatEntryPart(part) {
275
+ if (part.startsWith('dependencies:')) {
276
+ return this.logger.colors.yellow(part);
277
+ }
278
+ if (part.startsWith('themes:') || part.startsWith('theme:')) {
279
+ return this.logger.colors.hex('#7c3aed')(part);
280
+ }
281
+ if (part === 'module')
282
+ return this.logger.colors.green(part);
283
+ return this.logger.colors.gray(part);
284
+ }
285
+ formatSource(value) {
286
+ return this.logger.colors.bold(this.logger.colors.cyan(value));
287
+ }
288
+ formatDependency(value) {
289
+ return this.logger.colors.yellow(value);
290
+ }
291
+ formatOwnStyle(value) {
292
+ return this.logger.colors.green(value);
293
+ }
294
+ formatTheme(value) {
295
+ return this.logger.colors.bold(this.logger.colors.hex('#7c3aed')(value));
296
+ }
297
+ formatPackage(value) {
298
+ return this.logger.colors.bold(this.logger.colors.hex('#5b21b6')(value));
299
+ }
300
+ formatValue(value) {
301
+ return this.logger.colors.bold(this.logger.colors.cyan(String(value)));
302
+ }
303
+ }
@@ -2,13 +2,6 @@ import path from 'node:path';
2
2
  import { isString } from 'aidly';
3
3
  import { normalizeFileKey } from '#auklet/utils';
4
4
  import { createScopedAukletLogger } from '#auklet/logger';
5
- // package CSS 的 HMR 不能直接走 Vite 原生 CSS 文件链路:
6
- // 1. 浏览器 import 的是 auklet-css:* 虚拟 CSS 模块,不是真实的
7
- // packages/*/src/**/*.css 文件,所以真实 CSS 变化时 Vite 的 modules 可能为空。
8
- // 2. Vite dev 会把 CSS 转成自接受的 JS 模块,重新执行模块里的 updateStyle()
9
- // 才能更新样式,因此这里手动发送 js-update,而不是 css-update。
10
- // 3. @tailwindcss/vite 会在相关 CSS 变化时主动发 full-reload。package CSS 已由
11
- // 这个插件接管 HMR 时,需要在一个很短的窗口内吞掉这次 reload。
12
5
  const FULL_RELOAD_SUPPRESS_MS = 100;
13
6
  const DUPLICATE_UPDATE_IGNORE_MS = 500;
14
7
  const logger = createScopedAukletLogger('css:vite');
@@ -1,16 +1,23 @@
1
+ import { type AukletLogger } from '#auklet/logger';
1
2
  import type { ModuleStyleBuildConfig, ModuleStyleBuildContext } from '#auklet/types';
3
+ type ModuleStyleWatcherLogger = Pick<AukletLogger, 'error'>;
2
4
  export declare class ModuleStyleWatcher {
3
- private readonly config;
4
5
  private readonly context;
6
+ private readonly config;
7
+ private readonly logger;
5
8
  private timer;
9
+ private closed;
6
10
  private isBuilding;
7
11
  private shouldRebuild;
8
12
  private watcher;
9
- constructor(context?: ModuleStyleBuildContext, config?: ModuleStyleBuildConfig);
13
+ private readonly lastErrorLogTimes;
14
+ constructor(context?: ModuleStyleBuildContext, config?: ModuleStyleBuildConfig, logger?: ModuleStyleWatcherLogger | null);
10
15
  watch(): Promise<void>;
11
16
  private rebuild;
12
17
  private refreshWatcher;
18
+ private logError;
13
19
  private scheduleBuild;
14
20
  private shouldRebuildForFile;
15
21
  close(): Promise<void>;
16
22
  }
23
+ export {};
@@ -3,69 +3,112 @@ import path from 'node:path';
3
3
  import chokidar from 'chokidar';
4
4
  import { isString } from 'aidly';
5
5
  import { aukletConfigFiles, aukletDefaultOptions, isAukletConfigFile, } from '#auklet/config';
6
- import { moduleStyleBuildConfig } from '#auklet/css/config';
7
6
  import { SOURCE_MODULE_RE } from '#auklet/css/constants';
7
+ import { moduleStyleBuildConfig } from '#auklet/css/config';
8
8
  import { ModuleStyleBuilder } from '#auklet/css/production/builder';
9
+ import { createAukletLogger } from '#auklet/logger';
10
+ const errorLogInterval = 2_000;
9
11
  export class ModuleStyleWatcher {
10
- config;
11
12
  context;
13
+ config;
14
+ logger;
12
15
  timer = null;
16
+ closed = false;
13
17
  isBuilding = false;
14
18
  shouldRebuild = false;
15
19
  watcher = null;
16
- constructor(context = {}, config = moduleStyleBuildConfig) {
17
- this.config = config;
20
+ lastErrorLogTimes = new Map();
21
+ constructor(context = {}, config = moduleStyleBuildConfig, logger = null) {
18
22
  this.context = {
19
23
  packageRoot: process.cwd(),
20
24
  ...context,
21
25
  };
26
+ this.config = config;
27
+ this.logger = logger ?? createAukletLogger({ scope: 'css' });
22
28
  }
23
29
  async watch() {
30
+ if (this.closed)
31
+ return;
24
32
  await this.rebuild();
25
33
  }
26
34
  async rebuild() {
35
+ if (this.closed)
36
+ return;
27
37
  if (this.isBuilding) {
28
38
  this.shouldRebuild = true;
29
39
  return;
30
40
  }
31
41
  this.isBuilding = true;
32
42
  try {
33
- const builder = new ModuleStyleBuilder(this.context, this.config);
34
- await builder.build();
35
- await this.refreshWatcher();
36
- }
37
- catch (error) {
38
- // Watch mode keeps running after transient build or watcher errors.
43
+ try {
44
+ const builder = new ModuleStyleBuilder(this.context, this.config);
45
+ await builder.build();
46
+ }
47
+ catch (error) {
48
+ this.logError('build', 'CSS build failed; waiting for changes.', error);
49
+ }
50
+ if (!this.closed) {
51
+ try {
52
+ await this.refreshWatcher();
53
+ }
54
+ catch (error) {
55
+ this.logError('watch-refresh', 'CSS watcher failed; waiting for changes.', error);
56
+ }
57
+ }
39
58
  }
40
59
  finally {
41
60
  this.isBuilding = false;
42
- if (this.shouldRebuild) {
61
+ if (this.shouldRebuild && !this.closed) {
43
62
  this.shouldRebuild = false;
44
63
  this.scheduleBuild();
45
64
  }
46
65
  }
47
66
  }
48
67
  async refreshWatcher() {
68
+ if (this.closed)
69
+ return;
49
70
  const aukletConfig = this.context.aukletConfig ?? {};
50
71
  const sourceDir = this.context.source ?? aukletConfig.source ?? aukletDefaultOptions.source;
51
72
  const sourceRoot = path.join(this.context.packageRoot, sourceDir);
52
73
  const configPaths = aukletConfigFiles.map((file) => path.join(this.context.packageRoot, file));
53
74
  const watchPaths = [sourceRoot, ...configPaths].filter((file) => fs.existsSync(file));
54
- await this.watcher?.close();
75
+ const previousWatcher = this.watcher;
76
+ this.watcher = null;
77
+ await previousWatcher?.close();
78
+ if (this.closed)
79
+ return;
55
80
  this.watcher = chokidar.watch(watchPaths, {
56
81
  ignoreInitial: true,
57
82
  interval: 300,
58
83
  usePolling: true,
59
84
  });
60
85
  this.watcher.on('all', (_event, file) => {
86
+ if (this.closed)
87
+ return;
61
88
  if (isString(file) && !this.shouldRebuildForFile(file)) {
62
89
  return;
63
90
  }
64
91
  this.scheduleBuild();
65
92
  });
66
- this.watcher.on('error', () => undefined);
93
+ this.watcher.on('error', (error) => {
94
+ if (this.closed)
95
+ return;
96
+ this.logError('watch-event', 'CSS watcher error; waiting for changes.', error);
97
+ this.scheduleBuild();
98
+ });
99
+ }
100
+ logError(kind, message, error) {
101
+ const now = Date.now();
102
+ const lastLogTime = this.lastErrorLogTimes.get(kind) ?? 0;
103
+ if (now - lastLogTime < errorLogInterval)
104
+ return;
105
+ this.lastErrorLogTimes.set(kind, now);
106
+ this.logger.error(message);
107
+ this.logger.error(error);
67
108
  }
68
109
  scheduleBuild() {
110
+ if (this.closed)
111
+ return;
69
112
  if (this.timer)
70
113
  clearTimeout(this.timer);
71
114
  this.timer = setTimeout(() => {
@@ -81,8 +124,14 @@ export class ModuleStyleWatcher {
81
124
  return this.config.styleExtensions.includes(path.extname(file));
82
125
  }
83
126
  async close() {
84
- if (this.timer)
127
+ this.closed = true;
128
+ this.shouldRebuild = false;
129
+ if (this.timer) {
85
130
  clearTimeout(this.timer);
86
- await this.watcher?.close();
131
+ this.timer = null;
132
+ }
133
+ const watcher = this.watcher;
134
+ this.watcher = null;
135
+ await watcher?.close();
87
136
  }
88
137
  }