refactor: creators

This commit is contained in:
cha0s 2024-02-09 01:11:27 -06:00
parent 73eb334e05
commit f702e8b7f0
28 changed files with 2601 additions and 1713 deletions

8
build/concurrent.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = async function concurrent(inputs, task, jobs = require('os').cpus().length) {
const workers = new Array(jobs).fill(Promise.resolve(0));
inputs.forEach((input, i) => {
// then= :)
workers[i % jobs] = workers[i % jobs].then(async (code) => await task(input) || code);
});
return (await Promise.all(workers)).find((code) => code !== 0) || 0;
};

View File

@ -1,43 +1,150 @@
const {Buffer} = require('buffer');
const {exec} = require('child_process'); const {exec} = require('child_process');
const {createHash} = require('crypto');
const {createReadStream} = require('fs');
const {cp, mkdir, writeFile} = require('fs/promises');
const {join} = require('path'); const {join} = require('path');
const {processCode, spawnWith} = require('@flecks/core/src/server'); const {
processCode,
spawnWith,
} = require('@flecks/core/src/server');
const Arborist = require('@npmcli/arborist');
const {glob} = require('glob'); const {glob} = require('glob');
const concurrent = require('./concurrent');
const { const {
FLECKS_CORE_ROOT = process.cwd(), FLECKS_CORE_ROOT = process.cwd(),
} = process.env; } = process.env;
const args = ['npm', 'publish', '--provenance']; const args = ['npm', 'publish', ...process.argv.slice(2)];
const bumpedVersions = {};
const creators = ['create-app', 'create-fleck'];
const localVersions = {};
const packCache = join(FLECKS_CORE_ROOT, 'node_modules', '.cache', '@flecks', 'publish');
const {workspaces} = require(join(FLECKS_CORE_ROOT, 'package.json')); const {workspaces} = require(join(FLECKS_CORE_ROOT, 'package.json'));
(async () => { const run = (cmd) => (
const paths = (await Promise.all(workspaces.map((path) => glob(join(FLECKS_CORE_ROOT, path))))) new Promise((resolve) => {
.flat(); exec(cmd, (error, stdout) => {
const cpus = new Array(require('os').cpus().length).fill(Promise.resolve(0));
paths.forEach((cwd, i) => {
// then= :)
cpus[i % cpus.length] = cpus[i % cpus.length]
.then(async (code) => {
const {name, version} = require(join(cwd, 'package.json'));
const [localVersion, remoteVersion] = await Promise.all([
version,
new Promise((resolve, reject) => {
exec(`npm view ${name} version`, (error, stdout) => {
if (error) { if (error) {
reject(error) resolve(undefined)
return; return;
} }
resolve(stdout.trim()); resolve(stdout.trim());
}); });
}), })
]); );
if (localVersion === remoteVersion) {
return code; // Get integrity sums for creator dependencies.
const packPkg = async (pkg) => {
await processCode(spawnWith(
['npm', 'pack', '--pack-destination', packCache],
{cwd: join(FLECKS_CORE_ROOT, 'packages', pkg, 'dist', 'fleck'), stdio: 'ignore'},
));
};
const bumpDependencies = (dependencies) => (
Object.fromEntries(
Object.entries(dependencies)
.map(([pkg, version]) => ([pkg, localVersions[pkg] || version])),
)
);
const integrityForPkg = async (pkg) => {
const pack = join(packCache, `flecks-${pkg.split('/')[1]}-${localVersions[pkg]}.tgz`);
const buffers = [];
// eslint-disable-next-line no-restricted-syntax
for await (const data of createReadStream(pack).pipe(createHash('sha512'))) {
buffers.push(data);
} }
const publishCode = await processCode(spawnWith(args, {cwd: join(cwd, 'dist', 'fleck')})); return `sha512-${Buffer.concat(buffers).toString('base64')}`;
return publishCode || code; };
const shrinkwrap = async (path) => {
const arb = new Arborist({
path,
registry: await run('npm config get registry'),
}); });
}); const {meta} = await arb.buildIdealTree({saveType: 'prod'});
process.exitCode = (await Promise.all(cpus)).find((code) => code !== 0) || 0; const shrinkwrap = await meta.commit();
shrinkwrap.packages = Object.fromEntries(
await Promise.all(
Object.entries(shrinkwrap.packages)
.map(async ([pkg, config]) => {
if (pkg.match(/node_modules\/@flecks\/[^/]+$/)) {
if (config.dependencies) {
config.dependencies = bumpDependencies(config.dependencies);
}
if (config.devDependencies) {
config.devDependencies = bumpDependencies(config.devDependencies);
}
const subpkg = pkg.split('/').slice(1).join('/');
config.version = localVersions[subpkg];
config.integrity = await integrityForPkg(subpkg);
}
return [pkg, config];
}),
),
);
return shrinkwrap;
};
const shrinkwrapsAndPublish = async (creator) => {
const dist = join(FLECKS_CORE_ROOT, 'packages', creator, 'dist', 'fleck');
const fakePackage = join(packCache, creator);
await mkdir(fakePackage, {recursive: true});
// Get a shrinkwrap from template package.json and insert it as a package-lock.json.
await cp(join(dist, 'template', 'package.json.noconflict'), join(fakePackage, 'package.json'));
await writeFile(
join(dist, 'template', 'package-lock.json.noconflict'),
JSON.stringify(await shrinkwrap(fakePackage), null, 2),
);
// Get a shrinkwrap from the built creator and insert it as shrinkwrap.
await writeFile(
join(dist, 'npm-shrinkwrap.json'),
JSON.stringify(await shrinkwrap(dist), null, 2),
);
// Publish.
await processCode(spawnWith(args, {cwd: dist}));
};
(async () => {
await concurrent(
(await Promise.all(workspaces.map((path) => glob(join(FLECKS_CORE_ROOT, path))))).flat(),
async (cwd) => {
const {name, version} = require(join(cwd, 'package.json'));
const [localVersion, remoteVersion] = await Promise.all([
version,
run(`npm view ${name} version`),
]);
localVersions[name] = version;
if (localVersion === remoteVersion) {
return undefined;
}
bumpedVersions[name] = version;
// Skip creators for now.
if (creators.some((creator) => name.endsWith(creator))) {
return undefined;
}
return processCode(spawnWith(args, {cwd: join(cwd, 'dist', 'fleck')}));
},
);
// No creators? Bail.
if (!bumpedVersions['@flecks/create-app'] && !bumpedVersions['@flecks/create-app']) {
return;
}
// Pack dependencies.
await mkdir(packCache, {recursive: true});
const dependencies = ['build', 'core', 'fleck', 'server'];
await Promise.all(dependencies.map(packPkg));
if (bumpedVersions['@flecks/create-fleck']) {
await shrinkwrapsAndPublish('create-fleck');
}
if (bumpedVersions['@flecks/create-app']) {
// Needs packed create-fleck for package lock.
await packPkg('create-fleck');
await shrinkwrapsAndPublish('create-app');
}
})(); })();

View File

@ -3,21 +3,18 @@ const {join} = require('path');
const {processCode, spawnWith} = require('@flecks/core/src/server'); const {processCode, spawnWith} = require('@flecks/core/src/server');
const {glob} = require('glob'); const {glob} = require('glob');
const concurrent = require('./concurrent');
const { const {
FLECKS_CORE_ROOT = process.cwd(), FLECKS_CORE_ROOT = process.cwd(),
} = process.env; } = process.env;
const args = ['npm', 'run', ...process.argv.slice(2)]; const args = process.argv.slice(2);
const {workspaces} = require(join(FLECKS_CORE_ROOT, 'package.json')); const {workspaces} = require(join(FLECKS_CORE_ROOT, 'package.json'));
(async () => { (async () => {
const paths = (await Promise.all(workspaces.map((path) => glob(join(FLECKS_CORE_ROOT, path))))) process.exitCode = await concurrent(
.flat(); (await Promise.all(workspaces.map((path) => glob(join(FLECKS_CORE_ROOT, path))))).flat(),
const cpus = new Array(require('os').cpus().length).fill(Promise.resolve(0)); (cwd) => processCode(spawnWith(args, {cwd})),
paths.forEach((cwd, i) => { );
// then= :)
cpus[i % cpus.length] = cpus[i % cpus.length]
.then(async (code) => (await processCode(spawnWith(args, {cwd}))) || code);
});
process.exitCode = (await Promise.all(cpus)).find((code) => code !== 0) || 0;
})(); })();

3686
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,17 +7,20 @@
"url": "git+https://github.com/cha0s/flecks.git" "url": "git+https://github.com/cha0s/flecks.git"
}, },
"scripts": { "scripts": {
"build": "node build/tasks build", "build": "node build/tasks npm run build",
"ci": "act -j verify", "ci": "act -j verify",
"dox:bump": "npm run dox:gh-pages && cd dox && git add . && git commit -m $(git -C .. rev-parse HEAD) && git push origin gh-pages", "dox:bump": "npm run dox:gh-pages && cd dox && git add . && git commit -m $(git -C .. rev-parse HEAD) && git push origin gh-pages",
"dox:gh-pages": "flecks dox docusaurus && cd website && DOCUSAURUS_GENERATED_FILES_DIR_NAME=node_modules/.cache/docusaurus node_modules/.bin/docusaurus build --out-dir ../dox-tmp && cd .. && rm -rf dox/* && mv dox-tmp/* dox && rmdir dox-tmp", "dox:gh-pages": "flecks dox docusaurus && cd website && DOCUSAURUS_GENERATED_FILES_DIR_NAME=node_modules/.cache/docusaurus node_modules/.bin/docusaurus build --out-dir ../dox-tmp && cd .. && rm -rf dox/* && mv dox-tmp/* dox && rmdir dox-tmp",
"dox:serve": "flecks dox docusaurus && cd website && DOCUSAURUS_GENERATED_FILES_DIR_NAME=node_modules/.cache/docusaurus node_modules/.bin/docusaurus build --no-minify --out-dir ../dox-tmp && node_modules/.bin/docusaurus serve --dir ../dox-tmp", "dox:serve": "flecks dox docusaurus && cd website && DOCUSAURUS_GENERATED_FILES_DIR_NAME=node_modules/.cache/docusaurus node_modules/.bin/docusaurus build --no-minify --out-dir ../dox-tmp && node_modules/.bin/docusaurus serve --dir ../dox-tmp",
"dox": "flecks dox docusaurus && cd website && DOCUSAURUS_GENERATED_FILES_DIR_NAME=node_modules/.cache/docusaurus node_modules/.bin/docusaurus", "dox": "flecks dox docusaurus && cd website && DOCUSAURUS_GENERATED_FILES_DIR_NAME=node_modules/.cache/docusaurus node_modules/.bin/docusaurus",
"lint": "node build/tasks lint", "lint": "node build/tasks npm run lint",
"publish": "node build/publish", "publish": "node build/publish --provenance",
"test": "node build/tasks -- test -t 300000" "test": "node build/tasks npm run -- test -t 300000"
}, },
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
] ],
"devDependencies": {
"@npmcli/arborist": "^7.3.1"
}
} }

View File

@ -19,21 +19,20 @@ const {
isStringLiteral, isStringLiteral,
stringLiteral, stringLiteral,
} = require('@babel/types'); } = require('@babel/types');
const addPathsToYml = require('@flecks/core/build/add-paths-to-yml');
const D = require('@flecks/core/build/debug'); const D = require('@flecks/core/build/debug');
const { const {
add, add,
binaryPath, binaryPath,
loadYml,
lockFile, lockFile,
spawnWith, spawnWith,
} = require('@flecks/core/src/server'); } = require('@flecks/core/src/server');
const chokidar = require('chokidar'); const chokidar = require('chokidar');
const {glob} = require('glob'); const {glob} = require('glob');
const {load: loadYml} = require('js-yaml');
const {paperwork} = require('precinct'); const {paperwork} = require('precinct');
const {rimraf} = require('rimraf'); const {rimraf} = require('rimraf');
const addPathsToYml = require('./add-paths-to-yml');
const { const {
FLECKS_CORE_ROOT = process.cwd(), FLECKS_CORE_ROOT = process.cwd(),
} = process.env; } = process.env;
@ -236,6 +235,7 @@ exports.commands = (program, flecks) => {
...(target ? {FLECKS_CORE_BUILD_LIST: target} : {}), ...(target ? {FLECKS_CORE_BUILD_LIST: target} : {}),
...(hot ? {FLECKS_ENV__flecks_server__hot: 'true'} : {}), ...(hot ? {FLECKS_ENV__flecks_server__hot: 'true'} : {}),
}, },
// @todo This kills the pnpm. Let's use a real IPC channel.
useFork: true, useFork: true,
...rest, ...rest,
}; };

View File

@ -1,17 +1,18 @@
const {readFile} = require('fs/promises'); const {readFile} = require('fs/promises');
const {join} = require('path'); const {join} = require('path');
const {loadYml} = require('@flecks/core/src/server');
const D = require('@flecks/core/build/debug'); const D = require('@flecks/core/build/debug');
const debug = D('@flecks/build/build/load-config'); const debug = D('@flecks/build/build/load-config');
module.exports = async function loadConfig(root) { module.exports = async function loadConfig(root) {
try { try {
const {load} = require('js-yaml');
const filename = join(root, 'build', 'flecks.yml'); const filename = join(root, 'build', 'flecks.yml');
const buffer = await readFile(filename, 'utf8'); const buffer = await readFile(filename, 'utf8');
debug('parsing configuration from YML...'); debug('parsing configuration from YML...');
return ['YML', load(buffer, {filename})]; return ['YML', loadYml(buffer, {filename})];
} }
catch (error) { catch (error) {
if ('ENOENT' !== error.code) { if ('ENOENT' !== error.code) {

View File

@ -55,7 +55,6 @@
"glob": "^10.3.10", "glob": "^10.3.10",
"globals": "^13.23.0", "globals": "^13.23.0",
"graceful-fs": "^4.2.11", "graceful-fs": "^4.2.11",
"js-yaml": "4.1.0",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"precinct": "^11.0.5", "precinct": "^11.0.5",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",

View File

@ -1,10 +1,6 @@
/* eslint-disable global-require */ /* eslint-disable global-require */
const {dump: dumpYml, load: loadYml} = require('js-yaml');
module.exports = { module.exports = {
dumpYml,
loadYml,
rimraf: require('rimraf').rimraf, rimraf: require('rimraf').rimraf,
webpack: require('webpack'), webpack: require('webpack'),
...require('../build/webpack'), ...require('../build/webpack'),

View File

@ -1,7 +1,7 @@
import addPathsToYml from '@flecks/build/build/add-paths-to-yml'; import addPathsToYml from '@flecks/core/build/add-paths-to-yml';
import {loadYml} from '@flecks/core/server';
import {expect} from 'chai'; import {expect} from 'chai';
import {readFile, writeFile} from 'fs/promises'; import {readFile, writeFile} from 'fs/promises';
import {load as loadYml} from 'js-yaml';
it('can add paths to YML', async () => { it('can add paths to YML', async () => {
await writeFile( await writeFile(

View File

@ -9,7 +9,7 @@ const {
module.exports = async (paths, root) => { module.exports = async (paths, root) => {
const ymlPath = join(root || FLECKS_CORE_ROOT, 'build', 'flecks.yml'); const ymlPath = join(root || FLECKS_CORE_ROOT, 'build', 'flecks.yml');
let yml = loadYml(await readFile(ymlPath)); let yml = loadYml(await readFile(ymlPath)) || {};
yml = Object.fromEntries(Object.entries(yml).concat(paths.map((path) => [path, {}]))); yml = Object.fromEntries(Object.entries(yml).concat(paths.map((path) => [path, {}])));
await writeFile(ymlPath, dumpYml(yml, {sortKeys: true})); await writeFile(ymlPath, dumpYml(yml, {sortKeys: true}));
}; };

View File

@ -0,0 +1,16 @@
const {basename, dirname, join} = require('path');
const {JsonStream} = require('./stream');
const FileTree = require('./tree');
exports.move = async (name, source) => {
const fileTree = await FileTree.loadFrom(source);
// Renamed to avoid conflicts.
fileTree.glob('**/*.noconflict')
.forEach((path) => {
fileTree.move(path, join(dirname(path), basename(path, '.noconflict')));
});
// Add project name to `package.json`.
fileTree.pipe('package.json', new JsonStream((json) => ({name, ...json})));
return fileTree;
};

View File

@ -1,57 +1,70 @@
// eslint-disable-next-line max-classes-per-file // eslint-disable-next-line max-classes-per-file
const {dump: dumpYml, load: loadYml} = require('js-yaml');
const JsonParse = require('jsonparse'); const JsonParse = require('jsonparse');
const {Transform} = require('stream'); const {Transform} = require('stream');
exports.JsonStream = class JsonStream extends Transform { exports.JsonStream = class JsonStream extends Transform {
constructor() { constructor(decorator) {
super(); super();
const self = this;
this.done = undefined;
this.parser = new JsonParse(); this.parser = new JsonParse();
const self = this;
this.parser.onValue = function onValue(O) { this.parser.onValue = function onValue(O) {
if (0 === this.stack.length) { if (0 === this.stack.length) {
self.push(JSON.stringify(O)); self.transformed = JSON.stringify(decorator(O));
self.done();
} }
}; };
this.transformed = undefined;
}
// eslint-disable-next-line no-underscore-dangle
_flush(done) {
this.push(this.transformed);
done();
} }
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
_transform(chunk, encoding, done) { _transform(chunk, encoding, done) {
this.done = done;
this.parser.write(chunk); this.parser.write(chunk);
}
};
exports.JsonStream.PrettyPrint = class extends Transform {
constructor(indent = 2) {
super();
this.indent = indent;
}
// eslint-disable-next-line no-underscore-dangle
async _transform(chunk, encoding, done) {
this.push(JSON.stringify(JSON.parse(chunk), null, this.indent));
done(); done();
} }
}; };
exports.transform = (fn, opts = {}) => { exports.JsonStream.PrettyPrint = class extends exports.JsonStream {
class EasyTransform extends Transform {
constructor() { constructor(decorator, indent = 2) {
super(opts); super(decorator);
const self = this;
this.parser.onValue = function onValue(O) {
if (0 === this.stack.length) {
self.transformed = JSON.stringify(O, null, indent);
}
};
}
};
exports.YamlStream = class YamlStream extends Transform {
constructor(decorator, options = {dump: {}, load: {}}) {
super();
this.buffers = [];
this.decorator = decorator;
this.options = options;
}
// eslint-disable-next-line no-underscore-dangle
_flush(done) {
const yml = loadYml(Buffer.concat(this.buffers).toString(), this.options.load);
this.push(dumpYml(this.decorator(yml), this.options.dump));
done();
}
// eslint-disable-next-line no-underscore-dangle
_transform(chunk, encoding, done) {
this.buffers.push(chunk);
done();
} }
// eslint-disable-next-line no-underscore-dangle, class-methods-use-this
_transform(chunk, encoding, done) {
fn(chunk, encoding, done, this);
}
}
return new EasyTransform();
}; };

View File

@ -1,10 +1,12 @@
const {createReadStream, createWriteStream} = require('fs'); const {createReadStream, createWriteStream} = require('fs');
const {mkdir, stat} = require('fs/promises'); const {mkdir, stat} = require('fs/promises');
const {glob, JsonStream} = require('@flecks/core/server'); const {glob} = require('glob');
const minimatch = require('minimatch'); const minimatch = require('minimatch');
const {dirname, join} = require('path'); const {dirname, join} = require('path');
const {JsonStream} = require('./stream');
module.exports = class FileTree { module.exports = class FileTree {
constructor(files = {}) { constructor(files = {}) {
@ -19,6 +21,12 @@ module.exports = class FileTree {
this.files[path] = stream; this.files[path] = stream;
} }
delete(path) {
if (this.files[path]) {
delete this.files[path];
}
}
glob(glob) { glob(glob) {
return Object.keys(this.files).filter((path) => minimatch(path, glob, {dot: true})); return Object.keys(this.files).filter((path) => minimatch(path, glob, {dot: true}));
} }
@ -41,6 +49,13 @@ module.exports = class FileTree {
); );
} }
move(from, to) {
if (this.files[from]) {
this.files[to] = this.files[from];
this.delete(from);
}
}
pipe(path, stream) { pipe(path, stream) {
this.files[path] = this.files[path] ? this.files[path].pipe(stream) : undefined; this.files[path] = this.files[path] ? this.files[path].pipe(stream) : undefined;
} }

View File

@ -26,8 +26,10 @@
"dependencies": { "dependencies": {
"debug": "4.3.1", "debug": "4.3.1",
"glob": "^10.3.10", "glob": "^10.3.10",
"js-yaml": "4.1.0",
"jsonparse": "^1.3.1", "jsonparse": "^1.3.1",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"minimatch": "^5.0.1",
"set-value": "^4.1.0", "set-value": "^4.1.0",
"source-map-support": "0.5.19", "source-map-support": "0.5.19",
"supports-color": "9.2.1" "supports-color": "9.2.1"

View File

@ -1,7 +1,11 @@
/* eslint-disable global-require */ /* eslint-disable global-require */
const {dump: dumpYml, load: loadYml} = require('js-yaml');
module.exports = { module.exports = {
dumpYml,
glob: require('glob').glob, glob: require('glob').glob,
loadYml,
...require('../../build/stream'), ...require('../../build/stream'),
...require('./package-manager'), ...require('./package-manager'),
...require('./process'), ...require('./process'),

View File

@ -1,19 +1,17 @@
#!/usr/bin/env node #!/usr/bin/env node
const {stat} = require('fs/promises');
const {join} = require('path'); const {join} = require('path');
const {move} = require('@flecks/core/build/move');
const { const {
build, build,
install, install,
transform, YamlStream,
} = require('@flecks/core/server'); } = require('@flecks/core/src/server');
const {program} = require('commander'); const {program} = require('commander');
const {dump: dumpYml, load: loadYml} = require('js-yaml');
const validate = require('validate-npm-package-name'); const validate = require('validate-npm-package-name');
// const build = require('./build');
const {move, testDestination} = require('./move');
const { const {
FLECKS_CORE_ROOT = process.cwd(), FLECKS_CORE_ROOT = process.cwd(),
} = process.env; } = process.env;
@ -32,22 +30,23 @@ const {
} }
const destination = join(FLECKS_CORE_ROOT, app); const destination = join(FLECKS_CORE_ROOT, app);
const name = app.startsWith('@') ? app : `@${app}/monorepo`; const name = app.startsWith('@') ? app : `@${app}/monorepo`;
if (!await testDestination(destination)) { try {
await stat(destination);
const error = new Error( const error = new Error(
`@flecks/create-app: destination '${destination} already exists: aborting`, `@flecks/create-app: destination '${destination} already exists: aborting`,
); );
error.code = 129; error.code = 129;
throw error; throw error;
} }
// eslint-disable-next-line no-empty
catch (error) {}
const fileTree = await move(name, join(__dirname, '..', 'template')); const fileTree = await move(name, join(__dirname, '..', 'template'));
fileTree.pipe( fileTree.pipe(
'build/flecks.yml', 'build/flecks.yml',
transform((chunk, encoding, done, stream) => { new YamlStream(
const yml = loadYml(chunk); (yml) => ({...yml, '@flecks/core': {id: app}}),
yml['@flecks/core'].id = app; {dump: {forceQuotes: true, sortKeys: true}},
stream.push(dumpYml(yml, {forceQuotes: true, sortKeys: true})); ),
done();
}),
); );
// Write the tree. // Write the tree.
await fileTree.writeTo(destination); await fileTree.writeTo(destination);

View File

@ -1,39 +0,0 @@
const {stat} = require('fs/promises');
const {basename, dirname, join} = require('path');
const {transform} = require('@flecks/core/server');
const FileTree = require('./tree');
exports.testDestination = async (destination) => {
try {
await stat(destination);
return false;
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
return true;
}
};
exports.move = async (name, source) => {
const fileTree = await FileTree.loadFrom(source);
// Renamed to avoid conflicts.
const {files} = fileTree;
fileTree.glob('**/*.noconflict')
.forEach((path) => {
files[join(dirname(path), basename(path, '.noconflict'))] = files[path];
delete files[path];
});
// Add project name to `package.json`.
fileTree.pipe(
'package.json',
transform((chunk, encoding, done, stream) => {
stream.push(JSON.stringify({name, ...JSON.parse(chunk)}));
done();
}),
);
return fileTree;
};

View File

@ -22,14 +22,13 @@
"create-app": "./build/cli.js" "create-app": "./build/cli.js"
}, },
"files": [ "files": [
"npm-shrinkwrap.json",
"server.js", "server.js",
"template" "template"
], ],
"dependencies": { "dependencies": {
"@flecks/core": "^4.0.2", "@flecks/core": "^4.0.2",
"commander": "11.1.0", "commander": "11.1.0",
"js-yaml": "4.1.0",
"minimatch": "^5.0.1",
"validate-npm-package-name": "^3.0.0" "validate-npm-package-name": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1 +1,5 @@
export {default as validate} from 'validate-npm-package-name'; /* eslint-disable global-require */
module.exports = {
validate: require('validate-npm-package-name'),
};

View File

@ -1,3 +1 @@
'@flecks/build': {}
'@flecks/core': {}
'@flecks/server': {} '@flecks/server': {}

View File

@ -11,12 +11,11 @@
"start": "DEBUG=@flecks/*,-*:silly npm run dev" "start": "DEBUG=@flecks/*,-*:silly npm run dev"
}, },
"dependencies": { "dependencies": {
"@flecks/core": "^3.0.0", "@flecks/server": "^4.0.0"
"@flecks/server": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@flecks/build": "^3.0.0", "@flecks/build": "^4.0.0",
"@flecks/create-fleck": "^3.0.0", "@flecks/create-fleck": "^4.0.0",
"patch-package": "^8.0.0" "patch-package": "^8.0.0"
} }
} }

View File

@ -1,115 +1,97 @@
#!/usr/bin/env node #!/usr/bin/env node
/* eslint-disable camelcase */
const {stat} = require('fs/promises'); const {
const {join} = require('path'); basename,
dirname,
join,
relative,
sep,
} = require('path');
const addPathsToYml = require('@flecks/build/build/add-paths-to-yml'); const addPathsToYml = require('@flecks/core/build/add-paths-to-yml');
const {program} = require('commander'); const {program} = require('commander');
const {move} = require('@flecks/core/build/move');
const { const {
build, build,
install, install,
transform, JsonStream,
} = require('@flecks/core/server'); } = require('@flecks/core/src/server');
const {move, testDestination} = require('@flecks/create-app/build/move');
const {validate} = require('@flecks/create-app/src/server');
const { const {
FLECKS_CORE_ROOT = process.cwd(), FLECKS_CORE_ROOT = process.cwd(),
} = process.env; } = process.env;
const checkIsMonorepo = async () => { const {
try { npm_config_local_prefix,
await stat(join(FLECKS_CORE_ROOT, 'packages')); npm_config_scope,
return true; npm_package_json,
} } = process.env;
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
return false;
}
};
const monorepoScope = async () => {
try {
const {name} = require(join(FLECKS_CORE_ROOT, 'package.json'));
const [scope] = name.split('/');
return scope;
}
catch (error) {
if ('MODULE_NOT_FOUND' !== error.code) {
throw error;
}
return undefined;
}
};
const target = async (fleck) => {
const {errors} = validate(fleck);
if (errors) {
throw new Error(`@flecks/create-fleck: invalid fleck name: ${errors.join(', ')}`);
}
const parts = fleck.split('/');
let pkg;
let scope;
if (1 === parts.length) {
pkg = fleck;
if (await checkIsMonorepo()) {
scope = await monorepoScope();
}
return [scope, pkg];
}
return parts;
};
(async () => { (async () => {
program.argument('<fleck>', 'name of the fleck to create'); program.argument('[path]', "the path of the fleck (e.g.: 'packages/foobar')");
program.option('--no-add', 'do not add an entry to `build/flecks.yml`'); program.option('--no-add', 'do not add an entry to `build/flecks.yml`');
program.option('--no-alias', 'do not alias the fleck in `build/flecks.yml`'); program.option('--no-inherit-version', 'do not inherit root package version');
program.addOption( program.option('--alias', 'alias the fleck in `build/flecks.yml`');
program.createOption('-pm,--package-manager <binary>', 'package manager binary') program.option('--dry-run', 'just say what would be done without actually doing it');
.choices(['npm', 'bun', 'pnpm', 'yarn']), program.action(async (
); path,
program.action(async (fleck, {alias, add, packageManager}) => { {
alias,
add,
inheritVersion,
packageManager,
},
) => {
try { try {
const isMonorepo = await checkIsMonorepo(); if (!npm_config_local_prefix && !path) {
const [scope, pkg] = await target(fleck); throw new Error('name required');
const name = [scope, pkg].filter((e) => !!e).join('/');
const destination = join(
join(...[FLECKS_CORE_ROOT].concat(isMonorepo ? ['packages'] : [])),
pkg,
);
if (!await testDestination(destination)) {
const error = new Error(
`@flecks/create-fleck: destination '${destination} already exists: aborting`,
);
error.code = 129;
throw error;
} }
const root = npm_config_local_prefix || FLECKS_CORE_ROOT;
let rootJson;
try {
rootJson = require(join(root, 'package.json'));
}
// eslint-disable-next-line no-empty
catch (error) {}
let scope;
if (npm_config_scope) {
scope = npm_config_scope;
}
else if (rootJson?.name) {
const inferredScope = rootJson.name.split('/')[0] || '';
if (inferredScope.startsWith('@')) {
scope = inferredScope;
}
}
const local = basename(path || root);
const name = scope ? `${scope}/${local}` : local;
const fileTree = await move(name, join(__dirname, '..', 'template')); const fileTree = await move(name, join(__dirname, '..', 'template'));
if (isMonorepo) { if (inheritVersion && rootJson?.version) {
const {version} = require(join(FLECKS_CORE_ROOT, 'package.json'));
// Inherit version from monorepo root. // Inherit version from monorepo root.
fileTree.pipe( const {version} = rootJson;
'package.json', fileTree.pipe('package.json', new JsonStream((json) => ({...json, version})));
transform((chunk, encoding, done, stream) => {
stream.push(JSON.stringify({...JSON.parse(chunk), version}));
done();
}),
);
} }
// Write the tree. // Write the tree.
const destination = path ? join(root, path) : dirname(npm_package_json);
await fileTree.writeTo(destination); await fileTree.writeTo(destination);
// Install and build. // Install and build.
await install({cwd: destination, packageManager}); await install({cwd: destination, packageManager});
await build({cwd: destination, packageManager}); await build({cwd: destination, packageManager});
if (isMonorepo && add) { if (add) {
await addPathsToYml([[name].concat(alias ? `./packages/${pkg}` : []).join(':')]); const maybeAliasedPath = [name]
.concat(alias ? `.${sep}${relative(root, destination)}` : [])
.join(':');
try {
await addPathsToYml([maybeAliasedPath], root);
}
// eslint-disable-next-line no-empty
catch (error) {}
} }
} }
catch (error) { catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('Creation failed:', error); console.error('creation failed:', error);
} }
}); });
await program.parseAsync(process.argv); await program.parseAsync(process.argv);

View File

@ -22,15 +22,15 @@
"create-fleck": "./build/cli.js" "create-fleck": "./build/cli.js"
}, },
"files": [ "files": [
"npm-shrinkwrap.json",
"template" "template"
], ],
"dependencies": { "dependencies": {
"@flecks/build": "^4.0.2",
"@flecks/core": "^4.0.2", "@flecks/core": "^4.0.2",
"@flecks/create-app": "^4.0.2",
"commander": "11.1.0" "commander": "11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@flecks/build": "^4.0.2",
"@flecks/fleck": "^4.0.2" "@flecks/fleck": "^4.0.2"
} }
} }

View File

@ -11,10 +11,10 @@
"index.js" "index.js"
], ],
"dependencies": { "dependencies": {
"@flecks/core": "^3.0.0" "@flecks/core": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@flecks/build": "^3.0.0", "@flecks/build": "^4.0.0",
"@flecks/fleck": "^3.0.0" "@flecks/fleck": "^4.0.0"
} }
} }

View File

@ -1,4 +1,4 @@
const {dumpYml} = require('@flecks/build/src/server'); const {dumpYml} = require('@flecks/core/src/server');
const {generateComposeConfig, generateDockerFile} = require('./generate'); const {generateComposeConfig, generateDockerFile} = require('./generate');

View File

@ -1,5 +1,7 @@
const {banner} = require('@flecks/build/src/server'); const {banner} = require('@flecks/build/src/server');
exports.dependencies = ['@flecks/build'];
exports.hooks = { exports.hooks = {
'@flecks/build.config.alter': ({server}, env, argv, flecks) => { '@flecks/build.config.alter': ({server}, env, argv, flecks) => {
if (server) { if (server) {

View File

@ -1,3 +1,4 @@
import {Buffer} from 'buffer';
import {Transform} from 'stream'; import {Transform} from 'stream';
const { const {
@ -14,26 +15,42 @@ class InlineConfig extends Transform {
constructor(flecks, req) { constructor(flecks, req) {
super(); super();
this.buffers = [];
this.flecks = flecks; this.flecks = flecks;
this.req = req; this.req = req;
} }
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
async _transform(chunk, encoding, done) { async _flush(done) {
const string = chunk.toString('utf8'); const string = Buffer.concat(this.buffers).toString();
const {appMountId} = this.flecks.get('@flecks/web'); const {appMountId} = this.flecks.get('@flecks/web');
const rendered = string.replaceAll( const hideAttributes = 'production' === NODE_ENV ? 'data-flecks="ignore"' : '';
this.push(
string.replaceAll(
'<body>', '<body>',
/* eslint-disable indent */
[ [
'<body>', '<body>',
`<div id="${appMountId}-container">`, `<div id="${appMountId}-container">`,
`<script${'production' === NODE_ENV ? 'data-flecks="ignore"' : ''}>window.document.querySelector('#${appMountId}-container').style.display = 'none'</script>`, `<script${hideAttributes}>`,
`<script data-flecks="ignore">${await configSource(this.flecks, this.req)}</script>`, `window.document.querySelector('#${appMountId}-container').style.display = 'none'`,
'</script>',
'<script data-flecks="ignore">',
await configSource(this.flecks, this.req),
'</script>',
`<div id="${appMountId}"></div>`, `<div id="${appMountId}"></div>`,
'</div>', '</div>',
].join(''), ].join(''),
/* eslint-enable indent */
),
); );
this.push(rendered); this.buffers = [];
done();
}
// eslint-disable-next-line no-underscore-dangle
_transform(chunk, encoding, done) {
this.buffers.push(chunk);
done(); done();
} }