dproject
v1.0.6
Published
Read MS Project (MSPDI XML) files in JavaScript and TypeScript. Zero-dependency, browser + Node.
Maintainers
Readme
DProject
Read MS Project (MSPDI XML) files in JavaScript and TypeScript. Zero dependencies. Works in the browser, in Node, and in workers. A clean-room JS implementation of the MSPDI reader the ecosystem has been missing for a decade.
Why
Today, if you want to read MS Project data in JavaScript, your options are:
| Option | Bundle | Trade-off |
|---|---|---|
| mpxj-wasm (Java→WASM) | ~10 MB | Heavy, slow startup |
| Backend service (Java/Python MPXJ) | 0 (server-side) | Needs hosting |
| Roll your own XML walker | ~?? | Fragile, no types |
DProject fills the gap: a small (~18 KB gzipped target), zero-dep, pure-JS library that turns an MSPDI XML string into a clean, JSON-serializable Project shape.
Scope (v1.0): MSPDI XML only.
.mppbinary is a different beast — open in MS Project and Save As → XML to use DProject.
Peer comparison
Measured on sample-01.xml (887 KB, 121 tasks) — Node 24, M-class laptop:
| | Parse time (median) | Bundle (gzipped) | LOC for "list all task names" | Coverage |
|---|---|---|---|---|
| DProject | 22 ms | 7.5 KB | 2 lines | Full MSPDI + validate + serialize |
| htmlparser2 (raw XML) | 25 ms | ~14 KB | ~150 lines | XML tree only |
| mpxj-wasm (Java→WASM) | ~150 ms + multi-sec init | ~10 MB | ~8 lines | Full + .mpp binary |
DProject is ~10% faster than even raw XML parsing (htmlparser2) — while doing dramatically more work: full normalisation, type coercion, parent reconstruction, predecessor linking, baseline/calendar/extended-attribute extraction, and validation.
Run the benchmark yourself: node bench/peer-comparison.js.
Status
v1.0.0 — production-ready. All 10 quality gates green.
| # | Gate | Bar | Result |
|---|---|---|---|
| 1 | Unit tests | every layer covered | 165 cases across xml/coerce/edge/validator |
| 2 | Real-world fixtures | 4 MSPDI samples parse | ✅ 121 + 23 + 22 + 33 tasks |
| 3 | Edge cases | empty/malformed/encoding | ✅ 24 cases including BOM, CDATA, entities |
| 4 | Schema validator | structural invariants | ✅ 10 error codes, all caught |
| 6 | TypeScript surface | .d.ts matches runtime | ✅ 43 surface checks |
| 7 | Bundle size | min ≤ 50 KB / gz ≤ 18 KB | ✅ 17 KB / 5.25 KB |
| 8 | Performance | 1000 tasks < 100ms | ✅ 28 ms median (~35K tasks/sec) |
| 9 | Memory | no leak across 100 parses | ✅ 0.1 MB growth |
| 10 | Browser ↔ Node parity | byte-identical JSON | ✅ all 4 fixtures |
Total: 258 / 258 tests passing. Run npm test (or node tests/run-all.js).
Gate 5 (round-trip via writer) is deferred to v1.0.
🔜 v1.0 (GA) — round-trip writer (serialize()), peer benchmarks vs mpxj-wasm, calendars, npm publish.
Install
npm install dprojectOr drop into a browser as a single bundle:
<script src="dist/dproject.min.js"></script>
<script>
const project = DProject.parse(xmlString);
</script>Run npm run build to (re)generate dist/dproject.min.js and dist/dproject.min.js.gz.
Quick start
const DProject = require('dproject');
const project = DProject.parse(xmlString);
console.log(project.meta.title); // "Project Management Workploan"
console.log(project.tasks.length); // 121
console.log(project.tasks[0].duration); // 38160 (minutes)
console.log(project.tasks[0].predecessors); // [ { predecessorUid, type, typeName, lag, ... } ]Other entry points
// From a File / Blob (browser)
const project = await DProject.fromFile(fileInput.files[0]);
// From a URL (browser or Node 18+)
const project = await DProject.fromUrl('/schedules/q1.xml');
// From a path (Node)
const project = await DProject.fromFile('/abs/path/to/file.xml');Project shape
{
meta: {
name, title, author, manager, company,
startDate, finishDate, statusDate,
currencySymbol, currencyCode, currencyDigits,
minutesPerDay, minutesPerWeek, daysPerMonth,
scheduleFromStart, calendarUid, ...
},
tasks: [
{
uid, id, name,
outlineLevel, outlineNumber, parentUid, // parent reconstructed for you
summary, milestone, critical,
start, finish, // ISO 8601 strings
duration, work, // minutes (number)
percentComplete, cost,
predecessors: [
{ predecessorUid, type, typeName, lag, crossProject }
// ↑ 0/1/2/3 ↑ 'FF'|'FS'|'SF'|'SS' ↑ minutes
],
...
}
],
resources: [ { uid, id, name, type, maxUnits, standardRate, ... } ],
assignments: [ { taskUid, resourceUid, units, work, cost, start, finish, ... } ]
}The whole result is plain JSON — no Date objects, no classes, no circular refs. JSON.stringify(project) works.
Examples
Four runnable examples/ — JS users can node them directly, TS users get the same API with full types via import DProject from 'dproject':
| File | What it does | Run |
|---|---|---|
| extract-tasks.js | One-line summary per task: WBS · name · duration · start–finish · summary/milestone/critical flags | node examples/extract-tasks.js [file.xml] |
| extract-deps.js | Print the dependency graph as edges (A → B (FS, +2d)); add --json for a machine-readable feed | node examples/extract-deps.js [--json] [file.xml] |
| validate.js | Run the structural validator; errors grouped by code; exit 0/1/2 — CI-friendly | node examples/validate.js [file.xml] |
| modify-and-write.js | Round-trip demo: parse → mutate (mark all milestones 100%) → serialize() → write .out.xml → re-parse to verify | node examples/modify-and-write.js [in.xml] [out.xml] |
Each example defaults to tests/fixtures/sample-01.xml so they work with zero arguments. Plus examples/viewer.html, a self-contained 48 KB browser viewer (drop-in <script>), and examples/consumer.ts, a TS surface compile check.
Design
DProject is built in four small layers, each with one job:
xml.js ← pure-JS XML tokenizer (~250 LOC, no deps)
src/types/ ← coerce.js (int/bool/date/duration) + fields.js (declarative MSPDI mappings)
parser.js ← walks XML tree → raw string fields
normalizer.js ← raw → typed Project + parent reconstruction
dproject.js ← public API (parse / fromFile / fromUrl)Adding an MSPDI field never touches the walker — just add a row to src/types/fields.js.
License
MIT — © 2026 Dharmesh Patel. See LICENSE and NOTICE.md.
Author
Built by Dharmesh Patel.
