auklet 0.1.0 → 0.1.2

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.
@@ -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
  }
@@ -0,0 +1,4 @@
1
+ export declare function findNpmrcWithAuthToken(packageRoot: string, root: string, registry?: string): string | undefined;
2
+ export declare function findNpmrcFiles(packageRoot: string, root: string): string[];
3
+ export declare function hasAuthToken(content: string, registry?: string): boolean;
4
+ export declare function toNpmrcRegistryKey(registry: string): string;
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function findNpmrcWithAuthToken(packageRoot, root, registry) {
4
+ const npmrcFiles = findNpmrcFiles(packageRoot, root);
5
+ return npmrcFiles.find((file) => {
6
+ return hasAuthToken(fs.readFileSync(file, 'utf8'), registry);
7
+ });
8
+ }
9
+ export function findNpmrcFiles(packageRoot, root) {
10
+ const files = [];
11
+ let current = path.resolve(packageRoot);
12
+ const boundary = path.resolve(root);
13
+ while (true) {
14
+ const file = path.join(current, '.npmrc');
15
+ if (fs.existsSync(file))
16
+ files.push(file);
17
+ if (current === boundary)
18
+ break;
19
+ const parent = path.dirname(current);
20
+ if (parent === current)
21
+ break;
22
+ current = parent;
23
+ }
24
+ return files;
25
+ }
26
+ export function hasAuthToken(content, registry) {
27
+ const lines = content
28
+ .split(/\r?\n/)
29
+ .map((line) => line.trim())
30
+ .filter((line) => line && !line.startsWith('#') && !line.startsWith(';'));
31
+ if (!registry)
32
+ return lines.some((line) => line.includes('_authToken'));
33
+ const authKey = `${toNpmrcRegistryKey(registry)}:_authToken`;
34
+ return lines.some((line) => line.startsWith(authKey));
35
+ }
36
+ export function toNpmrcRegistryKey(registry) {
37
+ const url = parseRegistryUrl(registry);
38
+ const pathname = url.pathname.endsWith('/')
39
+ ? url.pathname
40
+ : `${url.pathname}/`;
41
+ return `//${url.host}${pathname}`;
42
+ }
43
+ const parseRegistryUrl = (registry) => {
44
+ try {
45
+ return new URL(registry);
46
+ }
47
+ catch (error) {
48
+ throw new Error(`[publish] invalid publishConfig.registry: ${registry}`, {
49
+ cause: error,
50
+ });
51
+ }
52
+ };
@@ -1,3 +1,4 @@
1
+ import { execa, type Options } from 'execa';
1
2
  import type { WorkspacePackage } from '#auklet/publish/types';
2
3
  export declare class NpmPublishAuthenticationError extends Error {
3
4
  readonly packageRoot: string;
@@ -6,21 +7,32 @@ export declare class NpmPublishAuthenticationError extends Error {
6
7
  export declare class NpmPackageVersionExistsError extends Error {
7
8
  constructor(packageName: string, version: string, registry?: string);
8
9
  }
10
+ export declare function withPnpmTimeout(subprocess: ReturnType<typeof execa>, timeout: number): Promise<import("node_modules/execa/types/return/result").CommonResult<false, Options> & import("node_modules/execa/types/return/result").OmitErrorIfReject<boolean | undefined>>;
9
11
  export declare function ensurePnpm(): Promise<string>;
10
12
  export declare function readPnpmWorkspacePackages(root: string): Promise<WorkspacePackage[]>;
11
13
  export declare function runPnpmBuild(packageRoot: string): Promise<void>;
12
- export declare function runPnpmPublish(packageRoot: string, args: Array<string>): Promise<void>;
14
+ export declare function runPnpmPublish(packageRoot: string, args: Array<string>, options?: {
15
+ token?: string;
16
+ }): Promise<void>;
13
17
  export declare function runPnpmWhoami(packageRoot: string, options?: {
14
18
  packageName?: string;
15
19
  registry?: string;
20
+ timeout?: number;
21
+ token?: string;
16
22
  }): Promise<string>;
17
23
  export declare function hasPublishedPackageVersion(packageRoot: string, packageName: string, version: string, options?: {
18
24
  registry?: string;
25
+ timeout?: number;
26
+ token?: string;
19
27
  }): Promise<boolean>;
20
28
  export declare function runPnpmOwnerAdd(packageName: string, user: string, options: {
21
29
  cwd: string;
22
30
  otp?: string;
23
31
  }): Promise<void>;
32
+ export declare function createPnpmAuthEnv(token?: string): {
33
+ NODE_AUTH_TOKEN: string;
34
+ NPM_TOKEN: string;
35
+ } | undefined;
24
36
  export declare function hasNpmAuthChallenge(result: {
25
37
  stdout?: unknown;
26
38
  stderr?: unknown;
@@ -17,15 +17,43 @@ export class NpmPackageVersionExistsError extends Error {
17
17
  }
18
18
  }
19
19
  const runPnpm = async (args, options = {}) => {
20
- return execa('pnpm', args, {
20
+ const timeout = options.timeout;
21
+ const subprocess = execa('pnpm', args, {
21
22
  reject: false,
22
23
  ...options,
24
+ timeout: undefined,
23
25
  });
26
+ if (!timeout)
27
+ return subprocess;
28
+ return withPnpmTimeout(subprocess, timeout);
24
29
  };
30
+ export async function withPnpmTimeout(subprocess, timeout) {
31
+ let timeoutId = null;
32
+ const result = await Promise.race([
33
+ subprocess,
34
+ new Promise((resolve) => {
35
+ timeoutId = setTimeout(() => {
36
+ const error = new Error(`pnpm command timed out after ${timeout}ms.`);
37
+ subprocess.kill('SIGKILL', error);
38
+ resolve({
39
+ failed: true,
40
+ timedOut: true,
41
+ exitCode: undefined,
42
+ stdout: '',
43
+ stderr: error.message,
44
+ });
45
+ }, timeout);
46
+ }),
47
+ ]);
48
+ if (timeoutId)
49
+ clearTimeout(timeoutId);
50
+ subprocess.catch(() => { });
51
+ return result;
52
+ }
25
53
  export async function ensurePnpm() {
26
54
  const result = await runPnpm(['--version']);
27
55
  const stdout = String(result.stdout ?? '');
28
- if (result.failed || !stdout) {
56
+ if (hasFailedPnpmResult(result) || !stdout) {
29
57
  throw new Error('[publish] pnpm is required for publishing.\n' +
30
58
  '[publish] Install pnpm first:\n' +
31
59
  ' corepack enable\n' +
@@ -57,19 +85,20 @@ export async function runPnpmBuild(packageRoot) {
57
85
  cwd: packageRoot,
58
86
  stdio: 'inherit',
59
87
  });
60
- if (result.exitCode) {
88
+ if (hasFailedPnpmResult(result)) {
61
89
  throw new Error(`[publish] build failed at ${packageRoot}.`);
62
90
  }
63
91
  }
64
- export async function runPnpmPublish(packageRoot, args) {
92
+ export async function runPnpmPublish(packageRoot, args, options = {}) {
65
93
  const isDryRun = args.includes('--dry-run');
66
94
  const result = await runPnpm(['publish', ...args], {
67
95
  cwd: packageRoot,
96
+ env: createPnpmAuthEnv(options.token),
68
97
  stdio: isDryRun ? 'pipe' : 'inherit',
69
98
  });
70
99
  if (isDryRun)
71
100
  writeProcessOutput(result);
72
- if (result.exitCode) {
101
+ if (hasFailedPnpmResult(result)) {
73
102
  if (hasNpmAuthChallenge(result)) {
74
103
  throw new NpmPublishAuthenticationError(packageRoot);
75
104
  }
@@ -82,12 +111,16 @@ export async function runPnpmWhoami(packageRoot, options = {}) {
82
111
  args.push('--registry', options.registry);
83
112
  const result = await runPnpm(args, {
84
113
  cwd: packageRoot,
114
+ env: createPnpmAuthEnv(options.token),
115
+ timeout: options.timeout,
85
116
  });
86
- if (result.exitCode) {
117
+ if (hasFailedPnpmResult(result)) {
87
118
  const target = options.packageName ? ` for ${options.packageName}` : '';
88
119
  const registry = options.registry ? ` at ${options.registry}` : '';
120
+ const reason = getPnpmFailureReason(result);
89
121
  throw new Error(`[publish] npm authentication is required${target}${registry} before publishing.\n` +
90
- '[publish] Run `pnpm login` or configure an npm token before retrying.');
122
+ '[publish] Run `pnpm login` or configure an npm token before retrying.' +
123
+ (reason ? `\n[publish] Reason: ${reason}` : ''));
91
124
  }
92
125
  return String(result.stdout ?? '').trim();
93
126
  }
@@ -97,12 +130,17 @@ export async function hasPublishedPackageVersion(packageRoot, packageName, versi
97
130
  args.push('--registry', options.registry);
98
131
  const result = await runPnpm(args, {
99
132
  cwd: packageRoot,
133
+ env: createPnpmAuthEnv(options.token),
134
+ timeout: options.timeout,
100
135
  });
101
- if (!result.exitCode)
136
+ if (!hasFailedPnpmResult(result)) {
102
137
  return String(result.stdout ?? '').trim() === version;
138
+ }
103
139
  if (isPackageNotFound(result))
104
140
  return false;
105
- throw new Error(`[publish] failed to check published version for ${packageName}@${version}.`);
141
+ const reason = getPnpmFailureReason(result);
142
+ throw new Error(`[publish] failed to check published version for ${packageName}@${version}.` +
143
+ (reason ? `\n[publish] Reason: ${reason}` : ''));
106
144
  }
107
145
  export async function runPnpmOwnerAdd(packageName, user, options) {
108
146
  const args = ['owner', 'add', user, packageName];
@@ -112,10 +150,18 @@ export async function runPnpmOwnerAdd(packageName, user, options) {
112
150
  cwd: options.cwd,
113
151
  stdio: 'inherit',
114
152
  });
115
- if (result.exitCode) {
153
+ if (hasFailedPnpmResult(result)) {
116
154
  throw new Error(`[publish] pnpm owner add failed for ${user} -> ${packageName}.`);
117
155
  }
118
156
  }
157
+ export function createPnpmAuthEnv(token) {
158
+ if (!token)
159
+ return undefined;
160
+ return {
161
+ NODE_AUTH_TOKEN: token,
162
+ NPM_TOKEN: token,
163
+ };
164
+ }
119
165
  const isWorkspacePackage = (value) => {
120
166
  if (!isPlainObject(value)) {
121
167
  return false;
@@ -128,6 +174,16 @@ const isWorkspacePackage = (value) => {
128
174
  value.version.length > 0 &&
129
175
  (value.private === undefined || typeof value.private === 'boolean'));
130
176
  };
177
+ const hasFailedPnpmResult = (result) => {
178
+ return result.failed === true || result.exitCode !== 0;
179
+ };
180
+ const getPnpmFailureReason = (result) => {
181
+ const stderr = String(result.stderr ?? '').trim();
182
+ if (stderr)
183
+ return stderr;
184
+ const stdout = String(result.stdout ?? '').trim();
185
+ return stdout || null;
186
+ };
131
187
  function throwInvalidWorkspacePackages() {
132
188
  throw new Error('[publish] failed to read pnpm workspace packages.\n' +
133
189
  '[publish] Expected `pnpm list -r --depth -1 --json` to return package objects with name/path/version.');
@@ -2,6 +2,11 @@ import { runPnpmPublish } from '#auklet/publish/api/pnpmApi';
2
2
  import { createPublishArgs } from '#auklet/publish/api/publishArgs';
3
3
  export class PnpmPublishApi {
4
4
  async publish(target, options) {
5
- await runPnpmPublish(target.packageRoot, createPublishArgs(target, options));
5
+ const args = createPublishArgs(target, options);
6
+ if (options.token) {
7
+ await runPnpmPublish(target.packageRoot, args, { token: options.token });
8
+ return;
9
+ }
10
+ await runPnpmPublish(target.packageRoot, args);
6
11
  }
7
12
  }
@@ -1,2 +1,3 @@
1
1
  import type { PublishOptions, PublishTarget } from '#auklet/publish/types';
2
2
  export declare function createPublishArgs(target: PublishTarget, options: PublishOptions): string[];
3
+ export declare function resolvePublishTag(versionSpec?: string): "alpha" | "beta" | null;
@@ -8,7 +8,7 @@ export function createPublishArgs(target, options) {
8
8
  args.push('--ignore-scripts');
9
9
  if (shouldAddPublicAccess(target))
10
10
  args.push('--access', 'public');
11
- const tag = getPublishTag(options.version);
11
+ const tag = resolvePublishTag(options.version);
12
12
  if (tag)
13
13
  args.push('--tag', tag);
14
14
  return args;
@@ -18,8 +18,8 @@ const shouldAddPublicAccess = (target) => {
18
18
  !target.private &&
19
19
  !target.packageJson.publishConfig?.access);
20
20
  };
21
- const getPublishTag = (versionSpec) => {
21
+ export function resolvePublishTag(versionSpec) {
22
22
  if (versionSpec === 'alpha' || versionSpec === 'beta')
23
23
  return versionSpec;
24
24
  return null;
25
- };
25
+ }
@@ -0,0 +1 @@
1
+ export declare function getPublishRegistry(publishConfig: unknown): string | undefined;
@@ -0,0 +1,7 @@
1
+ import { isPlainObject, isString } from 'aidly';
2
+ export function getPublishRegistry(publishConfig) {
3
+ if (!isPlainObject(publishConfig))
4
+ return undefined;
5
+ const registry = Reflect.get(publishConfig, 'registry');
6
+ return isString(registry) && registry.length > 0 ? registry : undefined;
7
+ }
@@ -1,2 +1,14 @@
1
1
  export declare function runPublishCli(args: Array<string>): Promise<void>;
2
+ export declare function resolvePublishCliOptions(args: Array<string>, cwd?: string): {
3
+ cwd: string;
4
+ otp: string | undefined;
5
+ filters: string[];
6
+ version: string | undefined;
7
+ git: boolean;
8
+ format: boolean;
9
+ dryRun: boolean;
10
+ allowDirty: boolean;
11
+ ignoreScripts: boolean;
12
+ token: string | undefined;
13
+ };
2
14
  export declare function runOwnerCli(args: Array<string>): Promise<void>;
@@ -11,28 +11,22 @@ const publishFlags = new Set([
11
11
  'format',
12
12
  'git',
13
13
  'otp',
14
+ 'token',
14
15
  'ignore-scripts',
15
16
  'allow-dirty',
16
17
  ]);
17
18
  const ownerFlags = new Set(['_', 'filter', 'package', 'otp']);
18
19
  export async function runPublishCli(args) {
19
- const cliArgs = stripLeadingArgsSeparator(args);
20
- validateNoPrefixedFlags(cliArgs, new Set(['--no-format', '--no-git']));
21
- const argv = minimist(cliArgs, {
22
- string: ['filter', 'version', 'otp'],
23
- boolean: ['dry-run', 'format', 'git', 'ignore-scripts', 'allow-dirty'],
24
- default: {
25
- git: true,
26
- format: true,
27
- },
28
- });
29
- validateFlags(argv, publishFlags);
20
+ await ensurePnpm();
21
+ await new PublishRunner(resolvePublishCliOptions(args)).run();
22
+ }
23
+ export function resolvePublishCliOptions(args, cwd = process.cwd()) {
24
+ const argv = parsePublishArgs(args);
30
25
  if (argv._.length) {
31
26
  throw new Error(`[publish] unknown publish argument: ${argv._.join(' ')}`);
32
27
  }
33
- await ensurePnpm();
34
- await new PublishRunner({
35
- cwd: process.cwd(),
28
+ return {
29
+ cwd,
36
30
  otp: stringOption(argv.otp),
37
31
  filters: toArray(argv.filter),
38
32
  version: stringOption(argv.version),
@@ -41,8 +35,23 @@ export async function runPublishCli(args) {
41
35
  dryRun: argv['dry-run'] === true,
42
36
  allowDirty: argv['allow-dirty'] === true,
43
37
  ignoreScripts: argv['ignore-scripts'] === true,
44
- }).run();
38
+ token: stringOption(argv.token),
39
+ };
45
40
  }
41
+ const parsePublishArgs = (args) => {
42
+ const cliArgs = stripLeadingArgsSeparator(args);
43
+ validateNoPrefixedFlags(cliArgs, new Set(['--no-format', '--no-git']));
44
+ const argv = minimist(cliArgs, {
45
+ string: ['filter', 'version', 'otp', 'token'],
46
+ boolean: ['dry-run', 'format', 'git', 'ignore-scripts', 'allow-dirty'],
47
+ default: {
48
+ git: true,
49
+ format: true,
50
+ },
51
+ });
52
+ validateFlags(argv, publishFlags);
53
+ return argv;
54
+ };
46
55
  export async function runOwnerCli(args) {
47
56
  const cliArgs = stripLeadingArgsSeparator(args);
48
57
  validateNoPrefixedFlags(cliArgs, new Set());
@@ -0,0 +1,29 @@
1
+ import { type PublishRegistryCheck } from '#auklet/publish/inspectRegistry';
2
+ import { type PackFileCheck } from '#auklet/publish/inspectPack';
3
+ import type { PublishOptions, PublishPlan } from '#auklet/publish/types';
4
+ export declare function runInspectPublishCli(args: Array<string>): Promise<0 | 1>;
5
+ export declare function createPublishInspectModel(options: PublishOptions, plan: PublishPlan, packageFileChecks?: Array<PackFileCheck>, registryChecks?: Array<PublishRegistryCheck>): {
6
+ details: {
7
+ mode: "monorepo" | "single";
8
+ publish: string;
9
+ packages: number;
10
+ version: string;
11
+ tag: string;
12
+ git: string;
13
+ format: string;
14
+ root: string;
15
+ };
16
+ packageFileChecks: PackFileCheck[];
17
+ targets: {
18
+ packageName: string;
19
+ version: string;
20
+ publishVersion: string;
21
+ registry: string;
22
+ access: string;
23
+ }[];
24
+ registryChecks: PublishRegistryCheck[];
25
+ hooks: {
26
+ name: string;
27
+ configured: boolean;
28
+ }[];
29
+ };