Release OpenScript 1.0.3

This commit is contained in:
2026-09-12 22:14:22 -07:00
parent 3697048c6e
commit 1cd7b693ab
10 changed files with 155 additions and 13 deletions

9
AGENTS.md Normal file
View File

@@ -0,0 +1,9 @@
# Chrome Web Store Instructions
The following environment variables are configured on the host system:
- `CHROME_CLIENT_ID`
- `CHROME_CLIENT_SECRET`
- `CHROME_REFRESH_TOKEN`
## Chrome Extension Details
- **Extension ID:** `dkelmgdchndagjemmodhkphdikhpnfol`

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"minimum_chrome_version": "138",
"name": "OpenScript",
"version": "1.0.2",
"version": "1.0.3",
"description": "A lightweight user script manager for modern browsers",
"action": {
"default_popup": "src/popup.html",

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "openscript",
"version": "1.0.2",
"version": "1.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openscript",
"version": "1.0.2",
"version": "1.0.3",
"dependencies": {
"lucide": "^0.475.0"
},

View File

@@ -1,6 +1,6 @@
{
"name": "openscript",
"version": "1.0.2",
"version": "1.0.3",
"private": true,
"type": "module",
"scripts": {
@@ -9,6 +9,7 @@
"build:icons": "node scripts/generate-icons.js",
"screenshot": "node scripts/make-screenshot.js",
"zip": "node scripts/build-zip.js",
"publish": "node scripts/publish.js",
"test": "node --test"
},
"dependencies": {

84
scripts/publish.js Normal file
View File

@@ -0,0 +1,84 @@
import fs from 'fs';
const EXTENSION_ID = 'dkelmgdchndagjemmodhkphdikhpnfol';
const { CHROME_CLIENT_ID, CHROME_CLIENT_SECRET, CHROME_REFRESH_TOKEN } = process.env;
if (!CHROME_CLIENT_ID || !CHROME_CLIENT_SECRET || !CHROME_REFRESH_TOKEN) {
console.error('Missing Chrome Web Store credentials in environment variables.');
process.exit(1);
}
const zipPath = 'openscript.zip';
if (!fs.existsSync(zipPath)) {
console.error(`Zip file not found: ${zipPath}`);
process.exit(1);
}
async function publish() {
console.log('1. Refreshing access token...');
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: CHROME_CLIENT_ID,
client_secret: CHROME_CLIENT_SECRET,
refresh_token: CHROME_REFRESH_TOKEN,
grant_type: 'refresh_token',
}),
});
const tokenData = await tokenRes.json();
if (!tokenRes.ok) {
console.error('Failed to obtain access token:', tokenData);
process.exit(1);
}
const accessToken = tokenData.access_token;
console.log('✓ Access token obtained.');
console.log(`2. Uploading ${zipPath} to Chrome Web Store (${EXTENSION_ID})...`);
const zipBuffer = fs.readFileSync(zipPath);
const uploadRes = await fetch(
`https://www.googleapis.com/upload/chromewebstore/v1.1/items/${EXTENSION_ID}`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'x-goog-api-version': '2',
},
body: zipBuffer,
}
);
const uploadData = await uploadRes.json();
if (!uploadRes.ok || uploadData.uploadState !== 'SUCCESS') {
console.error('Upload failed:', uploadData);
process.exit(1);
}
console.log('✓ Upload successful:', uploadData.uploadState);
console.log('3. Publishing new version to Chrome Web Store...');
const publishRes = await fetch(
`https://www.googleapis.com/chromewebstore/v1.1/items/${EXTENSION_ID}/publish`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'x-goog-api-version': '2',
'Content-Length': '0',
},
}
);
const publishData = await publishRes.json();
if (!publishRes.ok || (publishData.status && !publishData.status.includes('OK'))) {
console.error('Publish failed:', publishData);
process.exit(1);
}
console.log('✓ Publish response:', publishData);
}
publish().catch(err => {
console.error('Error during publish:', err);
process.exit(1);
});

View File

@@ -1,7 +1,7 @@
import {
getScripts, saveScripts, getSecrets, saveSecrets, garbageCollectScriptStorage,
} from './utils/storage.js';
import { parseMeta, getBoilerplate } from './utils/parser.js';
import { parseMeta, getBoilerplate, getMetaRunAt } from './utils/parser.js';
import { isUserScriptsAvailable } from './utils/userScripts.js';
import { renderIcons, icon } from './utils/icons.js';
import { VERSION } from './version.js';
@@ -44,6 +44,7 @@ const init = async () => {
state.scripts = scripts.map(s => ({
...s,
version: parseMeta(s.code || '').version,
runAt: getMetaRunAt(s.code || '') || s.runAt || 'document_idle',
}));
state.secrets = secrets;
render();
@@ -92,7 +93,7 @@ const saveCurrentScript = async () => {
requires: meta.requires,
requireCache: existing?.requireCache || {},
storageToken: existing?.storageToken,
runAt: $('#run-at-select')?.value || meta.runAt || 'document_idle',
runAt: getMetaRunAt(code) || $('#run-at-select')?.value || 'document_idle',
code,
enabled: existing ? existing.enabled : true,
updatedAt: Date.now(),
@@ -252,7 +253,7 @@ const renderScriptList = () => {
const renderEditor = () => {
const script = state.scripts.find(s => s.id === state.editingId);
const code = script ? script.code : getBoilerplate();
const runAt = script?.runAt || 'document_idle';
const runAt = getMetaRunAt(code) || script?.runAt || 'document_idle';
return `
<div class="flex flex-col flex-1 overflow-hidden bg-slate-50">
@@ -458,12 +459,23 @@ const bindEvents = () => {
$('#btn-cancel-edit')?.addEventListener('click', () => setTab('list'));
$('#btn-reset-boilerplate')?.addEventListener('click', () => {
const el = $('#editor-code');
if (el && confirm('Reset code to default boilerplate?')) el.value = getBoilerplate();
if (el && confirm('Reset code to default boilerplate?')) {
el.value = getBoilerplate();
const sel = $('#run-at-select');
const script = state.scripts.find(s => s.id === state.editingId);
if (sel) sel.value = script?.runAt || 'document_idle';
}
});
// Tab key indent in editor
// Tab key indent & metadata sync in editor
const textarea = $('#editor-code');
if (textarea) {
textarea.addEventListener('input', () => {
const metaRunAt = getMetaRunAt(textarea.value);
const sel = $('#run-at-select');
if (sel && metaRunAt) sel.value = metaRunAt;
});
textarea.addEventListener('keydown', e => {
if (e.key === 'Tab') {
e.preventDefault();

View File

@@ -2,6 +2,13 @@
const SINGLE_KEYS = new Set(['name', 'version', 'description', 'run-at']);
const MULTI_KEYS = new Set(['match', 'require']);
const RUN_AT_OPTIONS = new Set(['document_start', 'document_end', 'document_idle']);
export const getMetaRunAt = code => {
const block = code.match(/\/\/ ==UserScript==([\s\S]*?)\/\/ ==\/UserScript==/)?.[1] || '';
const val = block.match(/\/\/\s*@run-at\s+([\w-]+)/i)?.[1]?.toLowerCase().replace('-', '_');
return RUN_AT_OPTIONS.has(val) ? val : null;
};
export const parseMeta = code => {
const block = code.match(/\/\/ ==UserScript==([\s\S]*?)\/\/ ==\/UserScript==/)?.[1] || '';

View File

@@ -1,5 +1,5 @@
import { getScripts, saveScripts, getSecrets } from './storage.js';
import { normalizeMatch, parseMeta } from './parser.js';
import { normalizeMatch, parseMeta, getMetaRunAt } from './parser.js';
import { VERSION } from '../version.js';
const STORAGE_MESSAGE = 'OPEN_SCRIPT_STORAGE';
@@ -91,14 +91,16 @@ export const syncUserScripts = async ({ refreshRequires = false } = {}) => {
scripts = scripts.map(script => {
const requires = parseMeta(script.code || '').requires;
const storageToken = script.storageToken || crypto.randomUUID();
const runAt = getMetaRunAt(script.code || '') || script.runAt || 'document_idle';
if (requires.some(url => script.requireCache?.[url] === undefined)) missingCache = true;
if (storageToken === script.storageToken &&
runAt === script.runAt &&
JSON.stringify(requires) === JSON.stringify(script.requires || [])) return script;
changed = true;
const requireCache = Object.fromEntries(requires.flatMap(url =>
script.requireCache && Object.hasOwn(script.requireCache, url) ? [[url, script.requireCache[url]]] : []
));
return { ...script, requires, requireCache, storageToken };
return { ...script, runAt, requires, requireCache, storageToken };
});
let warnings = [];

View File

@@ -1 +1 @@
export const VERSION = '1.0.2';
export const VERSION = '1.0.3';

View File

@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseMeta, normalizeMatch, getBoilerplate } from '../src/utils/parser.js';
import { parseMeta, normalizeMatch, getBoilerplate, getMetaRunAt } from '../src/utils/parser.js';
import {
buildScriptCode, refreshRequireCaches, syncUserScripts, wrapScriptCode,
} from '../src/utils/userScripts.js';
@@ -39,6 +39,33 @@ test('parseMeta falls back to defaults when fields are missing', () => {
assert.deepEqual(meta.requires, []);
});
test('getMetaRunAt parses valid @run-at directives and ignores invalid or absent ones', () => {
assert.equal(getMetaRunAt('// ==UserScript==\n// @run-at document-start\n// ==/UserScript=='), 'document_start');
assert.equal(getMetaRunAt('// ==UserScript==\n// @run-at document_end\n// ==/UserScript=='), 'document_end');
assert.equal(getMetaRunAt('// ==UserScript==\n// @run-at document-idle\n// ==/UserScript=='), 'document_idle');
assert.equal(getMetaRunAt('// ==UserScript==\n// @run-at DOCUMENT-START\n// ==/UserScript=='), 'document_start');
assert.equal(getMetaRunAt('// ==UserScript==\n// @name Test\n// ==/UserScript=='), null);
assert.equal(getMetaRunAt('// @run-at document-start outside header'), null);
assert.equal(getMetaRunAt('// ==UserScript==\n// @run-at invalid-timing\n// ==/UserScript=='), null);
});
test('metadata run-at header overrides selector and falls back when absent', () => {
const resolveRunAt = (code, selectorVal, storedVal) =>
getMetaRunAt(code) || selectorVal || storedVal || 'document_idle';
const codeWithHeader = '// ==UserScript==\n// @run-at document-start\n// ==/UserScript==';
const codeWithoutHeader = '// ==UserScript==\n// @name Script\n// ==/UserScript==';
// First time creation: metadata overrides selector
assert.equal(resolveRunAt(codeWithHeader, 'document_idle', undefined), 'document_start');
// First time creation: no metadata uses selector
assert.equal(resolveRunAt(codeWithoutHeader, 'document_end', undefined), 'document_end');
// Opening to edit: metadata overrides stored value
assert.equal(resolveRunAt(codeWithHeader, null, 'document_idle'), 'document_start');
// Opening to edit: no metadata falls back to stored value
assert.equal(resolveRunAt(codeWithoutHeader, null, 'document_end'), 'document_end');
});
test('normalizeMatch formats URL patterns for Chrome userScripts API', () => {
assert.equal(normalizeMatch('https://example.com'), 'https://example.com/*');
assert.equal(normalizeMatch('example.com/*'), '*://example.com/*');