esbuild-plugin-scriptable
Advanced tools
Comparing version 0.7.1 to 0.7.2
@@ -9,61 +9,102 @@ 'use strict'; | ||
const timers = /* @__PURE__ */ new Map(); | ||
function createLogger(prefix, options = {}) { | ||
const { level = "info" } = options; | ||
const formatPrefix = () => chalk__default.default.cyan(`[${prefix}]`); | ||
const formatTime = (ms) => chalk__default.default.gray(`(${ms}ms)`); | ||
return { | ||
info: (message, ...args) => { | ||
console.log(`${formatPrefix()} ${message}`, ...args); | ||
}, | ||
warn: (message, ...args) => { | ||
console.warn(`${formatPrefix()} ${chalk__default.default.yellow("warning")} ${message}`, ...args); | ||
}, | ||
error: (message, ...args) => { | ||
console.error(`${formatPrefix()} ${chalk__default.default.red("error")} ${message}`, ...args); | ||
}, | ||
success: (message, ...args) => { | ||
console.log(`${formatPrefix()} ${chalk__default.default.green("\u2713")} ${message}`, ...args); | ||
}, | ||
report: (stats) => { | ||
const { total, success, skipped, failed } = stats; | ||
const hasIssues = failed > 0 || skipped > 0; | ||
console.log("\n" + formatPrefix() + " " + chalk__default.default.bold("Build Summary:")); | ||
const rows = [ | ||
[` ${chalk__default.default.bold("Total files:")}`, `${total}`], | ||
[` ${chalk__default.default.bold("Processed:")}`, `${chalk__default.default.green(success)}`], | ||
[` ${chalk__default.default.bold("Skipped:")}`, `${skipped > 0 ? chalk__default.default.yellow(skipped) : skipped}`], | ||
[` ${chalk__default.default.bold("Failed:")}`, `${failed > 0 ? chalk__default.default.red(failed) : failed}`] | ||
]; | ||
rows.forEach(([label, value]) => { | ||
console.log(`${label.padEnd(20)}${value}`); | ||
}); | ||
if (hasIssues) { | ||
console.log("\n" + formatPrefix() + " " + chalk__default.default.yellow("\u26A0\uFE0F Some files were not processed successfully")); | ||
} else { | ||
console.log("\n" + formatPrefix() + " " + chalk__default.default.green("\u2728 All files processed successfully")); | ||
} | ||
console.log(); | ||
}, | ||
time: (label) => { | ||
timers.set(label, performance.now()); | ||
}, | ||
timeEnd: (label) => { | ||
const start = timers.get(label); | ||
function convertEsbuildLogLevel(level) { | ||
switch (level) { | ||
case "debug": | ||
return "verbose"; | ||
case "info": | ||
return "info"; | ||
case "warning": | ||
return "warn"; | ||
case "error": | ||
return "error"; | ||
case "silent": | ||
return "silent"; | ||
default: | ||
return "info"; | ||
} | ||
} | ||
class ConsoleLogger { | ||
constructor(prefix, level = "info") { | ||
this.prefix = prefix; | ||
this.level = level; | ||
} | ||
timers = /* @__PURE__ */ new Map(); | ||
LOG_LEVELS = { | ||
silent: 0, | ||
error: 1, | ||
warn: 2, | ||
info: 3, | ||
verbose: 4 | ||
}; | ||
formatPrefix() { | ||
return chalk__default.default.cyan(`[${this.prefix}]`); | ||
} | ||
formatTime(ms) { | ||
return chalk__default.default.gray(`(${ms}ms)`); | ||
} | ||
shouldLog(messageLevel) { | ||
if (this.level === "silent") { | ||
return false; | ||
} | ||
return this.LOG_LEVELS[messageLevel] <= this.LOG_LEVELS[this.level]; | ||
} | ||
info(message, ...args) { | ||
if (this.shouldLog("info")) { | ||
console.log(`${this.formatPrefix()} ${message}`, ...args); | ||
} | ||
} | ||
warn(message, ...args) { | ||
if (this.shouldLog("warn")) { | ||
console.warn(`${this.formatPrefix()} ${chalk__default.default.yellow("warning")} ${message}`, ...args); | ||
} | ||
} | ||
error(message, ...args) { | ||
if (this.shouldLog("error")) { | ||
console.error(`${this.formatPrefix()} ${chalk__default.default.red("error")} ${message}`, ...args); | ||
} | ||
} | ||
success(message, ...args) { | ||
if (this.shouldLog("info")) { | ||
console.log(`${this.formatPrefix()} ${chalk__default.default.green("\u2713")} ${message}`, ...args); | ||
} | ||
} | ||
verbose(message, ...args) { | ||
if (this.shouldLog("verbose")) { | ||
console.log(`${this.formatPrefix()} ${chalk__default.default.gray(message)}`, ...args); | ||
} | ||
} | ||
time(label) { | ||
if (this.shouldLog("verbose")) { | ||
this.timers.set(label, performance.now()); | ||
} | ||
} | ||
timeEnd(label) { | ||
if (this.shouldLog("verbose")) { | ||
const start = this.timers.get(label); | ||
if (start) { | ||
const duration = Math.round(performance.now() - start); | ||
console.log(`${formatPrefix()} ${label} ${formatTime(duration)}`); | ||
timers.delete(label); | ||
console.log(`${this.formatPrefix()} ${label} ${this.formatTime(duration)}`); | ||
this.timers.delete(label); | ||
} | ||
}, | ||
verbose: (message, ...args) => { | ||
if (level === "verbose") { | ||
console.log(`${formatPrefix()} ${chalk__default.default.gray(message)}`, ...args); | ||
} | ||
} | ||
}; | ||
} | ||
report(stats) { | ||
if (this.level === "silent") return; | ||
const { total, success, skipped, failed } = stats; | ||
const summary = [ | ||
`Total: ${total}`, | ||
`\u2713 Success: ${chalk__default.default.green(success)}`, | ||
skipped > 0 ? `\u26A0\uFE0F Skipped: ${chalk__default.default.yellow(skipped)}` : null, | ||
failed > 0 ? `\u274C Failed: ${chalk__default.default.red(failed)}` : null | ||
].filter(Boolean).join(" "); | ||
console.log(`${this.formatPrefix()} Build Result: ${summary}`); | ||
} | ||
} | ||
function createLogger(prefix, options = {}) { | ||
return new ConsoleLogger(prefix, options.level ?? "info"); | ||
} | ||
exports.convertEsbuildLogLevel = convertEsbuildLogLevel; | ||
exports.createLogger = createLogger; | ||
//# sourceMappingURL=logger.js.map | ||
//# sourceMappingURL=logger.js.map |
@@ -15,2 +15,3 @@ 'use strict'; | ||
const { | ||
logLevel = "auto", | ||
manifestExtensions = DEFAULT_MANIFEST_EXTENSIONS, | ||
@@ -24,6 +25,7 @@ warnOnMissingManifest = true, | ||
} = options; | ||
const logger$1 = logger.createLogger("scriptable-banner"); | ||
return { | ||
name: "scriptable-banner", | ||
setup(build) { | ||
const level = logLevel === "auto" ? logger.convertEsbuildLogLevel(build.initialOptions.logLevel || "info") : logLevel; | ||
const logger$1 = logger.createLogger("scriptable-banner", { level }); | ||
let manifestCache = /* @__PURE__ */ new Map(); | ||
@@ -84,6 +86,9 @@ if (!build.initialOptions.metafile) { | ||
}; | ||
const outputs = result.metafile.outputs; | ||
for (const outputPath of Object.keys(outputs)) { | ||
stats.total++; | ||
const outputMeta = outputs[outputPath]; | ||
const outputs = Object.keys(result.metafile.outputs); | ||
stats.total = outputs.length; | ||
logger$1.info(`Processing ${stats.total} files...`); | ||
for (const [index, outputPath] of outputs.entries()) { | ||
const progress = Math.round((index + 1) / stats.total * 100); | ||
logger$1.verbose(`[${progress}%] Processing: ${outputPath}`); | ||
const outputMeta = result.metafile.outputs[outputPath]; | ||
const entryPoint = outputMeta?.entryPoint ?? ""; | ||
@@ -108,3 +113,3 @@ const normalizedEntryPath = entryPoint ? utils.normalizeAndResolvePath(entryPoint) : ""; | ||
stats.success++; | ||
logger$1.success(`Added banner to ${outputPath}`); | ||
logger$1.success(`[${progress}%] Added banner to ${outputPath}`); | ||
} | ||
@@ -118,8 +123,6 @@ } else { | ||
stats.success++; | ||
logger$1.success(`Added banner to ${outputPath}`); | ||
logger$1.success(`[${progress}%] Added banner to ${outputPath}`); | ||
} catch (error) { | ||
stats.failed++; | ||
logger$1.error( | ||
`Failed to process ${outputPath}: ${error instanceof Error ? error.message : String(error)}` | ||
); | ||
logger$1.error(`[${progress}%] Failed to process ${outputPath}`); | ||
} | ||
@@ -129,3 +132,3 @@ } | ||
stats.failed++; | ||
logger$1.error(`Failed to process ${outputPath}`, error); | ||
logger$1.error(`[${progress}%] Failed to process ${outputPath}`); | ||
} | ||
@@ -132,0 +135,0 @@ } |
'use strict'; | ||
var generateScriptableBanner = require('@scriptables/manifest/generateScriptableBanner'); | ||
var mergeScriptableBanner = require('@scriptables/manifest/mergeScriptableBanner'); | ||
var findicloud = require('findicloud'); | ||
@@ -26,3 +26,3 @@ var promises = require('fs/promises'); | ||
const { | ||
verbose = false, | ||
logLevel = "auto", | ||
continueOnError = true, | ||
@@ -35,5 +35,2 @@ scriptableDir, | ||
const filterFile = utils.createPatternMatcher(patterns); | ||
const logger$1 = logger.createLogger("scriptable-deploy", { | ||
level: verbose ? "verbose" : "info" | ||
}); | ||
let targetDir; | ||
@@ -43,2 +40,4 @@ return { | ||
setup(build) { | ||
const level = logLevel === "auto" ? logger.convertEsbuildLogLevel(build.initialOptions.logLevel || "info") : logLevel; | ||
const logger$1 = logger.createLogger("scriptable-deploy", { level }); | ||
if (!build.initialOptions.metafile) { | ||
@@ -92,3 +91,6 @@ logger$1.verbose("Automatically enabling metafile option"); | ||
stats.total = outputs.length; | ||
for (const file of outputs) { | ||
logger$1.info(`Deploying ${stats.total} files to ${targetDir}...`); | ||
for (const [index, file] of outputs.entries()) { | ||
const progress = Math.round((index + 1) / stats.total * 100); | ||
logger$1.verbose(`[${progress}%] Processing: ${file}`); | ||
try { | ||
@@ -126,7 +128,6 @@ const output = result.metafile.outputs[file]; | ||
try { | ||
const banner = generateScriptableBanner.generateScriptableBanner(manifest); | ||
const newContent = banner + "\n" + content; | ||
await promises.writeFile(targetPath, newContent); | ||
const { banner: newBanner, content: scriptContent } = mergeScriptableBanner.mergeScriptableBanner(content, manifest); | ||
await promises.writeFile(targetPath, newBanner + scriptContent); | ||
stats.success++; | ||
logger$1.success(`Deployed with banner: ${file} -> ${targetPath}`); | ||
logger$1.success(`Deployed with updated banner: ${file} -> ${targetPath}`); | ||
continue; | ||
@@ -133,0 +134,0 @@ } catch (error) { |
@@ -1,4 +0,5 @@ | ||
export { ScriptablePluginOptions, scriptableBanner } from './scriptable-banner.js'; | ||
export { ScriptableBannerOptions, scriptableBanner } from './scriptable-banner.js'; | ||
export { ScriptableDeployOptions, scriptableDeploy } from './scriptable-deploy.js'; | ||
export { DeployTarget, FileFilterOptions, FilterPattern } from './types.js'; | ||
import 'esbuild'; | ||
import './logger.js'; |
@@ -0,1 +1,5 @@ | ||
import { LogLevel as LogLevel$1 } from 'esbuild'; | ||
type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'verbose'; | ||
declare function convertEsbuildLogLevel(level: LogLevel$1): LogLevel; | ||
interface Logger { | ||
@@ -17,6 +21,6 @@ info: (message: string, ...args: any[]) => void; | ||
interface LoggerOptions { | ||
level?: 'verbose' | 'info'; | ||
level?: LogLevel; | ||
} | ||
declare function createLogger(prefix: string, options?: LoggerOptions): Logger; | ||
export { type Logger, type LoggerOptions, createLogger }; | ||
export { type LogLevel, type Logger, type LoggerOptions, convertEsbuildLogLevel, createLogger }; |
import chalk from 'chalk'; | ||
const timers = /* @__PURE__ */ new Map(); | ||
function createLogger(prefix, options = {}) { | ||
const { level = "info" } = options; | ||
const formatPrefix = () => chalk.cyan(`[${prefix}]`); | ||
const formatTime = (ms) => chalk.gray(`(${ms}ms)`); | ||
return { | ||
info: (message, ...args) => { | ||
console.log(`${formatPrefix()} ${message}`, ...args); | ||
}, | ||
warn: (message, ...args) => { | ||
console.warn(`${formatPrefix()} ${chalk.yellow("warning")} ${message}`, ...args); | ||
}, | ||
error: (message, ...args) => { | ||
console.error(`${formatPrefix()} ${chalk.red("error")} ${message}`, ...args); | ||
}, | ||
success: (message, ...args) => { | ||
console.log(`${formatPrefix()} ${chalk.green("\u2713")} ${message}`, ...args); | ||
}, | ||
report: (stats) => { | ||
const { total, success, skipped, failed } = stats; | ||
const hasIssues = failed > 0 || skipped > 0; | ||
console.log("\n" + formatPrefix() + " " + chalk.bold("Build Summary:")); | ||
const rows = [ | ||
[` ${chalk.bold("Total files:")}`, `${total}`], | ||
[` ${chalk.bold("Processed:")}`, `${chalk.green(success)}`], | ||
[` ${chalk.bold("Skipped:")}`, `${skipped > 0 ? chalk.yellow(skipped) : skipped}`], | ||
[` ${chalk.bold("Failed:")}`, `${failed > 0 ? chalk.red(failed) : failed}`] | ||
]; | ||
rows.forEach(([label, value]) => { | ||
console.log(`${label.padEnd(20)}${value}`); | ||
}); | ||
if (hasIssues) { | ||
console.log("\n" + formatPrefix() + " " + chalk.yellow("\u26A0\uFE0F Some files were not processed successfully")); | ||
} else { | ||
console.log("\n" + formatPrefix() + " " + chalk.green("\u2728 All files processed successfully")); | ||
} | ||
console.log(); | ||
}, | ||
time: (label) => { | ||
timers.set(label, performance.now()); | ||
}, | ||
timeEnd: (label) => { | ||
const start = timers.get(label); | ||
function convertEsbuildLogLevel(level) { | ||
switch (level) { | ||
case "debug": | ||
return "verbose"; | ||
case "info": | ||
return "info"; | ||
case "warning": | ||
return "warn"; | ||
case "error": | ||
return "error"; | ||
case "silent": | ||
return "silent"; | ||
default: | ||
return "info"; | ||
} | ||
} | ||
class ConsoleLogger { | ||
constructor(prefix, level = "info") { | ||
this.prefix = prefix; | ||
this.level = level; | ||
} | ||
timers = /* @__PURE__ */ new Map(); | ||
LOG_LEVELS = { | ||
silent: 0, | ||
error: 1, | ||
warn: 2, | ||
info: 3, | ||
verbose: 4 | ||
}; | ||
formatPrefix() { | ||
return chalk.cyan(`[${this.prefix}]`); | ||
} | ||
formatTime(ms) { | ||
return chalk.gray(`(${ms}ms)`); | ||
} | ||
shouldLog(messageLevel) { | ||
if (this.level === "silent") { | ||
return false; | ||
} | ||
return this.LOG_LEVELS[messageLevel] <= this.LOG_LEVELS[this.level]; | ||
} | ||
info(message, ...args) { | ||
if (this.shouldLog("info")) { | ||
console.log(`${this.formatPrefix()} ${message}`, ...args); | ||
} | ||
} | ||
warn(message, ...args) { | ||
if (this.shouldLog("warn")) { | ||
console.warn(`${this.formatPrefix()} ${chalk.yellow("warning")} ${message}`, ...args); | ||
} | ||
} | ||
error(message, ...args) { | ||
if (this.shouldLog("error")) { | ||
console.error(`${this.formatPrefix()} ${chalk.red("error")} ${message}`, ...args); | ||
} | ||
} | ||
success(message, ...args) { | ||
if (this.shouldLog("info")) { | ||
console.log(`${this.formatPrefix()} ${chalk.green("\u2713")} ${message}`, ...args); | ||
} | ||
} | ||
verbose(message, ...args) { | ||
if (this.shouldLog("verbose")) { | ||
console.log(`${this.formatPrefix()} ${chalk.gray(message)}`, ...args); | ||
} | ||
} | ||
time(label) { | ||
if (this.shouldLog("verbose")) { | ||
this.timers.set(label, performance.now()); | ||
} | ||
} | ||
timeEnd(label) { | ||
if (this.shouldLog("verbose")) { | ||
const start = this.timers.get(label); | ||
if (start) { | ||
const duration = Math.round(performance.now() - start); | ||
console.log(`${formatPrefix()} ${label} ${formatTime(duration)}`); | ||
timers.delete(label); | ||
console.log(`${this.formatPrefix()} ${label} ${this.formatTime(duration)}`); | ||
this.timers.delete(label); | ||
} | ||
}, | ||
verbose: (message, ...args) => { | ||
if (level === "verbose") { | ||
console.log(`${formatPrefix()} ${chalk.gray(message)}`, ...args); | ||
} | ||
} | ||
}; | ||
} | ||
report(stats) { | ||
if (this.level === "silent") return; | ||
const { total, success, skipped, failed } = stats; | ||
const summary = [ | ||
`Total: ${total}`, | ||
`\u2713 Success: ${chalk.green(success)}`, | ||
skipped > 0 ? `\u26A0\uFE0F Skipped: ${chalk.yellow(skipped)}` : null, | ||
failed > 0 ? `\u274C Failed: ${chalk.red(failed)}` : null | ||
].filter(Boolean).join(" "); | ||
console.log(`${this.formatPrefix()} Build Result: ${summary}`); | ||
} | ||
} | ||
function createLogger(prefix, options = {}) { | ||
return new ConsoleLogger(prefix, options.level ?? "info"); | ||
} | ||
export { createLogger }; | ||
export { convertEsbuildLogLevel, createLogger }; | ||
//# sourceMappingURL=logger.js.map | ||
//# sourceMappingURL=logger.js.map |
import { Plugin } from 'esbuild'; | ||
import { LogLevel } from './logger.js'; | ||
interface ScriptablePluginOptions { | ||
interface ScriptableBannerOptions { | ||
/** | ||
* Log level for the plugin | ||
* 'auto' will use esbuild's logLevel | ||
* Default: 'auto' | ||
*/ | ||
logLevel?: LogLevel | 'auto'; | ||
/** Custom manifest file extensions */ | ||
@@ -19,4 +26,4 @@ manifestExtensions?: string[]; | ||
} | ||
declare function scriptableBanner(options?: ScriptablePluginOptions): Plugin; | ||
declare function scriptableBanner(options?: ScriptableBannerOptions): Plugin; | ||
export { type ScriptablePluginOptions, scriptableBanner }; | ||
export { type ScriptableBannerOptions, scriptableBanner }; |
@@ -7,11 +7,12 @@ import { generateScriptableBanner } from "@scriptables/manifest/generateScriptableBanner"; | ||
import { dirname, basename, resolve } from "path"; | ||
import { createLogger } from "./logger.js"; | ||
import { convertEsbuildLogLevel, createLogger } from "./logger.js"; | ||
import { isMatch, normalizeAndResolvePath, isSamePath, waitForFile } from "./utils.js"; | ||
const DEFAULT_MANIFEST_EXTENSIONS = [".manifest.json", ".manifest", ".json"]; | ||
function scriptableBanner(options = {}) { | ||
const { manifestExtensions = DEFAULT_MANIFEST_EXTENSIONS, warnOnMissingManifest = true, warnOnInvalidManifest = true, warnOnMissingEntry = true, allowExtraManifestKeys = true, getManifestPath, patterns = ["*.{ts,tsx,js,jsx}"] } = options; | ||
const logger = createLogger("scriptable-banner"); | ||
const { logLevel = "auto", manifestExtensions = DEFAULT_MANIFEST_EXTENSIONS, warnOnMissingManifest = true, warnOnInvalidManifest = true, warnOnMissingEntry = true, allowExtraManifestKeys = true, getManifestPath, patterns = ["*.{ts,tsx,js,jsx}"] } = options; | ||
return { | ||
name: "scriptable-banner", | ||
setup(build) { | ||
const level = logLevel === "auto" ? convertEsbuildLogLevel(build.initialOptions.logLevel || "info") : logLevel; | ||
const logger = createLogger("scriptable-banner", { level }); | ||
let manifestCache = /* @__PURE__ */ new Map(); | ||
@@ -71,6 +72,9 @@ if (!build.initialOptions.metafile) { | ||
}; | ||
const outputs = result.metafile.outputs; | ||
for (const outputPath of Object.keys(outputs)) { | ||
stats.total++; | ||
const outputMeta = outputs[outputPath]; | ||
const outputs = Object.keys(result.metafile.outputs); | ||
stats.total = outputs.length; | ||
logger.info(`Processing ${stats.total} files...`); | ||
for (const [index, outputPath] of outputs.entries()) { | ||
const progress = Math.round((index + 1) / stats.total * 100); | ||
logger.verbose(`[${progress}%] Processing: ${outputPath}`); | ||
const outputMeta = result.metafile.outputs[outputPath]; | ||
const entryPoint = outputMeta?.entryPoint ?? ""; | ||
@@ -93,3 +97,3 @@ const normalizedEntryPath = entryPoint ? normalizeAndResolvePath(entryPoint) : ""; | ||
stats.success++; | ||
logger.success(`Added banner to ${outputPath}`); | ||
logger.success(`[${progress}%] Added banner to ${outputPath}`); | ||
} | ||
@@ -104,7 +108,7 @@ } | ||
stats.success++; | ||
logger.success(`Added banner to ${outputPath}`); | ||
logger.success(`[${progress}%] Added banner to ${outputPath}`); | ||
} | ||
catch (error) { | ||
stats.failed++; | ||
logger.error(`Failed to process ${outputPath}: ${error instanceof Error ? error.message : String(error)}`); | ||
logger.error(`[${progress}%] Failed to process ${outputPath}`); | ||
} | ||
@@ -115,3 +119,3 @@ } | ||
stats.failed++; | ||
logger.error(`Failed to process ${outputPath}`, error); | ||
logger.error(`[${progress}%] Failed to process ${outputPath}`); | ||
} | ||
@@ -118,0 +122,0 @@ } |
import { Plugin } from 'esbuild'; | ||
import { LogLevel } from './logger.js'; | ||
interface ScriptableDeployOptions { | ||
/** | ||
* Whether to show detailed logs during deployment | ||
* Default: false | ||
* Log level for the plugin | ||
* 'auto' will use esbuild's logLevel | ||
* Default: 'auto' | ||
*/ | ||
verbose?: boolean; | ||
logLevel?: LogLevel | 'auto'; | ||
/** | ||
@@ -10,0 +12,0 @@ * Whether to continue building when deployment fails |
@@ -1,2 +0,2 @@ | ||
import { generateScriptableBanner } from "@scriptables/manifest/generateScriptableBanner"; | ||
import { mergeScriptableBanner } from "@scriptables/manifest/mergeScriptableBanner"; | ||
import { findICloudDrivePaths, PathType } from "findicloud"; | ||
@@ -6,3 +6,3 @@ import { readFile, writeFile } from "fs/promises"; | ||
import { join, basename } from "path"; | ||
import { createLogger } from "./logger.js"; | ||
import { convertEsbuildLogLevel, createLogger } from "./logger.js"; | ||
import { createPatternMatcher, normalizeAndResolvePath, isSamePath, waitForFile } from "./utils.js"; | ||
@@ -24,7 +24,4 @@ const findICloudBugsUrl = "https://github.com/hexxspark/findicloud/issues"; | ||
function scriptableDeploy(options = {}) { | ||
const { verbose = false, continueOnError = true, scriptableDir, patterns = ["*.{ts,tsx,js,jsx}"], addBanner = true, manifestExtensions = [".manifest.json", ".manifest", ".json"] } = options; | ||
const { logLevel = "auto", continueOnError = true, scriptableDir, patterns = ["*.{ts,tsx,js,jsx}"], addBanner = true, manifestExtensions = [".manifest.json", ".manifest", ".json"] } = options; | ||
const filterFile = createPatternMatcher(patterns); | ||
const logger = createLogger("scriptable-deploy", { | ||
level: verbose ? "verbose" : "info" | ||
}); | ||
let targetDir; | ||
@@ -34,2 +31,4 @@ return { | ||
setup(build) { | ||
const level = logLevel === "auto" ? convertEsbuildLogLevel(build.initialOptions.logLevel || "info") : logLevel; | ||
const logger = createLogger("scriptable-deploy", { level }); | ||
if (!build.initialOptions.metafile) { | ||
@@ -84,3 +83,6 @@ logger.verbose("Automatically enabling metafile option"); | ||
stats.total = outputs.length; | ||
for (const file of outputs) { | ||
logger.info(`Deploying ${stats.total} files to ${targetDir}...`); | ||
for (const [index, file] of outputs.entries()) { | ||
const progress = Math.round((index + 1) / stats.total * 100); | ||
logger.verbose(`[${progress}%] Processing: ${file}`); | ||
try { | ||
@@ -120,7 +122,6 @@ const output = result.metafile.outputs[file]; | ||
try { | ||
const banner = generateScriptableBanner(manifest); | ||
const newContent = banner + "\n" + content; | ||
await writeFile(targetPath, newContent); | ||
const { banner: newBanner, content: scriptContent } = mergeScriptableBanner(content, manifest); | ||
await writeFile(targetPath, newBanner + scriptContent); | ||
stats.success++; | ||
logger.success(`Deployed with banner: ${file} -> ${targetPath}`); | ||
logger.success(`Deployed with updated banner: ${file} -> ${targetPath}`); | ||
continue; | ||
@@ -127,0 +128,0 @@ } |
{ | ||
"name": "esbuild-plugin-scriptable", | ||
"description": "An ESBuild plugin for developing Scriptable iOS app scripts with TypeScript, manifest support and auto-deployment", | ||
"version": "0.7.1", | ||
"version": "0.7.2", | ||
"keywords": [ | ||
@@ -6,0 +6,0 @@ "scriptable", |
@@ -22,2 +22,3 @@ # esbuild-plugin-scriptable | ||
- Continues building even when deployment fails (configurable) | ||
- Detailed deployment logging options | ||
@@ -54,3 +55,3 @@ ## Installation | ||
scriptableDeploy({ | ||
verbose: true, | ||
logLevel: 'verbose', | ||
patterns: '**/*.widget.ts', | ||
@@ -77,6 +78,17 @@ }), | ||
- `['**/*.ts', '!**/*.test.ts']` - exclude test files | ||
- `logLevel`: Control logging detail (default: 'auto') | ||
- 'auto': Use esbuild's logLevel | ||
- 'silent': No logs | ||
- 'error': Only errors | ||
- 'warn': Errors and warnings | ||
- 'info': Normal output | ||
- 'verbose': Detailed output | ||
### Deploy Plugin Options | ||
- `verbose`: Show detailed logs during deployment (default: false) | ||
- `logLevel`: Control deployment logging detail (default: 'normal') | ||
- 'silent': No logs | ||
- 'normal': Basic deployment info | ||
- 'verbose': Detailed deployment info | ||
- 'debug': All deployment details | ||
- `continueOnError`: Continue building when deployment fails (default: true) | ||
@@ -103,3 +115,5 @@ - `scriptableDir`: Custom Scriptable directory path (default: auto-detect in iCloud Drive) | ||
"iconColor": "blue", | ||
"iconGlyph": "cloud" | ||
"iconGlyph": "cloud", | ||
"version": "1.0.0", | ||
"description": "A weather widget for Scriptable" | ||
} | ||
@@ -133,3 +147,3 @@ ``` | ||
scriptableDeploy({ | ||
verbose: true, | ||
logLevel: 'verbose', | ||
addBanner: true, | ||
@@ -143,4 +157,56 @@ patterns: ['**/*.ts', '!**/*.test.ts'], | ||
## Banner Behavior | ||
The banner can be added in two stages: | ||
1. Build stage (via `scriptableBanner` plugin): | ||
- Adds banner to the build output files | ||
- Banner is part of the compiled result | ||
2. Deploy stage (via `scriptableDeploy` plugin's `addBanner` option): | ||
- Does not affect build output files | ||
- Only adds banner to the deployed files in Scriptable directory | ||
- Will update/override any existing banner in the target file | ||
When using both plugins: | ||
- `scriptableBanner` will add banner to build output | ||
- `scriptableDeploy` with `addBanner: true` will update/override the banner in deployed files | ||
- `scriptableDeploy` with `addBanner: false` will keep the existing banner (if any) | ||
```typescript | ||
// Example 1: Banner in both build output and deployed files | ||
plugins: [ | ||
scriptableBanner({...}), // Adds banner to build output | ||
scriptableDeploy({ | ||
addBanner: true // Updates banner in deployed files | ||
}) | ||
] | ||
// Example 2: Banner only in deployed files | ||
plugins: [ | ||
scriptableDeploy({ | ||
addBanner: true // Adds banner only to deployed files | ||
}) | ||
] | ||
// Example 3: Banner only in build output | ||
plugins: [ | ||
scriptableBanner({...}), // Adds banner to build output | ||
scriptableDeploy({ | ||
addBanner: false // Keeps existing banner in deployed files | ||
}) | ||
] | ||
``` | ||
The `addBanner` option in `scriptableDeploy`: | ||
- Only affects the files in Scriptable directory | ||
- Does not modify the build output files | ||
- When `true`, will ensure the latest banner from manifest is used in deployed files | ||
- When `false`, will preserve any existing banner in the files | ||
## License | ||
Apache-2.0 |
import chalk from 'chalk'; | ||
import type {LogLevel as EsbuildLogLevel} from 'esbuild'; | ||
export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'verbose'; | ||
export function convertEsbuildLogLevel(level: EsbuildLogLevel): LogLevel { | ||
switch (level) { | ||
case 'debug': | ||
return 'verbose'; | ||
case 'info': | ||
return 'info'; | ||
case 'warning': | ||
return 'warn'; | ||
case 'error': | ||
return 'error'; | ||
case 'silent': | ||
return 'silent'; | ||
default: | ||
return 'info'; | ||
} | ||
} | ||
export interface Logger { | ||
@@ -14,69 +34,104 @@ info: (message: string, ...args: any[]) => void; | ||
const timers = new Map<string, number>(); | ||
export interface LoggerOptions { | ||
level?: 'verbose' | 'info'; | ||
level?: LogLevel; | ||
} | ||
export function createLogger(prefix: string, options: LoggerOptions = {}): Logger { | ||
const {level = 'info'} = options; | ||
const formatPrefix = () => chalk.cyan(`[${prefix}]`); | ||
const formatTime = (ms: number) => chalk.gray(`(${ms}ms)`); | ||
class ConsoleLogger implements Logger { | ||
private readonly timers = new Map<string, number>(); | ||
private readonly LOG_LEVELS: Record<LogLevel, number> = { | ||
silent: 0, | ||
error: 1, | ||
warn: 2, | ||
info: 3, | ||
verbose: 4, | ||
}; | ||
return { | ||
info: (message: string, ...args) => { | ||
console.log(`${formatPrefix()} ${message}`, ...args); | ||
}, | ||
warn: (message: string, ...args) => { | ||
console.warn(`${formatPrefix()} ${chalk.yellow('warning')} ${message}`, ...args); | ||
}, | ||
error: (message: string, ...args) => { | ||
console.error(`${formatPrefix()} ${chalk.red('error')} ${message}`, ...args); | ||
}, | ||
success: (message: string, ...args) => { | ||
console.log(`${formatPrefix()} ${chalk.green('✓')} ${message}`, ...args); | ||
}, | ||
report: stats => { | ||
const {total, success, skipped, failed} = stats; | ||
const hasIssues = failed > 0 || skipped > 0; | ||
constructor( | ||
private readonly prefix: string, | ||
private readonly level: LogLevel = 'info', | ||
) {} | ||
console.log('\n' + formatPrefix() + ' ' + chalk.bold('Build Summary:')); | ||
private formatPrefix() { | ||
return chalk.cyan(`[${this.prefix}]`); | ||
} | ||
// 使用表格样式展示统计 | ||
const rows = [ | ||
[` ${chalk.bold('Total files:')}`, `${total}`], | ||
[` ${chalk.bold('Processed:')}`, `${chalk.green(success)}`], | ||
[` ${chalk.bold('Skipped:')}`, `${skipped > 0 ? chalk.yellow(skipped) : skipped}`], | ||
[` ${chalk.bold('Failed:')}`, `${failed > 0 ? chalk.red(failed) : failed}`], | ||
]; | ||
private formatTime(ms: number) { | ||
return chalk.gray(`(${ms}ms)`); | ||
} | ||
rows.forEach(([label, value]) => { | ||
console.log(`${label.padEnd(20)}${value}`); | ||
}); | ||
private shouldLog(messageLevel: LogLevel): boolean { | ||
if (this.level === 'silent') { | ||
return false; | ||
} | ||
// 如果有问题,添加提示 | ||
if (hasIssues) { | ||
console.log('\n' + formatPrefix() + ' ' + chalk.yellow('⚠️ Some files were not processed successfully')); | ||
} else { | ||
console.log('\n' + formatPrefix() + ' ' + chalk.green('✨ All files processed successfully')); | ||
} | ||
console.log(); // 空行 | ||
}, | ||
time: (label: string) => { | ||
timers.set(label, performance.now()); | ||
}, | ||
timeEnd: (label: string) => { | ||
const start = timers.get(label); | ||
return this.LOG_LEVELS[messageLevel] <= this.LOG_LEVELS[this.level]; | ||
} | ||
info(message: string, ...args: any[]) { | ||
if (this.shouldLog('info')) { | ||
console.log(`${this.formatPrefix()} ${message}`, ...args); | ||
} | ||
} | ||
warn(message: string, ...args: any[]) { | ||
if (this.shouldLog('warn')) { | ||
console.warn(`${this.formatPrefix()} ${chalk.yellow('warning')} ${message}`, ...args); | ||
} | ||
} | ||
error(message: string, ...args: any[]) { | ||
if (this.shouldLog('error')) { | ||
console.error(`${this.formatPrefix()} ${chalk.red('error')} ${message}`, ...args); | ||
} | ||
} | ||
success(message: string, ...args: any[]) { | ||
if (this.shouldLog('info')) { | ||
console.log(`${this.formatPrefix()} ${chalk.green('✓')} ${message}`, ...args); | ||
} | ||
} | ||
verbose(message: string, ...args: any[]) { | ||
if (this.shouldLog('verbose')) { | ||
console.log(`${this.formatPrefix()} ${chalk.gray(message)}`, ...args); | ||
} | ||
} | ||
time(label: string) { | ||
if (this.shouldLog('verbose')) { | ||
this.timers.set(label, performance.now()); | ||
} | ||
} | ||
timeEnd(label: string) { | ||
if (this.shouldLog('verbose')) { | ||
const start = this.timers.get(label); | ||
if (start) { | ||
const duration = Math.round(performance.now() - start); | ||
console.log(`${formatPrefix()} ${label} ${formatTime(duration)}`); | ||
timers.delete(label); | ||
console.log(`${this.formatPrefix()} ${label} ${this.formatTime(duration)}`); | ||
this.timers.delete(label); | ||
} | ||
}, | ||
verbose: (message: string, ...args) => { | ||
if (level === 'verbose') { | ||
console.log(`${formatPrefix()} ${chalk.gray(message)}`, ...args); | ||
} | ||
}, | ||
}; | ||
} | ||
} | ||
report(stats: {total: number; success: number; skipped: number; failed: number}) { | ||
if (this.level === 'silent') return; | ||
const {total, success, skipped, failed} = stats; | ||
const summary = [ | ||
`Total: ${total}`, | ||
`✓ Success: ${chalk.green(success)}`, | ||
skipped > 0 ? `⚠️ Skipped: ${chalk.yellow(skipped)}` : null, | ||
failed > 0 ? `❌ Failed: ${chalk.red(failed)}` : null, | ||
] | ||
.filter(Boolean) | ||
.join(' '); | ||
console.log(`${this.formatPrefix()} Build Result: ${summary}`); | ||
} | ||
} | ||
export function createLogger(prefix: string, options: LoggerOptions = {}): Logger { | ||
return new ConsoleLogger(prefix, options.level ?? 'info'); | ||
} |
@@ -10,3 +10,3 @@ import type {ScriptableManifest} from '@scriptables/manifest'; | ||
import {createLogger} from './logger'; | ||
import {convertEsbuildLogLevel, createLogger, LogLevel} from './logger'; | ||
import {isMatch, isSamePath, normalizeAndResolvePath, waitForFile} from './utils'; | ||
@@ -16,3 +16,10 @@ | ||
export interface ScriptablePluginOptions { | ||
export interface ScriptableBannerOptions { | ||
/** | ||
* Log level for the plugin | ||
* 'auto' will use esbuild's logLevel | ||
* Default: 'auto' | ||
*/ | ||
logLevel?: LogLevel | 'auto'; | ||
/** Custom manifest file extensions */ | ||
@@ -40,4 +47,5 @@ manifestExtensions?: string[]; | ||
export function scriptableBanner(options: ScriptablePluginOptions = {}): Plugin { | ||
export function scriptableBanner(options: ScriptableBannerOptions = {}): Plugin { | ||
const { | ||
logLevel = 'auto', | ||
manifestExtensions = DEFAULT_MANIFEST_EXTENSIONS, | ||
@@ -52,7 +60,9 @@ warnOnMissingManifest = true, | ||
const logger = createLogger('scriptable-banner'); | ||
return { | ||
name: 'scriptable-banner', | ||
setup(build) { | ||
const level = logLevel === 'auto' ? convertEsbuildLogLevel(build.initialOptions.logLevel || 'info') : logLevel; | ||
const logger = createLogger('scriptable-banner', {level}); | ||
let manifestCache = new Map<string, ScriptableManifest>(); | ||
@@ -133,7 +143,12 @@ | ||
const outputs = result.metafile.outputs; | ||
for (const outputPath of Object.keys(outputs)) { | ||
stats.total++; | ||
const outputs = Object.keys(result.metafile.outputs); | ||
stats.total = outputs.length; | ||
const outputMeta = outputs[outputPath]; | ||
logger.info(`Processing ${stats.total} files...`); | ||
for (const [index, outputPath] of outputs.entries()) { | ||
const progress = Math.round(((index + 1) / stats.total) * 100); | ||
logger.verbose(`[${progress}%] Processing: ${outputPath}`); | ||
const outputMeta = result.metafile.outputs[outputPath]; | ||
const entryPoint = outputMeta?.entryPoint ?? ''; | ||
@@ -161,3 +176,3 @@ const normalizedEntryPath = entryPoint ? normalizeAndResolvePath(entryPoint) : ''; | ||
stats.success++; | ||
logger.success(`Added banner to ${outputPath}`); | ||
logger.success(`[${progress}%] Added banner to ${outputPath}`); | ||
} | ||
@@ -171,13 +186,13 @@ } else { | ||
stats.success++; | ||
logger.success(`Added banner to ${outputPath}`); | ||
logger.success(`[${progress}%] Added banner to ${outputPath}`); | ||
// eslint-disable-next-line unused-imports/no-unused-vars | ||
} catch (error) { | ||
stats.failed++; | ||
logger.error( | ||
`Failed to process ${outputPath}: ${error instanceof Error ? error.message : String(error)}`, | ||
); | ||
logger.error(`[${progress}%] Failed to process ${outputPath}`); | ||
} | ||
} | ||
// eslint-disable-next-line unused-imports/no-unused-vars | ||
} catch (error) { | ||
stats.failed++; | ||
logger.error(`Failed to process ${outputPath}`, error); | ||
logger.error(`[${progress}%] Failed to process ${outputPath}`); | ||
} | ||
@@ -184,0 +199,0 @@ } |
import type {ScriptableManifest} from '@scriptables/manifest'; | ||
import {generateScriptableBanner} from '@scriptables/manifest/generateScriptableBanner'; | ||
import {mergeScriptableBanner} from '@scriptables/manifest/mergeScriptableBanner'; | ||
import type {Plugin} from 'esbuild'; | ||
@@ -9,3 +9,3 @@ import {findICloudDrivePaths, PathType} from 'findicloud'; | ||
import {createLogger} from './logger'; | ||
import {convertEsbuildLogLevel, createLogger, LogLevel} from './logger'; | ||
import {createPatternMatcher, isSamePath, normalizeAndResolvePath, waitForFile} from './utils'; | ||
@@ -15,6 +15,7 @@ | ||
/** | ||
* Whether to show detailed logs during deployment | ||
* Default: false | ||
* Log level for the plugin | ||
* 'auto' will use esbuild's logLevel | ||
* Default: 'auto' | ||
*/ | ||
verbose?: boolean; | ||
logLevel?: LogLevel | 'auto'; | ||
@@ -74,3 +75,3 @@ /** | ||
const { | ||
verbose = false, | ||
logLevel = 'auto', | ||
continueOnError = true, | ||
@@ -84,7 +85,2 @@ scriptableDir, | ||
const filterFile = createPatternMatcher(patterns); | ||
const logger = createLogger('scriptable-deploy', { | ||
level: verbose ? 'verbose' : 'info', | ||
}); | ||
let targetDir: string; | ||
@@ -95,2 +91,6 @@ | ||
setup(build) { | ||
const level = logLevel === 'auto' ? convertEsbuildLogLevel(build.initialOptions.logLevel || 'info') : logLevel; | ||
const logger = createLogger('scriptable-deploy', {level}); | ||
// Ensure metafile is enabled | ||
@@ -159,4 +159,8 @@ if (!build.initialOptions.metafile) { | ||
stats.total = outputs.length; | ||
logger.info(`Deploying ${stats.total} files to ${targetDir}...`); | ||
for (const file of outputs) { | ||
for (const [index, file] of outputs.entries()) { | ||
const progress = Math.round(((index + 1) / stats.total) * 100); | ||
logger.verbose(`[${progress}%] Processing: ${file}`); | ||
try { | ||
@@ -202,7 +206,6 @@ const output = result.metafile.outputs[file]; | ||
try { | ||
const banner = generateScriptableBanner(manifest); | ||
const newContent = banner + '\n' + content; | ||
await writeFile(targetPath, newContent); | ||
const {banner: newBanner, content: scriptContent} = mergeScriptableBanner(content, manifest); | ||
await writeFile(targetPath, newBanner + scriptContent); | ||
stats.success++; | ||
logger.success(`Deployed with banner: ${file} -> ${targetPath}`); | ||
logger.success(`Deployed with updated banner: ${file} -> ${targetPath}`); | ||
continue; | ||
@@ -209,0 +212,0 @@ } catch (error) { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
157894
1641
207