mirror of
https://github.com/GetOpenScript/OpenScript.git
synced 2026-09-18 09:45:43 +00:00
Release OpenScript 1.0.3
This commit is contained in:
9
AGENTS.md
Normal file
9
AGENTS.md
Normal 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`
|
||||
@@ -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
4
package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
84
scripts/publish.js
Normal 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);
|
||||
});
|
||||
22
src/popup.js
22
src/popup.js
@@ -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();
|
||||
|
||||
@@ -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] || '';
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = '1.0.2';
|
||||
export const VERSION = '1.0.3';
|
||||
|
||||
@@ -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/*');
|
||||
|
||||
Reference in New Issue
Block a user