@m4l-jweb/build 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +6 -4
- package/src/amxd.mjs +93 -93
- package/src/chains.mjs +338 -119
- package/src/index.mjs +246 -228
- package/src/init.mjs +38 -33
- package/templates/starter/.prettierrc +5 -0
- package/templates/starter/README.md +38 -12
- package/templates/starter/index.html +1 -1
- package/templates/starter/package.json +40 -34
- package/templates/starter/patcher/devices.mjs +33 -24
- package/templates/starter/scripts/build-ui.mjs +19 -0
- package/templates/starter/scripts/dev.mjs +24 -0
- package/templates/starter/scripts/devices.d.mts +24 -0
- package/templates/starter/scripts/devices.mjs +35 -0
- package/templates/starter/src/app/shared/Frame.tsx +51 -0
- package/templates/starter/src/app/shared/device.ts +79 -0
- package/templates/starter/src/app/{worker.ts → shared/worker.ts} +8 -8
- package/templates/starter/src/app/{{name}}/App.tsx +62 -0
- package/templates/starter/src/app/{{name}}/protocol.ts +32 -0
- package/templates/starter/src/app/{{name}}/surface.ts +26 -0
- package/templates/starter/src/index.css +117 -56
- package/templates/starter/src/main.tsx +35 -4
- package/templates/starter/src/vite-env.d.ts +2 -2
- package/templates/starter/tsconfig.app.json +26 -22
- package/templates/starter/tsconfig.node.json +1 -1
- package/templates/starter/vite.config.ts +61 -28
- package/templates/starter/vitest.config.ts +1 -1
- package/templates/starter/src/app/App.tsx +0 -87
- package/templates/starter/src/app/protocol.ts +0 -32
package/src/index.mjs
CHANGED
|
@@ -22,8 +22,6 @@ const require = createRequire(import.meta.url);
|
|
|
22
22
|
const pkgDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
23
|
const templates = path.join(pkgDir, "templates");
|
|
24
24
|
|
|
25
|
-
const UI_NAME = "ui.html";
|
|
26
|
-
|
|
27
25
|
/* ------------------------------------------------------------------ *
|
|
28
26
|
* Step 1: the wrapper
|
|
29
27
|
* ------------------------------------------------------------------ */
|
|
@@ -38,70 +36,70 @@ const UI_NAME = "ui.html";
|
|
|
38
36
|
* order.
|
|
39
37
|
*/
|
|
40
38
|
export function buildWrapper(root) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
39
|
+
const { sources, types } = require("@m4l-jweb/wrapper/sources");
|
|
40
|
+
const deviceExt = path.join(root, "wrapper", "device.ts");
|
|
41
|
+
const files = [...sources, ...(existsSync(deviceExt) ? [deviceExt] : [])];
|
|
42
|
+
|
|
43
|
+
const outDir = path.join(root, "dist", "wrapper");
|
|
44
|
+
const tmp = path.join(root, "dist", ".wrapper-tsc");
|
|
45
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
46
|
+
mkdirSync(tmp, { recursive: true });
|
|
47
|
+
mkdirSync(outDir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
// Copy every source next to each other FIRST, then compile in place.
|
|
50
|
+
//
|
|
51
|
+
// tsc derives its output layout from the common root of its inputs. The
|
|
52
|
+
// packaged sources live in node_modules and the device's own device.ts lives
|
|
53
|
+
// in the repo, so that common root can be some ancestor of both - and the
|
|
54
|
+
// outputs land in a mirrored directory tree instead of flat. Staging them in
|
|
55
|
+
// one directory makes the output names predictable, which is what lets us
|
|
56
|
+
// concatenate them in order below.
|
|
57
|
+
const staged = files.map((f, i) => {
|
|
58
|
+
// Prefix with the index: order is the contract (core must precede the rest),
|
|
59
|
+
// and two sources could share a basename.
|
|
60
|
+
const dest = path.join(tmp, `${String(i).padStart(2, "0")}-${path.basename(f)}`);
|
|
61
|
+
writeFileSync(dest, readFileSync(f, "utf8"));
|
|
62
|
+
return dest;
|
|
63
|
+
});
|
|
64
|
+
const stagedTypes = path.join(tmp, path.basename(types));
|
|
65
|
+
writeFileSync(stagedTypes, readFileSync(types, "utf8"));
|
|
66
|
+
|
|
67
|
+
// The ES5 target is a build gate, not a style preference. `module: "none"`
|
|
68
|
+
// forbids imports, which is exactly the [js] constraint.
|
|
69
|
+
const tsconfig = path.join(tmp, "tsconfig.json");
|
|
70
|
+
writeFileSync(
|
|
71
|
+
tsconfig,
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
compilerOptions: {
|
|
74
|
+
target: "ES5",
|
|
75
|
+
lib: ["ES5"],
|
|
76
|
+
module: "none",
|
|
77
|
+
outDir: tmp,
|
|
78
|
+
strict: true,
|
|
79
|
+
// At [js] global scope `this` IS the jsthis object - that is how
|
|
80
|
+
// `this.patcher.filepath` works.
|
|
81
|
+
noImplicitThis: false,
|
|
82
|
+
noImplicitAny: false,
|
|
83
|
+
skipLibCheck: true,
|
|
84
|
+
types: [],
|
|
85
|
+
},
|
|
86
|
+
files: [stagedTypes, ...staged],
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
execFileSync(process.execPath, [require.resolve("typescript/bin/tsc"), "-p", tsconfig], { stdio: "inherit" });
|
|
91
|
+
|
|
92
|
+
// Concatenate the emitted scripts in source order: core's lifecycle first, the
|
|
93
|
+
// device's own handlers last.
|
|
94
|
+
const js = staged.map((f) => readFileSync(f.replace(/\.ts$/, ".js"), "utf8")).join("\n");
|
|
95
|
+
assertES5(js, "wrapper");
|
|
96
|
+
|
|
97
|
+
const out = path.join(outDir, "wrapper.js");
|
|
98
|
+
writeFileSync(out, js);
|
|
99
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
100
|
+
|
|
101
|
+
console.log(`m4l-jweb: wrapper.js (${js.length} bytes, ES5 verified, from ${files.length} sources)`);
|
|
102
|
+
return out;
|
|
105
103
|
}
|
|
106
104
|
|
|
107
105
|
/* ------------------------------------------------------------------ *
|
|
@@ -109,16 +107,16 @@ export function buildWrapper(root) {
|
|
|
109
107
|
* ------------------------------------------------------------------ */
|
|
110
108
|
|
|
111
109
|
async function readManifest(root) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
const p = path.join(root, "patcher", "devices.mjs");
|
|
111
|
+
if (!existsSync(p)) throw new Error("patcher/devices.mjs not found - a device repo needs a manifest");
|
|
112
|
+
return (await import(pathToFileURL(p).href)).default;
|
|
115
113
|
}
|
|
116
114
|
|
|
117
115
|
/** patcher/base.json in the device repo wins; otherwise the packaged template. */
|
|
118
116
|
function readBase(root) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
const local = path.join(root, "patcher", "base.json");
|
|
118
|
+
const src = existsSync(local) ? local : path.join(templates, "base.json");
|
|
119
|
+
return JSON.parse(readFileSync(src, "utf8"));
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
/**
|
|
@@ -133,52 +131,52 @@ function readBase(root) {
|
|
|
133
131
|
* in the library.
|
|
134
132
|
*/
|
|
135
133
|
async function loadDeviceChains(root) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
const p = path.join(root, "patcher", "chains.mjs");
|
|
135
|
+
if (!existsSync(p)) return;
|
|
136
|
+
await import(pathToFileURL(p).href);
|
|
137
|
+
console.log("m4l-jweb: loaded device chains from patcher/chains.mjs");
|
|
140
138
|
}
|
|
141
139
|
|
|
142
140
|
export async function generatePatchers(root) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
141
|
+
const devices = await readManifest(root);
|
|
142
|
+
const base = readBase(root);
|
|
143
|
+
await loadDeviceChains(root);
|
|
144
|
+
const outDir = path.join(root, "dist", "patchers");
|
|
145
|
+
mkdirSync(outDir, { recursive: true });
|
|
146
|
+
|
|
147
|
+
for (const d of devices) {
|
|
148
|
+
const amxdtype = AMXD_TYPES[d.type];
|
|
149
|
+
if (!amxdtype) throw new Error(`unknown type "${d.type}" for device "${d.name}" (midi | audio | instrument)`);
|
|
150
|
+
|
|
151
|
+
const p = structuredClone(base);
|
|
152
|
+
const { boxes, lines } = p.patcher;
|
|
153
|
+
p.patcher.project.amxdtype = amxdtype;
|
|
154
|
+
resetLayout();
|
|
155
|
+
|
|
156
|
+
// The wrapper is mode-switched by its object-box argument. `mode` defaults
|
|
157
|
+
// to the device type, but they are not always the same thing: a sample
|
|
158
|
+
// player can be an audio-effect device ("type") that the wrapper must treat
|
|
159
|
+
// as a sampler ("mode").
|
|
160
|
+
//
|
|
161
|
+
// jsarguments[0] is the SCRIPT NAME, so the mode lands at jsarguments[1].
|
|
162
|
+
const mode = d.mode ?? d.type;
|
|
163
|
+
boxes.find((b) => b.box.id === "obj-js").box.text = `js wrapper.js ${mode}`;
|
|
164
|
+
|
|
165
|
+
const unmatchedId = d.unmatchedTo === "js" ? "obj-js" : (d.unmatchedTo ?? "obj-js");
|
|
166
|
+
|
|
167
|
+
for (const name of d.chains ?? []) {
|
|
168
|
+
const chain = CHAINS[name];
|
|
169
|
+
if (!chain) throw new Error(`unknown chain "${name}" for device "${d.name}" (known: ${Object.keys(CHAINS).join(", ")})`);
|
|
170
|
+
chain({ boxes, lines, jwebId: "obj-jweb", unmatchedId, device: d });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Parameters feed the UI: a knob move arrives as just another inlet message.
|
|
174
|
+
addParameters(boxes, lines, d.parameters ?? [], "obj-jweb");
|
|
175
|
+
|
|
176
|
+
writeFileSync(path.join(outDir, `${d.name}.json`), JSON.stringify(p, null, "\t"));
|
|
177
|
+
console.log(`m4l-jweb: ${d.name}.json (${d.type}, chains: ${(d.chains ?? []).join(", ") || "none"})`);
|
|
178
|
+
}
|
|
179
|
+
return devices;
|
|
182
180
|
}
|
|
183
181
|
|
|
184
182
|
/* ------------------------------------------------------------------ *
|
|
@@ -194,99 +192,119 @@ export async function generatePatchers(root) {
|
|
|
194
192
|
* requirement.
|
|
195
193
|
*/
|
|
196
194
|
export async function packageDevices(root) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
195
|
+
const dist = path.join(root, "dist");
|
|
196
|
+
const { name, version } = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
197
|
+
const devices = await readManifest(root);
|
|
198
|
+
|
|
199
|
+
const outDir = path.join(dist, name);
|
|
200
|
+
mkdirSync(outDir, { recursive: true });
|
|
201
|
+
|
|
202
|
+
const wrapperJs = readFileSync(path.join(dist, "wrapper", "wrapper.js"), "utf8");
|
|
203
|
+
|
|
204
|
+
// The build stamp is what makes a stale install visible: the wrapper posts it
|
|
205
|
+
// and the UI renders it. Live embeds a copy of the device in the set, so an
|
|
206
|
+
// instance already on a track does NOT update when you reinstall.
|
|
207
|
+
const stamp = `${version} ${new Date().toISOString()}`;
|
|
208
|
+
const banner = `var BUILD_STAMP = ${JSON.stringify(stamp)};\n`;
|
|
209
|
+
|
|
210
|
+
for (const d of devices) {
|
|
211
|
+
const deviceName = `${d.name}.amxd`;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Each device embeds its OWN UI bundle, from dist/ui/<ui ?? name>/index.html
|
|
215
|
+
* (see scripts/build-ui.mjs). A device ships what it is, not its siblings'
|
|
216
|
+
* code.
|
|
217
|
+
*
|
|
218
|
+
* The payload NAME is per-device too - `<device>.html`, not a shared
|
|
219
|
+
* `ui.html`. Every device in a repo extracts its payload into the SAME
|
|
220
|
+
* folder (next to the .amxd), so one shared name would mean two devices
|
|
221
|
+
* overwriting each other's UI on every load, each one convinced the file on
|
|
222
|
+
* disk was stale. The symptom would be a device showing its sibling's
|
|
223
|
+
* interface.
|
|
224
|
+
*/
|
|
225
|
+
const uiName = `${d.name}.html`;
|
|
226
|
+
const uiSrc = path.join(dist, "ui", d.ui ?? d.name, "index.html");
|
|
227
|
+
const legacy = path.join(dist, "index.html"); // single-UI repos (the starter template)
|
|
228
|
+
const uiFrom = existsSync(uiSrc) ? uiSrc : legacy;
|
|
229
|
+
if (!existsSync(uiFrom)) {
|
|
230
|
+
throw new Error(`no UI for "${d.name}" at ${uiSrc} - run \`pnpm build\` (scripts/build-ui.mjs) first`);
|
|
231
|
+
}
|
|
232
|
+
const uiHtml = readFileSync(uiFrom);
|
|
233
|
+
await copyFile(uiFrom, path.join(outDir, uiName));
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Payloads ride inside wrapper.js as base64 and are written to real files
|
|
237
|
+
* next to the .amxd on first load, because Chromium and any external
|
|
238
|
+
* process are blind to Max's frozen virtual filesystem. The UI is always
|
|
239
|
+
* one; a device can declare more (`payloads: ["dist/foo.cjs"]`).
|
|
240
|
+
*/
|
|
241
|
+
let wrapperData = banner + wrapperJs + payloadJs("UI_PAYLOAD", uiName, uiHtml);
|
|
242
|
+
const payloads = (d.payloads ?? []).map((f) => ({ name: path.basename(f), data: readFileSync(path.join(root, f)) }));
|
|
243
|
+
if (payloads.length) wrapperData += extraPayloadsJs(payloads);
|
|
244
|
+
|
|
245
|
+
const amxd = buildAmxd({
|
|
246
|
+
patcherJson: readFileSync(path.join(dist, "patchers", `${d.name}.json`), "utf8"),
|
|
247
|
+
wrapperJs: wrapperData,
|
|
248
|
+
deviceName,
|
|
249
|
+
// Frozen dependencies: readable by Max-native objects only (a poly~
|
|
250
|
+
// voice patcher, say), which is exactly why they can stay frozen.
|
|
251
|
+
extras: (d.extraFiles ?? []).map((f) => ({ name: path.basename(f), data: readFileSync(path.join(root, f)) })),
|
|
252
|
+
});
|
|
253
|
+
writeFileSync(path.join(outDir, deviceName), amxd);
|
|
254
|
+
console.log(`m4l-jweb: ${deviceName} (${d.type}, ${amxd.length} bytes)`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await copyFile(path.join(dist, "wrapper", "wrapper.js"), path.join(outDir, "wrapper.js"));
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Loose files sit NEXT TO the .amxd in the installed folder, as real files.
|
|
261
|
+
*
|
|
262
|
+
* Needed when a Max object resolves a filename when it INSTANTIATES - before
|
|
263
|
+
* the wrapper has run and before it could have extracted anything. Such an
|
|
264
|
+
* object cannot be repointed at runtime, so the file has to be on disk under
|
|
265
|
+
* exactly the name the object was created with. The embedded payload of the
|
|
266
|
+
* same file is then only a fallback for a bare .amxd copied on its own.
|
|
267
|
+
*/
|
|
268
|
+
const loose = [...new Set(devices.flatMap((d) => d.looseFiles ?? []))];
|
|
269
|
+
for (const f of loose) {
|
|
270
|
+
await copyFile(path.join(root, f), path.join(outDir, path.basename(f)));
|
|
271
|
+
console.log(`m4l-jweb: ${path.basename(f)} -> dist/${name}/ (loose)`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Installers go next to the devices so `dist/install-*.ps1` just works.
|
|
275
|
+
const installers = ["install-windows.ps1", "install-mac.sh"];
|
|
276
|
+
for (const f of installers) await copyFile(path.join(templates, f), path.join(dist, f));
|
|
277
|
+
|
|
278
|
+
const zipPath = path.join(dist, `${name}.zip`);
|
|
279
|
+
await new Promise((resolve, reject) => {
|
|
280
|
+
const output = createWriteStream(zipPath);
|
|
281
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
282
|
+
output.on("close", resolve);
|
|
283
|
+
archive.on("error", reject);
|
|
284
|
+
archive.pipe(output);
|
|
285
|
+
const files = [
|
|
286
|
+
...devices.map((d) => `${d.name}.amxd`),
|
|
287
|
+
...devices.map((d) => `${d.name}.html`), // each device's own UI, for inspection
|
|
288
|
+
...loose.map((f) => path.basename(f)),
|
|
289
|
+
"wrapper.js",
|
|
290
|
+
];
|
|
291
|
+
for (const f of files) {
|
|
292
|
+
archive.append(createReadStream(path.join(outDir, f)), { name: `${name}/${f}` });
|
|
293
|
+
}
|
|
294
|
+
for (const f of installers) {
|
|
295
|
+
archive.file(path.join(templates, f), { name: f, mode: 0o755 });
|
|
296
|
+
}
|
|
297
|
+
archive.finalize();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const { size } = await stat(zipPath);
|
|
301
|
+
console.log(`m4l-jweb: dist/${name}.zip (${size} bytes)`);
|
|
284
302
|
}
|
|
285
303
|
|
|
286
304
|
export async function buildAll(root) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
305
|
+
buildWrapper(root);
|
|
306
|
+
await generatePatchers(root);
|
|
307
|
+
await packageDevices(root);
|
|
290
308
|
}
|
|
291
309
|
|
|
292
310
|
/* ------------------------------------------------------------------ *
|
|
@@ -299,27 +317,27 @@ export async function buildAll(root) {
|
|
|
299
317
|
* Live has no Linux build, so there is nothing to install there.
|
|
300
318
|
* ------------------------------------------------------------------ */
|
|
301
319
|
export async function installDevices(root) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
320
|
+
const { name } = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
321
|
+
|
|
322
|
+
if (!existsSync(path.join(root, "dist", name))) {
|
|
323
|
+
throw new Error(`nothing built at dist/${name} - run \`pnpm build\` first`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// The packaged scripts are the real implementation - they have to read Live's
|
|
327
|
+
// own config files to locate the User Library. Pass the device name and the
|
|
328
|
+
// built folder explicitly, since the script does not live in the repo.
|
|
329
|
+
const src = path.join(root, "dist", name);
|
|
330
|
+
const runners = {
|
|
331
|
+
win32: [
|
|
332
|
+
"powershell",
|
|
333
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path.join(templates, "install-windows.ps1"), "-DeviceName", name, "-Src", src],
|
|
334
|
+
],
|
|
335
|
+
darwin: ["bash", [path.join(templates, "install-mac.sh"), name, src]],
|
|
336
|
+
};
|
|
337
|
+
const runner = runners[process.platform];
|
|
338
|
+
if (!runner) {
|
|
339
|
+
throw new Error(`no installer for ${process.platform} - Ableton Live runs on macOS and Windows only`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
execFileSync(runner[0], runner[1], { stdio: "inherit", cwd: root });
|
|
325
343
|
}
|
package/src/init.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* init.mjs - `m4l-jweb init [dir] [--name <name>]`: scaffold a new device repo.
|
|
3
3
|
*
|
|
4
|
-
* The template lives in templates/starter/ and
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* The template lives in templates/starter/ and mirrors this repo's own layout:
|
|
5
|
+
* the same scripts/, vite.config.ts, tsconfig and src/app/shared/, with one
|
|
6
|
+
* device instead of three.
|
|
7
|
+
*
|
|
8
|
+
* Those shared files are checked for drift by tests/starter.test.mjs, which
|
|
9
|
+
* compares them byte-for-byte against the root repo's. The template used to be
|
|
10
|
+
* kept in sync by hand and by good intentions, and it fell behind.
|
|
11
|
+
*
|
|
12
|
+
* `{{name}}` is substituted in file CONTENTS and in PATHS - the device's app
|
|
13
|
+
* lives at src/app/{{name}}/, so the folder is named after the device too.
|
|
11
14
|
*/
|
|
12
15
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
16
|
import path from "node:path";
|
|
@@ -17,36 +20,38 @@ const pkgDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
17
20
|
const starter = path.join(pkgDir, "templates", "starter");
|
|
18
21
|
|
|
19
22
|
function walk(dir) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
25
|
+
const p = path.join(dir, entry.name);
|
|
26
|
+
if (entry.isDirectory()) out.push(...walk(p));
|
|
27
|
+
else out.push(p);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export async function initProject(cwd, args = []) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
34
|
+
const nameFlagIndex = args.indexOf("--name");
|
|
35
|
+
const target = positional[0] ? path.resolve(cwd, positional[0]) : cwd;
|
|
36
|
+
const name = nameFlagIndex >= 0 ? args[nameFlagIndex + 1] : path.basename(target);
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
if (existsSync(target) && readdirSync(target).length > 0) {
|
|
39
|
+
throw new Error(`${target} is not empty - init needs an empty (or new) directory`);
|
|
40
|
+
}
|
|
41
|
+
mkdirSync(target, { recursive: true });
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
const files = walk(starter);
|
|
44
|
+
for (const src of files) {
|
|
45
|
+
// The device's app folder is named after the device, so {{name}} has to be
|
|
46
|
+
// substituted in the PATH as well as in the contents.
|
|
47
|
+
const rel = path.relative(starter, src).replaceAll("{{name}}", name);
|
|
48
|
+
const dest = path.join(target, rel);
|
|
49
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
50
|
+
const contents = readFileSync(src, "utf8").replaceAll("{{name}}", name);
|
|
51
|
+
writeFileSync(dest, contents);
|
|
52
|
+
}
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
console.log(`m4l-jweb: scaffolded "${name}" at ${target} (${files.length} files)`);
|
|
55
|
+
console.log(`m4l-jweb: next steps:\n cd ${path.relative(cwd, target) || "."}\n pnpm install\n pnpm dev`);
|
|
56
|
+
return target;
|
|
52
57
|
}
|