Skip to content

Commit c0c0be8

Browse files
authored
fix: cjs build (#33)
locate-character does not export a CJS module so we need to transpile it to ESM at build time.
1 parent f3deec3 commit c0c0be8

File tree

5 files changed

+152
-6
lines changed

5 files changed

+152
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,4 @@ TODO.md
5757
.env.sentry-build-plugin
5858
bin
5959

60+
AGENTS.md

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "promptl-ai",
3-
"version": "0.7.0",
3+
"version": "0.7.1",
44
"author": "Latitude Data",
55
"license": "MIT",
66
"description": "Compiler for PromptL, the prompt language",
@@ -39,16 +39,16 @@
3939
"code-red": "^1.0.3",
4040
"fast-sha256": "^1.3.0",
4141
"locate-character": "^3.0.0",
42-
"yaml": "^2.4.5",
4342
"openai": "^4.98.0",
43+
"yaml": "^2.4.5",
4444
"zod": "^3.23.8"
4545
},
4646
"devDependencies": {
4747
"@eslint/eslintrc": "^3.2.0",
4848
"@eslint/js": "^9.17.0",
4949
"@rollup/plugin-alias": "^5.1.0",
5050
"@rollup/plugin-commonjs": "^25.0.7",
51-
"@rollup/plugin-node-resolve": "^15.2.3",
51+
"@rollup/plugin-node-resolve": "^15.3.1",
5252
"@rollup/plugin-typescript": "^11.1.6",
5353
"@types/estree": "^1.0.1",
5454
"@types/node": "^20.12.12",

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rollup.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as url from 'url'
44
import alias from '@rollup/plugin-alias'
55
import typescript from '@rollup/plugin-typescript'
66
import { dts } from 'rollup-plugin-dts'
7+
import { nodeResolve } from '@rollup/plugin-node-resolve'
78

89
/**
910
* We have a internal circular dependency in the compiler,
@@ -39,6 +40,7 @@ export default [
3940
{ file: 'dist/index.cjs', format: 'cjs' },
4041
],
4142
plugins: [
43+
nodeResolve(),
4244
typescript({
4345
noEmit: true,
4446
tsconfig: './tsconfig.json',
@@ -48,8 +50,6 @@ export default [
4850
external: [
4951
'openai',
5052
'acorn',
51-
'locate-character',
52-
'code-red',
5353
'node:crypto',
5454
'yaml',
5555
'crypto',

src/build.test.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { execSync } from 'child_process'
2+
import { existsSync, readFileSync } from 'fs'
3+
import { join } from 'path'
4+
import { describe, expect, it, beforeAll } from 'vitest'
5+
6+
const DIST_DIR = join(process.cwd(), 'dist')
7+
const CJS_FILE = join(DIST_DIR, 'index.cjs')
8+
const ESM_FILE = join(DIST_DIR, 'index.js')
9+
const DTS_FILE = join(DIST_DIR, 'index.d.ts')
10+
11+
describe('Build Output Verification', () => {
12+
beforeAll(() => {
13+
// Ensure build exists
14+
if (!existsSync(DIST_DIR)) {
15+
execSync('pnpm build', { stdio: 'inherit' })
16+
}
17+
})
18+
19+
it('should generate all required build files', () => {
20+
expect(existsSync(CJS_FILE)).toBe(true)
21+
expect(existsSync(ESM_FILE)).toBe(true)
22+
expect(existsSync(DTS_FILE)).toBe(true)
23+
})
24+
25+
it('should generate valid CommonJS format', () => {
26+
const cjsContent = readFileSync(CJS_FILE, 'utf8')
27+
28+
// Should start with 'use strict'
29+
expect(cjsContent).toMatch(/^'use strict'/)
30+
31+
// Should use require() for dependencies
32+
expect(cjsContent).toMatch(/require\(['"]/)
33+
34+
// Should not contain ES6 import statements
35+
expect(cjsContent).not.toMatch(/^import\s+/)
36+
})
37+
38+
it('should generate valid ES module format', () => {
39+
const esmContent = readFileSync(ESM_FILE, 'utf8')
40+
41+
// Should use import statements
42+
expect(esmContent).toMatch(/^import\s+/)
43+
44+
// Should not contain require() calls
45+
expect(esmContent).not.toMatch(/require\(['"]/)
46+
})
47+
48+
it('should generate TypeScript definitions', () => {
49+
const dtsContent = readFileSync(DTS_FILE, 'utf8')
50+
51+
// Should contain export declarations
52+
expect(dtsContent).toMatch(/export\s+/)
53+
54+
// Should contain key function types
55+
expect(dtsContent).toMatch(/parse/)
56+
expect(dtsContent).toMatch(/render/)
57+
})
58+
59+
it('should load CJS build correctly', async () => {
60+
// Use dynamic require to avoid bundler issues
61+
const cjsModule = await import(CJS_FILE)
62+
63+
expect(typeof cjsModule.parse).toBe('function')
64+
expect(typeof cjsModule.render).toBe('function')
65+
expect(typeof cjsModule.createChain).toBe('function')
66+
expect(cjsModule.CompileError).toBeDefined()
67+
68+
// Should have all expected exports
69+
const exports = Object.keys(cjsModule)
70+
expect(exports).toContain('parse')
71+
expect(exports).toContain('render')
72+
expect(exports).toContain('CompileError')
73+
expect(exports).toContain('ContentType')
74+
expect(exports).toContain('MessageRole')
75+
})
76+
77+
it('should load ESM build correctly', async () => {
78+
const esmModule = await import(ESM_FILE)
79+
80+
expect(typeof esmModule.parse).toBe('function')
81+
expect(typeof esmModule.render).toBe('function')
82+
expect(typeof esmModule.createChain).toBe('function')
83+
expect(esmModule.CompileError).toBeDefined()
84+
85+
// Should have all expected exports
86+
const exports = Object.keys(esmModule)
87+
expect(exports).toContain('parse')
88+
expect(exports).toContain('render')
89+
expect(exports).toContain('CompileError')
90+
expect(exports).toContain('ContentType')
91+
expect(exports).toContain('MessageRole')
92+
})
93+
94+
it('should have functional parse and render in CJS build', async () => {
95+
const { parse, render } = await import(CJS_FILE)
96+
97+
const template = 'Hello {{name}}!'
98+
const parsed = parse(template)
99+
100+
expect(parsed).toBeDefined()
101+
expect(parsed.type).toBe('Fragment')
102+
103+
const rendered = await render({
104+
prompt: template,
105+
parameters: { name: 'World' }
106+
})
107+
108+
expect(rendered).toBeDefined()
109+
expect(rendered.messages).toBeDefined()
110+
expect(rendered.config).toBeDefined()
111+
expect(Array.isArray(rendered.messages)).toBe(true)
112+
})
113+
114+
it('should have functional parse and render in ESM build', async () => {
115+
const { parse, render } = await import(ESM_FILE)
116+
117+
const template = 'Hello {{name}}!'
118+
const parsed = parse(template)
119+
120+
expect(parsed).toBeDefined()
121+
expect(parsed.type).toBe('Fragment')
122+
123+
const rendered = await render({
124+
prompt: template,
125+
parameters: { name: 'World' }
126+
})
127+
128+
expect(rendered).toBeDefined()
129+
expect(rendered.messages).toBeDefined()
130+
expect(rendered.config).toBeDefined()
131+
expect(Array.isArray(rendered.messages)).toBe(true)
132+
})
133+
134+
it('should have identical exports between CJS and ESM builds', async () => {
135+
const cjsModule = await import(CJS_FILE)
136+
const esmModule = await import(ESM_FILE)
137+
138+
// Filter out 'default' export which is expected to differ between CJS and ESM
139+
const cjsExports = Object.keys(cjsModule).filter(key => key !== 'default').sort()
140+
const esmExports = Object.keys(esmModule).filter(key => key !== 'default').sort()
141+
142+
expect(cjsExports).toEqual(esmExports)
143+
expect(cjsExports.length).toBeGreaterThan(10) // Ensure we have substantial exports
144+
})
145+
})

0 commit comments

Comments
 (0)