Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions bin/pos-cli-check-update-docs.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env node
import { program } from '../lib/program.js';
import logger from '../lib/logger.js';
import ora from 'ora';
import ora from '../lib/ora.js';
import { MISSING_PACKAGE_MESSAGE } from '../lib/check-messages.js';

program
.name('pos-cli check update-docs')
Expand All @@ -11,10 +12,7 @@ program
try {
platformosCheck = await import('@platformos/platformos-check-node');
} catch {
await logger.Error(
'The @platformos/platformos-check-node package is not installed.\n' +
'Install it with: npm install @platformos/platformos-check-node'
);
await logger.Error(MISSING_PACKAGE_MESSAGE);
return;
}

Expand Down
2 changes: 1 addition & 1 deletion bin/pos-cli-clone-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Gateway from '../lib/proxy.js';
import { fetchSettings } from '../lib/settings.js';
import waitForStatus from '../lib/data/waitForStatus.js';
import logger from '../lib/logger.js';
import ora from 'ora';
import ora from '../lib/ora.js';

program.showHelpAfterError();
program
Expand Down
2 changes: 1 addition & 1 deletion bin/pos-cli-data-clean.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import waitForStatus from '../lib/data/waitForStatus.js';
import { fetchSettings } from '../lib/settings.js';
import logger from '../lib/logger.js';
import ServerError from '../lib/ServerError.js';
import ora from 'ora';
import ora from '../lib/ora.js';

const confirmationText = process.env.CONFIRMATION_TEXT || 'CLEAN DATA';

Expand Down
2 changes: 1 addition & 1 deletion bin/pos-cli-data-export.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import waitForStatus from '../lib/data/waitForStatus.js';
import downloadFile from '../lib/downloadFile.js';
import logger from '../lib/logger.js';
import report from '../lib/logger/report.js';
import ora from 'ora';
import ora from '../lib/ora.js';

let gateway;

Expand Down
2 changes: 1 addition & 1 deletion bin/pos-cli-data-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { uploadFile } from '../lib/s3UploadFile.js';
import { presignUrl } from '../lib/presignUrl.js';
import logger from '../lib/logger.js';
import report from '../lib/logger/report.js';
import ora from 'ora';
import ora from '../lib/ora.js';

let gateway;

Expand Down
2 changes: 1 addition & 1 deletion bin/pos-cli-data-update.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import transform from '../lib/data/uploadFiles.js';
import isValidJSON from '../lib/data/isValidJSON.js';
import logger from '../lib/logger.js';
import report from '../lib/logger/report.js';
import ora from 'ora';
import ora from '../lib/ora.js';

let gateway;

Expand Down
2 changes: 1 addition & 1 deletion bin/pos-cli-dns-export.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import fs from 'fs';
import path from 'path';
import ora from 'ora';
import ora from '../lib/ora.js';

import { program } from '../lib/program.js';
import logger from '../lib/logger.js';
Expand Down
2 changes: 1 addition & 1 deletion bin/pos-cli-dns-migrate.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node

import ora from 'ora';
import ora from '../lib/ora.js';
import table from 'text-table';

import { program } from '../lib/program.js';
Expand Down
2 changes: 1 addition & 1 deletion bin/pos-cli-modules-pull.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import logger from '../lib/logger.js';
import { fetchSettings } from '../lib/settings.js';
import downloadFile from '../lib/downloadFile.js';
import waitForStatus from '../lib/data/waitForStatus.js';
import ora from 'ora';
import ora from '../lib/ora.js';

program
.name('pos-cli modules pull')
Expand Down
2 changes: 1 addition & 1 deletion bin/pos-cli-pull.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { fetchSettings } from '../lib/settings.js';
import downloadFile from '../lib/downloadFile.js';
import waitForStatus from '../lib/data/waitForStatus.js';

import ora from 'ora';
import ora from '../lib/ora.js';

program
.name('pos-cli pull')
Expand Down
2 changes: 1 addition & 1 deletion bin/pos-cli-uploads-push.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { fetchSettings } from '../lib/settings.js';
import { uploadFile } from '../lib/s3UploadFile.js';
import { presignUrl } from '../lib/presignUrl.js';
import logger from '../lib/logger.js';
import ora from 'ora';
import ora from '../lib/ora.js';

const uploadZip = async (filepath, gateway) => {

Expand Down
13 changes: 13 additions & 0 deletions lib/check-messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Message text shared by everything that loads @platformos/platformos-check-node.
*
* Deliberately a leaf module with no imports: lib/check-worker.js runs on a worker
* thread, which has its own module registry, so importing this from lib/check.js
* instead would re-evaluate that whole graph (ora, chalk, YAML, logger) on the worker
* for a single string — about 100ms on every `pos-cli check run`.
*/
const MISSING_PACKAGE_MESSAGE =
'The @platformos/platformos-check-node package is not installed.\n' +
'Install it with: npm install @platformos/platformos-check-node';

export { MISSING_PACKAGE_MESSAGE };
78 changes: 78 additions & 0 deletions lib/check-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { parentPort, workerData } from 'node:worker_threads';
import { MISSING_PACKAGE_MESSAGE } from './check-messages.js';

/**
* The linter runs here rather than on the main thread so the main thread's event loop
* stays free: that keeps the spinner animating and, more importantly, keeps Ctrl+C
* instant. See lib/ora.js for why a spinner makes Ctrl+C depend on a free event loop.
*
* Nothing here is allowed to exit the process or write to the terminal: the main thread
* owns the spinner and all output, and gets everything through postMessage.
*/

/**
* A fixable offense carries `fix` / `suggest` closures, and structured clone cannot copy
* functions. Autofix runs on this side for that reason, so only the fields the reporter
* actually prints need to cross back.
*/
const toPlainOffense = (offense) => ({
check: offense.check,
message: offense.message,
uri: offense.uri,
severity: offense.severity,
start: { line: offense.start.line, character: offense.start.character },
end: { line: offense.end.line, character: offense.end.character }
});

const post = (message) => parentPort.postMessage(message);
const progress = (message) => message && post({ type: 'progress', message });

const { path: checkPath, autoFix, checks } = workerData;

/**
* Wrapped in a function purely so the early exits can `return`. Calling process.exit()
* here instead would risk the last postMessage never reaching the main thread, which
* would turn a clear "unknown check" message into a worker-died error.
*/
const main = async () => {
let platformosCheck;
try {
platformosCheck = await import('@platformos/platformos-check-node');
} catch {
return post({ type: 'userError', message: MISSING_PACKAGE_MESSAGE });
}

if (checks && checks.length > 0) {
const validNames = new Set(platformosCheck.allChecks.map((c) => c.meta.code));
const unknown = checks.filter((name) => !validNames.has(name));
if (unknown.length > 0) {
const available = Array.from(validNames).sort().join(', ');
return post({
type: 'userError',
message:
`Unknown check${unknown.length > 1 ? 's' : ''}: ${unknown.join(', ')}\n` +
`Available checks: ${available}`
});
}
}

const result = await platformosCheck.appCheckRun(checkPath, undefined, progress);

let offenses = checks
? result.offenses.filter((o) => checks.includes(o.check))
: result.offenses;

if (autoFix && offenses.length > 0) {
progress(`Applying automatic fixes to ${offenses.length} offense${offenses.length === 1 ? '' : 's'}...`);
await platformosCheck.autofix(result.app, offenses);

// Re-run the check after autofix to get updated offenses
progress('Re-checking after fixes...');
const recheck = await platformosCheck.appCheckRun(checkPath);
offenses = recheck.offenses;
}

post({ type: 'result', offenses: offenses.map(toPlainOffense) });
};

await main();
96 changes: 45 additions & 51 deletions lib/check.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import fs from 'fs';
import path from 'path';
import { Worker } from 'node:worker_threads';
import { fileURLToPath } from 'url';
import logger from './logger.js';
import chalk from 'chalk';
import YAML from 'yaml';
import ora from 'ora';
import ora from './ora.js';
import { MISSING_PACKAGE_MESSAGE } from './check-messages.js';

// Severity levels from platformos-check-node
const Severity = {
Expand All @@ -18,10 +20,7 @@ const loadPlatformosCheck = async () => {
const platformosCheck = await import('@platformos/platformos-check-node');
return platformosCheck;
} catch {
await logger.Error(
'The @platformos/platformos-check-node package is not installed.\n' +
'Install it with: npm install @platformos/platformos-check-node'
);
await logger.Error(MISSING_PACKAGE_MESSAGE);
}
};

Expand Down Expand Up @@ -349,29 +348,41 @@ const initConfig = async (rootPath) => {
}
};

/**
* Marks an error the user is meant to read as-is (an unknown check name, a missing
* package) rather than a crash to be reported with a stack trace.
*/
class UserFacingError extends Error {}

/**
* Runs the linter on a worker thread and resolves with its plain offenses.
* See lib/check-worker.js for why the check is off the main thread.
*/
const runInWorker = ({ checkPath, autoFix, checks, onProgress }) =>
new Promise((resolve, reject) => {
const worker = new Worker(new URL('./check-worker.js', import.meta.url), {
workerData: { path: checkPath, autoFix, checks }
});

worker.on('message', (message) => {
if (message.type === 'progress') onProgress(message.message);
else if (message.type === 'result') resolve(message.offenses);
else if (message.type === 'userError') reject(new UserFacingError(message.message));
});
worker.on('error', reject);
// A worker that dies without posting anything (an OOM, say) would otherwise leave
// this promise pending forever. Settled promises ignore a later rejection, so this
// is a no-op on the normal path.
worker.on('exit', (code) => reject(new Error(`Check worker stopped with exit code ${code}`)));
});

const run = async (opts) => {
const { path: checkPath, autoFix, checks, format, silent } = opts;

await validatePath(checkPath);

const platformosCheck = await loadPlatformosCheck();

if (checks && checks.length > 0) {
const validNames = new Set(platformosCheck.allChecks.map((c) => c.meta.code));
const unknown = checks.filter((name) => !validNames.has(name));
if (unknown.length > 0) {
const available = Array.from(validNames).sort().join(', ');
await logger.Error(
`Unknown check${unknown.length > 1 ? 's' : ''}: ${unknown.join(', ')}\n` +
`Available checks: ${available}`
);
return;
}
}

let offenses = [];
let offenses;
let spinner;
let app;

// Only show spinner for text output (not JSON)
if (format !== 'json' && !silent) {
Expand All @@ -380,42 +391,25 @@ const run = async (opts) => {
}

try {
// Run checks with progress callback
const result = await platformosCheck.appCheckRun(checkPath, undefined, (message) => {
if (spinner && message) {
spinner.text = message;
offenses = await runInWorker({
checkPath,
autoFix,
checks,
onProgress: (message) => {
if (spinner) spinner.text = message;
}
});

offenses = checks
? result.offenses.filter((o) => checks.includes(o.check))
: result.offenses;
app = result.app;

// Update spinner with completion info if it's still running
if (spinner && spinner.isSpinning) {
const fileCount = app.length;
spinner.text = `Checked ${fileCount} file${fileCount === 1 ? '' : 's'}`;
}

if (autoFix && offenses.length > 0) {
if (spinner) {
spinner.text = `Applying automatic fixes to ${offenses.length} offense${offenses.length === 1 ? '' : 's'}...`;
}
await platformosCheck.autofix(app, offenses);

// Re-run check after autofix to get updated offenses
if (spinner) {
spinner.text = 'Re-checking after fixes...';
}
const recheck = await platformosCheck.appCheckRun(checkPath);
offenses = recheck.offenses;
}

if (spinner) {
spinner.stop();
}
} catch (error) {
if (error instanceof UserFacingError) {
if (spinner) spinner.stop();
await logger.Error(error.message);
return;
}

if (spinner) {
spinner.fail('Check failed');
}
Expand Down
2 changes: 1 addition & 1 deletion lib/deploy/defaultStrategy.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ora from 'ora';
import ora from '../ora.js';
import { makeArchive } from '../archive.js';
import { push, printDeployReport } from '../push.js';
import logger from '../logger.js';
Expand Down
2 changes: 1 addition & 1 deletion lib/deploy/directAssetsUploadStrategy.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { performance } from 'perf_hooks';
import ora from 'ora';
import ora from '../ora.js';
import Gateway from '../proxy.js';
import { makeArchive } from '../archive.js';
import { deployAssets } from '../assets.js';
Expand Down
2 changes: 1 addition & 1 deletion lib/deploy/dryRunStrategy.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { performance } from 'perf_hooks';
import ora from 'ora';
import ora from '../ora.js';
import Gateway from '../proxy.js';
import { makeArchive } from '../archive.js';
import { push, printDeployReport } from '../push.js';
Expand Down
Loading
Loading