Skip to content

Commit 489d5b4

Browse files
committed
Merge branch 'jsr' into storybook
2 parents 1fb1a4e + fbbb31c commit 489d5b4

20 files changed

Lines changed: 685 additions & 124 deletions

.github/workflows/release.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ jobs:
102102
working-directory: ./build/sdk
103103
run: npm publish --access=public
104104

105+
- name: Publish SDK to JSR
106+
if: startsWith(github.head_ref, 'releases/sdk-v')
107+
run: |
108+
node ./scripts/prepare-jsr.mts
109+
cd dist/jsr
110+
npx jsr publish
111+
105112
- name: Create pull request to main (App)
106113
if: startsWith(github.head_ref, 'releases/v')
107114
run: gh pr create --title "release ${{steps.vars.outputs.version}}" --body "https://${{steps.vars.outputs.version}}.livecodes.io" --base main --head develop

functions/vendors/templates.js

Lines changed: 131 additions & 89 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "livecodes",
33
"version": "0.0.0",
44
"appVersion": "48",
5-
"description": "Code Playground That Just Works!",
5+
"description": "A Code Playground That Just Works!",
66
"author": "Hatem Hosny",
77
"license": "MIT",
88
"keywords": [],

scripts/bundle-types.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ const bundleTypes = () => {
1111
const outPath = outDir + outFile;
1212

1313
// delete if exists
14+
try {
15+
fs.unlinkSync(path.resolve(tempPath));
16+
} catch {}
1417
try {
1518
fs.unlinkSync(path.resolve(outPath));
1619
} catch {}

scripts/clean-types.js

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,12 @@ const path = require('path');
2424
const cleanTypes = () => {
2525
console.log('\nCleaning types...');
2626

27-
const packageJsonPath = process.argv[2] || path.join('src', 'sdk', 'package.sdk.json');
28-
const outDir = process.argv[3] || path.join('build', 'sdk', 'types');
27+
const packageJsonPath = path.join('src', 'sdk', 'package.sdk.json');
28+
const outDir = path.join('build', 'sdk', 'types');
2929

30-
const expectedDepsEnv = process.env.EXPECTED_DEPS;
31-
const expectedDeps = expectedDepsEnv
32-
? expectedDepsEnv
33-
.split(',')
34-
.map((s) => s.trim())
35-
.filter(Boolean)
36-
: ['models.d.ts'];
30+
const expectedDeps = ['models.d.ts'];
3731

38-
// ─── 1. Extract type paths from package.json ────────────────────────
32+
// ─── Extract type paths from package.json ────────────────────────
3933

4034
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
4135

@@ -64,7 +58,7 @@ const cleanTypes = () => {
6458
process.exit(1);
6559
}
6660

67-
// ─── 2. Map package.json paths to outDir-relative filenames ─────────
61+
// ─── Map package.json paths to outDir-relative filenames ─────────
6862

6963
const norm = (/** @type {string} */ p) => p.split(path.sep).join('/');
7064

@@ -92,7 +86,7 @@ const cleanTypes = () => {
9286
...new Set(stripped.map((p) => (commonDir ? p.slice(commonDir.length + 1) : p))),
9387
];
9488

95-
// ─── 3. Validate public entries exist ───────────────────────────────
89+
// ─── Validate public entries exist ───────────────────────────────
9690

9791
const missing = publicFiles.filter((f) => !fs.existsSync(path.join(outDir, f)));
9892
if (missing.length > 0) {
@@ -104,7 +98,7 @@ const cleanTypes = () => {
10498
process.exit(1);
10599
}
106100

107-
// ─── 4. Collect all .d.ts files in outDir (recursive) ──────────────
101+
// ─── Collect all .d.ts files in outDir (recursive) ──────────────
108102

109103
/**
110104
* @param {fs.PathLike} dir
@@ -124,7 +118,7 @@ const cleanTypes = () => {
124118

125119
const allFiles = collectDts(outDir);
126120

127-
// ─── 5. Walk transitive imports from public entries ─────────────────
121+
// ─── Walk transitive imports from public entries ─────────────────
128122

129123
/**
130124
* @param {fs.PathOrFileDescriptor} absPath
@@ -174,13 +168,13 @@ const cleanTypes = () => {
174168

175169
const reachable = findReachable(publicFiles);
176170

177-
// ─── 6. Classify files ─────────────────────────────────────────────
171+
// ─── Classify files ─────────────────────────────────────────────
178172

179173
const publicSet = new Set(publicFiles);
180174
const sharedDeps = [...reachable].filter((f) => !publicSet.has(f)).sort();
181175
const internal = allFiles.filter((/** @type {any} */ f) => !reachable.has(f)).sort();
182176

183-
// ─── 7. Validate shared dependencies against allow-list ─────────────
177+
// ─── Validate shared dependencies against allow-list ─────────────
184178

185179
const expectedSet = new Set(expectedDeps);
186180
const unexpected = sharedDeps.filter((f) => !expectedSet.has(f));
@@ -202,7 +196,7 @@ const cleanTypes = () => {
202196
sharedDeps.forEach((f) => console.log(` ${f}`));
203197
}
204198

205-
// ─── 8. Delete unreachable files ────────────────────────────────────
199+
// ─── Delete unreachable files ────────────────────────────────────
206200

207201
if (internal.length === 0) {
208202
console.log('No internal files to remove.');

scripts/prepare-jsr.mts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { existsSync } from 'node:fs';
2+
import { cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
3+
import { dirname, extname, join, resolve } from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = dirname(__filename);
8+
const ROOT = resolve(__dirname, '..');
9+
const SRC_SDK = join(ROOT, 'src', 'sdk');
10+
const DIST_SDK = join(ROOT, 'dist', 'jsr');
11+
12+
async function main(): Promise<void> {
13+
// Clean dist/sdk if it already exists
14+
if (existsSync(DIST_SDK)) {
15+
await rm(DIST_SDK, { recursive: true });
16+
}
17+
await mkdir(DIST_SDK, { recursive: true });
18+
19+
await copyDir(SRC_SDK, DIST_SDK);
20+
21+
for (const file of ['LICENSE', 'README.md']) {
22+
const src = join(ROOT, file);
23+
if (existsSync(src)) {
24+
await cp(src, join(DIST_SDK, file));
25+
}
26+
}
27+
28+
await processFiles(DIST_SDK);
29+
30+
console.log('JSR package prepared in dist/jsr');
31+
}
32+
33+
/**
34+
* Recursively copies a directory, skipping __tests__ subdirectories, UMD files and package.*.json files.
35+
*/
36+
async function copyDir(src: string, dest: string): Promise<void> {
37+
const entries = await readdir(src, { withFileTypes: true });
38+
await mkdir(dest, { recursive: true });
39+
40+
for (const entry of entries) {
41+
if (
42+
entry.name === '__tests__' ||
43+
entry.name.includes('.umd') ||
44+
entry.name.includes('package.')
45+
)
46+
continue;
47+
48+
const srcPath = join(src, entry.name);
49+
const destPath = join(dest, entry.name);
50+
51+
if (entry.isDirectory()) {
52+
await copyDir(srcPath, destPath);
53+
} else {
54+
await cp(srcPath, destPath);
55+
}
56+
}
57+
}
58+
59+
/**
60+
* Recursively processes all TypeScript files in a directory, applying
61+
* the required transformations for JSR compatibility.
62+
*/
63+
async function processFiles(dir: string): Promise<void> {
64+
const entries = await readdir(dir, { withFileTypes: true });
65+
66+
for (const entry of entries) {
67+
const fullPath = join(dir, entry.name);
68+
69+
if (entry.isDirectory()) {
70+
await processFiles(fullPath);
71+
continue;
72+
}
73+
74+
// Only process TypeScript files (.ts, .tsx, .mts, .cts, etc.)
75+
if (!/\.[mc]?tsx?$/.test(entry.name)) continue;
76+
77+
let content = await readFile(fullPath, 'utf-8');
78+
79+
// Remove `// @ts-ignore` comment lines (including optional trailing text)
80+
content = content.replace(/^\s*\/\/\s*@ts-ignore\b.*\r?\n/gm, '');
81+
82+
// Add file extensions to relative imports and exports
83+
content = addExtensions(content, fullPath);
84+
85+
// Replace @vue/runtime-core → vue
86+
content = content.replace(/(['"])@vue\/runtime-core\1/g, '$1vue$1');
87+
88+
await writeFile(fullPath, content);
89+
}
90+
}
91+
92+
/**
93+
* Adds file extensions to relative import/export specifiers that lack them.
94+
*
95+
* Handles:
96+
* import { foo } from './bar' → './bar.ts'
97+
* export { foo } from './bar' → './bar.ts'
98+
* export type { Foo } from './bar' → './bar.ts'
99+
* import('./bar') → './bar.ts'
100+
*/
101+
function addExtensions(content: string, filePath: string): string {
102+
const dir = dirname(filePath);
103+
104+
const replacer = (_match: string, prefix: string, specifier: string, suffix: string): string => {
105+
// Skip specifiers that already have a known file extension
106+
const ext = extname(specifier);
107+
if (
108+
ext &&
109+
['.ts', '.tsx', '.mts', '.js', '.jsx', '.mjs', '.json', '.svelte', '.css'].includes(ext)
110+
) {
111+
return `${prefix}${specifier}${suffix}`;
112+
}
113+
114+
const resolved = resolveExtension(dir, specifier);
115+
if (resolved) {
116+
return `${prefix}${specifier}${resolved}${suffix}`;
117+
}
118+
119+
return `${prefix}${specifier}${suffix}`;
120+
};
121+
122+
// Static imports/exports: from './foo' or from "../foo"
123+
content = content.replace(/(from\s+['"])(\.\.?\/[^'"]*?)(['"])/g, replacer);
124+
125+
// Dynamic imports: import('./foo')
126+
content = content.replace(/(import\(\s*['"])(\.\.?\/[^'"]*?)(['"]\s*\))/g, replacer);
127+
128+
return content;
129+
}
130+
131+
/**
132+
* Resolves the file extension for a relative specifier by checking the
133+
* filesystem for matching files in the dist directory.
134+
*
135+
* Returns the extension string (e.g. '.ts') to append, or '' if no match found.
136+
*/
137+
function resolveExtension(dir: string, specifier: string): string {
138+
const base = resolve(dir, specifier);
139+
const extensions = ['.ts', '.tsx', '.mts'];
140+
141+
// Direct file match: ./foo → ./foo.ts
142+
for (const ext of extensions) {
143+
if (existsSync(base + ext)) {
144+
return ext;
145+
}
146+
}
147+
148+
// Directory index match: ./foo → ./foo/index.ts
149+
for (const ext of extensions) {
150+
if (existsSync(join(base, `index${ext}`))) {
151+
return `/index${ext}`;
152+
}
153+
}
154+
155+
return '';
156+
}
157+
158+
main().catch((err) => {
159+
console.error('Failed to prepare JSR package:', err);
160+
process.exit(1);
161+
});

scripts/start-release.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import prettier from 'prettier';
99
const require = createRequire(import.meta.url);
1010
const appPkgPath = '../package.json';
1111
const sdkPkgPath = '../src/sdk/package.sdk.json';
12+
const jsrJsonPath = '../src/sdk/jsr.json';
1213
const changelogPath = '../CHANGELOG.md';
1314

1415
const appPkg = require(appPkgPath);
1516
const originalAppVersion = appPkg.appVersion;
1617
const sdkPkg = require(sdkPkgPath);
18+
const jsrJson = require(jsrJsonPath);
1719
const originalSDKVersion = sdkPkg.version;
1820
const prettierConfig = appPkg.prettier;
1921

@@ -225,10 +227,12 @@ const changeSDKVersion = async (releaseNotes) => {
225227
: selectedVersion;
226228
const versionName = 'sdk-v' + version;
227229
sdkPkg.version = version;
230+
jsrJson.version = version;
228231
if (!(await confirm({ message: `Creating SDK version: ${versionName}\nProceed?` }))) {
229232
return confirmCancel(() => changeSDKVersion(releaseNotes));
230233
}
231234
fs.writeFileSync(new URL(sdkPkgPath, import.meta.url), await stringify(sdkPkg), 'utf8');
235+
fs.writeFileSync(new URL(jsrJsonPath, import.meta.url), await stringify(jsrJson), 'utf8');
232236
return releaseNotes;
233237
};
234238

src/sdk/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
/**
2+
* LiveCodes SDK - A Code Playground That Just Works!
3+
*
4+
* This module is the main entry point for the LiveCodes SDK.
5+
* It provides the core `createPlayground` and `getPlaygroundUrl` functions.
6+
*
7+
* @module
8+
*/
9+
110
/* eslint-disable no-redeclare */
211
import { compressToEncodedURIComponent } from 'lz-string';
312
import { getIframeAllowAttribute, type CustomEvents } from './internal';

src/sdk/internal.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
// these should not be exported from the SDK
1+
/**
2+
* This module contains internal functions and types used by the LiveCodes SDK.
3+
* These are not part of the public API and should not be exported from SDK modules.
4+
*
5+
* @module
6+
*/
27

8+
/**
9+
* Custom events emitted by LiveCodes playground.
10+
*/
311
export interface CustomEvents {
412
init: 'livecodes-init';
513
/** @deprecated config is sent in hash params */
@@ -65,6 +73,11 @@ const allowAttributes: Record<'chrome' | 'firefox' | 'default', string[]> = {
6573
],
6674
};
6775

76+
/**
77+
* Detects the current browser type.
78+
*
79+
* @returns 'chrome', 'firefox', or 'default' based on user agent detection
80+
*/
6881
export const detectBrowser = (): 'chrome' | 'firefox' | 'default' => {
6982
if (typeof navigator === 'undefined') return 'default';
7083
const ua = navigator.userAgent;
@@ -73,6 +86,11 @@ export const detectBrowser = (): 'chrome' | 'firefox' | 'default' => {
7386
return 'default';
7487
};
7588

89+
/**
90+
* Generates the iframe allow attribute value based on detected browser capabilities.
91+
*
92+
* @returns A semicolon-separated string of allowed features for the iframe
93+
*/
7694
export const getIframeAllowAttribute = (): string =>
7795
allowAttributes[detectBrowser()]
7896
.filter((feature) => {

src/sdk/jsr.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@livecodes/sdk",
3+
"description": "A Code Playground That Just Works!",
4+
"version": "0.13.0",
5+
"license": "MIT",
6+
"exports": {
7+
".": "./index.ts",
8+
"./preact": "./preact.ts",
9+
"./react": "./react.tsx",
10+
"./solid": "./solid.ts",
11+
"./svelte": "./svelte.ts",
12+
"./vue": "./vue.ts",
13+
"./web-components": "./web-components.ts"
14+
},
15+
"include": ["**/*.ts", "**/*.tsx", "**/*.svelte", "README.md", "LICENSE"],
16+
"exclude": ["**/*.test.ts", "**/*.spec.ts", "**/*.umd.ts", "node_modules/**"],
17+
"imports": {
18+
"@live-codes/solid-sdk": "npm:@live-codes/solid-sdk@^0.4.0",
19+
"lz-string": "npm:lz-string@^1.4.4",
20+
"preact": "npm:preact@^10",
21+
"preact/hooks": "npm:preact@^10/hooks",
22+
"preact/jsx-runtime": "npm:preact@^10/jsx-runtime",
23+
"react": "npm:react@^19",
24+
"solid-js": "npm:solid-js@^1",
25+
"svelte": "npm:svelte@^5",
26+
"vue": "npm:vue@^3"
27+
}
28+
}

0 commit comments

Comments
 (0)